@wix/headless-bookings 0.0.46 → 0.0.47

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.
@@ -15,8 +15,14 @@ import { ServiceServiceDefinition, ServiceService, } from '../../../services/ser
15
15
  */
16
16
  function RootContent({ children, }) {
17
17
  const serviceService = useService(ServiceServiceDefinition);
18
+ const service = serviceService.serviceSignal.get();
18
19
  const selected = serviceService.selectedSignal.get();
19
20
  const bookable = serviceService.bookableSignal.get();
21
+ // In "from booking signal" mode, render null when no service is selected
22
+ // The signal will trigger a re-render when a service is selected
23
+ if (!service || !service._id) {
24
+ return null;
25
+ }
20
26
  if (typeof children === 'function') {
21
27
  return children({ selected, bookable });
22
28
  }
@@ -43,14 +43,32 @@ type ServiceRootWithSlug = ServiceRootBaseProps & {
43
43
  serviceId?: never;
44
44
  serviceSlug: string;
45
45
  };
46
+ /**
47
+ * Props for Service.Root that reads from BookingService's selected service.
48
+ * When used without service config, the component reads from the booking signal.
49
+ * Requires BookingService to be available in the context.
50
+ */
51
+ type ServiceRootFromBooking = ServiceRootBaseProps & {
52
+ service?: never;
53
+ serviceId?: never;
54
+ serviceSlug?: never;
55
+ };
46
56
  /**
47
57
  * Props for Service.Root component.
48
- * Requires exactly one of: service, serviceId, or serviceSlug.
58
+ * Accepts one of:
59
+ * - service: Pre-loaded service object (SSR pattern)
60
+ * - serviceId: Service ID for dynamic loading
61
+ * - serviceSlug: Service slug for dynamic loading
62
+ * - No service config: Reads from BookingService's selected service
49
63
  */
50
- export type ServiceRootProps = ServiceRootWithService | ServiceRootWithId | ServiceRootWithSlug;
64
+ export type ServiceRootProps = ServiceRootWithService | ServiceRootWithId | ServiceRootWithSlug | ServiceRootFromBooking;
51
65
  /**
52
66
  * Root component that provides service context to the entire app.
53
- * Requires exactly one of: service, serviceId, or serviceSlug.
67
+ * Can be used with:
68
+ * - service: Pre-loaded service object (SSR pattern)
69
+ * - serviceId: Service ID for dynamic loading
70
+ * - serviceSlug: Service slug for dynamic loading
71
+ * - No config: Reads from BookingService's selected service (requires BookingService)
54
72
  *
55
73
  * @order 1
56
74
  * @component
@@ -87,6 +105,17 @@ export type ServiceRootProps = ServiceRootWithService | ServiceRootWithId | Serv
87
105
  * </Service.Root>
88
106
  * );
89
107
  * }
108
+ *
109
+ * // Reading from BookingService's selected service
110
+ * // (requires Booking.Root parent context)
111
+ * function SelectedServiceDisplay() {
112
+ * return (
113
+ * <Service.Root>
114
+ * <Service.Name />
115
+ * <Service.Price />
116
+ * </Service.Root>
117
+ * );
118
+ * }
90
119
  * ```
91
120
  */
92
121
  export declare const Root: React.ForwardRefExoticComponent<ServiceRootProps & React.RefAttributes<HTMLElement>>;
@@ -49,7 +49,11 @@ var TestIds;
49
49
  })(TestIds || (TestIds = {}));
50
50
  /**
51
51
  * Root component that provides service context to the entire app.
52
- * Requires exactly one of: service, serviceId, or serviceSlug.
52
+ * Can be used with:
53
+ * - service: Pre-loaded service object (SSR pattern)
54
+ * - serviceId: Service ID for dynamic loading
55
+ * - serviceSlug: Service slug for dynamic loading
56
+ * - No config: Reads from BookingService's selected service (requires BookingService)
53
57
  *
54
58
  * @order 1
55
59
  * @component
@@ -86,11 +90,30 @@ var TestIds;
86
90
  * </Service.Root>
87
91
  * );
88
92
  * }
93
+ *
94
+ * // Reading from BookingService's selected service
95
+ * // (requires Booking.Root parent context)
96
+ * function SelectedServiceDisplay() {
97
+ * return (
98
+ * <Service.Root>
99
+ * <Service.Name />
100
+ * <Service.Price />
101
+ * </Service.Root>
102
+ * );
103
+ * }
89
104
  * ```
90
105
  */
