@wix/headless-bookings 0.0.45 → 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.
- package/cjs/dist/__mocks__/services.d.ts +21 -0
- package/cjs/dist/__mocks__/services.js +54 -0
- package/cjs/dist/api/index.d.ts +1 -0
- package/cjs/dist/api/index.js +1 -0
- package/cjs/dist/api/query-categories/index.d.ts +34 -0
- package/cjs/dist/api/query-categories/index.js +28 -0
- package/cjs/dist/api/query-services/index.js +9 -2
- package/cjs/dist/react/core/service/Service.js +6 -0
- package/cjs/dist/react/core/service-list/ServiceListFilter.d.ts +55 -0
- package/cjs/dist/react/core/service-list/ServiceListFilter.js +59 -0
- package/cjs/dist/react/service/Service.d.ts +32 -3
- package/cjs/dist/react/service/Service.js +25 -2
- package/cjs/dist/react/service-list/ServiceList.d.ts +73 -3
- package/cjs/dist/react/service-list/ServiceList.js +63 -7
- package/cjs/dist/services/service/service.js +23 -3
- package/cjs/dist/services/service-list/service-list.d.ts +14 -1
- package/cjs/dist/services/service-list/service-list.js +94 -12
- package/dist/__mocks__/services.d.ts +21 -0
- package/dist/__mocks__/services.js +54 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/query-categories/index.d.ts +34 -0
- package/dist/api/query-categories/index.js +28 -0
- package/dist/api/query-services/index.js +9 -2
- package/dist/react/core/service/Service.js +6 -0
- package/dist/react/core/service-list/ServiceListFilter.d.ts +55 -0
- package/dist/react/core/service-list/ServiceListFilter.js +59 -0
- package/dist/react/service/Service.d.ts +32 -3
- package/dist/react/service/Service.js +25 -2
- package/dist/react/service-list/ServiceList.d.ts +73 -3
- package/dist/react/service-list/ServiceList.js +63 -7
- package/dist/services/service/service.js +23 -3
- package/dist/services/service-list/service-list.d.ts +14 -1
- package/dist/services/service-list/service-list.js +94 -12
- package/package.json +2 -2
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Provides components for displaying services list with pagination
|
|
4
4
|
*/
|
|
5
5
|
import React from 'react';
|
|
6
|
-
import { ListVariant, GenericListLoadMoreRenderProps } from '@wix/headless-components/react';
|
|
6
|
+
import { ListVariant, GenericListLoadMoreRenderProps, Filter as FilterPrimitive } from '@wix/headless-components/react';
|
|
7
7
|
import { AsChildChildren } from '@wix/headless-utils/react';
|
|
8
8
|
import type { ServiceListServiceConfig } from '../../services/service-list/service-list.js';
|
|
9
9
|
import type { Service as ServiceType } from '@wix/auto_sdk_bookings_services';
|
|
@@ -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
|
|
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 />
|
|
@@ -185,3 +194,64 @@ export declare const LoadMore: React.ForwardRefExoticComponent<LoadMoreProps & R
|
|
|
185
194
|
export declare const Actions: {
|
|
186
195
|
LoadMore: React.ForwardRefExoticComponent<LoadMoreProps & React.RefAttributes<HTMLButtonElement>>;
|
|
187
196
|
};
|
|
197
|
+
/**
|
|
198
|
+
* Data available through asChild pattern
|
|
199
|
+
*/
|
|
200
|
+
export interface FilterAsChildProps {
|
|
201
|
+
/** Filter options configuration derived from categories */
|
|
202
|
+
filterOptions: FilterPrimitive.FilterOption[];
|
|
203
|
+
/** Current filter state */
|
|
204
|
+
currentFilter: FilterPrimitive.Filter;
|
|
205
|
+
/** Function to update filter */
|
|
206
|
+
updateFilter: (filter: FilterPrimitive.Filter) => void;
|
|
207
|
+
/** Function to clear all active filters */
|
|
208
|
+
clearFilters: () => void;
|
|
209
|
+
/** Whether any filters are currently active */
|
|
210
|
+
isFiltered: boolean;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Props for ServiceList Filter component
|
|
214
|
+
*/
|
|
215
|
+
export interface FilterProps {
|
|
216
|
+
/** Child components that will have access to filter functionality */
|
|
217
|
+
children: React.ReactNode | ((props: FilterAsChildProps) => React.ReactNode);
|
|
218
|
+
/** When true, the component will not render its own div wrapper */
|
|
219
|
+
asChild?: boolean;
|
|
220
|
+
/** CSS classes to apply to the filter container */
|
|
221
|
+
className?: string;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Filter component that provides filtering functionality for service lists.
|
|
225
|
+
* Integrates with ServiceListService for categories and BookingService for location.
|
|
226
|
+
*
|
|
227
|
+
* @component
|
|
228
|
+
* @example
|
|
229
|
+
* ```tsx
|
|
230
|
+
* import { Filter } from '@wix/headless-components/react';
|
|
231
|
+
*
|
|
232
|
+
* <ServiceList.Filter className="filter-sidebar">
|
|
233
|
+
* <Filter.FilterOptions>
|
|
234
|
+
* <Filter.FilterOptionRepeater>
|
|
235
|
+
* <Filter.FilterOption.Label />
|
|
236
|
+
* <Filter.FilterOption.SingleFilter />
|
|
237
|
+
* </Filter.FilterOptionRepeater>
|
|
238
|
+
* </Filter.FilterOptions>
|
|
239
|
+
* <Filter.Filtered>
|
|
240
|
+
* <Filter.Action.Clear label="Clear Filters" />
|
|
241
|
+
* </Filter.Filtered>
|
|
242
|
+
* </ServiceList.Filter>
|
|
243
|
+
*
|
|
244
|
+
* // With asChild pattern
|
|
245
|
+
* <ServiceList.Filter asChild>
|
|
246
|
+
* <aside className="filter-sidebar">
|
|
247
|
+
* <Filter.FilterOptions>
|
|
248
|
+
* <Filter.FilterOptionRepeater>
|
|
249
|
+
* <Filter.FilterOption.Label />
|
|
250
|
+
* <Filter.FilterOption.SingleFilter />
|
|
251
|
+
* </Filter.FilterOptionRepeater>
|
|
252
|
+
* </Filter.FilterOptions>
|
|
253
|
+
* </aside>
|
|
254
|
+
* </ServiceList.Filter>
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export declare const Filter: React.ForwardRefExoticComponent<FilterProps & React.RefAttributes<HTMLElement>>;
|
|
@@ -4,8 +4,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
4
4
|
* Provides components for displaying services list with pagination
|
|
5
5
|
*/
|
|
6
6
|
import React from 'react';
|
|
7
|
-
import { GenericList, } from '@wix/headless-components/react';
|
|
7
|
+
import { GenericList, Filter as FilterPrimitive, } from '@wix/headless-components/react';
|
|
8
|
+
import { AsChildSlot } from '@wix/headless-utils/react';
|
|
8
9
|
import * as CoreServiceList from '../core/service-list/ServiceList.js';
|
|
10
|
+
import * as CoreServiceListFilter from '../core/service-list/ServiceListFilter.js';
|
|
9
11
|
import * as Service from '../service/Service.js';
|
|
10
12
|
/**
|
|
11
13
|
* Booking app ID constant
|
|
@@ -17,6 +19,7 @@ var TestIds;
|
|
|
17
19
|
TestIds["serviceListServices"] = "service-list-services";
|
|
18
20
|
TestIds["serviceListService"] = "service-list-service";
|
|
19
21
|
TestIds["serviceListLoadMore"] = "service-list-load-more";
|
|
22
|
+
TestIds["serviceListFilter"] = "service-list-filter";
|
|
20
23
|
})(TestIds || (TestIds = {}));
|
|
21
24
|
/**
|
|
22
25
|
* Root component that provides the ServiceList service context for rendering services lists.
|
|
@@ -70,22 +73,26 @@ const DEFAULT_PAGE_SIZE = 100;
|
|
|
70
73
|
*/
|
|
71
74
|
const RootContent = React.forwardRef((props, ref) => {
|
|
72
75
|
const { children, className, variant, pageSize = DEFAULT_PAGE_SIZE } = props;
|
|
73
|
-
return (_jsx(CoreServiceList.Services, { children: ({ services: servicesList, isLoading, hasMore, loadMore }) => {
|
|
76
|
+
return (_jsx(CoreServiceList.Services, { children: ({ services: servicesList, isLoading, hasMore, loadMore, error }) => {
|
|
74
77
|
const items = servicesList.map((service) => ({
|
|
75
78
|
...service,
|
|
76
79
|
id: service._id,
|
|
77
80
|
}));
|
|
78
|
-
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 }));
|
|
79
82
|
} }));
|
|
80
83
|
});
|
|
81
84
|
/**
|
|
82
|
-
* Container for the services list with empty state support.
|
|
85
|
+
* Container for the services list with empty, loading, and error state support.
|
|
83
86
|
* Wraps GenericList.Items. Follows List Container Level pattern.
|
|
84
87
|
*
|
|
85
88
|
* @component
|
|
86
89
|
* @example
|
|
87
90
|
* ```tsx
|
|
88
|
-
* <ServiceList.Services
|
|
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
|
+
* >
|
|
89
96
|
* <ServiceList.ServiceRepeater>
|
|
90
97
|
* <Service.Name />
|
|
91
98
|
* <Service.Price />
|
|
@@ -94,8 +101,8 @@ const RootContent = React.forwardRef((props, ref) => {
|
|
|
94
101
|
* ```
|
|
95
102
|
*/
|
|
96
103
|
export const Services = React.forwardRef((props, ref) => {
|
|
97
|
-
const { children, ...otherProps } = props;
|
|
98
|
-
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 }));
|
|
99
106
|
});
|
|
100
107
|
Services.displayName = 'ServiceList.Services';
|
|
101
108
|
/**
|
|
@@ -170,3 +177,52 @@ LoadMore.displayName = 'ServiceList.Actions.LoadMore';
|
|
|
170
177
|
export const Actions = {
|
|
171
178
|
LoadMore,
|
|
172
179
|
};
|
|
180
|
+
/**
|
|
181
|
+
* Filter component that provides filtering functionality for service lists.
|
|
182
|
+
* Integrates with ServiceListService for categories and BookingService for location.
|
|
183
|
+
*
|
|
184
|
+
* @component
|
|
185
|
+
* @example
|
|
186
|
+
* ```tsx
|
|
187
|
+
* import { Filter } from '@wix/headless-components/react';
|
|
188
|
+
*
|
|
189
|
+
* <ServiceList.Filter className="filter-sidebar">
|
|
190
|
+
* <Filter.FilterOptions>
|
|
191
|
+
* <Filter.FilterOptionRepeater>
|
|
192
|
+
* <Filter.FilterOption.Label />
|
|
193
|
+
* <Filter.FilterOption.SingleFilter />
|
|
194
|
+
* </Filter.FilterOptionRepeater>
|
|
195
|
+
* </Filter.FilterOptions>
|
|
196
|
+
* <Filter.Filtered>
|
|
197
|
+
* <Filter.Action.Clear label="Clear Filters" />
|
|
198
|
+
* </Filter.Filtered>
|
|
199
|
+
* </ServiceList.Filter>
|
|
200
|
+
*
|
|
201
|
+
* // With asChild pattern
|
|
202
|
+
* <ServiceList.Filter asChild>
|
|
203
|
+
* <aside className="filter-sidebar">
|
|
204
|
+
* <Filter.FilterOptions>
|
|
205
|
+
* <Filter.FilterOptionRepeater>
|
|
206
|
+
* <Filter.FilterOption.Label />
|
|
207
|
+
* <Filter.FilterOption.SingleFilter />
|
|
208
|
+
* </Filter.FilterOptionRepeater>
|
|
209
|
+
* </Filter.FilterOptions>
|
|
210
|
+
* </aside>
|
|
211
|
+
* </ServiceList.Filter>
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export const Filter = React.forwardRef((props, ref) => {
|
|
215
|
+
const { children, asChild, className, ...otherProps } = props;
|
|
216
|
+
return (_jsx(CoreServiceListFilter.Root, { children: ({ filterOptions, currentFilter, updateFilter, clearFilters, isFiltered, }) => {
|
|
217
|
+
// Prepare asChildProps for function children
|
|
218
|
+
const asChildProps = {
|
|
219
|
+
filterOptions,
|
|
220
|
+
currentFilter: currentFilter,
|
|
221
|
+
updateFilter: (value) => updateFilter(value),
|
|
222
|
+
clearFilters,
|
|
223
|
+
isFiltered,
|
|
224
|
+
};
|
|
225
|
+
return (_jsx(FilterPrimitive.Root, { value: currentFilter, onChange: (value) => updateFilter(value), filterOptions: filterOptions, children: _jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceListFilter, "data-is-filtered": isFiltered, customElement: children, customElementProps: asChildProps, ...otherProps, children: children }) }));
|
|
226
|
+
} }));
|
|
227
|
+
});
|
|
228
|
+
Filter.displayName = 'ServiceList.Filter';
|
|
@@ -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
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { type Signal, type ReadOnlySignal } from '@wix/services-definitions/core-services/signals';
|
|
6
6
|
import type { Service } from '@wix/auto_sdk_bookings_services';
|
|
7
|
-
import type { QueryServicesRequest, PagingMetadata } from '../../api/query-services/index.js';
|
|
7
|
+
import type { QueryServicesRequest, PagingMetadata, ServiceListFilters } from '../../api/query-services/index.js';
|
|
8
|
+
import { type Category } from '../../api/query-categories/index.js';
|
|
9
|
+
import type { FilterOption } from '@wix/headless-components/react';
|
|
8
10
|
export type { ServiceListFilters } from '../../api/query-services/index.js';
|
|
9
11
|
/**
|
|
10
12
|
* Query options without paging metadata (user-controlled request params)
|
|
@@ -16,6 +18,7 @@ export type QueryOptions = Omit<QueryServicesRequest, 'pagingMetadata'>;
|
|
|
16
18
|
*/
|
|
17
19
|
export interface ServiceListServiceConfig {
|
|
18
20
|
services?: Service[];
|
|
21
|
+
categories?: Category[];
|
|
19
22
|
options?: QueryServicesRequest;
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
@@ -24,6 +27,8 @@ export interface ServiceListServiceConfig {
|
|
|
24
27
|
export interface ServiceListActions {
|
|
25
28
|
loadMore: (count: number) => Promise<void>;
|
|
26
29
|
setQueryOptions: (options: QueryOptions) => void;
|
|
30
|
+
setFilter: (filter: ServiceListFilters) => void;
|
|
31
|
+
resetFilter: () => void;
|
|
27
32
|
}
|
|
28
33
|
/**
|
|
29
34
|
* API interface for the ServiceList service
|
|
@@ -39,6 +44,14 @@ export interface ServiceListServiceAPI {
|
|
|
39
44
|
isLoadingSignal: Signal<boolean>;
|
|
40
45
|
/** Reactive signal containing any error message, or null if no error */
|
|
41
46
|
errorSignal: Signal<string | null>;
|
|
47
|
+
/** Reactive signal containing available categories for filtering */
|
|
48
|
+
categoriesSignal: Signal<Category[]>;
|
|
49
|
+
/** Reactive signal containing current filter state */
|
|
50
|
+
filterSignal: Signal<ServiceListFilters | null>;
|
|
51
|
+
/** Computed signal containing filter options derived from categories */
|
|
52
|
+
filterOptionsSignal: ReadOnlySignal<FilterOption[]>;
|
|
53
|
+
/** Computed signal indicating if any filters are active */
|
|
54
|
+
isFilteredSignal: ReadOnlySignal<boolean>;
|
|
42
55
|
/** Actions object containing query actions */
|
|
43
56
|
actions: ServiceListActions;
|
|
44
57
|
/** Reactive signal indicating if there are more services to load */
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { defineService, implementService } from '@wix/services-definitions';
|
|
6
6
|
import { SignalsServiceDefinition, } from '@wix/services-definitions/core-services/signals';
|
|
7
7
|
import { queryServices } from '../../api/query-services/index.js';
|
|
8
|
+
import { queryCategories, } from '../../api/query-categories/index.js';
|
|
9
|
+
import { BookingServiceDefinition } from '../booking/booking.js';
|
|
8
10
|
/**
|
|
9
11
|
* Booking app ID constant
|
|
10
12
|
*/
|
|
@@ -73,6 +75,7 @@ export const ServiceListServiceDefinition = defineService('service-list');
|
|
|
73
75
|
export const ServiceListService = implementService.withConfig()(ServiceListServiceDefinition, ({ getService, config }) => {
|
|
74
76
|
let firstRun = true;
|
|
75
77
|
const signalsService = getService(SignalsServiceDefinition);
|
|
78
|
+
const bookingService = getService(BookingServiceDefinition);
|
|
76
79
|
// Default appId to BOOKING_APP_ID if not provided
|
|
77
80
|
const appId = config.options?.appId ?? BOOKING_APP_ID;
|
|
78
81
|
// Check if services are provided (showcase mode vs query mode)
|
|
@@ -111,13 +114,59 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
111
114
|
const pagingMetadataSignal = signalsService.signal(initialPagingMetadata);
|
|
112
115
|
const isLoadingSignal = signalsService.signal(false);
|
|
113
116
|
const errorSignal = signalsService.signal(null);
|
|
117
|
+
// Categories signal - initialized from config
|
|
118
|
+
const categoriesSignal = signalsService.signal(config.categories || []);
|
|
119
|
+
// Filter signal - current filter state (separate from queryOptions for UI)
|
|
120
|
+
const filterSignal = signalsService.signal(config.options?.filters || null);
|
|
121
|
+
// Computed filter options from categories
|
|
122
|
+
const filterOptionsSignal = signalsService.computed(() => {
|
|
123
|
+
const categories = categoriesSignal.get();
|
|
124
|
+
if (!categories.length)
|
|
125
|
+
return [];
|
|
126
|
+
return [
|
|
127
|
+
{
|
|
128
|
+
key: 'category',
|
|
129
|
+
label: 'Category',
|
|
130
|
+
type: 'single',
|
|
131
|
+
displayType: 'text',
|
|
132
|
+
fieldName: 'categoryIds',
|
|
133
|
+
validValues: categories
|
|
134
|
+
.map((c) => c._id)
|
|
135
|
+
.filter((id) => Boolean(id)),
|
|
136
|
+
valueFormatter: (id) => {
|
|
137
|
+
const cat = categories.find((c) => c._id === id);
|
|
138
|
+
return cat?.name || String(id);
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
});
|
|
143
|
+
// Computed signal for filtered state
|
|
144
|
+
const isFilteredSignal = signalsService.computed(() => {
|
|
145
|
+
const filter = filterSignal.get();
|
|
146
|
+
return !!(filter?.categoryIds?.length || filter?.locationIds?.length);
|
|
147
|
+
});
|
|
148
|
+
// Helper to get effective filters including location from BookingService
|
|
149
|
+
const getEffectiveFilters = (baseFilters) => {
|
|
150
|
+
const selectedLocation = bookingService.location.get();
|
|
151
|
+
const locationId = selectedLocation?.business?._id;
|
|
152
|
+
if (!baseFilters && !locationId) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
...baseFilters,
|
|
157
|
+
...(locationId ? { locationIds: [locationId] } : {}),
|
|
158
|
+
};
|
|
159
|
+
};
|
|
114
160
|
// Auto-query on queryOptions changes (client-side only)
|
|
115
|
-
// IMPORTANT: Only subscribes to queryOptionsSignal
|
|
161
|
+
// IMPORTANT: Only subscribes to queryOptionsSignal and bookingService.location
|
|
116
162
|
if (typeof window !== 'undefined') {
|
|
117
163
|
signalsService.effect(async () => {
|
|
118
164
|
// CRITICAL: Read queryOptionsSignal to establish dependency
|
|
119
165
|
// Do NOT read pagingMetadataSignal here to avoid infinite loop
|
|
120
166
|
const queryOptions = queryOptionsSignal.get();
|
|
167
|
+
// Also subscribe to location changes from BookingService
|
|
168
|
+
// Reading the signal here establishes the dependency
|
|
169
|
+
bookingService.location.get();
|
|
121
170
|
if (firstRun) {
|
|
122
171
|
firstRun = false;
|
|
123
172
|
// If we have initial services (showcase mode), don't query on first run
|
|
@@ -126,13 +175,15 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
126
175
|
}
|
|
127
176
|
}
|
|
128
177
|
// Read paging metadata outside of dependency tracking for the fetch
|
|
129
|
-
const currentPaging = pagingMetadataSignal.
|
|
178
|
+
const currentPaging = pagingMetadataSignal.peek();
|
|
179
|
+
// Get effective filters including location from BookingService
|
|
180
|
+
const effectiveFilters = getEffectiveFilters(queryOptions.filters);
|
|
130
181
|
try {
|
|
131
182
|
isLoadingSignal.set(true);
|
|
132
183
|
errorSignal.set(null);
|
|
133
184
|
const result = await queryServices({
|
|
134
185
|
appId: queryOptions.appId,
|
|
135
|
-
filters:
|
|
186
|
+
filters: effectiveFilters,
|
|
136
187
|
pagingMetadata: {
|
|
137
188
|
limit: currentPaging.limit,
|
|
138
189
|
offset: 0, // Reset to first page when query options change
|
|
@@ -162,12 +213,14 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
162
213
|
offset: nextOffset,
|
|
163
214
|
hasNext: false, // Will be updated after query
|
|
164
215
|
};
|
|
216
|
+
// Get effective filters including location from BookingService
|
|
217
|
+
const effectiveFilters = getEffectiveFilters(currentQueryOptions.filters);
|
|
165
218
|
try {
|
|
166
219
|
isLoadingSignal.set(true);
|
|
167
220
|
errorSignal.set(null);
|
|
168
221
|
const result = await queryServices({
|
|
169
222
|
appId: currentQueryOptions.appId,
|
|
170
|
-
filters:
|
|
223
|
+
filters: effectiveFilters,
|
|
171
224
|
pagingMetadata: nextPaging,
|
|
172
225
|
sort: currentQueryOptions.sort,
|
|
173
226
|
});
|
|
@@ -189,6 +242,10 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
189
242
|
pagingMetadataSignal,
|
|
190
243
|
isLoadingSignal,
|
|
191
244
|
errorSignal,
|
|
245
|
+
categoriesSignal,
|
|
246
|
+
filterSignal,
|
|
247
|
+
filterOptionsSignal,
|
|
248
|
+
isFilteredSignal,
|
|
192
249
|
actions: {
|
|
193
250
|
loadMore: async (count) => {
|
|
194
251
|
await loadMoreCursor(count);
|
|
@@ -204,6 +261,26 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
204
261
|
});
|
|
205
262
|
queryOptionsSignal.set(options);
|
|
206
263
|
},
|
|
264
|
+
setFilter: (filter) => {
|
|
265
|
+
// Update filter signal
|
|
266
|
+
filterSignal.set(filter);
|
|
267
|
+
// Update query options with new filters (triggers re-fetch)
|
|
268
|
+
const currentOptions = queryOptionsSignal.get();
|
|
269
|
+
queryOptionsSignal.set({
|
|
270
|
+
...currentOptions,
|
|
271
|
+
filters: filter,
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
resetFilter: () => {
|
|
275
|
+
// Clear filter signal
|
|
276
|
+
filterSignal.set(null);
|
|
277
|
+
// Clear filters from query options (triggers re-fetch)
|
|
278
|
+
const currentOptions = queryOptionsSignal.get();
|
|
279
|
+
queryOptionsSignal.set({
|
|
280
|
+
...currentOptions,
|
|
281
|
+
filters: undefined,
|
|
282
|
+
});
|
|
283
|
+
},
|
|
207
284
|
},
|
|
208
285
|
hasMoreServicesSignal: signalsService.computed(() => {
|
|
209
286
|
const paging = pagingMetadataSignal.get();
|
|
@@ -247,19 +324,24 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
|
|
|
247
324
|
*/
|
|
248
325
|
export async function loadServiceListServiceInitialData(options) {
|
|
249
326
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
327
|
+
// Fetch services and categories in parallel
|
|
328
|
+
const [servicesResult, categoriesResult] = await Promise.all([
|
|
329
|
+
queryServices({
|
|
330
|
+
appId: options.appId,
|
|
331
|
+
filters: options.filters,
|
|
332
|
+
pagingMetadata: options.pagingMetadata,
|
|
333
|
+
sort: options.sort,
|
|
334
|
+
}),
|
|
335
|
+
queryCategories(),
|
|
336
|
+
]);
|
|
256
337
|
return {
|
|
257
338
|
type: 'success',
|
|
258
339
|
config: {
|
|
259
|
-
services:
|
|
340
|
+
services: servicesResult.services,
|
|
341
|
+
categories: categoriesResult.categories,
|
|
260
342
|
options: {
|
|
261
343
|
...options,
|
|
262
|
-
pagingMetadata:
|
|
344
|
+
pagingMetadata: servicesResult.pagingMetadata,
|
|
263
345
|
},
|
|
264
346
|
},
|
|
265
347
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/headless-bookings",
|
|
3
|
-
"version": "0.0.
|
|
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": "
|
|
75
|
+
"falconPackageHash": "b4efe3be968a8e8d6806b0e818d5e30742584c3d834296262bc1572f"
|
|
76
76
|
}
|