@narrative.io/jsonforms-provider-protocols 2.11.0 → 2.12.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 +193 -33
- package/dist/core/initFormData.d.ts +17 -0
- package/dist/core/initFormData.d.ts.map +1 -0
- package/dist/core/initFormData.js +99 -0
- package/dist/core/initFormData.js.map +1 -0
- package/dist/core/projection.d.ts +36 -0
- package/dist/core/projection.d.ts.map +1 -0
- package/dist/core/projection.js +77 -0
- package/dist/core/projection.js.map +1 -0
- package/dist/core/refs.d.ts +58 -0
- package/dist/core/refs.d.ts.map +1 -0
- package/dist/core/refs.js +70 -0
- package/dist/core/refs.js.map +1 -0
- package/dist/core/resolveScope.d.ts +17 -0
- package/dist/core/resolveScope.d.ts.map +1 -0
- package/dist/core/resolveScope.js +28 -0
- package/dist/core/resolveScope.js.map +1 -0
- package/dist/core/seedProjectionTargets.d.ts +60 -0
- package/dist/core/seedProjectionTargets.d.ts.map +1 -0
- package/dist/core/seedProjectionTargets.js +52 -0
- package/dist/core/seedProjectionTargets.js.map +1 -0
- package/dist/core/transforms.d.ts +8 -10
- package/dist/core/transforms.d.ts.map +1 -1
- package/dist/core/transforms.js +58 -13
- package/dist/core/transforms.js.map +1 -1
- package/dist/core/types.d.ts +8 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/dist/jsonforms-provider-protocols.css +6 -2
- package/dist/no-eval-ajv.d.ts +70 -0
- package/dist/no-eval-ajv.d.ts.map +1 -0
- package/dist/no-eval-ajv.js +247 -0
- package/dist/no-eval-ajv.js.map +1 -0
- package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
- package/dist/vue/components/ProviderAutocomplete.vue.js +12 -6
- package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
- package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
- package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
- package/dist/vue/components/ProviderMultiSelect.vue2.js +21 -11
- package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
- package/dist/vue/components/ProviderObjectMultiSelect.vue.d.ts +9 -0
- package/dist/vue/components/ProviderObjectMultiSelect.vue.d.ts.map +1 -0
- package/dist/vue/components/ProviderObjectMultiSelect.vue.js +8 -0
- package/dist/vue/components/ProviderObjectMultiSelect.vue.js.map +1 -0
- package/dist/vue/components/ProviderObjectMultiSelect.vue2.js +142 -0
- package/dist/vue/components/ProviderObjectMultiSelect.vue2.js.map +1 -0
- package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
- package/dist/vue/components/ProviderSelect.vue.js +1 -1
- package/dist/vue/components/ProviderSelect.vue2.js +22 -10
- package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
- package/dist/vue/composables/useDataLayer.d.ts +10 -0
- package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
- package/dist/vue/composables/useDataLayer.js +26 -0
- package/dist/vue/composables/useDataLayer.js.map +1 -0
- package/dist/vue/composables/useDerive.d.ts +5 -2
- package/dist/vue/composables/useDerive.d.ts.map +1 -1
- package/dist/vue/composables/useDerive.js +29 -12
- package/dist/vue/composables/useDerive.js.map +1 -1
- package/dist/vue/composables/useDeriveInitialValue.d.ts +36 -0
- package/dist/vue/composables/useDeriveInitialValue.d.ts.map +1 -0
- package/dist/vue/composables/useDeriveInitialValue.js +125 -0
- package/dist/vue/composables/useDeriveInitialValue.js.map +1 -0
- package/dist/vue/composables/useDirtyValidation.d.ts +9 -0
- package/dist/vue/composables/useDirtyValidation.d.ts.map +1 -0
- package/dist/vue/composables/useDirtyValidation.js +15 -0
- package/dist/vue/composables/useDirtyValidation.js.map +1 -0
- package/dist/vue/composables/useProjection.d.ts +42 -0
- package/dist/vue/composables/useProjection.d.ts.map +1 -0
- package/dist/vue/composables/useProjection.js +116 -0
- package/dist/vue/composables/useProjection.js.map +1 -0
- package/dist/vue/composables/useProvider.d.ts +2 -2
- package/dist/vue/composables/useProvider.d.ts.map +1 -1
- package/dist/vue/composables/useProvider.js +14 -10
- package/dist/vue/composables/useProvider.js.map +1 -1
- package/dist/vue/index.d.ts +9 -1
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +72 -34
- package/dist/vue/index.js.map +1 -1
- package/dist/vue/primevue/JfBoolean.vue.d.ts +9 -0
- package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfBoolean.vue.js +44 -17
- package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
- package/dist/vue/primevue/JfEnum.vue.d.ts +9 -0
- package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfEnum.vue.js +38 -24
- package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
- package/dist/vue/primevue/JfEnumArray.vue.d.ts +9 -0
- package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfEnumArray.vue.js +40 -20
- package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
- package/dist/vue/primevue/JfNumber.vue.d.ts +9 -0
- package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfNumber.vue.js +33 -23
- package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
- package/dist/vue/primevue/JfText.vue.d.ts +9 -0
- package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfText.vue.js +51 -35
- package/dist/vue/primevue/JfText.vue.js.map +1 -1
- package/dist/vue/primevue/JfTextArea.vue.d.ts +9 -0
- package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfTextArea.vue.js +34 -19
- package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
- package/dist/vue/primevue/index.d.ts.map +1 -1
- package/dist/vue/primevue/index.js +100 -8
- package/dist/vue/primevue/index.js.map +1 -1
- package/dist/vue/utils/objectMultiSelect.d.ts +68 -0
- package/dist/vue/utils/objectMultiSelect.d.ts.map +1 -0
- package/dist/vue/utils/objectMultiSelect.js +72 -0
- package/dist/vue/utils/objectMultiSelect.js.map +1 -0
- package/dist/vue/utils/placeholder.d.ts +17 -0
- package/dist/vue/utils/placeholder.d.ts.map +1 -0
- package/dist/vue/utils/placeholder.js +17 -0
- package/dist/vue/utils/placeholder.js.map +1 -0
- package/package.json +10 -2
- package/src/core/initFormData.ts +208 -0
- package/src/core/projection.ts +147 -0
- package/src/core/refs.ts +166 -0
- package/src/core/resolveScope.ts +54 -0
- package/src/core/seedProjectionTargets.ts +144 -0
- package/src/core/transforms.ts +118 -26
- package/src/core/types.ts +9 -0
- package/src/index.ts +22 -2
- package/src/no-eval-ajv.ts +381 -0
- package/src/vue/components/ProviderAutocomplete.vue +11 -7
- package/src/vue/components/ProviderMultiSelect.vue +22 -15
- package/src/vue/components/ProviderObjectMultiSelect.vue +169 -0
- package/src/vue/components/ProviderSelect.vue +23 -14
- package/src/vue/composables/useDataLayer.ts +43 -0
- package/src/vue/composables/useDerive.ts +62 -16
- package/src/vue/composables/useDeriveInitialValue.ts +195 -0
- package/src/vue/composables/useDirtyValidation.ts +20 -0
- package/src/vue/composables/useProjection.ts +245 -0
- package/src/vue/composables/useProvider.ts +28 -11
- package/src/vue/index.ts +83 -47
- package/src/vue/primevue/JfBoolean.vue +35 -12
- package/src/vue/primevue/JfEnum.vue +35 -26
- package/src/vue/primevue/JfEnumArray.vue +37 -20
- package/src/vue/primevue/JfNumber.vue +32 -24
- package/src/vue/primevue/JfText.vue +48 -33
- package/src/vue/primevue/JfTextArea.vue +32 -21
- package/src/vue/primevue/index.ts +114 -8
- package/src/vue/styles.css +26 -1
- package/src/vue/utils/objectMultiSelect.ts +171 -0
- package/src/vue/utils/placeholder.ts +42 -0
|
@@ -4,6 +4,8 @@ import JfNumber from "./JfNumber.vue";
|
|
|
4
4
|
import JfEnum from "./JfEnum.vue";
|
|
5
5
|
import JfEnumArray from "./JfEnumArray.vue";
|
|
6
6
|
import JfBoolean from "./JfBoolean.vue";
|
|
7
|
+
import { getProjectedSchema } from "../../core/projection";
|
|
8
|
+
import { resolveScopeSchema } from "../../core/resolveScope";
|
|
7
9
|
|
|
8
10
|
// Auto-inject layout styles
|
|
9
11
|
const injectLayoutStyles = () => {
|
|
@@ -17,13 +19,38 @@ const injectLayoutStyles = () => {
|
|
|
17
19
|
display: flex;
|
|
18
20
|
flex-direction: column;
|
|
19
21
|
align-items: flex-start;
|
|
20
|
-
gap:
|
|
22
|
+
gap: 24px;
|
|
21
23
|
width: 100%;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
.vertical-layout-item {
|
|
25
27
|
width: 100%;
|
|
26
28
|
}
|
|
29
|
+
|
|
30
|
+
/* Form control wrapper */
|
|
31
|
+
.jf-control {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 12px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Form control label typography */
|
|
38
|
+
.jf-label {
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
font-size: 14px;
|
|
41
|
+
line-height: 14px;
|
|
42
|
+
color: #031553;
|
|
43
|
+
text-align: left;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Form control description typography */
|
|
47
|
+
.jf-description {
|
|
48
|
+
font-weight: 400;
|
|
49
|
+
font-size: 14px;
|
|
50
|
+
line-height: 14px;
|
|
51
|
+
color: #415290;
|
|
52
|
+
text-align: left;
|
|
53
|
+
}
|
|
27
54
|
`;
|
|
28
55
|
document.head.appendChild(style);
|
|
29
56
|
}
|
|
@@ -54,6 +81,7 @@ export function registerPrimevueRenderers(jsonformsCore: any): unknown[] {
|
|
|
54
81
|
isNumberControl,
|
|
55
82
|
isIntegerControl,
|
|
56
83
|
and,
|
|
84
|
+
or,
|
|
57
85
|
isControl,
|
|
58
86
|
schemaMatches,
|
|
59
87
|
isBooleanControl,
|
|
@@ -97,27 +125,105 @@ export function registerPrimevueRenderers(jsonformsCore: any): unknown[] {
|
|
|
97
125
|
);
|
|
98
126
|
};
|
|
99
127
|
|
|
128
|
+
// Projection-aware schema check: when options.projection is set,
|
|
129
|
+
// resolve the projected schema and test against it instead of the original
|
|
130
|
+
|
|
131
|
+
const projectedSchemaMatches =
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
133
|
+
(check: (schema: any) => boolean) =>
|
|
134
|
+
(uischema: unknown, schema: unknown): boolean => {
|
|
135
|
+
const ui = uischema as {
|
|
136
|
+
type?: string;
|
|
137
|
+
scope?: string;
|
|
138
|
+
options?: { projection?: string };
|
|
139
|
+
};
|
|
140
|
+
const projection = ui?.options?.projection;
|
|
141
|
+
if (!projection || ui?.type !== "Control" || !ui?.scope) return false;
|
|
142
|
+
|
|
143
|
+
const propertySchema = resolveScopeSchema(
|
|
144
|
+
ui.scope,
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
146
|
+
schema as Record<string, any>,
|
|
147
|
+
);
|
|
148
|
+
if (!propertySchema) return false;
|
|
149
|
+
|
|
150
|
+
return check(getProjectedSchema(propertySchema, projection));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const isMultilineProjection = (uischema: unknown, schema: unknown) => {
|
|
154
|
+
const ui = uischema as { options?: { multi?: boolean } };
|
|
155
|
+
return (
|
|
156
|
+
ui?.options?.multi === true &&
|
|
157
|
+
projectedSchemaMatches((s) => s?.type === "string")(uischema, schema)
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
100
161
|
const renderers = [
|
|
101
162
|
// Multiline text has higher priority than regular text
|
|
102
|
-
{ tester: rankWith(PRIME + 4, isMultilineString), renderer: JfTextArea },
|
|
103
|
-
{ tester: rankWith(PRIME + 3, isStringControl), renderer: JfText },
|
|
104
163
|
{
|
|
105
|
-
tester: rankWith(PRIME +
|
|
164
|
+
tester: rankWith(PRIME + 4, or(isMultilineString, isMultilineProjection)),
|
|
165
|
+
renderer: JfTextArea,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
tester: rankWith(
|
|
169
|
+
PRIME + 3,
|
|
170
|
+
or(
|
|
171
|
+
isStringControl,
|
|
172
|
+
projectedSchemaMatches((s) => s?.type === "string"),
|
|
173
|
+
),
|
|
174
|
+
),
|
|
175
|
+
renderer: JfText,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
tester: rankWith(
|
|
179
|
+
PRIME + 6,
|
|
180
|
+
or(
|
|
181
|
+
isIntegerControl,
|
|
182
|
+
projectedSchemaMatches((s) => s?.type === "integer"),
|
|
183
|
+
),
|
|
184
|
+
),
|
|
106
185
|
renderer: JfNumber,
|
|
107
186
|
},
|
|
108
187
|
{
|
|
109
|
-
tester: rankWith(
|
|
188
|
+
tester: rankWith(
|
|
189
|
+
PRIME + 4,
|
|
190
|
+
or(
|
|
191
|
+
isNumberControl,
|
|
192
|
+
projectedSchemaMatches((s) => s?.type === "number"),
|
|
193
|
+
),
|
|
194
|
+
),
|
|
110
195
|
renderer: JfNumber,
|
|
111
196
|
},
|
|
112
197
|
{
|
|
113
|
-
tester: rankWith(
|
|
198
|
+
tester: rankWith(
|
|
199
|
+
PRIME + 7,
|
|
200
|
+
or(
|
|
201
|
+
and(isControl, schemaMatches(isScalarEnum)),
|
|
202
|
+
and(isControl, projectedSchemaMatches(isScalarEnum)),
|
|
203
|
+
),
|
|
204
|
+
),
|
|
114
205
|
renderer: JfEnum,
|
|
115
206
|
},
|
|
116
207
|
{
|
|
117
|
-
tester: rankWith(
|
|
208
|
+
tester: rankWith(
|
|
209
|
+
PRIME + 8,
|
|
210
|
+
or(
|
|
211
|
+
and(isControl, schemaMatches(isEnumArray)),
|
|
212
|
+
and(isControl, projectedSchemaMatches(isEnumArray)),
|
|
213
|
+
),
|
|
214
|
+
),
|
|
118
215
|
renderer: JfEnumArray,
|
|
119
216
|
},
|
|
120
|
-
{
|
|
217
|
+
{
|
|
218
|
+
tester: rankWith(
|
|
219
|
+
PRIME + 3,
|
|
220
|
+
or(
|
|
221
|
+
isBooleanControl,
|
|
222
|
+
projectedSchemaMatches((s) => s?.type === "boolean"),
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
renderer: JfBoolean,
|
|
226
|
+
},
|
|
121
227
|
];
|
|
122
228
|
|
|
123
229
|
// Update the exported array
|
package/src/vue/styles.css
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
display: flex;
|
|
6
6
|
flex-direction: column;
|
|
7
7
|
align-items: flex-start;
|
|
8
|
-
gap:
|
|
8
|
+
gap: 24px;
|
|
9
9
|
width: 100%;
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -14,6 +14,31 @@
|
|
|
14
14
|
width: 100%;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/* Form control wrapper */
|
|
18
|
+
.jf-control {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: 12px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Form control label typography */
|
|
25
|
+
.jf-label {
|
|
26
|
+
font-weight: 600;
|
|
27
|
+
font-size: 14px;
|
|
28
|
+
line-height: 14px;
|
|
29
|
+
color: #031553;
|
|
30
|
+
text-align: left;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Form control description typography */
|
|
34
|
+
.jf-description {
|
|
35
|
+
font-weight: 400;
|
|
36
|
+
font-size: 14px;
|
|
37
|
+
line-height: 14px;
|
|
38
|
+
color: #415290;
|
|
39
|
+
text-align: left;
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
/* PrimeVue dropdown text alignment fix */
|
|
18
43
|
.p-dropdown-label {
|
|
19
44
|
text-align: left !important;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { deref } from "../../core/refs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helpers for `ProviderObjectMultiSelect`: bidirectional translation between
|
|
5
|
+
* the form-data shape (paired objects with consumer-named keys) and the
|
|
6
|
+
* MultiSelect model shape (`{ value, label }` matching the provider's `map`
|
|
7
|
+
* config), plus `objectKeys` inference when the consumer doesn't specify
|
|
8
|
+
* them on the uischema.
|
|
9
|
+
*
|
|
10
|
+
* Pure functions — no Vue, no rendering — so the renderer can compose them
|
|
11
|
+
* and tests can exercise the logic in isolation.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface ObjectKeys {
|
|
15
|
+
/** Property name on the form-data object that holds the identifier. */
|
|
16
|
+
value: string;
|
|
17
|
+
/** Property name on the form-data object that holds the display string. */
|
|
18
|
+
label: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MultiSelectOption {
|
|
22
|
+
value: unknown;
|
|
23
|
+
label: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Translate form-data items (`[{ [valueKey]: v, [labelKey]: l }]`) into the
|
|
29
|
+
* shape PrimeVue's `<MultiSelect>` expects (`[{ value, label }]`). Items
|
|
30
|
+
* already in `{ value, label }` shape pass through; missing keys yield
|
|
31
|
+
* `undefined` / `""` so the renderer doesn't blow up on partial data.
|
|
32
|
+
*/
|
|
33
|
+
export function toMultiSelectShape(
|
|
34
|
+
formData: unknown,
|
|
35
|
+
keys: ObjectKeys,
|
|
36
|
+
): MultiSelectOption[] {
|
|
37
|
+
if (!Array.isArray(formData)) return [];
|
|
38
|
+
return formData
|
|
39
|
+
.filter((item) => item !== null && typeof item === "object")
|
|
40
|
+
.map((item) => {
|
|
41
|
+
const obj = item as Record<string, unknown>;
|
|
42
|
+
// Tolerate already-translated items: if both `value` and `label` are
|
|
43
|
+
// present and the form-data keys aren't, assume MultiSelect shape.
|
|
44
|
+
if (
|
|
45
|
+
!(keys.value in obj) &&
|
|
46
|
+
!(keys.label in obj) &&
|
|
47
|
+
"value" in obj &&
|
|
48
|
+
"label" in obj
|
|
49
|
+
) {
|
|
50
|
+
return obj as MultiSelectOption;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
value: obj[keys.value],
|
|
54
|
+
label: typeof obj[keys.label] === "string" ? (obj[keys.label] as string) : "",
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Inverse of `toMultiSelectShape`. Translates `<MultiSelect>` model items
|
|
61
|
+
* back into form-data shape using the consumer-specified property names.
|
|
62
|
+
*/
|
|
63
|
+
export function fromMultiSelectShape(
|
|
64
|
+
modelData: unknown,
|
|
65
|
+
keys: ObjectKeys,
|
|
66
|
+
): Record<string, unknown>[] {
|
|
67
|
+
if (!Array.isArray(modelData)) return [];
|
|
68
|
+
return modelData
|
|
69
|
+
.filter((item) => item !== null && typeof item === "object")
|
|
70
|
+
.map((item) => {
|
|
71
|
+
const obj = item as Record<string, unknown>;
|
|
72
|
+
return {
|
|
73
|
+
[keys.value]: obj.value,
|
|
74
|
+
[keys.label]: obj.label,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Order-insensitive equality of two object-shape selections by the
|
|
81
|
+
* identifier property. Used to short-circuit redundant `handleChange`
|
|
82
|
+
* calls when the model emits the same selection under reference inequality.
|
|
83
|
+
*/
|
|
84
|
+
export function sameObjectSet(
|
|
85
|
+
a: unknown,
|
|
86
|
+
b: unknown,
|
|
87
|
+
identifierKey: string,
|
|
88
|
+
): boolean {
|
|
89
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const ids = new Set(
|
|
93
|
+
b
|
|
94
|
+
.filter((x) => x !== null && typeof x === "object")
|
|
95
|
+
.map((x) => (x as Record<string, unknown>)[identifierKey]),
|
|
96
|
+
);
|
|
97
|
+
return a.every((x) => {
|
|
98
|
+
if (x === null || typeof x !== "object") return false;
|
|
99
|
+
return ids.has((x as Record<string, unknown>)[identifierKey]);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the items schema of an array control, dereferencing `items.$ref`
|
|
105
|
+
* against the root if present. Returns `undefined` if no items schema exists.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveItemsSchema(
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
+
arraySchema: Record<string, any> | undefined,
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
+
rootSchema: Record<string, any>,
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
): Record<string, any> | undefined {
|
|
114
|
+
if (!arraySchema || typeof arraySchema !== "object") return undefined;
|
|
115
|
+
const items = arraySchema.items;
|
|
116
|
+
if (!items || typeof items !== "object" || Array.isArray(items)) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return deref(items, rootSchema);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Infer `objectKeys` from a resolved items schema when the consumer hasn't
|
|
124
|
+
* specified them on the uischema.
|
|
125
|
+
*
|
|
126
|
+
* Strategy: look at `items.required`. If it has exactly two entries, the
|
|
127
|
+
* one whose property has `format: 'uuid'` becomes `value`; the other
|
|
128
|
+
* becomes `label`. If neither has a uuid format, the first entry is
|
|
129
|
+
* `value`, the second is `label`. Returns `undefined` (and the renderer
|
|
130
|
+
* throws at mount) for any other shape — explicit `objectKeys` is required.
|
|
131
|
+
*/
|
|
132
|
+
export function inferObjectKeys(
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
134
|
+
itemsSchema: Record<string, any> | undefined,
|
|
135
|
+
): ObjectKeys | undefined {
|
|
136
|
+
if (!itemsSchema || itemsSchema.type !== "object") return undefined;
|
|
137
|
+
const required = itemsSchema.required;
|
|
138
|
+
if (!Array.isArray(required) || required.length !== 2) return undefined;
|
|
139
|
+
const [a, b] = required as [string, string];
|
|
140
|
+
const props = (itemsSchema.properties ?? {}) as Record<
|
|
141
|
+
string,
|
|
142
|
+
{ format?: string }
|
|
143
|
+
>;
|
|
144
|
+
const aIsUuid = props[a]?.format === "uuid";
|
|
145
|
+
const bIsUuid = props[b]?.format === "uuid";
|
|
146
|
+
if (aIsUuid && !bIsUuid) return { value: a, label: b };
|
|
147
|
+
if (bIsUuid && !aIsUuid) return { value: b, label: a };
|
|
148
|
+
return { value: a, label: b };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resolve the active `objectKeys` for a control: prefer the explicit
|
|
153
|
+
* `uischema.options.objectKeys`, fall back to schema-driven inference.
|
|
154
|
+
* Returns `undefined` when neither is available; the renderer surfaces a
|
|
155
|
+
* runtime error in that case so the consumer knows to be explicit.
|
|
156
|
+
*/
|
|
157
|
+
export function resolveObjectKeys(
|
|
158
|
+
uischemaOptions: { objectKeys?: { value?: unknown; label?: unknown } } | undefined,
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
160
|
+
itemsSchema: Record<string, any> | undefined,
|
|
161
|
+
): ObjectKeys | undefined {
|
|
162
|
+
const explicit = uischemaOptions?.objectKeys;
|
|
163
|
+
if (
|
|
164
|
+
explicit &&
|
|
165
|
+
typeof explicit.value === "string" &&
|
|
166
|
+
typeof explicit.label === "string"
|
|
167
|
+
) {
|
|
168
|
+
return { value: explicit.value, label: explicit.label };
|
|
169
|
+
}
|
|
170
|
+
return inferObjectKeys(itemsSchema);
|
|
171
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder resolution for form controls.
|
|
3
|
+
*
|
|
4
|
+
* Precedence:
|
|
5
|
+
* 1. `uischema.options.placeholder` (explicit author intent — always wins)
|
|
6
|
+
* 2. `Select ${label}` or `Enter ${label}` when a label is resolvable
|
|
7
|
+
* 3. Kind-appropriate bare fallback
|
|
8
|
+
*
|
|
9
|
+
* Never falls back to `schema.description` — descriptions are rendered as
|
|
10
|
+
* prose above the field by our renderers, so re-using them as placeholder
|
|
11
|
+
* produces a duplicated, truncated string inside the input.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type PlaceholderKind = "select" | "input";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Strip a trailing required-indicator asterisk (" *" or "*") that `resolveLabel`
|
|
18
|
+
* appends for required fields, so composed placeholders read naturally.
|
|
19
|
+
*/
|
|
20
|
+
function stripRequiredMarker(label: string): string {
|
|
21
|
+
return label.replace(/\s*\*\s*$/, "").trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolvePlaceholder(
|
|
25
|
+
uischema: { options?: unknown } | undefined,
|
|
26
|
+
resolvedLabel: string | undefined,
|
|
27
|
+
kind: PlaceholderKind,
|
|
28
|
+
): string | undefined {
|
|
29
|
+
const options = uischema?.options;
|
|
30
|
+
const explicit =
|
|
31
|
+
options && typeof options === "object"
|
|
32
|
+
? (options as Record<string, unknown>).placeholder
|
|
33
|
+
: undefined;
|
|
34
|
+
if (typeof explicit === "string" && explicit.length > 0) return explicit;
|
|
35
|
+
|
|
36
|
+
const label = resolvedLabel ? stripRequiredMarker(resolvedLabel) : "";
|
|
37
|
+
if (label) {
|
|
38
|
+
return kind === "select" ? `Select ${label}` : `Enter ${label}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return kind === "select" ? "Select…" : undefined;
|
|
42
|
+
}
|