@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.
Files changed (35) hide show
  1. package/cjs/dist/__mocks__/services.d.ts +21 -0
  2. package/cjs/dist/__mocks__/services.js +54 -0
  3. package/cjs/dist/api/index.d.ts +1 -0
  4. package/cjs/dist/api/index.js +1 -0
  5. package/cjs/dist/api/query-categories/index.d.ts +34 -0
  6. package/cjs/dist/api/query-categories/index.js +28 -0
  7. package/cjs/dist/api/query-services/index.js +9 -2
  8. package/cjs/dist/react/core/service/Service.js +6 -0
  9. package/cjs/dist/react/core/service-list/ServiceListFilter.d.ts +55 -0
  10. package/cjs/dist/react/core/service-list/ServiceListFilter.js +59 -0
  11. package/cjs/dist/react/service/Service.d.ts +32 -3
  12. package/cjs/dist/react/service/Service.js +25 -2
  13. package/cjs/dist/react/service-list/ServiceList.d.ts +73 -3
  14. package/cjs/dist/react/service-list/ServiceList.js +63 -7
  15. package/cjs/dist/services/service/service.js +23 -3
  16. package/cjs/dist/services/service-list/service-list.d.ts +14 -1
  17. package/cjs/dist/services/service-list/service-list.js +94 -12
  18. package/dist/__mocks__/services.d.ts +21 -0
  19. package/dist/__mocks__/services.js +54 -0
  20. package/dist/api/index.d.ts +1 -0
  21. package/dist/api/index.js +1 -0
  22. package/dist/api/query-categories/index.d.ts +34 -0
  23. package/dist/api/query-categories/index.js +28 -0
  24. package/dist/api/query-services/index.js +9 -2
  25. package/dist/react/core/service/Service.js +6 -0
  26. package/dist/react/core/service-list/ServiceListFilter.d.ts +55 -0
  27. package/dist/react/core/service-list/ServiceListFilter.js +59 -0
  28. package/dist/react/service/Service.d.ts +32 -3
  29. package/dist/react/service/Service.js +25 -2
  30. package/dist/react/service-list/ServiceList.d.ts +73 -3
  31. package/dist/react/service-list/ServiceList.js +63 -7
  32. package/dist/services/service/service.js +23 -3
  33. package/dist/services/service-list/service-list.d.ts +14 -1
  34. package/dist/services/service-list/service-list.js +94 -12
  35. 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
- * Requires exactly one of: service, serviceId, or serviceSlug.
52
+ * Can be used with:
53
+ * - service: Pre-loaded service object (SSR pattern)
54
+ * - serviceId: Service ID for dynamic loading
55
+ * - serviceSlug: Service slug for dynamic loading
56
+ * - No config: Reads from BookingService's selected service (requires BookingService)
53
57
  *
54
58
  * @order 1
55
59
  * @component
@@ -86,11 +90,30 @@ var TestIds;
86
90
  * </Service.Root>
87
91
  * );
88
92
  * }
93
+ *
94
+ * // Reading from BookingService's selected service
95
+ * // (requires Booking.Root parent context)
96
+ * function SelectedServiceDisplay() {
97
+ * return (
98
+ * <Service.Root>
99
+ * <Service.Name />
100
+ * <Service.Price />
101
+ * </Service.Root>
102
+ * );
103
+ * }
89
104
  * ```
90
105
  */
91
106
  export const Root = React.forwardRef((props, ref) => {
92
107
  const { children, service, serviceId, serviceSlug, appId, asChild, className, ...attrs } = props;
93
- return (_jsx(CoreService.Root, { serviceServiceConfig: { service, serviceId, serviceSlug, appId }, children: ({ selected, bookable }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceRoot, "data-selected": selected, "data-bookable": bookable, customElement: children, customElementProps: { selected, bookable }, ...attrs, children: children })) }));
108
+ // Build config - when no service config is provided, pass empty object
109
+ // to trigger "from booking signal" mode in ServiceService
110
+ const serviceServiceConfig = {
111
+ service,
112
+ serviceId,
113
+ serviceSlug,
114
+ appId,
115
+ };
116
+ return (_jsx(CoreService.Root, { serviceServiceConfig: serviceServiceConfig, children: ({ selected, bookable }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceRoot, "data-selected": selected, "data-bookable": bookable, customElement: children, customElementProps: { selected, bookable }, ...attrs, children: children })) }));
94
117
  });
95
118
  Root.displayName = 'Service.Root';
96
119
  /**
@@ -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 emptyState={<div>No services found</div>}>
67
+ * <ServiceList.Services
68
+ * emptyState={<div>No services found</div>}
69
+ * loadingState={<div>Loading services...</div>}
70
+ * errorState={<div>Failed to load services</div>}
71
+ * >
63
72
  * <ServiceList.ServiceRepeater>
64
73
  * <Service.Name />
65
74
  * <Service.Price />
@@ -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 emptyState={<div>No services found</div>}>
91
+ * <ServiceList.Services
92
+ * emptyState={<div>No services found</div>}
93
+ * loadingState={<div>Loading services...</div>}
94
+ * errorState={<div>Failed to load services</div>}
95
+ * >
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
- // Initialize service signal - use empty service object if not provided
67
- const initialService = config.service || {};
68
- const serviceSignal = signalsService.signal(initialService);
68
+ // In "from booking signal" mode, BookingService must be available
69
+ if (noExplicitService && !bookingService) {
70
+ throw new Error('Service.Root requires either a service config (service, serviceId, or serviceSlug) or BookingService to be available');
71
+ }
72
+ // Initialize service signal
73
+ // In "from booking signal" mode, use a computed signal that reads from BookingService
74
+ let serviceSignal;
75
+ if (noExplicitService && bookingService) {
76
+ // Create a computed signal that reads from BookingService's first selected service
77
+ const computedServiceSignal = signalsService.computed(() => {
78
+ const selections = bookingService.serviceSelections.get();
79
+ return selections[0]?.service || null;
80
+ });
81
+ // Wrap computed signal as a Signal (read-only in practice for this mode)
82
+ serviceSignal = computedServiceSignal;
83
+ }
84
+ else {
85
+ // Standard mode: use provided service or empty object
86
+ const initialService = config.service || {};
87
+ serviceSignal = signalsService.signal(initialService);
88
+ }
69
89
  const isLoadingSignal = signalsService.signal(!!config.serviceId);
70
90
  const errorSignal = signalsService.signal(null);
71
91
  // Default appId to BOOKING_APP_ID if not provided
@@ -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, NOT pagingMetadataSignal
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.get();
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: queryOptions.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: currentQueryOptions.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
- const result = await queryServices({
251
- appId: options.appId,
252
- filters: options.filters,
253
- pagingMetadata: options.pagingMetadata,
254
- sort: options.sort,
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: result.services,
340
+ services: servicesResult.services,
341
+ categories: categoriesResult.categories,
260
342
  options: {
261
343
  ...options,
262
- pagingMetadata: result.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.45",
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": "122a9bd0c5095741e936f6e76ab60fa20cb7c69c33763af1a38937c5"
75
+ "falconPackageHash": "b4efe3be968a8e8d6806b0e818d5e30742584c3d834296262bc1572f"
76
76
  }