@narrative.io/jsonforms-provider-protocols 3.0.0-beta.1 → 3.0.0-beta.11
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/dist/core/initFormData.d.ts +10 -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.map +1 -1
- package/dist/core/projection.js.map +1 -1
- package/dist/core/transforms.d.ts.map +1 -1
- package/dist/core/transforms.js +3 -1
- package/dist/core/transforms.js.map +1 -1
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/jsonforms-provider-protocols.css +2 -2
- package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
- package/dist/vue/components/ProviderAutocomplete.vue.js +8 -5
- 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 +9 -6
- package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
- 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 +11 -6
- package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
- package/dist/vue/composables/useDataLayer.d.ts +1 -0
- package/dist/vue/composables/useDataLayer.d.ts.map +1 -1
- package/dist/vue/composables/useDataLayer.js +1 -0
- package/dist/vue/composables/useDataLayer.js.map +1 -1
- package/dist/vue/composables/useDerive.d.ts +1 -1
- package/dist/vue/composables/useDerive.d.ts.map +1 -1
- package/dist/vue/composables/useDerive.js +19 -2
- 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 +6 -0
- package/dist/vue/composables/useProjection.d.ts.map +1 -1
- package/dist/vue/composables/useProjection.js +54 -3
- package/dist/vue/composables/useProjection.js.map +1 -1
- 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 +3 -0
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +32 -10
- package/dist/vue/index.js.map +1 -1
- package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfBoolean.vue.js +26 -9
- package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
- package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfEnum.vue.js +21 -17
- package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
- package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfEnumArray.vue.js +22 -12
- package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
- package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfNumber.vue.js +21 -17
- package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
- package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfText.vue.js +29 -26
- package/dist/vue/primevue/JfText.vue.js.map +1 -1
- package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfTextArea.vue.js +22 -13
- 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 +93 -16
- package/dist/vue/primevue/index.js.map +1 -1
- package/dist/vue/utils/autoSelect.js.map +1 -1
- package/package.json +3 -1
- package/src/core/initFormData.ts +189 -0
- package/src/core/projection.ts +5 -5
- package/src/core/transforms.ts +33 -6
- package/src/core/types.ts +1 -0
- package/src/index.ts +1 -0
- package/src/vue/components/ProviderAutocomplete.vue +8 -5
- package/src/vue/components/ProviderMultiSelect.vue +13 -8
- package/src/vue/components/ProviderSelect.vue +14 -7
- package/src/vue/composables/useDataLayer.ts +1 -1
- package/src/vue/composables/useDerive.ts +46 -3
- package/src/vue/composables/useDeriveInitialValue.ts +195 -0
- package/src/vue/composables/useDirtyValidation.ts +20 -0
- package/src/vue/composables/useProjection.ts +108 -1
- package/src/vue/composables/useProvider.ts +28 -11
- package/src/vue/index.ts +29 -9
- package/src/vue/primevue/JfBoolean.vue +19 -6
- package/src/vue/primevue/JfEnum.vue +23 -21
- package/src/vue/primevue/JfEnumArray.vue +25 -15
- package/src/vue/primevue/JfNumber.vue +22 -20
- package/src/vue/primevue/JfText.vue +26 -24
- package/src/vue/primevue/JfTextArea.vue +22 -15
- package/src/vue/primevue/index.ts +104 -23
- package/src/vue/styles.css +26 -1
- package/src/vue/utils/autoSelect.ts +2 -2
|
@@ -45,6 +45,7 @@ import { computed, inject, getCurrentInstance, watch } from "vue";
|
|
|
45
45
|
import { useProvider } from "../composables/useProvider";
|
|
46
46
|
import { useDerive } from "../composables/useDerive";
|
|
47
47
|
import { useProjection } from "../composables/useProjection";
|
|
48
|
+
import { useDirtyValidation } from "../composables/useDirtyValidation";
|
|
48
49
|
import { shouldAutoSelectMulti } from "../utils/autoSelect";
|
|
49
50
|
import MultiSelect from "primevue/multiselect";
|
|
50
51
|
|
|
@@ -52,7 +53,12 @@ import MultiSelect from "primevue/multiselect";
|
|
|
52
53
|
const instance = getCurrentInstance()!;
|
|
53
54
|
const props = instance.props as unknown as ControlProps;
|
|
54
55
|
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
55
|
-
const {
|
|
56
|
+
const {
|
|
57
|
+
projectedData,
|
|
58
|
+
handleProjectedChange: handleChange,
|
|
59
|
+
projectedErrors,
|
|
60
|
+
projectedLabel,
|
|
61
|
+
} = useProjection(control, rawHandleChange);
|
|
56
62
|
|
|
57
63
|
type Opt = { label: string; value: unknown };
|
|
58
64
|
const toOptions = (schema?: JsonSchema): Opt[] => {
|
|
@@ -118,7 +124,7 @@ const {
|
|
|
118
124
|
} = useProvider(binding, {
|
|
119
125
|
data: rootData,
|
|
120
126
|
path: control.value.path,
|
|
121
|
-
dependsOnValues: depValues
|
|
127
|
+
dependsOnValues: depValues,
|
|
122
128
|
});
|
|
123
129
|
|
|
124
130
|
const options = computed(() => {
|
|
@@ -132,6 +138,9 @@ const options = computed(() => {
|
|
|
132
138
|
// Add derive functionality
|
|
133
139
|
useDerive({ control, handleChange, data: projectedData });
|
|
134
140
|
|
|
141
|
+
// Track user interaction — errors only show after first change
|
|
142
|
+
const { showErrors, markDirty } = useDirtyValidation(control, projectedErrors);
|
|
143
|
+
|
|
135
144
|
// Auto-select when provider returns only one item (opt-in for multiselect)
|
|
136
145
|
watch(
|
|
137
146
|
[providerItems, loading],
|
|
@@ -141,14 +150,16 @@ watch(
|
|
|
141
150
|
control.value.uischema?.options?.autoSelectSingle === true,
|
|
142
151
|
isLoading,
|
|
143
152
|
items,
|
|
144
|
-
currentValue: Array.isArray(projectedData.value)
|
|
153
|
+
currentValue: Array.isArray(projectedData.value)
|
|
154
|
+
? projectedData.value
|
|
155
|
+
: [],
|
|
145
156
|
});
|
|
146
157
|
|
|
147
158
|
if (valueToSelect !== null) {
|
|
148
159
|
handleChange(control.value.path, valueToSelect);
|
|
149
160
|
}
|
|
150
161
|
},
|
|
151
|
-
{ immediate: true }
|
|
162
|
+
{ immediate: true },
|
|
152
163
|
);
|
|
153
164
|
|
|
154
165
|
const placeholder = computed<string | undefined>(() => {
|
|
@@ -177,38 +188,37 @@ const model = computed<unknown[]>({
|
|
|
177
188
|
set(val) {
|
|
178
189
|
const next = Array.isArray(val) ? [...val] : [];
|
|
179
190
|
const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
|
|
180
|
-
if (!sameSet(curr, next))
|
|
191
|
+
if (!sameSet(curr, next)) {
|
|
192
|
+
markDirty();
|
|
193
|
+
handleChange(control.value.path, next);
|
|
194
|
+
}
|
|
181
195
|
},
|
|
182
196
|
});
|
|
183
197
|
</script>
|
|
184
198
|
|
|
185
199
|
<template>
|
|
186
|
-
<div class="
|
|
187
|
-
<label v-if="
|
|
188
|
-
|
|
189
|
-
}}</label>
|
|
190
|
-
<div v-if="control.description" class="text-color-secondary text-left">
|
|
200
|
+
<div class="jf-control">
|
|
201
|
+
<label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
|
|
202
|
+
<div v-if="control.description" class="jf-description">
|
|
191
203
|
{{ control.description }}
|
|
192
204
|
</div>
|
|
193
205
|
|
|
194
206
|
<MultiSelect
|
|
195
207
|
v-model="model"
|
|
196
|
-
class="w-full"
|
|
208
|
+
:class="['w-full!', { 'p-invalid': showErrors }]"
|
|
197
209
|
:options="options"
|
|
198
210
|
option-label="label"
|
|
199
211
|
option-value="value"
|
|
200
212
|
data-key="value"
|
|
201
213
|
display="chip"
|
|
202
214
|
:disabled="!control.enabled || loading"
|
|
203
|
-
:aria-invalid="
|
|
215
|
+
:aria-invalid="showErrors || undefined"
|
|
204
216
|
:placeholder="placeholder"
|
|
205
217
|
/>
|
|
206
218
|
|
|
207
219
|
<small v-if="error" class="p-error" role="alert"
|
|
208
220
|
>Failed to load: {{ error }}</small
|
|
209
221
|
>
|
|
210
|
-
<small v-else-if="
|
|
211
|
-
control.errors
|
|
212
|
-
}}</small>
|
|
222
|
+
<small v-else-if="showErrors" class="p-error">{{ projectedErrors }}</small>
|
|
213
223
|
</div>
|
|
214
224
|
</template>
|
|
@@ -40,16 +40,23 @@ export default {
|
|
|
40
40
|
<script setup lang="ts">
|
|
41
41
|
import type { ControlProps } from "@jsonforms/vue";
|
|
42
42
|
import { useJsonFormsControl } from "@jsonforms/vue";
|
|
43
|
-
import { computed,
|
|
43
|
+
import { computed, getCurrentInstance } from "vue";
|
|
44
44
|
import { useDerive } from "../composables/useDerive";
|
|
45
|
+
import { useDeriveInitialValue } from "../composables/useDeriveInitialValue";
|
|
45
46
|
import { useProjection } from "../composables/useProjection";
|
|
47
|
+
import { useDirtyValidation } from "../composables/useDirtyValidation";
|
|
46
48
|
import InputNumber from "primevue/inputnumber";
|
|
47
49
|
|
|
48
50
|
// Access props from the component instance
|
|
49
51
|
const instance = getCurrentInstance()!;
|
|
50
52
|
const props = instance.props as unknown as ControlProps;
|
|
51
53
|
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
52
|
-
const {
|
|
54
|
+
const {
|
|
55
|
+
projectedData,
|
|
56
|
+
handleProjectedChange: handleChange,
|
|
57
|
+
projectedErrors,
|
|
58
|
+
projectedLabel,
|
|
59
|
+
} = useProjection(control, rawHandleChange);
|
|
53
60
|
|
|
54
61
|
const options = computed(
|
|
55
62
|
() =>
|
|
@@ -64,6 +71,9 @@ const placeholder = computed<string | undefined>(
|
|
|
64
71
|
// Add derive functionality
|
|
65
72
|
useDerive({ control, handleChange, data: projectedData });
|
|
66
73
|
|
|
74
|
+
// Add deriveInitialValue — async API-based initial value seeding
|
|
75
|
+
useDeriveInitialValue({ control, handleChange });
|
|
76
|
+
|
|
67
77
|
// Currency and decimal configuration
|
|
68
78
|
const mode = computed(() => {
|
|
69
79
|
if (options.value.currency) return "currency";
|
|
@@ -100,31 +110,23 @@ const useGrouping = computed(() => {
|
|
|
100
110
|
return options.value.useGrouping === true;
|
|
101
111
|
});
|
|
102
112
|
|
|
103
|
-
// Track user interaction
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const showErrors = computed(() => hasInteracted.value && control.value.errors);
|
|
113
|
+
// Track user interaction — errors only show after blur
|
|
114
|
+
const { showErrors, markDirty } = useDirtyValidation(control, projectedErrors);
|
|
107
115
|
|
|
108
116
|
const onNumber = (val: number | null) => {
|
|
109
117
|
handleChange(control.value.path, val ?? undefined);
|
|
110
118
|
};
|
|
111
|
-
|
|
112
|
-
const onBlur = () => {
|
|
113
|
-
hasInteracted.value = true;
|
|
114
|
-
};
|
|
115
119
|
</script>
|
|
116
120
|
|
|
117
121
|
<template>
|
|
118
|
-
<div class="
|
|
119
|
-
<label v-if="
|
|
120
|
-
|
|
121
|
-
}}</label>
|
|
122
|
-
<div v-if="control.description" class="text-color-secondary text-left">
|
|
122
|
+
<div class="jf-control">
|
|
123
|
+
<label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
|
|
124
|
+
<div v-if="control.description" class="jf-description">
|
|
123
125
|
{{ control.description }}
|
|
124
126
|
</div>
|
|
125
127
|
<InputNumber
|
|
126
|
-
class="w-full"
|
|
127
|
-
input-class="w-full"
|
|
128
|
+
:class="['w-full!', { 'p-invalid': showErrors }]"
|
|
129
|
+
:input-class="['w-full!', { 'p-invalid': showErrors }]"
|
|
128
130
|
:use-grouping="useGrouping"
|
|
129
131
|
:mode="mode"
|
|
130
132
|
:currency="currency"
|
|
@@ -133,10 +135,10 @@ const onBlur = () => {
|
|
|
133
135
|
:model-value="typeof projectedData === 'number' ? projectedData : null"
|
|
134
136
|
:placeholder="placeholder"
|
|
135
137
|
:disabled="!control.enabled"
|
|
136
|
-
:aria-invalid="
|
|
138
|
+
:aria-invalid="showErrors || undefined"
|
|
137
139
|
@update:model-value="onNumber"
|
|
138
|
-
@blur="
|
|
140
|
+
@blur="markDirty"
|
|
139
141
|
/>
|
|
140
|
-
<small v-if="showErrors" class="p-error">{{
|
|
142
|
+
<small v-if="showErrors" class="p-error">{{ projectedErrors }}</small>
|
|
141
143
|
</div>
|
|
142
144
|
</template>
|
|
@@ -43,7 +43,9 @@ import { useJsonFormsControl } from "@jsonforms/vue";
|
|
|
43
43
|
import { computed, ref, inject, watch, getCurrentInstance } from "vue";
|
|
44
44
|
import { useProvider } from "../composables/useProvider";
|
|
45
45
|
import { useDerive } from "../composables/useDerive";
|
|
46
|
+
import { useDeriveInitialValue } from "../composables/useDeriveInitialValue";
|
|
46
47
|
import { useProjection } from "../composables/useProjection";
|
|
48
|
+
import { useDirtyValidation } from "../composables/useDirtyValidation";
|
|
47
49
|
import InputText from "primevue/inputtext";
|
|
48
50
|
import AutoComplete from "primevue/autocomplete";
|
|
49
51
|
|
|
@@ -51,7 +53,12 @@ import AutoComplete from "primevue/autocomplete";
|
|
|
51
53
|
const instance = getCurrentInstance()!;
|
|
52
54
|
const props = instance.props as unknown as ControlProps;
|
|
53
55
|
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
54
|
-
const {
|
|
56
|
+
const {
|
|
57
|
+
projectedData,
|
|
58
|
+
handleProjectedChange: handleChange,
|
|
59
|
+
projectedErrors,
|
|
60
|
+
projectedLabel,
|
|
61
|
+
} = useProjection(control, rawHandleChange);
|
|
55
62
|
|
|
56
63
|
// Provider support for autocomplete functionality
|
|
57
64
|
const binding = computed(() => {
|
|
@@ -97,7 +104,7 @@ const { items, loading, error, reload } = useProvider(binding, {
|
|
|
97
104
|
data: rootData,
|
|
98
105
|
path: control.value.path,
|
|
99
106
|
uiQuery: query.value,
|
|
100
|
-
dependsOnValues: depValues
|
|
107
|
+
dependsOnValues: depValues,
|
|
101
108
|
});
|
|
102
109
|
|
|
103
110
|
watch(query, () => {
|
|
@@ -117,11 +124,11 @@ const isAutocomplete = computed(() => !!binding.value);
|
|
|
117
124
|
// Add derive functionality
|
|
118
125
|
useDerive({ control, handleChange, data: projectedData });
|
|
119
126
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
const hasFocused = ref(false);
|
|
127
|
+
// Add deriveInitialValue — async API-based initial value seeding
|
|
128
|
+
useDeriveInitialValue({ control, handleChange });
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
// Track user interaction — errors only show after blur
|
|
131
|
+
const { showErrors, markDirty } = useDirtyValidation(control, projectedErrors);
|
|
125
132
|
|
|
126
133
|
function onInput(val: string | undefined) {
|
|
127
134
|
// Convert empty strings to undefined for proper required field validation
|
|
@@ -132,15 +139,14 @@ function onInput(val: string | undefined) {
|
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
function onBlur() {
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
markDirty();
|
|
143
|
+
// Normalize empty strings to undefined so required validation fires
|
|
144
|
+
const val = projectedData.value;
|
|
145
|
+
if (typeof val === "string" && val.trim() === "") {
|
|
146
|
+
handleChange(control.value.path, undefined);
|
|
137
147
|
}
|
|
138
148
|
}
|
|
139
149
|
|
|
140
|
-
function onFocus() {
|
|
141
|
-
hasFocused.value = true;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
150
|
// Autocomplete specific handlers
|
|
145
151
|
const onComplete = (event: { query: string }) => {
|
|
146
152
|
query.value = event.query;
|
|
@@ -153,43 +159,39 @@ const onSelect = (event: { value?: { value?: unknown } | unknown }) => {
|
|
|
153
159
|
</script>
|
|
154
160
|
|
|
155
161
|
<template>
|
|
156
|
-
<div class="
|
|
157
|
-
<label v-if="
|
|
158
|
-
|
|
159
|
-
}}</label>
|
|
160
|
-
<div v-if="control.description" class="text-color-secondary text-left">
|
|
162
|
+
<div class="jf-control">
|
|
163
|
+
<label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
|
|
164
|
+
<div v-if="control.description" class="jf-description">
|
|
161
165
|
{{ control.description }}
|
|
162
166
|
</div>
|
|
163
167
|
<AutoComplete
|
|
164
168
|
v-if="isAutocomplete"
|
|
165
|
-
class="w-full"
|
|
169
|
+
:class="['w-full!', { 'p-invalid': showErrors }]"
|
|
166
170
|
:model-value="projectedData ?? ''"
|
|
167
171
|
:suggestions="items"
|
|
168
172
|
option-label="label"
|
|
169
173
|
:placeholder="placeholder"
|
|
170
174
|
:disabled="!control.enabled"
|
|
171
|
-
:aria-invalid="
|
|
175
|
+
:aria-invalid="showErrors || undefined"
|
|
172
176
|
@complete="onComplete"
|
|
173
177
|
@item-select="onSelect"
|
|
174
178
|
@update:model-value="onInput"
|
|
175
179
|
@blur="onBlur"
|
|
176
|
-
@focus="onFocus"
|
|
177
180
|
/>
|
|
178
181
|
<InputText
|
|
179
182
|
v-else
|
|
180
|
-
class="w-full"
|
|
183
|
+
:class="['w-full!', { 'p-invalid': showErrors }]"
|
|
181
184
|
:model-value="(projectedData as string) ?? ''"
|
|
182
185
|
:disabled="!control.enabled"
|
|
183
|
-
:aria-invalid="
|
|
186
|
+
:aria-invalid="showErrors || undefined"
|
|
184
187
|
:placeholder="placeholder"
|
|
185
188
|
autocapitalize="off"
|
|
186
189
|
autocomplete="off"
|
|
187
190
|
spellcheck="false"
|
|
188
191
|
@update:model-value="onInput"
|
|
189
192
|
@blur="onBlur"
|
|
190
|
-
@focus="onFocus"
|
|
191
193
|
/>
|
|
192
194
|
<small v-if="error" class="p-error" role="alert">Failed: {{ error }}</small>
|
|
193
|
-
<small v-else-if="showErrors" class="p-error">{{
|
|
195
|
+
<small v-else-if="showErrors" class="p-error">{{ projectedErrors }}</small>
|
|
194
196
|
</div>
|
|
195
197
|
</template>
|
|
@@ -40,15 +40,21 @@ export default {
|
|
|
40
40
|
<script setup lang="ts">
|
|
41
41
|
import type { ControlProps } from "@jsonforms/vue";
|
|
42
42
|
import { useJsonFormsControl } from "@jsonforms/vue";
|
|
43
|
-
import { computed,
|
|
43
|
+
import { computed, getCurrentInstance } from "vue";
|
|
44
44
|
import { useProjection } from "../composables/useProjection";
|
|
45
|
+
import { useDirtyValidation } from "../composables/useDirtyValidation";
|
|
45
46
|
import Textarea from "primevue/textarea";
|
|
46
47
|
|
|
47
48
|
// Access props from the component instance
|
|
48
49
|
const instance = getCurrentInstance()!;
|
|
49
50
|
const props = instance.props as unknown as ControlProps;
|
|
50
51
|
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
51
|
-
const {
|
|
52
|
+
const {
|
|
53
|
+
projectedData,
|
|
54
|
+
handleProjectedChange: handleChange,
|
|
55
|
+
projectedErrors,
|
|
56
|
+
projectedLabel,
|
|
57
|
+
} = useProjection(control, rawHandleChange);
|
|
52
58
|
|
|
53
59
|
const placeholder = computed<string | undefined>(
|
|
54
60
|
() =>
|
|
@@ -56,10 +62,8 @@ const placeholder = computed<string | undefined>(
|
|
|
56
62
|
?.placeholder ?? control.value.description,
|
|
57
63
|
);
|
|
58
64
|
|
|
59
|
-
// Track user interaction
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const showErrors = computed(() => hasInteracted.value && control.value.errors);
|
|
65
|
+
// Track user interaction — errors only show after blur
|
|
66
|
+
const { showErrors, markDirty } = useDirtyValidation(control, projectedErrors);
|
|
63
67
|
|
|
64
68
|
function onInput(val: string | undefined) {
|
|
65
69
|
// Convert empty strings to undefined for proper required field validation
|
|
@@ -70,29 +74,32 @@ function onInput(val: string | undefined) {
|
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
function onBlur() {
|
|
73
|
-
|
|
77
|
+
markDirty();
|
|
78
|
+
// Normalize empty strings to undefined so required validation fires
|
|
79
|
+
const val = projectedData.value;
|
|
80
|
+
if (typeof val === "string" && val.trim() === "") {
|
|
81
|
+
handleChange(control.value.path, undefined);
|
|
82
|
+
}
|
|
74
83
|
}
|
|
75
84
|
</script>
|
|
76
85
|
|
|
77
86
|
<template>
|
|
78
|
-
<div class="
|
|
79
|
-
<label v-if="
|
|
80
|
-
|
|
81
|
-
}}</label>
|
|
82
|
-
<div v-if="control.description" class="text-color-secondary text-left">
|
|
87
|
+
<div class="jf-control">
|
|
88
|
+
<label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
|
|
89
|
+
<div v-if="control.description" class="jf-description">
|
|
83
90
|
{{ control.description }}
|
|
84
91
|
</div>
|
|
85
92
|
<Textarea
|
|
86
|
-
class="w-full"
|
|
93
|
+
:class="['w-full!', { 'p-invalid': showErrors }]"
|
|
87
94
|
:model-value="(projectedData as string) ?? ''"
|
|
88
95
|
:disabled="!control.enabled"
|
|
89
|
-
:aria-invalid="
|
|
96
|
+
:aria-invalid="showErrors || undefined"
|
|
90
97
|
:placeholder="placeholder"
|
|
91
98
|
:rows="4"
|
|
92
99
|
:auto-resize="true"
|
|
93
100
|
@update:model-value="onInput"
|
|
94
101
|
@blur="onBlur"
|
|
95
102
|
/>
|
|
96
|
-
<small v-if="showErrors" class="p-error">{{
|
|
103
|
+
<small v-if="showErrors" class="p-error">{{ projectedErrors }}</small>
|
|
97
104
|
</div>
|
|
98
105
|
</template>
|
|
@@ -19,13 +19,38 @@ const injectLayoutStyles = () => {
|
|
|
19
19
|
display: flex;
|
|
20
20
|
flex-direction: column;
|
|
21
21
|
align-items: flex-start;
|
|
22
|
-
gap:
|
|
22
|
+
gap: 24px;
|
|
23
23
|
width: 100%;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
.vertical-layout-item {
|
|
27
27
|
width: 100%;
|
|
28
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
|
+
}
|
|
29
54
|
`;
|
|
30
55
|
document.head.appendChild(style);
|
|
31
56
|
}
|
|
@@ -102,47 +127,103 @@ export function registerPrimevueRenderers(jsonformsCore: any): unknown[] {
|
|
|
102
127
|
|
|
103
128
|
// Projection-aware schema check: when options.projection is set,
|
|
104
129
|
// resolve the projected schema and test against it instead of the original
|
|
105
|
-
|
|
106
|
-
const projectedSchemaMatches =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
};
|
|
118
152
|
|
|
119
153
|
const isMultilineProjection = (uischema: unknown, schema: unknown) => {
|
|
120
154
|
const ui = uischema as { options?: { multi?: boolean } };
|
|
121
|
-
return
|
|
122
|
-
|
|
155
|
+
return (
|
|
156
|
+
ui?.options?.multi === true &&
|
|
157
|
+
projectedSchemaMatches((s) => s?.type === "string")(uischema, schema)
|
|
158
|
+
);
|
|
123
159
|
};
|
|
124
160
|
|
|
125
161
|
const renderers = [
|
|
126
162
|
// Multiline text has higher priority than regular text
|
|
127
|
-
{ tester: rankWith(PRIME + 4, or(isMultilineString, isMultilineProjection)), renderer: JfTextArea },
|
|
128
|
-
{ tester: rankWith(PRIME + 3, or(isStringControl, projectedSchemaMatches((s) => s?.type === "string"))), renderer: JfText },
|
|
129
163
|
{
|
|
130
|
-
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
|
+
),
|
|
131
185
|
renderer: JfNumber,
|
|
132
186
|
},
|
|
133
187
|
{
|
|
134
|
-
tester: rankWith(
|
|
188
|
+
tester: rankWith(
|
|
189
|
+
PRIME + 4,
|
|
190
|
+
or(
|
|
191
|
+
isNumberControl,
|
|
192
|
+
projectedSchemaMatches((s) => s?.type === "number"),
|
|
193
|
+
),
|
|
194
|
+
),
|
|
135
195
|
renderer: JfNumber,
|
|
136
196
|
},
|
|
137
197
|
{
|
|
138
|
-
tester: rankWith(
|
|
198
|
+
tester: rankWith(
|
|
199
|
+
PRIME + 7,
|
|
200
|
+
or(
|
|
201
|
+
and(isControl, schemaMatches(isScalarEnum)),
|
|
202
|
+
and(isControl, projectedSchemaMatches(isScalarEnum)),
|
|
203
|
+
),
|
|
204
|
+
),
|
|
139
205
|
renderer: JfEnum,
|
|
140
206
|
},
|
|
141
207
|
{
|
|
142
|
-
tester: rankWith(
|
|
208
|
+
tester: rankWith(
|
|
209
|
+
PRIME + 8,
|
|
210
|
+
or(
|
|
211
|
+
and(isControl, schemaMatches(isEnumArray)),
|
|
212
|
+
and(isControl, projectedSchemaMatches(isEnumArray)),
|
|
213
|
+
),
|
|
214
|
+
),
|
|
143
215
|
renderer: JfEnumArray,
|
|
144
216
|
},
|
|
145
|
-
{
|
|
217
|
+
{
|
|
218
|
+
tester: rankWith(
|
|
219
|
+
PRIME + 3,
|
|
220
|
+
or(
|
|
221
|
+
isBooleanControl,
|
|
222
|
+
projectedSchemaMatches((s) => s?.type === "boolean"),
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
renderer: JfBoolean,
|
|
226
|
+
},
|
|
146
227
|
];
|
|
147
228
|
|
|
148
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;
|
|
@@ -57,7 +57,7 @@ export interface AutoSelectMultiParams {
|
|
|
57
57
|
* - Current value is empty array OR current selection is not in the current options
|
|
58
58
|
*/
|
|
59
59
|
export function shouldAutoSelectMulti(
|
|
60
|
-
params: AutoSelectMultiParams
|
|
60
|
+
params: AutoSelectMultiParams,
|
|
61
61
|
): unknown[] | null {
|
|
62
62
|
const { autoSelectSingle, isLoading, items, currentValue } = params;
|
|
63
63
|
|
|
@@ -73,7 +73,7 @@ export function shouldAutoSelectMulti(
|
|
|
73
73
|
const currentArray = Array.isArray(currentValue) ? currentValue : [];
|
|
74
74
|
const isValueEmpty = currentArray.length === 0;
|
|
75
75
|
const hasValidSelection = currentArray.some((val) =>
|
|
76
|
-
items.some((item) => item.value === val)
|
|
76
|
+
items.some((item) => item.value === val),
|
|
77
77
|
);
|
|
78
78
|
|
|
79
79
|
if (isValueEmpty || !hasValidSelection) {
|