@narrative.io/jsonforms-provider-protocols 2.10.0 → 3.0.0-beta.2
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 +166 -30
- package/dist/core/projection.d.ts +32 -0
- package/dist/core/projection.d.ts.map +1 -0
- package/dist/core/projection.js +74 -0
- package/dist/core/projection.js.map +1 -0
- package/dist/core/resolveScope.d.ts +11 -0
- package/dist/core/resolveScope.d.ts.map +1 -0
- package/dist/core/resolveScope.js +22 -0
- package/dist/core/resolveScope.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 +56 -13
- package/dist/core/transforms.js.map +1 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- 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 +4 -2
- 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 +6 -4
- 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 +5 -3
- package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
- package/dist/vue/composables/useDataLayer.d.ts +9 -0
- package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
- package/dist/vue/composables/useDataLayer.js +25 -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 +12 -12
- package/dist/vue/composables/useDerive.js.map +1 -1
- 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 +35 -0
- package/dist/vue/composables/useProjection.d.ts.map +1 -0
- package/dist/vue/composables/useProjection.js +33 -0
- package/dist/vue/composables/useProjection.js.map +1 -0
- package/dist/vue/index.d.ts +5 -0
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +17 -29
- 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 +21 -10
- 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 +20 -18
- 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 +24 -14
- 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 +20 -18
- 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 +29 -28
- 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 +23 -14
- 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 +22 -7
- package/dist/vue/primevue/index.js.map +1 -1
- package/package.json +7 -3
- package/src/core/projection.ts +136 -0
- package/src/core/resolveScope.ts +39 -0
- package/src/core/transforms.ts +91 -26
- package/src/core/types.ts +8 -0
- package/src/index.ts +7 -0
- package/src/vue/components/ProviderAutocomplete.vue +4 -2
- package/src/vue/components/ProviderMultiSelect.vue +6 -4
- package/src/vue/components/ProviderSelect.vue +5 -3
- package/src/vue/composables/useDataLayer.ts +43 -0
- package/src/vue/composables/useDerive.ts +19 -16
- package/src/vue/composables/useDirtyValidation.ts +15 -0
- package/src/vue/composables/useProjection.ts +74 -0
- package/src/vue/index.ts +21 -46
- package/src/vue/primevue/JfBoolean.vue +18 -5
- package/src/vue/primevue/JfEnum.vue +16 -16
- package/src/vue/primevue/JfEnumArray.vue +21 -9
- package/src/vue/primevue/JfNumber.vue +16 -16
- package/src/vue/primevue/JfText.vue +22 -22
- package/src/vue/primevue/JfTextArea.vue +20 -11
- package/src/vue/primevue/index.ts +32 -7
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { computed, watch, inject, type Ref } from "vue";
|
|
1
|
+
import { computed, watch, unref, inject, type Ref, type ComputedRef } from "vue";
|
|
2
2
|
import { type ControlElement } from "@jsonforms/core";
|
|
3
|
+
import { useDataLayer } from "./useDataLayer";
|
|
3
4
|
|
|
4
5
|
interface DeriveOptions {
|
|
5
6
|
control: Ref<{
|
|
@@ -8,20 +9,21 @@ interface DeriveOptions {
|
|
|
8
9
|
data: unknown;
|
|
9
10
|
}>;
|
|
10
11
|
handleChange: (path: string, value: unknown) => void;
|
|
12
|
+
/** When projection is active, pass projectedData so the comparison
|
|
13
|
+
* matches the projected (unwrapped) value rather than raw scope data. */
|
|
14
|
+
data?: Ref<unknown> | ComputedRef<unknown>;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
export function useDerive({ control, handleChange }: DeriveOptions) {
|
|
17
|
+
export function useDerive({ control, handleChange, data: dataOverride }: DeriveOptions) {
|
|
14
18
|
// Get the root form data from JSONForms context
|
|
15
19
|
const injectedFormData = inject<{ value: unknown }>("formData", {
|
|
16
20
|
value: {},
|
|
17
21
|
});
|
|
18
22
|
const rootData = computed(() => injectedFormData.value || {});
|
|
19
23
|
|
|
20
|
-
// Get
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
});
|
|
24
|
-
const externalData = computed(() => injectedExternalData.value || {});
|
|
24
|
+
// Get data from the dataLayer
|
|
25
|
+
const dataLayerState = useDataLayer();
|
|
26
|
+
const dataLayerData = computed(() => dataLayerState.value || {});
|
|
25
27
|
|
|
26
28
|
// Extract derive configuration from uischema options
|
|
27
29
|
const deriveConfig = computed(() => {
|
|
@@ -34,9 +36,9 @@ export function useDerive({ control, handleChange }: DeriveOptions) {
|
|
|
34
36
|
};
|
|
35
37
|
});
|
|
36
38
|
|
|
37
|
-
// Watch for changes in form data and
|
|
39
|
+
// Watch for changes in form data and dataLayer and update derived field
|
|
38
40
|
watch(
|
|
39
|
-
[rootData,
|
|
41
|
+
[rootData, dataLayerData, deriveConfig],
|
|
40
42
|
([data, extData, config]) => {
|
|
41
43
|
if (!config.expression || config.mode !== "follow") {
|
|
42
44
|
return;
|
|
@@ -48,7 +50,8 @@ export function useDerive({ control, handleChange }: DeriveOptions) {
|
|
|
48
50
|
data,
|
|
49
51
|
extData,
|
|
50
52
|
);
|
|
51
|
-
|
|
53
|
+
const compareData = dataOverride ? unref(dataOverride) : control.value.data;
|
|
54
|
+
if (derivedValue !== compareData) {
|
|
52
55
|
handleChange(control.value.path, derivedValue);
|
|
53
56
|
}
|
|
54
57
|
} catch (error) {
|
|
@@ -65,12 +68,12 @@ export function useDerive({ control, handleChange }: DeriveOptions) {
|
|
|
65
68
|
function resolveDeriveExpression(
|
|
66
69
|
expression: string,
|
|
67
70
|
data: unknown,
|
|
68
|
-
|
|
71
|
+
dataLayerData?: unknown,
|
|
69
72
|
): unknown {
|
|
70
|
-
// Handle
|
|
71
|
-
if (expression.startsWith("
|
|
72
|
-
const propertyPath = expression.slice(
|
|
73
|
-
return resolvePropertyPath(propertyPath,
|
|
73
|
+
// Handle dataLayer() syntax
|
|
74
|
+
if (expression.startsWith("dataLayer(") && expression.endsWith(")")) {
|
|
75
|
+
const propertyPath = expression.slice(10, -1); // Remove "dataLayer(" and ")"
|
|
76
|
+
return resolvePropertyPath(propertyPath, dataLayerData);
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
// Handle simple property paths like "country.name"
|
|
@@ -82,7 +85,7 @@ function resolveDeriveExpression(
|
|
|
82
85
|
return resolvePropertyPath(expression, data);
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
// For now, we'll only support simple property paths and
|
|
88
|
+
// For now, we'll only support simple property paths and dataLayer() calls
|
|
86
89
|
// Complex expressions would require a safe expression evaluator
|
|
87
90
|
return resolvePropertyPath(expression, data);
|
|
88
91
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ref, computed, type Ref } from "vue";
|
|
2
|
+
|
|
3
|
+
export function useDirtyValidation(control: Ref<{ errors: string }>) {
|
|
4
|
+
const hasInteracted = ref(false);
|
|
5
|
+
|
|
6
|
+
const showErrors = computed(
|
|
7
|
+
() => hasInteracted.value && !!control.value.errors,
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const markDirty = () => {
|
|
11
|
+
hasInteracted.value = true;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return { hasInteracted, showErrors, markDirty };
|
|
15
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { computed, type ComputedRef, type Ref } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
getProjectedValue,
|
|
4
|
+
setProjectedValue,
|
|
5
|
+
getProjectedSchema,
|
|
6
|
+
} from "../../core/projection";
|
|
7
|
+
|
|
8
|
+
interface ProjectionControl {
|
|
9
|
+
data: unknown;
|
|
10
|
+
path: string;
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
schema: Record<string, any>;
|
|
13
|
+
uischema: { options?: { projection?: string; [key: string]: unknown } };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProjectionResult {
|
|
17
|
+
/** The value at the projected path (for rendering) */
|
|
18
|
+
projectedData: ComputedRef<unknown>;
|
|
19
|
+
/** The schema at the projected path (for renderer selection) */
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
projectedSchema: ComputedRef<Record<string, any>>;
|
|
22
|
+
/** Wrapped handleChange that writes through the projection */
|
|
23
|
+
handleProjectedChange: (path: string, value: unknown) => void;
|
|
24
|
+
/** Whether projection is active */
|
|
25
|
+
hasProjection: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Composable that wraps a JSON Forms control with projection support.
|
|
30
|
+
*
|
|
31
|
+
* When `options.projection` is set on the uischema, this composable:
|
|
32
|
+
* - Reads the projected sub-value from the control data
|
|
33
|
+
* - Resolves the projected sub-schema for renderer type resolution
|
|
34
|
+
* - Wraps handleChange to write back through the projection path (preserving siblings)
|
|
35
|
+
*
|
|
36
|
+
* When no projection is set, it passes through control data/schema/handleChange unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export function useProjection(
|
|
39
|
+
control: Ref<ProjectionControl>,
|
|
40
|
+
handleChange: (path: string, value: unknown) => void,
|
|
41
|
+
): ProjectionResult {
|
|
42
|
+
const projection = control.value.uischema?.options?.projection as
|
|
43
|
+
| string
|
|
44
|
+
| undefined;
|
|
45
|
+
|
|
46
|
+
if (!projection) {
|
|
47
|
+
return {
|
|
48
|
+
projectedData: computed(() => control.value.data),
|
|
49
|
+
projectedSchema: computed(() => control.value.schema),
|
|
50
|
+
handleProjectedChange: handleChange,
|
|
51
|
+
hasProjection: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const projectedData = computed(() =>
|
|
56
|
+
getProjectedValue(control.value.data, projection),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const projectedSchema = computed(() =>
|
|
60
|
+
getProjectedSchema(control.value.schema, projection),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const handleProjectedChange = (path: string, value: unknown) => {
|
|
64
|
+
const fullValue = setProjectedValue(control.value.data, projection, value);
|
|
65
|
+
handleChange(path, fullValue);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
projectedData,
|
|
70
|
+
projectedSchema,
|
|
71
|
+
handleProjectedChange,
|
|
72
|
+
hasProjection: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
package/src/vue/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
rankWith,
|
|
8
8
|
isControl,
|
|
9
9
|
} from "@jsonforms/core";
|
|
10
|
+
import { resolveScopeSchema } from "../core/resolveScope";
|
|
10
11
|
import ProviderAutocomplete from "./components/ProviderAutocomplete.vue";
|
|
11
12
|
import ProviderSelect from "./components/ProviderSelect.vue";
|
|
12
13
|
import ProviderMultiSelect from "./components/ProviderMultiSelect.vue";
|
|
@@ -16,29 +17,21 @@ const hasProvider = (uischema: UISchemaElement) => {
|
|
|
16
17
|
return uischema?.options?.provider !== undefined;
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
// Integer fallback tester — handles nested scopes like #/properties/parent/properties/child
|
|
21
|
+
const isIntegerScope = (uischema: unknown, schema: unknown) => {
|
|
22
|
+
const ui = uischema as { type?: string; scope?: string };
|
|
23
|
+
if (ui?.type !== "Control" || !ui?.scope) return false;
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
const propertySchema = resolveScopeSchema(ui.scope, schema as Record<string, any>);
|
|
27
|
+
return propertySchema?.type === "integer";
|
|
28
|
+
};
|
|
29
|
+
|
|
19
30
|
// Create specific testers for each component type
|
|
20
31
|
const providerSelectTester = rankWith(
|
|
21
32
|
106, // Higher than PrimeVue base (100) to ensure providers take precedence
|
|
22
33
|
and(
|
|
23
|
-
or(
|
|
24
|
-
isStringControl,
|
|
25
|
-
isNumberControl,
|
|
26
|
-
and(isControl, (uischema: unknown, schema: unknown) => {
|
|
27
|
-
const ui = uischema as { type?: string; scope?: string };
|
|
28
|
-
const rootSchema = schema as {
|
|
29
|
-
properties?: Record<string, { type?: string }>;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
if (ui?.type !== "Control" || !ui?.scope || !rootSchema?.properties)
|
|
33
|
-
return false;
|
|
34
|
-
|
|
35
|
-
// Extract property name from scope (e.g., "#/properties/age" -> "age")
|
|
36
|
-
const propertyName = ui.scope.replace("#/properties/", "");
|
|
37
|
-
const propertySchema = rootSchema.properties[propertyName];
|
|
38
|
-
|
|
39
|
-
return propertySchema?.type === "integer";
|
|
40
|
-
}),
|
|
41
|
-
),
|
|
34
|
+
or(isStringControl, isNumberControl, and(isControl, isIntegerScope)),
|
|
42
35
|
hasProvider,
|
|
43
36
|
(uischema) => !uischema?.options?.autocomplete,
|
|
44
37
|
),
|
|
@@ -47,44 +40,21 @@ const providerSelectTester = rankWith(
|
|
|
47
40
|
const providerAutocompleteTester = rankWith(
|
|
48
41
|
107, // Higher than PrimeVue base (100) to ensure providers take precedence
|
|
49
42
|
and(
|
|
50
|
-
or(
|
|
51
|
-
isStringControl,
|
|
52
|
-
isNumberControl,
|
|
53
|
-
and(isControl, (uischema: unknown, schema: unknown) => {
|
|
54
|
-
const ui = uischema as { type?: string; scope?: string };
|
|
55
|
-
const rootSchema = schema as {
|
|
56
|
-
properties?: Record<string, { type?: string }>;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
if (ui?.type !== "Control" || !ui?.scope || !rootSchema?.properties)
|
|
60
|
-
return false;
|
|
61
|
-
|
|
62
|
-
// Extract property name from scope (e.g., "#/properties/age" -> "age")
|
|
63
|
-
const propertyName = ui.scope.replace("#/properties/", "");
|
|
64
|
-
const propertySchema = rootSchema.properties[propertyName];
|
|
65
|
-
|
|
66
|
-
return propertySchema?.type === "integer";
|
|
67
|
-
}),
|
|
68
|
-
),
|
|
43
|
+
or(isStringControl, isNumberControl, and(isControl, isIntegerScope)),
|
|
69
44
|
hasProvider,
|
|
70
45
|
(uischema) => uischema?.options?.autocomplete === true,
|
|
71
46
|
),
|
|
72
47
|
);
|
|
73
48
|
|
|
74
|
-
// Custom array tester -
|
|
49
|
+
// Custom array tester - supports nested scope paths
|
|
75
50
|
const isArrayControl = (uischema: UISchemaElement, schema: unknown) => {
|
|
76
51
|
const controlSchema = uischema as { type: string; scope?: string };
|
|
77
52
|
if (controlSchema.type !== "Control" || !controlSchema.scope) {
|
|
78
53
|
return false;
|
|
79
54
|
}
|
|
80
55
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
const propertyPath = controlSchema.scope.replace("#/properties/", "");
|
|
84
|
-
const propertySchema = rootSchema?.properties?.[propertyPath] as {
|
|
85
|
-
type?: string;
|
|
86
|
-
};
|
|
87
|
-
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
const propertySchema = resolveScopeSchema(controlSchema.scope, schema as Record<string, any>);
|
|
88
58
|
return propertySchema?.type === "array";
|
|
89
59
|
};
|
|
90
60
|
|
|
@@ -105,6 +75,11 @@ export { primevueRenderers, registerPrimevueRenderers } from "./primevue";
|
|
|
105
75
|
// Export individual components
|
|
106
76
|
export { ProviderAutocomplete, ProviderSelect, ProviderMultiSelect };
|
|
107
77
|
export { useProvider } from "./composables/useProvider";
|
|
78
|
+
export { useProjection } from "./composables/useProjection";
|
|
79
|
+
export type { ProjectionResult } from "./composables/useProjection";
|
|
80
|
+
export { createDataLayer, useDataLayer } from "./composables/useDataLayer";
|
|
81
|
+
export type { DataLayer } from "./composables/useDataLayer";
|
|
82
|
+
export { useDirtyValidation } from "./composables/useDirtyValidation";
|
|
108
83
|
export * from "./testers";
|
|
109
84
|
|
|
110
85
|
// Export individual PrimeVue components using lazy evaluation to avoid circular deps
|
|
@@ -21,14 +21,17 @@ export default {
|
|
|
21
21
|
renderers: {
|
|
22
22
|
type: Array,
|
|
23
23
|
required: false,
|
|
24
|
+
default: undefined,
|
|
24
25
|
},
|
|
25
26
|
cells: {
|
|
26
27
|
type: Array,
|
|
27
28
|
required: false,
|
|
29
|
+
default: undefined,
|
|
28
30
|
},
|
|
29
31
|
config: {
|
|
30
32
|
type: Object,
|
|
31
33
|
required: false,
|
|
34
|
+
default: undefined,
|
|
32
35
|
},
|
|
33
36
|
},
|
|
34
37
|
};
|
|
@@ -38,26 +41,36 @@ export default {
|
|
|
38
41
|
import type { ControlProps } from "@jsonforms/vue";
|
|
39
42
|
import { useJsonFormsControl } from "@jsonforms/vue";
|
|
40
43
|
import { getCurrentInstance } from "vue";
|
|
44
|
+
import { useProjection } from "../composables/useProjection";
|
|
45
|
+
import { useDirtyValidation } from "../composables/useDirtyValidation";
|
|
41
46
|
import Checkbox from "primevue/checkbox";
|
|
42
47
|
|
|
43
48
|
// Access props from the component instance
|
|
44
49
|
const instance = getCurrentInstance()!;
|
|
45
50
|
const props = instance.props as unknown as ControlProps;
|
|
46
|
-
const { control, handleChange } = useJsonFormsControl(props);
|
|
51
|
+
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
52
|
+
const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
// Track user interaction — errors only show after first toggle
|
|
55
|
+
const { showErrors, markDirty } = useDirtyValidation(control);
|
|
56
|
+
|
|
57
|
+
const onToggle = (val: boolean) => {
|
|
58
|
+
markDirty();
|
|
59
|
+
handleChange(control.value.path, val);
|
|
60
|
+
};
|
|
49
61
|
</script>
|
|
50
62
|
|
|
51
63
|
<template>
|
|
52
64
|
<div class="flex items-center gap-2">
|
|
53
65
|
<Checkbox
|
|
54
66
|
:binary="true"
|
|
55
|
-
:model-value="!!
|
|
67
|
+
:model-value="!!projectedData"
|
|
56
68
|
:disabled="!control.enabled"
|
|
57
|
-
:
|
|
69
|
+
:class="{ 'p-invalid': showErrors }"
|
|
70
|
+
:aria-invalid="showErrors || undefined"
|
|
58
71
|
@update:model-value="onToggle"
|
|
59
72
|
/>
|
|
60
73
|
<label v-if="control.label">{{ control.label }}</label>
|
|
61
|
-
<small v-if="
|
|
74
|
+
<small v-if="showErrors" class="p-error">{{ control.errors }}</small>
|
|
62
75
|
</div>
|
|
63
76
|
</template>
|
|
@@ -22,14 +22,17 @@ export default {
|
|
|
22
22
|
renderers: {
|
|
23
23
|
type: Array,
|
|
24
24
|
required: false,
|
|
25
|
+
default: undefined,
|
|
25
26
|
},
|
|
26
27
|
cells: {
|
|
27
28
|
type: Array,
|
|
28
29
|
required: false,
|
|
30
|
+
default: undefined,
|
|
29
31
|
},
|
|
30
32
|
config: {
|
|
31
33
|
type: Object,
|
|
32
34
|
required: false,
|
|
35
|
+
default: undefined,
|
|
33
36
|
},
|
|
34
37
|
},
|
|
35
38
|
};
|
|
@@ -39,16 +42,19 @@ export default {
|
|
|
39
42
|
import type { JsonSchema } from "@jsonforms/core";
|
|
40
43
|
import type { ControlProps } from "@jsonforms/vue";
|
|
41
44
|
import { useJsonFormsControl } from "@jsonforms/vue";
|
|
42
|
-
import { computed,
|
|
45
|
+
import { computed, inject, getCurrentInstance, watch } from "vue";
|
|
43
46
|
import { useProvider } from "../composables/useProvider";
|
|
44
47
|
import { useDerive } from "../composables/useDerive";
|
|
48
|
+
import { useProjection } from "../composables/useProjection";
|
|
49
|
+
import { useDirtyValidation } from "../composables/useDirtyValidation";
|
|
45
50
|
import { shouldAutoSelect } from "../utils/autoSelect";
|
|
46
51
|
import Dropdown from "primevue/dropdown";
|
|
47
52
|
|
|
48
53
|
// Access props from the component instance
|
|
49
54
|
const instance = getCurrentInstance()!;
|
|
50
55
|
const props = instance.props as unknown as ControlProps;
|
|
51
|
-
const { control, handleChange } = useJsonFormsControl(props);
|
|
56
|
+
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
57
|
+
const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
|
|
52
58
|
|
|
53
59
|
type Opt = { label: string; value: unknown };
|
|
54
60
|
const toOptions = (schema?: JsonSchema): Opt[] => {
|
|
@@ -134,7 +140,7 @@ const options = computed(() => {
|
|
|
134
140
|
});
|
|
135
141
|
|
|
136
142
|
// Add derive functionality
|
|
137
|
-
useDerive({ control, handleChange });
|
|
143
|
+
useDerive({ control, handleChange, data: projectedData });
|
|
138
144
|
|
|
139
145
|
// Auto-select when provider returns only one item (enabled by default)
|
|
140
146
|
watch(
|
|
@@ -145,7 +151,7 @@ watch(
|
|
|
145
151
|
control.value.uischema?.options?.autoSelectSingle !== false,
|
|
146
152
|
isLoading,
|
|
147
153
|
items,
|
|
148
|
-
currentValue:
|
|
154
|
+
currentValue: projectedData.value,
|
|
149
155
|
});
|
|
150
156
|
|
|
151
157
|
if (valueToSelect !== null) {
|
|
@@ -155,18 +161,12 @@ watch(
|
|
|
155
161
|
{ immediate: true }
|
|
156
162
|
);
|
|
157
163
|
|
|
158
|
-
// Track user interaction
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
const showErrors = computed(() => hasInteracted.value && control.value.errors);
|
|
164
|
+
// Track user interaction — errors only show after blur
|
|
165
|
+
const { showErrors, markDirty } = useDirtyValidation(control);
|
|
162
166
|
|
|
163
167
|
const onSelect = (val: unknown) => {
|
|
164
168
|
handleChange(control.value.path, val);
|
|
165
169
|
};
|
|
166
|
-
|
|
167
|
-
const onBlur = () => {
|
|
168
|
-
hasInteracted.value = true;
|
|
169
|
-
};
|
|
170
170
|
</script>
|
|
171
171
|
|
|
172
172
|
<template>
|
|
@@ -178,17 +178,17 @@ const onBlur = () => {
|
|
|
178
178
|
{{ control.description }}
|
|
179
179
|
</div>
|
|
180
180
|
<Dropdown
|
|
181
|
-
class="w-full"
|
|
181
|
+
:class="['w-full', { 'p-invalid': showErrors }]"
|
|
182
182
|
:options="options"
|
|
183
183
|
option-label="label"
|
|
184
184
|
option-value="value"
|
|
185
|
-
:model-value="
|
|
185
|
+
:model-value="projectedData ?? null"
|
|
186
186
|
:placeholder="placeholder"
|
|
187
187
|
:disabled="!control.enabled || loading"
|
|
188
|
-
:aria-invalid="
|
|
188
|
+
:aria-invalid="showErrors || undefined"
|
|
189
189
|
:show-clear="true"
|
|
190
190
|
@update:model-value="onSelect"
|
|
191
|
-
@blur="
|
|
191
|
+
@blur="markDirty"
|
|
192
192
|
/>
|
|
193
193
|
<small v-if="error" class="p-error" role="alert"
|
|
194
194
|
>Failed to load: {{ error }}</small
|
|
@@ -21,14 +21,17 @@ export default {
|
|
|
21
21
|
renderers: {
|
|
22
22
|
type: Array,
|
|
23
23
|
required: false,
|
|
24
|
+
default: undefined,
|
|
24
25
|
},
|
|
25
26
|
cells: {
|
|
26
27
|
type: Array,
|
|
27
28
|
required: false,
|
|
29
|
+
default: undefined,
|
|
28
30
|
},
|
|
29
31
|
config: {
|
|
30
32
|
type: Object,
|
|
31
33
|
required: false,
|
|
34
|
+
default: undefined,
|
|
32
35
|
},
|
|
33
36
|
},
|
|
34
37
|
};
|
|
@@ -41,13 +44,16 @@ import { useJsonFormsControl } from "@jsonforms/vue";
|
|
|
41
44
|
import { computed, inject, getCurrentInstance, watch } from "vue";
|
|
42
45
|
import { useProvider } from "../composables/useProvider";
|
|
43
46
|
import { useDerive } from "../composables/useDerive";
|
|
47
|
+
import { useProjection } from "../composables/useProjection";
|
|
48
|
+
import { useDirtyValidation } from "../composables/useDirtyValidation";
|
|
44
49
|
import { shouldAutoSelectMulti } from "../utils/autoSelect";
|
|
45
50
|
import MultiSelect from "primevue/multiselect";
|
|
46
51
|
|
|
47
52
|
// Access props from the component instance
|
|
48
53
|
const instance = getCurrentInstance()!;
|
|
49
54
|
const props = instance.props as unknown as ControlProps;
|
|
50
|
-
const { control, handleChange } = useJsonFormsControl(props);
|
|
55
|
+
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
56
|
+
const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
|
|
51
57
|
|
|
52
58
|
type Opt = { label: string; value: unknown };
|
|
53
59
|
const toOptions = (schema?: JsonSchema): Opt[] => {
|
|
@@ -125,7 +131,10 @@ const options = computed(() => {
|
|
|
125
131
|
});
|
|
126
132
|
|
|
127
133
|
// Add derive functionality
|
|
128
|
-
useDerive({ control, handleChange });
|
|
134
|
+
useDerive({ control, handleChange, data: projectedData });
|
|
135
|
+
|
|
136
|
+
// Track user interaction — errors only show after first change
|
|
137
|
+
const { showErrors, markDirty } = useDirtyValidation(control);
|
|
129
138
|
|
|
130
139
|
// Auto-select when provider returns only one item (opt-in for multiselect)
|
|
131
140
|
watch(
|
|
@@ -136,7 +145,7 @@ watch(
|
|
|
136
145
|
control.value.uischema?.options?.autoSelectSingle === true,
|
|
137
146
|
isLoading,
|
|
138
147
|
items,
|
|
139
|
-
currentValue: Array.isArray(
|
|
148
|
+
currentValue: Array.isArray(projectedData.value) ? projectedData.value : [],
|
|
140
149
|
});
|
|
141
150
|
|
|
142
151
|
if (valueToSelect !== null) {
|
|
@@ -165,14 +174,17 @@ const sameSet = (a: unknown[], b: unknown[]) => {
|
|
|
165
174
|
// v-model with guard to avoid recursive updates
|
|
166
175
|
const model = computed<unknown[]>({
|
|
167
176
|
get() {
|
|
168
|
-
const curr = Array.isArray(
|
|
177
|
+
const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
|
|
169
178
|
// return a fresh copy so PrimeMultiSelect can't mutate JSONForms' array in place
|
|
170
179
|
return [...curr];
|
|
171
180
|
},
|
|
172
181
|
set(val) {
|
|
173
182
|
const next = Array.isArray(val) ? [...val] : [];
|
|
174
|
-
const curr = Array.isArray(
|
|
175
|
-
if (!sameSet(curr, next))
|
|
183
|
+
const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
|
|
184
|
+
if (!sameSet(curr, next)) {
|
|
185
|
+
markDirty();
|
|
186
|
+
handleChange(control.value.path, next);
|
|
187
|
+
}
|
|
176
188
|
},
|
|
177
189
|
});
|
|
178
190
|
</script>
|
|
@@ -188,21 +200,21 @@ const model = computed<unknown[]>({
|
|
|
188
200
|
|
|
189
201
|
<MultiSelect
|
|
190
202
|
v-model="model"
|
|
191
|
-
class="w-full"
|
|
203
|
+
:class="['w-full', { 'p-invalid': showErrors }]"
|
|
192
204
|
:options="options"
|
|
193
205
|
option-label="label"
|
|
194
206
|
option-value="value"
|
|
195
207
|
data-key="value"
|
|
196
208
|
display="chip"
|
|
197
209
|
:disabled="!control.enabled || loading"
|
|
198
|
-
:aria-invalid="
|
|
210
|
+
:aria-invalid="showErrors || undefined"
|
|
199
211
|
:placeholder="placeholder"
|
|
200
212
|
/>
|
|
201
213
|
|
|
202
214
|
<small v-if="error" class="p-error" role="alert"
|
|
203
215
|
>Failed to load: {{ error }}</small
|
|
204
216
|
>
|
|
205
|
-
<small v-else-if="
|
|
217
|
+
<small v-else-if="showErrors" class="p-error">{{
|
|
206
218
|
control.errors
|
|
207
219
|
}}</small>
|
|
208
220
|
</div>
|
|
@@ -21,14 +21,17 @@ export default {
|
|
|
21
21
|
renderers: {
|
|
22
22
|
type: Array,
|
|
23
23
|
required: false,
|
|
24
|
+
default: undefined,
|
|
24
25
|
},
|
|
25
26
|
cells: {
|
|
26
27
|
type: Array,
|
|
27
28
|
required: false,
|
|
29
|
+
default: undefined,
|
|
28
30
|
},
|
|
29
31
|
config: {
|
|
30
32
|
type: Object,
|
|
31
33
|
required: false,
|
|
34
|
+
default: undefined,
|
|
32
35
|
},
|
|
33
36
|
},
|
|
34
37
|
};
|
|
@@ -37,14 +40,17 @@ export default {
|
|
|
37
40
|
<script setup lang="ts">
|
|
38
41
|
import type { ControlProps } from "@jsonforms/vue";
|
|
39
42
|
import { useJsonFormsControl } from "@jsonforms/vue";
|
|
40
|
-
import { computed,
|
|
43
|
+
import { computed, getCurrentInstance } from "vue";
|
|
41
44
|
import { useDerive } from "../composables/useDerive";
|
|
45
|
+
import { useProjection } from "../composables/useProjection";
|
|
46
|
+
import { useDirtyValidation } from "../composables/useDirtyValidation";
|
|
42
47
|
import InputNumber from "primevue/inputnumber";
|
|
43
48
|
|
|
44
49
|
// Access props from the component instance
|
|
45
50
|
const instance = getCurrentInstance()!;
|
|
46
51
|
const props = instance.props as unknown as ControlProps;
|
|
47
|
-
const { control, handleChange } = useJsonFormsControl(props);
|
|
52
|
+
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
53
|
+
const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
|
|
48
54
|
|
|
49
55
|
const options = computed(
|
|
50
56
|
() =>
|
|
@@ -57,7 +63,7 @@ const placeholder = computed<string | undefined>(
|
|
|
57
63
|
);
|
|
58
64
|
|
|
59
65
|
// Add derive functionality
|
|
60
|
-
useDerive({ control, handleChange });
|
|
66
|
+
useDerive({ control, handleChange, data: projectedData });
|
|
61
67
|
|
|
62
68
|
// Currency and decimal configuration
|
|
63
69
|
const mode = computed(() => {
|
|
@@ -95,18 +101,12 @@ const useGrouping = computed(() => {
|
|
|
95
101
|
return options.value.useGrouping === true;
|
|
96
102
|
});
|
|
97
103
|
|
|
98
|
-
// Track user interaction
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
const showErrors = computed(() => hasInteracted.value && control.value.errors);
|
|
104
|
+
// Track user interaction — errors only show after blur
|
|
105
|
+
const { showErrors, markDirty } = useDirtyValidation(control);
|
|
102
106
|
|
|
103
107
|
const onNumber = (val: number | null) => {
|
|
104
108
|
handleChange(control.value.path, val ?? undefined);
|
|
105
109
|
};
|
|
106
|
-
|
|
107
|
-
const onBlur = () => {
|
|
108
|
-
hasInteracted.value = true;
|
|
109
|
-
};
|
|
110
110
|
</script>
|
|
111
111
|
|
|
112
112
|
<template>
|
|
@@ -118,19 +118,19 @@ const onBlur = () => {
|
|
|
118
118
|
{{ control.description }}
|
|
119
119
|
</div>
|
|
120
120
|
<InputNumber
|
|
121
|
-
class="w-full"
|
|
122
|
-
input-class="w-full"
|
|
121
|
+
:class="['w-full', { 'p-invalid': showErrors }]"
|
|
122
|
+
:input-class="['w-full', { 'p-invalid': showErrors }]"
|
|
123
123
|
:use-grouping="useGrouping"
|
|
124
124
|
:mode="mode"
|
|
125
125
|
:currency="currency"
|
|
126
126
|
:min-fraction-digits="minFractionDigits"
|
|
127
127
|
:max-fraction-digits="maxFractionDigits"
|
|
128
|
-
:model-value="typeof
|
|
128
|
+
:model-value="typeof projectedData === 'number' ? projectedData : null"
|
|
129
129
|
:placeholder="placeholder"
|
|
130
130
|
:disabled="!control.enabled"
|
|
131
|
-
:aria-invalid="
|
|
131
|
+
:aria-invalid="showErrors || undefined"
|
|
132
132
|
@update:model-value="onNumber"
|
|
133
|
-
@blur="
|
|
133
|
+
@blur="markDirty"
|
|
134
134
|
/>
|
|
135
135
|
<small v-if="showErrors" class="p-error">{{ control.errors }}</small>
|
|
136
136
|
</div>
|