@stapel/search-react 0.11.0 → 0.13.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 +178 -0
- package/MODULE.md +118 -3
- package/README.md +42 -0
- package/dist/api/generated/schema.d.ts +60 -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/FacetPanelPane.d.ts +13 -2
- package/dist/default/FacetPanelPane.d.ts.map +1 -1
- package/dist/default/FacetPanelPane.js +3 -1
- 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 +161 -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/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/SearchResultCard.d.ts.map +1 -1
- package/dist/default/SearchResultCard.js +83 -37
- package/dist/default/SearchResultCard.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/cardPhotos.d.ts +65 -0
- package/dist/default/cardPhotos.d.ts.map +1 -0
- package/dist/default/cardPhotos.js +127 -0
- package/dist/default/cardPhotos.js.map +1 -0
- 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 +10 -0
- package/dist/headless/FacetPanel.d.ts.map +1 -1
- package/dist/headless/FacetPanel.js +9 -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 +10 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +55 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +67 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +12 -0
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- 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/model/runtime.d.ts +40 -3
- package/dist/model/runtime.d.ts.map +1 -1
- package/dist/model/runtime.js +2 -1
- package/dist/model/runtime.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/facets.d.ts +56 -15
- package/dist/state/facets.d.ts.map +1 -1
- package/dist/state/facets.js +101 -27
- package/dist/state/facets.js.map +1 -1
- package/llms.txt +3 -3
- package/manifest.json +33 -4
- package/nav-manifest.json +4 -3
- package/package.json +11 -11
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +60 -6
- package/src/api/searchApi.ts +13 -6
- package/src/api/types.ts +115 -1
- package/src/default/FacetPanelPane.tsx +16 -2
- package/src/default/FilterChips.tsx +285 -38
- package/src/default/LocationSummaryLine.tsx +6 -1
- package/src/default/SearchBox.tsx +134 -6
- package/src/default/SearchPage.tsx +79 -5
- package/src/default/SearchResultCard.tsx +157 -71
- package/src/default/SearchResultsPane.tsx +48 -11
- package/src/default/SortSelect.tsx +70 -9
- package/src/default/cardPhotos.ts +195 -0
- package/src/default/index.ts +10 -2
- package/src/headless/FacetPanel.tsx +20 -1
- package/src/headless/useFacetLabels.ts +172 -0
- package/src/headless/useSearchBox.ts +132 -5
- package/src/i18n/es.ts +11 -0
- package/src/i18n/keys.ts +69 -0
- package/src/i18n/ru.ts +13 -0
- package/src/index.ts +22 -3
- package/src/model/queries.ts +6 -5
- package/src/model/queryKeys.ts +23 -0
- package/src/model/runtime.ts +43 -4
- package/src/nav/manifest.ts +24 -2
- package/src/state/facets.ts +123 -39
package/src/state/facets.ts
CHANGED
|
@@ -8,30 +8,95 @@
|
|
|
8
8
|
* they would have if you swapped to them. A panel that greys the siblings
|
|
9
9
|
* out has silently converted a drill-down facet into a naive one, and the
|
|
10
10
|
* e2e leg in the spec (§7.2) exists to catch exactly that.
|
|
11
|
-
* 2. **Option LABELS
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* and, under it, the answer's own `facet_labels` (stapel-search 0.4.0+).
|
|
11
|
+
* 2. **Option LABELS have one stated order: ANSWER, then schema, then the
|
|
12
|
+
* raw value** — with a host resolver between the last two, applied by
|
|
13
|
+
* `useHostFacetLabels` on what this module could not name.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
15
|
+
* The answer leads because it is the only source that always exists and
|
|
16
|
+
* the only one that has RESOLVED anything: `categoryFeatures` is an
|
|
17
|
+
* optional slot a live classified board never filled, and even where it is
|
|
18
|
+
* filled a `ref_select` config carries a pointer to a vocabulary and no
|
|
19
|
+
* option table at all — so the schema cannot name `apple` or `chernyy` no
|
|
20
|
+
* matter who threads it through. `facet_labels` (stapel-search 0.4.0+,
|
|
21
|
+
* vocabulary-backed from 0.6.0) is the server reading that vocabulary
|
|
22
|
+
* against the snapshot the documents were written with, and answering in
|
|
23
|
+
* the request's own language. The schema is the floor under it, for a
|
|
24
|
+
* server too old to send captions and for a slug the answer omits.
|
|
25
|
+
*
|
|
26
|
+
* Neither invents a label: a value nobody names renders as itself. A page
|
|
27
|
+
* against a pre-0.4.0 server therefore behaves exactly as it did — the key
|
|
28
|
+
* is absent, every option falls through to the schema, and nothing
|
|
29
|
+
* crashes on the missing key.
|
|
25
30
|
*
|
|
26
31
|
* A slug the server SKIPPED (`facet_meta.skipped`, dropped at
|
|
27
32
|
* `MAX_FACET_FIELDS`) is not counted at all. Its options carry `count: null`,
|
|
28
33
|
* never `0` — "we did not count this" and "there are none" are different
|
|
29
34
|
* sentences and the honest one has to survive to the screen.
|
|
35
|
+
*
|
|
36
|
+
* 3. **Not every counted slug is a FILTER.** The plan is built from the leaf
|
|
37
|
+
* category's feature defs and the counter counts whatever is indexed, so a
|
|
38
|
+
* live classified deployment answers with `imei: {"355971829187494": 1}`
|
|
39
|
+
* and `video_file_url: {}` beside its brand and its condition. Neither is
|
|
40
|
+
* something a person narrows by — one is unique per document, the other is
|
|
41
|
+
* a URL — and on a 390px chip row a chip offering one IMEI with a count of
|
|
42
|
+
* one pushes the chips that DO narrow off the screen. Which slugs can be
|
|
43
|
+
* chipped is therefore decided from the category's own feature defs, by
|
|
44
|
+
* value TYPE (see {@link FACETABLE_FEATURE_TYPES}), not by what came back.
|
|
30
45
|
*/
|
|
31
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
VOCABULARY_BACKED_TYPES,
|
|
48
|
+
featureConfig,
|
|
49
|
+
featureName,
|
|
50
|
+
featureType,
|
|
51
|
+
formatFeatureValue,
|
|
52
|
+
} from "@stapel/attributes-react";
|
|
32
53
|
import type { FeatureDef } from "@stapel/attributes-react";
|
|
33
54
|
import type { FacetMeta, SearchQueryState } from "../api/types.js";
|
|
34
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Value types whose values are a BOUNDED OPTION SET — the only kind of
|
|
58
|
+
* feature a person can be offered as a filter.
|
|
59
|
+
*
|
|
60
|
+
* The list is not invented here: the ref/vocabulary-backed half is
|
|
61
|
+
* `@stapel/attributes-react`'s own `VOCABULARY_BACKED_TYPES` (the one place
|
|
62
|
+
* that knows which types are drawn from a vocabulary), and the rest are the
|
|
63
|
+
* select family plus `bool`. Everything else a category can declare — a
|
|
64
|
+
* `string`, an `int`, a `date`, a `hex_color`, a `group` — enumerates as many
|
|
65
|
+
* terms as there are documents, which is a list, not a choice.
|
|
66
|
+
*
|
|
67
|
+
* `int`/`float`/`convertible_unit` are absent here and present in
|
|
68
|
+
* `RANGE_FEATURE_TYPES` (`state/ranges.ts`): a number is narrowed with two
|
|
69
|
+
* bounds, not with a checkbox per value. They are not dropped from the panel,
|
|
70
|
+
* they are drawn by the other half of the model.
|
|
71
|
+
*/
|
|
72
|
+
export const FACETABLE_FEATURE_TYPES: readonly string[] = [
|
|
73
|
+
"bool",
|
|
74
|
+
"hierarchical_select",
|
|
75
|
+
...VOCABULARY_BACKED_TYPES,
|
|
76
|
+
"select",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Can a person filter by the slug this feature def describes?
|
|
81
|
+
*
|
|
82
|
+
* **A missing feature def is not a "no".** `categoryFeatures` is an OPTIONAL
|
|
83
|
+
* slot and a whole deployment can run without it; a feature can also be
|
|
84
|
+
* retired between the write that indexed a value and the read that counts it.
|
|
85
|
+
* Answering "not facetable" for an absent def would blank the entire chip row
|
|
86
|
+
* for every host that never threaded the schema through — the same empty row
|
|
87
|
+
* this rule exists to fix, arrived at from the other side. So the schema can
|
|
88
|
+
* only ever REMOVE a chip it names and disowns; silence removes nothing.
|
|
89
|
+
*
|
|
90
|
+
* The same reasoning covers a def with no `config.type` at all: an untyped
|
|
91
|
+
* feature is a def that says nothing, and nothing is not a verdict.
|
|
92
|
+
*/
|
|
93
|
+
export function isFacetableFeature(feature: FeatureDef | undefined): boolean {
|
|
94
|
+
if (feature === undefined) return true;
|
|
95
|
+
const type = featureType(feature);
|
|
96
|
+
if (type === undefined) return true;
|
|
97
|
+
return FACETABLE_FEATURE_TYPES.includes(type);
|
|
98
|
+
}
|
|
99
|
+
|
|
35
100
|
/** One value of one facet. */
|
|
36
101
|
export interface FacetOption {
|
|
37
102
|
readonly value: string;
|
|
@@ -68,23 +133,25 @@ export interface BuildFacetGroupsInput {
|
|
|
68
133
|
/** The category's feature schema, for labels and option order. */
|
|
69
134
|
readonly categoryFeatures?: readonly FeatureDef[];
|
|
70
135
|
/**
|
|
71
|
-
* The envelope's `facet_labels` (stapel-search 0.4.0
|
|
72
|
-
* `{slug: {translatable, values: {value: caption}}}`.
|
|
136
|
+
* The envelope's `facet_labels` (stapel-search 0.4.0+, vocabulary-backed
|
|
137
|
+
* from 0.6.0): `{slug: {translatable, values: {value: caption}}}`.
|
|
73
138
|
*
|
|
74
|
-
* The
|
|
75
|
-
* the
|
|
76
|
-
* fetches `GET /categories/{id}/features/` with its own `Accept-Language`,
|
|
77
|
-
* so a host that threaded the schema through has the better-localized
|
|
78
|
-
* copy and keeps it. What the answer's captions fix is the case where
|
|
79
|
-
* there is no other copy at all — `categoryFeatures` is an OPTIONAL slot,
|
|
80
|
-
* a live classified board never filled it, and its buyers read
|
|
81
|
-
* "Condition: **b-u**" and "Listing kind: **prodayu-svoe**" on the SERP and in the
|
|
82
|
-
* filter chips. A caption that arrives with the counts cannot be
|
|
83
|
-
* forgotten by a host.
|
|
139
|
+
* The PRIMARY caption source, above `categoryFeatures`. Two reasons, and
|
|
140
|
+
* the second is the one that cannot be worked around:
|
|
84
141
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
142
|
+
* - it is always there. The schema slot is optional and a live classified
|
|
143
|
+
* board never filled it, so its buyers read "Condition: **b-u**" and
|
|
144
|
+
* "Listing kind: **prodayu-svoe**" on the SERP and in the filter chips.
|
|
145
|
+
* - it is the only source that RESOLVED anything. A `ref_select`'s config
|
|
146
|
+
* carries an `optionsRef` pointer and no option table, so a host that
|
|
147
|
+
* threads the entire schema through still has nothing to print for
|
|
148
|
+
* `apple` or `chernyy`. The server read that vocabulary against the
|
|
149
|
+
* snapshot the documents were written with.
|
|
150
|
+
*
|
|
151
|
+
* ABSENT on a server older than 0.4.0 — absent, not empty — which is why
|
|
152
|
+
* every read of it here is optional-chained and every option falls through
|
|
153
|
+
* to the schema and then to its raw self. No labels are invented at any
|
|
154
|
+
* step.
|
|
88
155
|
*/
|
|
89
156
|
readonly facetLabels?: Readonly<
|
|
90
157
|
Record<
|
|
@@ -149,14 +216,19 @@ function declaredOptionValues(feature: FeatureDef | undefined): readonly string[
|
|
|
149
216
|
}
|
|
150
217
|
|
|
151
218
|
/**
|
|
152
|
-
* One option's caption:
|
|
219
|
+
* One option's caption: the ANSWER, then the schema, then the raw value.
|
|
220
|
+
*
|
|
221
|
+
* The answer leads because it is the source that always exists and the only
|
|
222
|
+
* one that can name a vocabulary-backed value at all: a `ref_select`'s config
|
|
223
|
+
* is a POINTER (`optionsRef`), so a host who threaded the whole schema through
|
|
224
|
+
* still has nothing to print for `apple`. The server read that vocabulary
|
|
225
|
+
* against the snapshot the documents carry and answered in the request's own
|
|
226
|
+
* language, which is a strictly better-informed caption than the option table
|
|
227
|
+
* the schema may or may not hold.
|
|
153
228
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* the better-localized answer. Where it does not — no schema, a slug the
|
|
158
|
-
* schema omits, a value added since — the server's caption is what stops
|
|
159
|
-
* `b-u` reaching a buyer.
|
|
229
|
+
* The schema is the floor: a server too old to send `facet_labels`, a slug the
|
|
230
|
+
* answer omits, a value the vocabulary has since dropped. Below both, the raw
|
|
231
|
+
* value — never a blank, and never a guess.
|
|
160
232
|
*/
|
|
161
233
|
function resolveLabel(
|
|
162
234
|
input: BuildFacetGroupsInput,
|
|
@@ -165,9 +237,10 @@ function resolveLabel(
|
|
|
165
237
|
value: string,
|
|
166
238
|
labelOptions: { t?: (key: string) => string; locale?: string }
|
|
167
239
|
): string {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
240
|
+
return (
|
|
241
|
+
serverLabel(input.facetLabels, slug, value, input.t) ??
|
|
242
|
+
facetOptionLabel(feature, value, labelOptions)
|
|
243
|
+
);
|
|
171
244
|
}
|
|
172
245
|
|
|
173
246
|
/**
|
|
@@ -231,6 +304,13 @@ export function facetOptionLabel(
|
|
|
231
304
|
* person has filtered on — the last one matters: a filter whose slug fell out
|
|
232
305
|
* of the plan must stay visible, or it becomes a constraint with no control to
|
|
233
306
|
* remove it.
|
|
307
|
+
*
|
|
308
|
+
* A slug the category schema names and types as something no one can choose
|
|
309
|
+
* from (see {@link isFacetableFeature}) produces no group at all — not an
|
|
310
|
+
* empty one, because an empty group is still a heading in the panel and still
|
|
311
|
+
* a chip in the row. The APPLIED-filter clause outranks the type rule: a
|
|
312
|
+
* constraint the URL carries always gets its control back, whatever the schema
|
|
313
|
+
* now says about it, or a person is left holding a filter they cannot clear.
|
|
234
314
|
*/
|
|
235
315
|
export function buildFacetGroups(input: BuildFacetGroupsInput): readonly FacetGroup[] {
|
|
236
316
|
const bySlug = new Map<string, FeatureDef>();
|
|
@@ -246,6 +326,10 @@ export function buildFacetGroups(input: BuildFacetGroupsInput): readonly FacetGr
|
|
|
246
326
|
]) {
|
|
247
327
|
if (seen.has(slug)) continue;
|
|
248
328
|
seen.add(slug);
|
|
329
|
+
// Applied first, type second — in that order, so an `imei` somebody
|
|
330
|
+
// somehow got into a link keeps the control that removes it.
|
|
331
|
+
const applied = (input.state.filters[slug] ?? []).length > 0;
|
|
332
|
+
if (!applied && !isFacetableFeature(bySlug.get(slug))) continue;
|
|
249
333
|
slugs.push(slug);
|
|
250
334
|
}
|
|
251
335
|
|