@wix/headless-bookings 0.0.54 → 0.0.56
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/api/query-services/index.js +28 -2
- package/cjs/dist/react/core/location/Location.d.ts +2 -2
- package/cjs/dist/react/core/location/Location.js +1 -1
- package/cjs/dist/react/location/LocationList.js +1 -1
- package/cjs/dist/react/service/Service.d.ts +35 -0
- package/cjs/dist/react/service/Service.js +33 -0
- 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/service-list/service-list.js +41 -9
- package/dist/api/query-services/index.js +28 -2
- package/dist/react/core/location/Location.d.ts +2 -2
- package/dist/react/core/location/Location.js +1 -1
- package/dist/react/location/LocationList.js +1 -1
- package/dist/react/service/Service.d.ts +35 -0
- package/dist/react/service/Service.js +33 -0
- package/dist/react/time-slot-list/TimeSlot.d.ts +25 -0
- package/dist/react/time-slot-list/TimeSlot.js +48 -13
- package/dist/services/service-list/service-list.js +41 -9
- package/package.json +2 -2
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
* Query Services API
|
|
3
3
|
* Fetches single or multiple services with pagination and sorting
|
|
4
4
|
*/
|
|
5
|
-
import { queryServices as queryServicesApi, } from '@wix/auto_sdk_bookings_services';
|
|
5
|
+
import { queryServices as queryServicesApi, LocationType, } from '@wix/auto_sdk_bookings_services';
|
|
6
6
|
/**
|
|
7
7
|
* Booking app ID constant
|
|
8
8
|
*/
|
|
9
9
|
const BOOKING_APP_ID = '13d21c63-b5ec-5912-8397-c3a5ddb27a97';
|
|
10
|
+
/**
|
|
11
|
+
* Synthetic location ID constants for non-business locations
|
|
12
|
+
*/
|
|
13
|
+
const SYNTHETIC_CUSTOM_ID = '__synthetic_custom__';
|
|
14
|
+
const SYNTHETIC_CUSTOMER_ID = '__synthetic_customer__';
|
|
10
15
|
/**
|
|
11
16
|
* Fetches a service by its ID
|
|
12
17
|
* @param id - The service ID
|
|
@@ -80,7 +85,28 @@ const buildServicesQueryRequest = ({ appId, filters, paging, sort, }) => {
|
|
|
80
85
|
}
|
|
81
86
|
// Add locationIds filter if provided
|
|
82
87
|
if (filters?.locationIds && filters.locationIds.length > 0) {
|
|
83
|
-
|
|
88
|
+
// Separate synthetic IDs from business location IDs
|
|
89
|
+
const syntheticTypes = [];
|
|
90
|
+
const businessLocationIds = [];
|
|
91
|
+
for (const id of filters.locationIds) {
|
|
92
|
+
if (id === SYNTHETIC_CUSTOM_ID) {
|
|
93
|
+
syntheticTypes.push(LocationType.CUSTOM);
|
|
94
|
+
}
|
|
95
|
+
else if (id === SYNTHETIC_CUSTOMER_ID) {
|
|
96
|
+
syntheticTypes.push(LocationType.CUSTOMER);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
businessLocationIds.push(id);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Filter by location type for synthetic locations
|
|
103
|
+
if (syntheticTypes.length > 0) {
|
|
104
|
+
filter['locations.type'] = { $hasSome: syntheticTypes };
|
|
105
|
+
}
|
|
106
|
+
// Filter by business ID for business locations
|
|
107
|
+
if (businessLocationIds.length > 0) {
|
|
108
|
+
filter['locations.business.id'] = { $hasSome: businessLocationIds };
|
|
109
|
+
}
|
|
84
110
|
}
|
|
85
111
|
// staffMemberIds to be added later
|
|
86
112
|
const query = {
|
|
@@ -25,8 +25,8 @@ export interface SyntheticLocation {
|
|
|
25
25
|
locationType: LocationType.CUSTOMER | LocationType.CUSTOM;
|
|
26
26
|
/** Marker to identify synthetic locations */
|
|
27
27
|
synthetic: true;
|
|
28
|
-
/** Optional ID for tracking */
|
|
29
|
-
|
|
28
|
+
/** Optional ID for tracking (matches ServiceLocation._id) */
|
|
29
|
+
_id?: string;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Union type - location prop accepts Service or Synthetic location
|
|
@@ -110,7 +110,7 @@ function computeLocationRenderProps(rawLocation) {
|
|
|
110
110
|
locationType,
|
|
111
111
|
address: null,
|
|
112
112
|
name: undefined,
|
|
113
|
-
locationId: syntheticLoc.
|
|
113
|
+
locationId: syntheticLoc._id,
|
|
114
114
|
isCustomerLocation: locationType === LocationType.CUSTOMER,
|
|
115
115
|
isCustomLocation: locationType === LocationType.CUSTOM,
|
|
116
116
|
isSynthetic: true,
|
|
@@ -92,7 +92,7 @@ export const LocationRepeater = React.forwardRef((props, ref) => {
|
|
|
92
92
|
const location = displayLocation.location ?? {
|
|
93
93
|
locationType: displayLocation.type,
|
|
94
94
|
synthetic: true,
|
|
95
|
-
|
|
95
|
+
_id: displayLocation.id,
|
|
96
96
|
};
|
|
97
97
|
return (_jsx(Location.Root, { location: location, "data-testid": TestIds.locationListLocation, "data-location-id": displayLocation.id, "data-location-type": displayLocation.type, "data-item-id": displayLocation.id, children: itemChildren }, displayLocation.id));
|
|
98
98
|
};
|
|
@@ -1133,6 +1133,41 @@ export interface DefaultCapacityProps {
|
|
|
1133
1133
|
* ```
|
|
1134
1134
|
*/
|
|
1135
1135
|
export declare const DefaultCapacity: React.ForwardRefExoticComponent<DefaultCapacityProps & React.RefAttributes<HTMLElement>>;
|
|
1136
|
+
/**
|
|
1137
|
+
* Props for Service AvailableOnline component
|
|
1138
|
+
*/
|
|
1139
|
+
export interface AvailableOnlineProps {
|
|
1140
|
+
asChild?: boolean;
|
|
1141
|
+
children?: AsChildChildren<{
|
|
1142
|
+
availableOnline: boolean;
|
|
1143
|
+
}>;
|
|
1144
|
+
className?: string;
|
|
1145
|
+
label?: string;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Displays whether the service is available online (conferencing enabled).
|
|
1149
|
+
* Headless component - contains zero formatting logic.
|
|
1150
|
+
*
|
|
1151
|
+
* Maps to: service.conferencing.enabled
|
|
1152
|
+
*
|
|
1153
|
+
* @component
|
|
1154
|
+
* @example
|
|
1155
|
+
* ```tsx
|
|
1156
|
+
* // Default usage - displays label only when available online
|
|
1157
|
+
* <Service.AvailableOnline label="Available Online" className="text-sm text-secondary-foreground" />
|
|
1158
|
+
* ```
|
|
1159
|
+
*
|
|
1160
|
+
* @example
|
|
1161
|
+
* ```tsx
|
|
1162
|
+
* // asChild with custom component
|
|
1163
|
+
* <Service.AvailableOnline asChild>
|
|
1164
|
+
* {({ availableOnline }) => availableOnline ? (
|
|
1165
|
+
* <span className="badge bg-green-500">Online</span>
|
|
1166
|
+
* ) : null}
|
|
1167
|
+
* </Service.AvailableOnline>
|
|
1168
|
+
* ```
|
|
1169
|
+
*/
|
|
1170
|
+
export declare const AvailableOnline: React.ForwardRefExoticComponent<AvailableOnlineProps & React.RefAttributes<HTMLElement>>;
|
|
1136
1171
|
export declare const Locations: typeof LocationsBase & {
|
|
1137
1172
|
List: typeof LocationsList;
|
|
1138
1173
|
LocationRepeater: typeof LocationRepeater;
|
|
@@ -49,6 +49,8 @@ var TestIds;
|
|
|
49
49
|
TestIds["serviceScheduleFirstSessionStart"] = "service-schedule-first-session-start";
|
|
50
50
|
TestIds["serviceScheduleLastSessionEnd"] = "service-schedule-last-session-end";
|
|
51
51
|
TestIds["serviceScheduleSessionDuration"] = "service-schedule-session-duration";
|
|
52
|
+
// Conferencing component TestIds
|
|
53
|
+
TestIds["serviceAvailableOnline"] = "service-available-online";
|
|
52
54
|
})(TestIds || (TestIds = {}));
|
|
53
55
|
/**
|
|
54
56
|
* Root component that provides service context to the entire app.
|
|
@@ -1083,6 +1085,37 @@ export const DefaultCapacity = React.forwardRef((props, ref) => {
|
|
|
1083
1085
|
} }));
|
|
1084
1086
|
});
|
|
1085
1087
|
DefaultCapacity.displayName = 'Service.DefaultCapacity';
|
|
1088
|
+
/**
|
|
1089
|
+
* Displays whether the service is available online (conferencing enabled).
|
|
1090
|
+
* Headless component - contains zero formatting logic.
|
|
1091
|
+
*
|
|
1092
|
+
* Maps to: service.conferencing.enabled
|
|
1093
|
+
*
|
|
1094
|
+
* @component
|
|
1095
|
+
* @example
|
|
1096
|
+
* ```tsx
|
|
1097
|
+
* // Default usage - displays label only when available online
|
|
1098
|
+
* <Service.AvailableOnline label="Available Online" className="text-sm text-secondary-foreground" />
|
|
1099
|
+
* ```
|
|
1100
|
+
*
|
|
1101
|
+
* @example
|
|
1102
|
+
* ```tsx
|
|
1103
|
+
* // asChild with custom component
|
|
1104
|
+
* <Service.AvailableOnline asChild>
|
|
1105
|
+
* {({ availableOnline }) => availableOnline ? (
|
|
1106
|
+
* <span className="badge bg-green-500">Online</span>
|
|
1107
|
+
* ) : null}
|
|
1108
|
+
* </Service.AvailableOnline>
|
|
1109
|
+
* ```
|
|
1110
|
+
*/
|
|
1111
|
+
export const AvailableOnline = React.forwardRef((props, ref) => {
|
|
1112
|
+
const { asChild, children, className, label } = props;
|
|
1113
|
+
return (_jsx(CoreService.Service, { children: ({ service }) => {
|
|
1114
|
+
const availableOnline = service.conferencing?.enabled ?? false;
|
|
1115
|
+
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceAvailableOnline, customElement: children, customElementProps: { availableOnline }, children: availableOnline && label ? _jsx("span", { children: label }) : null }));
|
|
1116
|
+
} }));
|
|
1117
|
+
});
|
|
1118
|
+
AvailableOnline.displayName = 'Service.AvailableOnline';
|
|
1086
1119
|
// Create Locations with nested components
|
|
1087
1120
|
export const Locations = Object.assign(LocationsBase, {
|
|
1088
1121
|
List: LocationsList,
|
|
@@ -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
|
/**
|
|
@@ -73,13 +73,14 @@ export const ServiceListServiceDefinition = defineService('service-list');
|
|
|
73
73
|
* ```
|
|
74
74
|
*/
|
|
75
75
|
export const ServiceListService = implementService.withConfig()(ServiceListServiceDefinition, ({ getService, config }) => {
|
|
76
|
-
let firstRun = true;
|
|
77
76
|
const signalsService = getService(SignalsServiceDefinition);
|
|
78
77
|
const bookingService = getService(BookingServiceDefinition);
|
|
79
78
|
// Default appId to BOOKING_APP_ID if not provided
|
|
80
79
|
const appId = config.options?.appId ?? BOOKING_APP_ID;
|
|
81
80
|
// Check if services are provided (showcase mode vs query mode)
|
|
82
81
|
const hasInitialServices = config.services && config.services.length > 0;
|
|
82
|
+
// Check if categories are provided (from SSR prefetch)
|
|
83
|
+
const hasInitialCategories = config.categories && config.categories.length > 0;
|
|
83
84
|
// Initialize services signal - use empty array if not provided
|
|
84
85
|
const initialServices = config.services || [];
|
|
85
86
|
const servicesSignal = signalsService.signal(initialServices);
|
|
@@ -157,9 +158,43 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
157
158
|
...(locationId ? { locationIds: [locationId] } : {}),
|
|
158
159
|
};
|
|
159
160
|
};
|
|
160
|
-
//
|
|
161
|
+
// Initial fetch - runs once on client when no prefetched data
|
|
162
|
+
// Fetches both services AND categories in parallel
|
|
163
|
+
if (typeof window !== 'undefined' && !hasInitialServices) {
|
|
164
|
+
const initialQueryOptions = queryOptionsSignal.peek();
|
|
165
|
+
const currentPaging = pagingMetadataSignal.peek();
|
|
166
|
+
const effectiveFilters = getEffectiveFilters(initialQueryOptions.filters);
|
|
167
|
+
isLoadingSignal.set(true);
|
|
168
|
+
Promise.all([
|
|
169
|
+
queryServices({
|
|
170
|
+
appId: initialQueryOptions.appId,
|
|
171
|
+
filters: effectiveFilters,
|
|
172
|
+
pagingMetadata: {
|
|
173
|
+
limit: currentPaging.limit,
|
|
174
|
+
offset: 0,
|
|
175
|
+
},
|
|
176
|
+
sort: initialQueryOptions.sort,
|
|
177
|
+
}),
|
|
178
|
+
!hasInitialCategories ? queryCategories() : Promise.resolve(null),
|
|
179
|
+
])
|
|
180
|
+
.then(([servicesResult, categoriesResult]) => {
|
|
181
|
+
servicesSignal.set(servicesResult.services);
|
|
182
|
+
pagingMetadataSignal.set(servicesResult.pagingMetadata);
|
|
183
|
+
if (categoriesResult) {
|
|
184
|
+
categoriesSignal.set(categoriesResult.categories);
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
.catch((error) => {
|
|
188
|
+
errorSignal.set(error instanceof Error ? error.message : 'Unknown error');
|
|
189
|
+
})
|
|
190
|
+
.finally(() => {
|
|
191
|
+
isLoadingSignal.set(false);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// Reactive effect - only handles query/filter changes (not initial fetch)
|
|
161
195
|
// IMPORTANT: Only subscribes to queryOptionsSignal and bookingService.location
|
|
162
196
|
if (typeof window !== 'undefined') {
|
|
197
|
+
let isFirstEffectRun = true;
|
|
163
198
|
signalsService.effect(async () => {
|
|
164
199
|
// CRITICAL: Read queryOptionsSignal to establish dependency
|
|
165
200
|
// Do NOT read pagingMetadataSignal here to avoid infinite loop
|
|
@@ -167,12 +202,10 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
167
202
|
// Also subscribe to location changes from BookingService
|
|
168
203
|
// Reading the signal here establishes the dependency
|
|
169
204
|
bookingService.location.get();
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
205
|
+
// Skip first run - initial fetch is handled separately above
|
|
206
|
+
if (isFirstEffectRun) {
|
|
207
|
+
isFirstEffectRun = false;
|
|
208
|
+
return;
|
|
176
209
|
}
|
|
177
210
|
// Read paging metadata outside of dependency tracking for the fetch
|
|
178
211
|
const currentPaging = pagingMetadataSignal.peek();
|
|
@@ -202,7 +235,6 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
202
235
|
}
|
|
203
236
|
});
|
|
204
237
|
}
|
|
205
|
-
firstRun = false;
|
|
206
238
|
const loadMoreCursor = async (count) => {
|
|
207
239
|
const currentQueryOptions = queryOptionsSignal.get();
|
|
208
240
|
// Calculate next page offset based on current services count
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
* Query Services API
|
|
3
3
|
* Fetches single or multiple services with pagination and sorting
|
|
4
4
|
*/
|
|
5
|
-
import { queryServices as queryServicesApi, } from '@wix/auto_sdk_bookings_services';
|
|
5
|
+
import { queryServices as queryServicesApi, LocationType, } from '@wix/auto_sdk_bookings_services';
|
|
6
6
|
/**
|
|
7
7
|
* Booking app ID constant
|
|
8
8
|
*/
|
|
9
9
|
const BOOKING_APP_ID = '13d21c63-b5ec-5912-8397-c3a5ddb27a97';
|
|
10
|
+
/**
|
|
11
|
+
* Synthetic location ID constants for non-business locations
|
|
12
|
+
*/
|
|
13
|
+
const SYNTHETIC_CUSTOM_ID = '__synthetic_custom__';
|
|
14
|
+
const SYNTHETIC_CUSTOMER_ID = '__synthetic_customer__';
|
|
10
15
|
/**
|
|
11
16
|
* Fetches a service by its ID
|
|
12
17
|
* @param id - The service ID
|
|
@@ -80,7 +85,28 @@ const buildServicesQueryRequest = ({ appId, filters, paging, sort, }) => {
|
|
|
80
85
|
}
|
|
81
86
|
// Add locationIds filter if provided
|
|
82
87
|
if (filters?.locationIds && filters.locationIds.length > 0) {
|
|
83
|
-
|
|
88
|
+
// Separate synthetic IDs from business location IDs
|
|
89
|
+
const syntheticTypes = [];
|
|
90
|
+
const businessLocationIds = [];
|
|
91
|
+
for (const id of filters.locationIds) {
|
|
92
|
+
if (id === SYNTHETIC_CUSTOM_ID) {
|
|
93
|
+
syntheticTypes.push(LocationType.CUSTOM);
|
|
94
|
+
}
|
|
95
|
+
else if (id === SYNTHETIC_CUSTOMER_ID) {
|
|
96
|
+
syntheticTypes.push(LocationType.CUSTOMER);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
businessLocationIds.push(id);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Filter by location type for synthetic locations
|
|
103
|
+
if (syntheticTypes.length > 0) {
|
|
104
|
+
filter['locations.type'] = { $hasSome: syntheticTypes };
|
|
105
|
+
}
|
|
106
|
+
// Filter by business ID for business locations
|
|
107
|
+
if (businessLocationIds.length > 0) {
|
|
108
|
+
filter['locations.business.id'] = { $hasSome: businessLocationIds };
|
|
109
|
+
}
|
|
84
110
|
}
|
|
85
111
|
// staffMemberIds to be added later
|
|
86
112
|
const query = {
|
|
@@ -25,8 +25,8 @@ export interface SyntheticLocation {
|
|
|
25
25
|
locationType: LocationType.CUSTOMER | LocationType.CUSTOM;
|
|
26
26
|
/** Marker to identify synthetic locations */
|
|
27
27
|
synthetic: true;
|
|
28
|
-
/** Optional ID for tracking */
|
|
29
|
-
|
|
28
|
+
/** Optional ID for tracking (matches ServiceLocation._id) */
|
|
29
|
+
_id?: string;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Union type - location prop accepts Service or Synthetic location
|
|
@@ -110,7 +110,7 @@ function computeLocationRenderProps(rawLocation) {
|
|
|
110
110
|
locationType,
|
|
111
111
|
address: null,
|
|
112
112
|
name: undefined,
|
|
113
|
-
locationId: syntheticLoc.
|
|
113
|
+
locationId: syntheticLoc._id,
|
|
114
114
|
isCustomerLocation: locationType === LocationType.CUSTOMER,
|
|
115
115
|
isCustomLocation: locationType === LocationType.CUSTOM,
|
|
116
116
|
isSynthetic: true,
|
|
@@ -92,7 +92,7 @@ export const LocationRepeater = React.forwardRef((props, ref) => {
|
|
|
92
92
|
const location = displayLocation.location ?? {
|
|
93
93
|
locationType: displayLocation.type,
|
|
94
94
|
synthetic: true,
|
|
95
|
-
|
|
95
|
+
_id: displayLocation.id,
|
|
96
96
|
};
|
|
97
97
|
return (_jsx(Location.Root, { location: location, "data-testid": TestIds.locationListLocation, "data-location-id": displayLocation.id, "data-location-type": displayLocation.type, "data-item-id": displayLocation.id, children: itemChildren }, displayLocation.id));
|
|
98
98
|
};
|
|
@@ -1133,6 +1133,41 @@ export interface DefaultCapacityProps {
|
|
|
1133
1133
|
* ```
|
|
1134
1134
|
*/
|
|
1135
1135
|
export declare const DefaultCapacity: React.ForwardRefExoticComponent<DefaultCapacityProps & React.RefAttributes<HTMLElement>>;
|
|
1136
|
+
/**
|
|
1137
|
+
* Props for Service AvailableOnline component
|
|
1138
|
+
*/
|
|
1139
|
+
export interface AvailableOnlineProps {
|
|
1140
|
+
asChild?: boolean;
|
|
1141
|
+
children?: AsChildChildren<{
|
|
1142
|
+
availableOnline: boolean;
|
|
1143
|
+
}>;
|
|
1144
|
+
className?: string;
|
|
1145
|
+
label?: string;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Displays whether the service is available online (conferencing enabled).
|
|
1149
|
+
* Headless component - contains zero formatting logic.
|
|
1150
|
+
*
|
|
1151
|
+
* Maps to: service.conferencing.enabled
|
|
1152
|
+
*
|
|
1153
|
+
* @component
|
|
1154
|
+
* @example
|
|
1155
|
+
* ```tsx
|
|
1156
|
+
* // Default usage - displays label only when available online
|
|
1157
|
+
* <Service.AvailableOnline label="Available Online" className="text-sm text-secondary-foreground" />
|
|
1158
|
+
* ```
|
|
1159
|
+
*
|
|
1160
|
+
* @example
|
|
1161
|
+
* ```tsx
|
|
1162
|
+
* // asChild with custom component
|
|
1163
|
+
* <Service.AvailableOnline asChild>
|
|
1164
|
+
* {({ availableOnline }) => availableOnline ? (
|
|
1165
|
+
* <span className="badge bg-green-500">Online</span>
|
|
1166
|
+
* ) : null}
|
|
1167
|
+
* </Service.AvailableOnline>
|
|
1168
|
+
* ```
|
|
1169
|
+
*/
|
|
1170
|
+
export declare const AvailableOnline: React.ForwardRefExoticComponent<AvailableOnlineProps & React.RefAttributes<HTMLElement>>;
|
|
1136
1171
|
export declare const Locations: typeof LocationsBase & {
|
|
1137
1172
|
List: typeof LocationsList;
|
|
1138
1173
|
LocationRepeater: typeof LocationRepeater;
|
|
@@ -49,6 +49,8 @@ var TestIds;
|
|
|
49
49
|
TestIds["serviceScheduleFirstSessionStart"] = "service-schedule-first-session-start";
|
|
50
50
|
TestIds["serviceScheduleLastSessionEnd"] = "service-schedule-last-session-end";
|
|
51
51
|
TestIds["serviceScheduleSessionDuration"] = "service-schedule-session-duration";
|
|
52
|
+
// Conferencing component TestIds
|
|
53
|
+
TestIds["serviceAvailableOnline"] = "service-available-online";
|
|
52
54
|
})(TestIds || (TestIds = {}));
|
|
53
55
|
/**
|
|
54
56
|
* Root component that provides service context to the entire app.
|
|
@@ -1083,6 +1085,37 @@ export const DefaultCapacity = React.forwardRef((props, ref) => {
|
|
|
1083
1085
|
} }));
|
|
1084
1086
|
});
|
|
1085
1087
|
DefaultCapacity.displayName = 'Service.DefaultCapacity';
|
|
1088
|
+
/**
|
|
1089
|
+
* Displays whether the service is available online (conferencing enabled).
|
|
1090
|
+
* Headless component - contains zero formatting logic.
|
|
1091
|
+
*
|
|
1092
|
+
* Maps to: service.conferencing.enabled
|
|
1093
|
+
*
|
|
1094
|
+
* @component
|
|
1095
|
+
* @example
|
|
1096
|
+
* ```tsx
|
|
1097
|
+
* // Default usage - displays label only when available online
|
|
1098
|
+
* <Service.AvailableOnline label="Available Online" className="text-sm text-secondary-foreground" />
|
|
1099
|
+
* ```
|
|
1100
|
+
*
|
|
1101
|
+
* @example
|
|
1102
|
+
* ```tsx
|
|
1103
|
+
* // asChild with custom component
|
|
1104
|
+
* <Service.AvailableOnline asChild>
|
|
1105
|
+
* {({ availableOnline }) => availableOnline ? (
|
|
1106
|
+
* <span className="badge bg-green-500">Online</span>
|
|
1107
|
+
* ) : null}
|
|
1108
|
+
* </Service.AvailableOnline>
|
|
1109
|
+
* ```
|
|
1110
|
+
*/
|
|
1111
|
+
export const AvailableOnline = React.forwardRef((props, ref) => {
|
|
1112
|
+
const { asChild, children, className, label } = props;
|
|
1113
|
+
return (_jsx(CoreService.Service, { children: ({ service }) => {
|
|
1114
|
+
const availableOnline = service.conferencing?.enabled ?? false;
|
|
1115
|
+
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceAvailableOnline, customElement: children, customElementProps: { availableOnline }, children: availableOnline && label ? _jsx("span", { children: label }) : null }));
|
|
1116
|
+
} }));
|
|
1117
|
+
});
|
|
1118
|
+
AvailableOnline.displayName = 'Service.AvailableOnline';
|
|
1086
1119
|
// Create Locations with nested components
|
|
1087
1120
|
export const Locations = Object.assign(LocationsBase, {
|
|
1088
1121
|
List: LocationsList,
|
|
@@ -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
|
/**
|
|
@@ -73,13 +73,14 @@ export const ServiceListServiceDefinition = defineService('service-list');
|
|
|
73
73
|
* ```
|
|
74
74
|
*/
|
|
75
75
|
export const ServiceListService = implementService.withConfig()(ServiceListServiceDefinition, ({ getService, config }) => {
|
|
76
|
-
let firstRun = true;
|
|
77
76
|
const signalsService = getService(SignalsServiceDefinition);
|
|
78
77
|
const bookingService = getService(BookingServiceDefinition);
|
|
79
78
|
// Default appId to BOOKING_APP_ID if not provided
|
|
80
79
|
const appId = config.options?.appId ?? BOOKING_APP_ID;
|
|
81
80
|
// Check if services are provided (showcase mode vs query mode)
|
|
82
81
|
const hasInitialServices = config.services && config.services.length > 0;
|
|
82
|
+
// Check if categories are provided (from SSR prefetch)
|
|
83
|
+
const hasInitialCategories = config.categories && config.categories.length > 0;
|
|
83
84
|
// Initialize services signal - use empty array if not provided
|
|
84
85
|
const initialServices = config.services || [];
|
|
85
86
|
const servicesSignal = signalsService.signal(initialServices);
|
|
@@ -157,9 +158,43 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
157
158
|
...(locationId ? { locationIds: [locationId] } : {}),
|
|
158
159
|
};
|
|
159
160
|
};
|
|
160
|
-
//
|
|
161
|
+
// Initial fetch - runs once on client when no prefetched data
|
|
162
|
+
// Fetches both services AND categories in parallel
|
|
163
|
+
if (typeof window !== 'undefined' && !hasInitialServices) {
|
|
164
|
+
const initialQueryOptions = queryOptionsSignal.peek();
|
|
165
|
+
const currentPaging = pagingMetadataSignal.peek();
|
|
166
|
+
const effectiveFilters = getEffectiveFilters(initialQueryOptions.filters);
|
|
167
|
+
isLoadingSignal.set(true);
|
|
168
|
+
Promise.all([
|
|
169
|
+
queryServices({
|
|
170
|
+
appId: initialQueryOptions.appId,
|
|
171
|
+
filters: effectiveFilters,
|
|
172
|
+
pagingMetadata: {
|
|
173
|
+
limit: currentPaging.limit,
|
|
174
|
+
offset: 0,
|
|
175
|
+
},
|
|
176
|
+
sort: initialQueryOptions.sort,
|
|
177
|
+
}),
|
|
178
|
+
!hasInitialCategories ? queryCategories() : Promise.resolve(null),
|
|
179
|
+
])
|
|
180
|
+
.then(([servicesResult, categoriesResult]) => {
|
|
181
|
+
servicesSignal.set(servicesResult.services);
|
|
182
|
+
pagingMetadataSignal.set(servicesResult.pagingMetadata);
|
|
183
|
+
if (categoriesResult) {
|
|
184
|
+
categoriesSignal.set(categoriesResult.categories);
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
.catch((error) => {
|
|
188
|
+
errorSignal.set(error instanceof Error ? error.message : 'Unknown error');
|
|
189
|
+
})
|
|
190
|
+
.finally(() => {
|
|
191
|
+
isLoadingSignal.set(false);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// Reactive effect - only handles query/filter changes (not initial fetch)
|
|
161
195
|
// IMPORTANT: Only subscribes to queryOptionsSignal and bookingService.location
|
|
162
196
|
if (typeof window !== 'undefined') {
|
|
197
|
+
let isFirstEffectRun = true;
|
|
163
198
|
signalsService.effect(async () => {
|
|
164
199
|
// CRITICAL: Read queryOptionsSignal to establish dependency
|
|
165
200
|
// Do NOT read pagingMetadataSignal here to avoid infinite loop
|
|
@@ -167,12 +202,10 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
167
202
|
// Also subscribe to location changes from BookingService
|
|
168
203
|
// Reading the signal here establishes the dependency
|
|
169
204
|
bookingService.location.get();
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
205
|
+
// Skip first run - initial fetch is handled separately above
|
|
206
|
+
if (isFirstEffectRun) {
|
|
207
|
+
isFirstEffectRun = false;
|
|
208
|
+
return;
|
|
176
209
|
}
|
|
177
210
|
// Read paging metadata outside of dependency tracking for the fetch
|
|
178
211
|
const currentPaging = pagingMetadataSignal.peek();
|
|
@@ -202,7 +235,6 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
202
235
|
}
|
|
203
236
|
});
|
|
204
237
|
}
|
|
205
|
-
firstRun = false;
|
|
206
238
|
const loadMoreCursor = async (count) => {
|
|
207
239
|
const currentQueryOptions = queryOptionsSignal.get();
|
|
208
240
|
// Calculate next page offset based on current services count
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/headless-bookings",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.56",
|
|
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": "
|
|
75
|
+
"falconPackageHash": "8792da90af7c78ece12ac4244e507751fbe186381decf6b266bee92a"
|
|
76
76
|
}
|