diff --git a/demo/pages/Index.tsx b/demo/pages/Index.tsx index 7e7348b..84289dd 100644 --- a/demo/pages/Index.tsx +++ b/demo/pages/Index.tsx @@ -157,6 +157,7 @@ const Index = () => { Ukrainian Spanish Chinese + Polish diff --git a/package.json b/package.json index 570b22b..c88a0a8 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "preview": "rsbuild preview", "build": "rslib build", "build:demo": "rsbuild build", - "build:dev": "rslib build --mode development", + "build:dev": "rslib build --env-mode development", "lint": "biome lint .", "format": "biome format . --write", "check": "biome check .", diff --git a/src/components/SchemaEditor/AddFieldButton.tsx b/src/components/SchemaEditor/AddFieldButton.tsx index 1366ad2..062fe8b 100644 --- a/src/components/SchemaEditor/AddFieldButton.tsx +++ b/src/components/SchemaEditor/AddFieldButton.tsx @@ -1,5 +1,5 @@ import { CirclePlus, HelpCircle, Info } from "lucide-react"; -import { type FC, type FormEvent, useId, useState } from "react"; +import { type FC, type FormEvent, useId, useRef, useState } from "react"; import { Badge } from "../../components/ui/badge.tsx"; import { Button } from "../../components/ui/button.tsx"; import { @@ -18,11 +18,13 @@ import { TooltipTrigger, } from "../../components/ui/tooltip.tsx"; import { useTranslation } from "../../hooks/use-translation.ts"; +import { validateRegexPattern } from "../../lib/schemaEditor.ts"; import type { NewField, SchemaType } from "../../types/jsonSchema.ts"; +import { ButtonToggle } from "../ui/button-toggle.tsx"; import SchemaTypeSelector from "./SchemaTypeSelector.tsx"; interface AddFieldButtonProps { - onAddField: (field: NewField) => void; + onAddField: (field: NewField, isProperty: boolean) => void; variant?: "primary" | "secondary"; } @@ -35,29 +37,41 @@ const AddFieldButton: FC = ({ const [fieldType, setFieldType] = useState("string"); const [fieldDesc, setFieldDesc] = useState(""); const [fieldRequired, setFieldRequired] = useState(false); + const [isProperty, setProperty] = useState(false); + const [additionalProperties, setAdditionalProperties] = useState(true); const fieldNameId = useId(); const fieldDescId = useId(); const fieldRequiredId = useId(); const fieldTypeId = useId(); + const additionalPropertiesId = useId(); + const fieldInputRef = useRef(null); const t = useTranslation(); const handleSubmit = (e: FormEvent) => { e.preventDefault(); + if (!fieldName.trim()) return; - onAddField({ - name: fieldName, - type: fieldType, - description: fieldDesc, - required: fieldRequired, - }); + onAddField( + { + name: fieldName, + type: fieldType, + description: fieldDesc, + required: fieldRequired, + additionalProperties: + fieldType === "object" ? additionalProperties : undefined, + }, + isProperty, + ); setFieldName(""); setFieldType("string"); setFieldDesc(""); setFieldRequired(false); setDialogOpen(false); + setProperty(false); + setAdditionalProperties(true); }; return ( @@ -111,14 +125,52 @@ const AddFieldButton: FC = ({ + {/* Properties toggle */} + { + setProperty(!isProperty); + + // Reset required for properties, as they cannot be required + if (fieldRequired && !isProperty) { + setFieldRequired(false); + } + + // reset field name + setFieldName(""); + }} + className={ + isProperty + ? "bg-emerald-50 text-emerald-600" + : "bg-secondary text-muted-foreground" + } + > + {isProperty + ? t.patternPropertiesTitle + : t.regularPropertiesTitle} + setFieldName(e.target.value)} - placeholder={t.fieldNamePlaceholder} + placeholder={ + isProperty + ? t.patternPropertyNamePlaceholder + : t.fieldNamePlaceholder + } className="font-mono text-sm w-full" + validate={ + isProperty + ? (value) => { + const { valid, error } = + validateRegexPattern(value); + + return valid ? null : error; + } + : undefined + } required + ref={fieldInputRef} /> @@ -149,19 +201,46 @@ const AddFieldButton: FC = ({ className="text-sm w-full" /> - -
- setFieldRequired(e.target.checked)} - className="rounded border-gray-300 shrink-0" - /> - -
+ {isProperty ? null : ( +
+ setFieldRequired(e.target.checked)} + className="rounded border-gray-300 shrink-0" + /> + +
+ )} + {fieldType === "object" ? ( +
+ + setAdditionalProperties(e.target.checked) + } + className="rounded border-gray-300 shrink-0" + /> + + + + + + + +

{t.additionalPropertiesTooltip}

+
+
+
+
+ ) : null}
diff --git a/src/components/SchemaEditor/SchemaFieldList.tsx b/src/components/SchemaEditor/SchemaFieldList.tsx index 518d2d2..b3d6163 100644 --- a/src/components/SchemaEditor/SchemaFieldList.tsx +++ b/src/components/SchemaEditor/SchemaFieldList.tsx @@ -13,21 +13,28 @@ import SchemaPropertyEditor from "./SchemaPropertyEditor.tsx"; interface SchemaFieldListProps { schema: JSONSchemaType; readOnly: boolean; - onAddField: (newField: NewField) => void; - onEditField: (name: string, updatedField: NewField) => void; - onDeleteField: (name: string) => void; + onAddField: (newField: NewField, isPatternProperty?: boolean) => void; + onEditField: ( + name: string, + updatedField: NewField, + isPatternProperty?: boolean, + ) => void; + onDeleteField: (name: string, isPatternProperty?: boolean) => void; + onPropertyToggle: (name: string, isPatternProperty?: boolean) => void; } const SchemaFieldList: FC = ({ schema, onEditField, onDeleteField, + onPropertyToggle, readOnly = false, }) => { const t = useTranslation(); // Get the properties from the schema const properties = getSchemaProperties(schema); + const patternProperties = getSchemaProperties(schema, true); // Get schema type as a valid SchemaType const getValidSchemaType = (propSchema: JSONSchemaType): SchemaType => { @@ -43,23 +50,33 @@ const SchemaFieldList: FC = ({ }; // Handle field name change (generates an edit event) - const handleNameChange = (oldName: string, newName: string) => { - const property = properties.find((prop) => prop.name === oldName); + const handleNameChange = ( + oldName: string, + newName: string, + isPatternProperty = false, + ) => { + const schemaProperties = isPatternProperty ? patternProperties : properties; + const property = schemaProperties.find((prop) => prop.name === oldName); + if (!property) return; - onEditField(oldName, { - name: newName, - type: getValidSchemaType(property.schema), - description: - typeof property.schema === "boolean" - ? "" - : property.schema.description || "", - required: property.required, - validation: - typeof property.schema === "boolean" - ? { type: "object" } - : property.schema, - }); + onEditField( + oldName, + { + name: newName, + type: getValidSchemaType(property.schema), + description: + typeof property.schema === "boolean" + ? "" + : property.schema.description || "", + required: property.required, + validation: + typeof property.schema === "boolean" + ? { type: "object" } + : property.schema, + }, + isPatternProperty, + ); }; // Handle required status change @@ -86,21 +103,28 @@ const SchemaFieldList: FC = ({ const handleSchemaChange = ( name: string, updatedSchema: ObjectJSONSchema, + isPatternProperty = false, ) => { - const property = properties.find((prop) => prop.name === name); + const schemaProperties = isPatternProperty ? patternProperties : properties; + const property = schemaProperties.find((prop) => prop.name === name); + if (!property) return; const type = updatedSchema.type || "object"; // Ensure we're using a single type, not an array of types const validType = Array.isArray(type) ? type[0] || "object" : type; - onEditField(name, { + onEditField( name, - type: validType, - description: updatedSchema.description || "", - required: property.required, - validation: updatedSchema, - }); + { + name, + type: validType, + description: updatedSchema.description || "", + required: property.required, + validation: updatedSchema, + }, + isPatternProperty, + ); }; const validationTree = useMemo( @@ -110,6 +134,7 @@ const SchemaFieldList: FC = ({ return (
+ {properties.length > 0 ?

{t.regularPropertiesTitle}:

: null} {properties.map((property) => ( = ({ } onSchemaChange={(schema) => handleSchemaChange(property.name, schema)} readOnly={readOnly} + onPropertyToggle={onPropertyToggle} + /> + ))} + {patternProperties.length > 0 ? ( +

{t.patternPropertiesTitle}:

+ ) : null} + {patternProperties.map((property) => ( + onDeleteField(property.name, true)} + onNameChange={(newName) => + handleNameChange(property.name, newName, true) + } + onRequiredChange={(required) => + handleRequiredChange(property.name, required) + } + onSchemaChange={(schema) => + handleSchemaChange(property.name, schema, true) + } + readOnly={readOnly} + onPropertyToggle={onPropertyToggle} + isPatternProperty /> ))}
diff --git a/src/components/SchemaEditor/SchemaPropertyEditor.tsx b/src/components/SchemaEditor/SchemaPropertyEditor.tsx index dfe4a2e..9f62845 100644 --- a/src/components/SchemaEditor/SchemaPropertyEditor.tsx +++ b/src/components/SchemaEditor/SchemaPropertyEditor.tsx @@ -15,6 +15,13 @@ import { } from "../../types/jsonSchema.ts"; import type { ValidationTreeNode } from "../../types/validation.ts"; import { Badge } from "../ui/badge.tsx"; +import { ButtonToggle } from "../ui/button-toggle.tsx"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "../ui/tooltip.tsx"; import TypeDropdown from "./TypeDropdown.tsx"; import TypeEditor from "./TypeEditor.tsx"; @@ -29,6 +36,8 @@ export interface SchemaPropertyEditorProps { onRequiredChange: (required: boolean) => void; onSchemaChange: (schema: ObjectJSONSchema) => void; depth?: number; + isPatternProperty?: boolean; + onPropertyToggle?: (name: string, isPatternProperty?: boolean) => void; } export const SchemaPropertyEditor: React.FC = ({ @@ -41,7 +50,9 @@ export const SchemaPropertyEditor: React.FC = ({ onNameChange, onRequiredChange, onSchemaChange, + onPropertyToggle, depth = 0, + isPatternProperty = false, }) => { const t = useTranslation(); const [expanded, setExpanded] = useState(false); @@ -169,8 +180,23 @@ export const SchemaPropertyEditor: React.FC = ({ )}
- {/* Type display */}
+ {/* Regular/Pattern toggle */} + { + onPropertyToggle?.(name, isPatternProperty); + }} + className={ + isPatternProperty + ? "bg-emerald-50 text-emerald-600" + : "bg-secondary text-muted-foreground" + } + > + {isPatternProperty + ? t.patternPropertiesTitleShort + : t.regularPropertiesTitleShort} + + {/* Type display */} = ({ }); }} /> - {/* Required toggle */} - + {isPatternProperty ? ( + + + +
+ {t.propertyOptional} +
+
+ + {t.propertyRequiredToggleDisabledTooltip} + +
+
+ ) : ( + !readOnly && onRequiredChange(!required)} + className={ + required + ? "bg-red-50 text-red-500" + : "bg-secondary text-muted-foreground" + } + > + {required ? t.propertyRequired : t.propertyOptional} + + )}
diff --git a/src/components/SchemaEditor/SchemaVisualEditor.tsx b/src/components/SchemaEditor/SchemaVisualEditor.tsx index 52b2ef9..9194d41 100644 --- a/src/components/SchemaEditor/SchemaVisualEditor.tsx +++ b/src/components/SchemaEditor/SchemaVisualEditor.tsx @@ -10,6 +10,7 @@ import type { JSONSchema, NewField } from "../../types/jsonSchema.ts"; import { asObjectSchema, isBooleanSchema } from "../../types/jsonSchema.ts"; import AddFieldButton from "./AddFieldButton.tsx"; import SchemaFieldList from "./SchemaFieldList.tsx"; +import handlePropertyToggle from "./utils/handlePropertyToggle.tsx"; /** @public */ export interface SchemaVisualEditorProps { @@ -26,7 +27,7 @@ const SchemaVisualEditor: FC = ({ }) => { const t = useTranslation(); // Handle adding a top-level field - const handleAddField = (newField: NewField) => { + const handleAddField = (newField: NewField, isProperty = false) => { // Create a field schema based on the new field data const fieldSchema = createFieldSchema(newField); @@ -35,6 +36,7 @@ const SchemaVisualEditor: FC = ({ asObjectSchema(schema), newField.name, fieldSchema, + isProperty, ); // Update required status if needed @@ -47,7 +49,11 @@ const SchemaVisualEditor: FC = ({ }; // Handle editing a top-level field - const handleEditField = (name: string, updatedField: NewField) => { + const handleEditField = ( + name: string, + updatedField: NewField, + isProperty = false, + ) => { // Create a field schema based on the updated field data const fieldSchema = createFieldSchema(updatedField); @@ -55,16 +61,27 @@ const SchemaVisualEditor: FC = ({ // If name changed, rename the property while preserving order if (name !== updatedField.name) { - newSchema = renameObjectProperty(newSchema, name, updatedField.name); + newSchema = renameObjectProperty( + newSchema, + name, + updatedField.name, + isProperty, + ); // Update the field schema after rename newSchema = updateObjectProperty( newSchema, updatedField.name, fieldSchema, + isProperty, ); } else { // Name didn't change, just update the schema - newSchema = updateObjectProperty(newSchema, name, fieldSchema); + newSchema = updateObjectProperty( + newSchema, + name, + fieldSchema, + isProperty, + ); } // Update required status @@ -79,18 +96,20 @@ const SchemaVisualEditor: FC = ({ }; // Handle deleting a top-level field - const handleDeleteField = (name: string) => { + const handleDeleteField = (name: string, isProperty = false) => { + const schemaProperty = isProperty ? "Properties" : "properties"; + // Check if the schema is valid first - if (isBooleanSchema(schema) || !schema.properties) { + if (isBooleanSchema(schema) || !schema[schemaProperty]) { return; } // Create a new schema without the field - const { [name]: _, ...remainingProps } = schema.properties; + const { [name]: _, ...remainingProps } = schema[schemaProperty]; const newSchema = { ...schema, - properties: remainingProps, + [schemaProperty]: remainingProps, }; // Remove from required array if present @@ -102,11 +121,20 @@ const SchemaVisualEditor: FC = ({ onChange(newSchema); }; - const hasFields = - !isBooleanSchema(schema) && + const hasObjectSchema = !isBooleanSchema(schema); + + const hasProperties = + hasObjectSchema && + schema.properties && + Object.keys(schema.properties).length > 0; + + const hasPropertiesFields = + hasObjectSchema && schema.properties && Object.keys(schema.properties).length > 0; + const hasFields = hasProperties || hasPropertiesFields; + return (
{!readOnly && ( @@ -128,6 +156,9 @@ const SchemaVisualEditor: FC = ({ onAddField={handleAddField} onEditField={handleEditField} onDeleteField={handleDeleteField} + onPropertyToggle={(name, isProperty) => + handlePropertyToggle(onChange, schema, name, isProperty) + } /> )}
diff --git a/src/components/SchemaEditor/types/ObjectEditor.tsx b/src/components/SchemaEditor/types/ObjectEditor.tsx index 64d0cde..7812795 100644 --- a/src/components/SchemaEditor/types/ObjectEditor.tsx +++ b/src/components/SchemaEditor/types/ObjectEditor.tsx @@ -7,9 +7,11 @@ import { } from "../../../lib/schemaEditor.ts"; import type { NewField, ObjectJSONSchema } from "../../../types/jsonSchema.ts"; import { asObjectSchema, isBooleanSchema } from "../../../types/jsonSchema.ts"; +import { ButtonToggle } from "../../ui/button-toggle.tsx"; import AddFieldButton from "../AddFieldButton.tsx"; import SchemaPropertyEditor from "../SchemaPropertyEditor.tsx"; import type { TypeEditorProps } from "../TypeEditor.tsx"; +import handlePropertyToggle from "../utils/handlePropertyToggle.tsx"; const ObjectEditor: React.FC = ({ schema, @@ -22,19 +24,25 @@ const ObjectEditor: React.FC = ({ // Get object properties const properties = getSchemaProperties(schema); + const patternProperties = getSchemaProperties(schema, true); // Create a normalized schema object const normalizedSchema: ObjectJSONSchema = isBooleanSchema(schema) ? { type: "object", properties: {} } : { ...schema, type: "object", properties: schema.properties || {} }; + const { additionalProperties } = normalizedSchema; + // Handle adding a new property - const handleAddProperty = (newField: NewField) => { + const handleAddProperty = (newField: NewField, isPatternProperty = false) => { // Create field schema from the new field data + const { type, description, validation, additionalProperties } = newField; + const fieldSchema = { - type: newField.type, - description: newField.description || undefined, - ...(newField.validation || {}), + type, + description: description || undefined, + ...(validation || {}), + ...(additionalProperties === false ? { additionalProperties } : {}), } as ObjectJSONSchema; // Add the property to the schema @@ -42,6 +50,7 @@ const ObjectEditor: React.FC = ({ normalizedSchema, newField.name, fieldSchema, + isPatternProperty, ); // Update required status if needed @@ -54,13 +63,24 @@ const ObjectEditor: React.FC = ({ }; // Handle deleting a property - const handleDeleteProperty = (propertyName: string) => { - const newSchema = removeObjectProperty(normalizedSchema, propertyName); + const handleDeleteProperty = ( + propertyName: string, + isPatternProperty = false, + ) => { + const newSchema = removeObjectProperty( + normalizedSchema, + propertyName, + isPatternProperty, + ); onChange(newSchema); }; // Handle property name change - const handlePropertyNameChange = (oldName: string, newName: string) => { + const handlePropertyNameChange = ( + oldName: string, + newName: string, + isPatternProperty = false, + ) => { if (oldName === newName) return; const property = properties.find((p) => p.name === oldName); @@ -73,13 +93,14 @@ const ObjectEditor: React.FC = ({ normalizedSchema, newName, propertySchemaObj, + isPatternProperty, ); if (property.required) { newSchema = updatePropertyRequired(newSchema, newName, true); } - newSchema = removeObjectProperty(newSchema, oldName); + newSchema = removeObjectProperty(newSchema, oldName, isPatternProperty); onChange(newSchema); }; @@ -100,52 +121,131 @@ const ObjectEditor: React.FC = ({ const handlePropertySchemaChange = ( propertyName: string, propertySchema: ObjectJSONSchema, + isPatternProperty = false, ) => { const newSchema = updateObjectProperty( normalizedSchema, propertyName, propertySchema, + isPatternProperty, ); onChange(newSchema); }; + const handleAdditionalPropertiesToggle = () => { + const { additionalProperties, ...restOfSchema } = normalizedSchema; + + const updatedSchema = asObjectSchema(restOfSchema); + + if (additionalProperties !== false) { + updatedSchema.additionalProperties = false; + } + + onChange(updatedSchema); + }; + + const hasProperties = properties.length > 0 || patternProperties.length > 0; + return ( -
- {properties.length > 0 ? ( -
- {properties.map((property) => ( - handleDeleteProperty(property.name)} - onNameChange={(newName) => - handlePropertyNameChange(property.name, newName) - } - onRequiredChange={(required) => - handlePropertyRequiredChange(property.name, required) - } - onSchemaChange={(schema) => - handlePropertySchemaChange(property.name, schema) - } - depth={depth} +
+ {/* Regular Properties Section */} +
+ {hasProperties ? ( +
+ {properties.length > 0 ? ( +

{t.regularPropertiesTitle}:

+ ) : null} + {properties.map((property) => ( + handleDeleteProperty(property.name)} + onNameChange={(newName) => + handlePropertyNameChange(property.name, newName) + } + onRequiredChange={(required) => + handlePropertyRequiredChange(property.name, required) + } + onSchemaChange={(schema) => + handlePropertySchemaChange(property.name, schema) + } + depth={depth} + onPropertyToggle={(name, isPatternProperty) => + handlePropertyToggle( + onChange, + normalizedSchema, + name, + isPatternProperty, + ) + } + /> + ))} + {patternProperties.length > 0 ? ( +

{t.patternPropertiesTitle}:

+ ) : null} + {patternProperties.map((property) => ( + handleDeleteProperty(property.name, true)} + onNameChange={(newName) => + handlePropertyNameChange(property.name, newName, true) + } + onRequiredChange={(required) => + handlePropertyRequiredChange(property.name, required) + } + onSchemaChange={(schema) => + handlePropertySchemaChange(property.name, schema, true) + } + depth={depth} + onPropertyToggle={(name, isPatternProperty) => + handlePropertyToggle( + onChange, + normalizedSchema, + name, + isPatternProperty, + ) + } + isPatternProperty + /> + ))} +
+ ) : ( +
+ {t.objectPropertiesNone} +
+ )} + + {!readOnly && ( +
+ - ))} -
- ) : ( -
- {t.objectPropertiesNone} -
- )} - - {!readOnly && ( -
- -
- )} + {/* Additional properties */} + + {additionalProperties === false + ? t.additionalPropertiesForbid + : t.additionalPropertiesAllow} + +
+ )} +
); }; diff --git a/src/components/SchemaEditor/utils/handlePropertyToggle.tsx b/src/components/SchemaEditor/utils/handlePropertyToggle.tsx new file mode 100644 index 0000000..50183f9 --- /dev/null +++ b/src/components/SchemaEditor/utils/handlePropertyToggle.tsx @@ -0,0 +1,38 @@ +import { + removeObjectProperty, + updateObjectProperty, +} from "../../../lib/schemaEditor.ts"; +import type { ObjectJSONSchema } from "../../../types/jsonSchema.ts"; +import { isBooleanSchema } from "../../../types/jsonSchema.ts"; + +// Handle toggling between properties and patternProperties +const handlePropertyToggle = ( + onChange: (newSchema: ObjectJSONSchema) => void, + schema: ObjectJSONSchema, + name: string, + isPatternProperty = false, +) => { + const schemaProperty = isPatternProperty ? "patternProperties" : "properties"; + + if (isBooleanSchema(schema) || !schema[schemaProperty]) { + return; + } + + const fieldSchema = schema[schemaProperty][name]; + + const schemaAfterRemove = removeObjectProperty( + schema, + name, + isPatternProperty, + ); + const newSchema = updateObjectProperty( + schemaAfterRemove, + name, + fieldSchema, + !isPatternProperty, + ); + + onChange(newSchema); +}; + +export default handlePropertyToggle; diff --git a/src/components/ui/button-toggle.tsx b/src/components/ui/button-toggle.tsx new file mode 100644 index 0000000..228187d --- /dev/null +++ b/src/components/ui/button-toggle.tsx @@ -0,0 +1,24 @@ +import { type ComponentProps, forwardRef } from "react"; +import { cn } from "../../lib/utils.ts"; + +const ButtonToggle = forwardRef>( + ({ className, onClick, children, ...props }, ref) => { + return ( + + ); + }, +); +ButtonToggle.displayName = "ButtonToggle"; + +export { ButtonToggle }; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 7ea4cf7..af7f627 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,18 +1,54 @@ -import { type ComponentProps, forwardRef } from "react"; +import { type ComponentProps, forwardRef, useEffect, useState } from "react"; import { cn } from "../../lib/utils.ts"; -const Input = forwardRef>( - ({ className, type, ...props }, ref) => { +interface InputProps extends ComponentProps<"input"> { + validate?: (value: string) => string | null; + showError?: boolean; + errorMessage?: string; +} + +const Input = forwardRef( + ( + { className, type, validate, showError = true, errorMessage, ...props }, + ref, + ) => { + const [error, setError] = useState(null); + + const handleChange = (e: React.ChangeEvent) => { + console.log("handle change"); + const value = e.target.value; + const validationError = validate?.(value) || null; + setError(validationError); + props.onChange?.(e); + }; + + useEffect(() => { + if (props.value === "") { + setError(null); + } + }, [props.value]); + + const hasError = error || errorMessage; + return ( - + + {showError && hasError && ( + + {errorMessage || error} + )} - ref={ref} - {...props} - /> +
); }, ); diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 99eafae..76e1125 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -41,6 +41,8 @@ export const de: Translation = { propertyDescriptionPlaceholder: "Beschreibung hinzufügen...", propertyDescriptionButton: "Beschreibung hinzufügen...", propertyRequired: "Erforderlich", + propertyRequiredToggleDisabledTooltip: + "Erforderlich kann nur für Eigenschaften aktiviert werden, die im Abschnitt 'Reguläre Eigenschaften' definiert sind", propertyOptional: "Optional", propertyDelete: "Feld löschen", @@ -96,6 +98,18 @@ export const de: Translation = { objectValidationErrorMinMax: "'minProperties' darf nicht größer als 'maxProperties' sein.", + patternPropertiesTitle: "Mustereigenschaften", + patternPropertiesTitleShort: "Muster", + patternPropertyNamePlaceholder: "^[a-z]+$", + + regularPropertiesTitle: "Reguläre Eigenschaften", + regularPropertiesTitleShort: "Regulär", + + additionalPropertiesAllow: "Zusätzliche Eigenschaften erlauben", + additionalPropertiesForbid: "Zusätzliche Eigenschaften verbieten", + additionalPropertiesTooltip: + "Steuert, ob Eigenschaften, die nicht in 'properties' oder 'patternProperties' definiert sind, erlaubt sind", + stringNoConstraint: "Keine Einschränkung", stringMinimumLengthLabel: "Minimale Länge", stringMinimumLengthPlaceholder: "Kein Minimum", diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 96114e2..18924d7 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -40,6 +40,8 @@ export const en: Translation = { propertyDescriptionPlaceholder: "Add description...", propertyDescriptionButton: "Add description...", propertyRequired: "Required", + propertyRequiredToggleDisabledTooltip: + "Require can be toggled only for properties defined in the 'Regular Properties' section", propertyOptional: "Optional", propertyDelete: "Delete field", @@ -93,6 +95,18 @@ export const en: Translation = { objectValidationErrorMinMax: "'minProperties' cannot be greater than 'maxProperties'.", + patternPropertiesTitle: "Pattern Properties", + patternPropertiesTitleShort: "Pattern", + patternPropertyNamePlaceholder: "^[a-z]+$", + + regularPropertiesTitle: "Regular Properties", + regularPropertiesTitleShort: "Regular", + + additionalPropertiesAllow: "Allow additional properties", + additionalPropertiesForbid: "Forbid additional properties", + additionalPropertiesTooltip: + "Controls whether properties not defined in 'properties' or 'patternProperties' are allowed", + stringNoConstraint: "No constraint", stringMinimumLengthLabel: "Minimum Length", stringMinimumLengthPlaceholder: "No minimum", diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 8b030ca..c0bbaa5 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -41,6 +41,8 @@ export const es: Translation = { propertyDescriptionPlaceholder: "Agregar descripción...", propertyDescriptionButton: "Agregar descripción...", propertyRequired: "Requerido", + propertyRequiredToggleDisabledTooltip: + "Requerido solo puede ser activado para propiedades definidas en la sección 'Propiedades Regulares'", propertyOptional: "Opcional", propertyDelete: "Eliminar campo", @@ -95,6 +97,18 @@ export const es: Translation = { objectValidationErrorMinMax: "'minProperties' no puede ser mayor que 'maxProperties'.", + patternPropertiesTitle: "Propiedades de Patrón", + patternPropertiesTitleShort: "Patrón", + patternPropertyNamePlaceholder: "^[a-z]+$", + + regularPropertiesTitle: "Propiedades Regulares", + regularPropertiesTitleShort: "Regulares", + + additionalPropertiesAllow: "Permitir propiedades adicionales", + additionalPropertiesForbid: "Prohibir propiedades adicionales", + additionalPropertiesTooltip: + "Controla si se permiten propiedades no definidas en 'properties' o 'patternProperties'", + stringNoConstraint: "Sin restricción", stringMinimumLengthLabel: "Longitud Mínima", stringMinimumLengthPlaceholder: "Sin mínimo", diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 205a7d3..fd6e525 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -42,6 +42,8 @@ export const fr: Translation = { propertyDescriptionPlaceholder: "Ajouter une description...", propertyDescriptionButton: "Ajouter une description...", propertyRequired: "Obligatoire", + propertyRequiredToggleDisabledTooltip: + "Obligatoire ne peut être activé que pour les propriétés définies dans la section 'Propriétés régulières'", propertyOptional: "Facultatif", propertyDelete: "Supprimer le champ", @@ -97,6 +99,18 @@ export const fr: Translation = { objectValidationErrorMinMax: "'minProperties' ne peut pas être supérieur à 'maxProperties'.", + patternPropertiesTitle: "Propriétés de Motif", + patternPropertiesTitleShort: "Motif", + patternPropertyNamePlaceholder: "^[a-z]+$", + + regularPropertiesTitle: "Propriétés Régulières", + regularPropertiesTitleShort: "Régulières", + + additionalPropertiesAllow: "Autoriser les propriétés additionnelles", + additionalPropertiesForbid: "Interdire les propriétés additionnelles", + additionalPropertiesTooltip: + "Contrôle si les propriétés non définies dans 'properties' ou 'patternProperties' sont autorisées", + stringNoConstraint: "Pas de constrainte", stringMinimumLengthLabel: "Longueur minimale", stringMinimumLengthPlaceholder: "Pas de minimum", diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts new file mode 100644 index 0000000..84289ca --- /dev/null +++ b/src/i18n/locales/pl.ts @@ -0,0 +1,176 @@ +import type { Translation } from "../translation-keys.ts"; + +export const pl: Translation = { + collapse: "Zwiń", + expand: "Rozwiń", + + fieldDescriptionPlaceholder: "Opisz przeznaczenie tego pola", + fieldDelete: "Usuń pole", + fieldDescription: "Opis", + fieldDescriptionTooltip: "Dodaj kontekst o tym, co reprezentuje to pole", + fieldNameLabel: "Nazwa pola", + fieldNamePlaceholder: "np. firstName, age, isActive", + fieldNameTooltip: "Używaj camelCase dla lepszej czytelności (np. firstName)", + fieldRequiredLabel: "Pole wymagane", + fieldType: "Typ pola", + fieldTypeExample: "Przykład:", + fieldTypeTooltipString: "string: Tekst", + fieldTypeTooltipNumber: "number: Liczby", + fieldTypeTooltipBoolean: "boolean: Prawda/fałsz", + fieldTypeTooltipObject: "object: Zagnieżdżony JSON", + fieldTypeTooltipArray: "array: Listy wartości", + fieldAddNewButton: "Dodaj pole", + fieldAddNewBadge: "Kreator schematów", + fieldAddNewCancel: "Anuluj", + fieldAddNewConfirm: "Dodaj pole", + fieldAddNewDescription: "Utwórz nowe pole dla swojego schematu JSON", + fieldAddNewLabel: "Dodaj nowe pole", + + fieldTypeTextLabel: "Tekst", + fieldTypeTextDescription: "Dla wartości tekstowych jak nazwy, opisy itp.", + fieldTypeNumberLabel: "Liczba", + fieldTypeNumberDescription: "Dla liczb dziesiętnych lub całkowitych", + fieldTypeBooleanLabel: "Tak/Nie", + fieldTypeBooleanDescription: "Dla wartości prawda/fałsz", + fieldTypeObjectLabel: "Obiekt", + fieldTypeObjectDescription: "Do grupowania powiązanych pól razem", + fieldTypeArrayLabel: "Lista", + fieldTypeArrayDescription: "Dla kolekcji elementów", + + propertyDescriptionPlaceholder: "Dodaj opis...", + propertyDescriptionButton: "Dodaj opis...", + propertyRequired: "Wymagane", + propertyRequiredToggleDisabledTooltip: + "Wymagane można włączyć tylko dla właściwości zdefiniowanych w sekcji 'Właściwości zwyczajne'", + propertyOptional: "Opcjonalne", + propertyDelete: "Usuń pole", + + schemaEditorTitle: "Edytor schematu JSON", + schemaEditorToggleFullscreen: "Przełącz tryb pełnoekranowy", + schemaEditorEditModeVisual: "Wizualny", + schemaEditorEditModeJson: "JSON", + schemaEditorLoading: "Ładowanie edytora...", + + arrayNoConstraint: "Bez ograniczeń", + arrayMinimumLabel: "Minimalna liczba elementów", + arrayMinimumPlaceholder: "Brak minimum", + arrayMaximumLabel: "Maksymalna liczba elementów", + arrayMaximumPlaceholder: "Brak maksimum", + arrayForceUniqueItemsLabel: "Wymuś unikalne elementy", + arrayItemTypeLabel: "Typ elementu", + arrayValidationErrorMinMax: "'minItems' nie może być większe niż 'maxItems'.", + arrayValidationErrorContainsMinMax: + "'minContains' nie może być większe niż 'maxContains'.", + + booleanNoConstraint: "Bez ograniczeń", + booleanAllowedValuesLabel: "Dozwolone wartości", + booleanAllowFalseLabel: "Zezwalaj na wartość fałsz", + booleanAllowTrueLabel: "Zezwalaj na wartość prawda", + booleanNeitherWarning: + "Ostrzeżenie: Musisz zezwolić na przynajmniej jedną wartość.", + + numberNoConstraint: "Bez ograniczeń", + numberMinimumLabel: "Minimalna wartość", + numberMinimumPlaceholder: "Brak minimum", + numberMaximumLabel: "Maksymalna wartość", + numberMaximumPlaceholder: "Brak maksimum", + numberExclusiveMinimumLabel: "Minimum wyłączne", + numberExclusiveMinimumPlaceholder: "Brak minimum wyłącznego", + numberExclusiveMaximumLabel: "Maksimum wyłączne", + numberExclusiveMaximumPlaceholder: "Brak maksimum wyłącznego", + numberMultipleOfLabel: "Wielokrotność", + numberMultipleOfPlaceholder: "Dowolna", + numberAllowedValuesEnumLabel: "Dozwolone wartości (enum)", + numberAllowedValuesEnumNone: "Nie ustawiono ograniczonych wartości", + numberAllowedValuesEnumAddLabel: "Dodaj", + numberAllowedValuesEnumAddPlaceholder: "Dodaj dozwoloną wartość...", + numberValidationErrorMinMax: + "Wartości minimalna i maksymalna muszą być spójne.", + numberValidationErrorBothExclusiveAndInclusiveMin: + "Nie można jednocześnie ustawić 'exclusiveMinimum' i 'minimum'.", + numberValidationErrorBothExclusiveAndInclusiveMax: + "Nie można jednocześnie ustawić 'exclusiveMaximum' i 'maximum'.", + numberValidationErrorEnumOutOfRange: + "Wartości enum muszą być w zdefiniowanym zakresie.", + + objectPropertiesNone: "Nie zdefiniowano właściwości", + objectValidationErrorMinMax: + "'minProperties' nie może być większe niż 'maxProperties'.", + + patternPropertiesTitle: "Właściwości wzorca", + patternPropertiesTitleShort: "Wzorzec", + patternPropertyNamePlaceholder: "^[a-z]+$", + + regularPropertiesTitle: "Właściwości zwykłe", + regularPropertiesTitleShort: "Zwykłe", + + additionalPropertiesAllow: "Zezwalaj na dodatkowe właściwości", + additionalPropertiesForbid: "Zabroń dodatkowych właściwości", + additionalPropertiesTooltip: + "Kontroluje czy właściwości niezdefiniowane w 'properties' lub 'patternProperties' są dozwolone", + + stringNoConstraint: "Bez ograniczeń", + stringMinimumLengthLabel: "Minimalna długość", + stringMinimumLengthPlaceholder: "Brak minimum", + stringMaximumLengthLabel: "Maksymalna długość", + stringMaximumLengthPlaceholder: "Brak maksimum", + stringPatternLabel: "Wzorzec (regex)", + stringPatternPlaceholder: "^[a-zA-Z]+$", + stringFormatLabel: "Format", + stringFormatNone: "Brak", + stringFormatDateTime: "Data-czas", + stringFormatDate: "Data", + stringFormatTime: "Czas", + stringFormatEmail: "Email", + stringFormatUri: "URI", + stringFormatUuid: "UUID", + stringFormatHostname: "Nazwa hosta", + stringFormatIpv4: "Adres IPv4", + stringFormatIpv6: "Adres IPv6", + stringAllowedValuesEnumLabel: "Dozwolone wartości (enum)", + stringAllowedValuesEnumNone: "Nie ustawiono ograniczonych wartości", + stringAllowedValuesEnumAddPlaceholder: "Dodaj dozwoloną wartość...", + stringAllowedValuesEnumAddLabel: "Dodaj", + stringFormatSelectPlaceholder: "Wybierz format", + stringValidationErrorLengthRange: + "'Minimalna długość' nie może być większa niż 'Maksymalna długość'.", + + schemaTypeArray: "Lista", + schemaTypeBoolean: "Tak/Nie", + schemaTypeNumber: "Liczba", + schemaTypeObject: "Obiekt", + schemaTypeString: "Tekst", + schemaTypeNull: "Pusty", + + inferrerTitle: "Wnioskuj schemat JSON", + inferrerDescription: + "Wklej swój dokument JSON poniżej, aby wygenerować z niego schemat.", + inferrerCancel: "Anuluj", + inferrerGenerate: "Generuj schemat", + inferrerErrorInvalidJson: + "Nieprawidłowy format JSON. Sprawdź swoje dane wejściowe.", + + validatorTitle: "Waliduj JSON", + validatorDescription: + "Wklej swój dokument JSON, aby zwalidować go względem bieżącego schematu. Walidacja następuje automatycznie podczas pisania.", + validatorCurrentSchema: "Bieżący schemat:", + validatorContent: "Twój JSON:", + validatorValid: "JSON jest prawidłowy zgodnie ze schematem!", + validatorErrorInvalidSyntax: "Nieprawidłowa składnia JSON", + validatorErrorSchemaValidation: "Błąd walidacji schematu", + validatorErrorCount: "Wykryto {count} błędów walidacji", + validatorErrorPathRoot: "Korzeń", + validatorErrorLocationLineAndColumn: "Linia {line}, Kolumna {column}", + validatorErrorLocationLineOnly: "Linia {line}", + + visualizerDownloadTitle: "Pobierz schemat", + visualizerDownloadFileName: "schema.json", + visualizerSource: "Schemat JSON", + + visualEditorNoFieldsHint1: "Nie zdefiniowano jeszcze pól", + visualEditorNoFieldsHint2: "Dodaj swoje pierwsze pole, aby rozpocząć", + + typeValidationErrorNegativeLength: "Wartości długości nie mogą być ujemne.", + typeValidationErrorIntValue: "Wartość musi być liczbą całkowitą.", + typeValidationErrorPositive: "Wartość musi być dodatnia.", +}; diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index a8266b2..12d3315 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -42,6 +42,8 @@ export const ru: Translation = { propertyDescriptionPlaceholder: "Добавить описание...", propertyDescriptionButton: "Добавить описание...", propertyRequired: "Обязательное", + propertyRequiredToggleDisabledTooltip: + "Обязательное может быть включено только для свойств, определенных в разделе 'Регулярные свойства'", propertyOptional: "Необязательное", propertyDelete: "Удалить поле", @@ -96,6 +98,18 @@ export const ru: Translation = { objectValidationErrorMinMax: "'minProperties' не может быть больше 'maxProperties'.", + patternPropertiesTitle: "Свойства по шаблону", + patternPropertiesTitleShort: "Шаблон", + patternPropertyNamePlaceholder: "^[a-z]+$", + + regularPropertiesTitle: "Обычные свойства", + regularPropertiesTitleShort: "Обычные", + + additionalPropertiesAllow: "Разрешить дополнительные свойства", + additionalPropertiesForbid: "Запретить дополнительные свойства", + additionalPropertiesTooltip: + "Определяет, разрешены ли свойства, не указанные в 'properties' или 'patternProperties'", + stringNoConstraint: "Без ограничений", stringMinimumLengthLabel: "Минимальная длина", stringMinimumLengthPlaceholder: "Нет минимума", diff --git a/src/i18n/locales/uk.ts b/src/i18n/locales/uk.ts index 2dc763c..4f151a4 100644 --- a/src/i18n/locales/uk.ts +++ b/src/i18n/locales/uk.ts @@ -41,6 +41,8 @@ export const uk: Translation = { propertyDescriptionPlaceholder: "Додати опис...", propertyDescriptionButton: "Додати опис...", propertyRequired: "Обов'язкове", + propertyRequiredToggleDisabledTooltip: + "Обов'язкове може бути включено тільки для властивостей, визначених у розділі 'Звичайні властивості'", propertyOptional: "Необов'язкове", propertyDelete: "Видалити поле", @@ -95,6 +97,18 @@ export const uk: Translation = { objectValidationErrorMinMax: "'minProperties' не може бути більше за 'maxProperties'.", + patternPropertiesTitle: "Властивості за шаблоном", + patternPropertiesTitleShort: "Шаблон", + patternPropertyNamePlaceholder: "^[a-z]+$", + + regularPropertiesTitle: "Звичайні властивості", + regularPropertiesTitleShort: "Звичайні", + + additionalPropertiesAllow: "Дозволити додаткові властивості", + additionalPropertiesForbid: "Заборонити додаткові властивості", + additionalPropertiesTooltip: + "Контролює, чи дозволені властивості, не визначені в 'properties' або 'patternProperties'", + stringNoConstraint: "Без обмежень", stringMinimumLengthLabel: "Мінімальна довжина", stringMinimumLengthPlaceholder: "Немає мінімуму", diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 3f07c1c..ba70917 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -40,6 +40,8 @@ export const zh: Translation = { propertyDescriptionPlaceholder: "添加描述...", propertyDescriptionButton: "添加描述...", propertyRequired: "必填", + propertyRequiredToggleDisabledTooltip: + "必填只能针对在 '常规属性' 部分定义的属性启用", propertyOptional: "可选", propertyDelete: "删除字段", @@ -91,6 +93,18 @@ export const zh: Translation = { objectPropertiesNone: "没有定义属性", objectValidationErrorMinMax: "「最少属性数」不能大于「最多属性数」.", + patternPropertiesTitle: "模式属性", + patternPropertiesTitleShort: "模式", + patternPropertyNamePlaceholder: "^[a-z]+$", + + regularPropertiesTitle: "常规属性", + regularPropertiesTitleShort: "常规", + + additionalPropertiesAllow: "允许附加属性", + additionalPropertiesForbid: "禁止附加属性", + additionalPropertiesTooltip: + "控制是否允许未在 'properties' 或 'patternProperties' 中定义的属性", + stringNoConstraint: "无约束", stringMinimumLengthLabel: "最小长度", stringMinimumLengthPlaceholder: "无最小长度", diff --git a/src/i18n/translation-keys.ts b/src/i18n/translation-keys.ts index 3317bb2..ed16e3c 100644 --- a/src/i18n/translation-keys.ts +++ b/src/i18n/translation-keys.ts @@ -219,6 +219,12 @@ export interface Translation { * > Required */ readonly propertyRequired: string; + /** + * The translation for the key `propertyRequiredToggleDisabledTooltip`. English default is: + * + * > Require can be toggled only for properties defined in the 'properties' section + */ + readonly propertyRequiredToggleDisabledTooltip: string; /** * The translation for the key `propertyOptional`. English default is: * @@ -446,6 +452,54 @@ export interface Translation { */ readonly objectValidationErrorMinMax: string; + /** + * The translation for the key `patternPropertiesTitle`. English default is: + * + * > Pattern Properties + */ + readonly patternPropertiesTitle: string; + /** + * The translation for the key `patternPropertiesTitleShort`. English default is: + * + * > Pattern + */ + readonly patternPropertiesTitleShort: string; + /** + * The translation for the key `patternPropertyNamePlaceholder`. English default is: + * + * > ^[a-z]+$ + */ + readonly patternPropertyNamePlaceholder: string; + /** + * The translation for the key `additionalPropertiesAllow`. English default is: + * + * > Allow additional properties + */ + readonly additionalPropertiesAllow: string; + /** + * The translation for the key `additionalPropertiesForbid`. English default is: + * + * > Forbid additional properties + */ + readonly additionalPropertiesForbid: string; + /** + * The translation for the key `additionalPropertiesTooltip`. English default is: + * + * > Controls whether properties not defined in 'properties' or 'patternProperties' are allowed + */ + readonly additionalPropertiesTooltip: string; + /** + * The translation for the key `regularPropertiesTitle`. English default is: + * + * > Regular Property + */ + readonly regularPropertiesTitle: string; + /** + * The translation for the key `regularPropertiesTitleShort`. English default is: + * + * > Regular + */ + readonly regularPropertiesTitleShort: string; /** * The translation for the key `stringMinimumLengthLabel`. English default is: * diff --git a/src/lib/schemaEditor.ts b/src/lib/schemaEditor.ts index 14f7685..b6b6548 100644 --- a/src/lib/schemaEditor.ts +++ b/src/lib/schemaEditor.ts @@ -11,6 +11,11 @@ export type Property = { required: boolean; }; +export type PatternProperty = { + pattern: string; + schema: JSONSchema; +}; + export function copySchema(schema: T): T { if (typeof structuredClone === "function") return structuredClone(schema); return JSON.parse(JSON.stringify(schema)); @@ -23,15 +28,28 @@ export function updateObjectProperty( schema: ObjectJSONSchema, propertyName: string, propertySchema: JSONSchema, + isPatternProperty = false, ): ObjectJSONSchema { if (!isObjectSchema(schema)) return schema; const newSchema = copySchema(schema); + if (!newSchema.properties) { newSchema.properties = {}; } + if (isPatternProperty) { + if (!newSchema.patternProperties) { + newSchema.patternProperties = {}; + } + + newSchema.patternProperties[propertyName] = propertySchema; + + return newSchema; + } + newSchema.properties[propertyName] = propertySchema; + return newSchema; } @@ -41,12 +59,15 @@ export function updateObjectProperty( export function removeObjectProperty( schema: ObjectJSONSchema, propertyName: string, + isPatternProperty = false, ): ObjectJSONSchema { - if (!isObjectSchema(schema) || !schema.properties) return schema; + const schemaProperty = isPatternProperty ? "patternProperties" : "properties"; + + if (!isObjectSchema(schema) || !schema[schemaProperty]) return schema; const newSchema = copySchema(schema); - const { [propertyName]: _, ...remainingProps } = newSchema.properties; - newSchema.properties = remainingProps; + const { [propertyName]: _, ...remainingProps } = newSchema[schemaProperty]; + newSchema[schemaProperty] = remainingProps; // Also remove from required array if present if (newSchema.required) { @@ -108,14 +129,17 @@ export function updateArrayItems( * Creates a schema for a new field */ export function createFieldSchema(field: NewField): JSONSchema { - const { type, description, validation } = field; + const { type, description, validation, additionalProperties } = field; + if (isObjectSchema(validation)) { return { type, description, ...validation, + ...(additionalProperties === false ? { additionalProperties } : {}), }; } + return validation; } @@ -135,12 +159,17 @@ export function validateFieldName(name: string): boolean { /** * Gets properties from an object schema */ -export function getSchemaProperties(schema: JSONSchema): Property[] { - if (!isObjectSchema(schema) || !schema.properties) return []; +export function getSchemaProperties( + schema: JSONSchema, + isPatternProperty = false, +): Property[] { + const schemaProperty = isPatternProperty ? "patternProperties" : "properties"; + + if (!isObjectSchema(schema) || !schema[schemaProperty]) return []; const required = schema.required || []; - return Object.entries(schema.properties).map(([name, propSchema]) => ({ + return Object.entries(schema[schemaProperty]).map(([name, propSchema]) => ({ name, schema: propSchema, required: required.includes(name), @@ -164,14 +193,17 @@ export function renameObjectProperty( schema: ObjectJSONSchema, oldName: string, newName: string, + isPatternProperty = false, ): ObjectJSONSchema { - if (!isObjectSchema(schema) || !schema.properties) return schema; + const schemaProperty = isPatternProperty ? "patternProperties" : "properties"; + + if (!isObjectSchema(schema) || !schema[schemaProperty]) return schema; const newSchema = copySchema(schema); const newProperties: Record = {}; // Iterate through properties in order, replacing old key with new key - for (const [key, value] of Object.entries(newSchema.properties)) { + for (const [key, value] of Object.entries(newSchema[schemaProperty])) { if (key === oldName) { newProperties[newName] = value; } else { @@ -179,7 +211,7 @@ export function renameObjectProperty( } } - newSchema.properties = newProperties; + newSchema[schemaProperty] = newProperties; // Update required array if the field name changed if (newSchema.required) { @@ -207,3 +239,38 @@ export function hasChildren(schema: JSONSchema): boolean { return false; } + +/** + * Gets pattern properties from an object schema + */ +export function getSchemaPatternProperties( + schema: JSONSchema, +): PatternProperty[] { + if (!isObjectSchema(schema) || !schema.patternProperties) return []; + + return Object.entries(schema.patternProperties).map( + ([pattern, propSchema]) => ({ + pattern, + schema: propSchema, + }), + ); +} + +/** + * Validates a regex pattern + */ +export function validateRegexPattern(pattern: string): { + valid: boolean; + error?: string; +} { + if (!pattern || pattern.trim() === "") { + return { valid: false, error: "Pattern cannot be empty" }; + } + + try { + new RegExp(pattern); + return { valid: true }; + } catch (e) { + return { valid: false, error: `Invalid regex: ${(e as Error).message}` }; + } +} diff --git a/src/types/jsonSchema.ts b/src/types/jsonSchema.ts index aa8f847..c894f4e 100644 --- a/src/types/jsonSchema.ts +++ b/src/types/jsonSchema.ts @@ -130,6 +130,7 @@ export interface NewField { description: string; required: boolean; validation?: ObjectJSONSchema; + additionalProperties?: boolean; } export interface SchemaEditorState { diff --git a/test/components/SchemaEditor/SchemaVisualEditor.test.tsx.snapshot b/test/components/SchemaEditor/SchemaVisualEditor.test.tsx.snapshot index 27a474a..964fcb2 100644 --- a/test/components/SchemaEditor/SchemaVisualEditor.test.tsx.snapshot +++ b/test/components/SchemaEditor/SchemaVisualEditor.test.tsx.snapshot @@ -1,7 +1,7 @@ exports[`SchemaVisualEditor > read-only mode doesn't show constraints 1`] = ` -"
" +"

Regular Properties:

" `; exports[`SchemaVisualEditor > write mode does show constraints 1`] = ` -"
" +"

Regular Properties:

" `; diff --git a/test/components/SchemaEditor/types/ArrayEditor.test.tsx.snapshot b/test/components/SchemaEditor/types/ArrayEditor.test.tsx.snapshot index 174e30d..0b0873b 100644 --- a/test/components/SchemaEditor/types/ArrayEditor.test.tsx.snapshot +++ b/test/components/SchemaEditor/types/ArrayEditor.test.tsx.snapshot @@ -3,5 +3,5 @@ exports[`ArrayEditor > read-only mode doesn't show constraints 1`] = ` `; exports[`ArrayEditor > write mode does show constraints 1`] = ` -"
Loading editor...
" +"
Loading editor...
" `; diff --git a/test/components/SchemaEditor/types/NumberEditor.test.tsx.snapshot b/test/components/SchemaEditor/types/NumberEditor.test.tsx.snapshot index c545fd2..dab3fcf 100644 --- a/test/components/SchemaEditor/types/NumberEditor.test.tsx.snapshot +++ b/test/components/SchemaEditor/types/NumberEditor.test.tsx.snapshot @@ -3,5 +3,5 @@ exports[`NumberEditor > read-only mode doesn't show constraints 1`] = ` `; exports[`NumberEditor > write mode does show constraints 1`] = ` -"

No restricted values set

" +"

No restricted values set

" `; diff --git a/test/components/SchemaEditor/types/ObjectEditor.test.tsx.snapshot b/test/components/SchemaEditor/types/ObjectEditor.test.tsx.snapshot index 881eef1..d78c49c 100644 --- a/test/components/SchemaEditor/types/ObjectEditor.test.tsx.snapshot +++ b/test/components/SchemaEditor/types/ObjectEditor.test.tsx.snapshot @@ -1,7 +1,7 @@ exports[`ObjectEditor > read-only mode doesn't show constraints 1`] = ` -"
" +"

Regular Properties:

" `; exports[`ObjectEditor > write mode does show constraints 1`] = ` -"
" +"

Regular Properties:

" `; diff --git a/test/components/SchemaEditor/types/StringEditor.test.tsx.snapshot b/test/components/SchemaEditor/types/StringEditor.test.tsx.snapshot index f88ad5a..b86bd4b 100644 --- a/test/components/SchemaEditor/types/StringEditor.test.tsx.snapshot +++ b/test/components/SchemaEditor/types/StringEditor.test.tsx.snapshot @@ -3,5 +3,5 @@ exports[`StringEditor > read-only mode doesn't show constraints 1`] = ` `; exports[`StringEditor > write mode does show constraints 1`] = ` -"

No restricted values set

" +"

No restricted values set

" `;