@stapel/attributes-react 0.1.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/README.md +123 -0
- package/dist/default/FeatureBadges.d.ts +33 -0
- package/dist/default/FeatureBadges.d.ts.map +1 -0
- package/dist/default/FeatureBadges.js +56 -0
- package/dist/default/FeatureBadges.js.map +1 -0
- package/dist/default/FeatureFields.d.ts +52 -0
- package/dist/default/FeatureFields.d.ts.map +1 -0
- package/dist/default/FeatureFields.js +60 -0
- package/dist/default/FeatureFields.js.map +1 -0
- package/dist/default/editors.d.ts +24 -0
- package/dist/default/editors.d.ts.map +1 -0
- package/dist/default/editors.js +354 -0
- package/dist/default/editors.js.map +1 -0
- package/dist/default/index.d.ts +27 -0
- package/dist/default/index.d.ts.map +1 -0
- package/dist/default/index.js +25 -0
- package/dist/default/index.js.map +1 -0
- package/dist/dto.d.ts +40 -0
- package/dist/dto.d.ts.map +1 -0
- package/dist/dto.js +64 -0
- package/dist/dto.js.map +1 -0
- package/dist/errors.d.ts +57 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +98 -0
- package/dist/errors.js.map +1 -0
- package/dist/format.d.ts +46 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +159 -0
- package/dist/format.js.map +1 -0
- package/dist/i18n/es.d.ts +13 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +36 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/keys.d.ts +56 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +80 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +19 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +42 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +122 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +82 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +35 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +88 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +410 -0
- package/dist/validate.js.map +1 -0
- package/manifest.json +96 -0
- package/package.json +107 -0
- package/src/default/FeatureBadges.tsx +123 -0
- package/src/default/FeatureFields.tsx +140 -0
- package/src/default/editors.tsx +578 -0
- package/src/default/index.ts +34 -0
- package/src/dto.ts +78 -0
- package/src/errors.ts +127 -0
- package/src/format.ts +210 -0
- package/src/i18n/es.ts +41 -0
- package/src/i18n/keys.ts +89 -0
- package/src/i18n/ru.ts +48 -0
- package/src/index.ts +98 -0
- package/src/registry.ts +167 -0
- package/src/types.ts +166 -0
- package/src/validate.ts +507 -0
- package/tsconfig.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @stapel/attributes-react
|
|
2
|
+
|
|
3
|
+
The React value layer for `stapel-attributes`' dynamic feature types: draw an
|
|
4
|
+
attribute, check it, submit it, display it.
|
|
5
|
+
|
|
6
|
+
## The axis — `config.type`, not `FormField.kind`
|
|
7
|
+
|
|
8
|
+
`stapel-attributes` has **two** field vocabularies, and mixing them up is the
|
|
9
|
+
one mistake this package exists to prevent:
|
|
10
|
+
|
|
11
|
+
| | vocabulary | who reads it |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `FormField.kind` | the field kinds of the **admin form that configures a type** (`config_form.py`; `GET /forms/api/v1/field-kinds`) | [`@stapel/forms-react`](../forms-react) |
|
|
14
|
+
| `config["type"]` | the **value type** — `string`, `int`, `float`, `bool`, `select`, `date`, `header`, `hex_color`, `hierarchical_select`, `convertible_unit` | **this package** |
|
|
15
|
+
|
|
16
|
+
A person filling in a listing edits the second one. A useful consequence: a
|
|
17
|
+
storefront needs **no catalogue endpoint** for these types — the type arrives
|
|
18
|
+
in the data, on every feature that
|
|
19
|
+
`GET /categories/api/v1/categories/{id}/features/` returns.
|
|
20
|
+
|
|
21
|
+
## What kind of package this is
|
|
22
|
+
|
|
23
|
+
**L0, not a pair** — modelled on [`@stapel/image`](../image): no client, no
|
|
24
|
+
queries, no generated `schema.ts`. Its backend counterpart is an L1 library
|
|
25
|
+
with no HTTP surface at all (no models, no views, no urls), so there is no
|
|
26
|
+
`/attributes/api/v1/` to pair with and nothing for `pnpm gen:api` to read.
|
|
27
|
+
Feature definitions and validation verdicts reach a browser inside the
|
|
28
|
+
responses of the modules that own them (categories, listings), and those pairs
|
|
29
|
+
depend on this one to draw and check what they carry.
|
|
30
|
+
|
|
31
|
+
The builtin type set and the `ValidationErrorCode` vocabulary are pinned
|
|
32
|
+
against the engine's own generated corpora
|
|
33
|
+
(`stapel-attributes/tests/golden/{declarations,error_codes}.json`) by
|
|
34
|
+
`test/contract.test.ts` — always against the committed fixture, and
|
|
35
|
+
additionally against the live checkout when one is present.
|
|
36
|
+
|
|
37
|
+
## The ladder
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
explicit registerValueEditor(type, …) ← a host's, always wins
|
|
41
|
+
→ BUILTIN_VALUE_EDITORS ← /default's antd editors, ten types
|
|
42
|
+
→ <UnsupportedValueEditor/> ← loud, never a skipped field
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
…and, while an undrawable feature is on screen, `unsupportedTypeGate` blocks
|
|
46
|
+
the submit **with the reason named**. That last rung is the point: a category
|
|
47
|
+
can legally carry a type this build has no editor for, and drawing nothing
|
|
48
|
+
would silently drop a feature that may be MANDATORY — the person would submit
|
|
49
|
+
a listing they could not complete and be told, by the server, that an
|
|
50
|
+
attribute they never saw is missing.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import {
|
|
54
|
+
featureErrorsBySlug, mirrorValidate, toFeaturesDto, unsupportedTypeGate,
|
|
55
|
+
} from "@stapel/attributes-react";
|
|
56
|
+
import { BUILTIN_VALUE_EDITOR_TYPES, FeatureFields } from "@stapel/attributes-react/default";
|
|
57
|
+
|
|
58
|
+
const gate = unsupportedTypeGate(features, BUILTIN_VALUE_EDITOR_TYPES);
|
|
59
|
+
const errors = featureErrorsBySlug(mirrorValidate(features, toFeaturesDto(features, values)));
|
|
60
|
+
|
|
61
|
+
<FeatureFields features={features} values={values} onChange={setValue} errors={errors} />;
|
|
62
|
+
<Button disabled={!gate.available}>…</Button>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Adding a type your backend registered through `EXTRA_TYPES` takes one line at
|
|
66
|
+
startup:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
registerValueEditor("size_grid", SizeGridEditor);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## The mirror
|
|
73
|
+
|
|
74
|
+
`mirrorValidate(features, dto)` returns the server's own
|
|
75
|
+
`ValidationBatchResult` shape, built from the same rules and reporting the
|
|
76
|
+
same `error.400.feature_*` keys — so a "too long" caught locally and one
|
|
77
|
+
caught by the server render the same sentence. It is feedback, never a
|
|
78
|
+
verdict: `POST /categories/{pk}/validate-dto/` and
|
|
79
|
+
`POST /listings/{pk}/publish/` decide.
|
|
80
|
+
|
|
81
|
+
Two contract details it gets right on purpose, both from
|
|
82
|
+
`stapel-attributes/MODULE.md` §"Pattern contract":
|
|
83
|
+
|
|
84
|
+
- `pattern` matches the **whole** value (`re.fullmatch`), not a prefix.
|
|
85
|
+
- string length is counted in Unicode **code points** on both sides — five
|
|
86
|
+
emoji are five characters, not ten.
|
|
87
|
+
|
|
88
|
+
And two places it deliberately stands down, because a mirror that refuses what
|
|
89
|
+
the server accepts blocks a valid submit:
|
|
90
|
+
|
|
91
|
+
- a value type it does not know (it may be a perfectly good `EXTRA_TYPES`
|
|
92
|
+
registration whose rules live only in Python) — it still checks
|
|
93
|
+
mandatory/empty, which is type-independent;
|
|
94
|
+
- `convertible_unit`'s range, whose `min`/`max` are in the unit family's base
|
|
95
|
+
unit and whose conversion table is server-side. The number and the unit code
|
|
96
|
+
ARE checked.
|
|
97
|
+
|
|
98
|
+
## Value shapes that are not what you would guess
|
|
99
|
+
|
|
100
|
+
Three of the ten types do not carry a bare scalar, and every one of them is a
|
|
101
|
+
submit that fails silently if you assume otherwise:
|
|
102
|
+
|
|
103
|
+
| type | wire value |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `select` | **always a list**, even when `maxSelected: 1` |
|
|
106
|
+
| `date` | a **Unix timestamp (integer)**, not an ISO string |
|
|
107
|
+
| `hex_color` | an **object** `{simple, hex?, label?}` where `simple` is required and drawn from eighteen colour categories |
|
|
108
|
+
| `convertible_unit` | an **object** `{value, unit}` — the number as typed, tagged with the unit; the server converts |
|
|
109
|
+
|
|
110
|
+
`toFeaturesDto` builds the `{slug: {type, value}}` envelope from a plain
|
|
111
|
+
`{slug: value}` map, tagging each entry from the FEATURE's config (never from
|
|
112
|
+
the editor — the engine overrides a client-sent `type` anyway) and dropping
|
|
113
|
+
headers, untyped rows and blanks.
|
|
114
|
+
|
|
115
|
+
## Display
|
|
116
|
+
|
|
117
|
+
`formatFeatureValue` is React-free and lives in the main entry, so a result
|
|
118
|
+
card can format a value without pulling antd in. `/default` adds
|
|
119
|
+
`<FeatureBadges/>` (the `show_as_badge` values as tags) and
|
|
120
|
+
`<FeatureValueList/>` (the whole spec table). Both keep the two absences
|
|
121
|
+
apart: "not specified" for a value nobody entered, and a named notice for a
|
|
122
|
+
type this build cannot read — an empty cell would claim the first when it
|
|
123
|
+
means the second.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DISPLAY half — `<FeatureBadges/>` for a card, `<FeatureValueList/>` for
|
|
3
|
+
* a detail page. Both are renderers over `formatFeatureValue`; neither
|
|
4
|
+
* re-implements a single type's formatting.
|
|
5
|
+
*
|
|
6
|
+
* Two rules they hold that a naive spec table does not:
|
|
7
|
+
*
|
|
8
|
+
* - **An unreadable value says so.** A type this build cannot format renders
|
|
9
|
+
* a named notice, not an empty cell — the display twin of the unsupported
|
|
10
|
+
* editor. A blank cell where a spec line belongs reads as "this listing
|
|
11
|
+
* has no engine size", which is a different and false statement.
|
|
12
|
+
* - **`show_as_badge` / `show_at_title` are the CATEGORY's decision**, made
|
|
13
|
+
* by whoever configured the feature, and this component honours it instead
|
|
14
|
+
* of picking its own first three values.
|
|
15
|
+
*/
|
|
16
|
+
import type { ReactElement } from "react";
|
|
17
|
+
import type { FeatureDef, FeaturesDto } from "../types.js";
|
|
18
|
+
export interface FeatureDisplayProps {
|
|
19
|
+
readonly features: readonly FeatureDef[];
|
|
20
|
+
/** The stored values, in the same `{slug: {type, value}}` envelope the
|
|
21
|
+
* composer submitted. */
|
|
22
|
+
readonly values: FeaturesDto;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The `show_as_badge` values, as antd `Tag`s — what a result card shows under
|
|
26
|
+
* a title. Features with no value are omitted here (a card is a summary, and
|
|
27
|
+
* "not specified" is not a selling point); the detail list below says it
|
|
28
|
+
* explicitly instead.
|
|
29
|
+
*/
|
|
30
|
+
export declare function FeatureBadges(props: FeatureDisplayProps): ReactElement;
|
|
31
|
+
/** Every feature and its value — the spec table of a detail page. */
|
|
32
|
+
export declare function FeatureValueList(props: FeatureDisplayProps): ReactElement;
|
|
33
|
+
//# sourceMappingURL=FeatureBadges.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeatureBadges.d.ts","sourceRoot":"","sources":["../../src/default/FeatureBadges.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAG1C,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK3D,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;IACzC;6BACyB;IACzB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAiDD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,mBAAmB,GAAG,YAAY,CAgBtE;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,mBAAmB,GAAG,YAAY,CAoBzE"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Descriptions, Flex, Tag, Typography } from "antd";
|
|
3
|
+
import { useI18n, useT } from "@stapel/core";
|
|
4
|
+
import { featureName, featureType } from "../types.js";
|
|
5
|
+
import { formatFeatureValue, hexColorSwatch } from "../format.js";
|
|
6
|
+
import { ATTRIBUTES_I18N_KEYS } from "../i18n/keys.js";
|
|
7
|
+
/** Feature + its formatted value, in the order the category declared. */
|
|
8
|
+
function useRows(props, only) {
|
|
9
|
+
const t = useT();
|
|
10
|
+
const { locale } = useI18n();
|
|
11
|
+
return props.features
|
|
12
|
+
.filter((feature) => featureType(feature) !== "header")
|
|
13
|
+
.filter((feature) => (only ? only(feature) : true))
|
|
14
|
+
.map((feature) => {
|
|
15
|
+
const dto = props.values[feature.slug];
|
|
16
|
+
return {
|
|
17
|
+
feature,
|
|
18
|
+
text: formatFeatureValue(feature, dto, { t, locale }),
|
|
19
|
+
swatch: featureType(feature) === "hex_color" ? hexColorSwatch(dto) : undefined,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** A row's value, or the named reason there is none. */
|
|
24
|
+
function ValueText(props) {
|
|
25
|
+
const t = useT();
|
|
26
|
+
if (props.text !== undefined)
|
|
27
|
+
return _jsx(_Fragment, { children: props.text });
|
|
28
|
+
const type = featureType(props.feature);
|
|
29
|
+
// Two different absences, said differently: nothing was entered, versus
|
|
30
|
+
// something was entered that this build cannot read.
|
|
31
|
+
if (!props.hasValue || type === undefined) {
|
|
32
|
+
return (_jsx(Typography.Text, { type: "secondary", children: t(ATTRIBUTES_I18N_KEYS.valueNotSet) }));
|
|
33
|
+
}
|
|
34
|
+
return (_jsx(Typography.Text, { type: "warning", "data-testid": "attributes-unreadable-value", children: t(ATTRIBUTES_I18N_KEYS.valueUnreadable, { type }) }));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The `show_as_badge` values, as antd `Tag`s — what a result card shows under
|
|
38
|
+
* a title. Features with no value are omitted here (a card is a summary, and
|
|
39
|
+
* "not specified" is not a selling point); the detail list below says it
|
|
40
|
+
* explicitly instead.
|
|
41
|
+
*/
|
|
42
|
+
export function FeatureBadges(props) {
|
|
43
|
+
const rows = useRows(props, (feature) => feature.show_as_badge === true);
|
|
44
|
+
const shown = rows.filter((row) => row.text !== undefined);
|
|
45
|
+
return (_jsx(Flex, { gap: 4, wrap: true, "data-testid": "attributes-badges", children: shown.map((row) => (_jsx(Tag, { ...(row.swatch ? { color: row.swatch } : {}), "data-testid": `attributes-badge-${row.feature.slug}`, children: row.text }, row.feature.slug))) }));
|
|
46
|
+
}
|
|
47
|
+
/** Every feature and its value — the spec table of a detail page. */
|
|
48
|
+
export function FeatureValueList(props) {
|
|
49
|
+
const rows = useRows(props);
|
|
50
|
+
return (_jsx(Descriptions, { column: 1, size: "small", "data-testid": "attributes-value-list", items: rows.map((row) => ({
|
|
51
|
+
key: row.feature.slug,
|
|
52
|
+
label: featureName(row.feature),
|
|
53
|
+
children: (_jsx(ValueText, { feature: row.feature, text: row.text, hasValue: props.values[row.feature.slug] !== undefined })),
|
|
54
|
+
})) }));
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=FeatureBadges.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeatureBadges.js","sourceRoot":"","sources":["../../src/default/FeatureBadges.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<FeatureFields/>` — a category's features, drawn as antd form rows, and
|
|
3
|
+
* the place the three-rung ladder is actually executed.
|
|
4
|
+
*
|
|
5
|
+
* It holds no state and owns no submit: values and errors come in, changes go
|
|
6
|
+
* out. That is what lets the composer that DOES own the submit
|
|
7
|
+
* (listings-react) keep one source of truth for a draft, and what lets this
|
|
8
|
+
* component be dropped into any form — a filter panel, an admin preview —
|
|
9
|
+
* without dragging a mutation along.
|
|
10
|
+
*/
|
|
11
|
+
import type { ReactElement, ReactNode } from "react";
|
|
12
|
+
import type { FlowError } from "@stapel/core";
|
|
13
|
+
import type { FeatureDef } from "../types.js";
|
|
14
|
+
/** Stable per-feature DOM id, so `<label for>` reaches the control. Slugs are
|
|
15
|
+
* `[a-z0-9_]`-shaped by the engine, so this needs no further escaping. */
|
|
16
|
+
export declare function featureControlId(slug: string): string;
|
|
17
|
+
export interface UnsupportedValueEditorProps {
|
|
18
|
+
readonly feature: FeatureDef;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The loud last rung. NOT a skipped field: a category can legally carry a
|
|
22
|
+
* type this build has no editor for, and drawing nothing would silently drop
|
|
23
|
+
* a feature that may be mandatory — the person would submit a listing they
|
|
24
|
+
* could not complete and be told, by the server, that an attribute they never
|
|
25
|
+
* saw is missing.
|
|
26
|
+
*/
|
|
27
|
+
export declare function UnsupportedValueEditor(props: UnsupportedValueEditorProps): ReactElement;
|
|
28
|
+
export interface FeatureFieldsProps {
|
|
29
|
+
readonly features: readonly FeatureDef[];
|
|
30
|
+
/** Current answers keyed by slug. The DTO envelope is built at submit time
|
|
31
|
+
* with `toFeaturesDto`, not held here. */
|
|
32
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
33
|
+
onChange(slug: string, value: unknown): void;
|
|
34
|
+
/** Refusals keyed by slug — mirrored or from the server, folded through
|
|
35
|
+
* `featureErrorsBySlug` either way. */
|
|
36
|
+
readonly errors?: Readonly<Record<string, FlowError>>;
|
|
37
|
+
/** True while a submit is in flight — every editor goes read-only. */
|
|
38
|
+
readonly disabled?: boolean;
|
|
39
|
+
/** Rendered instead of a default `Form.Item` row, for a host with its own
|
|
40
|
+
* field chrome. */
|
|
41
|
+
readonly renderRow?: (row: FeatureRowProps) => ReactNode;
|
|
42
|
+
}
|
|
43
|
+
export interface FeatureRowProps {
|
|
44
|
+
readonly feature: FeatureDef;
|
|
45
|
+
readonly controlId: string;
|
|
46
|
+
readonly control: ReactNode;
|
|
47
|
+
readonly error: FlowError | undefined;
|
|
48
|
+
readonly required: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** A category's features as form rows, each drawn by the ladder's winner. */
|
|
51
|
+
export declare function FeatureFields(props: FeatureFieldsProps): ReactElement;
|
|
52
|
+
//# sourceMappingURL=FeatureFields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeatureFields.d.ts","sourceRoot":"","sources":["../../src/default/FeatureFields.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAM9C;0EAC0E;AAC1E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,2BAA2B,GACjC,YAAY,CAgBd;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;IACzC;8CAC0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7C;2CACuC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IACtD,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B;uBACmB;IACnB,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,SAAS,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAsBD,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,YAAY,CAqCrE"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Alert, Form } from "antd";
|
|
3
|
+
import { useFormatFlowError, useT } from "@stapel/core";
|
|
4
|
+
import { featureName, featureType } from "../types.js";
|
|
5
|
+
import { resolveValueEditor } from "../registry.js";
|
|
6
|
+
import { BUILTIN_VALUE_EDITORS } from "./editors.js";
|
|
7
|
+
import { ATTRIBUTES_I18N_KEYS } from "../i18n/keys.js";
|
|
8
|
+
/** Stable per-feature DOM id, so `<label for>` reaches the control. Slugs are
|
|
9
|
+
* `[a-z0-9_]`-shaped by the engine, so this needs no further escaping. */
|
|
10
|
+
export function featureControlId(slug) {
|
|
11
|
+
return `attributes-field-${slug}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The loud last rung. NOT a skipped field: a category can legally carry a
|
|
15
|
+
* type this build has no editor for, and drawing nothing would silently drop
|
|
16
|
+
* a feature that may be mandatory — the person would submit a listing they
|
|
17
|
+
* could not complete and be told, by the server, that an attribute they never
|
|
18
|
+
* saw is missing.
|
|
19
|
+
*/
|
|
20
|
+
export function UnsupportedValueEditor(props) {
|
|
21
|
+
const t = useT();
|
|
22
|
+
const type = featureType(props.feature);
|
|
23
|
+
return (_jsx(Alert, { type: "warning", showIcon: true, "data-testid": "attributes-unsupported-type", message: featureName(props.feature), description: type === undefined
|
|
24
|
+
? t(ATTRIBUTES_I18N_KEYS.untypedFeature)
|
|
25
|
+
: t(ATTRIBUTES_I18N_KEYS.unsupportedType, { type }) }));
|
|
26
|
+
}
|
|
27
|
+
function FeatureRow(props) {
|
|
28
|
+
const format = useFormatFlowError();
|
|
29
|
+
// A header is a caption: no label, no colon, no required marker — rendering
|
|
30
|
+
// one inside a labelled Form.Item would make a section heading look like a
|
|
31
|
+
// question.
|
|
32
|
+
if (featureType(props.feature) === "header") {
|
|
33
|
+
return _jsx(Form.Item, { style: { marginBottom: 8 }, children: props.control });
|
|
34
|
+
}
|
|
35
|
+
return (_jsx(Form.Item, { label: featureName(props.feature), htmlFor: props.controlId, required: props.required, ...(props.error ? { validateStatus: "error", help: format(props.error) } : {}), children: props.control }));
|
|
36
|
+
}
|
|
37
|
+
/** A category's features as form rows, each drawn by the ladder's winner. */
|
|
38
|
+
export function FeatureFields(props) {
|
|
39
|
+
const errors = props.errors ?? {};
|
|
40
|
+
return (_jsx(_Fragment, { children: props.features.map((feature) => {
|
|
41
|
+
const controlId = featureControlId(feature.slug);
|
|
42
|
+
const type = featureType(feature);
|
|
43
|
+
// The ladder: a host's explicit registration outranks the skin's
|
|
44
|
+
// builtin, and nothing at all is a NOTICE, never an omission.
|
|
45
|
+
const Editor = (type === undefined ? null : resolveValueEditor(type)) ??
|
|
46
|
+
(type === undefined ? undefined : BUILTIN_VALUE_EDITORS[type]);
|
|
47
|
+
const control = Editor === undefined || Editor === null ? (_jsx(UnsupportedValueEditor, { feature: feature })) : (_jsx(Editor, { id: controlId, feature: feature, value: props.values[feature.slug], onChange: (value) => props.onChange(feature.slug, value), error: errors[feature.slug], disabled: props.disabled === true }));
|
|
48
|
+
const row = {
|
|
49
|
+
feature,
|
|
50
|
+
controlId,
|
|
51
|
+
control,
|
|
52
|
+
error: errors[feature.slug],
|
|
53
|
+
required: feature.mandatory === true,
|
|
54
|
+
};
|
|
55
|
+
if (props.renderRow)
|
|
56
|
+
return _jsx("div", { children: props.renderRow(row) }, feature.slug);
|
|
57
|
+
return _jsx(FeatureRow, { ...row }, feature.slug);
|
|
58
|
+
}) }));
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=FeatureFields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeatureFields.js","sourceRoot":"","sources":["../../src/default/FeatureFields.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ValueEditor } from "../registry.js";
|
|
2
|
+
/** Unix seconds → the string a native input of this precision displays,
|
|
3
|
+
* in the VIEWER's time zone (which is what the person typed it in). */
|
|
4
|
+
export declare function timestampToInputValue(seconds: number, precision: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* The inverse. Takes no `precision`: a native input's value already says
|
|
7
|
+
* which shape it is (`2010`, `2010-06`, `2010-06-15`, `2010-06-15T14:30`), so
|
|
8
|
+
* reading the config here would only create a way for the two to disagree.
|
|
9
|
+
*
|
|
10
|
+
* Returns `undefined` for an empty or unparseable input rather than 0 — `0`
|
|
11
|
+
* is 1970, a real timestamp, and the one value that must never be produced by
|
|
12
|
+
* "the person cleared the field".
|
|
13
|
+
*/
|
|
14
|
+
export declare function inputValueToTimestamp(text: string): number | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The skin's builtin editor per value type — the second rung of the ladder.
|
|
17
|
+
* A type absent from this table has no default drawing and reaches
|
|
18
|
+
* `<UnsupportedValueEditor/>`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const BUILTIN_VALUE_EDITORS: Readonly<Record<string, ValueEditor>>;
|
|
21
|
+
/** The types this skin can draw — handed to `unsupportedTypes` so the
|
|
22
|
+
* headless half can judge renderability without importing the skin. */
|
|
23
|
+
export declare const BUILTIN_VALUE_EDITOR_TYPES: readonly string[];
|
|
24
|
+
//# sourceMappingURL=editors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editors.d.ts","sourceRoot":"","sources":["../../src/default/editors.tsx"],"names":[],"mappings":"AAiDA,OAAO,KAAK,EAAE,WAAW,EAAoB,MAAM,gBAAgB,CAAC;AAuNpE;uEACuE;AACvE,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAMhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CActE;AAmQD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAWvE,CAAC;AAEF;uEACuE;AACvE,eAAO,MAAM,0BAA0B,EAAE,SAAS,MAAM,EACb,CAAC"}
|