@stapel/search-react 0.30.1 → 0.32.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 +97 -0
- package/dist/api/generated/schema.d.ts +71 -6
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/api/types.d.ts +118 -33
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js +13 -0
- package/dist/api/types.js.map +1 -1
- package/dist/default/FacetGroupControl.d.ts +18 -1
- package/dist/default/FacetGroupControl.d.ts.map +1 -1
- package/dist/default/FacetGroupControl.js +25 -0
- package/dist/default/FacetGroupControl.js.map +1 -1
- package/dist/default/FacetPanelPane.d.ts +16 -0
- package/dist/default/FacetPanelPane.d.ts.map +1 -1
- package/dist/default/FacetPanelPane.js +94 -19
- package/dist/default/FacetPanelPane.js.map +1 -1
- package/dist/default/FilterChips.d.ts +22 -0
- package/dist/default/FilterChips.d.ts.map +1 -1
- package/dist/default/FilterChips.js +31 -2
- package/dist/default/FilterChips.js.map +1 -1
- package/dist/default/PopularValues.d.ts +49 -2
- package/dist/default/PopularValues.d.ts.map +1 -1
- package/dist/default/PopularValues.js +57 -3
- package/dist/default/PopularValues.js.map +1 -1
- package/dist/default/RangeFilterRow.d.ts +25 -0
- package/dist/default/RangeFilterRow.d.ts.map +1 -1
- package/dist/default/RangeFilterRow.js +77 -5
- package/dist/default/RangeFilterRow.js.map +1 -1
- package/dist/default/SearchPage.d.ts +28 -1
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +13 -6
- package/dist/default/SearchPage.js.map +1 -1
- package/dist/default/SearchResultsPane.d.ts +10 -0
- package/dist/default/SearchResultsPane.d.ts.map +1 -1
- package/dist/default/SearchResultsPane.js +1 -1
- package/dist/default/SearchResultsPane.js.map +1 -1
- package/dist/default/SortSelect.d.ts +10 -10
- package/dist/default/SortSelect.d.ts.map +1 -1
- package/dist/default/SortSelect.js +19 -8
- package/dist/default/SortSelect.js.map +1 -1
- package/dist/default/index.d.ts +4 -4
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +3 -3
- package/dist/default/index.js.map +1 -1
- package/dist/headless/FacetPanel.d.ts +7 -0
- package/dist/headless/FacetPanel.d.ts.map +1 -1
- package/dist/headless/FacetPanel.js +2 -1
- package/dist/headless/FacetPanel.js.map +1 -1
- package/dist/headless/SearchStateProvider.d.ts +15 -0
- package/dist/headless/SearchStateProvider.d.ts.map +1 -1
- package/dist/headless/SearchStateProvider.js +0 -0
- package/dist/headless/SearchStateProvider.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/state/facets.d.ts +13 -0
- package/dist/state/facets.d.ts.map +1 -1
- package/dist/state/facets.js +13 -0
- package/dist/state/facets.js.map +1 -1
- package/dist/state/panel.d.ts +79 -0
- package/dist/state/panel.d.ts.map +1 -0
- package/dist/state/panel.js +75 -0
- package/dist/state/panel.js.map +1 -0
- package/dist/state/ranges.d.ts +55 -3
- package/dist/state/ranges.d.ts.map +1 -1
- package/dist/state/ranges.js +78 -20
- package/dist/state/ranges.js.map +1 -1
- package/llms.txt +4 -4
- package/manifest.json +12 -2
- package/nav-manifest.json +1 -1
- package/package.json +9 -9
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +71 -6
- package/src/api/types.ts +132 -32
- package/src/default/FacetGroupControl.tsx +29 -1
- package/src/default/FacetPanelPane.tsx +240 -124
- package/src/default/FilterChips.tsx +39 -1
- package/src/default/PopularValues.tsx +88 -4
- package/src/default/RangeFilterRow.tsx +117 -20
- package/src/default/SearchPage.tsx +46 -4
- package/src/default/SearchResultsPane.tsx +13 -0
- package/src/default/SortSelect.tsx +29 -19
- package/src/default/index.ts +8 -1
- package/src/headless/FacetPanel.tsx +17 -2
- package/src/headless/SearchStateProvider.tsx +23 -0
- package/src/index.ts +8 -0
- package/src/state/facets.ts +27 -0
- package/src/state/panel.ts +138 -0
- package/src/state/ranges.ts +158 -47
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE panel, not two halves of one — the order a filter rail draws its facet
|
|
3
|
+
* groups and its numeric axes in.
|
|
4
|
+
*
|
|
5
|
+
* ── The defect this closes ────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* The rail drew three blocks in a fixed sequence: the CORE ranges (price),
|
|
8
|
+
* then every facet group, then every attribute range. That sequence is a
|
|
9
|
+
* client's opinion about a catalogue it has never read, and on a live cars
|
|
10
|
+
* leaf it put "Year" — a measurement the category authors second,
|
|
11
|
+
* right after the make — below forty checkbox groups, while "Price" sat above
|
|
12
|
+
* a make picker the schema puts first. A host asking for
|
|
13
|
+
* `partition → make → price → year` could pin the two GROUPS and had no way
|
|
14
|
+
* to say where the two RANGES went, because the two halves were ordered by
|
|
15
|
+
* different code and never compared.
|
|
16
|
+
*
|
|
17
|
+
* stapel-search 0.16.0 ends the argument by numbering both halves in ONE
|
|
18
|
+
* sequence: `facet_labels[<slug>].order` and `facet_meta.ranges[<slug>].order`
|
|
19
|
+
* are the same integer scale, assigned by the plan — core ranges first (they
|
|
20
|
+
* address a column every document in every corpus has), then the category's
|
|
21
|
+
* own schema order, mandatory first. Sorting both halves by that one key is
|
|
22
|
+
* all this module does.
|
|
23
|
+
*
|
|
24
|
+
* ── What happens when nobody numbered anything ────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* An older server states no `order` at all, and a row built from the CATEGORY
|
|
27
|
+
* SCHEMA alone never has one however new the server is. Those items keep the
|
|
28
|
+
* band order this pair has always used — core ranges, then the groups in the
|
|
29
|
+
* order the caller handed them (which is `orderFacetGroupsBySchema`'s), then
|
|
30
|
+
* the remaining measurements — and sit AFTER everything the plan did number,
|
|
31
|
+
* because a stated position is evidence and an assumed one is not.
|
|
32
|
+
*
|
|
33
|
+
* `pinned` outranks both. It is the host saying "this page is about make",
|
|
34
|
+
* and a page that has decided what it is about is not overruled by the
|
|
35
|
+
* category's own idea of a reading order.
|
|
36
|
+
*/
|
|
37
|
+
import type { FacetGroup } from "./facets.js";
|
|
38
|
+
import type { RangeGroup } from "./ranges.js";
|
|
39
|
+
|
|
40
|
+
/** One row of the panel: a bucket list, or a from/to picker. */
|
|
41
|
+
export type PanelItem =
|
|
42
|
+
| { readonly kind: "group"; readonly slug: string; readonly group: FacetGroup }
|
|
43
|
+
| { readonly kind: "range"; readonly slug: string; readonly range: RangeGroup };
|
|
44
|
+
|
|
45
|
+
export interface OrderPanelItemsInput {
|
|
46
|
+
/**
|
|
47
|
+
* The facet groups, already filtered to what is drawable and ordered the
|
|
48
|
+
* way the rail wants them — `orderFacetGroupsBySchema`'s output. Their
|
|
49
|
+
* relative order is preserved for every group the plan did not number.
|
|
50
|
+
*/
|
|
51
|
+
readonly groups: readonly FacetGroup[];
|
|
52
|
+
/** The range rows, `buildRangeGroups`' output. */
|
|
53
|
+
readonly ranges: readonly RangeGroup[];
|
|
54
|
+
/**
|
|
55
|
+
* Slugs pinned above everything, in the order given — the axis a page has
|
|
56
|
+
* already decided is its subject. A slug matches whichever half carries it,
|
|
57
|
+
* so `["make", "price", "year"]` is one sequence over both.
|
|
58
|
+
*/
|
|
59
|
+
readonly pinned?: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The panel's rows, in the one order they are read in.
|
|
64
|
+
*
|
|
65
|
+
* Three bands, and every one of them is stable — equal-ranked items keep the
|
|
66
|
+
* order they arrived in, so nothing reshuffles under a click:
|
|
67
|
+
*
|
|
68
|
+
* 1. `pinned`, in the order given.
|
|
69
|
+
* 2. everything the ANSWER numbered (`order`), ascending. Both halves share
|
|
70
|
+
* the scale, so a group and a range can interleave inside it.
|
|
71
|
+
* 3. everything nobody numbered: core ranges, then groups, then attribute
|
|
72
|
+
* ranges — the band order the rail shipped with, kept for exactly the case
|
|
73
|
+
* it was invented for (a server or a row with no stated position).
|
|
74
|
+
*/
|
|
75
|
+
export function orderPanelItems(
|
|
76
|
+
input: OrderPanelItemsInput
|
|
77
|
+
): readonly PanelItem[] {
|
|
78
|
+
const items: PanelItem[] = [
|
|
79
|
+
...input.groups.map(
|
|
80
|
+
(group): PanelItem => ({ kind: "group", slug: group.slug, group })
|
|
81
|
+
),
|
|
82
|
+
...input.ranges.map(
|
|
83
|
+
(range): PanelItem => ({ kind: "range", slug: range.slug, range })
|
|
84
|
+
),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const pinnedIndex = new Map<string, number>();
|
|
88
|
+
(input.pinned ?? []).forEach((slug, index) => {
|
|
89
|
+
if (!pinnedIndex.has(slug)) pinnedIndex.set(slug, index);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const stated = (item: PanelItem): number | undefined =>
|
|
93
|
+
item.kind === "range" ? item.range.order : orderOf(item.group);
|
|
94
|
+
|
|
95
|
+
// The fallback band's own three tiers. A core axis first for the reason it
|
|
96
|
+
// has always been first: on a phones leaf every attribute range is parcel
|
|
97
|
+
// logistics and the one number a buyer narrows by is the price.
|
|
98
|
+
const fallbackTier = (item: PanelItem): number => {
|
|
99
|
+
if (item.kind === "range") return item.range.core ? 0 : 2;
|
|
100
|
+
return 1;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const decorated = items.map((item, index) => ({ item, index }));
|
|
104
|
+
decorated.sort((a, b) => {
|
|
105
|
+
const pinA = pinnedIndex.get(a.item.slug);
|
|
106
|
+
const pinB = pinnedIndex.get(b.item.slug);
|
|
107
|
+
if (pinA !== undefined || pinB !== undefined) {
|
|
108
|
+
if (pinA === undefined) return 1;
|
|
109
|
+
if (pinB === undefined) return -1;
|
|
110
|
+
if (pinA !== pinB) return pinA - pinB;
|
|
111
|
+
return a.index - b.index;
|
|
112
|
+
}
|
|
113
|
+
const orderA = stated(a.item);
|
|
114
|
+
const orderB = stated(b.item);
|
|
115
|
+
if (orderA !== undefined || orderB !== undefined) {
|
|
116
|
+
// A stated position beats an assumed one, so the unnumbered tail follows
|
|
117
|
+
// the plan rather than being interleaved into it by a guess.
|
|
118
|
+
if (orderA === undefined) return 1;
|
|
119
|
+
if (orderB === undefined) return -1;
|
|
120
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
121
|
+
return a.index - b.index;
|
|
122
|
+
}
|
|
123
|
+
const tier = fallbackTier(a.item) - fallbackTier(b.item);
|
|
124
|
+
if (tier !== 0) return tier;
|
|
125
|
+
return a.index - b.index;
|
|
126
|
+
});
|
|
127
|
+
return decorated.map((entry) => entry.item);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A group's stated position, or `undefined`.
|
|
132
|
+
*
|
|
133
|
+
* `null` is the server's own "the plan has no place for this group" and reads
|
|
134
|
+
* exactly like an absent field — both mean "sort me with the rest".
|
|
135
|
+
*/
|
|
136
|
+
function orderOf(group: FacetGroup): number | undefined {
|
|
137
|
+
return typeof group.order === "number" ? group.order : undefined;
|
|
138
|
+
}
|
package/src/state/ranges.ts
CHANGED
|
@@ -12,14 +12,19 @@
|
|
|
12
12
|
* values; a range is not enumerable and no bucket is ever sent for one. So
|
|
13
13
|
* the rows come from three places:
|
|
14
14
|
*
|
|
15
|
-
* - the ANSWER's `facet_meta.ranges` (stapel-search 0.14.7
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
15
|
+
* - the ANSWER's `facet_meta.ranges` (stapel-search 0.14.7+, a whole axis
|
|
16
|
+
* rather than two numbers since 0.16.0) — `{slug: {min, max, label,
|
|
17
|
+
* label_translatable, unit?, order}}` for every axis this page has NUMBERS
|
|
18
|
+
* behind, measured with the range filters removed. When the answer reports
|
|
19
|
+
* it, it is the authority on which attribute axes exist, where their ends
|
|
20
|
+
* are, WHAT THEY ARE CALLED and where in the panel they sit: the schema
|
|
21
|
+
* says an axis COULD be numeric, and this says it IS, on these documents,
|
|
22
|
+
* under this name. It reaches axes the schema's own type cannot — a
|
|
23
|
+
* vocabulary-backed `year`, a `floor`, a `doors` are CHOICES in the
|
|
24
|
+
* catalogue and from/to's to a buyer, and both are served.
|
|
25
|
+
* - the ANSWER's `facet_meta.withheld` (0.16.0), which is the same authority
|
|
26
|
+
* used in the negative: an axis it names as a withheld `range` is one this
|
|
27
|
+
* answer planned and declined to offer, and no schema row resurrects it.
|
|
23
28
|
* - the CATEGORY SCHEMA — the same `categoryFeatures` slot that gives the
|
|
24
29
|
* checkboxes their labels — filtered to the numeric value types. It is the
|
|
25
30
|
* FALLBACK, and the only source against a server that reports no `ranges`
|
|
@@ -49,9 +54,11 @@ import { featureConfig, featureName, featureType } from "@stapel/attributes-reac
|
|
|
49
54
|
import type { FeatureDef } from "@stapel/attributes-react";
|
|
50
55
|
import type {
|
|
51
56
|
FacetRangesMap,
|
|
57
|
+
FacetWithheldAxis,
|
|
52
58
|
SearchQueryState,
|
|
53
59
|
SearchRange,
|
|
54
60
|
} from "../api/types.js";
|
|
61
|
+
import { withheldSlugs } from "../api/types.js";
|
|
55
62
|
|
|
56
63
|
/**
|
|
57
64
|
* Value types a numeric range row is drawn for (`config.type`, the
|
|
@@ -66,7 +73,13 @@ export const RANGE_FEATURE_TYPES: readonly string[] = [
|
|
|
66
73
|
/** One `r.<slug>` row of the filter panel. */
|
|
67
74
|
export interface RangeGroup {
|
|
68
75
|
readonly slug: string;
|
|
69
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* What to write above the picker: the ANSWER's own caption first (0.16.0
|
|
78
|
+
* resolves it from the same definition a group heading comes from), the
|
|
79
|
+
* schema's display name next, this package's key for a core axis, and — only
|
|
80
|
+
* for a slug the URL constrains that none of them explain — the slug itself.
|
|
81
|
+
* {@link named} says which of those two worlds the row is in.
|
|
82
|
+
*/
|
|
70
83
|
readonly label: string;
|
|
71
84
|
/** The schema entry behind the slug, when the host supplied one. */
|
|
72
85
|
readonly feature: FeatureDef | undefined;
|
|
@@ -87,7 +100,10 @@ export interface RangeGroup {
|
|
|
87
100
|
* has. Never set on a core axis — see {@link BuildRangeGroupsInput.ranges}.
|
|
88
101
|
*/
|
|
89
102
|
readonly measured: boolean;
|
|
90
|
-
/**
|
|
103
|
+
/** What the numbers are measured in: the ANSWER's `unit` (the definition's
|
|
104
|
+
* `postfix`, or the BASE unit of a convertible family) when it states one,
|
|
105
|
+
* the schema's own affixes otherwise. Absent, never `""`, on an axis nobody
|
|
106
|
+
* gave a unit — a price among them. */
|
|
91
107
|
readonly unit: string | undefined;
|
|
92
108
|
/** `1` for an integer feature — a whole-number input for a whole number. */
|
|
93
109
|
readonly step: number | undefined;
|
|
@@ -121,6 +137,31 @@ export interface RangeGroup {
|
|
|
121
137
|
* carries a `postfix`, not a currency.
|
|
122
138
|
*/
|
|
123
139
|
readonly currency: string | undefined;
|
|
140
|
+
/**
|
|
141
|
+
* Where this axis sits in the ONE sequence the panel draws — the answer's
|
|
142
|
+
* `facet_meta.ranges[<slug>].order`, numbered together with the groups'
|
|
143
|
+
* `facet_labels[<slug>].order` (stapel-search 0.16.0+).
|
|
144
|
+
*
|
|
145
|
+
* `undefined` means the answer stated no position (an older server, an axis
|
|
146
|
+
* the plan has no place for, or a row that came from the schema alone), and
|
|
147
|
+
* a panel then falls back to the band order it has always used: core axes
|
|
148
|
+
* first, then the groups, then the rest of the measurements. See
|
|
149
|
+
* `orderPanelItems`.
|
|
150
|
+
*/
|
|
151
|
+
readonly order: number | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* `true` when {@link label} is a name SOMEBODY GAVE the axis — the answer's
|
|
154
|
+
* own caption, the schema's `FeatureDef.name`, or this package's key for a
|
|
155
|
+
* core axis — and `false` when it is the storage slug standing in for a name
|
|
156
|
+
* nobody has.
|
|
157
|
+
*
|
|
158
|
+
* A row is only ever unlabelled when the URL already constrains the slug: a
|
|
159
|
+
* constraint must keep the control that removes it, even one captioned
|
|
160
|
+
* `power_w`. Every other unnamed axis is dropped before it reaches a
|
|
161
|
+
* surface, which is the client half of the rule stapel-search 0.16.0 states
|
|
162
|
+
* on the wire by WITHHOLDING an axis it could not caption.
|
|
163
|
+
*/
|
|
164
|
+
readonly named: boolean;
|
|
124
165
|
}
|
|
125
166
|
|
|
126
167
|
/**
|
|
@@ -175,6 +216,24 @@ export interface BuildRangeGroupsInput {
|
|
|
175
216
|
* that wants to draw a histogram over it.
|
|
176
217
|
*/
|
|
177
218
|
readonly ranges?: FacetRangesMap;
|
|
219
|
+
/**
|
|
220
|
+
* `facet_meta.withheld` — the axes the server planned and then did NOT
|
|
221
|
+
* offer (stapel-search 0.16.0+).
|
|
222
|
+
*
|
|
223
|
+
* Only the rows whose `axis` is `"range"` are read here, and reading them is
|
|
224
|
+
* not optional: an axis withheld for `coverage` describes three of fifty-two
|
|
225
|
+
* documents and an axis withheld as `unlabelled` has no caption anyone could
|
|
226
|
+
* print, and the server leaving it out of `ranges` is only half the
|
|
227
|
+
* statement — the CATEGORY SCHEMA still names the same slug, so a rail that
|
|
228
|
+
* ignored this list would draw from the schema exactly the row the answer
|
|
229
|
+
* just declined to offer.
|
|
230
|
+
*
|
|
231
|
+
* The one exemption is a slug the URL constrains. The server promises never
|
|
232
|
+
* to withhold one (that would leave a filter applied with no control to undo
|
|
233
|
+
* it) and this module keeps the row anyway if it ever does, for the same
|
|
234
|
+
* reason it keeps a row the schema cannot explain.
|
|
235
|
+
*/
|
|
236
|
+
readonly withheld?: readonly FacetWithheldAxis[];
|
|
178
237
|
/** ISO 4217 code for the money axes, when the surface knows one. */
|
|
179
238
|
readonly currency?: string;
|
|
180
239
|
/** Translator for label keys (the schema's `name` is often one). */
|
|
@@ -279,6 +338,9 @@ export function buildRangeGroups(
|
|
|
279
338
|
const core = new Set(input.coreRanges ?? []);
|
|
280
339
|
const bounds = input.ranges;
|
|
281
340
|
const reported = new Set(Object.keys(bounds ?? {}));
|
|
341
|
+
// The axes this answer planned and declined to offer — sparse, or with no
|
|
342
|
+
// caption anyone could print. Dropped unless the URL constrains them.
|
|
343
|
+
const withheld = new Set(withheldSlugs(input.withheld, "range"));
|
|
282
344
|
const slugs: string[] = [...core];
|
|
283
345
|
for (const feature of input.categoryFeatures ?? []) {
|
|
284
346
|
// A core slug shadows a same-named attribute — which is exactly what the
|
|
@@ -300,43 +362,92 @@ export function buildRangeGroups(
|
|
|
300
362
|
if (!slugs.includes(slug)) slugs.push(slug);
|
|
301
363
|
}
|
|
302
364
|
|
|
303
|
-
return slugs
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
to
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
365
|
+
return slugs
|
|
366
|
+
.map((slug): RangeGroup => {
|
|
367
|
+
const isCore = core.has(slug);
|
|
368
|
+
const feature = isCore ? undefined : bySlug.get(slug);
|
|
369
|
+
const config = feature === undefined ? {} : featureConfig(feature);
|
|
370
|
+
const applied: SearchRange | undefined = input.state.ranges[slug];
|
|
371
|
+
// The ANSWER's row for the slug. Read for its caption on every axis
|
|
372
|
+
// including a core one — the server names `price` out of its own
|
|
373
|
+
// `CORE_RANGE_LABELS` — and for its BOUNDS on an attribute axis only:
|
|
374
|
+
// see `BuildRangeGroupsInput.ranges` for why the price input keeps its
|
|
375
|
+
// own ends.
|
|
376
|
+
const axis = bounds?.[slug];
|
|
377
|
+
const measured = !isCore && axis !== undefined;
|
|
378
|
+
const min = measured ? axis.min : num(config["min"]);
|
|
379
|
+
const max = measured ? axis.max : num(config["max"]);
|
|
380
|
+
// The caption, in the order of who is entitled to give one:
|
|
381
|
+
// 1. the ANSWER. `label` is resolved server-side from the same
|
|
382
|
+
// definition a group heading comes from, it is present for a core
|
|
383
|
+
// axis the client has no definition for, and it is the only source
|
|
384
|
+
// a host that threaded no schema has at all.
|
|
385
|
+
// 2. the SCHEMA's own `name`, for a server too old to send one.
|
|
386
|
+
// 3. this package's key for a core axis, same reason as (2).
|
|
387
|
+
// and never the slug, which is storage. An axis none of the three name
|
|
388
|
+
// is dropped below.
|
|
389
|
+
const answered = str(axis?.label);
|
|
390
|
+
const named =
|
|
391
|
+
answered !== undefined || isCore || feature !== undefined;
|
|
392
|
+
const label =
|
|
393
|
+
answered !== undefined
|
|
394
|
+
? // `label_translatable: false` is literal text the catalogue wrote
|
|
395
|
+
// and must not be looked up; absent means the server did not say,
|
|
396
|
+
// and a lookup that misses returns the string unchanged.
|
|
397
|
+
axis?.label_translatable === false
|
|
398
|
+
? answered
|
|
399
|
+
: translate(input.t, answered)
|
|
400
|
+
: isCore
|
|
401
|
+
? translate(input.t, coreRangeLabelKey(slug))
|
|
402
|
+
: feature === undefined
|
|
403
|
+
? slug
|
|
404
|
+
: translate(input.t, featureName(feature));
|
|
405
|
+
return {
|
|
406
|
+
slug,
|
|
407
|
+
label,
|
|
408
|
+
named,
|
|
409
|
+
feature,
|
|
410
|
+
from: applied?.from,
|
|
411
|
+
to: applied?.to,
|
|
412
|
+
min,
|
|
413
|
+
max,
|
|
414
|
+
measured,
|
|
415
|
+
// The unit, same precedence and same reason: the answer resolved it
|
|
416
|
+
// off the definition (a `convertible_unit`'s BASE unit, which the
|
|
417
|
+
// client cannot work out from `unit_m`/`unit_i` alone), the schema's
|
|
418
|
+
// own affixes second. Translated, because the server states plainly
|
|
419
|
+
// that it never translates one and the string may be a key.
|
|
420
|
+
//
|
|
421
|
+
// A core money axis carries a CURRENCY, not a unit suffix: "₽" is
|
|
422
|
+
// formatted from the code for the reader's locale, a unit suffix is a
|
|
423
|
+
// literal the category author typed.
|
|
424
|
+
unit: isCore
|
|
425
|
+
? undefined
|
|
426
|
+
: str(axis?.unit) !== undefined
|
|
427
|
+
? translate(input.t, str(axis?.unit) ?? "")
|
|
428
|
+
: (str(config["postfix"]) ?? str(config["unit_m"]) ?? str(config["unit_i"])),
|
|
429
|
+
step: !isCore && isIntegerAxis(feature, min, max, measured) ? 1 : undefined,
|
|
430
|
+
picker: isCore ? undefined : pickerValues(feature, min, max, measured),
|
|
431
|
+
active: applied !== undefined,
|
|
432
|
+
core: isCore,
|
|
433
|
+
currency: isCore ? str(input.currency) : undefined,
|
|
434
|
+
order: typeof axis?.order === "number" ? axis.order : undefined,
|
|
435
|
+
};
|
|
436
|
+
})
|
|
437
|
+
.filter((group) => {
|
|
438
|
+
// A CONSTRAINT ALWAYS KEEPS ITS CONTROL. Everything below removes a row
|
|
439
|
+
// the reader has not used; a row they have is never one of them.
|
|
440
|
+
if (group.active) return true;
|
|
441
|
+
// The server planned this axis and declined to offer it — sparse, or
|
|
442
|
+
// unnameable. The schema still declares the same slug, so without this
|
|
443
|
+
// the rail would draw exactly the row the answer withheld.
|
|
444
|
+
if (withheld.has(group.slug)) return false;
|
|
445
|
+
// Nobody named it. A from/to picker captioned `kilometrage` is a control
|
|
446
|
+
// whose meaning a reader has to guess out of the numbers inside it,
|
|
447
|
+
// which is not a filter — the same judgement stapel-search 0.16.0 makes
|
|
448
|
+
// on the wire, made again here for the rows that come from the schema.
|
|
449
|
+
return group.named;
|
|
450
|
+
});
|
|
340
451
|
}
|
|
341
452
|
|
|
342
453
|
/**
|