feat: expose form validation methods from EventTypePlatformWrapper (#22655)
* feat: expose form validation methods from EventTypePlatformWrapper - Add validateForm and handleFormSubmit methods to onFormStateChange prop - Update useEventTypeForm hook to return validateForm method - Add proper TypeScript types for form validation result - Enable external form validation and submission capabilities Co-Authored-By: somay@cal.com <somay@cal.com> * fix: correct forwardRef syntax to resolve parsing errors - Fix forwardRef parameter destructuring syntax - Use proper props parameter pattern for forwardRef components - Ensure prettier can parse the file correctly Co-Authored-By: somay@cal.com <somay@cal.com> * feat: add form validation and submission handlers to event types platform wrapper * refactor: move form validation and submit handlers from hook to wrapper component * fix: add ref to the save button for proper form validation * feat: extend ref-based validation API to AvailabilitySettingsPlatformWrapper - Add AvailabilityFormValidationResult and AvailabilitySettingsPlatformWrapperRef types - Implement forwardRef pattern in AvailabilitySettings component with save button ref - Add validateForm and handleFormSubmit methods using useImperativeHandle - Update AvailabilitySettingsPlatformWrapper to support ref forwarding - Add comprehensive documentation for ref-based validation API in both event-type.mdx and availability-settings.mdx - Follow same patterns established in EventTypePlatformWrapper for consistency Co-Authored-By: somay@cal.com <somay@cal.com> * refactor: rename AvailabilitySettingsPlatformWrapperRef to AvailabilitySettingsFormRef * feat: add ref-based validation pattern to availability example page - Mirror the ref-based validation implementation from event-types.tsx - Add useRef, handleValidate, and handleSubmit functions - Demonstrate validateForm() and handleFormSubmit() methods - Add validation and submit buttons to showcase the new API - Use AvailabilitySettingsFormRef type for proper typing Co-Authored-By: somay@cal.com <somay@cal.com> * refactor: remove unused state variables * docs: clarify form validation behavior for availability and event type atoms * chore: remove console.logs --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: somay@cal.com <somay@cal.com> Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
This commit is contained in:
co-authored by
somay@cal.com
Somay Chauhan
parent
16a7bb7bc5
commit
eb4eebe6f9
@@ -133,3 +133,59 @@ Along with the props, Availability settings atom accepts custom styles via the *
|
||||
| overridesModalClassNames | Adds styling to the date overrides modal |
|
||||
|
||||
<Info>Please ensure all custom classnames are valid [Tailwind CSS](https://tailwindcss.com/) classnames.</Info>
|
||||
|
||||
## Programmatic Form Validation and Submission
|
||||
|
||||
The AvailabilitySettings component supports programmatic form validation and submission through a ref-based API. This allows you to validate availability data and submit forms programmatically without user interaction.
|
||||
|
||||
```js
|
||||
import { useRef } from 'react';
|
||||
import { AvailabilitySettings } from "@calcom/atoms";
|
||||
import type { AvailabilitySettingsFormRef } from "@calcom/atoms";
|
||||
|
||||
export function AvailabilityWithValidation() {
|
||||
const availabilityRef = useRef<AvailabilitySettingsFormRef>(null);
|
||||
|
||||
const handleValidate = async () => {
|
||||
const result = await availabilityRef.current?.validateForm();
|
||||
if (result?.isValid) {
|
||||
console.log("Availability form is valid");
|
||||
} else {
|
||||
console.log("Validation errors:", result?.errors);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
availabilityRef.current?.handleFormSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AvailabilitySettings
|
||||
ref={availabilityRef}
|
||||
onUpdateSuccess={() => {
|
||||
console.log("Updated schedule successfully");
|
||||
}}
|
||||
onFormStateChange={(formState) => {
|
||||
console.log("Form state changed:", formState);
|
||||
}}
|
||||
/>
|
||||
<button onClick={handleValidate}>Validate Form</button>
|
||||
<button onClick={handleSubmit}>Submit Form</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Ref Methods
|
||||
|
||||
| Method | Description |
|
||||
|:-------|:------------|
|
||||
| validateForm | Validates the current availability form state and returns a promise with validation results. |
|
||||
| handleFormSubmit | Programmatically submits the availability form, triggering the same validation and submission flow as clicking the save button. Unlike `validateForm`, this method will check required fields and prevent submission unless all required fields are set |
|
||||
|
||||
The `validateForm` method returns an `AvailabilityFormValidationResult` object with:
|
||||
- `isValid`: Boolean indicating if the form passed validation
|
||||
- `errors`: Object containing any validation errors found
|
||||
|
||||
**Note:** If a required field is not filled in, the `validateForm` method will not return any error. The validation focuses on the format and consistency of provided data rather than enforcing required field completion.
|
||||
|
||||
@@ -196,6 +196,60 @@ Along with the props, event type settings atom accepts custom styles via the **c
|
||||
Please ensure all custom classnames are valid [Tailwind CSS](https://tailwindcss.com/) classnames.
|
||||
</Info>
|
||||
|
||||
## Programmatic Form Validation and Submission
|
||||
|
||||
The EventTypeSettings component supports programmatic form validation and submission through a ref-based API. This allows you to validate form data and submit forms programmatically without user interaction.
|
||||
|
||||
```js
|
||||
import { useRef } from 'react';
|
||||
import { EventTypeSettings } from "@calcom/atoms";
|
||||
import type { EventTypePlatformWrapperRef } from "@calcom/atoms";
|
||||
|
||||
export function EventTypeWithValidation(eventTypeId: number) {
|
||||
const eventTypeRef = useRef<EventTypePlatformWrapperRef>(null);
|
||||
|
||||
const handleValidate = async () => {
|
||||
const result = await eventTypeRef.current?.validateForm();
|
||||
if (result?.isValid) {
|
||||
console.log("Form is valid");
|
||||
} else {
|
||||
console.log("Validation errors:", result?.errors);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
eventTypeRef.current?.handleFormSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EventTypeSettings
|
||||
ref={eventTypeRef}
|
||||
id={eventTypeId}
|
||||
onSuccess={(eventType) => {
|
||||
console.log("EventType updated successfully", eventType);
|
||||
}}
|
||||
/>
|
||||
<button onClick={handleValidate}>Validate Form</button>
|
||||
<button onClick={handleSubmit}>Submit Form</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Ref Methods
|
||||
|
||||
| Method | Description |
|
||||
|:-------|:------------|
|
||||
| validateForm | Validates the current event type form state and returns a promise with validation results. |
|
||||
| handleFormSubmit | Programmatically submits the event type form, triggering the same validation and submission flow as clicking the save button. Unlike `validateForm`, this method will check required fields and prevent submission unless all required fields are set |
|
||||
|
||||
The `validateForm` method returns an `EventTypeFormValidationResult` object with:
|
||||
- `isValid`: Boolean indicating if the form passed validation
|
||||
- `errors`: Object containing any validation errors found
|
||||
|
||||
**Note:** If a required field is not filled in, the `validateForm` method will not return any error. The validation focuses on the format and consistency of provided data rather than enforcing required field completion.
|
||||
|
||||
Following are the tabs that are available in the event type settings atom:
|
||||
|
||||
<p></p>
|
||||
|
||||
@@ -64,6 +64,7 @@ export type EventTypeComponentProps = EventTypeSetupProps & {
|
||||
tabName: (typeof tabs)[number];
|
||||
tabsNavigation: VerticalTabItemProps[];
|
||||
allowDelete?: boolean;
|
||||
saveButtonRef?: React.RefObject<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
export const EventType = ({
|
||||
@@ -82,6 +83,7 @@ export const EventType = ({
|
||||
handleSubmit,
|
||||
children,
|
||||
allowDelete = true,
|
||||
saveButtonRef,
|
||||
}: EventTypeComponentProps) => {
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
@@ -101,7 +103,8 @@ export const EventType = ({
|
||||
isDeleting={isDeleting}
|
||||
isPlatform={isPlatform}
|
||||
allowDelete={allowDelete}
|
||||
tabsNavigation={tabsNavigation}>
|
||||
tabsNavigation={tabsNavigation}
|
||||
saveButtonRef={saveButtonRef}>
|
||||
<Form form={formMethods} id="event-type-form" handleSubmit={handleSubmit}>
|
||||
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||
</Form>
|
||||
|
||||
@@ -47,6 +47,7 @@ type Props = {
|
||||
isPlatform?: boolean;
|
||||
tabsNavigation: VerticalTabItemProps[];
|
||||
allowDelete?: boolean;
|
||||
saveButtonRef?: React.RefObject<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
function EventTypeSingleLayout({
|
||||
@@ -64,6 +65,7 @@ function EventTypeSingleLayout({
|
||||
isPlatform,
|
||||
tabsNavigation,
|
||||
allowDelete = true,
|
||||
saveButtonRef,
|
||||
}: Props) {
|
||||
const { t } = useLocale();
|
||||
const eventTypesLockedByOrg = eventType.team?.parent?.organizationSettings?.lockEventTypeCreationForUsers;
|
||||
@@ -262,6 +264,7 @@ function EventTypeSingleLayout({
|
||||
</Dropdown>
|
||||
<div className="border-default border-l-2" />
|
||||
<Button
|
||||
ref={saveButtonRef}
|
||||
className="ml-4 lg:ml-0"
|
||||
type="submit"
|
||||
loading={isUpdateMutationLoading}
|
||||
|
||||
@@ -220,3 +220,13 @@ export type SelectClassNames = {
|
||||
label?: string;
|
||||
container?: string;
|
||||
};
|
||||
|
||||
export type FormValidationResult = {
|
||||
isValid: boolean;
|
||||
errors: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export interface EventTypePlatformWrapperRef {
|
||||
validateForm: () => Promise<FormValidationResult>;
|
||||
handleFormSubmit: () => void;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import type { SetStateAction, Dispatch } from "react";
|
||||
import React, { useMemo, useState, useEffect } from "react";
|
||||
import React, {
|
||||
useMemo,
|
||||
useState,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { Controller, useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
@@ -38,7 +46,7 @@ import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import { Shell as PlatformShell } from "../src/components/ui/shell";
|
||||
import { cn } from "../src/lib/utils";
|
||||
import { Timezone as PlatformTimzoneSelect } from "../timezone/index";
|
||||
import type { AvailabilityFormValues, scheduleClassNames } from "./types";
|
||||
import type { AvailabilityFormValues, scheduleClassNames, AvailabilitySettingsFormRef } from "./types";
|
||||
|
||||
export type Schedule = {
|
||||
id: number;
|
||||
@@ -257,406 +265,447 @@ const SmallScreenSideBar = ({ open, children }: { open: boolean; children: JSX.E
|
||||
);
|
||||
};
|
||||
|
||||
export function AvailabilitySettings({
|
||||
schedule,
|
||||
travelSchedules,
|
||||
handleDelete,
|
||||
isDeleting,
|
||||
isLoading,
|
||||
isSaving,
|
||||
timeFormat,
|
||||
weekStart,
|
||||
backPath,
|
||||
handleSubmit,
|
||||
isPlatform = false,
|
||||
customClassNames,
|
||||
disableEditableHeading = false,
|
||||
enableOverrides = false,
|
||||
onFormStateChange,
|
||||
bulkUpdateModalProps,
|
||||
allowSetToDefault = true,
|
||||
allowDelete = true,
|
||||
}: AvailabilitySettingsProps) {
|
||||
const [openSidebar, setOpenSidebar] = useState(false);
|
||||
const { t, i18n } = useLocale();
|
||||
export const AvailabilitySettings = forwardRef<AvailabilitySettingsFormRef, AvailabilitySettingsProps>(
|
||||
function AvailabilitySettings(props, ref) {
|
||||
const {
|
||||
schedule,
|
||||
travelSchedules,
|
||||
handleDelete,
|
||||
isDeleting,
|
||||
isLoading,
|
||||
isSaving,
|
||||
timeFormat,
|
||||
weekStart,
|
||||
backPath,
|
||||
handleSubmit,
|
||||
isPlatform = false,
|
||||
customClassNames,
|
||||
disableEditableHeading = false,
|
||||
enableOverrides = false,
|
||||
onFormStateChange,
|
||||
bulkUpdateModalProps,
|
||||
allowSetToDefault = true,
|
||||
allowDelete = true,
|
||||
} = props;
|
||||
const [openSidebar, setOpenSidebar] = useState(false);
|
||||
const { t, i18n } = useLocale();
|
||||
|
||||
const form = useForm<AvailabilityFormValues>({
|
||||
defaultValues: {
|
||||
...schedule,
|
||||
schedule: schedule.availability || [],
|
||||
},
|
||||
});
|
||||
const form = useForm<AvailabilityFormValues>({
|
||||
defaultValues: {
|
||||
...schedule,
|
||||
schedule: schedule.availability || [],
|
||||
},
|
||||
});
|
||||
|
||||
const watchedValues = useWatch({
|
||||
control: form.control,
|
||||
});
|
||||
const watchedValues = useWatch({
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
// Trigger callback whenever the form state changes
|
||||
useEffect(() => {
|
||||
if (onFormStateChange && watchedValues) {
|
||||
onFormStateChange(watchedValues as AvailabilityFormValues);
|
||||
}
|
||||
}, [watchedValues, onFormStateChange]);
|
||||
|
||||
const [Shell, Schedule, TimezoneSelect] = useMemo(() => {
|
||||
return isPlatform
|
||||
? [PlatformShell, PlatformSchedule, PlatformTimzoneSelect]
|
||||
: [WebShell, WebSchedule, WebTimezoneSelect];
|
||||
}, [isPlatform]);
|
||||
|
||||
return (
|
||||
<Shell
|
||||
headerClassName={cn(customClassNames?.containerClassName)}
|
||||
backPath={backPath}
|
||||
title={schedule.name ? `${schedule.name} | ${t("availability")}` : t("availability")}
|
||||
heading={
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<EditableHeading
|
||||
className={cn(customClassNames?.editableHeadingClassName)}
|
||||
isReady={!isLoading}
|
||||
disabled={disableEditableHeading}
|
||||
{...field}
|
||||
data-testid="availablity-title"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
// Trigger callback whenever the form state changes
|
||||
useEffect(() => {
|
||||
if (onFormStateChange && watchedValues) {
|
||||
onFormStateChange(watchedValues as AvailabilityFormValues);
|
||||
}
|
||||
subtitle={
|
||||
schedule ? (
|
||||
schedule.schedule
|
||||
.filter((availability) => !!availability.days.length)
|
||||
.map((availability) =>
|
||||
availabilityAsString(availability, {
|
||||
locale: i18n.language,
|
||||
hour12: timeFormat === 12,
|
||||
})
|
||||
)
|
||||
// sort the availability strings as per user's weekstart (settings)
|
||||
.sort(sortAvailabilityStrings(i18n.language, weekStart))
|
||||
.map((availabilityString, index) => (
|
||||
<span key={index} className={cn(customClassNames?.subtitlesClassName)}>
|
||||
{availabilityString}
|
||||
<br />
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<SkeletonText className="h-4 w-48" />
|
||||
)
|
||||
}, [watchedValues, onFormStateChange]);
|
||||
|
||||
const [Shell, Schedule, TimezoneSelect] = useMemo(() => {
|
||||
return isPlatform
|
||||
? [PlatformShell, PlatformSchedule, PlatformTimzoneSelect]
|
||||
: [WebShell, WebSchedule, WebTimezoneSelect];
|
||||
}, [isPlatform]);
|
||||
|
||||
const saveButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleFormSubmit = useCallback(() => {
|
||||
if (saveButtonRef.current) {
|
||||
saveButtonRef.current.click();
|
||||
} else {
|
||||
form.handleSubmit(handleSubmit)();
|
||||
}
|
||||
CTA={
|
||||
<div className={cn(customClassNames?.ctaClassName, "flex items-center justify-end")}>
|
||||
<div className="sm:hover:bg-muted hidden items-center rounded-md px-2 transition sm:flex">
|
||||
{!openSidebar && allowSetToDefault ? (
|
||||
<>
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
className="mt-2 cursor-pointer self-center pe-2"
|
||||
loadingClassName="me-4"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("set_to_default")}
|
||||
</Skeleton>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="isDefault"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Switch
|
||||
id="hiddenSwitch"
|
||||
classNames={{
|
||||
container: cn(customClassNames?.hiddenSwitchClassname?.container),
|
||||
thumb: cn(customClassNames?.hiddenSwitchClassname?.thumb),
|
||||
}}
|
||||
disabled={isSaving || schedule.isDefault}
|
||||
checked={value}
|
||||
onCheckedChange={(checked) => {
|
||||
onChange(checked);
|
||||
bulkUpdateModalProps?.setIsOpen(checked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<VerticalDivider className="hidden sm:inline" />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
}, [form, handleSubmit]);
|
||||
|
||||
{bulkUpdateModalProps && bulkUpdateModalProps?.isOpen && (
|
||||
<BulkEditDefaultForEventsModal
|
||||
isPending={bulkUpdateModalProps?.isSaving}
|
||||
open={bulkUpdateModalProps?.isOpen}
|
||||
setOpen={bulkUpdateModalProps.setIsOpen}
|
||||
bulkUpdateFunction={bulkUpdateModalProps?.save}
|
||||
description={t("default_schedules_bulk_description")}
|
||||
eventTypes={bulkUpdateModalProps?.eventTypes}
|
||||
isEventTypesFetching={bulkUpdateModalProps?.isEventTypesFetching}
|
||||
handleBulkEditDialogToggle={bulkUpdateModalProps.handleBulkEditDialogToggle}
|
||||
/>
|
||||
)}
|
||||
const validateForm = useCallback(async () => {
|
||||
const isValid = await form.trigger();
|
||||
return {
|
||||
isValid,
|
||||
errors: form.formState.errors,
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
{allowDelete && (
|
||||
<>
|
||||
<DeleteDialogButton
|
||||
buttonClassName={cn("hidden me-2 sm:inline", customClassNames?.deleteButtonClassname)}
|
||||
disabled={schedule.isLastSchedule}
|
||||
isPending={isDeleting}
|
||||
handleDelete={handleDelete}
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
validateForm,
|
||||
handleFormSubmit,
|
||||
}),
|
||||
[validateForm, handleFormSubmit]
|
||||
);
|
||||
|
||||
return (
|
||||
<Shell
|
||||
headerClassName={cn(customClassNames?.containerClassName)}
|
||||
backPath={backPath}
|
||||
title={schedule.name ? `${schedule.name} | ${t("availability")}` : t("availability")}
|
||||
heading={
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<EditableHeading
|
||||
className={cn(customClassNames?.editableHeadingClassName)}
|
||||
isReady={!isLoading}
|
||||
disabled={disableEditableHeading}
|
||||
{...field}
|
||||
data-testid="availablity-title"
|
||||
/>
|
||||
<VerticalDivider className="hidden sm:inline" />
|
||||
</>
|
||||
)}
|
||||
<SmallScreenSideBar open={openSidebar}>
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
openSidebar
|
||||
? "fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 sm:hidden"
|
||||
: ""
|
||||
)}>
|
||||
<div
|
||||
className={classNames(
|
||||
"bg-default fixed right-0 z-20 flex h-screen w-80 flex-col space-y-2 overflow-x-hidden rounded-md px-2 pb-3 transition-transform",
|
||||
openSidebar ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
|
||||
)}>
|
||||
<div className="flex flex-row items-center pt-16">
|
||||
<Button StartIcon="arrow-left" color="minimal" onClick={() => setOpenSidebar(false)} />
|
||||
<p className="-ml-2">{t("availability_settings")}</p>
|
||||
{allowDelete && (
|
||||
<DeleteDialogButton
|
||||
buttonClassName={cn("ml-16 inline", customClassNames?.deleteButtonClassname)}
|
||||
disabled={schedule.isLastSchedule}
|
||||
isPending={isDeleting}
|
||||
handleDelete={handleDelete}
|
||||
onDeleteConfirmed={() => {
|
||||
setOpenSidebar(false);
|
||||
)}
|
||||
/>
|
||||
}
|
||||
subtitle={
|
||||
schedule ? (
|
||||
schedule.schedule
|
||||
.filter((availability) => !!availability.days.length)
|
||||
.map((availability) =>
|
||||
availabilityAsString(availability, {
|
||||
locale: i18n.language,
|
||||
hour12: timeFormat === 12,
|
||||
})
|
||||
)
|
||||
// sort the availability strings as per user's weekstart (settings)
|
||||
.sort(sortAvailabilityStrings(i18n.language, weekStart))
|
||||
.map((availabilityString, index) => (
|
||||
<span key={index} className={cn(customClassNames?.subtitlesClassName)}>
|
||||
{availabilityString}
|
||||
<br />
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<SkeletonText className="h-4 w-48" />
|
||||
)
|
||||
}
|
||||
CTA={
|
||||
<div className={cn(customClassNames?.ctaClassName, "flex items-center justify-end")}>
|
||||
<div className="sm:hover:bg-muted hidden items-center rounded-md px-2 transition sm:flex">
|
||||
{!openSidebar && allowSetToDefault ? (
|
||||
<>
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
className="mt-2 cursor-pointer self-center pe-2"
|
||||
loadingClassName="me-4"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("set_to_default")}
|
||||
</Skeleton>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="isDefault"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Switch
|
||||
id="hiddenSwitch"
|
||||
classNames={{
|
||||
container: cn(customClassNames?.hiddenSwitchClassname?.container),
|
||||
thumb: cn(customClassNames?.hiddenSwitchClassname?.thumb),
|
||||
}}
|
||||
disabled={isSaving || schedule.isDefault}
|
||||
checked={value}
|
||||
onCheckedChange={(checked) => {
|
||||
onChange(checked);
|
||||
bulkUpdateModalProps?.setIsOpen(checked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col px-2 py-2">
|
||||
<Skeleton as={Label} waitForTranslation={!isPlatform}>
|
||||
{t("name")}
|
||||
</Skeleton>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<input
|
||||
className="hover:border-emphasis dark:focus:border-emphasis border-default bg-default placeholder:text-muted text-emphasis focus:ring-brand-default disabled:bg-subtle disabled:hover:border-subtle focus:border-subtle mb-2 block h-9 w-full rounded-md border px-3 py-2 text-sm leading-4 focus:outline-none focus:ring-2 disabled:cursor-not-allowed"
|
||||
{...field}
|
||||
/>
|
||||
<VerticalDivider className="hidden sm:inline" />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{bulkUpdateModalProps && bulkUpdateModalProps?.isOpen && (
|
||||
<BulkEditDefaultForEventsModal
|
||||
isPending={bulkUpdateModalProps?.isSaving}
|
||||
open={bulkUpdateModalProps?.isOpen}
|
||||
setOpen={bulkUpdateModalProps.setIsOpen}
|
||||
bulkUpdateFunction={bulkUpdateModalProps?.save}
|
||||
description={t("default_schedules_bulk_description")}
|
||||
eventTypes={bulkUpdateModalProps?.eventTypes}
|
||||
isEventTypesFetching={bulkUpdateModalProps?.isEventTypesFetching}
|
||||
handleBulkEditDialogToggle={bulkUpdateModalProps.handleBulkEditDialogToggle}
|
||||
/>
|
||||
)}
|
||||
|
||||
{allowDelete && (
|
||||
<>
|
||||
<DeleteDialogButton
|
||||
buttonClassName={cn("hidden me-2 sm:inline", customClassNames?.deleteButtonClassname)}
|
||||
disabled={schedule.isLastSchedule}
|
||||
isPending={isDeleting}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
<VerticalDivider className="hidden sm:inline" />
|
||||
</>
|
||||
)}
|
||||
<SmallScreenSideBar open={openSidebar}>
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
openSidebar
|
||||
? "fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 sm:hidden"
|
||||
: ""
|
||||
)}>
|
||||
<div
|
||||
className={classNames(
|
||||
"bg-default fixed right-0 z-20 flex h-screen w-80 flex-col space-y-2 overflow-x-hidden rounded-md px-2 pb-3 transition-transform",
|
||||
openSidebar ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
|
||||
)}>
|
||||
<div className="flex flex-row items-center pt-16">
|
||||
<Button StartIcon="arrow-left" color="minimal" onClick={() => setOpenSidebar(false)} />
|
||||
<p className="-ml-2">{t("availability_settings")}</p>
|
||||
{allowDelete && (
|
||||
<DeleteDialogButton
|
||||
buttonClassName={cn("ml-16 inline", customClassNames?.deleteButtonClassname)}
|
||||
disabled={schedule.isLastSchedule}
|
||||
isPending={isDeleting}
|
||||
handleDelete={handleDelete}
|
||||
onDeleteConfirmed={() => {
|
||||
setOpenSidebar(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-9 flex-row-reverse items-center justify-end gap-3 px-2">
|
||||
{allowSetToDefault && (
|
||||
<>
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
className="mt-2 cursor-pointer self-center pr-2 sm:inline"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("set_to_default")}
|
||||
</Skeleton>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="isDefault"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Switch
|
||||
classNames={{
|
||||
container: cn(customClassNames?.hiddenSwitchClassname?.container),
|
||||
thumb: cn(customClassNames?.hiddenSwitchClassname?.thumb),
|
||||
}}
|
||||
id="hiddenSwitch"
|
||||
disabled={isSaving || value}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-40 col-span-3 space-y-2 px-2 py-4 lg:col-span-1">
|
||||
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
|
||||
<div>
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="timeZone-sm-viewport"
|
||||
className="mb-0 inline-block leading-none"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("timezone")}
|
||||
</Skeleton>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="timeZone"
|
||||
render={({ field: { onChange, value } }) =>
|
||||
value ? (
|
||||
<TimezoneSelect
|
||||
inputId="timeZone-sm-viewport"
|
||||
value={value}
|
||||
className={cn(
|
||||
"focus:border-brand-default border-default mt-1 block w-72 rounded-md text-sm",
|
||||
customClassNames?.timezoneSelectClassName
|
||||
)}
|
||||
onChange={(timezone) => onChange(timezone.value)}
|
||||
/>
|
||||
) : (
|
||||
<SelectSkeletonLoader className="mt-1 w-72" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!isPlatform && (
|
||||
</div>
|
||||
<div className="flex flex-col px-2 py-2">
|
||||
<Skeleton as={Label} waitForTranslation={!isPlatform}>
|
||||
{t("name")}
|
||||
</Skeleton>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<input
|
||||
className="hover:border-emphasis dark:focus:border-emphasis border-default bg-default placeholder:text-muted text-emphasis focus:ring-brand-default disabled:bg-subtle disabled:hover:border-subtle focus:border-subtle mb-2 block h-9 w-full rounded-md border px-3 py-2 text-sm leading-4 focus:outline-none focus:ring-2 disabled:cursor-not-allowed"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-9 flex-row-reverse items-center justify-end gap-3 px-2">
|
||||
{allowSetToDefault && (
|
||||
<>
|
||||
<hr className="border-subtle my-7" />
|
||||
<div className="rounded-md md:block">
|
||||
<Skeleton
|
||||
as="h3"
|
||||
className="mb-0 inline-block text-sm font-medium"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("something_doesnt_look_right")}
|
||||
</Skeleton>
|
||||
<div className="mt-3 flex">
|
||||
<Skeleton
|
||||
as={Button}
|
||||
href="/availability/troubleshoot"
|
||||
color="secondary"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("launch_troubleshooter")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
className="mt-2 cursor-pointer self-center pr-2 sm:inline"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("set_to_default")}
|
||||
</Skeleton>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="isDefault"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Switch
|
||||
classNames={{
|
||||
container: cn(customClassNames?.hiddenSwitchClassname?.container),
|
||||
thumb: cn(customClassNames?.hiddenSwitchClassname?.thumb),
|
||||
}}
|
||||
id="hiddenSwitch"
|
||||
disabled={isSaving || value}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-40 col-span-3 space-y-2 px-2 py-4 lg:col-span-1">
|
||||
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
|
||||
<div>
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="timeZone-sm-viewport"
|
||||
className="mb-0 inline-block leading-none"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("timezone")}
|
||||
</Skeleton>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="timeZone"
|
||||
render={({ field: { onChange, value } }) =>
|
||||
value ? (
|
||||
<TimezoneSelect
|
||||
inputId="timeZone-sm-viewport"
|
||||
value={value}
|
||||
className={cn(
|
||||
"focus:border-brand-default border-default mt-1 block w-72 rounded-md text-sm",
|
||||
customClassNames?.timezoneSelectClassName
|
||||
)}
|
||||
onChange={(timezone) => onChange(timezone.value)}
|
||||
/>
|
||||
) : (
|
||||
<SelectSkeletonLoader className="mt-1 w-72" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!isPlatform && (
|
||||
<>
|
||||
<hr className="border-subtle my-7" />
|
||||
<div className="rounded-md md:block">
|
||||
<Skeleton
|
||||
as="h3"
|
||||
className="mb-0 inline-block text-sm font-medium"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("something_doesnt_look_right")}
|
||||
</Skeleton>
|
||||
<div className="mt-3 flex">
|
||||
<Skeleton
|
||||
as={Button}
|
||||
href="/availability/troubleshoot"
|
||||
color="secondary"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("launch_troubleshooter")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</SmallScreenSideBar>
|
||||
<div className="border-default border-l-2" />
|
||||
<Button
|
||||
ref={saveButtonRef}
|
||||
className="ml-4 lg:ml-0"
|
||||
type="submit"
|
||||
form="availability-form"
|
||||
loading={isSaving}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-3 sm:hidden"
|
||||
StartIcon="ellipsis-vertical"
|
||||
variant="icon"
|
||||
color="secondary"
|
||||
onClick={() => setOpenSidebar(true)}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
<div className="mt-4 w-full md:mt-0">
|
||||
<Form
|
||||
form={form}
|
||||
id="availability-form"
|
||||
handleSubmit={async (props) => {
|
||||
handleSubmit(props);
|
||||
}}
|
||||
className={cn(customClassNames?.formClassName, "flex flex-col sm:mx-0 xl:flex-row xl:space-x-6")}>
|
||||
<div className="flex-1 flex-row xl:mr-0">
|
||||
<div
|
||||
className={cn(
|
||||
"border-subtle mb-6 rounded-md border",
|
||||
customClassNames?.scheduleClassNames?.scheduleContainer
|
||||
)}>
|
||||
<div>
|
||||
{typeof weekStart === "string" && (
|
||||
<Schedule
|
||||
labels={{
|
||||
addTime: t("add_time_availability"),
|
||||
copyTime: t("copy_times_to"),
|
||||
deleteTime: t("delete"),
|
||||
}}
|
||||
classNames={
|
||||
customClassNames?.scheduleClassNames ? { ...customClassNames.scheduleClassNames } : {}
|
||||
}
|
||||
control={form.control}
|
||||
name="schedule"
|
||||
userTimeFormat={timeFormat}
|
||||
weekStart={
|
||||
[
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
].indexOf(weekStart) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</SmallScreenSideBar>
|
||||
<div className="border-default border-l-2" />
|
||||
<Button className="ml-4 lg:ml-0" type="submit" form="availability-form" loading={isSaving}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-3 sm:hidden"
|
||||
StartIcon="ellipsis-vertical"
|
||||
variant="icon"
|
||||
color="secondary"
|
||||
onClick={() => setOpenSidebar(true)}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
<div className="mt-4 w-full md:mt-0">
|
||||
<Form
|
||||
form={form}
|
||||
id="availability-form"
|
||||
handleSubmit={async (props) => {
|
||||
handleSubmit(props);
|
||||
}}
|
||||
className={cn(customClassNames?.formClassName, "flex flex-col sm:mx-0 xl:flex-row xl:space-x-6")}>
|
||||
<div className="flex-1 flex-row xl:mr-0">
|
||||
<div
|
||||
className={cn(
|
||||
"border-subtle mb-6 rounded-md border",
|
||||
customClassNames?.scheduleClassNames?.scheduleContainer
|
||||
)}>
|
||||
<div>
|
||||
{typeof weekStart === "string" && (
|
||||
<Schedule
|
||||
labels={{
|
||||
addTime: t("add_time_availability"),
|
||||
copyTime: t("copy_times_to"),
|
||||
deleteTime: t("delete"),
|
||||
}}
|
||||
classNames={
|
||||
customClassNames?.scheduleClassNames ? { ...customClassNames.scheduleClassNames } : {}
|
||||
}
|
||||
control={form.control}
|
||||
name="schedule"
|
||||
userTimeFormat={timeFormat}
|
||||
weekStart={
|
||||
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
||||
weekStart
|
||||
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
{enableOverrides && (
|
||||
<DateOverride
|
||||
workingHours={schedule.workingHours}
|
||||
userTimeFormat={timeFormat}
|
||||
handleSubmit={handleSubmit}
|
||||
travelSchedules={travelSchedules}
|
||||
weekStart={
|
||||
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
||||
weekStart
|
||||
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
}
|
||||
overridesModalClassNames={customClassNames?.overridesModalClassNames}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-40 col-span-3 hidden space-y-2 md:block lg:col-span-1">
|
||||
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
|
||||
<div>
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="timeZone-lg-viewport"
|
||||
className="mb-0 inline-block leading-none"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("timezone")}
|
||||
</Skeleton>
|
||||
<Controller
|
||||
name="timeZone"
|
||||
render={({ field: { onChange, value } }) =>
|
||||
value ? (
|
||||
<TimezoneSelect
|
||||
inputId="timeZone-lg-viewport"
|
||||
value={value}
|
||||
className="focus:border-brand-default border-default mt-1 block w-72 rounded-md text-sm"
|
||||
onChange={(timezone) => onChange(timezone.value)}
|
||||
/>
|
||||
) : (
|
||||
<SelectSkeletonLoader className="mt-1 w-72" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{isPlatform ? (
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
<hr className="border-subtle my-6 mr-8" />
|
||||
<div className="rounded-md">
|
||||
<Skeleton
|
||||
as="h3"
|
||||
className="mb-0 inline-block text-sm font-medium"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("something_doesnt_look_right")}
|
||||
</Skeleton>
|
||||
<div className="mt-3 flex">
|
||||
<Skeleton
|
||||
as={Button}
|
||||
href="/availability/troubleshoot"
|
||||
color="secondary"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("launch_troubleshooter")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{enableOverrides && (
|
||||
<DateOverride
|
||||
workingHours={schedule.workingHours}
|
||||
userTimeFormat={timeFormat}
|
||||
handleSubmit={handleSubmit}
|
||||
travelSchedules={travelSchedules}
|
||||
weekStart={
|
||||
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
||||
weekStart
|
||||
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
}
|
||||
overridesModalClassNames={customClassNames?.overridesModalClassNames}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-40 col-span-3 hidden space-y-2 md:block lg:col-span-1">
|
||||
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
|
||||
<div>
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="timeZone-lg-viewport"
|
||||
className="mb-0 inline-block leading-none"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("timezone")}
|
||||
</Skeleton>
|
||||
<Controller
|
||||
name="timeZone"
|
||||
render={({ field: { onChange, value } }) =>
|
||||
value ? (
|
||||
<TimezoneSelect
|
||||
inputId="timeZone-lg-viewport"
|
||||
value={value}
|
||||
className="focus:border-brand-default border-default mt-1 block w-72 rounded-md text-sm"
|
||||
onChange={(timezone) => onChange(timezone.value)}
|
||||
/>
|
||||
) : (
|
||||
<SelectSkeletonLoader className="mt-1 w-72" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{isPlatform ? (
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
<hr className="border-subtle my-6 mr-8" />
|
||||
<div className="rounded-md">
|
||||
<Skeleton
|
||||
as="h3"
|
||||
className="mb-0 inline-block text-sm font-medium"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("something_doesnt_look_right")}
|
||||
</Skeleton>
|
||||
<div className="mt-3 flex">
|
||||
<Skeleton
|
||||
as={Button}
|
||||
href="/availability/troubleshoot"
|
||||
color="secondary"
|
||||
waitForTranslation={!isPlatform}>
|
||||
{t("launch_troubleshooter")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
</Form>
|
||||
</div>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -29,3 +29,13 @@ export type scheduleClassNames = {
|
||||
labelAndSwitchContainer?: string;
|
||||
scheduleContainer?: string;
|
||||
};
|
||||
|
||||
export type AvailabilityFormValidationResult = {
|
||||
isValid: boolean;
|
||||
errors: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export interface AvailabilitySettingsFormRef {
|
||||
validateForm: () => Promise<AvailabilityFormValidationResult>;
|
||||
handleFormSubmit: () => void;
|
||||
}
|
||||
|
||||
+26
-20
@@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
import type { ScheduleLabelsType } from "@calcom/features/schedules/components/Schedule";
|
||||
import type { UpdateScheduleResponse } from "@calcom/lib/schedules/updateSchedule";
|
||||
@@ -11,7 +12,7 @@ import { useMe } from "../../hooks/useMe";
|
||||
import { AtomsWrapper } from "../../src/components/atoms-wrapper";
|
||||
import { useToast } from "../../src/components/ui/use-toast";
|
||||
import type { Availability } from "../AvailabilitySettings";
|
||||
import type { CustomClassNames } from "../AvailabilitySettings";
|
||||
import type { CustomClassNames, AvailabilitySettingsFormRef } from "../AvailabilitySettings";
|
||||
import { AvailabilitySettings } from "../AvailabilitySettings";
|
||||
import type { AvailabilityFormValues } from "../types";
|
||||
|
||||
@@ -37,24 +38,28 @@ export type AvailabilitySettingsPlatformWrapperProps = {
|
||||
loadingStateChildren?: ReactNode;
|
||||
};
|
||||
|
||||
export const AvailabilitySettingsPlatformWrapper = ({
|
||||
id,
|
||||
customClassNames,
|
||||
onDeleteError,
|
||||
onDeleteSuccess,
|
||||
onUpdateError,
|
||||
onUpdateSuccess,
|
||||
disableEditableHeading = false,
|
||||
enableOverrides = false,
|
||||
onBeforeUpdate,
|
||||
onFormStateChange,
|
||||
allowDelete,
|
||||
allowSetToDefault,
|
||||
disableToasts,
|
||||
isDryRun = false,
|
||||
noScheduleChildren,
|
||||
loadingStateChildren,
|
||||
}: AvailabilitySettingsPlatformWrapperProps) => {
|
||||
export const AvailabilitySettingsPlatformWrapper = forwardRef<
|
||||
AvailabilitySettingsFormRef,
|
||||
AvailabilitySettingsPlatformWrapperProps
|
||||
>(function AvailabilitySettingsPlatformWrapper(props, ref) {
|
||||
const {
|
||||
id,
|
||||
customClassNames,
|
||||
onDeleteError,
|
||||
onDeleteSuccess,
|
||||
onUpdateError,
|
||||
onUpdateSuccess,
|
||||
disableEditableHeading = false,
|
||||
enableOverrides = false,
|
||||
onBeforeUpdate,
|
||||
onFormStateChange,
|
||||
allowDelete,
|
||||
allowSetToDefault,
|
||||
disableToasts,
|
||||
isDryRun = false,
|
||||
noScheduleChildren,
|
||||
loadingStateChildren,
|
||||
} = props;
|
||||
const { isLoading, data: atomSchedule } = useAtomSchedule(id);
|
||||
const { data: me } = useMe();
|
||||
const { timeFormat } = me?.data || { timeFormat: null };
|
||||
@@ -139,6 +144,7 @@ export const AvailabilitySettingsPlatformWrapper = ({
|
||||
return (
|
||||
<AtomsWrapper>
|
||||
<AvailabilitySettings
|
||||
ref={ref}
|
||||
disableEditableHeading={disableEditableHeading}
|
||||
handleDelete={() => {
|
||||
if (isDryRun) {
|
||||
@@ -195,4 +201,4 @@ export const AvailabilitySettingsPlatformWrapper = ({
|
||||
/>
|
||||
</AtomsWrapper>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -196,16 +196,6 @@ export const useEventTypeForm = ({
|
||||
// Watch all form values to trigger onFormStateChange on any change
|
||||
const watchedValues = form.watch();
|
||||
|
||||
useEffect(() => {
|
||||
if (onFormStateChange) {
|
||||
onFormStateChange({
|
||||
isDirty: isFormDirty,
|
||||
dirtyFields: dirtyFields as Partial<FormValues>,
|
||||
values: watchedValues,
|
||||
});
|
||||
}
|
||||
}, [isFormDirty, watchedValues, onFormStateChange]);
|
||||
|
||||
const isObject = <T>(value: T): boolean => {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
};
|
||||
@@ -421,5 +411,15 @@ export const useEventTypeForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (onFormStateChange) {
|
||||
onFormStateChange({
|
||||
isDirty: isFormDirty,
|
||||
dirtyFields: dirtyFields as Partial<FormValues>,
|
||||
values: watchedValues,
|
||||
});
|
||||
}
|
||||
}, [isFormDirty, dirtyFields, watchedValues, onFormStateChange]);
|
||||
|
||||
return { form, handleSubmit };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { useRef, useState, useEffect, forwardRef, useImperativeHandle, useCallback } from "react";
|
||||
|
||||
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import { EventType as EventTypeComponent } from "@calcom/features/eventtypes/components/EventType";
|
||||
@@ -12,7 +12,11 @@ import type { EventAvailabilityTabCustomClassNames } from "@calcom/features/even
|
||||
import type { EventLimitsTabCustomClassNames } from "@calcom/features/eventtypes/components/tabs/limits/EventLimitsTab";
|
||||
import type { EventRecurringTabCustomClassNames } from "@calcom/features/eventtypes/components/tabs/recurring/RecurringEventController";
|
||||
import type { EventSetupTabCustomClassNames } from "@calcom/features/eventtypes/components/tabs/setup/EventSetupTab";
|
||||
import type { EventTypeSetupProps, FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import type {
|
||||
EventTypeSetupProps,
|
||||
FormValues,
|
||||
EventTypePlatformWrapperRef,
|
||||
} from "@calcom/features/eventtypes/lib/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
@@ -64,28 +68,30 @@ export type EventTypePlatformWrapperProps = {
|
||||
}) => void;
|
||||
};
|
||||
|
||||
const EventType = ({
|
||||
tabs = ["setup", "availability", "team", "limits", "advanced", "recurring", "payments"],
|
||||
onSuccess,
|
||||
onError,
|
||||
onDeleteSuccess,
|
||||
onDeleteError,
|
||||
id,
|
||||
allowDelete = true,
|
||||
customClassNames,
|
||||
disableToasts = false,
|
||||
isDryRun = false,
|
||||
onFormStateChange,
|
||||
...props
|
||||
}: EventTypeSetupProps & EventTypePlatformWrapperProps) => {
|
||||
const EventType = forwardRef<
|
||||
EventTypePlatformWrapperRef,
|
||||
EventTypeSetupProps & EventTypePlatformWrapperProps
|
||||
>(function EventType(props, ref) {
|
||||
const {
|
||||
tabs = ["setup", "availability", "team", "limits", "advanced", "recurring", "payments"],
|
||||
onSuccess,
|
||||
onError,
|
||||
onDeleteSuccess,
|
||||
onDeleteError,
|
||||
id,
|
||||
allowDelete = true,
|
||||
customClassNames,
|
||||
disableToasts = false,
|
||||
isDryRun = false,
|
||||
onFormStateChange,
|
||||
...restProps
|
||||
} = props;
|
||||
const { t } = useLocale();
|
||||
const { toast } = useToast();
|
||||
const { organizationId } = useAtomsContext();
|
||||
const isTeamEventTypeDeleted = useRef(false);
|
||||
const leaveWithoutAssigningHosts = useRef(false);
|
||||
const [isOpenAssignmentWarnDialog, setIsOpenAssignmentWarnDialog] = useState<boolean>(false);
|
||||
const [pendingRoute, setPendingRoute] = useState("");
|
||||
const { eventType, locationOptions, team, teamMembers, destinationCalendar } = props;
|
||||
const { eventType, locationOptions, team, teamMembers, destinationCalendar } = restProps;
|
||||
const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState<ChildrenEventType[]>([]);
|
||||
const { data: user, isLoading: isUserLoading } = useMe();
|
||||
|
||||
@@ -93,7 +99,6 @@ const EventType = ({
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
isTeamEventTypeDeleted.current = true;
|
||||
setSlugExistsChildrenDialogOpen([]);
|
||||
setIsOpenAssignmentWarnDialog(false);
|
||||
onDeleteSuccess?.();
|
||||
};
|
||||
|
||||
@@ -158,9 +163,37 @@ const EventType = ({
|
||||
},
|
||||
onFormStateChange: onFormStateChange,
|
||||
});
|
||||
|
||||
// Create a ref for the save button to trigger its click
|
||||
const saveButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleFormSubmit = useCallback(() => {
|
||||
if (saveButtonRef.current) {
|
||||
saveButtonRef.current.click();
|
||||
} else {
|
||||
form.handleSubmit(handleSubmit)();
|
||||
}
|
||||
}, [handleSubmit, form]);
|
||||
|
||||
const validateForm = useCallback(async () => {
|
||||
const isValid = await form.trigger();
|
||||
return {
|
||||
isValid,
|
||||
errors: form.formState.errors,
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
validateForm,
|
||||
handleFormSubmit,
|
||||
}),
|
||||
[validateForm, handleFormSubmit]
|
||||
);
|
||||
const slug = form.watch("slug") ?? eventType.slug;
|
||||
|
||||
const showToast = (message: string, variant: "success" | "warning" | "error") => {
|
||||
const showToast = (message: string, _variant: "success" | "warning" | "error") => {
|
||||
if (!disableToasts) {
|
||||
toast({ description: message });
|
||||
}
|
||||
@@ -246,9 +279,8 @@ const EventType = ({
|
||||
hosts: eventType.hosts,
|
||||
assignAllTeamMembers: eventType.assignAllTeamMembers,
|
||||
isManagedEventType: eventType.schedulingType === SchedulingType.MANAGED,
|
||||
onError: (url) => {
|
||||
setIsOpenAssignmentWarnDialog(true);
|
||||
setPendingRoute(url);
|
||||
onError: () => {
|
||||
return;
|
||||
},
|
||||
onStart: () => {
|
||||
return;
|
||||
@@ -284,7 +316,7 @@ const EventType = ({
|
||||
return (
|
||||
<AtomsWrapper customClassName={customClassNames?.atomsWrapper}>
|
||||
<EventTypeComponent
|
||||
{...props}
|
||||
{...restProps}
|
||||
tabMap={tabMap}
|
||||
onDelete={onDelete}
|
||||
onConflict={onConflict}
|
||||
@@ -294,7 +326,8 @@ const EventType = ({
|
||||
isPlatform
|
||||
tabName={currentTab}
|
||||
tabsNavigation={tabsNavigation}
|
||||
allowDelete={allowDelete}>
|
||||
allowDelete={allowDelete}
|
||||
saveButtonRef={saveButtonRef}>
|
||||
<>
|
||||
{slugExistsChildrenDialogOpen.length ? (
|
||||
<ManagedEventTypeDialog
|
||||
@@ -315,20 +348,24 @@ const EventType = ({
|
||||
</EventTypeComponent>
|
||||
</AtomsWrapper>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export const EventTypePlatformWrapper = ({
|
||||
id,
|
||||
tabs,
|
||||
onSuccess,
|
||||
onError,
|
||||
onDeleteSuccess,
|
||||
onDeleteError,
|
||||
allowDelete = true,
|
||||
customClassNames,
|
||||
isDryRun,
|
||||
onFormStateChange,
|
||||
}: EventTypePlatformWrapperProps) => {
|
||||
export const EventTypePlatformWrapper = forwardRef<
|
||||
EventTypePlatformWrapperRef,
|
||||
EventTypePlatformWrapperProps
|
||||
>(function EventTypePlatformWrapper(props, ref) {
|
||||
const {
|
||||
id,
|
||||
tabs,
|
||||
onSuccess,
|
||||
onError,
|
||||
onDeleteSuccess,
|
||||
onDeleteError,
|
||||
allowDelete = true,
|
||||
customClassNames,
|
||||
isDryRun,
|
||||
onFormStateChange,
|
||||
} = props;
|
||||
const { data: eventTypeQueryData } = useAtomsEventTypeById(id);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -344,7 +381,7 @@ export const EventTypePlatformWrapper = ({
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [queryClient, id]);
|
||||
}, [queryClient, id, eventTypeQueryData]);
|
||||
|
||||
if (!eventTypeQueryData) return null;
|
||||
|
||||
@@ -361,6 +398,7 @@ export const EventTypePlatformWrapper = ({
|
||||
customClassNames={customClassNames}
|
||||
isDryRun={isDryRun}
|
||||
onFormStateChange={onFormStateChange}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { TabMap } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { EventTypePlatformWrapperRef as EventSettingsFromRef } from "@calcom/features/eventtypes/lib/types";
|
||||
|
||||
export type { EventSettingsFromRef };
|
||||
|
||||
export type PlatformTabs = keyof Omit<TabMap, "workflows" | "webhooks" | "instant" | "ai" | "apps">;
|
||||
|
||||
@@ -29,6 +29,8 @@ export { DestinationCalendarSettingsPlatformWrapper as DestinationCalendarSettin
|
||||
export { CalendarSettingsPlatformWrapper as CalendarSettings } from "./calendar-settings/index";
|
||||
export type { UpdateScheduleInput_2024_06_11 as UpdateScheduleBody } from "@calcom/platform-types";
|
||||
export { EventTypePlatformWrapper as EventTypeSettings } from "./event-types/wrappers/EventTypePlatformWrapper";
|
||||
export type { EventSettingsFromRef } from "./event-types/wrappers/types";
|
||||
export type { AvailabilitySettingsFormRef } from "./availability/types";
|
||||
export { ConferencingAppsViewPlatformWrapper as ConferencingAppsSettings } from "./connect/conferencing-apps/ConferencingAppsViewPlatformWrapper";
|
||||
export { StripeConnect } from "./connect/stripe/StripeConnect";
|
||||
export { CreateEventTypePlatformWrapper as CreateEventType } from "./event-types/wrappers/CreateEventTypePlatformWrapper";
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/cal-atoms.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/cal-atoms.umd.cjs"
|
||||
},
|
||||
"./add-members-switch/AddMembersWithSwitchPlatformWrapper": "./add-members-switch/AddMembersWithSwitchPlatformWrapper.tsx",
|
||||
"./add-members-switch/AddMembersWithSwitchWebWrapper": "./add-members-switch/AddMembersWithSwitchWebWrapper.tsx",
|
||||
@@ -84,5 +85,6 @@
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"main": "./dist/cal-atoms.umd.cjs"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,49 @@
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
import { Inter } from "next/font/google";
|
||||
import { useRef, useCallback } from "react";
|
||||
|
||||
import type { AvailabilitySettingsFormRef } from "@calcom/atoms";
|
||||
import { AvailabilitySettings } from "@calcom/atoms";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export default function Availability(props: { calUsername: string; calEmail: string }) {
|
||||
const availabilityRef = useRef<AvailabilitySettingsFormRef>(null);
|
||||
|
||||
const handleFormStateChange = useCallback((formState: unknown) => {
|
||||
console.log(formState, "formStateeeeee");
|
||||
}, []);
|
||||
|
||||
const handleValidate = async () => {
|
||||
const result = await availabilityRef.current?.validateForm();
|
||||
console.log("Validation result:", result);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
availabilityRef.current?.handleFormSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<main className={`flex min-h-screen flex-col ${inter.className}`}>
|
||||
<Navbar username={props.calUsername} />
|
||||
<div>
|
||||
<h1 className="mx-10 my-4 text-2xl font-semibold">Availability Settings</h1>
|
||||
|
||||
<div className="mx-10 mb-4 flex gap-4">
|
||||
<button
|
||||
onClick={handleValidate}
|
||||
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
Validate Form
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
|
||||
Submit Form
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AvailabilitySettings
|
||||
ref={availabilityRef}
|
||||
enableOverrides={true}
|
||||
customClassNames={{
|
||||
subtitlesClassName: "text-red-500",
|
||||
@@ -18,6 +51,7 @@ export default function Availability(props: { calUsername: string; calEmail: str
|
||||
editableHeadingClassName: "underline font-semibold",
|
||||
hiddenSwitchClassname: { thumb: "bg-red-500" },
|
||||
}}
|
||||
onFormStateChange={handleFormStateChange}
|
||||
onUpdateSuccess={() => {
|
||||
console.log("Updated successfully");
|
||||
}}
|
||||
|
||||
@@ -2,8 +2,9 @@ import { Navbar } from "@/components/Navbar";
|
||||
import { Inter } from "next/font/google";
|
||||
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
|
||||
import type { EventSettingsFromRef } from "@calcom/atoms";
|
||||
import {
|
||||
useEventTypes,
|
||||
useTeamEventTypes,
|
||||
@@ -18,6 +19,20 @@ export default function Bookings(props: { calUsername: string; calEmail: string
|
||||
const [eventTypeId, setEventTypeId] = useState<number | null>(null);
|
||||
const [isTeamEvent, setIsTeamEvent] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
const eventTypeRef = useRef<EventSettingsFromRef>(null);
|
||||
|
||||
const handleFormStateChange = useCallback((formState: unknown) => {
|
||||
console.log(formState, "formStateeeeee");
|
||||
}, []);
|
||||
|
||||
const handleValidate = async () => {
|
||||
const result = await eventTypeRef.current?.validateForm();
|
||||
console.log("Validation result:", result);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
eventTypeRef.current?.handleFormSubmit();
|
||||
};
|
||||
const { isLoading: isLoadingEvents, data: eventTypes, refetch } = useEventTypes(props.calUsername);
|
||||
const { data: teams } = useTeams();
|
||||
const {
|
||||
@@ -739,7 +754,22 @@ export default function Bookings(props: { calUsername: string; calEmail: string
|
||||
setEventTypeId(null);
|
||||
}}
|
||||
onDeleteError={console.error}
|
||||
onFormStateChange={handleFormStateChange}
|
||||
ref={eventTypeRef}
|
||||
/>
|
||||
|
||||
<div className="mt-4 flex justify-center gap-4">
|
||||
<button
|
||||
onClick={handleValidate}
|
||||
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
|
||||
Validate Form
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600">
|
||||
Submit Form
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user