91
106
  export const Root = React.forwardRef((props, ref) => {
92
107
  const { children, service, serviceId, serviceSlug, appId, asChild, className, ...attrs } = props;
93
- return (_jsx(CoreService.Root, { serviceServiceConfig: { service, serviceId, serviceSlug, appId }, children: ({ selected, bookable }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceRoot, "data-selected": selected, "data-bookable": bookable, customElement: children, customElementProps: { selected, bookable }, ...attrs, children: children })) }));
108
+ // Build config - when no service config is provided, pass empty object
109
+ // to trigger "from booking signal" mode in ServiceService
110
+ const serviceServiceConfig = {
111
+ service,
112
+ serviceId,
113
+ serviceSlug,
114
+ appId,
115
+ };
116
+ return (_jsx(CoreService.Root, { serviceServiceConfig: serviceServiceConfig, children: ({ selected, bookable }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceRoot, "data-selected": selected, "data-bookable": bookable, customElement: children, customElementProps: { selected, bookable }, ...attrs, children: children })) }));
94
117
  });
95
118
  Root.displayName = 'Service.Root';
96
119
  /**
@@ -47,19 +47,28 @@ export declare const Root: React.ForwardRefExoticComponent<ServiceListRootProps
47
47
  */
48
48
  export interface ServicesProps {
49
49
  children: React.ReactNode;
50
+ /** Content to display when no services are available */
50
51
  emptyState?: React.ReactNode;
52
+ /** Content to display when loading and no services are available */
53
+ loadingState?: React.ReactNode;
54
+ /** Content to display when an error occurs and no services are available */
55
+ errorState?: React.ReactNode;
51
56
  infiniteScroll?: boolean;
52
57
  pageSize?: number;
53
58
  className?: string;
54
59
  }
55
60
  /**
56
- * Container for the services list with empty state support.
61
+ * Container for the services list with empty, loading, and error state support.
57
62
  * Wraps GenericList.Items. Follows List Container Level pattern.
58
63
  *
59
64
  * @component
60
65
  * @example
61
66
  * ```tsx
62
- * <ServiceList.Services emptyState={<div>No services found</div>}>
67
+ * <ServiceList.Services
68
+ * emptyState={<div>No services found</div>}
69
+ * loadingState={<div>Loading services...</div>}
70
+ * errorState={<div>Failed to load services</div>}
71
+ * >
63
72
  * <ServiceList.ServiceRepeater>
64
73
  * <Service.Name />
65
74
  * <Service.Price />
@@ -73,22 +73,26 @@ const DEFAULT_PAGE_SIZE = 100;
73
73
  */
