@stapel/listings-react 0.23.0 → 0.25.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/MODULE.md +4 -0
  3. package/README.md +75 -0
  4. package/dist/api/generated/schema.d.ts +126 -11
  5. package/dist/api/generated/schema.d.ts.map +1 -1
  6. package/dist/api/listingsApi.d.ts +5 -3
  7. package/dist/api/listingsApi.d.ts.map +1 -1
  8. package/dist/api/listingsApi.js.map +1 -1
  9. package/dist/api/types.d.ts +51 -4
  10. package/dist/api/types.d.ts.map +1 -1
  11. package/dist/api/types.js.map +1 -1
  12. package/dist/default/CardBadges.d.ts +47 -0
  13. package/dist/default/CardBadges.d.ts.map +1 -0
  14. package/dist/default/CardBadges.js +38 -0
  15. package/dist/default/CardBadges.js.map +1 -0
  16. package/dist/default/ListingCard.d.ts.map +1 -1
  17. package/dist/default/ListingCard.js +6 -6
  18. package/dist/default/ListingCard.js.map +1 -1
  19. package/dist/default/ListingDetailPane.d.ts.map +1 -1
  20. package/dist/default/ListingDetailPane.js +12 -19
  21. package/dist/default/ListingDetailPane.js.map +1 -1
  22. package/dist/default/ListingPhoto.d.ts.map +1 -1
  23. package/dist/default/ListingPhoto.js +12 -8
  24. package/dist/default/ListingPhoto.js.map +1 -1
  25. package/dist/default/ListingSerpCard.d.ts.map +1 -1
  26. package/dist/default/ListingSerpCard.js +3 -5
  27. package/dist/default/ListingSerpCard.js.map +1 -1
  28. package/dist/default/ListingSpecList.d.ts +74 -0
  29. package/dist/default/ListingSpecList.d.ts.map +1 -0
  30. package/dist/default/ListingSpecList.js +90 -0
  31. package/dist/default/ListingSpecList.js.map +1 -0
  32. package/dist/default/cardGallery.d.ts +81 -0
  33. package/dist/default/cardGallery.d.ts.map +1 -0
  34. package/dist/default/cardGallery.js +266 -0
  35. package/dist/default/cardGallery.js.map +1 -0
  36. package/dist/default/index.d.ts +6 -0
  37. package/dist/default/index.d.ts.map +1 -1
  38. package/dist/default/index.js +5 -0
  39. package/dist/default/index.js.map +1 -1
  40. package/dist/headless/ListingComposer.d.ts +20 -2
  41. package/dist/headless/ListingComposer.d.ts.map +1 -1
  42. package/dist/headless/ListingComposer.js +19 -4
  43. package/dist/headless/ListingComposer.js.map +1 -1
  44. package/dist/i18n/es.d.ts.map +1 -1
  45. package/dist/i18n/es.js +1 -0
  46. package/dist/i18n/es.js.map +1 -1
  47. package/dist/i18n/generated/errors.es.gen.d.ts +1 -1
  48. package/dist/i18n/generated/errors.es.gen.js +1 -1
  49. package/dist/i18n/generated/errors.gen.d.ts +6 -0
  50. package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
  51. package/dist/i18n/generated/errors.gen.js +3 -0
  52. package/dist/i18n/generated/errors.gen.js.map +1 -1
  53. package/dist/i18n/generated/errors.ru.gen.d.ts +1 -1
  54. package/dist/i18n/generated/errors.ru.gen.js +1 -1
  55. package/dist/i18n/ru.d.ts.map +1 -1
  56. package/dist/i18n/ru.js +3 -0
  57. package/dist/i18n/ru.js.map +1 -1
  58. package/dist/index.d.ts +3 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +3 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/model/cardBadges.d.ts +134 -0
  63. package/dist/model/cardBadges.d.ts.map +1 -0
  64. package/dist/model/cardBadges.js +131 -0
  65. package/dist/model/cardBadges.js.map +1 -0
  66. package/dist/model/draft.d.ts +10 -2
  67. package/dist/model/draft.d.ts.map +1 -1
  68. package/dist/model/draft.js +22 -4
  69. package/dist/model/draft.js.map +1 -1
  70. package/dist/model/featureText.d.ts +85 -0
  71. package/dist/model/featureText.d.ts.map +1 -0
  72. package/dist/model/featureText.js +153 -0
  73. package/dist/model/featureText.js.map +1 -0
  74. package/dist/model/features.d.ts.map +1 -1
  75. package/dist/model/features.js +59 -0
  76. package/dist/model/features.js.map +1 -1
  77. package/llms.txt +5 -2
  78. package/manifest.json +47 -1
  79. package/nav-manifest.json +1 -1
  80. package/package.json +10 -10
  81. package/src/analytics/generated/events.json +1 -1
  82. package/src/api/generated/schema.ts +126 -11
  83. package/src/api/listingsApi.ts +5 -3
  84. package/src/api/types.ts +60 -6
  85. package/src/default/CardBadges.tsx +105 -0
  86. package/src/default/ListingCard.tsx +7 -21
  87. package/src/default/ListingDetailPane.tsx +13 -37
  88. package/src/default/ListingPhoto.tsx +54 -26
  89. package/src/default/ListingSerpCard.tsx +10 -28
  90. package/src/default/ListingSpecList.tsx +195 -0
  91. package/src/default/cardGallery.ts +287 -0
  92. package/src/default/index.ts +22 -0
  93. package/src/headless/ListingComposer.tsx +39 -5
  94. package/src/i18n/es.ts +3 -0
  95. package/src/i18n/generated/errors.es.gen.ts +1 -1
  96. package/src/i18n/generated/errors.gen.ts +3 -0
  97. package/src/i18n/generated/errors.json +9 -0
  98. package/src/i18n/generated/errors.ru.gen.ts +1 -1
  99. package/src/i18n/ru.ts +5 -0
  100. package/src/index.ts +15 -0
  101. package/src/model/cardBadges.ts +237 -0
  102. package/src/model/draft.ts +23 -5
  103. package/src/model/featureText.ts +251 -0
  104. package/src/model/features.ts +60 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The CARD BADGE CONTRACT — what a badge on a result card is allowed to say.
