Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SchemaEditor UX Improve #123

Merged
merged 13 commits into from
Jan 10, 2024
88 changes: 66 additions & 22 deletions src/components/SchemaEditor/SchemaEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,10 @@ import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline";
import SchemaPropertyEditor from "./SchemaPropertyEditor";
import { v4 as uuidv4 } from "uuid";

import { Box, Typography } from "@mui/material";
import React, { forwardRef, useEffect, useState } from "react";
import { TreeItem, TreeView } from "@mui/lab";
import {
addProperty,
changeProperty,
getTypeStyle,
removeProperty,
} from "./SchemaUtils";
import { addProperty, changeProperty, removeProperty } from "./SchemaUtils";

const SchemaEditor = forwardRef(
({ initialData = {}, customTypes = [] }, ref) => {
Expand Down Expand Up @@ -42,7 +38,7 @@ const SchemaEditor = forwardRef(
}, [initialData, schemaData]);

const handleAddProperty = (newProperty, parentId = null) => {
addProperty(newProperty, parentId, setSchemaData);
addProperty(parentId, setSchemaData);
};

const handleRemoveProperty = (propertyId) => {
Expand All @@ -66,7 +62,14 @@ const SchemaEditor = forwardRef(
width: "100%",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "1px",
width: "100%",
}}
>
<SchemaPropertyEditor
node={node}
onNameChange={(newName) => {
Expand All @@ -83,7 +86,7 @@ const SchemaEditor = forwardRef(
}}
customTypes={customTypes}
/>
{(node.type === "object" || node.type === "array") && (
{true && (
<IconButton
size="small"
onClick={(e) => {
Expand All @@ -95,17 +98,20 @@ const SchemaEditor = forwardRef(
);
}}
disabled={
node.type === "array" &&
node.properties &&
node.properties.length >= 1
node.type !== "object" ||
(node.type === "array" && node.properties.length >= 1)
}
sx={{
color: (theme) => theme.palette.grey[600],
marginRight: "-8px ",
}}
>
<AddCircleOutlineIcon fontSize="small" />
</IconButton>
)}
</div>

{level > 0 && (
{true && (
<IconButton
size="small"
style={{ marginLeft: "auto" }}
Expand All @@ -114,6 +120,10 @@ const SchemaEditor = forwardRef(
e.stopPropagation();
handleRemoveProperty(node.id);
}}
disabled={level === 0}
sx={{
color: (theme) => theme.palette.grey[600],
}}
>
<RemoveCircleOutlineIcon fontSize="small" />
</IconButton>
Expand All @@ -139,23 +149,41 @@ const SchemaEditor = forwardRef(
)?.schema;

if (!customTypeSchema || !customTypeSchema.properties) {
return <div style={{ paddingLeft: "20px" }}>No properties defined</div>;
return <Box sx={{ paddingLeft: "20px" }}>No properties defined</Box>;
}

return (
<div style={{ paddingLeft: "20px" }}>
<Box sx={{ paddingLeft: "20px" }}>
{customTypeSchema.properties.map((prop, index) => (
<div
<Box
key={index}
style={{ paddingTop: "5px", paddingBottom: "5px" }}
sx={{
paddingTop: "5px",
paddingBottom: "5px",
display: "flex",
alignItems: "center",
}}
>
<span>{prop.name}:</span>
<span style={{ ...getTypeStyle(prop.type), marginLeft: "8px" }}>
<Typography
variant="body2"
sx={{
color: (theme) => theme.palette.grey[600],
}}
>
{prop.name}
</Typography>
<Typography
variant="body2"
sx={{
marginLeft: "8px",
color: (theme) => theme.palette.grey[500],
}}
>
{prop.type}
</span>
</div>
</Typography>
</Box>
))}
</div>
</Box>
);
};

Expand Down Expand Up @@ -199,9 +227,25 @@ const SchemaEditor = forwardRef(
width: "100%",
display: "flex",
justifyContent: "space-between",
padding: "1px 8px",
borderRadius: "4px",
margin: "1px 0",
transition: "all 0.3s",
},
".MuiTreeItem-label": {
width: "100%",
fontWeight: "bold",
},
".MuiTreeItem-group": {
marginLeft: "16px !important",
paddingLeft: "8px",
borderLeft: `1px solid`,
borderColor: (theme) => theme.palette.grey[400],
},
".MuiTreeItem-iconContainer": {
minWidth: "0",
marginRight: "0px",
padding: "0px",
},
}}
>
Expand Down
80 changes: 38 additions & 42 deletions src/components/SchemaEditor/SchemaEditor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,24 @@ describe("SchemaEditor Component", () => {
});

test("adds a new property", () => {
render(<SchemaEditor initialData={initialSchema} />);
const emptySchema = { type: "object", properties: [] };
render(<SchemaEditor initialData={emptySchema} />);

act(() => {
SchemaEditor.addProperty({ type: "string", name: "additional" });
SchemaEditor.addProperty(null);
});
const currentSchema = SchemaEditor.schemaOutput();
expect(currentSchema).toEqual({
...initialSchema,
properties: [
...initialSchema.properties,
{ type: "string", name: "additional" },
],

let updatedSchema = SchemaEditor.schemaOutput();
expect(updatedSchema.properties).toHaveLength(1);
expect(updatedSchema.properties[0].name).toBe("id");

act(() => {
SchemaEditor.addProperty(null);
});

updatedSchema = SchemaEditor.schemaOutput();
expect(updatedSchema.properties).toHaveLength(2);
expect(updatedSchema.properties[1].name).toBe("prop2");
});

test("removes a property by ID", () => {
Expand Down Expand Up @@ -117,49 +123,33 @@ describe("SchemaEditor Component", () => {
test("adds a nested object and a property to it", () => {
render(<SchemaEditor initialData={initialSchema} />);

const newNestedObject = { type: "object", name: "newNestedObject" };
act(() => {
SchemaEditor.addProperty(newNestedObject);
SchemaEditor.addProperty(null);
});

const currentSchemaWithIDs = SchemaEditor.schemaOutputWithIDs();

const nestedObjectId = currentSchemaWithIDs.properties.find(
(prop) => prop.name === "newNestedObject"
let updatedSchemaWithIDs = SchemaEditor.schemaOutputWithIDs();
const newNestedObjectId = updatedSchemaWithIDs.properties.find(
(prop) => prop.name === "prop4"
).id;
if (!nestedObjectId) {
throw new Error("Nested object not found");
}

const newPropertyForNestedObject = {
type: "string",
name: "nestedProperty",
};
act(() => {
SchemaEditor.addProperty(newPropertyForNestedObject, nestedObjectId);
SchemaEditor.addProperty(newNestedObjectId);
});

const updatedSchemaWithIDs = SchemaEditor.schemaOutputWithIDs();

updatedSchemaWithIDs = SchemaEditor.schemaOutputWithIDs();
const updatedNestedObject = updatedSchemaWithIDs.properties.find(
(prop) => prop.id === nestedObjectId
(prop) => prop.id === newNestedObjectId
);
console.log("updatedNestedObject: ", updatedNestedObject);
expect(updatedNestedObject).toBeDefined();
const hasNestedProperty = updatedNestedObject.properties.some(
(prop) =>
prop.name === newPropertyForNestedObject.name &&
prop.type === newPropertyForNestedObject.type
);
expect(hasNestedProperty).toBe(true);
expect(updatedNestedObject.name).toBe("prop4");
});

test("adds a property with a custom type", () => {
test("changes a property to custom type", () => {
const customTypes = [
{
name: "Item",
type: "OPENAPI",
schema: {
name: "Item",
type: "object",
properties: [
{ type: "string", name: "id" },
Expand All @@ -174,16 +164,22 @@ describe("SchemaEditor Component", () => {
<SchemaEditor initialData={initialSchema} customTypes={customTypes} />
);

const newPropertyWithCustomType = { type: "Item", name: "newItem" };
const currentSchemaWithIDs = SchemaEditor.schemaOutputWithIDs();
const propertyToChange = currentSchemaWithIDs.properties[0];

const change = {
type: "Item",
};

act(() => {
SchemaEditor.addProperty(newPropertyWithCustomType);
SchemaEditor.changeProperty(propertyToChange.id, change);
});

const updatedSchema = SchemaEditor.schemaOutput();
const addedProperty = updatedSchema.properties.find(
(prop) => prop.name === "newItem"
const updatedSchemaWithIDs = SchemaEditor.schemaOutputWithIDs();
const updatedProperty = updatedSchemaWithIDs.properties.find(
(prop) => prop.id === propertyToChange.id
);
expect(addedProperty).toBeDefined();
expect(addedProperty.type).toBe("Item");
expect(updatedProperty).toBeDefined();
expect(updatedProperty.type).toBe("Item");
});
});
69 changes: 52 additions & 17 deletions src/components/SchemaEditor/SchemaPropertyEditor.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { getTypeStyle } from "./SchemaUtils";

import { Input, MenuItem, Select } from "@mui/material";
import { Box, Input, MenuItem, Select, Typography } from "@mui/material";
import React, { useState } from "react";

const SchemaPropertyEditor = ({
Expand Down Expand Up @@ -33,15 +31,22 @@ const SchemaPropertyEditor = ({

const propertyTypes = [
"string",
"integer",
"number",
"boolean",
"object",
"array",
...customTypes.map((type) => type.name),
];

return (
<div style={{ display: "flex", gap: "8px" }}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
width: "100%",
gap: "4px",
}}
>
{editMode === "name" ? (
<Input
value={name}
Expand All @@ -51,9 +56,30 @@ const SchemaPropertyEditor = ({
disableUnderline
autoFocus
fullWidth
sx={{
fontFamily: "monospace",
border: "1px solid lightgray",
borderRadius: "4px",
}}
/>
) : (
<span onClick={() => setEditMode("name")}>{node.name}</span>
<Box
sx={{ display: "flex", alignItems: "center", cursor: "pointer" }}
onClick={() => setEditMode("name")}
>
<Typography
variant="body2"
sx={{
fontFamily: "monospace",
padding: "2px 2px",
borderRadius: "4px",
"&:hover": { textDecoration: "underline" },
}}
>
{node.name}
{node.name && ":"}
</Typography>
</Box>
)}

{editMode === "type" ? (
Expand All @@ -66,7 +92,6 @@ const SchemaPropertyEditor = ({
setIsSelectOpen(false);
}}
onOpen={() => setIsSelectOpen(true)}
fullWidth
>
{propertyTypes.map((typeOption) => (
<MenuItem value={typeOption} key={typeOption}>
Expand All @@ -75,17 +100,27 @@ const SchemaPropertyEditor = ({
))}
</Select>
) : (
<span
onClick={() => {
setEditMode("type");
setIsSelectOpen(true);
}}
style={getTypeStyle(node.type)}
>
{node.type}
</span>
<Box sx={{ display: "flex", alignItems: "center", cursor: "pointer" }}>
<Typography
variant="body2"
onClick={() => {
setEditMode("type");
setIsSelectOpen(true);
}}
sx={{
cursor: "pointer",
fontFamily: "monospace",
borderRadius: "4px",
"&:hover": {
backgroundColor: (theme) => theme.palette.grey[600],
},
}}
>
{node.type}
</Typography>
</Box>
)}
</div>
</Box>
);
};

Expand Down
Loading
Loading