@stapel/listings-react 0.14.0 → 0.15.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 +67 -1
- package/dist/api/generated/schema.d.ts +158 -7
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/api/types.d.ts +29 -0
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js.map +1 -1
- package/dist/default/ListingDetailPane.d.ts.map +1 -1
- package/dist/default/ListingDetailPane.js +18 -3
- package/dist/default/ListingDetailPane.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model/features.d.ts +32 -0
- package/dist/model/features.d.ts.map +1 -1
- package/dist/model/features.js +121 -2
- package/dist/model/features.js.map +1 -1
- package/llms.txt +2 -2
- package/manifest.json +3 -2
- package/nav-manifest.json +1 -1
- package/package.json +5 -5
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +158 -7
- package/src/api/types.ts +29 -0
- package/src/default/ListingDetailPane.tsx +19 -4
- package/src/index.ts +1 -0
- package/src/model/features.ts +125 -2
|
@@ -408,6 +408,10 @@ export interface components {
|
|
|
408
408
|
title?: boolean | null;
|
|
409
409
|
badge?: boolean | null;
|
|
410
410
|
translate?: string | null;
|
|
411
|
+
visibility?: string | null;
|
|
412
|
+
verification?: {
|
|
413
|
+
[key: string]: unknown;
|
|
414
|
+
} | null;
|
|
411
415
|
/**
|
|
412
416
|
* @description discriminator enum property added by openapi-typescript
|
|
413
417
|
* @enum {string}
|
|
@@ -433,6 +437,10 @@ export interface components {
|
|
|
433
437
|
title?: boolean | null;
|
|
434
438
|
badge?: boolean | null;
|
|
435
439
|
translate?: string | null;
|
|
440
|
+
visibility?: string | null;
|
|
441
|
+
verification?: {
|
|
442
|
+
[key: string]: unknown;
|
|
443
|
+
} | null;
|
|
436
444
|
/**
|
|
437
445
|
* @description discriminator enum property added by openapi-typescript
|
|
438
446
|
* @enum {string}
|
|
@@ -464,6 +472,10 @@ export interface components {
|
|
|
464
472
|
title?: boolean | null;
|
|
465
473
|
badge?: boolean | null;
|
|
466
474
|
translate?: string | null;
|
|
475
|
+
visibility?: string | null;
|
|
476
|
+
verification?: {
|
|
477
|
+
[key: string]: unknown;
|
|
478
|
+
} | null;
|
|
467
479
|
/**
|
|
468
480
|
* @description discriminator enum property added by openapi-typescript
|
|
469
481
|
* @enum {string}
|
|
@@ -539,6 +551,10 @@ export interface components {
|
|
|
539
551
|
title?: boolean | null;
|
|
540
552
|
badge?: boolean | null;
|
|
541
553
|
translate?: string | null;
|
|
554
|
+
visibility?: string | null;
|
|
555
|
+
verification?: {
|
|
556
|
+
[key: string]: unknown;
|
|
557
|
+
} | null;
|
|
542
558
|
/**
|
|
543
559
|
* @description discriminator enum property added by openapi-typescript
|
|
544
560
|
* @enum {string}
|
|
@@ -568,6 +584,10 @@ export interface components {
|
|
|
568
584
|
title?: boolean | null;
|
|
569
585
|
badge?: boolean | null;
|
|
570
586
|
translate?: string | null;
|
|
587
|
+
visibility?: string | null;
|
|
588
|
+
verification?: {
|
|
589
|
+
[key: string]: unknown;
|
|
590
|
+
} | null;
|
|
571
591
|
/**
|
|
572
592
|
* @description discriminator enum property added by openapi-typescript
|
|
573
593
|
* @enum {string}
|
|
@@ -595,6 +615,10 @@ export interface components {
|
|
|
595
615
|
title?: boolean | null;
|
|
596
616
|
badge?: boolean | null;
|
|
597
617
|
translate?: string | null;
|
|
618
|
+
visibility?: string | null;
|
|
619
|
+
verification?: {
|
|
620
|
+
[key: string]: unknown;
|
|
621
|
+
} | null;
|
|
598
622
|
/**
|
|
599
623
|
* @description discriminator enum property added by openapi-typescript
|
|
600
624
|
* @enum {string}
|
|
@@ -618,6 +642,10 @@ export interface components {
|
|
|
618
642
|
title?: boolean | null;
|
|
619
643
|
badge?: boolean | null;
|
|
620
644
|
translate?: string | null;
|
|
645
|
+
visibility?: string | null;
|
|
646
|
+
verification?: {
|
|
647
|
+
[key: string]: unknown;
|
|
648
|
+
} | null;
|
|
621
649
|
/**
|
|
622
650
|
* @description discriminator enum property added by openapi-typescript
|
|
623
651
|
* @enum {string}
|
|
@@ -649,6 +677,10 @@ export interface components {
|
|
|
649
677
|
title?: boolean | null;
|
|
650
678
|
badge?: boolean | null;
|
|
651
679
|
translate?: string | null;
|
|
680
|
+
visibility?: string | null;
|
|
681
|
+
verification?: {
|
|
682
|
+
[key: string]: unknown;
|
|
683
|
+
} | null;
|
|
652
684
|
/**
|
|
653
685
|
* @description discriminator enum property added by openapi-typescript
|
|
654
686
|
* @enum {string}
|
|
@@ -672,6 +704,10 @@ export interface components {
|
|
|
672
704
|
title?: boolean | null;
|
|
673
705
|
badge?: boolean | null;
|
|
674
706
|
translate?: string | null;
|
|
707
|
+
visibility?: string | null;
|
|
708
|
+
verification?: {
|
|
709
|
+
[key: string]: unknown;
|
|
710
|
+
} | null;
|
|
675
711
|
/**
|
|
676
712
|
* @description discriminator enum property added by openapi-typescript
|
|
677
713
|
* @enum {string}
|
|
@@ -707,8 +743,36 @@ export interface components {
|
|
|
707
743
|
price_base?: string | null;
|
|
708
744
|
currency?: string;
|
|
709
745
|
readonly images: string[] | null;
|
|
710
|
-
readonly features_title: components["schemas"]["FeatureDao"]
|
|
711
|
-
|
|
746
|
+
readonly features_title: (components["schemas"]["FeatureDao"] | {
|
|
747
|
+
slug?: string;
|
|
748
|
+
type?: string;
|
|
749
|
+
name?: string | null;
|
|
750
|
+
order?: number | null;
|
|
751
|
+
translate?: string | null;
|
|
752
|
+
/** @enum {unknown} */
|
|
753
|
+
visibility?: "owner" | "staff";
|
|
754
|
+
verification?: {
|
|
755
|
+
[key: string]: unknown;
|
|
756
|
+
};
|
|
757
|
+
/** @constant */
|
|
758
|
+
redacted: true;
|
|
759
|
+
present: boolean;
|
|
760
|
+
})[];
|
|
761
|
+
readonly features_badges: (components["schemas"]["FeatureDao"] | {
|
|
762
|
+
slug?: string;
|
|
763
|
+
type?: string;
|
|
764
|
+
name?: string | null;
|
|
765
|
+
order?: number | null;
|
|
766
|
+
translate?: string | null;
|
|
767
|
+
/** @enum {unknown} */
|
|
768
|
+
visibility?: "owner" | "staff";
|
|
769
|
+
verification?: {
|
|
770
|
+
[key: string]: unknown;
|
|
771
|
+
};
|
|
772
|
+
/** @constant */
|
|
773
|
+
redacted: true;
|
|
774
|
+
present: boolean;
|
|
775
|
+
})[];
|
|
712
776
|
location_label?: string;
|
|
713
777
|
geohash?: string;
|
|
714
778
|
/** Format: decimal */
|
|
@@ -743,9 +807,51 @@ export interface components {
|
|
|
743
807
|
lat?: string | null;
|
|
744
808
|
/** Format: decimal */
|
|
745
809
|
lon?: string | null;
|
|
746
|
-
readonly features: components["schemas"]["FeatureDao"]
|
|
747
|
-
|
|
748
|
-
|
|
810
|
+
readonly features: (components["schemas"]["FeatureDao"] | {
|
|
811
|
+
slug?: string;
|
|
812
|
+
type?: string;
|
|
813
|
+
name?: string | null;
|
|
814
|
+
order?: number | null;
|
|
815
|
+
translate?: string | null;
|
|
816
|
+
/** @enum {unknown} */
|
|
817
|
+
visibility?: "owner" | "staff";
|
|
818
|
+
verification?: {
|
|
819
|
+
[key: string]: unknown;
|
|
820
|
+
};
|
|
821
|
+
/** @constant */
|
|
822
|
+
redacted: true;
|
|
823
|
+
present: boolean;
|
|
824
|
+
})[];
|
|
825
|
+
readonly features_title: (components["schemas"]["FeatureDao"] | {
|
|
826
|
+
slug?: string;
|
|
827
|
+
type?: string;
|
|
828
|
+
name?: string | null;
|
|
829
|
+
order?: number | null;
|
|
830
|
+
translate?: string | null;
|
|
831
|
+
/** @enum {unknown} */
|
|
832
|
+
visibility?: "owner" | "staff";
|
|
833
|
+
verification?: {
|
|
834
|
+
[key: string]: unknown;
|
|
835
|
+
};
|
|
836
|
+
/** @constant */
|
|
837
|
+
redacted: true;
|
|
838
|
+
present: boolean;
|
|
839
|
+
})[];
|
|
840
|
+
readonly features_badges: (components["schemas"]["FeatureDao"] | {
|
|
841
|
+
slug?: string;
|
|
842
|
+
type?: string;
|
|
843
|
+
name?: string | null;
|
|
844
|
+
order?: number | null;
|
|
845
|
+
translate?: string | null;
|
|
846
|
+
/** @enum {unknown} */
|
|
847
|
+
visibility?: "owner" | "staff";
|
|
848
|
+
verification?: {
|
|
849
|
+
[key: string]: unknown;
|
|
850
|
+
};
|
|
851
|
+
/** @constant */
|
|
852
|
+
redacted: true;
|
|
853
|
+
present: boolean;
|
|
854
|
+
})[];
|
|
749
855
|
readonly features_search: unknown;
|
|
750
856
|
status?: components["schemas"]["StatusD41Enum"];
|
|
751
857
|
moderation_status?: components["schemas"]["ModerationStatusEnum"];
|
|
@@ -853,8 +959,36 @@ export interface components {
|
|
|
853
959
|
price_base?: string | null;
|
|
854
960
|
currency?: string;
|
|
855
961
|
readonly images: string[] | null;
|
|
856
|
-
readonly features_title: components["schemas"]["FeatureDao"]
|
|
857
|
-
|
|
962
|
+
readonly features_title: (components["schemas"]["FeatureDao"] | {
|
|
963
|
+
slug?: string;
|
|
964
|
+
type?: string;
|
|
965
|
+
name?: string | null;
|
|
966
|
+
order?: number | null;
|
|
967
|
+
translate?: string | null;
|
|
968
|
+
/** @enum {unknown} */
|
|
969
|
+
visibility?: "owner" | "staff";
|
|
970
|
+
verification?: {
|
|
971
|
+
[key: string]: unknown;
|
|
972
|
+
};
|
|
973
|
+
/** @constant */
|
|
974
|
+
redacted: true;
|
|
975
|
+
present: boolean;
|
|
976
|
+
})[];
|
|
977
|
+
readonly features_badges: (components["schemas"]["FeatureDao"] | {
|
|
978
|
+
slug?: string;
|
|
979
|
+
type?: string;
|
|
980
|
+
name?: string | null;
|
|
981
|
+
order?: number | null;
|
|
982
|
+
translate?: string | null;
|
|
983
|
+
/** @enum {unknown} */
|
|
984
|
+
visibility?: "owner" | "staff";
|
|
985
|
+
verification?: {
|
|
986
|
+
[key: string]: unknown;
|
|
987
|
+
};
|
|
988
|
+
/** @constant */
|
|
989
|
+
redacted: true;
|
|
990
|
+
present: boolean;
|
|
991
|
+
})[];
|
|
858
992
|
location_label?: string;
|
|
859
993
|
geohash?: string;
|
|
860
994
|
/** Format: decimal */
|
|
@@ -955,6 +1089,10 @@ export interface components {
|
|
|
955
1089
|
title?: boolean | null;
|
|
956
1090
|
badge?: boolean | null;
|
|
957
1091
|
translate?: string | null;
|
|
1092
|
+
visibility?: string | null;
|
|
1093
|
+
verification?: {
|
|
1094
|
+
[key: string]: unknown;
|
|
1095
|
+
} | null;
|
|
958
1096
|
/**
|
|
959
1097
|
* @description discriminator enum property added by openapi-typescript
|
|
960
1098
|
* @enum {string}
|
|
@@ -981,6 +1119,10 @@ export interface components {
|
|
|
981
1119
|
title?: boolean | null;
|
|
982
1120
|
badge?: boolean | null;
|
|
983
1121
|
translate?: string | null;
|
|
1122
|
+
visibility?: string | null;
|
|
1123
|
+
verification?: {
|
|
1124
|
+
[key: string]: unknown;
|
|
1125
|
+
} | null;
|
|
984
1126
|
/**
|
|
985
1127
|
* @description discriminator enum property added by openapi-typescript
|
|
986
1128
|
* @enum {string}
|
|
@@ -1007,12 +1149,17 @@ export interface components {
|
|
|
1007
1149
|
title?: boolean | null;
|
|
1008
1150
|
badge?: boolean | null;
|
|
1009
1151
|
translate?: string | null;
|
|
1152
|
+
visibility?: string | null;
|
|
1153
|
+
verification?: {
|
|
1154
|
+
[key: string]: unknown;
|
|
1155
|
+
} | null;
|
|
1010
1156
|
/**
|
|
1011
1157
|
* @description discriminator enum property added by openapi-typescript
|
|
1012
1158
|
* @enum {string}
|
|
1013
1159
|
*/
|
|
1014
1160
|
type: "select";
|
|
1015
1161
|
value?: string[];
|
|
1162
|
+
labels?: string[];
|
|
1016
1163
|
uiStyle?: (components["schemas"]["UiStyleEnum"] | components["schemas"]["NullEnum"]) | null;
|
|
1017
1164
|
maxSelected?: number | null;
|
|
1018
1165
|
};
|
|
@@ -1045,6 +1192,10 @@ export interface components {
|
|
|
1045
1192
|
title?: boolean | null;
|
|
1046
1193
|
badge?: boolean | null;
|
|
1047
1194
|
translate?: string | null;
|
|
1195
|
+
visibility?: string | null;
|
|
1196
|
+
verification?: {
|
|
1197
|
+
[key: string]: unknown;
|
|
1198
|
+
} | null;
|
|
1048
1199
|
/**
|
|
1049
1200
|
* @description discriminator enum property added by openapi-typescript
|
|
1050
1201
|
* @enum {string}
|
package/src/api/types.ts
CHANGED
|
@@ -194,6 +194,35 @@ export interface ListingFeatureDao {
|
|
|
194
194
|
/** Rendered as a badge on the card. */
|
|
195
195
|
readonly badge?: boolean | null;
|
|
196
196
|
readonly translate?: string | null;
|
|
197
|
+
/**
|
|
198
|
+
* Which audience may READ the stored value (stapel-attributes 0.8.1).
|
|
199
|
+
* Stamped into the DAO at write time and absent on a public row, so an
|
|
200
|
+
* existing row is byte-identical. `owner`/`staff` mark an identifier of one
|
|
201
|
+
* physical unit — a VIN, an IMEI, a serial — which stapel-listings 0.12.0
|
|
202
|
+
* withholds from a reader who is not entitled to it.
|
|
203
|
+
*/
|
|
204
|
+
readonly visibility?: string | null;
|
|
205
|
+
/**
|
|
206
|
+
* `true` on a **stub**: the row survived redaction as its own identity with
|
|
207
|
+
* no value at all (`RedactedFeatureDao`). It is kept in place and in order
|
|
208
|
+
* so the public spec table has the same rows as the seller's own — a buyer
|
|
209
|
+
* can see the field exists rather than not knowing it was ever asked.
|
|
210
|
+
*/
|
|
211
|
+
readonly redacted?: boolean;
|
|
212
|
+
/**
|
|
213
|
+
* On a stub: whether the seller actually filled the withheld value in.
|
|
214
|
+
* A FACT this system observed, and the only one it has — see
|
|
215
|
+
* {@link ListingFeatureDao.verification}.
|
|
216
|
+
*/
|
|
217
|
+
readonly present?: boolean;
|
|
218
|
+
/**
|
|
219
|
+
* On a stub: the result of an outside check of the value, passed through
|
|
220
|
+
* redaction verbatim. **Absent for everything in the fleet today** — no
|
|
221
|
+
* product runs a VIN or an IMEI check — which is exactly why `present`
|
|
222
|
+
* alone may never be rendered as "verified". stapel-attributes deliberately
|
|
223
|
+
* leaves the `status` vocabulary to whichever product runs the check.
|
|
224
|
+
*/
|
|
225
|
+
readonly verification?: Readonly<Record<string, unknown>>;
|
|
197
226
|
/** The type's own config keys (`prefix`, `precision`, `unitType`, …). */
|
|
198
227
|
readonly [key: string]: unknown;
|
|
199
228
|
}
|
|
@@ -50,10 +50,10 @@ import {
|
|
|
50
50
|
} from "@stapel/core";
|
|
51
51
|
import { spacing } from "@stapel/tokens";
|
|
52
52
|
import { FeatureValueList } from "@stapel/attributes-react/default";
|
|
53
|
-
import { formatFeatureValue } from "@stapel/attributes-react";
|
|
53
|
+
import { formatFeatureValue, isRedactedValue } from "@stapel/attributes-react";
|
|
54
54
|
import { useListingDetail } from "../headless/ListingDetail.js";
|
|
55
55
|
import { useListingActions } from "../headless/ListingActions.js";
|
|
56
|
-
import { asFeatureDaoList,
|
|
56
|
+
import { asFeatureDaoList, featureValuesForDisplay } from "../model/features.js";
|
|
57
57
|
import { LISTINGS_I18N_KEYS } from "../i18n/keys.js";
|
|
58
58
|
import { HeartIcon } from "./icons.js";
|
|
59
59
|
import { ListingPhoto } from "./ListingPhoto.js";
|
|
@@ -218,6 +218,14 @@ export function ListingDetailPane(props: ListingDetailPaneProps): ReactElement {
|
|
|
218
218
|
{bag.titleFeatures.length > 0 ? (
|
|
219
219
|
<Typography.Text type="secondary" data-testid="listings-detail-title-features">
|
|
220
220
|
{bag.titleFeatures
|
|
221
|
+
// A hidden value is never part of a title: the server
|
|
222
|
+
// keeps one out of `features_title` entirely, and
|
|
223
|
+
// `formatFeatureValue` refuses a stub besides (it carries
|
|
224
|
+
// no value, so there is nothing to format). The filter is
|
|
225
|
+
// the third belt, and it is here rather than at the
|
|
226
|
+
// formatter's edge because THIS is the line where a
|
|
227
|
+
// leaked identifier would be read out loud.
|
|
228
|
+
.filter((view) => !isRedactedValue(view.value))
|
|
221
229
|
.map((view) =>
|
|
222
230
|
formatFeatureValue(view.feature, view.value, { t, locale })
|
|
223
231
|
)
|
|
@@ -332,8 +340,15 @@ export function ListingDetailPane(props: ListingDetailPaneProps): ReactElement {
|
|
|
332
340
|
) : (
|
|
333
341
|
<FeatureValueList
|
|
334
342
|
features={bag.features.map((view) => view.feature)}
|
|
335
|
-
|
|
336
|
-
|
|
343
|
+
// The DISPLAY envelope, not the edit one: a redacted row
|
|
344
|
+
// keeps its place in the table and says the seller supplied
|
|
345
|
+
// the value. `featuresDtoFromDaoList` deliberately drops a
|
|
346
|
+
// stub, because it is what seeds a composer.
|
|
347
|
+
values={featureValuesForDisplay(
|
|
348
|
+
asFeatureDaoList(listing.features),
|
|
349
|
+
props.categoryFeatures !== undefined
|
|
350
|
+
? { categoryFeatures: props.categoryFeatures }
|
|
351
|
+
: {}
|
|
337
352
|
)}
|
|
338
353
|
/>
|
|
339
354
|
)}
|
package/src/index.ts
CHANGED
package/src/model/features.ts
CHANGED
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
* and an unknown key resolves to itself either way.
|
|
98
98
|
*/
|
|
99
99
|
import type { FeatureDef, FeatureValueDto } from "@stapel/attributes-react";
|
|
100
|
-
import { featureType } from "@stapel/attributes-react";
|
|
100
|
+
import { featureType, isRedactedValue } from "@stapel/attributes-react";
|
|
101
101
|
import type { ListingFeatureDao, ListingFeatureView } from "../api/types.js";
|
|
102
102
|
|
|
103
103
|
/**
|
|
@@ -111,7 +111,30 @@ import type { ListingFeatureDao, ListingFeatureView } from "../api/types.js";
|
|
|
111
111
|
* two types print. For `select` the key is simply inert — its formatter reads
|
|
112
112
|
* `options` and nothing else — so carrying it costs a reference.
|
|
113
113
|
*/
|
|
114
|
-
const ENVELOPE = new Set([
|
|
114
|
+
const ENVELOPE = new Set([
|
|
115
|
+
"slug",
|
|
116
|
+
"value",
|
|
117
|
+
"name",
|
|
118
|
+
"order",
|
|
119
|
+
"title",
|
|
120
|
+
"badge",
|
|
121
|
+
// ── The visibility axis (stapel-attributes 0.8.1 / stapel-listings 0.12.0) ──
|
|
122
|
+
//
|
|
123
|
+
// These four are envelope for exactly the reason the six above are: they
|
|
124
|
+
// describe the ROW, not the type, and no formatter has ever read one. But
|
|
125
|
+
// they matter more than the others, because the alternative is not noise —
|
|
126
|
+
// it is a marker reaching the display BY ACCIDENT, through the index
|
|
127
|
+
// signature, with nothing stating that it did.
|
|
128
|
+
//
|
|
129
|
+
// `visibility` is a genuine `FeatureDef` field and is lifted onto the
|
|
130
|
+
// definition below; `redacted`/`present`/`verification` describe THIS
|
|
131
|
+
// reader's access to THIS stored value, so they ride the value envelope,
|
|
132
|
+
// which is where `@stapel/attributes-react`'s predicates read them.
|
|
133
|
+
"visibility",
|
|
134
|
+
"redacted",
|
|
135
|
+
"present",
|
|
136
|
+
"verification",
|
|
137
|
+
]);
|
|
115
138
|
|
|
116
139
|
/** The type whose stored `value` is a flat list of option keys rather than the
|
|
117
140
|
* things themselves, and whose copy therefore has to be repaired pairwise.
|
|
@@ -139,6 +162,32 @@ function isTranslateMode(value: unknown): value is "all" | "title" | "none" {
|
|
|
139
162
|
return value === "all" || value === "title" || value === "none";
|
|
140
163
|
}
|
|
141
164
|
|
|
165
|
+
/**
|
|
166
|
+
* The stamped `FeatureDef.visibility`, or `undefined` when the row is public.
|
|
167
|
+
*
|
|
168
|
+
* `public` is stamped as nothing at all upstream (`dataclass_to_dict_no_none`
|
|
169
|
+
* drops it), which is why an existing public row is byte-identical after the
|
|
170
|
+
* axis landed — so an absent key means public and is dropped here too.
|
|
171
|
+
*
|
|
172
|
+
* A string that is NOT one of the three becomes `"staff"`, the most
|
|
173
|
+
* restrictive one. Python raises `UnknownVisibility` on the same input; this
|
|
174
|
+
* side has nobody to raise at, so it takes the only direction that cannot
|
|
175
|
+
* leak — a typo must not publish a VIN. The engine normalizes at write time,
|
|
176
|
+
* so this is the belt on a stored row nobody expects to see.
|
|
177
|
+
*/
|
|
178
|
+
function storedVisibility(value: unknown): "public" | "owner" | "staff" | undefined {
|
|
179
|
+
if (typeof value !== "string" || value.length === 0 || value === "public") return undefined;
|
|
180
|
+
return value === "owner" || value === "staff" ? value : "staff";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A `verification` result, when the row carries one. An object and nothing
|
|
184
|
+
* else: the shape belongs to whichever product ran the check, and this
|
|
185
|
+
* projection passes it through rather than describing it. */
|
|
186
|
+
function storedVerification(value: unknown): Readonly<Record<string, unknown>> | undefined {
|
|
187
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
188
|
+
return value as Readonly<Record<string, unknown>>;
|
|
189
|
+
}
|
|
190
|
+
|
|
142
191
|
/**
|
|
143
192
|
* The category's feature defs, for a display surface that has them.
|
|
144
193
|
*
|
|
@@ -317,6 +366,7 @@ function featureView(
|
|
|
317
366
|
selectOptions(dao, config, categoryDef) ?? adoptedTree(dao, config, categoryDef);
|
|
318
367
|
if (options !== undefined) config["options"] = options;
|
|
319
368
|
|
|
369
|
+
const visibility = storedVisibility(dao.visibility);
|
|
320
370
|
const feature: FeatureDef = {
|
|
321
371
|
slug: dao.slug,
|
|
322
372
|
config,
|
|
@@ -326,8 +376,38 @@ function featureView(
|
|
|
326
376
|
// the three is dropped rather than smuggled through: `FeatureDef`'s own
|
|
327
377
|
// default is `all`, which is what the engine falls back to anyway.
|
|
328
378
|
...(isTranslateMode(dao.translate) ? { translate: dao.translate } : {}),
|
|
379
|
+
// A genuine `FeatureDef` field, so it goes on the DEFINITION rather than
|
|
380
|
+
// into `config`: it is what `isPublicFeature` reads to keep a hidden
|
|
381
|
+
// value off a badge strip.
|
|
382
|
+
...(visibility !== undefined ? { visibility } : {}),
|
|
329
383
|
};
|
|
330
384
|
|
|
385
|
+
// A REDACTED STUB — a row this reader may not see — carries no `value` key
|
|
386
|
+
// at all, and this envelope must not invent one. What it carries instead is
|
|
387
|
+
// what the system honestly observed: `present` (did the seller answer) and,
|
|
388
|
+
// reserved for the day some product actually runs a VIN or an IMEI check,
|
|
389
|
+
// `verification`. `@stapel/attributes-react`'s `<FeatureValueList/>` reads
|
|
390
|
+
// exactly these and prints "provided by the seller" — never "verified".
|
|
391
|
+
//
|
|
392
|
+
// The row is built even when `type` is missing, which the normal path below
|
|
393
|
+
// refuses: an unkeyable value is a row a buyer cannot be told about, but a
|
|
394
|
+
// stub's whole content is "this field exists and was answered", and that is
|
|
395
|
+
// still true without a type slug.
|
|
396
|
+
if (dao.redacted === true) {
|
|
397
|
+
const verification = storedVerification(dao.verification);
|
|
398
|
+
const stub: FeatureValueDto = {
|
|
399
|
+
type: typeof dao.type === "string" ? dao.type : "",
|
|
400
|
+
// No stored answer exists on this side of the wire. Spelled rather than
|
|
401
|
+
// omitted only because `FeatureValueDto.value` is a required key;
|
|
402
|
+
// `isBlank(undefined)` is true, so no formatter prints anything for it.
|
|
403
|
+
value: undefined,
|
|
404
|
+
redacted: true,
|
|
405
|
+
present: dao.present === true,
|
|
406
|
+
...(verification !== undefined ? { verification } : {}),
|
|
407
|
+
};
|
|
408
|
+
return { feature, value: stub };
|
|
409
|
+
}
|
|
410
|
+
|
|
331
411
|
const value: FeatureValueDto | undefined =
|
|
332
412
|
typeof dao.type === "string" && dao.type.length > 0
|
|
333
413
|
? { type: dao.type, value: dao.value }
|
|
@@ -365,12 +445,55 @@ export function unreadableFeatureCount(
|
|
|
365
445
|
* a composer reopening a PUBLISHED listing needs: `draftValuesFromDetail`
|
|
366
446
|
* seeds its editors from the published values, and the published values live
|
|
367
447
|
* only in the DAO list.
|
|
448
|
+
*
|
|
449
|
+
* ── Why a redacted stub is DROPPED here, and not passed through ────────────
|
|
450
|
+
*
|
|
451
|
+
* This is the EDIT envelope, and the only one that can destroy data. A stub
|
|
452
|
+
* has no value; seeding an editor from it would put `undefined` in the
|
|
453
|
+
* composer's draft under the seller's own slug, and the next save would write
|
|
454
|
+
* that back — blanking a stored VIN the seller never touched and cannot see
|
|
455
|
+
* was blanked. Dropping the row instead leaves the field empty in the form
|
|
456
|
+
* and untouched in the record, which is the recoverable failure.
|
|
457
|
+
*
|
|
458
|
+
* A composer reopening a listing belongs to its OWNER, whose read is
|
|
459
|
+
* unredacted, so a stub reaching this function means something upstream
|
|
460
|
+
* already went wrong — a cached anonymous payload, a viewer id that did not
|
|
461
|
+
* arrive, a host wiring the public detail into an edit form. It fails safe
|
|
462
|
+
* rather than fails loudly on purpose: there is no honest recovery from "the
|
|
463
|
+
* value was withheld from the person editing it", and refusing the whole seed
|
|
464
|
+
* would take the other twenty answers down with it.
|
|
465
|
+
*
|
|
466
|
+
* {@link featureValuesForDisplay} is the other half of this split: a spec
|
|
467
|
+
* table WANTS the stub, because "the seller answered this" is exactly what it
|
|
468
|
+
* has to say.
|
|
368
469
|
*/
|
|
369
470
|
export function featuresDtoFromDaoList(
|
|
370
471
|
daos: readonly ListingFeatureDao[] | null | undefined
|
|
371
472
|
): Readonly<Record<string, FeatureValueDto>> {
|
|
372
473
|
const out: Record<string, FeatureValueDto> = {};
|
|
373
474
|
for (const view of featuresFromDaoList(daos)) {
|
|
475
|
+
if (view.value === undefined || isRedactedValue(view.value)) continue;
|
|
476
|
+
out[view.feature.slug] = view.value;
|
|
477
|
+
}
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* A stored projection → the envelope a DISPLAY component reads, redacted
|
|
483
|
+
* stubs and all.
|
|
484
|
+
*
|
|
485
|
+
* Same shape as {@link featuresDtoFromDaoList} and a different job, which is
|
|
486
|
+
* why it is a different function rather than a flag: a stub belongs in a spec
|
|
487
|
+
* table (in place, in order, saying the seller answered) and must never reach
|
|
488
|
+
* an editor. A boolean argument would put the destructive default one
|
|
489
|
+
* forgotten parameter away.
|
|
490
|
+
*/
|
|
491
|
+
export function featureValuesForDisplay(
|
|
492
|
+
daos: readonly ListingFeatureDao[] | null | undefined,
|
|
493
|
+
source: FeatureCopySource = {}
|
|
494
|
+
): Readonly<Record<string, FeatureValueDto>> {
|
|
495
|
+
const out: Record<string, FeatureValueDto> = {};
|
|
496
|
+
for (const view of featuresFromDaoList(daos, source)) {
|
|
374
497
|
if (view.value !== undefined) out[view.feature.slug] = view.value;
|
|
375
498
|
}
|
|
376
499
|
return out;
|