@wix/headless-bookings 1.0.8 → 1.0.10
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/__mocks__/services/services-mocks.d.ts +17 -1
- package/cjs/dist/__mocks__/services/services-mocks.js +35 -1
- package/cjs/dist/api/index.d.ts +1 -1
- package/cjs/dist/api/index.js +2 -1
- package/cjs/dist/api/query-services/index.d.ts +60 -15
- package/cjs/dist/api/query-services/index.js +86 -10
- package/cjs/dist/react/core/service-list/ServiceList.d.ts +2 -3
- package/cjs/dist/react/service-list/ServiceList.d.ts +3 -3
- package/cjs/dist/react/service-list/ServiceList.js +1 -1
- package/cjs/dist/react/service-list/ServiceList.stories.d.ts +29 -0
- package/cjs/dist/react/service-list/ServiceList.stories.js +125 -0
- package/cjs/dist/services/booking/book-action/bookAction.js +9 -18
- package/cjs/dist/services/booking/book-action/buildCartRequest.d.ts +7 -18
- package/cjs/dist/services/booking/book-action/buildCartRequest.js +0 -7
- package/cjs/dist/services/booking/book-action/buildDayRangeBookingRequest.d.ts +7 -4
- package/cjs/dist/services/booking/book-action/buildDayRangeBookingRequest.js +4 -5
- package/cjs/dist/services/booking/book-action/createBookingsForCart.d.ts +2 -2
- package/cjs/dist/services/booking/book-action/createBookingsForCart.js +5 -21
- package/cjs/dist/services/booking/book-action/dayRange.d.ts +2 -1
- package/cjs/dist/services/booking/book-action/dayRange.js +2 -1
- package/cjs/dist/services/booking/book-action/types.d.ts +2 -3
- package/cjs/dist/services/booking/booking.d.ts +4 -4
- package/cjs/dist/services/constants.d.ts +2 -0
- package/cjs/dist/services/constants.js +3 -1
- package/cjs/dist/services/index.d.ts +3 -3
- package/cjs/dist/services/index.js +2 -2
- package/cjs/dist/services/service-list/service-list.d.ts +45 -36
- package/cjs/dist/services/service-list/service-list.js +90 -112
- package/dist/__mocks__/services/services-mocks.d.ts +17 -1
- package/dist/__mocks__/services/services-mocks.js +34 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/query-services/index.d.ts +60 -15
- package/dist/api/query-services/index.js +89 -10
- package/dist/react/core/service-list/ServiceList.d.ts +2 -3
- package/dist/react/service-list/ServiceList.d.ts +3 -3
- package/dist/react/service-list/ServiceList.js +1 -1
- package/dist/react/service-list/ServiceList.stories.d.ts +29 -0
- package/dist/react/service-list/ServiceList.stories.js +89 -0
- package/dist/services/booking/book-action/bookAction.js +10 -19
- package/dist/services/booking/book-action/buildCartRequest.d.ts +7 -18
- package/dist/services/booking/book-action/buildCartRequest.js +0 -6
- package/dist/services/booking/book-action/buildDayRangeBookingRequest.d.ts +7 -4
- package/dist/services/booking/book-action/buildDayRangeBookingRequest.js +4 -5
- package/dist/services/booking/book-action/createBookingsForCart.d.ts +2 -2
- package/dist/services/booking/book-action/createBookingsForCart.js +3 -19
- package/dist/services/booking/book-action/dayRange.d.ts +2 -1
- package/dist/services/booking/book-action/dayRange.js +2 -1
- package/dist/services/booking/book-action/types.d.ts +2 -3
- package/dist/services/booking/booking.d.ts +4 -4
- package/dist/services/constants.d.ts +2 -0
- package/dist/services/constants.js +2 -0
- package/dist/services/index.d.ts +3 -3
- package/dist/services/index.js +2 -2
- package/dist/services/service-list/service-list.d.ts +45 -36
- package/dist/services/service-list/service-list.js +90 -112
- package/package.json +17 -2
package/dist/services/index.js
CHANGED
|
@@ -6,8 +6,8 @@ export { ServiceService, ServiceServiceDefinition, loadServiceServiceInitialData
|
|
|
6
6
|
export { BookingService, BookingServiceDefinition, } from './booking/booking.js';
|
|
7
7
|
export { TimeSlotListService, TimeSlotListServiceDefinition, TimeSlotService, TimeSlotServiceDefinition, ViewMode, loadTimeSlotListConfig, } from './time-slot-list/index.js';
|
|
8
8
|
export { ServiceListService, ServiceListServiceDefinition, loadServiceListServiceInitialData, } from './service-list/service-list.js';
|
|
9
|
-
export { SYNTHETIC_CUSTOM_ID, SYNTHETIC_CUSTOMER_ID, } from '../api/query-services/index.js';
|
|
10
|
-
export { executeBookAction, canBook, buildBookingRequest, buildCartRequest,
|
|
9
|
+
export { SYNTHETIC_CUSTOM_ID, SYNTHETIC_CUSTOMER_ID, AvailabilityLocationType, } from '../api/query-services/index.js';
|
|
10
|
+
export { executeBookAction, canBook, buildBookingRequest, buildCartRequest, buildDayRangeBookingRequest, createBookingsForCart, isDayRangeService, getDayRangeBounds, } from './booking/book-action/index.js';
|
|
11
11
|
export { BookingFormService, BookingFormServiceDefinition, } from './booking-form/booking-form.js';
|
|
12
12
|
export { PaymentService, PaymentServiceDefinition, loadPaymentConfig, } from './payment/payment.js';
|
|
13
13
|
export { LocationListService, LocationListServiceDefinition, LocationType, loadLocationListServiceInitialData, locationListServiceBinding, getLocationById, } from './location-list/location-list.js';
|
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ServiceList Service - Reactive service list data management
|
|
3
|
-
* Handles querying and pagination of services list (NO selection management)
|
|
3
|
+
* Handles querying and pagination of services list (NO selection management).
|
|
4
|
+
*
|
|
5
|
+
* Availability is just part of the query: when the query options carry an
|
|
6
|
+
* `availabilityFilter`, the API layer queries services by real bookable
|
|
7
|
+
* availability (and each service gets an `available` flag); otherwise it runs the
|
|
8
|
+
* plain offset query. The service does not know or care which endpoint is used —
|
|
9
|
+
* that decision lives in {@link queryServices}.
|
|
4
10
|
*/
|
|
5
11
|
import { type Signal, type ReadOnlySignal } from '@wix/services-definitions/core-services/signals';
|
|
6
|
-
import type {
|
|
7
|
-
import type { QueryServicesRequest, PagingMetadata } from '../../api/query-services/index.js';
|
|
12
|
+
import type { Sorting } from '@wix/auto_sdk_bookings_services';
|
|
13
|
+
import type { QueryServicesRequest, PagingMetadata, ServiceWithAvailabilityItem, AvailabilityFilter } from '../../api/query-services/index.js';
|
|
8
14
|
import { type Category } from '../../api/query-categories/index.js';
|
|
9
15
|
import type { FilterOption, FilterValue as Filter } from '@wix/headless-components/react';
|
|
10
16
|
/**
|
|
11
|
-
* Query options without paging metadata (user-controlled request params)
|
|
12
|
-
*
|
|
17
|
+
* Query options without paging metadata (user-controlled request params).
|
|
18
|
+
* Includes `filter` (category/location) and `availabilityFilter`; the effect
|
|
19
|
+
* subscribes to changes in this signal to trigger re-fetches.
|
|
13
20
|
*/
|
|
14
21
|
export type QueryOptions = Omit<QueryServicesRequest, 'pagingMetadata'>;
|
|
15
22
|
/**
|
|
16
23
|
* Configuration for the ServiceList service
|
|
17
24
|
*/
|
|
18
25
|
export interface ServiceListServiceConfig {
|
|
19
|
-
services?:
|
|
26
|
+
services?: ServiceWithAvailabilityItem[];
|
|
20
27
|
categories?: Category[];
|
|
21
28
|
options?: QueryServicesRequest;
|
|
22
29
|
}
|
|
@@ -35,10 +42,10 @@ export interface ServiceListActions {
|
|
|
35
42
|
*/
|
|
36
43
|
export interface ServiceListServiceAPI {
|
|
37
44
|
/** Reactive signal containing the list of services */
|
|
38
|
-
servicesSignal: Signal<
|
|
39
|
-
/** Reactive signal containing current query options (appId, filter, sort) - changes trigger re-fetch */
|
|
45
|
+
servicesSignal: Signal<ServiceWithAvailabilityItem[]>;
|
|
46
|
+
/** Reactive signal containing current query options (appId, filter, availabilityFilter, sort) - changes trigger re-fetch */
|
|
40
47
|
queryOptionsSignal: Signal<QueryOptions>;
|
|
41
|
-
/** Reactive signal containing pagination metadata (limit, offset, count, hasNext) - updated after fetches */
|
|
48
|
+
/** Reactive signal containing pagination metadata (limit, offset/cursor, count, hasNext) - updated after fetches */
|
|
42
49
|
pagingMetadataSignal: Signal<PagingMetadata>;
|
|
43
50
|
/** Reactive signal indicating if services are currently being loaded */
|
|
44
51
|
isLoadingSignal: Signal<boolean>;
|
|
@@ -50,6 +57,8 @@ export interface ServiceListServiceAPI {
|
|
|
50
57
|
filterOptionsSignal: ReadOnlySignal<FilterOption[]>;
|
|
51
58
|
/** Computed signal indicating if any filters are active (from queryOptionsSignal.filter) */
|
|
52
59
|
isFilteredSignal: ReadOnlySignal<boolean>;
|
|
60
|
+
/** Computed signal indicating whether an availability filter is currently applied */
|
|
61
|
+
hasAvailabilityFilterSignal: ReadOnlySignal<boolean>;
|
|
53
62
|
/** Computed signal containing effective filter (queryOptions.filter + location from BookingService) */
|
|
54
63
|
effectiveFilterSignal: ReadOnlySignal<Filter | undefined>;
|
|
55
64
|
/** Actions object containing query actions */
|
|
@@ -91,7 +100,6 @@ export declare const ServiceListServiceDefinition: string & {
|
|
|
91
100
|
* const services = serviceListService.servicesSignal.get();
|
|
92
101
|
* const isLoading = serviceListService.isLoadingSignal.get();
|
|
93
102
|
* const error = serviceListService.errorSignal.get();
|
|
94
|
-
* const pagingMetadata = serviceListService.pagingMetadataSignal.get();
|
|
95
103
|
* const hasMore = serviceListService.hasMoreServicesSignal.get();
|
|
96
104
|
*
|
|
97
105
|
* if (isLoading) return <div>Loading services...</div>;
|
|
@@ -110,9 +118,6 @@ export declare const ServiceListServiceDefinition: string & {
|
|
|
110
118
|
* Load More
|
|
111
119
|
* </button>
|
|
112
120
|
* )}
|
|
113
|
-
* {pagingMetadata.hasNext && (
|
|
114
|
-
* <span>More services available</span>
|
|
115
|
-
* )}
|
|
116
121
|
* </div>
|
|
117
122
|
* );
|
|
118
123
|
* }
|
|
@@ -135,36 +140,40 @@ export type ErrorServiceListServiceConfigResult = {
|
|
|
135
140
|
error: Error;
|
|
136
141
|
};
|
|
137
142
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
* Options for {@link loadServiceListServiceInitialData}.
|
|
144
|
+
* Pass an `availabilityFilter` to load availability-aware services.
|
|
145
|
+
*/
|
|
146
|
+
export interface LoadServiceListServiceOptions {
|
|
147
|
+
appId?: string;
|
|
148
|
+
filter?: Filter;
|
|
149
|
+
pagingMetadata?: PagingMetadata;
|
|
150
|
+
sort?: Sorting[];
|
|
151
|
+
availabilityFilter?: AvailabilityFilter;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Loads service list configuration for SSR, pre-fetching services and categories.
|
|
155
|
+
* Pass an `availabilityFilter` to query availability-aware services.
|
|
142
156
|
*
|
|
143
|
-
* @param options - Query options
|
|
144
|
-
* @returns A
|
|
157
|
+
* @param options - Query options.
|
|
158
|
+
* @returns A success result with the config, or an error result.
|
|
145
159
|
*
|
|
146
160
|
* @example
|
|
147
161
|
* ```ts
|
|
148
|
-
* //
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* // Load with query options
|
|
152
|
-
* const serviceListResult = await loadServiceListServiceInitialData({
|
|
162
|
+
* // Plain offset query
|
|
163
|
+
* const result = await loadServiceListServiceInitialData({
|
|
153
164
|
* appId: '13d21c63-b5ec-5912-8397-c3a5ddb27a97',
|
|
154
165
|
* pagingMetadata: { limit: 20, offset: 0 },
|
|
155
166
|
* });
|
|
156
167
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* // More services can be loaded
|
|
167
|
-
* }
|
|
168
|
+
* // Availability-aware query (cursor paging + `available` flag)
|
|
169
|
+
* const availabilityResult = await loadServiceListServiceInitialData({
|
|
170
|
+
* availabilityFilter: {
|
|
171
|
+
* localStartDate: '2026-06-01T09:00:00',
|
|
172
|
+
* localEndDate: '2026-06-07T17:00:00',
|
|
173
|
+
* timeZone: 'America/New_York',
|
|
174
|
+
* },
|
|
175
|
+
* pagingMetadata: { limit: 20 },
|
|
176
|
+
* });
|
|
168
177
|
* ```
|
|
169
178
|
*/
|
|
170
|
-
export declare function loadServiceListServiceInitialData(options:
|
|
179
|
+
export declare function loadServiceListServiceInitialData(options: LoadServiceListServiceOptions): Promise<SuccessServiceListServiceConfigResult | ErrorServiceListServiceConfigResult>;
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ServiceList Service - Reactive service list data management
|
|
3
|
-
* Handles querying and pagination of services list (NO selection management)
|
|
3
|
+
* Handles querying and pagination of services list (NO selection management).
|
|
4
|
+
*
|
|
5
|
+
* Availability is just part of the query: when the query options carry an
|
|
6
|
+
* `availabilityFilter`, the API layer queries services by real bookable
|
|
7
|
+
* availability (and each service gets an `available` flag); otherwise it runs the
|
|
8
|
+
* plain offset query. The service does not know or care which endpoint is used —
|
|
9
|
+
* that decision lives in {@link queryServices}.
|
|
4
10
|
*/
|
|
5
11
|
import { defineService, implementService } from '@wix/services-definitions';
|
|
6
12
|
import { SignalsServiceDefinition, } from '@wix/services-definitions/core-services/signals';
|
|
7
|
-
import { queryServices } from '../../api/query-services/index.js';
|
|
13
|
+
import { queryServices, hasActiveAvailabilityFilter, } from '../../api/query-services/index.js';
|
|
8
14
|
import { queryCategories, } from '../../api/query-categories/index.js';
|
|
9
15
|
import { BookingServiceDefinition } from '../booking/booking.js';
|
|
10
|
-
import { BOOKING_APP_ID } from '../constants.js';
|
|
11
|
-
/**
|
|
12
|
-
* Default query limit
|
|
13
|
-
*/
|
|
14
|
-
const DEFAULT_QUERY_LIMIT = 100;
|
|
16
|
+
import { BOOKING_APP_ID, DEFAULT_QUERY_LIMIT } from '../constants.js';
|
|
15
17
|
/**
|
|
16
18
|
* Service definition for the ServiceList service
|
|
17
19
|
*/
|
|
@@ -42,7 +44,6 @@ export const ServiceListServiceDefinition = defineService('service-list');
|
|
|
42
44
|
* const services = serviceListService.servicesSignal.get();
|
|
43
45
|
* const isLoading = serviceListService.isLoadingSignal.get();
|
|
44
46
|
* const error = serviceListService.errorSignal.get();
|
|
45
|
-
* const pagingMetadata = serviceListService.pagingMetadataSignal.get();
|
|
46
47
|
* const hasMore = serviceListService.hasMoreServicesSignal.get();
|
|
47
48
|
*
|
|
48
49
|
* if (isLoading) return <div>Loading services...</div>;
|
|
@@ -61,9 +62,6 @@ export const ServiceListServiceDefinition = defineService('service-list');
|
|
|
61
62
|
* Load More
|
|
62
63
|
* </button>
|
|
63
64
|
* )}
|
|
64
|
-
* {pagingMetadata.hasNext && (
|
|
65
|
-
* <span>More services available</span>
|
|
66
|
-
* )}
|
|
67
65
|
* </div>
|
|
68
66
|
* );
|
|
69
67
|
* }
|
|
@@ -81,17 +79,19 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
81
79
|
// Initialize services signal - use empty array if not provided
|
|
82
80
|
const initialServices = config.services || [];
|
|
83
81
|
const servicesSignal = signalsService.signal(initialServices);
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
82
|
+
// Build query options from config. The filter/availabilityFilter/sort must
|
|
83
|
+
// be preserved even when services are prefetched (e.g. SSR), so loadMore
|
|
84
|
+
// keeps querying the same way and stays in availability mode. A pure
|
|
85
|
+
// showcase config (services without options) naturally yields just
|
|
86
|
+
// { appId } since these fields are absent.
|
|
87
|
+
const initialQueryOptions = {
|
|
88
|
+
appId,
|
|
89
|
+
filter: config.options?.filter,
|
|
90
|
+
availabilityFilter: config.options?.availabilityFilter,
|
|
91
|
+
sort: config.options?.sort,
|
|
92
|
+
};
|
|
93
|
+
// Initialize paging metadata separately (response data - doesn't trigger effect).
|
|
94
|
+
// Carries either offset (plain mode) or cursor (availability mode).
|
|
95
95
|
const initialPagingMetadata = config.options
|
|
96
96
|
?.pagingMetadata || {
|
|
97
97
|
limit: DEFAULT_QUERY_LIMIT,
|
|
@@ -155,6 +155,10 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
155
155
|
return true;
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
|
+
// Computed signal indicating whether an availability filter is currently
|
|
159
|
+
// applied. Reuses the same predicate as the API dispatch so the two can't
|
|
160
|
+
// drift. Exposed for the availability filter UI (follow-up PR).
|
|
161
|
+
const hasAvailabilityFilterSignal = signalsService.computed(() => hasActiveAvailabilityFilter(queryOptionsSignal.get().availabilityFilter));
|
|
158
162
|
// Computed signal for effective filter - combines queryOptions.filter with location from BookingService
|
|
159
163
|
const effectiveFilterSignal = signalsService.computed(() => {
|
|
160
164
|
const filter = queryOptionsSignal.get().filter;
|
|
@@ -171,23 +175,21 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
171
175
|
: {}),
|
|
172
176
|
};
|
|
173
177
|
});
|
|
174
|
-
// Initial fetch - runs once on client when no prefetched data
|
|
175
|
-
// Fetches both services AND categories in parallel
|
|
178
|
+
// Initial fetch - runs once on client when no prefetched data.
|
|
179
|
+
// Fetches both services AND categories in parallel.
|
|
176
180
|
if (typeof window !== 'undefined' && !hasInitialServices) {
|
|
177
|
-
const
|
|
178
|
-
const currentPaging = pagingMetadataSignal.peek();
|
|
179
|
-
// Use effectiveFilterSignal.peek() to get the computed filter value
|
|
180
|
-
const effectiveFilter = effectiveFilterSignal.peek();
|
|
181
|
+
const options = queryOptionsSignal.peek();
|
|
181
182
|
isLoadingSignal.set(true);
|
|
182
183
|
Promise.allSettled([
|
|
183
184
|
queryServices({
|
|
184
|
-
appId:
|
|
185
|
-
filter:
|
|
185
|
+
appId: options.appId,
|
|
186
|
+
filter: effectiveFilterSignal.peek(),
|
|
187
|
+
availabilityFilter: options.availabilityFilter,
|
|
186
188
|
pagingMetadata: {
|
|
187
|
-
limit:
|
|
189
|
+
limit: pagingMetadataSignal.peek().limit,
|
|
188
190
|
offset: 0,
|
|
189
191
|
},
|
|
190
|
-
sort:
|
|
192
|
+
sort: options.sort,
|
|
191
193
|
}),
|
|
192
194
|
!hasInitialCategories ? queryCategories() : Promise.resolve(null),
|
|
193
195
|
])
|
|
@@ -211,37 +213,34 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
211
213
|
isLoadingSignal.set(false);
|
|
212
214
|
});
|
|
213
215
|
}
|
|
214
|
-
//
|
|
215
|
-
// IMPORTANT: Subscribes to effectiveFilterSignal which includes queryOptions.filter + location
|
|
216
|
+
// Re-fetch when query options or the effective filter (filter + location) change.
|
|
216
217
|
if (typeof window !== 'undefined') {
|
|
217
218
|
let isFirstEffectRun = true;
|
|
218
219
|
signalsService.effect(async () => {
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
const queryOptions = queryOptionsSignal.get();
|
|
220
|
+
// Read both signals to establish dependencies.
|
|
221
|
+
const options = queryOptionsSignal.get();
|
|
222
222
|
const effectiveFilter = effectiveFilterSignal.get();
|
|
223
|
-
// Skip first run - initial fetch is handled separately above
|
|
223
|
+
// Skip first run - initial fetch is handled separately above.
|
|
224
224
|
if (isFirstEffectRun) {
|
|
225
225
|
isFirstEffectRun = false;
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
|
-
// Read paging metadata outside of dependency tracking for the fetch
|
|
229
|
-
const currentPaging = pagingMetadataSignal.peek();
|
|
230
228
|
try {
|
|
231
229
|
isLoadingSignal.set(true);
|
|
232
230
|
servicesSignal.set([]);
|
|
233
231
|
errorSignal.set(null);
|
|
232
|
+
// Reset to the first page when query options change.
|
|
234
233
|
const result = await queryServices({
|
|
235
|
-
appId:
|
|
234
|
+
appId: options.appId,
|
|
236
235
|
filter: effectiveFilter,
|
|
236
|
+
availabilityFilter: options.availabilityFilter,
|
|
237
237
|
pagingMetadata: {
|
|
238
|
-
limit:
|
|
239
|
-
offset: 0,
|
|
238
|
+
limit: pagingMetadataSignal.peek().limit,
|
|
239
|
+
offset: 0,
|
|
240
240
|
},
|
|
241
|
-
sort:
|
|
241
|
+
sort: options.sort,
|
|
242
242
|
});
|
|
243
243
|
servicesSignal.set(result.services);
|
|
244
|
-
// Update ONLY pagingMetadataSignal (not queryOptionsSignal) to avoid re-triggering effect
|
|
245
244
|
pagingMetadataSignal.set(result.pagingMetadata);
|
|
246
245
|
}
|
|
247
246
|
catch (error) {
|
|
@@ -252,29 +251,25 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
252
251
|
}
|
|
253
252
|
});
|
|
254
253
|
}
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
const currentServicesCount = servicesSignal.get().length;
|
|
259
|
-
const nextOffset = currentServicesCount;
|
|
260
|
-
const nextPaging = {
|
|
261
|
-
limit: count || DEFAULT_QUERY_LIMIT,
|
|
262
|
-
offset: nextOffset,
|
|
263
|
-
};
|
|
264
|
-
// Get effective filter from computed signal (includes location from BookingService)
|
|
265
|
-
const effectiveFilter = effectiveFilterSignal.get();
|
|
254
|
+
const loadMore = async (count) => {
|
|
255
|
+
const options = queryOptionsSignal.peek();
|
|
256
|
+
const currentPaging = pagingMetadataSignal.peek();
|
|
266
257
|
try {
|
|
267
258
|
isLoadingSignal.set(true);
|
|
268
259
|
errorSignal.set(null);
|
|
269
260
|
const result = await queryServices({
|
|
270
|
-
appId:
|
|
271
|
-
filter:
|
|
272
|
-
|
|
273
|
-
|
|
261
|
+
appId: options.appId,
|
|
262
|
+
filter: effectiveFilterSignal.peek(),
|
|
263
|
+
availabilityFilter: options.availabilityFilter,
|
|
264
|
+
pagingMetadata: {
|
|
265
|
+
limit: count ?? currentPaging.limit ?? DEFAULT_QUERY_LIMIT,
|
|
266
|
+
// Offset (plain mode) and cursor (availability mode) — queryServices uses what it needs.
|
|
267
|
+
offset: servicesSignal.peek().length,
|
|
268
|
+
cursor: currentPaging.cursor,
|
|
269
|
+
},
|
|
270
|
+
sort: options.sort,
|
|
274
271
|
});
|
|
275
|
-
// Append new services to existing ones
|
|
276
272
|
servicesSignal.set([...servicesSignal.get(), ...result.services]);
|
|
277
|
-
// Update ONLY pagingMetadataSignal (not queryOptionsSignal)
|
|
278
273
|
pagingMetadataSignal.set(result.pagingMetadata);
|
|
279
274
|
}
|
|
280
275
|
catch (error) {
|
|
@@ -293,101 +288,81 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
293
288
|
categoriesSignal,
|
|
294
289
|
filterOptionsSignal,
|
|
295
290
|
isFilteredSignal,
|
|
291
|
+
hasAvailabilityFilterSignal,
|
|
296
292
|
effectiveFilterSignal,
|
|
297
293
|
actions: {
|
|
298
|
-
loadMore
|
|
299
|
-
await loadMoreCursor(count);
|
|
300
|
-
},
|
|
294
|
+
loadMore,
|
|
301
295
|
setQueryOptions: (options) => {
|
|
302
|
-
// Reset services and paging when query options change
|
|
296
|
+
// Reset services and paging when query options change.
|
|
303
297
|
servicesSignal.set([]);
|
|
304
298
|
pagingMetadataSignal.set({
|
|
305
299
|
limit: pagingMetadataSignal.get().limit || DEFAULT_QUERY_LIMIT,
|
|
306
300
|
offset: 0,
|
|
307
301
|
count: 0,
|
|
308
302
|
hasNext: false,
|
|
303
|
+
cursor: undefined,
|
|
309
304
|
});
|
|
310
305
|
queryOptionsSignal.set(options);
|
|
311
306
|
},
|
|
312
307
|
setFilter: (filter) => {
|
|
313
|
-
|
|
314
|
-
const currentOptions = queryOptionsSignal.get();
|
|
315
|
-
queryOptionsSignal.set({
|
|
316
|
-
...currentOptions,
|
|
317
|
-
filter,
|
|
318
|
-
});
|
|
308
|
+
queryOptionsSignal.set({ ...queryOptionsSignal.get(), filter });
|
|
319
309
|
},
|
|
320
310
|
resetFilter: () => {
|
|
321
|
-
// Clear filter from query options (triggers re-fetch)
|
|
322
|
-
const currentOptions = queryOptionsSignal.get();
|
|
323
311
|
queryOptionsSignal.set({
|
|
324
|
-
...
|
|
312
|
+
...queryOptionsSignal.get(),
|
|
325
313
|
filter: undefined,
|
|
326
314
|
});
|
|
327
315
|
},
|
|
328
316
|
resetFilterField: (fieldName) => {
|
|
329
|
-
|
|
330
|
-
const currentFilter = queryOptionsSignal.get().filter || {};
|
|
331
|
-
const newFilter = { ...currentFilter };
|
|
332
|
-
// Remove the specific field
|
|
317
|
+
const newFilter = { ...(queryOptionsSignal.get().filter || {}) };
|
|
333
318
|
delete newFilter[fieldName];
|
|
334
|
-
// Update query options with new filter (triggers re-fetch)
|
|
335
|
-
const currentOptions = queryOptionsSignal.get();
|
|
336
319
|
queryOptionsSignal.set({
|
|
337
|
-
...
|
|
320
|
+
...queryOptionsSignal.get(),
|
|
338
321
|
filter: Object.keys(newFilter).length > 0 ? newFilter : undefined,
|
|
339
322
|
});
|
|
340
323
|
},
|
|
341
324
|
},
|
|
342
|
-
hasMoreServicesSignal: signalsService.computed(() =>
|
|
343
|
-
const paging = pagingMetadataSignal.get();
|
|
344
|
-
// Use hasNext flag from pagingMetadata to determine if more services are available
|
|
345
|
-
return paging.hasNext ?? false;
|
|
346
|
-
}),
|
|
325
|
+
hasMoreServicesSignal: signalsService.computed(() => pagingMetadataSignal.get().hasNext ?? false),
|
|
347
326
|
};
|
|
348
327
|
});
|
|
349
328
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* to pre-fetch services data before the React components are hydrated on the client.
|
|
353
|
-
* The returned config includes paging metadata with hasNext flag indicating if more services are available.
|
|
329
|
+
* Loads service list configuration for SSR, pre-fetching services and categories.
|
|
330
|
+
* Pass an `availabilityFilter` to query availability-aware services.
|
|
354
331
|
*
|
|
355
|
-
* @param options - Query options
|
|
356
|
-
* @returns A
|
|
332
|
+
* @param options - Query options.
|
|
333
|
+
* @returns A success result with the config, or an error result.
|
|
357
334
|
*
|
|
358
335
|
* @example
|
|
359
336
|
* ```ts
|
|
360
|
-
* //
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* // Load with query options
|
|
364
|
-
* const serviceListResult = await loadServiceListServiceInitialData({
|
|
337
|
+
* // Plain offset query
|
|
338
|
+
* const result = await loadServiceListServiceInitialData({
|
|
365
339
|
* appId: '13d21c63-b5ec-5912-8397-c3a5ddb27a97',
|
|
366
340
|
* pagingMetadata: { limit: 20, offset: 0 },
|
|
367
341
|
* });
|
|
368
342
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
* // More services can be loaded
|
|
379
|
-
* }
|
|
343
|
+
* // Availability-aware query (cursor paging + `available` flag)
|
|
344
|
+
* const availabilityResult = await loadServiceListServiceInitialData({
|
|
345
|
+
* availabilityFilter: {
|
|
346
|
+
* localStartDate: '2026-06-01T09:00:00',
|
|
347
|
+
* localEndDate: '2026-06-07T17:00:00',
|
|
348
|
+
* timeZone: 'America/New_York',
|
|
349
|
+
* },
|
|
350
|
+
* pagingMetadata: { limit: 20 },
|
|
351
|
+
* });
|
|
380
352
|
* ```
|
|
381
353
|
*/
|
|
382
354
|
export async function loadServiceListServiceInitialData(options) {
|
|
383
355
|
try {
|
|
384
|
-
|
|
356
|
+
const appId = options.appId ?? BOOKING_APP_ID;
|
|
357
|
+
// Fetch services and categories in parallel; categories failure is non-fatal.
|
|
358
|
+
// queryServices picks the endpoint by the presence of availabilityFilter.
|
|
385
359
|
const [servicesResult, categoriesResult] = await Promise.all([
|
|
386
360
|
queryServices({
|
|
387
|
-
appId
|
|
361
|
+
appId,
|
|
388
362
|
filter: options.filter,
|
|
389
363
|
pagingMetadata: options.pagingMetadata,
|
|
390
364
|
sort: options.sort,
|
|
365
|
+
availabilityFilter: options.availabilityFilter,
|
|
391
366
|
}),
|
|
392
367
|
queryCategories().catch(() => null),
|
|
393
368
|
]);
|
|
@@ -397,7 +372,10 @@ export async function loadServiceListServiceInitialData(options) {
|
|
|
397
372
|
services: servicesResult.services,
|
|
398
373
|
categories: categoriesResult?.categories ?? [],
|
|
399
374
|
options: {
|
|
400
|
-
|
|
375
|
+
appId,
|
|
376
|
+
filter: options.filter,
|
|
377
|
+
sort: options.sort,
|
|
378
|
+
availabilityFilter: options.availabilityFilter,
|
|
401
379
|
pagingMetadata: servicesResult.pagingMetadata,
|
|
402
380
|
},
|
|
403
381
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/headless-bookings",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"scripts": {
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"build": "npm run build:esm && npm run build:cjs",
|
|
9
9
|
"build:esm": "tsc -p tsconfig.json",
|
|
10
10
|
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
11
|
+
"storybook": "storybook dev -p 6006",
|
|
12
|
+
"storybook:build": "storybook build",
|
|
11
13
|
"test": "vitest",
|
|
12
14
|
"lint:fix": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
|
|
13
15
|
"lint:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"dependencies": {
|
|
37
39
|
"@wix/auto_sdk_bookings_availability-time-slots": "^1.0.244",
|
|
38
40
|
"@wix/auto_sdk_bookings_bookings": "^1.0.192",
|
|
41
|
+
"@wix/auto_sdk_bookings_catalog-search": "^1.0.0",
|
|
39
42
|
"@wix/auto_sdk_bookings_event-time-slots": "^1.0.112",
|
|
40
43
|
"@wix/auto_sdk_bookings_pricing": "^1.0.87",
|
|
41
44
|
"@wix/auto_sdk_bookings_services": "^1.0.226",
|
|
@@ -43,23 +46,35 @@
|
|
|
43
46
|
"@wix/forms": "^1.0.384",
|
|
44
47
|
"@wix/headless-media": "0.0.25",
|
|
45
48
|
"@wix/headless-utils": "0.0.12",
|
|
49
|
+
"@wix/sdk-runtime": "^1.0.12",
|
|
50
|
+
"@wix/sdk-types": "^1.17.7",
|
|
46
51
|
"@wix/services-definitions": "^1.0.1",
|
|
47
52
|
"@wix/services-manager-react": "^1.0.3",
|
|
48
53
|
"date-fns": "^4.1.0"
|
|
49
54
|
},
|
|
50
55
|
"devDependencies": {
|
|
56
|
+
"@storybook/addon-essentials": "^8.6.0",
|
|
57
|
+
"@storybook/addon-interactions": "^8.6.0",
|
|
58
|
+
"@storybook/addon-links": "^8.6.0",
|
|
59
|
+
"@storybook/blocks": "^8.6.0",
|
|
60
|
+
"@storybook/react": "^8.6.0",
|
|
61
|
+
"@storybook/react-vite": "^8.6.0",
|
|
62
|
+
"@storybook/test": "^8.6.0",
|
|
51
63
|
"@testing-library/dom": "^10.4.0",
|
|
52
64
|
"@testing-library/jest-dom": "^6.6.3",
|
|
53
65
|
"@testing-library/react": "^16.3.0",
|
|
54
66
|
"@types/node": "^20.9.0",
|
|
55
67
|
"@types/react": "^18.2.0",
|
|
56
68
|
"@types/react-dom": "^18.2.0",
|
|
69
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
57
70
|
"@vitest/ui": "^3.1.4",
|
|
58
71
|
"@wix/cjs-esm-build-verifier": "1.0.0",
|
|
59
72
|
"@wix/headless-components": "0.0.0",
|
|
60
73
|
"jsdom": "^26.1.0",
|
|
61
74
|
"prettier": "^3.4.2",
|
|
75
|
+
"storybook": "^8.6.0",
|
|
62
76
|
"typescript": "^5.8.3",
|
|
77
|
+
"vite": "^6.3.1",
|
|
63
78
|
"vitest": "^3.1.4"
|
|
64
79
|
},
|
|
65
80
|
"peerDependencies": {
|
|
@@ -76,5 +91,5 @@
|
|
|
76
91
|
"groupId": "com.wixpress.headless-components"
|
|
77
92
|
}
|
|
78
93
|
},
|
|
79
|
-
"falconPackageHash": "
|
|
94
|
+
"falconPackageHash": "982db1d1e4f6337441d55feaf920c8b04f62f86edf8adc4b7d268a60"
|
|
80
95
|
}
|