@stapel/search-react 0.10.0 → 0.12.0
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/CHANGELOG.md +160 -0
- package/MODULE.md +111 -0
- package/README.md +42 -0
- package/dist/api/generated/schema.d.ts +76 -6
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/api/searchApi.d.ts +13 -6
- package/dist/api/searchApi.d.ts.map +1 -1
- package/dist/api/searchApi.js.map +1 -1
- package/dist/api/types.d.ts +107 -1
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js +29 -0
- package/dist/api/types.js.map +1 -1
- package/dist/default/DegradationNotice.d.ts +22 -13
- package/dist/default/DegradationNotice.d.ts.map +1 -1
- package/dist/default/DegradationNotice.js +7 -8
- package/dist/default/DegradationNotice.js.map +1 -1
- package/dist/default/FacetPanelPane.d.ts +13 -2
- package/dist/default/FacetPanelPane.d.ts.map +1 -1
- package/dist/default/FacetPanelPane.js +29 -16
- package/dist/default/FacetPanelPane.js.map +1 -1
- package/dist/default/FilterChips.d.ts +88 -3
- package/dist/default/FilterChips.d.ts.map +1 -1
- package/dist/default/FilterChips.js +166 -13
- package/dist/default/FilterChips.js.map +1 -1
- package/dist/default/LocationSummaryLine.d.ts.map +1 -1
- package/dist/default/LocationSummaryLine.js +1 -1
- package/dist/default/LocationSummaryLine.js.map +1 -1
- package/dist/default/RangeFilterRow.d.ts.map +1 -1
- package/dist/default/RangeFilterRow.js +49 -5
- package/dist/default/RangeFilterRow.js.map +1 -1
- package/dist/default/SearchBox.d.ts.map +1 -1
- package/dist/default/SearchBox.js +74 -4
- package/dist/default/SearchBox.js.map +1 -1
- package/dist/default/SearchPage.d.ts +29 -1
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +25 -10
- package/dist/default/SearchPage.js.map +1 -1
- package/dist/default/SearchResultsPane.d.ts +22 -0
- package/dist/default/SearchResultsPane.d.ts.map +1 -1
- package/dist/default/SearchResultsPane.js +2 -2
- package/dist/default/SearchResultsPane.js.map +1 -1
- package/dist/default/SortSelect.d.ts +23 -0
- package/dist/default/SortSelect.d.ts.map +1 -1
- package/dist/default/SortSelect.js +29 -9
- package/dist/default/SortSelect.js.map +1 -1
- package/dist/default/index.d.ts +2 -2
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +1 -1
- package/dist/default/index.js.map +1 -1
- package/dist/headless/FacetPanel.d.ts +26 -0
- package/dist/headless/FacetPanel.d.ts.map +1 -1
- package/dist/headless/FacetPanel.js +16 -1
- package/dist/headless/FacetPanel.js.map +1 -1
- package/dist/headless/useFacetLabels.d.ts +39 -0
- package/dist/headless/useFacetLabels.d.ts.map +1 -0
- package/dist/headless/useFacetLabels.js +129 -0
- package/dist/headless/useFacetLabels.js.map +1 -0
- package/dist/headless/useSearchBox.d.ts +71 -0
- package/dist/headless/useSearchBox.d.ts.map +1 -1
- package/dist/headless/useSearchBox.js +67 -2
- package/dist/headless/useSearchBox.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +8 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +48 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +58 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +10 -0
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/model/queries.d.ts +6 -5
- package/dist/model/queries.d.ts.map +1 -1
- package/dist/model/queries.js +4 -3
- package/dist/model/queries.js.map +1 -1
- package/dist/model/queryKeys.d.ts +12 -0
- package/dist/model/queryKeys.d.ts.map +1 -1
- package/dist/model/queryKeys.js +7 -0
- package/dist/model/queryKeys.js.map +1 -1
- package/dist/nav/manifest.d.ts +21 -0
- package/dist/nav/manifest.d.ts.map +1 -1
- package/dist/nav/manifest.js +3 -2
- package/dist/nav/manifest.js.map +1 -1
- package/dist/state/degradations.d.ts +32 -0
- package/dist/state/degradations.d.ts.map +1 -1
- package/dist/state/degradations.js +33 -0
- package/dist/state/degradations.js.map +1 -1
- package/dist/state/facets.d.ts +64 -0
- package/dist/state/facets.d.ts.map +1 -1
- package/dist/state/facets.js +134 -9
- package/dist/state/facets.js.map +1 -1
- package/dist/state/ranges.d.ts +45 -3
- package/dist/state/ranges.d.ts.map +1 -1
- package/dist/state/ranges.js +55 -14
- package/dist/state/ranges.js.map +1 -1
- package/llms.txt +2 -2
- package/manifest.json +27 -2
- package/nav-manifest.json +4 -3
- package/package.json +10 -10
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +76 -6
- package/src/api/searchApi.ts +13 -6
- package/src/api/types.ts +115 -1
- package/src/default/DegradationNotice.tsx +30 -20
- package/src/default/FacetPanelPane.tsx +55 -25
- package/src/default/FilterChips.tsx +290 -38
- package/src/default/LocationSummaryLine.tsx +6 -1
- package/src/default/RangeFilterRow.tsx +61 -3
- package/src/default/SearchBox.tsx +134 -6
- package/src/default/SearchPage.tsx +79 -5
- package/src/default/SearchResultsPane.tsx +48 -11
- package/src/default/SortSelect.tsx +70 -9
- package/src/default/index.ts +10 -2
- package/src/headless/FacetPanel.tsx +44 -1
- package/src/headless/useFacetLabels.ts +172 -0
- package/src/headless/useSearchBox.ts +132 -5
- package/src/i18n/es.ts +9 -0
- package/src/i18n/keys.ts +60 -0
- package/src/i18n/ru.ts +11 -0
- package/src/index.ts +28 -4
- package/src/model/queries.ts +6 -5
- package/src/model/queryKeys.ts +23 -0
- package/src/nav/manifest.ts +24 -2
- package/src/state/degradations.ts +56 -0
- package/src/state/facets.ts +181 -9
- package/src/state/ranges.ts +85 -15
|
@@ -102,13 +102,24 @@ export interface paths {
|
|
|
102
102
|
cookie?: never;
|
|
103
103
|
};
|
|
104
104
|
/**
|
|
105
|
-
*
|
|
106
|
-
* @description ``GET /search/api/v1/suggest`` —
|
|
105
|
+
* Type-ahead: category paths with live counts, plus title prefixes
|
|
106
|
+
* @description ``GET /search/api/v1/suggest`` — what to offer under the search box.
|
|
107
107
|
*
|
|
108
|
-
*
|
|
108
|
+
* ``categories`` is the primary half: each row is a destination with its
|
|
109
|
+
* full ancestor path and the number of listings a buyer would actually
|
|
110
|
+
* see there, ranked by that number. ``terms`` is the title-prefix half.
|
|
111
|
+
*
|
|
112
|
+
* Neither comes from a query log: no query log is kept, which is a privacy
|
|
109
113
|
* decision before it is a product one, and on day one there would be
|
|
110
114
|
* nothing in it anyway.
|
|
111
115
|
*
|
|
116
|
+
* The answer is public, identical for every reader and requested on every
|
|
117
|
+
* keystroke, so it carries ``Cache-Control: public`` and an ``ETag``. This
|
|
118
|
+
* is the module's first conditional read — ``query`` has none, because a
|
|
119
|
+
* SERP answer embeds ``took_ms`` and a cursor and would revalidate to a
|
|
120
|
+
* miss every time. Here the payload is deliberately free of anything that
|
|
121
|
+
* varies with the clock.
|
|
122
|
+
*
|
|
112
123
|
* **Permissions:** `AllowAny`
|
|
113
124
|
*/
|
|
114
125
|
get: operations["search_api_v1_suggest_retrieve"];
|
|
@@ -124,6 +135,38 @@ export interface paths {
|
|
|
124
135
|
export type webhooks = Record<string, never>;
|
|
125
136
|
export interface components {
|
|
126
137
|
schemas: {
|
|
138
|
+
/** @description One destination in the dropdown, ready to render and ready to follow. */
|
|
139
|
+
CategorySuggestion: {
|
|
140
|
+
/** @description Category id. */
|
|
141
|
+
id: number;
|
|
142
|
+
/** @description Category slug. */
|
|
143
|
+
slug: string;
|
|
144
|
+
/** @description The category's own display name. */
|
|
145
|
+
name: string;
|
|
146
|
+
/** @description Display names root->leaf, e.g. ['Мужская одежда', 'Шорты']. This is what distinguishes three categories that share a name. */
|
|
147
|
+
path: string[];
|
|
148
|
+
/** @description The ancestry as ids joined with '/'. Pass it verbatim as the `category` parameter of /query — do not re-join path segments yourself. */
|
|
149
|
+
category: string;
|
|
150
|
+
/** @description Live listings a buyer would see under this category, descendants included — the same number the SERP reports for it. */
|
|
151
|
+
count: number;
|
|
152
|
+
/** @description Number of segments in `path`. */
|
|
153
|
+
depth: number;
|
|
154
|
+
/**
|
|
155
|
+
* @description How the name matched. Informational; ranking is by `count`.
|
|
156
|
+
*
|
|
157
|
+
* * `prefix` - prefix
|
|
158
|
+
* * `substring` - substring
|
|
159
|
+
*/
|
|
160
|
+
match: components["schemas"]["MatchEnum"];
|
|
161
|
+
};
|
|
162
|
+
/** @description Captions for one slug's option codes. */
|
|
163
|
+
FacetLabels: {
|
|
164
|
+
/** @description True when `values` holds translation KEYS to run through the catalogue; false when it holds literal captions. The reader cannot tell by looking — `b.apple` and `Б/у` are both strings. */
|
|
165
|
+
translatable: boolean;
|
|
166
|
+
values: {
|
|
167
|
+
[key: string]: string;
|
|
168
|
+
};
|
|
169
|
+
};
|
|
127
170
|
FacetMeta: {
|
|
128
171
|
/** @description True when counts came from a sample because the candidate set exceeded FACET_CANDIDATE_CAP. */
|
|
129
172
|
approximate: boolean;
|
|
@@ -132,6 +175,8 @@ export interface components {
|
|
|
132
175
|
counted: string[];
|
|
133
176
|
/** @description Plan slugs dropped at MAX_FACET_FIELDS — reported, not vanished. */
|
|
134
177
|
skipped: string[];
|
|
178
|
+
/** @description Range slugs that address a core document column rather than an attribute (`r.price`). Offer them as filters unconditionally: they exist for every document in every category, which is why they are not in the category's own plan. */
|
|
179
|
+
core_ranges: string[];
|
|
135
180
|
};
|
|
136
181
|
HealthResponse: {
|
|
137
182
|
backend: string;
|
|
@@ -145,6 +190,12 @@ export interface components {
|
|
|
145
190
|
lag_seconds?: number | null;
|
|
146
191
|
stale_reason?: string;
|
|
147
192
|
};
|
|
193
|
+
/**
|
|
194
|
+
* @description * `prefix` - prefix
|
|
195
|
+
* * `substring` - substring
|
|
196
|
+
* @enum {string}
|
|
197
|
+
*/
|
|
198
|
+
MatchEnum: "prefix" | "substring";
|
|
148
199
|
/** @description The P2B Art. 5 disclosure, generated from the scorer registry. */
|
|
149
200
|
RankingResponse: {
|
|
150
201
|
doc_type: string;
|
|
@@ -208,6 +259,10 @@ export interface components {
|
|
|
208
259
|
[key: string]: number;
|
|
209
260
|
};
|
|
210
261
|
};
|
|
262
|
+
/** @description {slug: {translatable, values: {value: caption}}} for slugs whose options are inline in the category schema. Absent for a vocabulary-backed slug: its level lives outside the schema and the plan will not invent a caption it has not read. */
|
|
263
|
+
facet_labels: {
|
|
264
|
+
[key: string]: components["schemas"]["FacetLabels"];
|
|
265
|
+
};
|
|
211
266
|
facet_meta: components["schemas"]["FacetMeta"];
|
|
212
267
|
next_anchor: string | null;
|
|
213
268
|
prev_anchor: string | null;
|
|
@@ -222,11 +277,22 @@ export interface components {
|
|
|
222
277
|
/** @description What the configured engine could not do for this query. */
|
|
223
278
|
degraded: string[];
|
|
224
279
|
backend: string;
|
|
280
|
+
/** @description The language whose dictionary and analyzer configuration answered — `lang`, else Accept-Language, else DEFAULT_LANGUAGE. When the fallback is wrong the synonym layer silently does not apply, and this field is the only place the answer says so. */
|
|
281
|
+
language: string;
|
|
225
282
|
sort: string;
|
|
226
283
|
took_ms: number;
|
|
227
284
|
};
|
|
228
285
|
SuggestResponse: {
|
|
286
|
+
/** @description Destinations, ranked by live listing count desc, then depth, then name. */
|
|
287
|
+
categories: components["schemas"]["CategorySuggestion"][];
|
|
288
|
+
/** @description Title prefixes from the index. */
|
|
289
|
+
terms: string[];
|
|
290
|
+
/** @description Deprecated alias of `terms`, kept for one minor. */
|
|
229
291
|
items: string[];
|
|
292
|
+
/** @description Which dictionary answered — the same resolution /query reports. */
|
|
293
|
+
language: string;
|
|
294
|
+
/** @description What this answer could not do: `category_suggestions` (no provider for category names), `category_rollup` (no ancestry, so counts would read 0). */
|
|
295
|
+
degraded: string[];
|
|
230
296
|
backend: string;
|
|
231
297
|
};
|
|
232
298
|
};
|
|
@@ -356,11 +422,15 @@ export interface operations {
|
|
|
356
422
|
};
|
|
357
423
|
search_api_v1_suggest_retrieve: {
|
|
358
424
|
parameters: {
|
|
359
|
-
query
|
|
425
|
+
query?: {
|
|
426
|
+
/** @description Language of the query: picks the dictionary, so «shorty» reaches «шорты». Falls back to Accept-Language, then DEFAULT_LANGUAGE. */
|
|
427
|
+
lang?: string;
|
|
428
|
+
/** @description Rows per half. Capped by MAX_SUGGEST_LIMIT. */
|
|
360
429
|
limit?: number;
|
|
361
|
-
/** @description
|
|
430
|
+
/** @description What the buyer has typed so far. */
|
|
362
431
|
q?: string;
|
|
363
|
-
type
|
|
432
|
+
/** @description Registered doc_type. Optional when exactly one type is registered — a type-ahead should not have to name the only corpus there is. */
|
|
433
|
+
type?: string;
|
|
364
434
|
};
|
|
365
435
|
header?: never;
|
|
366
436
|
path?: never;
|
package/src/api/searchApi.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type {
|
|
|
3
3
|
RankingResponse,
|
|
4
4
|
SearchQueryState,
|
|
5
5
|
SearchResponse,
|
|
6
|
+
SuggestAnswer,
|
|
6
7
|
SuggestParams,
|
|
7
|
-
SuggestResponse,
|
|
8
8
|
} from "./types.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -53,15 +53,22 @@ export interface SearchApi {
|
|
|
53
53
|
): Promise<SearchResponse>;
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
56
|
+
* What to offer under the search box: CATEGORIES first, then title-prefix
|
|
57
|
+
* terms (stapel-search 0.7.0).
|
|
58
|
+
*
|
|
59
|
+
* Neither half comes from a query log — the module keeps none, which is a
|
|
60
|
+
* privacy decision before it is a product one — so every suggestion is a
|
|
61
|
+
* destination or a search that has results.
|
|
62
|
+
*
|
|
63
|
+
* Typed as {@link SuggestAnswer} rather than as the generated
|
|
64
|
+
* `SuggestResponse`: a build regenerated against a pre-0.7.0 schema would
|
|
65
|
+
* otherwise hide the categories half from the compiler. See that type for
|
|
66
|
+
* why every member but `backend` is optional.
|
|
60
67
|
*/
|
|
61
68
|
suggest(
|
|
62
69
|
params: SuggestParams,
|
|
63
70
|
options?: { readonly signal?: AbortSignal }
|
|
64
|
-
): Promise<
|
|
71
|
+
): Promise<SuggestAnswer>;
|
|
65
72
|
|
|
66
73
|
/**
|
|
67
74
|
* The P2B Art. 5 ranking disclosure for a doc type: which parameters rank
|
package/src/api/types.ts
CHANGED
|
@@ -25,9 +25,123 @@ export type SearchItem = Schemas["SearchItem"];
|
|
|
25
25
|
* `counted`, `skipped`. Rendered, never swallowed (spec §4.2). */
|
|
26
26
|
export type FacetMeta = Schemas["FacetMeta"];
|
|
27
27
|
|
|
28
|
-
/** `GET /suggest` 200. */
|
|
28
|
+
/** `GET /suggest` 200, as the CURRENT generated schema describes it. */
|
|
29
29
|
export type SuggestResponse = Schemas["SuggestResponse"];
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* How a category's name matched the typed prefix. Informational — the server
|
|
33
|
+
* ranks by `count`, never by this.
|
|
34
|
+
*/
|
|
35
|
+
export type SuggestCategoryMatch = "prefix" | "substring";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One CATEGORY the type-ahead offers: a destination, not a search term.
|
|
39
|
+
*
|
|
40
|
+
* A classified's search box is a navigation control before it is a text
|
|
41
|
+
* filter. "Shorts" is not one destination but three — men's, women's,
|
|
42
|
+
* children's — and the only things that let a buyer pick between them are the
|
|
43
|
+
* ancestor path and how many live listings sit behind each. Both are here,
|
|
44
|
+
* and neither can be computed on the client: the count is one aggregate over
|
|
45
|
+
* the index, and a client-side matcher over a fetched tree would have the
|
|
46
|
+
* names and no numbers.
|
|
47
|
+
*/
|
|
48
|
+
export interface SuggestCategory {
|
|
49
|
+
readonly id: number;
|
|
50
|
+
readonly slug: string;
|
|
51
|
+
/** The category's own display name. */
|
|
52
|
+
readonly name: string;
|
|
53
|
+
/**
|
|
54
|
+
* Display names root→leaf, e.g. `["Menswear", "Shorts"]` — this is what
|
|
55
|
+
* distinguishes three categories sharing a name, and it is what a row has
|
|
56
|
+
* to print.
|
|
57
|
+
*/
|
|
58
|
+
readonly path: readonly string[];
|
|
59
|
+
/**
|
|
60
|
+
* The ancestry as ids joined with `/`, ready to pass VERBATIM as the
|
|
61
|
+
* `category` parameter of `/query`.
|
|
62
|
+
*
|
|
63
|
+
* The server serves the joined string rather than only the segments
|
|
64
|
+
* precisely so that a client cannot invent a different join and silently
|
|
65
|
+
* miss — so nothing in this pair rebuilds it from {@link path} or
|
|
66
|
+
* {@link slug}.
|
|
67
|
+
*/
|
|
68
|
+
readonly category: string;
|
|
69
|
+
/**
|
|
70
|
+
* Live listings a buyer would see under this category, descendants
|
|
71
|
+
* included — the same number the SERP reports for it.
|
|
72
|
+
*/
|
|
73
|
+
readonly count: number;
|
|
74
|
+
/** Number of segments in {@link path}. */
|
|
75
|
+
readonly depth: number;
|
|
76
|
+
readonly match: SuggestCategoryMatch;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The `/suggest` answer this pair actually reads.
|
|
81
|
+
*
|
|
82
|
+
* GENERATOR NOTE, and the reason this is not `Schemas["SuggestResponse"]`:
|
|
83
|
+
* the generated type describes ONE server. stapel-search 0.7.0 made the answer
|
|
84
|
+
* three-part — `categories`, `terms`, and `items` as a deprecated alias of
|
|
85
|
+
* `terms` — plus `language` and `degraded`, and declares all five REQUIRED,
|
|
86
|
+
* which is true of a 0.7.0 answer and false of every answer sent by the
|
|
87
|
+
* servers a storefront is also deployed against.
|
|
88
|
+
*
|
|
89
|
+
* Every member but `backend` is therefore OPTIONAL here, which is not
|
|
90
|
+
* sloppiness but the deployment story: a storefront ships against whichever
|
|
91
|
+
* server is actually running, an older one sends no `categories` key at all,
|
|
92
|
+
* and "the key is absent" has to read as "this server offers no destinations"
|
|
93
|
+
* rather than as a crash or as an empty group under a heading. A pair typed
|
|
94
|
+
* against the required-field version would compile while reading `undefined`
|
|
95
|
+
* from a field the compiler swore was there.
|
|
96
|
+
*/
|
|
97
|
+
export interface SuggestAnswer {
|
|
98
|
+
readonly backend: string;
|
|
99
|
+
/** Destinations, ranked by live listing count desc, then depth, then name. */
|
|
100
|
+
readonly categories?: readonly SuggestCategory[];
|
|
101
|
+
/** Title prefixes from the index. */
|
|
102
|
+
readonly terms?: readonly string[];
|
|
103
|
+
/** Deprecated alias of {@link terms}, and the only half a pre-0.7.0 server
|
|
104
|
+
* sends. Read through {@link suggestTerms}, never directly. */
|
|
105
|
+
readonly items?: readonly string[];
|
|
106
|
+
/** Which dictionary answered — the same resolution `/query` reports. */
|
|
107
|
+
readonly language?: string;
|
|
108
|
+
/** What this answer could not do — see {@link SUGGEST_DEGRADED_CATEGORIES}
|
|
109
|
+
* and {@link SUGGEST_DEGRADED_ROLLUP}. */
|
|
110
|
+
readonly degraded?: readonly string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The suggest answer had NO category provider, so the categories half is
|
|
115
|
+
* empty for a reason that is not "nothing matched".
|
|
116
|
+
*
|
|
117
|
+
* A dropdown has no room for a sentence about a provider being down, and the
|
|
118
|
+
* person reading it is mid-word. So the group is ABSENT rather than empty —
|
|
119
|
+
* an empty group under a heading is the box claiming the catalogue has no
|
|
120
|
+
* section by that name, which is a different and untrue statement.
|
|
121
|
+
*/
|
|
122
|
+
export const SUGGEST_DEGRADED_CATEGORIES = "category_suggestions";
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Ancestry never arrived, so every stored path is one segment long and every
|
|
126
|
+
* count would read `0`.
|
|
127
|
+
*
|
|
128
|
+
* The rows are still destinations and still worth offering; their COUNTS are
|
|
129
|
+
* the part that is not an answer, so a surface drops the number rather than
|
|
130
|
+
* printing a catalogue of zeros.
|
|
131
|
+
*/
|
|
132
|
+
export const SUGGEST_DEGRADED_ROLLUP = "category_rollup";
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The term half of a suggest answer, from whichever key this server sends.
|
|
136
|
+
*
|
|
137
|
+
* `terms` is 0.7.0's name and `items` is the deprecated alias kept for one
|
|
138
|
+
* minor; a client that read only one of them would go blank against half the
|
|
139
|
+
* servers in the fleet.
|
|
140
|
+
*/
|
|
141
|
+
export function suggestTerms(answer: SuggestAnswer | undefined): readonly string[] {
|
|
142
|
+
return answer?.terms ?? answer?.items ?? [];
|
|
143
|
+
}
|
|
144
|
+
|
|
31
145
|
/** `GET /ranking` 200 — the P2B Art. 5 disclosure. */
|
|
32
146
|
export type RankingResponse = Schemas["RankingResponse"];
|
|
33
147
|
|
|
@@ -12,27 +12,34 @@
|
|
|
12
12
|
*
|
|
13
13
|
* ── Two deliberate ways NOT to shout ───────────────────────────────────────
|
|
14
14
|
*
|
|
15
|
-
* 1.
|
|
16
|
-
*
|
|
17
|
-
* the
|
|
18
|
-
*
|
|
19
|
-
* is
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
15
|
+
* 1. **A degradation addressed to the OPERATOR never reaches the reader.**
|
|
16
|
+
* `typo_tolerance` and `phrase_synonyms` both say, in the shipped ru copy,
|
|
17
|
+
* "the search engine in use cannot do this" — a sentence about which
|
|
18
|
+
* engine somebody licensed, printed at a person trying to buy a phone. It
|
|
19
|
+
* is the same sentence on every query forever, which is what makes it
|
|
20
|
+
* invisible by the day `category_rollup` shows up in the same box.
|
|
21
|
+
* `exact_total` joins them: the count already says "N+".
|
|
22
|
+
*
|
|
23
|
+
* Measured live on a classified board: a full-screen yellow "synonyms
|
|
24
|
+
* were not substituted" between the sort control and the first card, on every
|
|
25
|
+
* query, for every buyer. Note what the fix is NOT — the string was not
|
|
26
|
+
* deleted and the kind was not special-cased. `readerFacing` names the
|
|
27
|
+
* *audience*, so the next engine-capability literal is filtered by the
|
|
28
|
+
* same rule instead of growing its own copy of this comment.
|
|
24
29
|
* 2. **`variant`.** A catalogue page wants the banner; a landing page that
|
|
25
30
|
* shows six cards under a hero has no room for a warning box and passes
|
|
26
|
-
* `"inline"` (one quiet line) or `"off"`. `"
|
|
27
|
-
*
|
|
28
|
-
*
|
|
31
|
+
* `"inline"` (one quiet line) or `"off"`. `"debug"` is the operator's
|
|
32
|
+
* view: everything, unfiltered, for a status page or a support tool.
|
|
33
|
+
* `"off"` is a decision the CONTAINER makes about ITS surface — the
|
|
34
|
+
* notice stays the default, and nothing here silences a reader-facing
|
|
35
|
+
* degradation for everyone.
|
|
29
36
|
*/
|
|
30
37
|
import type { ReactElement } from "react";
|
|
31
38
|
import { Alert, Flex, Typography } from "antd";
|
|
32
39
|
import { fontSize, spacing } from "@stapel/tokens";
|
|
33
40
|
import { useT } from "@stapel/core";
|
|
34
41
|
import type { SearchDegradation } from "../api/types.js";
|
|
35
|
-
import {
|
|
42
|
+
import { readerFacing } from "../state/degradations.js";
|
|
36
43
|
import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
|
|
37
44
|
|
|
38
45
|
/**
|
|
@@ -40,10 +47,12 @@ import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
|
|
|
40
47
|
*
|
|
41
48
|
* - `"banner"` (default) — an antd warning `Alert` with one line per item.
|
|
42
49
|
* - `"inline"` — the same sentences as quiet secondary text, no box.
|
|
50
|
+
* - `"debug"` — every degradation including the operator's, for a status page
|
|
51
|
+
* or a support tool. The one variant that does not filter by audience.
|
|
43
52
|
* - `"off"` — nothing. For a surface where the notice does not belong at all;
|
|
44
53
|
* the container that turns it off owns saying so somewhere else.
|
|
45
54
|
*/
|
|
46
|
-
export type DegradationNoticeVariant = "banner" | "inline" | "off";
|
|
55
|
+
export type DegradationNoticeVariant = "banner" | "inline" | "debug" | "off";
|
|
47
56
|
|
|
48
57
|
export interface DegradationNoticeProps {
|
|
49
58
|
readonly degradations: readonly SearchDegradation[];
|
|
@@ -63,9 +72,10 @@ export function DegradationNotice(
|
|
|
63
72
|
const t = useT();
|
|
64
73
|
const variant: DegradationNoticeVariant = props.variant ?? "banner";
|
|
65
74
|
if (variant === "off") return null;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
// Everything, or only what this page's reader can act on.
|
|
76
|
+
const degradations =
|
|
77
|
+
variant === "debug" ? props.degradations : readerFacing(props.degradations);
|
|
78
|
+
if (degradations.length === 0) return null;
|
|
69
79
|
|
|
70
80
|
const say = (degradation: SearchDegradation): string => {
|
|
71
81
|
const slug = degradation.scorer;
|
|
@@ -82,7 +92,7 @@ export function DegradationNotice(
|
|
|
82
92
|
// theme's warning tint, and grey body text on it measured under 3:1 in the
|
|
83
93
|
// visual pass. Inside a coloured box the readable colour is the box's own
|
|
84
94
|
// text colour, which is what a plain `<Typography.Text>` inherits.
|
|
85
|
-
const lines =
|
|
95
|
+
const lines = degradations.map((degradation) => (
|
|
86
96
|
<li key={degradation.raw} data-degradation={degradation.raw}>
|
|
87
97
|
<Typography.Text>{say(degradation)}</Typography.Text>
|
|
88
98
|
</li>
|
|
@@ -95,7 +105,7 @@ export function DegradationNotice(
|
|
|
95
105
|
{t(SEARCH_I18N_KEYS.degradedTitle)}
|
|
96
106
|
</Typography.Text>
|
|
97
107
|
<ul style={{ margin: 0, paddingInlineStart: spacing[5], fontSize: fontSize.xs.fontSize }}>
|
|
98
|
-
{
|
|
108
|
+
{degradations.map((degradation) => (
|
|
99
109
|
<li key={degradation.raw} data-degradation={degradation.raw}>
|
|
100
110
|
<Typography.Text type="secondary">{say(degradation)}</Typography.Text>
|
|
101
111
|
</li>
|
|
@@ -110,7 +120,7 @@ export function DegradationNotice(
|
|
|
110
120
|
type="warning"
|
|
111
121
|
showIcon
|
|
112
122
|
data-testid="search-degraded"
|
|
113
|
-
data-variant="banner"
|
|
123
|
+
data-variant={variant === "debug" ? "debug" : "banner"}
|
|
114
124
|
title={t(SEARCH_I18N_KEYS.degradedTitle)}
|
|
115
125
|
description={
|
|
116
126
|
<ul style={{ margin: 0, paddingInlineStart: spacing[5] }}>{lines}</ul>
|
|
@@ -54,6 +54,7 @@ import { featureName } from "@stapel/attributes-react";
|
|
|
54
54
|
import type { FeatureDef } from "@stapel/attributes-react";
|
|
55
55
|
import type { SearchGeo } from "../api/types.js";
|
|
56
56
|
import { FacetPanel } from "../headless/FacetPanel.js";
|
|
57
|
+
import type { FacetLabelResolver } from "../headless/useFacetLabels.js";
|
|
57
58
|
import { useSearchState } from "../headless/SearchStateProvider.js";
|
|
58
59
|
import { FacetGroupControl } from "./FacetGroupControl.js";
|
|
59
60
|
import { buildRangeGroups } from "../state/ranges.js";
|
|
@@ -78,11 +79,21 @@ export interface GeoFilterSlotProps {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
export interface FacetPanelPaneProps extends ThemeModeProp {
|
|
81
|
-
/** The category's feature schema — the source of option LABELS
|
|
82
|
-
* slugs get a numeric range row
|
|
82
|
+
/** The category's feature schema — the source of option LABELS, of which
|
|
83
|
+
* slugs get a numeric range row, and of which slugs are a filter at all
|
|
84
|
+
* (`isFacetableFeature`: an `imei` is counted and is not one). */
|
|
83
85
|
readonly categoryFeatures?: readonly FeatureDef[];
|
|
84
86
|
readonly locale?: string;
|
|
85
87
|
readonly enabled?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Name the values neither the answer nor the schema names — see
|
|
90
|
+
* {@link FacetLabelResolver}. A `ref_select` facet carries only a pointer to
|
|
91
|
+
* a vocabulary in its config, and the vocabulary is the host's to read.
|
|
92
|
+
*
|
|
93
|
+
* The same prop reaches `<FilterChips>` from `<SearchPage>`, so the panel
|
|
94
|
+
* and the chip row cannot print two different words for one value.
|
|
95
|
+
*/
|
|
96
|
+
readonly resolveFacetLabels?: FacetLabelResolver;
|
|
86
97
|
/** The catalogue picker (`categories-react`'s `CategoryPickerField`, bound
|
|
87
98
|
* to a path). Unfilled, an active category still gets a "clear" control. */
|
|
88
99
|
readonly renderCategoryFilter?: (slot: CategoryFilterSlotProps) => ReactNode;
|
|
@@ -306,13 +317,6 @@ function skippedNames(
|
|
|
306
317
|
export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
|
|
307
318
|
const t = useT();
|
|
308
319
|
const { state } = useSearchState();
|
|
309
|
-
const ranges = buildRangeGroups({
|
|
310
|
-
state,
|
|
311
|
-
...(props.categoryFeatures !== undefined
|
|
312
|
-
? { categoryFeatures: props.categoryFeatures }
|
|
313
|
-
: {}),
|
|
314
|
-
t,
|
|
315
|
-
});
|
|
316
320
|
|
|
317
321
|
return (
|
|
318
322
|
<SkinTheme {...(props.mode !== undefined ? { mode: props.mode } : {})}>
|
|
@@ -322,8 +326,28 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
|
|
|
322
326
|
: {})}
|
|
323
327
|
{...(props.locale !== undefined ? { locale: props.locale } : {})}
|
|
324
328
|
{...(props.enabled !== undefined ? { enabled: props.enabled } : {})}
|
|
329
|
+
{...(props.resolveFacetLabels !== undefined
|
|
330
|
+
? { resolveFacetLabels: props.resolveFacetLabels }
|
|
331
|
+
: {})}
|
|
325
332
|
>
|
|
326
|
-
{(bag) =>
|
|
333
|
+
{(bag) => {
|
|
334
|
+
// Built INSIDE the bag, because which axes exist is a property of
|
|
335
|
+
// the ANSWER now: `facet_meta.core_ranges` names the core columns
|
|
336
|
+
// this server can actually filter on (`r.price`), and the corpus
|
|
337
|
+
// currency is read off the cards it just returned. Computed
|
|
338
|
+
// outside, the panel would have had to keep its own list of core
|
|
339
|
+
// slugs — which is how a board ends up offering a price filter
|
|
340
|
+
// against a server that answers zero for one.
|
|
341
|
+
const ranges = buildRangeGroups({
|
|
342
|
+
state,
|
|
343
|
+
...(props.categoryFeatures !== undefined
|
|
344
|
+
? { categoryFeatures: props.categoryFeatures }
|
|
345
|
+
: {}),
|
|
346
|
+
coreRanges: bag.coreRanges,
|
|
347
|
+
...(bag.currency !== undefined ? { currency: bag.currency } : {}),
|
|
348
|
+
t,
|
|
349
|
+
});
|
|
350
|
+
return (
|
|
327
351
|
<Flex vertical gap={spacing[3]} data-testid="search-facets">
|
|
328
352
|
<Flex justify="space-between" align="center" gap={spacing[2]}>
|
|
329
353
|
{props.heading === null ? (
|
|
@@ -360,6 +384,25 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
|
|
|
360
384
|
{...(props.geoLabel !== undefined ? { label: props.geoLabel } : {})}
|
|
361
385
|
/>
|
|
362
386
|
|
|
387
|
+
{/* Price first. Every numeric row below it is an ATTRIBUTE the
|
|
388
|
+
category happens to declare — on the phone board this was
|
|
389
|
+
measured against, all seven of them were parcel dimensions
|
|
390
|
+
and wholesale packing. The one number a buyer narrows by is
|
|
391
|
+
the price, and `buildRangeGroups` puts the core axes first. */}
|
|
392
|
+
{ranges.length > 0 && (
|
|
393
|
+
<Flex vertical gap={spacing[3]} data-testid="search-ranges">
|
|
394
|
+
{ranges.map((group) => (
|
|
395
|
+
<RangeFilterRow
|
|
396
|
+
key={group.slug}
|
|
397
|
+
group={group}
|
|
398
|
+
onApply={bag.setRange}
|
|
399
|
+
/>
|
|
400
|
+
))}
|
|
401
|
+
</Flex>
|
|
402
|
+
)}
|
|
403
|
+
|
|
404
|
+
{ranges.length > 0 && <Divider style={{ margin: 0 }} />}
|
|
405
|
+
|
|
363
406
|
{/* Honesty flags, not failures: the counts ARE approximate and
|
|
364
407
|
those slugs WERE skipped, and a red box would teach a person
|
|
365
408
|
the page is broken. `ErrorAlert` is for a read that did not
|
|
@@ -383,20 +426,6 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
|
|
|
383
426
|
/>
|
|
384
427
|
)}
|
|
385
428
|
|
|
386
|
-
{ranges.length > 0 && (
|
|
387
|
-
<Flex vertical gap={spacing[3]} data-testid="search-ranges">
|
|
388
|
-
{ranges.map((group) => (
|
|
389
|
-
<RangeFilterRow
|
|
390
|
-
key={group.slug}
|
|
391
|
-
group={group}
|
|
392
|
-
onApply={bag.setRange}
|
|
393
|
-
/>
|
|
394
|
-
))}
|
|
395
|
-
</Flex>
|
|
396
|
-
)}
|
|
397
|
-
|
|
398
|
-
{ranges.length > 0 && <Divider style={{ margin: 0 }} />}
|
|
399
|
-
|
|
400
429
|
<LoadList
|
|
401
430
|
state={bag.state}
|
|
402
431
|
testId="facets"
|
|
@@ -440,7 +469,8 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
|
|
|
440
469
|
)}
|
|
441
470
|
</LoadList>
|
|
442
471
|
</Flex>
|
|
443
|
-
|
|
472
|
+
);
|
|
473
|
+
}}
|
|
444
474
|
</FacetPanel>
|
|
445
475
|
</SkinTheme>
|
|
446
476
|
);
|