Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demo/pages/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ const Index = () => {
<SelectItem value="uk">Ukrainian</SelectItem>
<SelectItem value="es">Spanish</SelectItem>
<SelectItem value="zh">Chinese</SelectItem>
<SelectItem value="pl">Polish</SelectItem>
</SelectContent>
</Select>
</div>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand Down
123 changes: 101 additions & 22 deletions src/components/SchemaEditor/AddFieldButton.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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";
}

Expand All @@ -35,29 +37,41 @@ const AddFieldButton: FC<AddFieldButtonProps> = ({
const [fieldType, setFieldType] = useState<SchemaType>("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<HTMLInputElement>(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 (
Expand Down Expand Up @@ -111,14 +125,52 @@ const AddFieldButton: FC<AddFieldButtonProps> = ({
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/* Properties toggle */}
<ButtonToggle
onClick={() => {
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}
</ButtonToggle>
</div>
<Input
id={fieldNameId}
value={fieldName}
onChange={(e) => 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}
/>
</div>

Expand Down Expand Up @@ -149,19 +201,46 @@ const AddFieldButton: FC<AddFieldButtonProps> = ({
className="text-sm w-full"
/>
</div>

<div className="flex items-center gap-3 p-3 rounded-lg border bg-muted/50">
<input
type="checkbox"
id={fieldRequiredId}
checked={fieldRequired}
onChange={(e) => setFieldRequired(e.target.checked)}
className="rounded border-gray-300 shrink-0"
/>
<label htmlFor={fieldRequiredId} className="text-sm">
{t.fieldRequiredLabel}
</label>
</div>
{isProperty ? null : (
<div className="flex items-center gap-3 p-3 rounded-lg border bg-muted/50">
<input
type="checkbox"
id={fieldRequiredId}
checked={fieldRequired}
onChange={(e) => setFieldRequired(e.target.checked)}
className="rounded border-gray-300 shrink-0"
/>
<label htmlFor={fieldRequiredId} className="text-sm">
{t.fieldRequiredLabel}
</label>
</div>
)}
{fieldType === "object" ? (
<div className="flex items-center gap-3 p-3 rounded-lg border bg-muted/50">
<input
type="checkbox"
id={additionalPropertiesId}
checked={additionalProperties}
onChange={(e) =>
setAdditionalProperties(e.target.checked)
}
className="rounded border-gray-300 shrink-0"
/>
<label htmlFor={additionalPropertiesId} className="text-sm">
{t.additionalPropertiesAllow}
</label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-4 w-4 text-muted-foreground shrink-0" />
</TooltipTrigger>
<TooltipContent className="max-w-[90vw]">
<p>{t.additionalPropertiesTooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
) : null}
</div>

<div className="space-y-4 min-w-[280px]">
Expand Down
101 changes: 76 additions & 25 deletions src/components/SchemaEditor/SchemaFieldList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SchemaFieldListProps> = ({
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 => {
Expand All @@ -43,23 +50,33 @@ const SchemaFieldList: FC<SchemaFieldListProps> = ({
};

// 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
Expand All @@ -86,21 +103,28 @@ const SchemaFieldList: FC<SchemaFieldListProps> = ({
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(
Expand All @@ -110,6 +134,7 @@ const SchemaFieldList: FC<SchemaFieldListProps> = ({

return (
<div className="space-y-2 animate-in">
{properties.length > 0 ? <h3>{t.regularPropertiesTitle}:</h3> : null}
{properties.map((property) => (
<SchemaPropertyEditor
key={property.name}
Expand All @@ -124,6 +149,32 @@ const SchemaFieldList: FC<SchemaFieldListProps> = ({
}
onSchemaChange={(schema) => handleSchemaChange(property.name, schema)}
readOnly={readOnly}
onPropertyToggle={onPropertyToggle}
/>
))}
{patternProperties.length > 0 ? (
<h3>{t.patternPropertiesTitle}:</h3>
) : null}
{patternProperties.map((property) => (
<SchemaPropertyEditor
key={property.name}
name={property.name}
schema={property.schema}
required={property.required}
validationNode={validationTree.children[property.name] ?? undefined}
onDelete={() => 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
/>
))}
</div>
Expand Down
Loading