Extract functions
This commit is contained in:
@@ -3,6 +3,8 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import JsonEditor from "./JsonEditor";
|
import JsonEditor from "./JsonEditor";
|
||||||
|
import { updateValueAtPath, JsonObject } from "@/utils/jsonPathUtils";
|
||||||
|
import { generateDefaultValue, formatFieldLabel } from "@/utils/schemaUtils";
|
||||||
|
|
||||||
export type JsonValue =
|
export type JsonValue =
|
||||||
| string
|
| string
|
||||||
@@ -19,8 +21,6 @@ export type JsonSchemaType = {
|
|||||||
items?: JsonSchemaType;
|
items?: JsonSchemaType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type JsonObject = { [key: string]: JsonValue };
|
|
||||||
|
|
||||||
interface DynamicJsonFormProps {
|
interface DynamicJsonFormProps {
|
||||||
schema: JsonSchemaType;
|
schema: JsonSchemaType;
|
||||||
value: JsonValue;
|
value: JsonValue;
|
||||||
@@ -28,12 +28,6 @@ interface DynamicJsonFormProps {
|
|||||||
maxDepth?: number;
|
maxDepth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatFieldLabel = (key: string): string => {
|
|
||||||
return key
|
|
||||||
.replace(/([A-Z])/g, " $1") // Insert space before capital letters
|
|
||||||
.replace(/_/g, " ") // Replace underscores with spaces
|
|
||||||
.replace(/^\w/, (c) => c.toUpperCase()); // Capitalize first letter
|
|
||||||
};
|
|
||||||
|
|
||||||
const DynamicJsonForm = ({
|
const DynamicJsonForm = ({
|
||||||
schema,
|
schema,
|
||||||
@@ -41,30 +35,6 @@ const DynamicJsonForm = ({
|
|||||||
onChange,
|
onChange,
|
||||||
maxDepth = 3,
|
maxDepth = 3,
|
||||||
}: DynamicJsonFormProps) => {
|
}: DynamicJsonFormProps) => {
|
||||||
const generateDefaultValue = (propSchema: JsonSchemaType): JsonValue => {
|
|
||||||
switch (propSchema.type) {
|
|
||||||
case "string":
|
|
||||||
return "";
|
|
||||||
case "number":
|
|
||||||
case "integer":
|
|
||||||
return 0;
|
|
||||||
case "boolean":
|
|
||||||
return false;
|
|
||||||
case "array":
|
|
||||||
return [];
|
|
||||||
case "object": {
|
|
||||||
const obj: JsonObject = {};
|
|
||||||
if (propSchema.properties) {
|
|
||||||
Object.entries(propSchema.properties).forEach(([key, prop]) => {
|
|
||||||
obj[key] = generateDefaultValue(prop);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [isJsonMode, setIsJsonMode] = useState(false);
|
const [isJsonMode, setIsJsonMode] = useState(false);
|
||||||
const [jsonError, setJsonError] = useState<string>();
|
const [jsonError, setJsonError] = useState<string>();
|
||||||
@@ -272,106 +242,8 @@ const DynamicJsonForm = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateArray = (
|
|
||||||
array: JsonValue[],
|
|
||||||
path: string[],
|
|
||||||
value: JsonValue,
|
|
||||||
): JsonValue[] => {
|
|
||||||
const [index, ...restPath] = path;
|
|
||||||
const arrayIndex = Number(index);
|
|
||||||
|
|
||||||
// Validate array index
|
|
||||||
if (isNaN(arrayIndex)) {
|
|
||||||
console.error(`Invalid array index: ${index}`);
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check array bounds
|
|
||||||
if (arrayIndex < 0) {
|
|
||||||
console.error(`Array index out of bounds: ${arrayIndex} < 0`);
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newArray = [...array];
|
|
||||||
|
|
||||||
if (restPath.length === 0) {
|
|
||||||
newArray[arrayIndex] = value;
|
|
||||||
} else {
|
|
||||||
// Ensure index position exists
|
|
||||||
if (arrayIndex >= array.length) {
|
|
||||||
console.warn(`Extending array to index ${arrayIndex}`);
|
|
||||||
newArray.length = arrayIndex + 1;
|
|
||||||
newArray.fill(null, array.length, arrayIndex);
|
|
||||||
}
|
|
||||||
newArray[arrayIndex] = updateValue(
|
|
||||||
newArray[arrayIndex],
|
|
||||||
restPath,
|
|
||||||
value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return newArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateObject = (
|
|
||||||
obj: JsonObject,
|
|
||||||
path: string[],
|
|
||||||
value: JsonValue,
|
|
||||||
): JsonObject => {
|
|
||||||
const [key, ...restPath] = path;
|
|
||||||
|
|
||||||
// Validate object key
|
|
||||||
if (typeof key !== "string") {
|
|
||||||
console.error(`Invalid object key: ${key}`);
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newObj = { ...obj };
|
|
||||||
|
|
||||||
if (restPath.length === 0) {
|
|
||||||
newObj[key] = value;
|
|
||||||
} else {
|
|
||||||
// Ensure key exists
|
|
||||||
if (!(key in newObj)) {
|
|
||||||
console.warn(`Creating new key in object: ${key}`);
|
|
||||||
newObj[key] = {};
|
|
||||||
}
|
|
||||||
newObj[key] = updateValue(newObj[key], restPath, value);
|
|
||||||
}
|
|
||||||
return newObj;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateValue = (
|
|
||||||
current: JsonValue,
|
|
||||||
path: string[],
|
|
||||||
value: JsonValue,
|
|
||||||
): JsonValue => {
|
|
||||||
if (path.length === 0) return value;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!current) {
|
const newValue = updateValueAtPath(value, path, fieldValue);
|
||||||
current = !isNaN(Number(path[0])) ? [] : {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type checking
|
|
||||||
if (Array.isArray(current)) {
|
|
||||||
return updateArray(current, path, value);
|
|
||||||
} else if (typeof current === "object" && current !== null) {
|
|
||||||
return updateObject(current, path, value);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
`Cannot update path ${path.join(".")} in non-object/array value:`,
|
|
||||||
current,
|
|
||||||
);
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error updating value at path ${path.join(".")}:`, error);
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newValue = updateValue(value, path, fieldValue);
|
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update form value:", error);
|
console.error("Failed to update form value:", error);
|
||||||
|
|||||||
152
client/src/utils/jsonPathUtils.ts
Normal file
152
client/src/utils/jsonPathUtils.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { JsonValue } from "../components/DynamicJsonForm";
|
||||||
|
|
||||||
|
export type JsonObject = { [key: string]: JsonValue };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a value at a specific path in a nested JSON structure
|
||||||
|
* @param obj The original JSON value
|
||||||
|
* @param path Array of keys/indices representing the path to the value
|
||||||
|
* @param value The new value to set
|
||||||
|
* @returns A new JSON value with the updated path
|
||||||
|
*/
|
||||||
|
export function updateValueAtPath(
|
||||||
|
obj: JsonValue,
|
||||||
|
path: string[],
|
||||||
|
value: JsonValue
|
||||||
|
): JsonValue {
|
||||||
|
if (path.length === 0) return value;
|
||||||
|
|
||||||
|
// Initialize if null/undefined
|
||||||
|
if (obj === null || obj === undefined) {
|
||||||
|
obj = !isNaN(Number(path[0])) ? [] : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle arrays
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return updateArray(obj, path, value);
|
||||||
|
}
|
||||||
|
// Handle objects
|
||||||
|
else if (typeof obj === "object" && obj !== null) {
|
||||||
|
return updateObject(obj as JsonObject, path, value);
|
||||||
|
}
|
||||||
|
// Cannot update primitives
|
||||||
|
else {
|
||||||
|
console.error(
|
||||||
|
`Cannot update path ${path.join(".")} in non-object/array value:`,
|
||||||
|
obj
|
||||||
|
);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an array at a specific path
|
||||||
|
*/
|
||||||
|
function updateArray(
|
||||||
|
array: JsonValue[],
|
||||||
|
path: string[],
|
||||||
|
value: JsonValue
|
||||||
|
): JsonValue[] {
|
||||||
|
const [index, ...restPath] = path;
|
||||||
|
const arrayIndex = Number(index);
|
||||||
|
|
||||||
|
// Validate array index
|
||||||
|
if (isNaN(arrayIndex)) {
|
||||||
|
console.error(`Invalid array index: ${index}`);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check array bounds
|
||||||
|
if (arrayIndex < 0) {
|
||||||
|
console.error(`Array index out of bounds: ${arrayIndex} < 0`);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newArray = [...array];
|
||||||
|
|
||||||
|
if (restPath.length === 0) {
|
||||||
|
newArray[arrayIndex] = value;
|
||||||
|
} else {
|
||||||
|
// Ensure index position exists
|
||||||
|
if (arrayIndex >= array.length) {
|
||||||
|
console.warn(`Extending array to index ${arrayIndex}`);
|
||||||
|
newArray.length = arrayIndex + 1;
|
||||||
|
newArray.fill(null, array.length, arrayIndex);
|
||||||
|
}
|
||||||
|
newArray[arrayIndex] = updateValueAtPath(
|
||||||
|
newArray[arrayIndex],
|
||||||
|
restPath,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return newArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an object at a specific path
|
||||||
|
*/
|
||||||
|
function updateObject(
|
||||||
|
obj: JsonObject,
|
||||||
|
path: string[],
|
||||||
|
value: JsonValue
|
||||||
|
): JsonObject {
|
||||||
|
const [key, ...restPath] = path;
|
||||||
|
|
||||||
|
// Validate object key
|
||||||
|
if (typeof key !== "string") {
|
||||||
|
console.error(`Invalid object key: ${key}`);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newObj = { ...obj };
|
||||||
|
|
||||||
|
if (restPath.length === 0) {
|
||||||
|
newObj[key] = value;
|
||||||
|
} else {
|
||||||
|
// Ensure key exists
|
||||||
|
if (!(key in newObj)) {
|
||||||
|
console.warn(`Creating new key in object: ${key}`);
|
||||||
|
newObj[key] = {};
|
||||||
|
}
|
||||||
|
newObj[key] = updateValueAtPath(newObj[key], restPath, value);
|
||||||
|
}
|
||||||
|
return newObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a value at a specific path in a nested JSON structure
|
||||||
|
* @param obj The JSON value to traverse
|
||||||
|
* @param path Array of keys/indices representing the path to the value
|
||||||
|
* @param defaultValue Value to return if path doesn't exist
|
||||||
|
* @returns The value at the path, or defaultValue if not found
|
||||||
|
*/
|
||||||
|
export function getValueAtPath(
|
||||||
|
obj: JsonValue,
|
||||||
|
path: string[],
|
||||||
|
defaultValue: JsonValue = null
|
||||||
|
): JsonValue {
|
||||||
|
if (path.length === 0) return obj;
|
||||||
|
|
||||||
|
const [first, ...rest] = path;
|
||||||
|
|
||||||
|
if (obj === null || obj === undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
const index = Number(first);
|
||||||
|
if (isNaN(index) || index < 0 || index >= obj.length) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return getValueAtPath(obj[index], rest, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === "object" && obj !== null) {
|
||||||
|
if (!(first in obj)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return getValueAtPath((obj as JsonObject)[first], rest, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
72
client/src/utils/schemaUtils.ts
Normal file
72
client/src/utils/schemaUtils.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { JsonValue, JsonSchemaType } from "../components/DynamicJsonForm";
|
||||||
|
import { JsonObject } from "./jsonPathUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a default value based on a JSON schema type
|
||||||
|
* @param schema The JSON schema definition
|
||||||
|
* @returns A default value matching the schema type
|
||||||
|
*/
|
||||||
|
export function generateDefaultValue(schema: JsonSchemaType): JsonValue {
|
||||||
|
switch (schema.type) {
|
||||||
|
case "string":
|
||||||
|
return "";
|
||||||
|
case "number":
|
||||||
|
case "integer":
|
||||||
|
return 0;
|
||||||
|
case "boolean":
|
||||||
|
return false;
|
||||||
|
case "array":
|
||||||
|
return [];
|
||||||
|
case "object": {
|
||||||
|
const obj: JsonObject = {};
|
||||||
|
if (schema.properties) {
|
||||||
|
Object.entries(schema.properties).forEach(([key, prop]) => {
|
||||||
|
obj[key] = generateDefaultValue(prop);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a field key into a human-readable label
|
||||||
|
* @param key The field key to format
|
||||||
|
* @returns A formatted label string
|
||||||
|
*/
|
||||||
|
export function formatFieldLabel(key: string): string {
|
||||||
|
return key
|
||||||
|
.replace(/([A-Z])/g, " $1") // Insert space before capital letters
|
||||||
|
.replace(/_/g, " ") // Replace underscores with spaces
|
||||||
|
.replace(/^\w/, (c) => c.toUpperCase()); // Capitalize first letter
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a value conforms to a JSON schema
|
||||||
|
* @param value The value to validate
|
||||||
|
* @param schema The JSON schema to validate against
|
||||||
|
* @returns True if valid, false otherwise
|
||||||
|
*/
|
||||||
|
export function validateValueAgainstSchema(
|
||||||
|
value: JsonValue,
|
||||||
|
schema: JsonSchemaType
|
||||||
|
): boolean {
|
||||||
|
// Basic type validation
|
||||||
|
switch (schema.type) {
|
||||||
|
case "string":
|
||||||
|
return typeof value === "string";
|
||||||
|
case "number":
|
||||||
|
case "integer":
|
||||||
|
return typeof value === "number";
|
||||||
|
case "boolean":
|
||||||
|
return typeof value === "boolean";
|
||||||
|
case "array":
|
||||||
|
return Array.isArray(value);
|
||||||
|
case "object":
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user