74
74
  const RootContent = React.forwardRef((props, ref) => {
75
75
  const { children, className, variant, pageSize = DEFAULT_PAGE_SIZE } = props;
76
- return (_jsx(CoreServiceList.Services, { children: ({ services: servicesList, isLoading, hasMore, loadMore }) => {
76
+ return (_jsx(CoreServiceList.Services, { children: ({ services: servicesList, isLoading, hasMore, loadMore, error }) => {
77
77
  const items = servicesList.map((service) => ({
78
78
  ...service,
79
79
  id: service._id,
80
80
  }));
81
- return (_jsx(GenericList.Root, { items: items, loadMore: () => loadMore(pageSize), hasMore: hasMore, isLoading: isLoading, className: className, ref: ref, "data-testid": TestIds.serviceListRoot, variant: variant, children: children }));
81
+ return (_jsx(GenericList.Root, { items: items, loadMore: () => loadMore(pageSize), hasMore: hasMore, isLoading: isLoading, error: error, className: className, ref: ref, "data-testid": TestIds.serviceListRoot, variant: variant, children: children }));
82
82
  } }));
83
83
  });
84
84
  /**
85
- * Container for the services list with empty state support.
85
+ * Container for the services list with empty, loading, and error state support.
86
86
  * Wraps GenericList.Items. Follows List Container Level pattern.
87
87
  *
88
88
  * @component
89
89
  * @example
90
90
  * ```tsx
91
- * <ServiceList.Services emptyState={<div>No services found</div>}>
91
+ * <ServiceList.Services
92
+ * emptyState={<div>No services found</div>}
93
+ * loadingState={<div>Loading services...</div>}
94
+ * errorState={<div>Failed to load services</div>}
95
+ * >
92
96
  * <ServiceList.ServiceRepeater>
93
97
  * <Service.Name />
94
98
  * <Service.Price />
@@ -97,8 +101,8 @@ const RootContent = React.forwardRef((props, ref) => {
97
101
  * ```
98
102
  */
99
103
  export const Services = React.forwardRef((props, ref) => {
100
- const { children, ...otherProps } = props;
101
- return (_jsx(GenericList.Items, { ref: ref, "data-testid": TestIds.serviceListServices, ...otherProps, children: children }));
104
+ const { children, emptyState, loadingState, errorState, ...otherProps } = props;
105
+ return (_jsx(GenericList.Items, { ref: ref, "data-testid": TestIds.serviceListServices, emptyState: emptyState, loadingState: loadingState, errorState: errorState, ...otherProps, children: children }));
102
106
  });
103
107
  Services.displayName = 'ServiceList.Services';
104
108
  /**
@@ -54,6 +54,8 @@ export const ServiceServiceDefinition = defineService('service');
54
54
  */
55
55
  export const ServiceService = implementService.withConfig()(ServiceServiceDefinition, ({ getService, config }) => {
56
56
  const signalsService = getService(SignalsServiceDefinition);
57
+ // Determine if we're in "from booking signal" mode (no service config provided)
58
+ const noExplicitService = !config.service && !config.serviceId && !config.serviceSlug;
57
59
  // Try to get BookingService (optional dependency for showcase mode)
58
60
  let bookingService = null;
59
61
  try {
@@ -63,9 +65,27 @@ export const ServiceService = implementService.withConfig()(ServiceServiceDefini
63
65
  // BookingService not available - showcase mode
64
66
  bookingService = null;
65
67
  }
66
- // Initialize service signal - use empty service object if not provided
67
- const initialService = config.service || {};
68
- const serviceSignal = signalsService.signal(initialService);
68
+ // In "from booking signal" mode, BookingService must be available
69
+ if (noExplicitService && !bookingService) {
70
+ throw new Error('Service.Root requires either a service config (service, serviceId, or serviceSlug) or BookingService to be available');
71
+ }
72
+ // Initialize service signal
73
+ // In "from booking signal" mode, use a computed signal that reads from BookingService
74
+ let serviceSignal;
75
+ if (noExplicitService && bookingService) {
76
+ // Create a computed signal that reads from BookingService's first selected service
77
+ const computedServiceSignal = signalsService.computed(() => {
78
+ const selections = bookingService.serviceSelections.get();
79
+ return selections[0]?.service || null;
80
+ });
81
+ // Wrap computed signal as a Signal (read-only in practice for this mode)
82
+ serviceSignal = computedServiceSignal;
83
+ }
84
+ else {
85
+ // Standard mode: use provided service or empty object
86
+ const initialService = config.service || {};
87
+ serviceSignal = signalsService.signal(initialService);
88
+ }
69
89
  const isLoadingSignal = signalsService.signal(!!config.serviceId);
70
90
  const errorSignal = signalsService.signal(null);
71
91
  // Default appId to BOOKING_APP_ID if not provided
@@ -15,8 +15,14 @@ import { ServiceServiceDefinition, ServiceService, } from '../../../services/ser
15
15
  */
16
16
  function RootContent({ children, }) {
17
17
  const serviceService = useService(ServiceServiceDefinition);
18
+ const service = serviceService.serviceSignal.get();
18
19
  const selected = serviceService.selectedSignal.get();
19
20
  const bookable = serviceService.bookableSignal.get();
21
+ // In "from booking signal" mode, render null when no service is selected
22
+ // The signal will trigger a re-render when a service is selected
23
+ if (!service || !service._id) {
24
+ return null;
25
+ }
20
26
  if (typeof children === 'function') {
21
27
  return children({ selected, bookable });
22
28
  }
@@ -43,14 +43,32 @@ type ServiceRootWithSlug = ServiceRootBaseProps & {
43
43
  serviceId?: never;
44
44
  serviceSlug: string;
45
45
  };
46
+ /**
47
+ * Props for Service.Root that reads from BookingService's selected service.
48
+ * When used without service config, the component reads from the booking signal.
49
+ * Requires BookingService to be available in the context.
50
+ */
51
+ type ServiceRootFromBooking = ServiceRootBaseProps & {
52
+ service?: never;
53
+ serviceId?: never;
54
+ serviceSlug?: never;
55
+ };
46
56
  /**
47
57
  * Props for Service.Root component.
48
- * Requires exactly one of: service, serviceId, or serviceSlug.
58
+ * Accepts one of:
59
+ * - service: Pre-loaded service object (SSR pattern)
60
+ * - serviceId: Service ID for dynamic loading
61
+ * - serviceSlug: Service slug for dynamic loading
62
+ * - No service config: Reads from BookingService's selected service
49
63
  */
50
- export type ServiceRootProps = ServiceRootWithService | ServiceRootWithId | ServiceRootWithSlug;
64
+ export type ServiceRootProps = ServiceRootWithService | ServiceRootWithId | ServiceRootWithSlug | ServiceRootFromBooking;
51
65
  /**
52
66
  * Root component that provides service context to the entire app.
53
- * Requires exactly one of: service, serviceId, or serviceSlug.
67
+ * Can be used with:
68
+ * - service: Pre-loaded service object (SSR pattern)
69
+ * - serviceId: Service ID for dynamic loading
70
+ * - serviceSlug: Service slug for dynamic loading
71
+ * - No config: Reads from BookingService's selected service (requires BookingService)
54
72
  *
55
73
  * @order 1
56
74
  * @component
@@ -87,6 +105,17 @@ export type ServiceRootProps = ServiceRootWithService | ServiceRootWithId | Serv
87
105
  * </Service.Root>
88
106
  * );
89
107
  * }
108
+ *
109
+ * // Reading from BookingService's selected service
110
+ * // (requires Booking.Root parent context)
111
+ * function SelectedServiceDisplay() {
112
+ * return (
113
+ * <Service.Root>
114
+ * <Service.Name />
115
+ * <Service.Price />
116
+ * </Service.Root>
117
+ * );
118
+ * }
90
119
  * ```
91
120
  */
92
121
  export declare const Root: React.ForwardRefExoticComponent<ServiceRootProps & React.RefAttributes<HTMLElement>>;
@@ -49,7 +49,11 @@ var TestIds;
49
49
  })(TestIds || (TestIds = {}));
50
50
  /**
51
51
  * Root component that provides service context to the entire app.
52
- * Requires exactly one of: service, serviceId, or serviceSlug.
52
+ * Can be used with:
53
+ * - service: Pre-loaded service object (SSR pattern)
54
+ * - serviceId: Service ID for dynamic loading
55
+ * - serviceSlug: Service slug for dynamic loading
56
+ * - No config: Reads from BookingService's selected service (requires BookingService)
53
57
  *
54
58
  * @order 1
55
59
  * @component
@@ -86,11 +90,30 @@ var TestIds;
86
90
  * </Service.Root>
87
91
  * );
88
92
  * }
93
+ *
94
+ * // Reading from BookingService's selected service
95
+ * // (requires Booking.Root parent context)
96
+ * function SelectedServiceDisplay() {
97
+ * return (
98
+ * <Service.Root>
99
+ * <Service.Name />
100
+ * <Service.Price />
101
+ * </Service.Root>
102
+ * );
103
+ * }
89
104
  * ```
90
105
  */
91
106
  export const Root = React.forwardRef((props, ref) => {
92
107
  const { children, service, serviceId, serviceSlug, appId, asChild, className, ...attrs } = props;
93
- return (_jsx(CoreService.Root, { serviceServiceConfig: { service, serviceId, serviceSlug, appId }, children: ({ selected, bookable }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceRoot, "data-selected": selected, "data-bookable": bookable, customElement: children, customElementProps: { selected, bookable }, ...attrs, children: children })) }));
108
+ // Build config - when no service config is provided, pass empty object
109
+ // to trigger "from booking signal" mode in ServiceService
110
+ const serviceServiceConfig = {
111
+ service,
112
+ serviceId,
113
+ serviceSlug,
114
+ appId,
115
+ };
116
+ return (_jsx(CoreService.Root, { serviceServiceConfig: serviceServiceConfig, children: ({ selected, bookable }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceRoot, "data-selected": selected, "data-bookable": bookable, customElement: children, customElementProps: { selected, bookable }, ...attrs, children: children })) }));
94
117
  });
95
118
  Root.displayName = 'Service.Root';
96
119
  /**
@@ -47,19 +47,28 @@ export declare const Root: React.ForwardRefExoticComponent<ServiceListRootProps
47
47
  */
48
48
  export interface ServicesProps {
49
49
  children: React.ReactNode;
50
+ /** Content to display when no services are available */
50
51
  emptyState?: React.ReactNode;
52
+ /** Content to display when loading and no services are available */
53
+ loadingState?: React.ReactNode;
54
+ /** Content to display when an error occurs and no services are available */
55
+ errorState?: React.ReactNode;
51
56
  infiniteScroll?: boolean;
52
57
  pageSize?: number;
53
58
  className?: string;
54
59
  }
55
60
  /**
56
- * Container for the services list with empty state support.
61
+ * Container for the services list with empty, loading, and error state support.
57
62
  * Wraps GenericList.Items. Follows List Container Level pattern.
58
63
  *
59
64
  * @component
60
65
  * @example
61
66
  * ```tsx
62
- * <ServiceList.Services emptyState={<div>No services found</div>}>
67
+ * <ServiceList.Services
68
+ * emptyState={<div>No services found</div>}
69
+ * loadingState={<div>Loading services...</div>}
70
+ * errorState={<div>Failed to load services</div>}
71
+ * >
63
72
  * <ServiceList.ServiceRepeater>
64
73
  * <Service.Name />
65
74
  * <Service.Price />
@@ -73,22 +73,26 @@ const DEFAULT_PAGE_SIZE = 100;
73
73
  */
74
74
  const RootContent = React.forwardRef((props, ref) => {
75
75
  const { children, className, variant, pageSize = DEFAULT_PAGE_SIZE } = props;
76
- return (_jsx(CoreServiceList.Services, { children: ({ services: servicesList, isLoading, hasMore, loadMore }) => {
76
+ return (_jsx(CoreServiceList.Services, { children: ({ services: servicesList, isLoading, hasMore, loadMore, error }) => {
77
77
  const items = servicesList.map((service) => ({
78
78
  ...service,
79
79
  id: service._id,
80
80
  }));
81
- return (_jsx(GenericList.Root, { items: items, loadMore: () => loadMore(pageSize), hasMore: hasMore, isLoading: isLoading, className: className, ref: ref, "data-testid": TestIds.serviceListRoot, variant: variant, children: children }));
81
+ return (_jsx(GenericList.Root, { items: items, loadMore: () => loadMore(pageSize), hasMore: hasMore, isLoading: isLoading, error: error, className: className, ref: ref, "data-testid": TestIds.serviceListRoot, variant: variant, children: children }));
82
82
  } }));
83
83
  });
84
84
  /**
85
- * Container for the services list with empty state support.
85
+ * Container for the services list with empty, loading, and error state support.
86
86
  * Wraps GenericList.Items. Follows List Container Level pattern.
87
87
  *
88
88
  * @component
89
89
  * @example
90
90
  * ```tsx
91
- * <ServiceList.Services emptyState={<div>No services found</div>}>
91
+ * <ServiceList.Services
92
+ * emptyState={<div>No services found</div>}
93
+ * loadingState={<div>Loading services...</div>}
94
+ * errorState={<div>Failed to load services</div>}
95
+ * >
92
96
  * <ServiceList.ServiceRepeater>
93
97
  * <Service.Name />
94
98
  * <Service.Price />
@@ -97,8 +101,8 @@ const RootContent = React.forwardRef((props, ref) => {
97
101
  * ```
98
102
  */
99
103
  export const Services = React.forwardRef((props, ref) => {
100
- const { children, ...otherProps } = props;
101
- return (_jsx(GenericList.Items, { ref: ref, "data-testid": TestIds.serviceListServices, ...otherProps, children: children }));
104
+ const { children, emptyState, loadingState, errorState, ...otherProps } = props;
105
+ return (_jsx(GenericList.Items, { ref: ref, "data-testid": TestIds.serviceListServices, emptyState: emptyState, loadingState: loadingState, errorState: errorState, ...otherProps, children: children }));
102
106
  });
103
107
  Services.displayName = 'ServiceList.Services';
104
108
  /**
@@ -54,6 +54,8 @@ export const ServiceServiceDefinition = defineService('service');
54
54
  */
55
55
  export const ServiceService = implementService.withConfig()(ServiceServiceDefinition, ({ getService, config }) => {
56
56
  const signalsService = getService(SignalsServiceDefinition);
57
+ // Determine if we're in "from booking signal" mode (no service config provided)
58
+ const noExplicitService = !config.service && !config.serviceId && !config.serviceSlug;
57
59
  // Try to get BookingService (optional dependency for showcase mode)
58
60
  let bookingService = null;
59
61
  try {
@@ -63,9 +65,27 @@ export const ServiceService = implementService.withConfig()(ServiceServiceDefini
63
65
  // BookingService not available - showcase mode
64
66
  bookingService = null;
65
67
  }
66
- // Initialize service signal - use empty service object if not provided
67
- const initialService = config.service || {};
68
- const serviceSignal = signalsService.signal(initialService);
68
+ // In "from booking signal" mode, BookingService must be available
69
+ if (noExplicitService && !bookingService) {
70
+ throw new Error('Service.Root requires either a service config (service, serviceId, or serviceSlug) or BookingService to be available');
71
+ }
72
+ // Initialize service signal
73
+ // In "from booking signal" mode, use a computed signal that reads from BookingService
74
+ let serviceSignal;
75
+ if (noExplicitService && bookingService) {
76
+ // Create a computed signal that reads from BookingService's first selected service
77
+ const computedServiceSignal = signalsService.computed(() => {
78
+ const selections = bookingService.serviceSelections.get();
79
+ return selections[0]?.service || null;
80
+ });
81
+ // Wrap computed signal as a Signal (read-only in practice for this mode)
82
+ serviceSignal = computedServiceSignal;
83
+ }
84
+ else {
85
+ // Standard mode: use provided service or empty object
86
+ const initialService = config.service || {};
87
+ serviceSignal = signalsService.signal(initialService);
88
+ }
69
89
  const isLoadingSignal = signalsService.signal(!!config.serviceId);
70
90
  const errorSignal = signalsService.signal(null);
71
91
  // Default appId to BOOKING_APP_ID if not provided
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/headless-bookings",
3
- "version": "0.0.46",
3
+ "version": "0.0.47",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -72,5 +72,5 @@
72
72
  "groupId": "com.wixpress.headless-components"
73
73
  }
74
74
  },
75
- "falconPackageHash": "e188c85a08b0f3efd04be3127dd88996b016941dcd522265f9a7de0b"
75
+ "falconPackageHash": "b4efe3be968a8e8d6806b0e818d5e30742584c3d834296262bc1572f"
76
76
  }