3
+ *
4
+ * ── What was on screen ────────────────────────────────────────────────────
5
+ *
6
+ * A live card's badge line read "Brick · 3 · 9". Three true facts about a
7
+ * flat, and two of them unreadable: 3 what, 9 what. The projection carried
8
+ * the VALUES and the display half printed them, because that is all a stored
9
+ * `features_badges` row could offer — a number with no unit and no question
10
+ * beside it.
11
+ *
12
+ * stapel-listings 0.21.3 adds the missing half to each element:
13
+ *
14
+ * `value` the raw answer
15
+ * `label` its resolved copy, where the answer is an option
16
+ * `unit` what the number is measured in
17
+ * `name` the question — the feature's own display name
18
+ * `presentation` which of those to print, decided by the SERVER
19
+ *
20
+ * `presentation` is the load-bearing key, and it is the server's decision
21
+ * rather than a rule guessed here, because the right badge for a value
22
+ * depends on the CATEGORY: "3 rooms" wants its name, "Brick" is a
23
+ * boolean whose name IS the badge, "20 000 km" wants its unit and no name at
24
+ * all, and no client-side heuristic gets all three right for every catalogue
25
+ * in the fleet.
26
+ *
27
+ * ── The four presentations ───────────────────────────────────────────────
28
+ *
29
+ * `value` the value alone — "Brick" (an option label)
30
+ * `value_unit` value and unit — "20 000 km"
31
+ * `name_value` name and value — "Floor 3": a SPACE and never a colon,
32
+ * because a card is a caption and not a
33
+ * form. The unit is kept when the
34
+ * element carries one — a named row
35
+ * that dropped it reads "Mileage 20 000"
36
+ * `name` the name alone — a TRUE boolean, whose whole content
37
+ * is that the flat is brick. A FALSE
38
+ * one renders nothing: "not brick" is
39
+ * not a selling point, and a card is a
40
+ * summary rather than a form.
41
+ *
42
+ * ── Why a local type extension and not a regenerated schema ───────────────
43
+ *
44
+ * The generated `FeatureDao` union is already unusable on this side (the
45
+ * discriminator defect, `api/types.ts`), `features_badges` is a `JSONField`
46
+ * on the serializer, and this pair reads the row through
47
+ * {@link ListingFeatureDao} — a hand-written mirror of the runtime shape —
48
+ * for exactly that reason. The contract is an additive extension of the same
49
+ * mirror, so a regeneration cannot take it away and an older backend that
50
+ * sends none of the five keys is simply a row where
51
+ * {@link hasCardBadgeContract} is false.
52
+ *
53
+ * ── The fallback is the whole compatibility story ─────────────────────────
54
+ *
55
+ * A row with no `presentation` is a row from a server that predates the
56
+ * contract, and it is rendered exactly as it is rendered today — through
57
+ * `@stapel/attributes-react`'s `<FeatureBadges>`, off the stored DAO's own
58
+ * config. Nothing about this module is required for a card to draw.
59
+ */
60
+ import type { ListingCardBadgeElement, ListingFeatureDao } from "../api/types.js";
61
+ /**
62
+ * Which parts of a badge element the server asked to be printed.
63
+ *
64
+ * Read off the GENERATED element rather than spelled again here: the four
65
+ * readings are the server's enum, and a fifth added upstream must turn this
66
+ * build red instead of falling through {@link badgePresentation}'s default in
67
+ * silence.
68
+ */
69
+ export type CardBadgePresentation = ListingCardBadgeElement["presentation"];
70
+ /**
71
+ * The keys stapel-listings 0.21.3 adds to a `features_badges` /
72
+ * `features_title` element.
73
+ *
74
+ * The NAMES and their types come from the generated element; the optionality
75
+ * is this pair's own, and is the whole compatibility story — the contract
76
+ * makes them required, and a server that predates it sends none of them. Same
77
+ * argument as {@link ListingEngagementFields}, one file over: a generated type
78
+ * is a promise about the contract, not about the bytes a deployment sends.
79
+ */
80
+ export type CardBadgeContract = {
81
+ /** The raw answer. Absent on a redacted row, which is why the renderers
82
+ * below all tolerate it, and `unknown` because the DAO union types it per
83
+ * feature type and a badge prints whatever it was given. */
84
+ readonly value?: unknown;
85
+ } & {
86
+ readonly [K in "label" | "unit" | "name"]?: ListingCardBadgeElement[K] | null;
87
+ } & {
88
+ /** Widened back to `string` on purpose: {@link badgePresentation} exists to
89
+ * REFUSE a reading it does not know, and a field typed as the four would
90
+ * make that refusal unreachable — and untestable — while a server is still
91
+ * free to send a fifth. */
92
+ readonly presentation?: string | null;
93
+ };
94
+ /** A stored badge row, with the contract's keys where the server sends them. */
95
+ export type CardBadgeRow = ListingFeatureDao & CardBadgeContract;
96
+ /**
97
+ * `presentation` as one of the four, or `undefined`.
98
+ *
99
+ * An unrecognised spelling is `undefined` rather than a guess: the whole
100
+ * point of the key is that the SERVER decided, and a client that invented a
101
+ * fifth reading would print one category's rule on another category's card.
102
+ */
103
+ export declare function badgePresentation(row: CardBadgeRow): CardBadgePresentation | undefined;
104
+ /**
105
+ * Does this projection speak the contract at all?
106
+ *
107
+ * ONE row declaring a `presentation` is enough: the server writes the whole
108
+ * list in one pass, so a mixed list means a row the server deliberately left
109
+ * plain, and that row still renders (as its value alone) rather than
110
+ * dragging the other four back to the old path.
111
+ */
112
+ export declare function hasCardBadgeContract(rows: readonly CardBadgeRow[]): boolean;
113
+ /**
114
+ * The element's answer, as a person reads it: the server's `label` when there
115
+ * is one, the value with its digits grouped when it is a number, the value
116
+ * verbatim otherwise.
117
+ */
118
+ export declare function badgeValueText(row: CardBadgeRow, locale?: string): string;
119
+ /**
120
+ * One badge element → the text a card prints, or `undefined` for an element
121
+ * that has nothing to say (a false boolean, a blank value).
122
+ *
123
+ * The one place the four presentations are read. Every card surface calls
124
+ * this and none of them re-derives it, for the same reason `CardTarget`
125
+ * exists: three cards each re-deciding what a badge says is three places for
126
+ * "Brick · 3 · 9" to come back.
127
+ */
128
+ export declare function cardBadgeText(row: CardBadgeRow, locale?: string): string | undefined;
129
+ /** Every element that has something to say, in the server's order. */
130
+ export declare function cardBadgeTexts(rows: readonly CardBadgeRow[], locale?: string): readonly {
131
+ readonly slug: string;
132
+ readonly text: string;
133
+ }[];
134
+ //# sourceMappingURL=cardBadges.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cardBadges.d.ts","sourceRoot":"","sources":["../../src/model/cardBadges.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,OAAO,KAAK,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAElF;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,uBAAuB,CAAC,cAAc,CAAC,CAAC;AAE5E;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;gEAE4D;IAC5D,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B,GAAG;IACF,QAAQ,EAAE,CAAC,IAAI,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAAG,IAAI;CAC9E,GAAG;IACF;;;+BAG2B;IAC3B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC,CAAC;AAEF,gFAAgF;AAChF,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEjE;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,qBAAqB,GAAG,SAAS,CAYtF;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,YAAY,EAAE,GAAG,OAAO,CAE3E;AAqBD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAmBzE;AAOD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA8BpF;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAC5B,IAAI,EAAE,SAAS,YAAY,EAAE,EAC7B,MAAM,CAAC,EAAE,MAAM,GACd,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAS7D"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `presentation` as one of the four, or `undefined`.
3
+ *
4
+ * An unrecognised spelling is `undefined` rather than a guess: the whole
5
+ * point of the key is that the SERVER decided, and a client that invented a
6
+ * fifth reading would print one category's rule on another category's card.
7
+ */
8
+ export function badgePresentation(row) {
9
+ const raw = row.presentation;
10
+ if (typeof raw !== "string")
11
+ return undefined;
12
+ switch (raw) {
13
+ case "value":
14
+ case "value_unit":
15
+ case "name_value":
16
+ case "name":
17
+ return raw;
18
+ default:
19
+ return undefined;
20
+ }
21
+ }
22
+ /**
23
+ * Does this projection speak the contract at all?
24
+ *
25
+ * ONE row declaring a `presentation` is enough: the server writes the whole
26
+ * list in one pass, so a mixed list means a row the server deliberately left
27
+ * plain, and that row still renders (as its value alone) rather than
28
+ * dragging the other four back to the old path.
29
+ */
30
+ export function hasCardBadgeContract(rows) {
31
+ return rows.some((row) => badgePresentation(row) !== undefined);
32
+ }
33
+ /** Trimmed copy, or `""` — `null` is the shape the wire uses for absent. */
34
+ function text(value) {
35
+ return typeof value === "string" ? value.trim() : "";
36
+ }
37
+ /**
38
+ * How many decimals the wire actually stated.
39
+ *
40
+ * A card badge must not invent precision and must not lose it: the server
41
+ * sends "2.0" for an engine volume and `2` for a number of rooms, and JS
42
+ * cannot tell `2.0` from `2` once it is a number — so a STRING value keeps
43
+ * the decimals it was written with and a number keeps its own.
44
+ */
45
+ function statedDecimals(raw) {
46
+ const written = typeof raw === "string" ? raw : String(raw);
47
+ const tail = written.split(".")[1];
48
+ return tail === undefined ? 0 : tail.replace(/[^0-9]/g, "").length;
49
+ }
50
+ /**
51
+ * The element's answer, as a person reads it: the server's `label` when there
52
+ * is one, the value with its digits grouped when it is a number, the value
53
+ * verbatim otherwise.
54
+ */
55
+ export function badgeValueText(row, locale) {
56
+ const label = text(row.label);
57
+ if (label.length > 0)
58
+ return label;
59
+ const raw = row.value;
60
+ if (raw === null || raw === undefined || raw === "")
61
+ return "";
62
+ if (typeof raw === "boolean")
63
+ return "";
64
+ const parsed = typeof raw === "number" ? raw : Number(raw);
65
+ if (typeof raw !== "object" && Number.isFinite(parsed)) {
66
+ const digits = statedDecimals(raw);
67
+ try {
68
+ return new Intl.NumberFormat(locale, {
69
+ minimumFractionDigits: digits,
70
+ maximumFractionDigits: digits,
71
+ }).format(parsed);
72
+ }
73
+ catch {
74
+ return parsed.toFixed(digits);
75
+ }
76
+ }
77
+ return typeof raw === "object" ? "" : String(raw);
78
+ }
79
+ /** Is this element's value the `true` a `name`-presented boolean needs? */
80
+ function isTrue(raw) {
81
+ return raw === true || raw === 1 || raw === "true";
82
+ }
83
+ /**
84
+ * One badge element → the text a card prints, or `undefined` for an element
85
+ * that has nothing to say (a false boolean, a blank value).
86
+ *
87
+ * The one place the four presentations are read. Every card surface calls
88
+ * this and none of them re-derives it, for the same reason `CardTarget`
89
+ * exists: three cards each re-deciding what a badge says is three places for
90
+ * "Brick · 3 · 9" to come back.
91
+ */
92
+ export function cardBadgeText(row, locale) {
93
+ const presentation = badgePresentation(row);
94
+ const name = text(row.name);
95
+ const unit = text(row.unit);
96
+ if (presentation === "name") {
97
+ // The name IS the badge, and only while the answer is yes.
98
+ return isTrue(row.value) && name.length > 0 ? name : undefined;
99
+ }
100
+ const value = badgeValueText(row, locale);
101
+ if (value.length === 0)
102
+ return undefined;
103
+ const withUnit = unit.length > 0 ? `${value} ${unit}` : value;
104
+ switch (presentation) {
105
+ case "value_unit":
106
+ return withUnit;
107
+ case "name_value":
108
+ // A SPACE, not a colon. "Floor 3" is a caption; "Floor: 3" is a form
109
+ // field, and a card is neither a form nor a table — the colon is the
110
+ // punctuation the spec list uses because a spec list IS the table.
111
+ // The unit rides along: a named row that dropped it reads "Mileage
112
+ // 20 000", which is the defect this contract exists to close.
113
+ return name.length > 0 ? `${name} ${withUnit}` : withUnit;
114
+ default:
115
+ // `value`, and an element the server left plain in a list that speaks
116
+ // the contract. The value alone, never the unit — the server said so.
117
+ return value;
118
+ }
119
+ }
120
+ /** Every element that has something to say, in the server's order. */
121
+ export function cardBadgeTexts(rows, locale) {
122
+ const out = [];
123
+ for (const row of rows) {
124
+ const printed = cardBadgeText(row, locale);
125
+ if (printed !== undefined && typeof row.slug === "string" && row.slug.length > 0) {
126
+ out.push({ slug: row.slug, text: printed });
127
+ }
128
+ }
129
+ return out;
130
+ }
131
+ //# sourceMappingURL=cardBadges.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cardBadges.js","sourceRoot":"","sources":["../../src/model/cardBadges.ts"],"names":[],"mappings":"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"}
@@ -111,15 +111,23 @@ export declare function draftValuesFromDetail(detail: ListingDetail, featuresDto
111
111
  */
112
112
  export declare function draftPatchFromValues(values: ListingDraftValues, features: readonly FeatureDef[]): ListingDraftPatch;
113
113
  /**
114
- * The body for CREATING a draft: `category_id` and nothing else.
114
+ * The body for CREATING a draft: the category if there is one, `{}` if there
115
+ * is not.
115
116
  *
116
117
  * `perform_create` forces `owner` and `status`, and everything else has a
117
118
  * model default, so a create that also carried the form's current contents
118
119
  * would be a second write of data the very next `save-draft` sends anyway —
119
120
  * and would fail the whole submission on a field the person could still fix.
120
121
  * Create the row, then save into it.
122
+ *
123
+ * `{}` IS a valid create body since stapel-listings 0.21.4 made `category_id`
124
+ * nullable on a draft: a draft may exist before its category is chosen, and
125
+ * `publish` is where the category becomes mandatory (`publish_validation
126
+ * _failed` naming `category_id`). Not being able to create the row first is
127
+ * what left an analysis job addressed by the draft id with no id to start
128
+ * from (D261).
121
129
  */
122
- export declare function createDraftBody(categoryId: string): ListingDraftPatch;
130
+ export declare function createDraftBody(categoryId?: string): ListingDraftPatch;
123
131
  /**
124
132
  * Switching category: keep the answers whose slug the new schema also
125
133
  * declares, drop the rest.
@@ -1 +1 @@
1
- {"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../../src/model/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAExE,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAGzB;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;mEAC+D;IAC/D,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,cAAc,EAAE,eAM5B,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kDAAkD;IAClD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;iFAE6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrD,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;iDACiD;AACjD,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,iBAAsB,GAC9B,kBAAkB,CAepB;AAMD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,YAAY,EACnB,OAAO,GAAE,iBAAsB,GAC9B,kBAAkB,CAwBpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,WAAW,EACxB,OAAO,GAAE,iBAAsB,GAC9B,kBAAkB,CAwBpB;AA4BD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,SAAS,UAAU,EAAE,GAC9B,iBAAiB,CAwBnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,iBAAiB,CAErE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzC,QAAQ,EAAE,SAAS,UAAU,EAAE,GAC9B,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAOnC;AAED;;;eAGe;AACf,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzC,QAAQ,EAAE,SAAS,UAAU,EAAE,GAC9B,SAAS,MAAM,EAAE,CAKnB"}
1
+ {"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../../src/model/draft.ts"],"names":[],"mappings":"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"}
@@ -72,7 +72,11 @@ export function draftValuesFromWire(draft, options = {}) {
72
72
  export function draftValuesFromDetail(detail, featuresDto, options = {}) {
73
73
  const base = emptyDraftValues(options);
74
74
  return {
75
- categoryId: detail.category_id,
75
+ // `text()` for the same reason as the draft seed above: 0.21.4 answers
76
+ // `category_id: null` for a row created before its category was chosen,
77
+ // and a seed that took it verbatim put `null` where a string is declared
78
+ // and crashed the first control that measured its length.
79
+ categoryId: text(detail.category_id),
76
80
  title: text(detail.title),
77
81
  description: text(detail.description),
78
82
  price: text(detail.price),
@@ -125,7 +129,11 @@ function toWireFeatures(features, values) {
125
129
  */
126
130
  export function draftPatchFromValues(values, features) {
127
131
  return {
128
- category_id: values.categoryId,
132
+ // Omitted while unchosen rather than sent as `""`: a draft is allowed to
133
+ // have no category (0.21.4), and `""` is not "no category" on the wire —
134
+ // it is an empty id the serializer refuses. The category is written by
135
+ // whichever save follows the pick.
136
+ ...(values.categoryId.length > 0 ? { category_id: values.categoryId } : {}),
129
137
  title_draft: values.title,
130
138
  description_draft: values.description,
131
139
  price_draft: values.price.length > 0 ? values.price : null,
@@ -149,16 +157,26 @@ export function draftPatchFromValues(values, features) {
149
157
  };
150
158
  }
151
159
  /**
152
- * The body for CREATING a draft: `category_id` and nothing else.
160
+ * The body for CREATING a draft: the category if there is one, `{}` if there
161
+ * is not.
153
162
  *
154
163
  * `perform_create` forces `owner` and `status`, and everything else has a
155
164
  * model default, so a create that also carried the form's current contents
156
165
  * would be a second write of data the very next `save-draft` sends anyway —
157
166
  * and would fail the whole submission on a field the person could still fix.
158
167
  * Create the row, then save into it.
168
+ *
169
+ * `{}` IS a valid create body since stapel-listings 0.21.4 made `category_id`
170
+ * nullable on a draft: a draft may exist before its category is chosen, and
171
+ * `publish` is where the category becomes mandatory (`publish_validation
172
+ * _failed` naming `category_id`). Not being able to create the row first is
173
+ * what left an analysis job addressed by the draft id with no id to start
174
+ * from (D261).
159
175
  */
160
176
  export function createDraftBody(categoryId) {
161
- return { category_id: categoryId };
177
+ return categoryId !== undefined && categoryId.length > 0
178
+ ? { category_id: categoryId }
179
+ : {};
162
180
  }
163
181
  /**
164
182
  * Switching category: keep the answers whose slug the new schema also
@@ -1 +1 @@
1
- {"version":3,"file":"draft.js","sourceRoot":"","sources":["../../src/model/draft.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAM1E,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAgC3D,MAAM,CAAC,MAAM,cAAc,GAAoB;IAC7C,UAAU,EAAE,EAAE;IACd,aAAa,EAAE,EAAE;IACjB,GAAG,EAAE,IAAI;IACT,GAAG,EAAE,IAAI;IACT,OAAO,EAAE,EAAE;CACZ,CAAC;AAqCF;iDACiD;AACjD,MAAM,UAAU,gBAAgB,CAC9B,UAA6B,EAAE;IAE/B,OAAO;QACL,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE;QACpC,KAAK,EAAE,EAAE;QACT,WAAW,EAAE,EAAE;QACf,KAAK,EAAE,EAAE;QACT,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,wBAAwB;QACtD,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE;QAChC,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE,cAAc;QACxB,QAAQ,EAAE,EAAE;QACZ,SAAS,EAAE,KAAK;QAChB,aAAa,EAAE,IAAI;QACnB,aAAa,EAAE,KAAK;KACrB,CAAC;AACJ,CAAC;AAED,SAAS,IAAI,CAAC,KAAgC;IAC5C,OAAO,KAAK,IAAI,EAAE,CAAC;AACrB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAmB,EACnB,UAA6B,EAAE;IAE/B,MAAM,IAAI,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,OAAO;QACL,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;QACnC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;QAC9B,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC;QAC1C,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;QAC9B,QAAQ,EAAE,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;YACjE,CAAC,CAAC,KAAK,CAAC,QAAQ;YAChB,CAAC,CAAC,IAAI,CAAC,QAAQ;QACjB,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC9B,MAAM,EAAE,KAAK,CAAC,YAAY,IAAI,EAAE;QAChC,QAAQ,EAAE;YACR,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC;YACzC,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC;YAC/C,GAAG,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI;YAC5B,GAAG,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI;YAC5B,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC;SACnC;QACD,QAAQ,EAAE,eAAe,CAAC,CAAC,KAAK,CAAC,cAAc,IAAI,EAAE,CAAgB,CAAC;QACtE,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,KAAK;QACnC,aAAa,EAAE,KAAK,CAAC,cAAc,IAAI,IAAI;QAC3C,aAAa,EAAE,KAAK,CAAC,cAAc,IAAI,KAAK;KAC7C,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAAqB,EACrB,WAAwB,EACxB,UAA6B,EAAE;IAE/B,MAAM,IAAI,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,OAAO;QACL,UAAU,EAAE,MAAM,CAAC,WAAW;QAC9B,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;QACzB,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;QACrC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;YACnE,CAAC,CAAC,MAAM,CAAC,QAAQ;YACjB,CAAC,CAAC,IAAI,CAAC,QAAQ;QACjB,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;QAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;QAC3B,QAAQ,EAAE;YACR,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;YACpC,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC;YAC1C,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI;YACvB,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI;YACvB,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;SAC9B;QACD,QAAQ,EAAE,eAAe,CAAC,WAAW,CAAC;QACtC,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,KAAK;QACpC,aAAa,EAAE,MAAM,CAAC,cAAc,IAAI,IAAI;QAC5C,aAAa,EAAE,MAAM,CAAC,cAAc,IAAI,KAAK;KAC9C,CAAC;AACJ,CAAC;AAMD;;;;;;;;;;;;;;GAcG;AACH,SAAS,cAAc,CACrB,QAA+B,EAC/B,MAAyC;IAEzC,OAAO,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAiC,CAAC;AACzE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAA0B,EAC1B,QAA+B;IAE/B,OAAO;QACL,WAAW,EAAE,MAAM,CAAC,UAAU;QAC9B,WAAW,EAAE,MAAM,CAAC,KAAK;QACzB,iBAAiB,EAAE,MAAM,CAAC,WAAW;QACrC,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI;QAC1D,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;QAChC,iBAAiB,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU;QAC7C,oBAAoB,EAAE,MAAM,CAAC,QAAQ,CAAC,aAAa;QACnD,4EAA4E;QAC5E,4EAA4E;QAC5E,yEAAyE;QACzE,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG;QAC9B,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG;QAC9B,cAAc,EAAE,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;QACzD,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,sEAAsE;QACtE,wEAAwE;QACxE,gEAAgE;QAChE,cAAc,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI;QAC9D,cAAc,EAAE,MAAM,CAAC,aAAa;KACrC,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkB;IAChD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CACtC,MAAyC,EACzC,QAA+B;IAE/B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/D,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IACzC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;eAGe;AACf,MAAM,UAAU,mBAAmB,CACjC,MAAyC,EACzC,QAA+B;IAE/B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/D,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;SACvB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;SAClC,IAAI,EAAE,CAAC;AACZ,CAAC"}
1
+ {"version":3,"file":"draft.js","sourceRoot":"","sources":["../../src/model/draft.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAM1E,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAgC3D,MAAM,CAAC,MAAM,cAAc,GAAoB;IAC7C,UAAU,EAAE,EAAE;IACd,aAAa,EAAE,EAAE;IACjB,GAAG,EAAE,IAAI;IACT,GAAG,EAAE,IAAI;IACT,OAAO,EAAE,EAAE;CACZ,CAAC;AAqCF;iDACiD;AACjD,MAAM,UAAU,gBAAgB,CAC9B,UAA6B,EAAE;IAE/B,OAAO;QACL,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE;QACpC,KAAK,EAAE,EAAE;QACT,WAAW,EAAE,EAAE;QACf,KAAK,EAAE,EAAE;QACT,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,wBAAwB;QACtD,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE;QAChC,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE,cAAc;QACxB,QAAQ,EAAE,EAAE;QACZ,SAAS,EAAE,KAAK;QAChB,aAAa,EAAE,IAAI;QACnB,aAAa,EAAE,KAAK;KACrB,CAAC;AACJ,CAAC;AAED,SAAS,IAAI,CAAC,KAAgC;IAC5C,OAAO,KAAK,IAAI,EAAE,CAAC;AACrB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAmB,EACnB,UAA6B,EAAE;IAE/B,MAAM,IAAI,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,OAAO;QACL,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;QACnC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;QAC9B,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC;QAC1C,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;QAC9B,QAAQ,EAAE,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;YACjE,CAAC,CAAC,KAAK,CAAC,QAAQ;YAChB,CAAC,CAAC,IAAI,CAAC,QAAQ;QACjB,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC9B,MAAM,EAAE,KAAK,CAAC,YAAY,IAAI,EAAE;QAChC,QAAQ,EAAE;YACR,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC;YACzC,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC;YAC/C,GAAG,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI;YAC5B,GAAG,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI;YAC5B,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC;SACnC;QACD,QAAQ,EAAE,eAAe,CAAC,CAAC,KAAK,CAAC,cAAc,IAAI,EAAE,CAAgB,CAAC;QACtE,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,KAAK;QACnC,aAAa,EAAE,KAAK,CAAC,cAAc,IAAI,IAAI;QAC3C,aAAa,EAAE,KAAK,CAAC,cAAc,IAAI,KAAK;KAC7C,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAAqB,EACrB,WAAwB,EACxB,UAA6B,EAAE;IAE/B,MAAM,IAAI,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,OAAO;QACL,uEAAuE;QACvE,wEAAwE;QACxE,yEAAyE;QACzE,0DAA0D;QAC1D,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;QACpC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;QACzB,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;QACrC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;YACnE,CAAC,CAAC,MAAM,CAAC,QAAQ;YACjB,CAAC,CAAC,IAAI,CAAC,QAAQ;QACjB,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;QAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;QAC3B,QAAQ,EAAE;YACR,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;YACpC,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC;YAC1C,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI;YACvB,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI;YACvB,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;SAC9B;QACD,QAAQ,EAAE,eAAe,CAAC,WAAW,CAAC;QACtC,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,KAAK;QACpC,aAAa,EAAE,MAAM,CAAC,cAAc,IAAI,IAAI;QAC5C,aAAa,EAAE,MAAM,CAAC,cAAc,IAAI,KAAK;KAC9C,CAAC;AACJ,CAAC;AAMD;;;;;;;;;;;;;;GAcG;AACH,SAAS,cAAc,CACrB,QAA+B,EAC/B,MAAyC;IAEzC,OAAO,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAiC,CAAC;AACzE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAA0B,EAC1B,QAA+B;IAE/B,OAAO;QACL,yEAAyE;QACzE,yEAAyE;QACzE,uEAAuE;QACvE,mCAAmC;QACnC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3E,WAAW,EAAE,MAAM,CAAC,KAAK;QACzB,iBAAiB,EAAE,MAAM,CAAC,WAAW;QACrC,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI;QAC1D,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;QAChC,iBAAiB,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU;QAC7C,oBAAoB,EAAE,MAAM,CAAC,QAAQ,CAAC,aAAa;QACnD,4EAA4E;QAC5E,4EAA4E;QAC5E,yEAAyE;QACzE,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG;QAC9B,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG;QAC9B,cAAc,EAAE,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;QACzD,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,sEAAsE;QACtE,wEAAwE;QACxE,gEAAgE;QAChE,cAAc,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI;QAC9D,cAAc,EAAE,MAAM,CAAC,aAAa;KACrC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,eAAe,CAAC,UAAmB;IACjD,OAAO,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QACtD,CAAC,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE;QAC7B,CAAC,CAAC,EAAE,CAAC;AACT,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CACtC,MAAyC,EACzC,QAA+B;IAE/B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/D,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IACzC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;eAGe;AACf,MAAM,UAAU,mBAAmB,CACjC,MAAyC,EACzC,QAA+B;IAE/B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/D,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;SACvB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;SAClC,IAAI,EAAE,CAAC;AACZ,CAAC"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * A stored feature value as a person READS it — with its unit, and with its
3
+ * digits grouped the way the reader's locale groups them.
4
+ *
5
+ * ── What was on screen ────────────────────────────────────────────────────
6
+ *
7
+ * Measured on the live listing page: "Power 173", "Mileage 20000", "Engine
8
+ * volume 2.0". Three defects in three rows:
9
+ *
10
+ * - **no unit.** 173 what. 20000 what. A spec row whose value is a bare
11
+ * number is a row a buyer has to guess at, and mileage is the single
12
+ * number a used-car buyer decides on;
13
+ * - **no digit grouping.** `20000` is read digit by digit; "20 000" is read
14
+ * at a glance. `formatFeatureValue`'s integer branch is `String(value)`
15
+ * (attributes-react `src/format.ts:196`), deliberately — it mirrors the
16
+ * engine's own `str(value)` — and the engine renders for a machine-read
17
+ * API, not for a spec table;
18
+ * - **the wrong decimal mark.** `2.0` in a Russian storefront. The engine
19
+ * writes `toFixed`, which is invariant by construction.
20
+ *
21
+ * ── Where the unit comes from ─────────────────────────────────────────────
22
+ *
23
+ * There is NO generic `unit` key anywhere in this fleet's feature contract —
24
+ * not on `FeatureDef` (`stapel-attributes/base.py:154-208`), not on
25
+ * `IntConfig`/`FloatConfig` (`attributes-react/src/generated/featureDef.ts:163`),
26
+ * not on the stored DAO. The unit of an `int`/`float` IS its `postfix`, free
27
+ * text on the type's config, with `postfix1000` as the abbreviated unit the
28
+ * engine switches to at a thousand. `convertible_unit` is the one type with
29
+ * real unit semantics (`unitType`/`unit_m`/`unit_i`), and its own formatter
30
+ * already appends the resolved code — so this module leaves it alone.
31
+ *
32
+ * So "render the unit from the definition" means: read `postfix`. It reaches
33
+ * a display surface two ways, and `model/features.ts` now tries both — the
34
+ * stored row's own config first (`dto_to_dao` copies `postfix` at write
35
+ * time), then the CATEGORY's definition of the same slug, which is the path
36
+ * that repairs every listing published before a catalogue gained its units.
37
+ *
38
+ * ── Why the whole formatter is not delegated ──────────────────────────────
39
+ *
40
+ * `formatFeatureValue` is the fleet's one formatter and stays it: every type
41
+ * but `int` and `float` goes straight through it, untouched. The two numeric
42
+ * types are intercepted because the change is not a different ANSWER, it is
43
+ * the same answer typeset — same value, same precision rule, same
44
+ * `postfix1000` switch at a thousand, same translated unit, with the digits
45
+ * run through `Intl.NumberFormat` instead of `String()`.
46
+ *
47
+ * ── Which numbers are grouped, and which are not (D307) ────────────────────
48
+ *
49
+ * Grouping every number is how a card came to read "2 024" for a model year.
50
+ * A year is not a quantity: it is an identifier spelled in digits, and so is
51
+ * a house number, a floor, a room count. Grouping them is not a nicety
52
+ * applied too widely, it is the wrong reading.
53
+ *
54
+ * The rule is the one fact the catalogue already states, plus a magnitude no
55
+ * identifier reaches:
56
+ *
57
+ * - a feature that carries a UNIT (`prefix`, `postfix`, `postfix1000`) is a
58
+ * measurement — "20 000 km", "173 hp", "42 m²" — and is grouped;
59
+ * - a unitless value is grouped only from 10 000 up. A year is four digits
60
+ * for the next eight thousand of them and a count of doors is one, so the
61
+ * threshold sorts years from counts WITHOUT a "does this slug look like a
62
+ * year" heuristic — which is guesswork the moment a deployment names the
63
+ * slug in its own language.
64
+ *
65
+ * Grouping is switched off through `Intl` rather than around it, so a unitless
66
+ * float still gets the reader's DECIMAL MARK ("2,5", not "2.5"): the two are
67
+ * separate defects and only one of them is being repaired here.
68
+ */
69
+ import type { FeatureDef, FeatureValueDto, FormatOptions } from "@stapel/attributes-react";
70
+ /**
71
+ * The unit a numeric feature is measured in, as the reader sees it — or `""`.
72
+ *
73
+ * `postfix1000` when the engine's own thousand switch applies, `postfix`
74
+ * otherwise, so the unit and the number can never disagree about scale.
75
+ */
76
+ export declare function featureUnit(feature: FeatureDef, dto: FeatureValueDto | undefined, options?: FormatOptions): string;
77
+ /**
78
+ * A stored value as a spec row prints it: `formatFeatureValue` for every type
79
+ * but the two numeric ones, which are typeset here instead.
80
+ *
81
+ * Signature-compatible with `formatFeatureValue` on purpose — a surface
82
+ * swaps one for the other and nothing else changes.
83
+ */
84
+ export declare function formatSpecValue(feature: FeatureDef, dto: FeatureValueDto | undefined, options?: FormatOptions): string | undefined;
85
+ //# sourceMappingURL=featureText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"featureText.d.ts","sourceRoot":"","sources":["../../src/model/featureText.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AA+B3F;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,UAAU,EACnB,GAAG,EAAE,eAAe,GAAG,SAAS,EAChC,OAAO,CAAC,EAAE,aAAa,GACtB,MAAM,CAOR;AAoHD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,UAAU,EACnB,GAAG,EAAE,eAAe,GAAG,SAAS,EAChC,OAAO,CAAC,EAAE,aAAa,GACtB,MAAM,GAAG,SAAS,CAOpB"}
@@ -0,0 +1,153 @@
1
+ import { featureConfig, featureType, formatFeatureValue } from "@stapel/attributes-react";
2
+ /** The engine's own defaults (`types/int/type.py`, `types/float/type.py`). */
3
+ const DEFAULT_DIGITS = {
4
+ int: 1,
5
+ float: 2,
6
+ };
7
+ function str(value) {
8
+ return typeof value === "string" ? value : "";
9
+ }
10
+ /**
11
+ * A config string the engine declares to be a translation key, resolved.
12
+ *
13
+ * `prefix`/`postfix`/`postfix1000` are collected by upstream's
14
+ * `get_translation_keys` unconditionally, so they go through the host's
15
+ * catalogue exactly as `formatFeatureValue` sends them — a Russian storefront
16
+ * was reading "L" and "each" off its own spec table before that landed, and
17
+ * this path must not reintroduce it.
18
+ */
19
+ function translated(options, raw) {
20
+ const text = str(raw);
21
+ if (text.length === 0)
22
+ return "";
23
+ const t = options?.t;
24
+ if (t === undefined)
25
+ return text;
26
+ const resolved = t(text);
27
+ return resolved.length > 0 ? resolved : text;
28
+ }
29
+ /**
30
+ * The unit a numeric feature is measured in, as the reader sees it — or `""`.
31
+ *
32
+ * `postfix1000` when the engine's own thousand switch applies, `postfix`
33
+ * otherwise, so the unit and the number can never disagree about scale.
34
+ */
35
+ export function featureUnit(feature, dto, options) {
36
+ const type = featureType(feature);
37
+ if (type !== "int" && type !== "float")
38
+ return "";
39
+ const config = featureConfig(feature);
40
+ const parsed = Number(dto?.value);
41
+ const big = str(config["postfix1000"]).length > 0 && Number.isFinite(parsed) && Math.abs(parsed) >= 1000;
42
+ return translated(options, config[big ? "postfix1000" : "postfix"]);
43
+ }
44
+ /**
45
+ * `Intl.NumberFormat` for one value, or `undefined` when the runtime has no
46
+ * `Intl` for this locale to offer.
47
+ *
48
+ * A bad BCP-47 tag throws `RangeError`, and a spec table that renders nothing
49
+ * because a host passed `"en_US"` would be a worse outcome than an ungrouped
50
+ * number — so the caller falls back rather than the page failing.
51
+ */
52
+ function localized(value, digits, locale, useGrouping) {
53
+ try {
54
+ return new Intl.NumberFormat(locale, {
55
+ minimumFractionDigits: digits,
56
+ maximumFractionDigits: digits,
57
+ useGrouping,
58
+ }).format(value);
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ /** Digits below which a unitless number is an identifier rather than a
65
+ * quantity — see the module header. */
66
+ const GROUPING_FLOOR = 10_000;
67
+ /**
68
+ * Does this feature measure something?
69
+ *
70
+ * The RAW config, not the translated unit: whether a value is a measurement
71
+ * is a fact about the catalogue, and it must not change because a host's
72
+ * message catalogue happens to resolve a unit key to an empty string.
73
+ */
74
+ function hasUnit(config) {
75
+ return (str(config["prefix"]).length > 0 ||
76
+ str(config["postfix"]).length > 0 ||
77
+ str(config["postfix1000"]).length > 0);
78
+ }
79
+ /**
80
+ * The one numeric row, typeset — see the module header for what each piece
81
+ * is repairing.
82
+ *
83
+ * Returns `undefined` for a value that is not a number, so the caller falls
84
+ * back to the shared formatter and an unreadable row keeps saying so.
85
+ */
86
+ function formatNumeric(feature, dto, kind, options) {
87
+ // `Number(null)` is 0 and `Number("")` is 0, so a blank row would render as
88
+ // a confident zero. The shared formatter's own `isBlank` gate is upstream of
89
+ // this branch and must be restated here rather than assumed.
90
+ const raw = dto.value;
91
+ if (raw === null || raw === undefined || raw === "")
92
+ return undefined;
93
+ const parsed = typeof raw === "number" ? raw : Number(raw);
94
+ if (!Number.isFinite(parsed))
95
+ return undefined;
96
+ const config = featureConfig(feature);
97
+ const precision = config["precision"];
98
+ const digits = typeof precision === "number" && precision >= 0 ? precision : DEFAULT_DIGITS[kind];
99
+ const prefix = translated(options, config["prefix"]);
100
+ const postfix1000 = str(config["postfix1000"]);
101
+ // The engine switches unit AND scale at exactly a thousand
102
+ // (`format_value`: `value / 1000`, trailing zeros stripped, `postfix1000`
103
+ // as the unit). Grouping a scaled number is harmless and keeps one code
104
+ // path; the trailing-zero strip is the engine's, done on the plain digits
105
+ // before the locale ever sees them, because "1.50" and "1,5" are the same
106
+ // decision made in two places.
107
+ if (postfix1000.length > 0 && Math.abs(parsed) >= 1000) {
108
+ const scaled = Number((parsed / 1000).toFixed(digits));
109
+ const body = localized(scaled, decimalsOf(scaled, digits), options?.locale, true) ??
110
+ String(scaled);
111
+ return join(prefix, body, translated(options, postfix1000));
112
+ }
113
+ // `precision` drives ONLY the scaled branch of an int — the engine's plain
114
+ // branch is `str(value)`, and an integer never grows a decimal tail
115
+ // whatever the config says (D26: a live category shipped `precision: 1` on
116
+ // a year field and the card read "2024.0").
117
+ const value = kind === "int" ? Math.trunc(parsed) : parsed;
118
+ const fraction = kind === "int" ? 0 : digits;
119
+ // A measurement is grouped; a unitless number is an identifier until it is
120
+ // too big to be one (D307 — the card read "2 024" for a model year).
121
+ const group = hasUnit(config) || Math.abs(value) >= GROUPING_FLOOR;
122
+ const body = localized(value, fraction, options?.locale, group) ?? value.toFixed(fraction);
123
+ return join(prefix, body, translated(options, config["postfix"]));
124
+ }
125
+ /** How many decimals a scaled value actually has, capped at the configured
126
+ * precision — the engine's `.rstrip('0').rstrip('.')`, expressed as a count
127
+ * so `Intl` can do the rendering. */
128
+ function decimalsOf(value, cap) {
129
+ const tail = value.toFixed(cap).split(".")[1] ?? "";
130
+ return tail.replace(/0+$/, "").length;
131
+ }
132
+ /** `prefix` + body + unit, with the single space the engine puts between a
133
+ * number and its unit and no space at all where there is no unit. */
134
+ function join(prefix, body, unit) {
135
+ return unit.length > 0 ? `${prefix}${body} ${unit}` : `${prefix}${body}`;
136
+ }
137
+ /**
138
+ * A stored value as a spec row prints it: `formatFeatureValue` for every type
139
+ * but the two numeric ones, which are typeset here instead.
140
+ *
141
+ * Signature-compatible with `formatFeatureValue` on purpose — a surface
142
+ * swaps one for the other and nothing else changes.
143
+ */
144
+ export function formatSpecValue(feature, dto, options) {
145
+ const type = featureType(feature);
146
+ if (dto !== undefined && (type === "int" || type === "float")) {
147
+ const typeset = formatNumeric(feature, dto, type, options);
148
+ if (typeset !== undefined)
149
+ return typeset;
150
+ }
151
+ return formatFeatureValue(feature, dto, options);
152
+ }
153
+ //# sourceMappingURL=featureText.js.map