@wix/headless-bookings 0.0.55 → 0.0.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/dist/react/booking-form/BookingForm.d.ts +117 -17
- package/cjs/dist/react/booking-form/BookingForm.js +149 -15
- package/cjs/dist/react/core/booking-form/BookingForm.d.ts +112 -10
- package/cjs/dist/react/core/booking-form/BookingForm.js +204 -64
- package/cjs/dist/react/time-slot-list/TimeSlot.d.ts +25 -0
- package/cjs/dist/react/time-slot-list/TimeSlot.js +48 -13
- package/cjs/dist/services/booking-form/booking-form.d.ts +22 -13
- package/cjs/dist/services/booking-form/booking-form.js +67 -25
- package/cjs/dist/services/booking-form/utils.d.ts +52 -0
- package/cjs/dist/services/booking-form/utils.js +47 -0
- package/dist/react/booking-form/BookingForm.d.ts +117 -17
- package/dist/react/booking-form/BookingForm.js +149 -15
- package/dist/react/core/booking-form/BookingForm.d.ts +112 -10
- package/dist/react/core/booking-form/BookingForm.js +204 -64
- package/dist/react/time-slot-list/TimeSlot.d.ts +25 -0
- package/dist/react/time-slot-list/TimeSlot.js +48 -13
- package/dist/services/booking-form/booking-form.d.ts +22 -13
- package/dist/services/booking-form/booking-form.js +67 -25
- package/dist/services/booking-form/utils.d.ts +52 -0
- package/dist/services/booking-form/utils.js +47 -0
- package/package.json +3 -3
|
@@ -9,11 +9,18 @@
|
|
|
9
9
|
* 2. SSR/SSG: Provide a pre-loaded form object for server-side rendering
|
|
10
10
|
*/
|
|
11
11
|
import * as React from 'react';
|
|
12
|
-
import type
|
|
13
|
-
import { forms } from '@wix/forms';
|
|
14
|
-
import { type ValidationResult } from '../../../services/booking-form/booking-form.js';
|
|
12
|
+
import { type FormValues } from '@wix/form-public';
|
|
13
|
+
import { forms, submissions } from '@wix/forms';
|
|
14
|
+
import { BookingFormConfigurationError, type ValidationResult } from '../../../services/booking-form/booking-form.js';
|
|
15
15
|
import { Form } from '@wix/headless-forms/react';
|
|
16
16
|
type FieldMap = Parameters<typeof Form.Fields>[0]['fieldMap'];
|
|
17
|
+
export type Submit = () => Promise<submissions.GetSubmissionResponse['submission'] | undefined>;
|
|
18
|
+
export type ValidateStep = (stepId: string) => Promise<boolean>;
|
|
19
|
+
export type FormHandle = {
|
|
20
|
+
submit: Submit;
|
|
21
|
+
validate: () => Promise<boolean>;
|
|
22
|
+
validateStep?: ValidateStep;
|
|
23
|
+
};
|
|
17
24
|
/**
|
|
18
25
|
* Render props data provided to BookingForm children
|
|
19
26
|
*/
|
|
@@ -26,19 +33,23 @@ export interface BookingFormRenderProps {
|
|
|
26
33
|
formSubmission: FormValues | null;
|
|
27
34
|
/** Action to store form submission data */
|
|
28
35
|
setFormSubmission: (formValues: FormValues) => void;
|
|
29
|
-
/** Action to validate the form */
|
|
30
|
-
validateFormSubmission: () => Promise<
|
|
36
|
+
/** Action to validate the form fields. Returns true if valid, false if there are errors. */
|
|
37
|
+
validateFormSubmission: () => Promise<boolean>;
|
|
31
38
|
/** Merged field map (DEFAULT_BOOKING_FIELD_MAP + user overrides) */
|
|
32
39
|
fields: FieldMap;
|
|
40
|
+
/** Ref to access form methods imperatively (pass to Form.Fields) */
|
|
41
|
+
formRef: React.RefObject<FormHandle | null>;
|
|
33
42
|
}
|
|
34
43
|
/**
|
|
35
44
|
* Base props shared by all BookingForm config patterns
|
|
36
45
|
*/
|
|
37
46
|
interface BookingFormBaseProps {
|
|
47
|
+
/** Optional service IDs to pass to FormService (will be merged into additionalMetadata) */
|
|
48
|
+
serviceIds?: string[];
|
|
38
49
|
/** Optional additional metadata to pass to FormService */
|
|
39
50
|
additionalMetadata?: Record<string, string | string[]>;
|
|
40
|
-
/**
|
|
41
|
-
fieldMap
|
|
51
|
+
/** Field map to override default booking field components */
|
|
52
|
+
fieldMap: FieldMap;
|
|
42
53
|
/** Render prop function that receives form data and actions */
|
|
43
54
|
children: (data: BookingFormRenderProps) => React.ReactNode;
|
|
44
55
|
}
|
|
@@ -46,13 +57,14 @@ interface BookingFormBaseProps {
|
|
|
46
57
|
* Props for BookingForm with formId (client-side loading)
|
|
47
58
|
*/
|
|
48
59
|
interface BookingFormWithFormIdProps extends BookingFormBaseProps {
|
|
49
|
-
/** The form ID to load (client-side loading) */
|
|
50
|
-
formId
|
|
60
|
+
/** The form ID to load (client-side loading) if not provided, the formId will be extracted from the selected service if available */
|
|
61
|
+
formId?: string;
|
|
51
62
|
/** Pre-loaded form - not used in this mode */
|
|
52
63
|
form?: never;
|
|
53
64
|
}
|
|
54
65
|
/**
|
|
55
66
|
* Props for BookingForm with pre-loaded form (SSR/SSG)
|
|
67
|
+
* Returns undefined if formId should be extracted from services by the service layer
|
|
56
68
|
*/
|
|
57
69
|
interface BookingFormWithFormProps extends BookingFormBaseProps {
|
|
58
70
|
/** Pre-loaded form object (SSR/SSG) - must be from bookings namespace */
|
|
@@ -136,4 +148,94 @@ export type BookingFormProps = BookingFormWithFormIdProps | BookingFormWithFormP
|
|
|
136
148
|
* ```
|
|
137
149
|
*/
|
|
138
150
|
export declare function Root(props: BookingFormProps): React.ReactNode;
|
|
139
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Render props for BookingForm.Actions component
|
|
153
|
+
*/
|
|
154
|
+
export interface ActionsRenderProps {
|
|
155
|
+
/** Action to validate the form submission */
|
|
156
|
+
validateFormSubmission: () => Promise<ValidationResult>;
|
|
157
|
+
/** The form ID being used */
|
|
158
|
+
formId: string;
|
|
159
|
+
/** Current form values */
|
|
160
|
+
formValues: FormValues;
|
|
161
|
+
/** Current form submission data */
|
|
162
|
+
formSubmission: FormValues | null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Props for BookingForm.Actions component
|
|
166
|
+
*/
|
|
167
|
+
export interface ActionsProps {
|
|
168
|
+
children: (data: ActionsRenderProps) => React.ReactNode;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Core component that provides booking form actions via render props.
|
|
172
|
+
* Must be used within BookingForm.Root context.
|
|
173
|
+
*
|
|
174
|
+
* @component
|
|
175
|
+
* @example
|
|
176
|
+
* ```tsx
|
|
177
|
+
* <CoreBookingForm.Root formId="form-123">
|
|
178
|
+
* {({ fields }) => (
|
|
179
|
+
* <>
|
|
180
|
+
* <Form.Fields fieldMap={fields} />
|
|
181
|
+
* <CoreBookingForm.Actions>
|
|
182
|
+
* {({ validateFormSubmission, formId }) => (
|
|
183
|
+
* <button onClick={async () => {
|
|
184
|
+
* const result = await validateFormSubmission();
|
|
185
|
+
* console.log('Validation result:', result);
|
|
186
|
+
* }}>
|
|
187
|
+
* Validate Form
|
|
188
|
+
* </button>
|
|
189
|
+
* )}
|
|
190
|
+
* </CoreBookingForm.Actions>
|
|
191
|
+
* </>
|
|
192
|
+
* )}
|
|
193
|
+
* </CoreBookingForm.Root>
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export declare function Actions(props: ActionsProps): React.ReactNode;
|
|
197
|
+
/**
|
|
198
|
+
* Props for BookingForm.LoadError component
|
|
199
|
+
*/
|
|
200
|
+
export interface LoadErrorProps {
|
|
201
|
+
/** The error that was caught */
|
|
202
|
+
error: Error;
|
|
203
|
+
/** Whether to render as a child component */
|
|
204
|
+
asChild?: boolean;
|
|
205
|
+
/** Custom content to render. If not provided, displays the error message. */
|
|
206
|
+
children?: React.ReactNode;
|
|
207
|
+
/** Additional CSS class name */
|
|
208
|
+
className?: string;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Component to display when BookingForm fails to load due to configuration errors.
|
|
212
|
+
* This component should be used in an error boundary to catch BookingFormConfigurationError.
|
|
213
|
+
*
|
|
214
|
+
* @component
|
|
215
|
+
* @example
|
|
216
|
+
* ```tsx
|
|
217
|
+
* import * as CoreBookingForm from '@wix/headless-bookings/react/core/booking-form';
|
|
218
|
+
*
|
|
219
|
+
* // Default usage with className
|
|
220
|
+
* <CoreBookingForm.LoadError error={error} className="text-destructive p-4" />
|
|
221
|
+
*
|
|
222
|
+
* // Custom content
|
|
223
|
+
* <CoreBookingForm.LoadError error={error}>
|
|
224
|
+
* <div className="error-container">
|
|
225
|
+
* <h3>Error Loading Form</h3>
|
|
226
|
+
* <p>Please try again later.</p>
|
|
227
|
+
* </div>
|
|
228
|
+
* </CoreBookingForm.LoadError>
|
|
229
|
+
*
|
|
230
|
+
* // With asChild for custom components
|
|
231
|
+
* <CoreBookingForm.LoadError error={error} asChild>
|
|
232
|
+
* <CustomErrorComponent />
|
|
233
|
+
* </CoreBookingForm.LoadError>
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
export declare const LoadError: React.ForwardRefExoticComponent<LoadErrorProps & React.RefAttributes<HTMLElement>>;
|
|
237
|
+
/**
|
|
238
|
+
* Type guard to check if an error is a BookingFormConfigurationError
|
|
239
|
+
*/
|
|
240
|
+
export declare function isBookingFormConfigurationError(error: unknown): error is BookingFormConfigurationError;
|
|
241
|
+
export { BookingFormConfigurationError };
|
|
@@ -1,58 +1,42 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Core BookingForm Component
|
|
4
|
+
*
|
|
5
|
+
* Provides low-level access to booking form functionality via render props.
|
|
6
|
+
* Wraps CoreForm.Root and provides merged field map with booking defaults.
|
|
7
|
+
*
|
|
8
|
+
* Supports two modes:
|
|
9
|
+
* 1. Client-side: Provide formId to load the form dynamically
|
|
10
|
+
* 2. SSR/SSG: Provide a pre-loaded form object for server-side rendering
|
|
11
|
+
*/
|
|
12
|
+
import * as React from 'react';
|
|
13
|
+
import { useMemo, useRef, useCallback } from 'react';
|
|
3
14
|
import { WixServices, useService } from '@wix/services-manager-react';
|
|
4
15
|
import { createServicesMap } from '@wix/services-manager';
|
|
5
|
-
import {
|
|
16
|
+
import { UniqueFieldSuffixContextProvider, } from '@wix/form-public';
|
|
17
|
+
import { BookingFormServiceDefinition, BookingFormService, BookingFormConfigurationError, BOOKING_FORM_NAMESPACE, extractFormIdFromConfig, } from '../../../services/booking-form/booking-form.js';
|
|
18
|
+
import { BookingServiceDefinition } from '../../../services/booking/booking.js';
|
|
6
19
|
import { FormServiceDefinition, FormService, } from '@wix/headless-forms/services';
|
|
7
|
-
// TODO: remove this when form will be updated to support optional field map
|
|
8
|
-
const DEFAULT_BOOKING_FIELD_MAP = {
|
|
9
|
-
TEXT_INPUT: () => null,
|
|
10
|
-
TEXT_AREA: () => null,
|
|
11
|
-
PHONE_INPUT: () => null,
|
|
12
|
-
EMAIL_INPUT: () => null,
|
|
13
|
-
NUMBER_INPUT: () => null,
|
|
14
|
-
CHECKBOX: () => null,
|
|
15
|
-
DROPDOWN: () => null,
|
|
16
|
-
DATE_PICKER: () => null,
|
|
17
|
-
SUBMIT_BUTTON: () => null,
|
|
18
|
-
RADIO_GROUP: () => null,
|
|
19
|
-
CHECKBOX_GROUP: () => null,
|
|
20
|
-
DATE_INPUT: () => null,
|
|
21
|
-
DATE_TIME_INPUT: () => null,
|
|
22
|
-
TIME_INPUT: () => null,
|
|
23
|
-
FILE_UPLOAD: () => null,
|
|
24
|
-
SIGNATURE: () => null,
|
|
25
|
-
TEXT: () => null,
|
|
26
|
-
MULTILINE_ADDRESS: () => null,
|
|
27
|
-
RATING_INPUT: () => null,
|
|
28
|
-
TAGS: () => null,
|
|
29
|
-
PRODUCT_LIST: () => null,
|
|
30
|
-
FIXED_PAYMENT: () => null,
|
|
31
|
-
PAYMENT_INPUT: () => null,
|
|
32
|
-
DONATION: () => null,
|
|
33
|
-
APPOINTMENT: () => null,
|
|
34
|
-
IMAGE_CHOICE: () => null,
|
|
35
|
-
};
|
|
36
20
|
// ============================================================================
|
|
37
21
|
// Component
|
|
38
22
|
// ============================================================================
|
|
39
23
|
/**
|
|
40
24
|
* Extracts the form ID from props (either directly or from form object)
|
|
25
|
+
* Returns undefined if formId should be extracted from services by the service layer
|
|
41
26
|
*/
|
|
42
27
|
function extractFormIdFromProps(props) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return formId;
|
|
28
|
+
// Convert props to config-like object for the utility
|
|
29
|
+
const config = props.form
|
|
30
|
+
? { form: props.form }
|
|
31
|
+
: props.formId
|
|
32
|
+
? { formId: props.formId }
|
|
33
|
+
: undefined;
|
|
34
|
+
const formId = extractFormIdFromConfig(config);
|
|
35
|
+
// If form was provided but we couldn't extract formId, that's an error
|
|
36
|
+
if (props.form && !formId) {
|
|
37
|
+
throw new Error('BookingForm: Could not extract form ID from provided form object.');
|
|
54
38
|
}
|
|
55
|
-
|
|
39
|
+
return formId;
|
|
56
40
|
}
|
|
57
41
|
/**
|
|
58
42
|
* Core component that provides booking form data via render props.
|
|
@@ -123,41 +107,75 @@ function extractFormIdFromProps(props) {
|
|
|
123
107
|
* ```
|
|
124
108
|
*/
|
|
125
109
|
export function Root(props) {
|
|
126
|
-
const { additionalMetadata, fieldMap
|
|
110
|
+
const { serviceIds, additionalMetadata, fieldMap, children } = props;
|
|
127
111
|
// Determine if we have a pre-loaded form
|
|
128
112
|
const hasPreloadedForm = 'form' in props && props.form;
|
|
129
113
|
const preloadedForm = hasPreloadedForm ? props.form : null;
|
|
130
114
|
// Extract formId from props
|
|
131
|
-
const
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
115
|
+
const propsFormId = extractFormIdFromProps(props);
|
|
116
|
+
// Try to get BookingService if available (to extract formId and serviceIds from services)
|
|
117
|
+
let bookingService = null;
|
|
118
|
+
try {
|
|
119
|
+
bookingService = useService(BookingServiceDefinition);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// BookingService not available - that's ok if formId and serviceIds were provided explicitly
|
|
123
|
+
bookingService = null;
|
|
124
|
+
}
|
|
125
|
+
// Determine final formId: use props formId if provided, otherwise extract from services
|
|
126
|
+
let formId = propsFormId;
|
|
127
|
+
if (!formId && bookingService) {
|
|
128
|
+
const serviceSelections = bookingService.serviceSelections.get();
|
|
129
|
+
const services = serviceSelections.length
|
|
130
|
+
? serviceSelections.map((s) => s.service)
|
|
131
|
+
: [];
|
|
132
|
+
formId = services.find((s) => s.form?._id)?.form?._id;
|
|
133
|
+
}
|
|
134
|
+
// Determine final serviceIds: use props serviceIds if provided, otherwise extract from services
|
|
135
|
+
let resolvedServiceIds = serviceIds;
|
|
136
|
+
if (!resolvedServiceIds && bookingService) {
|
|
137
|
+
const serviceSelections = bookingService.serviceSelections.get();
|
|
138
|
+
resolvedServiceIds = serviceSelections
|
|
139
|
+
.map((s) => s.service._id)
|
|
140
|
+
.filter((id) => Boolean(id));
|
|
141
|
+
}
|
|
142
|
+
// Merge serviceIds into additionalMetadata
|
|
143
|
+
const mergedMetadata = {
|
|
144
|
+
...additionalMetadata,
|
|
145
|
+
...(resolvedServiceIds &&
|
|
146
|
+
resolvedServiceIds.length > 0 && { serviceIds: resolvedServiceIds }),
|
|
147
|
+
};
|
|
137
148
|
// Build formServiceConfig for FormService
|
|
138
|
-
// If we have a pre-loaded form, pass it directly
|
|
149
|
+
// If we have a pre-loaded form, pass it directly
|
|
150
|
+
// If we have formId, pass it with namespace and metadata
|
|
139
151
|
const formServiceConfig = useMemo(() => {
|
|
140
152
|
if (preloadedForm) {
|
|
141
153
|
return { form: preloadedForm };
|
|
142
154
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
155
|
+
// Only provide config if formId is available
|
|
156
|
+
if (formId) {
|
|
157
|
+
return {
|
|
158
|
+
formId,
|
|
159
|
+
namespace: BOOKING_FORM_NAMESPACE,
|
|
160
|
+
...(Object.keys(mergedMetadata).length > 0 && {
|
|
161
|
+
additionalMetadata: mergedMetadata,
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}, [formId, mergedMetadata, preloadedForm]);
|
|
149
166
|
// Build bookingFormServiceConfig
|
|
150
167
|
const bookingFormServiceConfig = useMemo(() => {
|
|
151
168
|
if (preloadedForm) {
|
|
152
169
|
return { form: preloadedForm };
|
|
153
170
|
}
|
|
154
|
-
|
|
155
|
-
|
|
171
|
+
// formId is optional - service will extract from services if not provided
|
|
172
|
+
return formId ? { formId, serviceIds } : undefined;
|
|
173
|
+
}, [formId, preloadedForm, serviceIds]);
|
|
156
174
|
// Both FormService and BookingFormService are added to the same WixServices context
|
|
157
175
|
// This allows BookingFormService to access FormService via getService internally
|
|
158
176
|
return (_jsx(WixServices, { servicesMap: createServicesMap()
|
|
159
177
|
.addService(FormServiceDefinition, FormService, formServiceConfig)
|
|
160
|
-
.addService(BookingFormServiceDefinition, BookingFormService, bookingFormServiceConfig), children: _jsx(
|
|
178
|
+
.addService(BookingFormServiceDefinition, BookingFormService, bookingFormServiceConfig), children: _jsx(UniqueFieldSuffixContextProvider, { children: _jsx(BookingFormContent, { fields: fieldMap, children: children }) }) }));
|
|
161
179
|
}
|
|
162
180
|
/**
|
|
163
181
|
* Internal component that consumes BookingFormService and provides render props.
|
|
@@ -165,17 +183,139 @@ export function Root(props) {
|
|
|
165
183
|
*
|
|
166
184
|
* Note: BookingFormService internally accesses FormService to get formValues,
|
|
167
185
|
* so we only need to consume BookingFormService here.
|
|
186
|
+
*
|
|
187
|
+
* Also automatically syncs form submission to BookingService when available,
|
|
188
|
+
* so Booking.Actions.Book can check canBook() correctly.
|
|
189
|
+
*
|
|
190
|
+
* Note: BookingService must be available in a parent WixServices context
|
|
191
|
+
* (typically provided by Booking.Root) for the sync to work. If BookingService
|
|
192
|
+
* is not available, useService will throw. This is expected since Booking.Actions.Book
|
|
193
|
+
* requires Booking.Root to be in the component tree.
|
|
168
194
|
*/
|
|
169
|
-
function BookingFormContent({
|
|
195
|
+
function BookingFormContent({ fields, children, }) {
|
|
170
196
|
const bookingFormService = useService(BookingFormServiceDefinition);
|
|
197
|
+
// Try to get BookingService from parent context (provided by Booking.Root)
|
|
198
|
+
// Note: useService must be called unconditionally per React hooks rules.
|
|
199
|
+
// If BookingService is not available, useService will throw an error.
|
|
200
|
+
// This is expected - Booking.Actions.Book requires Booking.Root to be in the tree.
|
|
201
|
+
// We use a wrapper component pattern to handle this gracefully.
|
|
202
|
+
// For now, we'll always attempt to get it and let it throw if not available
|
|
203
|
+
// The user should wrap BookingForm with Booking.Root when using Booking.Actions.Book
|
|
204
|
+
const bookingService = useService(BookingServiceDefinition);
|
|
205
|
+
// Create internal ref for form handle
|
|
206
|
+
const formRef = useRef(null);
|
|
207
|
+
// Create validate action that uses the form ref
|
|
208
|
+
const validate = useCallback(() => {
|
|
209
|
+
return formRef.current?.validate() ?? Promise.resolve(false);
|
|
210
|
+
}, []);
|
|
211
|
+
// Wrap setFormSubmission to also sync to BookingService when available
|
|
212
|
+
const setFormSubmission = useCallback((formValues) => {
|
|
213
|
+
// Set in BookingFormService (always)
|
|
214
|
+
bookingFormService.setFormSubmission(formValues);
|
|
215
|
+
// Also set in BookingService if available (for canBook check)
|
|
216
|
+
// This allows Booking.Actions.Book to work correctly
|
|
217
|
+
if (bookingService) {
|
|
218
|
+
bookingService.actions.setFormSubmission(formValues);
|
|
219
|
+
}
|
|
220
|
+
}, [bookingFormService, bookingService]);
|
|
171
221
|
const formSubmission = bookingFormService.formSubmission.get();
|
|
172
222
|
const formValues = bookingFormService.formValues.get();
|
|
223
|
+
// Get formId from service (it extracts from services if not provided in config)
|
|
224
|
+
const resolvedFormId = bookingFormService.formId.get();
|
|
173
225
|
return children({
|
|
174
|
-
formId,
|
|
226
|
+
formId: resolvedFormId,
|
|
175
227
|
formValues,
|
|
176
228
|
formSubmission,
|
|
177
|
-
setFormSubmission
|
|
178
|
-
validateFormSubmission:
|
|
229
|
+
setFormSubmission,
|
|
230
|
+
validateFormSubmission: validate,
|
|
231
|
+
formRef,
|
|
179
232
|
fields,
|
|
180
233
|
});
|
|
181
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Core component that provides booking form actions via render props.
|
|
237
|
+
* Must be used within BookingForm.Root context.
|
|
238
|
+
*
|
|
239
|
+
* @component
|
|
240
|
+
* @example
|
|
241
|
+
* ```tsx
|
|
242
|
+
* <CoreBookingForm.Root formId="form-123">
|
|
243
|
+
* {({ fields }) => (
|
|
244
|
+
* <>
|
|
245
|
+
* <Form.Fields fieldMap={fields} />
|
|
246
|
+
* <CoreBookingForm.Actions>
|
|
247
|
+
* {({ validateFormSubmission, formId }) => (
|
|
248
|
+
* <button onClick={async () => {
|
|
249
|
+
* const result = await validateFormSubmission();
|
|
250
|
+
* console.log('Validation result:', result);
|
|
251
|
+
* }}>
|
|
252
|
+
* Validate Form
|
|
253
|
+
* </button>
|
|
254
|
+
* )}
|
|
255
|
+
* </CoreBookingForm.Actions>
|
|
256
|
+
* </>
|
|
257
|
+
* )}
|
|
258
|
+
* </CoreBookingForm.Root>
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
export function Actions(props) {
|
|
262
|
+
const bookingFormService = useService(BookingFormServiceDefinition);
|
|
263
|
+
const formSubmission = bookingFormService.formSubmission.get();
|
|
264
|
+
const formValues = bookingFormService.formValues.get();
|
|
265
|
+
const formId = bookingFormService.formId.get();
|
|
266
|
+
return props.children({
|
|
267
|
+
validateFormSubmission: bookingFormService.validateFormSubmission,
|
|
268
|
+
formId,
|
|
269
|
+
formValues,
|
|
270
|
+
formSubmission,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Component to display when BookingForm fails to load due to configuration errors.
|
|
275
|
+
* This component should be used in an error boundary to catch BookingFormConfigurationError.
|
|
276
|
+
*
|
|
277
|
+
* @component
|
|
278
|
+
* @example
|
|
279
|
+
* ```tsx
|
|
280
|
+
* import * as CoreBookingForm from '@wix/headless-bookings/react/core/booking-form';
|
|
281
|
+
*
|
|
282
|
+
* // Default usage with className
|
|
283
|
+
* <CoreBookingForm.LoadError error={error} className="text-destructive p-4" />
|
|
284
|
+
*
|
|
285
|
+
* // Custom content
|
|
286
|
+
* <CoreBookingForm.LoadError error={error}>
|
|
287
|
+
* <div className="error-container">
|
|
288
|
+
* <h3>Error Loading Form</h3>
|
|
289
|
+
* <p>Please try again later.</p>
|
|
290
|
+
* </div>
|
|
291
|
+
* </CoreBookingForm.LoadError>
|
|
292
|
+
*
|
|
293
|
+
* // With asChild for custom components
|
|
294
|
+
* <CoreBookingForm.LoadError error={error} asChild>
|
|
295
|
+
* <CustomErrorComponent />
|
|
296
|
+
* </CoreBookingForm.LoadError>
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
export const LoadError = React.forwardRef((props, ref) => {
|
|
300
|
+
const { error, asChild, children, className, ...otherProps } = props;
|
|
301
|
+
const errorData = { error, message: error.message };
|
|
302
|
+
if (asChild && React.isValidElement(children)) {
|
|
303
|
+
return React.cloneElement(children, {
|
|
304
|
+
ref,
|
|
305
|
+
className,
|
|
306
|
+
'data-testid': 'booking-form-load-error',
|
|
307
|
+
role: 'alert',
|
|
308
|
+
...errorData,
|
|
309
|
+
...otherProps,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return (_jsx("div", { ref: ref, className: className, "data-testid": "booking-form-load-error", role: "alert", ...otherProps, children: children ?? error.message }));
|
|
313
|
+
});
|
|
314
|
+
/**
|
|
315
|
+
* Type guard to check if an error is a BookingFormConfigurationError
|
|
316
|
+
*/
|
|
317
|
+
export function isBookingFormConfigurationError(error) {
|
|
318
|
+
return error instanceof BookingFormConfigurationError;
|
|
319
|
+
}
|
|
320
|
+
// Re-export the error class for convenience
|
|
321
|
+
export { BookingFormConfigurationError };
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import { AsChildChildren } from '@wix/headless-utils/react';
|
|
10
10
|
import type { TimeSlot as TimeSlotType } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
11
|
+
import * as CoreTimeSlot from '../core/time-slot-list/TimeSlot.js';
|
|
11
12
|
export type { StaffMemberData } from '../core/time-slot-list/TimeSlot.js';
|
|
12
13
|
/**
|
|
13
14
|
* Props for TimeSlot.Root component
|
|
@@ -131,6 +132,7 @@ export interface SelectProps {
|
|
|
131
132
|
}) => React.ReactNode);
|
|
132
133
|
className?: string;
|
|
133
134
|
label?: string;
|
|
135
|
+
onClicked?: (timeSlot: TimeSlotType) => void;
|
|
134
136
|
}
|
|
135
137
|
/**
|
|
136
138
|
* Button to select this time slot.
|
|
@@ -150,6 +152,13 @@ export interface SelectProps {
|
|
|
150
152
|
* <button className="btn-primary">Book this slot</button>
|
|
151
153
|
* </TimeSlot.Actions.Select>
|
|
152
154
|
*
|
|
155
|
+
* // With onClicked callback for navigation
|
|
156
|
+
* <TimeSlot.Actions.Select
|
|
157
|
+
* onClicked={(timeSlot) => {
|
|
158
|
+
* router.push(`/booking/confirm/${timeSlot.localStartDate}`);
|
|
159
|
+
* }}
|
|
160
|
+
* />
|
|
161
|
+
*
|
|
153
162
|
* // Using render prop pattern
|
|
154
163
|
* <TimeSlot.Actions.Select>
|
|
155
164
|
* {({ isSelected, bookable, onClick }) => (
|
|
@@ -171,6 +180,7 @@ export interface ClearStaffSelectionProps {
|
|
|
171
180
|
}) => React.ReactNode);
|
|
172
181
|
className?: string;
|
|
173
182
|
label?: string;
|
|
183
|
+
onClicked?: (timeSlot: TimeSlotType) => void;
|
|
174
184
|
}
|
|
175
185
|
/**
|
|
176
186
|
* Button to clear staff selection while keeping the slot selected.
|
|
@@ -191,6 +201,13 @@ export interface ClearStaffSelectionProps {
|
|
|
191
201
|
* <button className="btn-secondary">Clear staff</button>
|
|
192
202
|
* </TimeSlot.Actions.ClearStaffSelection>
|
|
193
203
|
*
|
|
204
|
+
* // With onClicked callback
|
|
205
|
+
* <TimeSlot.Actions.ClearStaffSelection
|
|
206
|
+
* onClicked={(timeSlot) => {
|
|
207
|
+
* console.log('Staff selection cleared for', timeSlot.localStartDate);
|
|
208
|
+
* }}
|
|
209
|
+
* />
|
|
210
|
+
*
|
|
194
211
|
* // Using render prop pattern
|
|
195
212
|
* <TimeSlot.Actions.ClearStaffSelection>
|
|
196
213
|
* {({ onClick }) => <button onClick={onClick}>Change selection</button>}
|
|
@@ -324,6 +341,7 @@ export interface SelectStaffMemberProps {
|
|
|
324
341
|
}) => React.ReactNode);
|
|
325
342
|
className?: string;
|
|
326
343
|
label?: string;
|
|
344
|
+
onClicked?: (staffMember: CoreTimeSlot.StaffMemberData) => void;
|
|
327
345
|
}
|
|
328
346
|
/**
|
|
329
347
|
* Button to select this staff member.
|
|
@@ -343,6 +361,13 @@ export interface SelectStaffMemberProps {
|
|
|
343
361
|
* <button className="btn-primary">Choose Staff</button>
|
|
344
362
|
* </TimeSlot.StaffMember.Actions.Select>
|
|
345
363
|
*
|
|
364
|
+
* // With onClicked callback
|
|
365
|
+
* <TimeSlot.StaffMember.Actions.Select
|
|
366
|
+
* onClicked={(staffMember) => {
|
|
367
|
+
* console.log('Selected staff:', staffMember.name);
|
|
368
|
+
* }}
|
|
369
|
+
* />
|
|
370
|
+
*
|
|
346
371
|
* // Using render prop pattern
|
|
347
372
|
* <TimeSlot.StaffMember.Actions.Select>
|
|
348
373
|
* {({ isSelected, onClick }) => (
|
|
@@ -139,6 +139,13 @@ Duration.displayName = 'TimeSlot.Duration';
|
|
|
139
139
|
* <button className="btn-primary">Book this slot</button>
|
|
140
140
|
* </TimeSlot.Actions.Select>
|
|
141
141
|
*
|
|
142
|
+
* // With onClicked callback for navigation
|
|
143
|
+
* <TimeSlot.Actions.Select
|
|
144
|
+
* onClicked={(timeSlot) => {
|
|
145
|
+
* router.push(`/booking/confirm/${timeSlot.localStartDate}`);
|
|
146
|
+
* }}
|
|
147
|
+
* />
|
|
148
|
+
*
|
|
142
149
|
* // Using render prop pattern
|
|
143
150
|
* <TimeSlot.Actions.Select>
|
|
144
151
|
* {({ isSelected, bookable, onClick }) => (
|
|
@@ -150,14 +157,18 @@ Duration.displayName = 'TimeSlot.Duration';
|
|
|
150
157
|
* ```
|
|
151
158
|
*/
|
|
152
159
|
export const Select = React.forwardRef((props, ref) => {
|
|
153
|
-
const { asChild, children, className, label = 'Select' } = props;
|
|
154
|
-
return (_jsx(CoreTimeSlot.Actions, { children: ({ selectTimeSlot, isSelected, bookable }) => {
|
|
160
|
+
const { asChild, children, className, label = 'Select', onClicked } = props;
|
|
161
|
+
return (_jsx(CoreTimeSlot.Actions, { children: ({ selectTimeSlot, isSelected, bookable, timeSlot }) => {
|
|
162
|
+
const handleClick = () => {
|
|
163
|
+
selectTimeSlot();
|
|
164
|
+
onClicked?.(timeSlot);
|
|
165
|
+
};
|
|
155
166
|
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.timeSlotActionSelect, "data-selected": isSelected, "data-bookable": bookable, customElement: children, customElementProps: {
|
|
156
|
-
onClick:
|
|
167
|
+
onClick: handleClick,
|
|
157
168
|
disabled: !bookable,
|
|
158
169
|
isSelected,
|
|
159
170
|
bookable,
|
|
160
|
-
}, children: _jsx("button", { onClick:
|
|
171
|
+
}, children: _jsx("button", { onClick: handleClick, disabled: !bookable, children: label }) }));
|
|
161
172
|
} }));
|
|
162
173
|
});
|
|
163
174
|
Select.displayName = 'TimeSlot.Actions.Select';
|
|
@@ -180,6 +191,13 @@ Select.displayName = 'TimeSlot.Actions.Select';
|
|
|
180
191
|
* <button className="btn-secondary">Clear staff</button>
|
|
181
192
|
* </TimeSlot.Actions.ClearStaffSelection>
|
|
182
193
|
*
|
|
194
|
+
* // With onClicked callback
|
|
195
|
+
* <TimeSlot.Actions.ClearStaffSelection
|
|
196
|
+
* onClicked={(timeSlot) => {
|
|
197
|
+
* console.log('Staff selection cleared for', timeSlot.localStartDate);
|
|
198
|
+
* }}
|
|
199
|
+
* />
|
|
200
|
+
*
|
|
183
201
|
* // Using render prop pattern
|
|
184
202
|
* <TimeSlot.Actions.ClearStaffSelection>
|
|
185
203
|
* {({ onClick }) => <button onClick={onClick}>Change selection</button>}
|
|
@@ -187,14 +205,18 @@ Select.displayName = 'TimeSlot.Actions.Select';
|
|
|
187
205
|
* ```
|
|
188
206
|
*/
|
|
189
207
|
export const ClearStaffSelection = React.forwardRef((props, ref) => {
|
|
190
|
-
const { asChild, children, className, label = 'Change' } = props;
|
|
191
|
-
return (_jsx(CoreTimeSlot.Actions, { children: ({ clearStaffSelection }) => {
|
|
208
|
+
const { asChild, children, className, label = 'Change', onClicked } = props;
|
|
209
|
+
return (_jsx(CoreTimeSlot.Actions, { children: ({ clearStaffSelection, timeSlot }) => {
|
|
192
210
|
if (!clearStaffSelection) {
|
|
193
211
|
return null;
|
|
194
212
|
}
|
|
213
|
+
const handleClick = () => {
|
|
214
|
+
clearStaffSelection();
|
|
215
|
+
onClicked?.(timeSlot);
|
|
216
|
+
};
|
|
195
217
|
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.timeSlotActionClearStaffSelection, customElement: children, customElementProps: {
|
|
196
|
-
onClick:
|
|
197
|
-
}, children: _jsx("button", { onClick:
|
|
218
|
+
onClick: handleClick,
|
|
219
|
+
}, children: _jsx("button", { onClick: handleClick, children: label }) }));
|
|
198
220
|
} }));
|
|
199
221
|
});
|
|
200
222
|
ClearStaffSelection.displayName = 'TimeSlot.Actions.ClearStaffSelection';
|
|
@@ -324,6 +346,13 @@ StaffMemberName.displayName = 'TimeSlot.StaffMember.Name';
|
|
|
324
346
|
* <button className="btn-primary">Choose Staff</button>
|
|
325
347
|
* </TimeSlot.StaffMember.Actions.Select>
|
|
326
348
|
*
|
|
349
|
+
* // With onClicked callback
|
|
350
|
+
* <TimeSlot.StaffMember.Actions.Select
|
|
351
|
+
* onClicked={(staffMember) => {
|
|
352
|
+
* console.log('Selected staff:', staffMember.name);
|
|
353
|
+
* }}
|
|
354
|
+
* />
|
|
355
|
+
*
|
|
327
356
|
* // Using render prop pattern
|
|
328
357
|
* <TimeSlot.StaffMember.Actions.Select>
|
|
329
358
|
* {({ isSelected, onClick }) => (
|
|
@@ -335,11 +364,17 @@ StaffMemberName.displayName = 'TimeSlot.StaffMember.Name';
|
|
|
335
364
|
* ```
|
|
336
365
|
*/
|
|
337
366
|
export const SelectStaffMember = React.forwardRef((props, ref) => {
|
|
338
|
-
const { asChild, children, className, label = 'Select' } = props;
|
|
339
|
-
return (_jsx(CoreTimeSlot.StaffMemberActions, { children: ({ selectStaffMember, isSelected }) =>
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
367
|
+
const { asChild, children, className, label = 'Select', onClicked } = props;
|
|
368
|
+
return (_jsx(CoreTimeSlot.StaffMemberActions, { children: ({ selectStaffMember, isSelected, staffMember }) => {
|
|
369
|
+
const handleClick = () => {
|
|
370
|
+
selectStaffMember();
|
|
371
|
+
onClicked?.(staffMember);
|
|
372
|
+
};
|
|
373
|
+
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.timeSlotStaffMemberActionSelect, "data-selected": isSelected, customElement: children, customElementProps: {
|
|
374
|
+
onClick: handleClick,
|
|
375
|
+
isSelected,
|
|
376
|
+
}, children: _jsx("button", { onClick: handleClick, children: label }) }));
|
|
377
|
+
} }));
|
|
343
378
|
});
|
|
344
379
|
SelectStaffMember.displayName = 'TimeSlot.StaffMember.Actions.Select';
|
|
345
380
|
/**
|