@seekora-ai/ui-sdk-react 0.2.23 → 0.2.25
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/dist/components/ClearRefinements.d.ts.map +1 -1
- package/dist/components/ClearRefinements.js +7 -6
- package/dist/components/CurrentRefinements.d.ts.map +1 -1
- package/dist/components/CurrentRefinements.js +32 -30
- package/dist/components/SearchLayout.js +1 -1
- package/dist/hooks/useFilters.d.ts.map +1 -1
- package/dist/hooks/useFilters.js +11 -2
- package/dist/index.umd.js +1 -1
- package/dist/src/index.esm.js +94 -54
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +94 -54
- package/dist/src/index.js.map +1 -1
- package/package.json +3 -3
package/dist/src/index.js
CHANGED
|
@@ -303,6 +303,9 @@ class SearchStateManager {
|
|
|
303
303
|
constructor(config) {
|
|
304
304
|
this.listeners = [];
|
|
305
305
|
this.debounceTimer = null;
|
|
306
|
+
this.notifyScheduled = false;
|
|
307
|
+
this.searchCoalesceTimer = null;
|
|
308
|
+
this.searchCoalesceResolvers = [];
|
|
306
309
|
this.client = config.client;
|
|
307
310
|
this.autoSearch = config.autoSearch !== false;
|
|
308
311
|
this.debounceMs = config.debounceMs || 300;
|
|
@@ -447,13 +450,27 @@ class SearchStateManager {
|
|
|
447
450
|
this.debouncedSearch();
|
|
448
451
|
}
|
|
449
452
|
}
|
|
450
|
-
// Manual search trigger
|
|
453
|
+
// Manual search trigger — coalesces rapid calls within 10ms into a single API request
|
|
451
454
|
async search(additionalOptions) {
|
|
452
455
|
// Clear debounce timer if exists
|
|
453
456
|
if (this.debounceTimer) {
|
|
454
457
|
clearTimeout(this.debounceTimer);
|
|
455
458
|
this.debounceTimer = null;
|
|
456
459
|
}
|
|
460
|
+
return new Promise((resolve, reject) => {
|
|
461
|
+
this.searchCoalesceResolvers.push({ resolve, reject });
|
|
462
|
+
if (this.searchCoalesceTimer) {
|
|
463
|
+
clearTimeout(this.searchCoalesceTimer);
|
|
464
|
+
}
|
|
465
|
+
this.searchCoalesceTimer = setTimeout(() => {
|
|
466
|
+
this.searchCoalesceTimer = null;
|
|
467
|
+
const resolvers = [...this.searchCoalesceResolvers];
|
|
468
|
+
this.searchCoalesceResolvers = [];
|
|
469
|
+
this._executeSearch(additionalOptions).then((result) => resolvers.forEach(r => r.resolve(result)), (err) => resolvers.forEach(r => r.reject(err)));
|
|
470
|
+
}, 10);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
async _executeSearch(additionalOptions) {
|
|
457
474
|
this.setState({ loading: true, error: null });
|
|
458
475
|
try {
|
|
459
476
|
const searchOptions = this.buildSearchOptions(additionalOptions);
|
|
@@ -550,19 +567,27 @@ class SearchStateManager {
|
|
|
550
567
|
this.state = { ...this.state, ...updates };
|
|
551
568
|
this.notifyListeners();
|
|
552
569
|
}
|
|
553
|
-
// Notify all listeners of state changes
|
|
570
|
+
// Notify all listeners of state changes, batched via microtask.
|
|
571
|
+
// Multiple synchronous mutations (e.g. addRefinement + page reset)
|
|
572
|
+
// coalesce into a single listener notification.
|
|
554
573
|
notifyListeners() {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
|
|
574
|
+
if (this.notifyScheduled)
|
|
575
|
+
return;
|
|
576
|
+
this.notifyScheduled = true;
|
|
577
|
+
queueMicrotask(() => {
|
|
578
|
+
this.notifyScheduled = false;
|
|
579
|
+
const state = this.getState();
|
|
580
|
+
this.listeners.forEach(listener => {
|
|
581
|
+
try {
|
|
582
|
+
listener(state);
|
|
583
|
+
}
|
|
584
|
+
catch (err) {
|
|
585
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
586
|
+
log.error('SearchStateManager: Error in listener', {
|
|
587
|
+
error: error.message,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
});
|
|
566
591
|
});
|
|
567
592
|
}
|
|
568
593
|
/** Explicitly clear results (bypasses keepResultsOnClear) */
|
|
@@ -608,10 +633,13 @@ class SearchStateManager {
|
|
|
608
633
|
async fetchFilters(options) {
|
|
609
634
|
log.verbose('SearchStateManager: Fetching filters', { options });
|
|
610
635
|
try {
|
|
611
|
-
|
|
636
|
+
// Do NOT pass refinement-based filters to the Filters API.
|
|
637
|
+
// Facets should be generated from the search query only, not narrowed
|
|
638
|
+
// by active filter selections. This keeps facet options stable when
|
|
639
|
+
// users toggle filters (same behaviour as performSimplifiedFacetSearch
|
|
640
|
+
// in the search API).
|
|
612
641
|
const response = await this.client.getFilters({
|
|
613
642
|
q: this.state.query || undefined,
|
|
614
|
-
filter: filterString || undefined,
|
|
615
643
|
...options,
|
|
616
644
|
});
|
|
617
645
|
log.info('SearchStateManager: Filters fetched', {
|
|
@@ -3521,13 +3549,22 @@ const useFilters = (options) => {
|
|
|
3521
3549
|
}
|
|
3522
3550
|
}
|
|
3523
3551
|
}, [stateManager, options?.facetBy, options?.maxFacetValues, options?.disjunctiveFacets?.join(',')]);
|
|
3524
|
-
//
|
|
3552
|
+
// Track query + refinements to only refetch when they actually change
|
|
3553
|
+
const prevKeyRef = React.useRef('');
|
|
3554
|
+
// Refetch when query or refinements change (not on every state update)
|
|
3525
3555
|
React.useEffect(() => {
|
|
3526
3556
|
if (!autoFetch)
|
|
3527
3557
|
return;
|
|
3528
|
-
const unsubscribe = stateManager.subscribe((
|
|
3558
|
+
const unsubscribe = stateManager.subscribe((state) => {
|
|
3559
|
+
const key = `${state.query}|${state.refinements.map(r => `${r.field}:${r.value}`).sort().join(',')}`;
|
|
3560
|
+
if (key === prevKeyRef.current)
|
|
3561
|
+
return;
|
|
3562
|
+
prevKeyRef.current = key;
|
|
3529
3563
|
fetchFilters();
|
|
3530
3564
|
});
|
|
3565
|
+
// subscribe() immediately invokes the listener with current state,
|
|
3566
|
+
// which handles the initial fetch (prevKeyRef starts as '' so key
|
|
3567
|
+
// will differ). No explicit fetchFilters() call needed here.
|
|
3531
3568
|
return unsubscribe;
|
|
3532
3569
|
}, [stateManager, autoFetch, fetchFilters]);
|
|
3533
3570
|
// Fetch schema once on mount
|
|
@@ -5189,9 +5226,6 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
|
|
|
5189
5226
|
opacity: 0.6,
|
|
5190
5227
|
}, "aria-label": `Clear ${refinement.label || refinement.field}: ${refinement.value}`, onMouseEnter: e => (e.currentTarget.style.opacity = '1'), onMouseLeave: e => (e.currentTarget.style.opacity = '0.6') }, renderCloseIcon ? renderCloseIcon() : defaultCloseIcon())));
|
|
5191
5228
|
};
|
|
5192
|
-
if (refinements.length === 0) {
|
|
5193
|
-
return null;
|
|
5194
|
-
}
|
|
5195
5229
|
// Group refinements by field for grouped layout
|
|
5196
5230
|
const groupedRefinements = layout === 'grouped'
|
|
5197
5231
|
? refinements.reduce((acc, r) => {
|
|
@@ -5203,13 +5237,17 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
|
|
|
5203
5237
|
: null;
|
|
5204
5238
|
const containerStyles = {
|
|
5205
5239
|
...style,
|
|
5240
|
+
// When a custom list theme is provided, make the container transparent to layout
|
|
5241
|
+
// so the list div becomes the direct layout child (enables overflow scroll from parent)
|
|
5242
|
+
...(refinementsTheme.list && !style?.display ? { display: 'contents' } : {}),
|
|
5206
5243
|
};
|
|
5207
5244
|
const listStyles = {
|
|
5208
5245
|
display: 'flex',
|
|
5209
|
-
flexWrap: layout === 'vertical' ? 'nowrap' : 'wrap',
|
|
5210
5246
|
flexDirection: layout === 'vertical' ? 'column' : 'row',
|
|
5211
5247
|
alignItems: layout === 'vertical' ? 'flex-start' : 'center',
|
|
5212
|
-
marginBottom: showClearAll ? theme.spacing.medium : 0,
|
|
5248
|
+
marginBottom: showClearAll && refinements.length > 0 ? theme.spacing.medium : 0,
|
|
5249
|
+
// Only apply flex-wrap if no custom list theme is provided (let theme classes control wrapping)
|
|
5250
|
+
...(!refinementsTheme.list ? { flexWrap: layout === 'vertical' ? 'nowrap' : 'wrap' } : {}),
|
|
5213
5251
|
};
|
|
5214
5252
|
return (React.createElement("div", { className: clsx(refinementsTheme.container, className), style: containerStyles },
|
|
5215
5253
|
React.createElement("style", null, `
|
|
@@ -5218,34 +5256,35 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
|
|
|
5218
5256
|
to { opacity: 1; transform: scale(1); }
|
|
5219
5257
|
}
|
|
5220
5258
|
`),
|
|
5221
|
-
|
|
5222
|
-
React.createElement("div", { className: refinementsTheme.
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5259
|
+
refinements.length > 0 && (React.createElement(React.Fragment, null,
|
|
5260
|
+
layout === 'grouped' && groupedRefinements ? (Object.entries(groupedRefinements).map(([field, items]) => (React.createElement("div", { key: field, className: refinementsTheme.group, style: { marginBottom: theme.spacing.medium } },
|
|
5261
|
+
React.createElement("div", { className: refinementsTheme.groupLabel, style: {
|
|
5262
|
+
fontSize: theme.typography.fontSize.small,
|
|
5263
|
+
fontWeight: 600,
|
|
5264
|
+
color: theme.colors.text,
|
|
5265
|
+
marginBottom: theme.spacing.small,
|
|
5266
|
+
textTransform: 'capitalize',
|
|
5267
|
+
} }, items[0]?.label || field),
|
|
5268
|
+
React.createElement("div", { role: "list", style: listStyles }, items.map((refinement, index) => {
|
|
5269
|
+
return renderRefinement
|
|
5270
|
+
? renderRefinement(refinement, index)
|
|
5271
|
+
: defaultRenderRefinement(refinement, index);
|
|
5272
|
+
})))))) : (React.createElement("div", { role: "list", className: refinementsTheme.list, style: listStyles }, refinements.map((refinement, index) => {
|
|
5230
5273
|
return renderRefinement
|
|
5231
5274
|
? renderRefinement(refinement, index)
|
|
5232
5275
|
: defaultRenderRefinement(refinement, index);
|
|
5233
|
-
})))
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
fontSize: theme.typography.fontSize.small,
|
|
5246
|
-
textDecoration: 'underline',
|
|
5247
|
-
transition: `background-color ${TRANSITIONS$7.fast}`,
|
|
5248
|
-
} }, "Clear all filters"))));
|
|
5276
|
+
}))),
|
|
5277
|
+
showClearAll && refinements.length > 1 && (React.createElement("button", { type: "button", onClick: handleClearAll, className: refinementsTheme.clearAllButton, style: {
|
|
5278
|
+
padding: `${theme.spacing.small} ${theme.spacing.medium}`,
|
|
5279
|
+
border: `1px solid ${theme.colors.border}`,
|
|
5280
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
5281
|
+
backgroundColor: theme.colors.background,
|
|
5282
|
+
color: theme.colors.text,
|
|
5283
|
+
cursor: 'pointer',
|
|
5284
|
+
fontSize: theme.typography.fontSize.small,
|
|
5285
|
+
textDecoration: 'underline',
|
|
5286
|
+
transition: `background-color ${TRANSITIONS$7.fast}`,
|
|
5287
|
+
} }, "Clear all filters"))))));
|
|
5249
5288
|
};
|
|
5250
5289
|
|
|
5251
5290
|
/**
|
|
@@ -5313,16 +5352,17 @@ const ClearRefinements = ({ clearsQuery = false, resetLabel = 'Clear all filters
|
|
|
5313
5352
|
padding: `${theme.spacing.small} ${theme.spacing.medium}`,
|
|
5314
5353
|
fontSize: theme.typography.fontSize.medium,
|
|
5315
5354
|
fontWeight: theme.typography.fontWeight?.medium || 500,
|
|
5316
|
-
backgroundColor:
|
|
5317
|
-
color:
|
|
5355
|
+
backgroundColor: theme.colors.primary,
|
|
5356
|
+
color: '#ffffff',
|
|
5318
5357
|
border: 'none',
|
|
5319
5358
|
borderRadius: typeof theme.borderRadius === 'string'
|
|
5320
5359
|
? theme.borderRadius
|
|
5321
5360
|
: theme.borderRadius.medium,
|
|
5322
|
-
cursor: canClear ? 'pointer' : '
|
|
5323
|
-
opacity: canClear ? 1 : 0
|
|
5324
|
-
|
|
5325
|
-
|
|
5361
|
+
cursor: canClear ? 'pointer' : 'default',
|
|
5362
|
+
opacity: canClear ? 1 : 0,
|
|
5363
|
+
pointerEvents: canClear ? 'auto' : 'none',
|
|
5364
|
+
transition: 'none',
|
|
5365
|
+
}, "aria-label": resetLabel }, resetLabel)));
|
|
5326
5366
|
};
|
|
5327
5367
|
|
|
5328
5368
|
/**
|
|
@@ -5360,7 +5400,7 @@ const SearchLayout = ({ sidebar, children, header, footer, sidebarWidth = '300px
|
|
|
5360
5400
|
padding: responsivePadding,
|
|
5361
5401
|
backgroundColor: theme.colors.background,
|
|
5362
5402
|
color: theme.colors.text,
|
|
5363
|
-
overflow:
|
|
5403
|
+
overflow: 'visible',
|
|
5364
5404
|
} },
|
|
5365
5405
|
sidebar && (!isMobile || showSidebarOnMobile) && (React.createElement("aside", { className: layoutTheme.sidebar, style: {
|
|
5366
5406
|
width: isMobile ? '100%' : sidebarWidth,
|