From 284eb189dbcb28b95215f9dd902094b1ab66d07b Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Thu, 21 May 2026 14:26:34 +0200 Subject: [PATCH 1/3] Simplify pattern properties UI --- .../SchemaEditor/AddFieldButton.tsx | 111 +++++++++++++---- .../SchemaEditor/SchemaFieldList.tsx | 116 ++++++++++++------ .../SchemaEditor/SchemaPropertyEditor.tsx | 47 +++++-- .../SchemaEditor/SchemaVisualEditor.tsx | 69 ++++++----- .../SchemaEditor/types/ObjectEditor.tsx | 65 +++++++--- src/i18n/locales/de.ts | 9 ++ src/i18n/locales/en.ts | 9 ++ src/i18n/locales/es.ts | 9 ++ src/i18n/locales/fr.ts | 9 ++ src/i18n/locales/pl.ts | 9 ++ src/i18n/locales/ru.ts | 9 ++ src/i18n/locales/uk.ts | 9 ++ src/i18n/locales/zh.ts | 8 ++ src/i18n/translation-keys.ts | 42 +++++++ src/lib/schemaEditor.ts | 46 ++++--- 15 files changed, 431 insertions(+), 136 deletions(-) diff --git a/src/components/SchemaEditor/AddFieldButton.tsx b/src/components/SchemaEditor/AddFieldButton.tsx index 62b0d26..10fb458 100644 --- a/src/components/SchemaEditor/AddFieldButton.tsx +++ b/src/components/SchemaEditor/AddFieldButton.tsx @@ -18,11 +18,12 @@ import { TooltipTrigger, } from "../../components/ui/tooltip.tsx"; import { useTranslation } from "../../hooks/use-translation.ts"; +import { cn } from "../../lib/utils.ts"; import type { NewField, SchemaType } from "../../types/jsonSchema.ts"; import SchemaTypeSelector from "./SchemaTypeSelector.tsx"; interface AddFieldButtonProps { - onAddField: (field: NewField) => void; + onAddField: (field: NewField, isPatternProperty?: boolean) => void; variant?: "primary" | "secondary"; autoFocus?: boolean; } @@ -37,32 +38,56 @@ const AddFieldButton: FC = ({ const [fieldType, setFieldType] = useState("string"); const [fieldDesc, setFieldDesc] = useState(""); const [fieldRequired, setFieldRequired] = useState(false); + const [useNameRegex, setUseNameRegex] = useState(false); const [additionalProperties, setAdditionalProperties] = useState(true); const fieldNameId = useId(); + const fieldNameHelpId = useId(); const fieldDescId = useId(); const fieldRequiredId = useId(); const fieldTypeId = useId(); const additionalPropertiesId = useId(); const t = useTranslation(); + const isPatternProperty = useNameRegex; + const regexError = (() => { + if (!isPatternProperty || !fieldName.trim()) return ""; + try { + new RegExp(fieldName); + return ""; + } catch { + return t.fieldNameRegexError; + } + })(); + + const setNameRegexMode = (enabled: boolean) => { + setUseNameRegex(enabled); + if (enabled) { + setFieldRequired(false); + } + }; const handleSubmit = (e: FormEvent) => { e.preventDefault(); if (!fieldName.trim()) return; + if (regexError) return; - onAddField({ - name: fieldName, - type: fieldType, - description: fieldDesc, - required: fieldRequired, - additionalProperties: - fieldType === "object" ? additionalProperties : undefined, - }); + onAddField( + { + name: fieldName, + type: fieldType, + description: fieldDesc, + required: isPatternProperty ? false : fieldRequired, + additionalProperties: + fieldType === "object" ? additionalProperties : undefined, + }, + isPatternProperty, + ); setFieldName(""); setFieldType("string"); setFieldDesc(""); setFieldRequired(false); + setUseNameRegex(false); setDialogOpen(false); setAdditionalProperties(true); }; @@ -106,7 +131,9 @@ const AddFieldButton: FC = ({ htmlFor={fieldNameId} className="text-sm font-medium" > - {t.fieldNameLabel} + {isPatternProperty + ? t.fieldNameRegexLabel + : t.fieldNameLabel} @@ -114,20 +141,54 @@ const AddFieldButton: FC = ({ -

{t.fieldNameTooltip}

+

+ {isPatternProperty + ? t.fieldNameRegexHelp + : t.fieldNameTooltip} +

+ setFieldName(e.target.value)} - placeholder={t.fieldNamePlaceholder} + placeholder={ + isPatternProperty + ? t.fieldNameRegexPlaceholder + : t.fieldNamePlaceholder + } + aria-describedby={ + isPatternProperty ? fieldNameHelpId : undefined + } + aria-invalid={regexError ? true : undefined} className="font-mono text-sm w-full" autoFocus={autoFocus} required /> + {isPatternProperty ? ( +

+ {regexError || t.fieldNameRegexHelp} +

+ ) : null}
@@ -158,18 +219,20 @@ const AddFieldButton: FC = ({ />
-
- setFieldRequired(e.target.checked)} - className="rounded border-gray-300 shrink-0" - /> - -
+ {isPatternProperty ? null : ( +
+ setFieldRequired(e.target.checked)} + className="rounded border-gray-300 shrink-0" + /> + +
+ )} {fieldType === "object" ? (
void; onDeleteEnum?: (ctx: EnumChangeContext) => void; - 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; autoFocus?: boolean; } @@ -40,6 +44,8 @@ const SchemaFieldList: FC = ({ // Get the properties from the schema const properties = getSchemaProperties(schema); + const patternProperties = getSchemaProperties(schema, true); + const allProperties = [...properties, ...patternProperties]; // Get schema type as a valid SchemaType const getValidSchemaType = (propSchema: JSONSchemaType): SchemaType => { @@ -63,23 +69,32 @@ 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 @@ -106,8 +121,10 @@ 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; // combinator schemas have no direct type field @@ -116,13 +133,17 @@ const SchemaFieldList: FC = ({ isOneOfSchema(updatedSchema) || isAllOfSchema(updatedSchema) ) { - onEditField(name, { + onEditField( name, - type: "object", - description: updatedSchema.description || "", - required: property.required, - validation: updatedSchema, - }); + { + name, + type: "object", + description: updatedSchema.description || "", + required: property.required, + validation: updatedSchema, + }, + isPatternProperty, + ); return; } @@ -130,13 +151,17 @@ const SchemaFieldList: FC = ({ // 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( @@ -146,23 +171,40 @@ const SchemaFieldList: FC = ({ return (
- {properties.map((property) => ( + {allProperties.map((property) => ( onDeleteField(property.name)} - onNameChange={(newName) => handleNameChange(property.name, newName)} + onDelete={() => + onDeleteField(property.name, property.isPatternProperty) + } + onNameChange={(newName) => + handleNameChange(property.name, newName, property.isPatternProperty) + } onRequiredChange={(required) => handleRequiredChange(property.name, required) } - onSchemaChange={(schema) => handleSchemaChange(property.name, schema)} + onSchemaChange={(schema) => + handleSchemaChange( + property.name, + schema, + property.isPatternProperty, + ) + } readOnly={readOnly} + isPatternProperty={property.isPatternProperty} autoFocus={autoFocus} /> ))} diff --git a/src/components/SchemaEditor/SchemaPropertyEditor.tsx b/src/components/SchemaEditor/SchemaPropertyEditor.tsx index 3f65e81..d681684 100644 --- a/src/components/SchemaEditor/SchemaPropertyEditor.tsx +++ b/src/components/SchemaEditor/SchemaPropertyEditor.tsx @@ -34,6 +34,7 @@ export interface SchemaPropertyEditorProps { onRequiredChange: (required: boolean) => void; onSchemaChange: (schema: ObjectJSONSchema) => void; depth?: number; + isPatternProperty?: boolean; } export const SchemaPropertyEditor: React.FC = ({ @@ -51,6 +52,7 @@ export const SchemaPropertyEditor: React.FC = ({ onRequiredChange, onSchemaChange, depth = 0, + isPatternProperty = false, }) => { const t = useTranslation(); const [expanded, setExpanded] = useState(false); @@ -135,12 +137,24 @@ export const SchemaPropertyEditor: React.FC = ({ type="button" onClick={() => setIsEditingName(true)} onKeyDown={(e) => e.key === "Enter" && setIsEditingName(true)} - className="json-field-label font-medium cursor-text px-2 py-0.5 -mx-0.5 rounded-sm hover:bg-secondary/30 hover:shadow-xs hover:ring-1 hover:ring-ring/20 transition-all text-left truncate min-w-[80px] max-w-[50%]" + aria-label={ + isPatternProperty + ? `${t.fieldNameRegexLabel}: ${name}` + : undefined + } + title={ + isPatternProperty + ? t.propertyNameRegexDescription + : undefined + } + className={cn( + "json-field-label font-medium cursor-text px-2 py-0.5 -mx-0.5 rounded-sm hover:bg-secondary/30 hover:shadow-xs hover:ring-1 hover:ring-ring/20 transition-all text-left truncate min-w-[80px] max-w-[50%]", + isPatternProperty && "font-mono", + )} > {name} )} - {/* Description */} {!readOnly && isEditingDesc ? ( = ({ /> {/* Required toggle */} - !readOnly && onRequiredChange(!required)} - className={ - required - ? "bg-red-50 text-red-500" - : "bg-secondary text-muted-foreground" - } - > - {required ? t.propertyRequired : t.propertyOptional} - + {isPatternProperty ? ( + + /.*/ + + ) : ( + !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 e1cbb1a..7ff406d 100644 --- a/src/components/SchemaEditor/SchemaVisualEditor.tsx +++ b/src/components/SchemaEditor/SchemaVisualEditor.tsx @@ -2,6 +2,7 @@ import type { FC } from "react"; import { useTranslation } from "../../hooks/use-translation.ts"; import { createFieldSchema, + removeObjectProperty, renameObjectProperty, updateObjectProperty, updatePropertyRequired, @@ -33,7 +34,7 @@ const SchemaVisualEditor: FC = ({ }) => { const t = useTranslation(); // Handle adding a top-level field - const handleAddField = (newField: NewField) => { + const handleAddField = (newField: NewField, isPatternProperty = false) => { // Create a field schema based on the new field data const fieldSchema = createFieldSchema(newField); @@ -42,10 +43,11 @@ const SchemaVisualEditor: FC = ({ asObjectSchema(schema), newField.name, fieldSchema, + isPatternProperty, ); // Update required status if needed - if (newField.required) { + if (!isPatternProperty && newField.required) { newSchema = updatePropertyRequired(newSchema, newField.name, true); } @@ -54,7 +56,11 @@ const SchemaVisualEditor: FC = ({ }; // Handle editing a top-level field - const handleEditField = (name: string, updatedField: NewField) => { + const handleEditField = ( + name: string, + updatedField: NewField, + isPatternProperty = false, + ) => { // Create a field schema based on the updated field data const fieldSchema = createFieldSchema(updatedField); @@ -62,57 +68,54 @@ 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, + isPatternProperty, + ); // Update the field schema after rename newSchema = updateObjectProperty( newSchema, updatedField.name, fieldSchema, + isPatternProperty, ); } else { // Name didn't change, just update the schema - newSchema = updateObjectProperty(newSchema, name, fieldSchema); + newSchema = updateObjectProperty( + newSchema, + name, + fieldSchema, + isPatternProperty, + ); } // Update required status - newSchema = updatePropertyRequired( - newSchema, - updatedField.name, - updatedField.required || false, - ); + if (!isPatternProperty) { + newSchema = updatePropertyRequired( + newSchema, + updatedField.name, + updatedField.required || false, + ); + } // Update the schema onChange(newSchema); }; // Handle deleting a top-level field - const handleDeleteField = (name: string) => { - // Check if the schema is valid first - if (isBooleanSchema(schema) || !schema.properties) { - return; - } - - // Create a new schema without the field - const { [name]: _, ...remainingProps } = schema.properties; - - const newSchema = { - ...schema, - properties: remainingProps, - }; - - // Remove from required array if present - if (newSchema.required) { - newSchema.required = newSchema.required.filter((field) => field !== name); - } - - // Update the schema - onChange(newSchema); + const handleDeleteField = (name: string, isPatternProperty = false) => { + onChange( + removeObjectProperty(asObjectSchema(schema), name, isPatternProperty), + ); }; const hasFields = !isBooleanSchema(schema) && - schema.properties && - Object.keys(schema.properties).length > 0; + ((schema.properties && Object.keys(schema.properties).length > 0) || + (schema.patternProperties && + Object.keys(schema.patternProperties).length > 0)); return (
diff --git a/src/components/SchemaEditor/types/ObjectEditor.tsx b/src/components/SchemaEditor/types/ObjectEditor.tsx index 0ce512a..d1f72e9 100644 --- a/src/components/SchemaEditor/types/ObjectEditor.tsx +++ b/src/components/SchemaEditor/types/ObjectEditor.tsx @@ -26,6 +26,8 @@ const ObjectEditor: React.FC = ({ // Get object properties const properties = getSchemaProperties(schema); + const patternProperties = getSchemaProperties(schema, true); + const allProperties = [...properties, ...patternProperties]; // Create a normalized schema object const normalizedSchema: ObjectJSONSchema = isBooleanSchema(schema) @@ -35,7 +37,7 @@ const ObjectEditor: React.FC = ({ 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; @@ -51,10 +53,11 @@ const ObjectEditor: React.FC = ({ normalizedSchema, newField.name, fieldSchema, + isPatternProperty, ); // Update required status if needed - if (newField.required) { + if (!isPatternProperty && newField.required) { newSchema = updatePropertyRequired(newSchema, newField.name, true); } @@ -63,16 +66,28 @@ 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); + const schemaProperties = isPatternProperty ? patternProperties : properties; + const property = schemaProperties.find((p) => p.name === oldName); if (!property) return; const propertySchemaObj = asObjectSchema(property.schema); @@ -82,13 +97,14 @@ const ObjectEditor: React.FC = ({ normalizedSchema, newName, propertySchemaObj, + isPatternProperty, ); - if (property.required) { + if (!isPatternProperty && property.required) { newSchema = updatePropertyRequired(newSchema, newName, true); } - newSchema = removeObjectProperty(newSchema, oldName); + newSchema = removeObjectProperty(newSchema, oldName, isPatternProperty); onChange(newSchema); }; @@ -109,11 +125,13 @@ const ObjectEditor: React.FC = ({ const handlePropertySchemaChange = ( propertyName: string, propertySchema: ObjectJSONSchema, + isPatternProperty = false, ) => { const newSchema = updateObjectProperty( normalizedSchema, propertyName, propertySchema, + isPatternProperty, ); onChange(newSchema); }; @@ -132,32 +150,49 @@ const ObjectEditor: React.FC = ({ return (
- {properties.length > 0 ? ( + {allProperties.length > 0 ? (
- {properties.map((property) => ( + {allProperties.map((property) => ( handleDeleteProperty(property.name)} + onDelete={() => + handleDeleteProperty(property.name, property.isPatternProperty) + } onNameChange={(newName) => - handlePropertyNameChange(property.name, newName) + handlePropertyNameChange( + property.name, + newName, + property.isPatternProperty, + ) } onRequiredChange={(required) => handlePropertyRequiredChange(property.name, required) } onSchemaChange={(schema) => - handlePropertySchemaChange(property.name, schema) + handlePropertySchemaChange( + property.name, + schema, + property.isPatternProperty, + ) } depth={depth} + isPatternProperty={property.isPatternProperty} /> ))}
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 86963eb..f5360c4 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -12,6 +12,13 @@ export const de: Translation = { fieldNamePlaceholder: "z.B. firstName, age, isActive", fieldNameTooltip: "CamelCase für bessere Lesbarkeit verwenden (z.B. firstName)", + fieldNameUseRegex: "Regulären Ausdruck verwenden", + fieldNameUseExactName: "Exakten Namen verwenden", + fieldNameRegexLabel: "Regulärer Ausdruck für Eigenschaftsnamen", + fieldNameRegexPlaceholder: "z.B. ^S_", + fieldNameRegexHelp: + "Erlaubt jede Eigenschaft, deren Name auf diesen regulären Ausdruck passt. Passende Eigenschaften müssen diesem Feldschema entsprechen. Verwenden Sie ^ und $, um den ganzen Namen abzugleichen.", + fieldNameRegexError: "Geben Sie einen gültigen regulären Ausdruck ein.", fieldRequiredLabel: "Pflichtfeld", fieldType: "Feldart", fieldTypeExample: "Beispiel:", @@ -41,6 +48,8 @@ export const de: Translation = { propertyDescriptionPlaceholder: "Beschreibung hinzufügen...", propertyDescriptionButton: "Beschreibung hinzufügen...", propertyRequired: "Erforderlich", + propertyNameRegexDescription: + "Erlaubt und validiert Eigenschaften, deren Name auf diesen regulären Ausdruck passt.", propertyOptional: "Optional", propertyDelete: "Feld löschen", diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 367a34b..4e3762b 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -11,6 +11,13 @@ export const en: Translation = { fieldNameLabel: "Field Name", fieldNamePlaceholder: "e.g. firstName, age, isActive", fieldNameTooltip: "Use camelCase for better readability (e.g., firstName)", + fieldNameUseRegex: "Use regular expression", + fieldNameUseExactName: "Use exact name", + fieldNameRegexLabel: "Regular expression for property names", + fieldNameRegexPlaceholder: "e.g. ^S_", + fieldNameRegexHelp: + "Allows any property whose name matches this regular expression. Matching properties must follow this field schema. Use ^ and $ to match the whole name.", + fieldNameRegexError: "Enter a valid regular expression.", fieldRequiredLabel: "Required Field", fieldType: "Field Type", fieldTypeExample: "Example:", @@ -40,6 +47,8 @@ export const en: Translation = { propertyDescriptionPlaceholder: "Add description...", propertyDescriptionButton: "Add description...", propertyRequired: "Required", + propertyNameRegexDescription: + "Allows and validates properties whose names match this regular expression.", propertyOptional: "Optional", propertyDelete: "Delete field", diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 24ec987..1b12a65 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -11,6 +11,13 @@ export const es: Translation = { fieldNameLabel: "Nombre del campo", fieldNamePlaceholder: "ej. firstName, age, isActive", fieldNameTooltip: "Usa camelCase para mejor legibilidad (ej., firstName)", + fieldNameUseRegex: "Usar expresión regular", + fieldNameUseExactName: "Usar nombre exacto", + fieldNameRegexLabel: "Expresión regular para nombres de propiedades", + fieldNameRegexPlaceholder: "ej. ^S_", + fieldNameRegexHelp: + "Permite cualquier propiedad cuyo nombre coincida con esta expresión regular. Las propiedades coincidentes deben cumplir este esquema de campo. Usa ^ y $ para coincidir con el nombre completo.", + fieldNameRegexError: "Introduce una expresión regular válida.", fieldRequiredLabel: "Campo obligatorio", fieldType: "Tipo de campo", fieldTypeExample: "Ejemplo:", @@ -41,6 +48,8 @@ export const es: Translation = { propertyDescriptionPlaceholder: "Agregar descripción...", propertyDescriptionButton: "Agregar descripción...", propertyRequired: "Requerido", + propertyNameRegexDescription: + "Permite y valida propiedades cuyo nombre coincide con esta expresión regular.", propertyOptional: "Opcional", propertyDelete: "Eliminar campo", diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 9971786..b83ae5c 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -12,6 +12,13 @@ export const fr: Translation = { fieldNamePlaceholder: "ex. prenom, age, estActif", fieldNameTooltip: "Utilisez camelCase pour une meilleure lisibilité (ex. prenom)", + fieldNameUseRegex: "Utiliser une expression régulière", + fieldNameUseExactName: "Utiliser un nom exact", + fieldNameRegexLabel: "Expression régulière pour les noms de propriétés", + fieldNameRegexPlaceholder: "ex. ^S_", + fieldNameRegexHelp: + "Autorise toute propriété dont le nom correspond à cette expression régulière. Les propriétés correspondantes doivent respecter ce schéma de champ. Utilisez ^ et $ pour cibler le nom entier.", + fieldNameRegexError: "Saisissez une expression régulière valide.", fieldRequiredLabel: "Champ obligatoire", fieldType: "Type de champ", fieldTypeExample: "Exemple:", @@ -42,6 +49,8 @@ export const fr: Translation = { propertyDescriptionPlaceholder: "Ajouter une description...", propertyDescriptionButton: "Ajouter une description...", propertyRequired: "Obligatoire", + propertyNameRegexDescription: + "Autorise et valide les propriétés dont le nom correspond à cette expression régulière.", propertyOptional: "Facultatif", propertyDelete: "Supprimer le champ", diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index 0ffc8a4..bfd4a1c 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -11,6 +11,13 @@ export const pl: Translation = { fieldNameLabel: "Nazwa pola", fieldNamePlaceholder: "np. firstName, age, isActive", fieldNameTooltip: "Używaj camelCase dla lepszej czytelności (np. firstName)", + fieldNameUseRegex: "Użyj wyrażenia regularnego", + fieldNameUseExactName: "Użyj dokładnej nazwy", + fieldNameRegexLabel: "Wyrażenie regularne dla nazw właściwości", + fieldNameRegexPlaceholder: "np. ^S_", + fieldNameRegexHelp: + "Dopuszcza każdą właściwość, której nazwa pasuje do tego wyrażenia regularnego. Pasujące właściwości muszą spełniać ten schemat pola. Użyj ^ i $, aby dopasować całą nazwę.", + fieldNameRegexError: "Wpisz poprawne wyrażenie regularne.", fieldRequiredLabel: "Pole wymagane", fieldType: "Typ pola", fieldTypeExample: "Przykład:", @@ -40,6 +47,8 @@ export const pl: Translation = { propertyDescriptionPlaceholder: "Dodaj opis...", propertyDescriptionButton: "Dodaj opis...", propertyRequired: "Wymagane", + propertyNameRegexDescription: + "Dopuszcza i waliduje właściwości, których nazwa pasuje do tego wyrażenia regularnego.", propertyOptional: "Opcjonalne", propertyDelete: "Usuń pole", diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 2384ffe..88902be 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -12,6 +12,13 @@ export const ru: Translation = { fieldNamePlaceholder: "например, firstName, age, isActive", fieldNameTooltip: "Используйте camelCase для лучшей читаемости (например, firstName)", + fieldNameUseRegex: "Использовать регулярное выражение", + fieldNameUseExactName: "Использовать точное имя", + fieldNameRegexLabel: "Регулярное выражение для имен свойств", + fieldNameRegexPlaceholder: "например, ^S_", + fieldNameRegexHelp: + "Разрешает любое свойство, имя которого совпадает с этим регулярным выражением. Совпавшие свойства должны соответствовать этой схеме поля. Используйте ^ и $, чтобы проверять имя целиком.", + fieldNameRegexError: "Введите допустимое регулярное выражение.", fieldRequiredLabel: "Обязательное поле", fieldType: "Тип поля", fieldTypeExample: "Пример:", @@ -42,6 +49,8 @@ export const ru: Translation = { propertyDescriptionPlaceholder: "Добавить описание...", propertyDescriptionButton: "Добавить описание...", propertyRequired: "Обязательное", + propertyNameRegexDescription: + "Разрешает и проверяет свойства, имена которых совпадают с этим регулярным выражением.", propertyOptional: "Необязательное", propertyDelete: "Удалить поле", diff --git a/src/i18n/locales/uk.ts b/src/i18n/locales/uk.ts index eb7e461..71dd543 100644 --- a/src/i18n/locales/uk.ts +++ b/src/i18n/locales/uk.ts @@ -12,6 +12,13 @@ export const uk: Translation = { fieldNamePlaceholder: "наприклад, firstName, age, isActive", fieldNameTooltip: "Використовуйте camelCase для кращої читабельності (наприклад, firstName)", + fieldNameUseRegex: "Використати регулярний вираз", + fieldNameUseExactName: "Використати точну назву", + fieldNameRegexLabel: "Регулярний вираз для назв властивостей", + fieldNameRegexPlaceholder: "наприклад, ^S_", + fieldNameRegexHelp: + "Дозволяє будь-яку властивість, назва якої збігається з цим регулярним виразом. Властивості, що збігаються, мають відповідати цій схемі поля. Використовуйте ^ і $, щоб перевіряти назву повністю.", + fieldNameRegexError: "Введіть коректний регулярний вираз.", fieldRequiredLabel: "Обов'язкове поле", fieldType: "Тип поля", fieldTypeExample: "Приклад:", @@ -41,6 +48,8 @@ export const uk: Translation = { propertyDescriptionPlaceholder: "Додати опис...", propertyDescriptionButton: "Додати опис...", propertyRequired: "Обов'язкове", + propertyNameRegexDescription: + "Дозволяє й перевіряє властивості, назви яких збігаються з цим регулярним виразом.", propertyOptional: "Необов'язкове", propertyDelete: "Видалити поле", diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index e44180a..8de4142 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -11,6 +11,13 @@ export const zh: Translation = { fieldNameLabel: "字段名称", fieldNamePlaceholder: "例如:firstName, age, isActive", fieldNameTooltip: "使用驼峰命名法提高可读性(例如:firstName)", + fieldNameUseRegex: "使用正则表达式", + fieldNameUseExactName: "使用精确名称", + fieldNameRegexLabel: "属性名称正则表达式", + fieldNameRegexPlaceholder: "例如:^S_", + fieldNameRegexHelp: + "允许名称匹配此正则表达式的任何属性。匹配的属性必须符合此字段 schema。使用 ^ 和 $ 匹配完整名称。", + fieldNameRegexError: "请输入有效的正则表达式。", fieldRequiredLabel: "必填字段", fieldType: "字段类型", fieldTypeExample: "示例:", @@ -40,6 +47,7 @@ export const zh: Translation = { propertyDescriptionPlaceholder: "添加描述...", propertyDescriptionButton: "添加描述...", propertyRequired: "必填", + propertyNameRegexDescription: "允许并验证名称匹配此正则表达式的属性。", propertyOptional: "可选", propertyDelete: "删除字段", diff --git a/src/i18n/translation-keys.ts b/src/i18n/translation-keys.ts index fa74f3a..8982e1e 100644 --- a/src/i18n/translation-keys.ts +++ b/src/i18n/translation-keys.ts @@ -42,6 +42,42 @@ export interface Translation { * > Use camelCase for better readability (e.g., firstName) */ readonly fieldNameTooltip: string; + /** + * The translation for the key `fieldNameUseRegex`. English default is: + * + * > Use regular expression + */ + readonly fieldNameUseRegex: string; + /** + * The translation for the key `fieldNameUseExactName`. English default is: + * + * > Use exact name + */ + readonly fieldNameUseExactName: string; + /** + * The translation for the key `fieldNameRegexLabel`. English default is: + * + * > Regular expression for property names + */ + readonly fieldNameRegexLabel: string; + /** + * The translation for the key `fieldNameRegexPlaceholder`. English default is: + * + * > e.g. ^S_ + */ + readonly fieldNameRegexPlaceholder: string; + /** + * The translation for the key `fieldNameRegexHelp`. English default is: + * + * > Allows any property whose name matches this regular expression. Matching properties must follow this field schema. Use ^ and $ to match the whole name. + */ + readonly fieldNameRegexHelp: string; + /** + * The translation for the key `fieldNameRegexError`. English default is: + * + * > Enter a valid regular expression. + */ + readonly fieldNameRegexError: string; /** * The translation for the key `fieldRequiredLabel`. English default is: * @@ -219,6 +255,12 @@ export interface Translation { * > Required */ readonly propertyRequired: string; + /** + * The translation for the key `propertyNameRegexDescription`. English default is: + * + * > Allows and validates properties whose names match this regular expression. + */ + readonly propertyNameRegexDescription: string; /** * The translation for the key `propertyOptional`. English default is: * diff --git a/src/lib/schemaEditor.ts b/src/lib/schemaEditor.ts index 3475f5d..9d2ee50 100644 --- a/src/lib/schemaEditor.ts +++ b/src/lib/schemaEditor.ts @@ -9,6 +9,7 @@ export type Property = { name: string; schema: JSONSchema; required: boolean; + isPatternProperty?: boolean; }; export function copySchema(schema: T): T { @@ -23,15 +24,17 @@ 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 = {}; + const schemaProperty = isPatternProperty ? "patternProperties" : "properties"; + if (!newSchema[schemaProperty]) { + newSchema[schemaProperty] = {}; } - newSchema.properties[propertyName] = propertySchema; + newSchema[schemaProperty][propertyName] = propertySchema; return newSchema; } @@ -41,15 +44,21 @@ 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]; + if (isPatternProperty && Object.keys(remainingProps).length === 0) { + delete newSchema.patternProperties; + } else { + newSchema[schemaProperty] = remainingProps; + } // Also remove from required array if present - if (newSchema.required) { + if (!isPatternProperty && newSchema.required) { newSchema.required = newSchema.required.filter( (name) => name !== propertyName, ); @@ -136,15 +145,20 @@ 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), + required: !isPatternProperty && required.includes(name), + isPatternProperty, })); } @@ -165,14 +179,16 @@ 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 { @@ -180,10 +196,10 @@ export function renameObjectProperty( } } - newSchema.properties = newProperties; + newSchema[schemaProperty] = newProperties; // Update required array if the field name changed - if (newSchema.required) { + if (!isPatternProperty && newSchema.required) { newSchema.required = newSchema.required.map((field) => field === oldName ? newName : field, ); From 5ae5b309302ef27ff9843305c1c1ee0ea7cf264d Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Thu, 21 May 2026 14:47:44 +0200 Subject: [PATCH 2/3] clean regex field architecture --- .../SchemaEditor/AddFieldButton.tsx | 48 ++-- .../SchemaEditor/SchemaFieldList.tsx | 225 +++++++++--------- .../SchemaEditor/SchemaPropertyEditor.tsx | 126 +++++++--- .../SchemaEditor/SchemaPropertyRows.tsx | 92 +++++++ .../SchemaEditor/SchemaVisualEditor.tsx | 80 +++++-- .../SchemaEditor/types/ObjectEditor.tsx | 158 ++++++------ src/lib/schemaEditor.ts | 145 ++++++++--- 7 files changed, 552 insertions(+), 322 deletions(-) create mode 100644 src/components/SchemaEditor/SchemaPropertyRows.tsx diff --git a/src/components/SchemaEditor/AddFieldButton.tsx b/src/components/SchemaEditor/AddFieldButton.tsx index 10fb458..6469940 100644 --- a/src/components/SchemaEditor/AddFieldButton.tsx +++ b/src/components/SchemaEditor/AddFieldButton.tsx @@ -23,13 +23,15 @@ import type { NewField, SchemaType } from "../../types/jsonSchema.ts"; import SchemaTypeSelector from "./SchemaTypeSelector.tsx"; interface AddFieldButtonProps { - onAddField: (field: NewField, isPatternProperty?: boolean) => void; + onAddField: (field: NewField) => void; + onAddPatternField: (field: NewField) => void; variant?: "primary" | "secondary"; autoFocus?: boolean; } const AddFieldButton: FC = ({ onAddField, + onAddPatternField, variant = "primary", autoFocus = true, }) => { @@ -48,9 +50,8 @@ const AddFieldButton: FC = ({ const additionalPropertiesId = useId(); const t = useTranslation(); - const isPatternProperty = useNameRegex; const regexError = (() => { - if (!isPatternProperty || !fieldName.trim()) return ""; + if (!useNameRegex || !fieldName.trim()) return ""; try { new RegExp(fieldName); return ""; @@ -71,17 +72,20 @@ const AddFieldButton: FC = ({ if (!fieldName.trim()) return; if (regexError) return; - onAddField( - { - name: fieldName, - type: fieldType, - description: fieldDesc, - required: isPatternProperty ? false : fieldRequired, - additionalProperties: - fieldType === "object" ? additionalProperties : undefined, - }, - isPatternProperty, - ); + const field = { + name: fieldName, + type: fieldType, + description: fieldDesc, + required: useNameRegex ? false : fieldRequired, + additionalProperties: + fieldType === "object" ? additionalProperties : undefined, + }; + + if (useNameRegex) { + onAddPatternField(field); + } else { + onAddField(field); + } setFieldName(""); setFieldType("string"); @@ -131,9 +135,7 @@ const AddFieldButton: FC = ({ htmlFor={fieldNameId} className="text-sm font-medium" > - {isPatternProperty - ? t.fieldNameRegexLabel - : t.fieldNameLabel} + {useNameRegex ? t.fieldNameRegexLabel : t.fieldNameLabel} @@ -142,7 +144,7 @@ const AddFieldButton: FC = ({

- {isPatternProperty + {useNameRegex ? t.fieldNameRegexHelp : t.fieldNameTooltip}

@@ -154,7 +156,7 @@ const AddFieldButton: FC = ({ onClick={() => setNameRegexMode(!useNameRegex)} className="text-xs text-muted-foreground hover:text-foreground underline-offset-2 hover:underline" > - {isPatternProperty + {useNameRegex ? t.fieldNameUseExactName : t.fieldNameUseRegex} @@ -164,19 +166,19 @@ const AddFieldButton: FC = ({ value={fieldName} onChange={(e) => setFieldName(e.target.value)} placeholder={ - isPatternProperty + useNameRegex ? t.fieldNameRegexPlaceholder : t.fieldNamePlaceholder } aria-describedby={ - isPatternProperty ? fieldNameHelpId : undefined + useNameRegex ? fieldNameHelpId : undefined } aria-invalid={regexError ? true : undefined} className="font-mono text-sm w-full" autoFocus={autoFocus} required /> - {isPatternProperty ? ( + {useNameRegex ? (

= ({ />

- {isPatternProperty ? null : ( + {useNameRegex ? null : (
void; onDeleteEnum?: (ctx: EnumChangeContext) => void; - onAddField: (newField: NewField, isPatternProperty?: boolean) => void; - onEditField: ( - name: string, - updatedField: NewField, - isPatternProperty?: boolean, - ) => void; - onDeleteField: (name: string, isPatternProperty?: boolean) => void; + onEditField: (name: string, updatedField: NewField) => void; + onDeleteField: (name: string) => void; + onEditPatternField: (name: string, updatedField: NewField) => void; + onDeletePatternField: (name: string) => void; autoFocus?: boolean; } @@ -35,6 +36,8 @@ const SchemaFieldList: FC = ({ schema, onEditField, onDeleteField, + onEditPatternField, + onDeletePatternField, onAddEnum, onDeleteEnum, readOnly = false, @@ -44,8 +47,7 @@ const SchemaFieldList: FC = ({ // Get the properties from the schema const properties = getSchemaProperties(schema); - const patternProperties = getSchemaProperties(schema, true); - const allProperties = [...properties, ...patternProperties]; + const patternProperties = getSchemaPatternProperties(schema); // Get schema type as a valid SchemaType const getValidSchemaType = (propSchema: JSONSchemaType): SchemaType => { @@ -68,100 +70,90 @@ const SchemaFieldList: FC = ({ return type || "object"; }; + const createUpdatedField = ( + property: Property, + overrides: Partial = {}, + ): NewField => ({ + name: property.name, + type: getValidSchemaType(property.schema), + description: + typeof property.schema === "boolean" + ? "" + : property.schema.description || "", + required: property.required, + validation: + typeof property.schema === "boolean" + ? { type: "object" } + : property.schema, + ...overrides, + }); + + const updateProperty = ( + schemaProperties: Property[], + name: string, + editField: (name: string, updatedField: NewField) => void, + overrides: Partial, + ) => { + const property = schemaProperties.find((prop) => prop.name === name); + if (!property) return; + + editField(name, createUpdatedField(property, overrides)); + }; + // Handle field name change (generates an edit event) const handleNameChange = ( + schemaProperties: Property[], + editField: (name: string, updatedField: NewField) => void, 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, - }, - isPatternProperty, - ); + updateProperty(schemaProperties, oldName, editField, { name: newName }); }; // Handle required status change const handleRequiredChange = (name: string, required: boolean) => { - const property = properties.find((prop) => prop.name === name); - if (!property) return; - - onEditField(name, { - name, - type: getValidSchemaType(property.schema), - description: - typeof property.schema === "boolean" - ? "" - : property.schema.description || "", - required, - validation: - typeof property.schema === "boolean" - ? { type: "object" } - : property.schema, - }); + updateProperty(properties, name, onEditField, { required }); }; - // Handle schema change - const handleSchemaChange = ( - name: string, + const createFieldForSchemaChange = ( + property: Property, updatedSchema: ObjectJSONSchema, - isPatternProperty = false, - ) => { - const schemaProperties = isPatternProperty ? patternProperties : properties; - const property = schemaProperties.find((prop) => prop.name === name); - if (!property) return; - + ): NewField => { // combinator schemas have no direct type field if ( isAnyOfSchema(updatedSchema) || isOneOfSchema(updatedSchema) || isAllOfSchema(updatedSchema) ) { - onEditField( - name, - { - name, - type: "object", - description: updatedSchema.description || "", - required: property.required, - validation: updatedSchema, - }, - isPatternProperty, - ); - return; + return createUpdatedField(property, { + type: "object", + description: updatedSchema.description || "", + validation: updatedSchema, + }); } 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, - { - name, - type: validType, - description: updatedSchema.description || "", - required: property.required, - validation: updatedSchema, - }, - isPatternProperty, - ); + return createUpdatedField(property, { + type: validType, + description: updatedSchema.description || "", + validation: updatedSchema, + }); + }; + + // Handle schema change + const handleSchemaChange = ( + schemaProperties: Property[], + editField: (name: string, updatedField: NewField) => void, + name: string, + updatedSchema: ObjectJSONSchema, + ) => { + const property = schemaProperties.find((prop) => prop.name === name); + if (!property) return; + + editField(name, createFieldForSchemaChange(property, updatedSchema)); }; const validationTree = useMemo( @@ -171,43 +163,40 @@ const SchemaFieldList: FC = ({ return (
- {allProperties.map((property) => ( - - onDeleteField(property.name, property.isPatternProperty) - } - onNameChange={(newName) => - handleNameChange(property.name, newName, property.isPatternProperty) - } - onRequiredChange={(required) => - handleRequiredChange(property.name, required) - } - onSchemaChange={(schema) => - handleSchemaChange( - property.name, - schema, - property.isPatternProperty, - ) - } - readOnly={readOnly} - isPatternProperty={property.isPatternProperty} - autoFocus={autoFocus} - /> - ))} + + handleNameChange(properties, onEditField, oldName, newName) + } + onPatternNameChange={(oldName, newName) => + handleNameChange( + patternProperties, + onEditPatternField, + oldName, + newName, + ) + } + onRequiredChange={handleRequiredChange} + onSchemaChange={(name, updatedSchema) => + handleSchemaChange(properties, onEditField, name, updatedSchema) + } + onPatternSchemaChange={(name, updatedSchema) => + handleSchemaChange( + patternProperties, + onEditPatternField, + name, + updatedSchema, + ) + } + readOnly={readOnly} + autoFocus={autoFocus} + />
); }; diff --git a/src/components/SchemaEditor/SchemaPropertyEditor.tsx b/src/components/SchemaEditor/SchemaPropertyEditor.tsx index d681684..c435b0f 100644 --- a/src/components/SchemaEditor/SchemaPropertyEditor.tsx +++ b/src/components/SchemaEditor/SchemaPropertyEditor.tsx @@ -1,5 +1,5 @@ -import { ChevronDown, ChevronRight, X } from "lucide-react"; -import { useEffect, useState } from "react"; +import { ChevronDown, ChevronRight, Regex, X } from "lucide-react"; +import { type ReactNode, useEffect, useState } from "react"; import { Input } from "../../components/ui/input.tsx"; import { useTranslation } from "../../hooks/use-translation.ts"; import { cn } from "../../lib/utils.ts"; @@ -34,14 +34,31 @@ export interface SchemaPropertyEditorProps { onRequiredChange: (required: boolean) => void; onSchemaChange: (schema: ObjectJSONSchema) => void; depth?: number; - isPatternProperty?: boolean; } -export const SchemaPropertyEditor: React.FC = ({ +interface SchemaPropertyEditorFrameProps { + name: string; + schema: JSONSchema; + schemaKey?: string; + readOnly: boolean; + autoFocus?: boolean; + validationNode?: ValidationTreeNode; + onAddEnum?: (ctx: EnumChangeContext) => void; + onDeleteEnum?: (ctx: EnumChangeContext) => void; + onDelete: () => void; + onNameChange: (newName: string) => void; + onSchemaChange: (schema: ObjectJSONSchema) => void; + depth?: number; + nameAriaLabel?: string; + nameClassName?: string; + nameTitle?: string; + statusControl: ReactNode; +} + +const SchemaPropertyEditorFrame: React.FC = ({ name, schema, schemaKey, - required, readOnly = false, autoFocus = true, validationNode, @@ -49,10 +66,12 @@ export const SchemaPropertyEditor: React.FC = ({ onDeleteEnum, onDelete, onNameChange, - onRequiredChange, onSchemaChange, depth = 0, - isPatternProperty = false, + nameAriaLabel, + nameClassName, + nameTitle, + statusControl, }) => { const t = useTranslation(); const [expanded, setExpanded] = useState(false); @@ -137,19 +156,11 @@ export const SchemaPropertyEditor: React.FC = ({ type="button" onClick={() => setIsEditingName(true)} onKeyDown={(e) => e.key === "Enter" && setIsEditingName(true)} - aria-label={ - isPatternProperty - ? `${t.fieldNameRegexLabel}: ${name}` - : undefined - } - title={ - isPatternProperty - ? t.propertyNameRegexDescription - : undefined - } + aria-label={nameAriaLabel} + title={nameTitle} className={cn( "json-field-label font-medium cursor-text px-2 py-0.5 -mx-0.5 rounded-sm hover:bg-secondary/30 hover:shadow-xs hover:ring-1 hover:ring-ring/20 transition-all text-left truncate min-w-[80px] max-w-[50%]", - isPatternProperty && "font-mono", + nameClassName, )} > {name} @@ -228,26 +239,8 @@ export const SchemaPropertyEditor: React.FC = ({ }} /> - {/* Required toggle */} - {isPatternProperty ? ( - - /.*/ - - ) : ( - !readOnly && onRequiredChange(!required)} - className={ - required - ? "bg-red-50 text-red-500" - : "bg-secondary text-muted-foreground" - } - > - {required ? t.propertyRequired : t.propertyOptional} - - )} + {/* Status */} + {statusControl}
@@ -297,4 +290,61 @@ export const SchemaPropertyEditor: React.FC = ({ ); }; +export const SchemaPropertyEditor: React.FC = ({ + required, + readOnly = false, + onRequiredChange, + ...props +}) => { + const t = useTranslation(); + + return ( + !readOnly && onRequiredChange(!required)} + className={ + required + ? "bg-red-50 text-red-500" + : "bg-secondary text-muted-foreground" + } + > + {required ? t.propertyRequired : t.propertyOptional} + + } + /> + ); +}; + +export type PatternSchemaPropertyEditorProps = Omit< + SchemaPropertyEditorFrameProps, + "nameAriaLabel" | "nameClassName" | "nameTitle" | "statusControl" +>; + +export const PatternSchemaPropertyEditor: React.FC< + PatternSchemaPropertyEditorProps +> = (props) => { + const t = useTranslation(); + + return ( + +