From b1e88df64b45f89a42e034989f3b6d170bf54644 Mon Sep 17 00:00:00 2001 From: Daniel Lehr Date: Tue, 20 Aug 2024 20:59:19 +0200 Subject: [PATCH] Refactor schema view --- .../lib/plugins/openapi/OperationListItem.tsx | 6 +- .../src/lib/plugins/openapi/ParameterList.tsx | 18 +-- .../lib/plugins/openapi/SchemaListView.tsx | 75 ----------- .../plugins/openapi/SchemaListViewItem.tsx | 125 ----------------- .../openapi/SchemaListViewItemGroup.tsx | 63 --------- .../src/lib/plugins/openapi/Sidecar.tsx | 7 +- .../openapi/schema/SchemaComponents.tsx | 126 ++++++++++++++++++ .../lib/plugins/openapi/schema/SchemaView.tsx | 110 +++++++++++++++ .../src/lib/plugins/openapi/schema/utils.ts | 10 ++ 9 files changed, 263 insertions(+), 277 deletions(-) delete mode 100644 packages/zudoku/src/lib/plugins/openapi/SchemaListView.tsx delete mode 100644 packages/zudoku/src/lib/plugins/openapi/SchemaListViewItem.tsx delete mode 100644 packages/zudoku/src/lib/plugins/openapi/SchemaListViewItemGroup.tsx create mode 100644 packages/zudoku/src/lib/plugins/openapi/schema/SchemaComponents.tsx create mode 100644 packages/zudoku/src/lib/plugins/openapi/schema/SchemaView.tsx create mode 100644 packages/zudoku/src/lib/plugins/openapi/schema/utils.ts diff --git a/packages/zudoku/src/lib/plugins/openapi/OperationListItem.tsx b/packages/zudoku/src/lib/plugins/openapi/OperationListItem.tsx index 0f1a5b1c..a5b342a8 100644 --- a/packages/zudoku/src/lib/plugins/openapi/OperationListItem.tsx +++ b/packages/zudoku/src/lib/plugins/openapi/OperationListItem.tsx @@ -6,9 +6,9 @@ import { groupBy } from "../../util/groupBy.js"; import { renderIf } from "../../util/renderIf.js"; import { OperationsFragment } from "./OperationList.js"; import { ParameterList } from "./ParameterList.js"; -import { SchemaListView } from "./SchemaListView.js"; import { Sidecar } from "./Sidecar.js"; import { FragmentType, useFragment } from "./graphql/index.js"; +import { SchemaView } from "./schema/SchemaView.js"; import { SchemaProseClasses } from "./util/prose.js"; export const PARAM_GROUPS = ["path", "query", "header", "cookie"] as const; @@ -62,7 +62,7 @@ export const OperationListItem = ({ Request Body - + ))} {operation.responses.length > 0 && ( @@ -93,7 +93,7 @@ export const OperationListItem = ({ {renderIf( response.content?.find((content) => content.schema), (content) => { - return ; + return ; }, ) ?? ( diff --git a/packages/zudoku/src/lib/plugins/openapi/ParameterList.tsx b/packages/zudoku/src/lib/plugins/openapi/ParameterList.tsx index a97c4ded..c1bd1f0b 100644 --- a/packages/zudoku/src/lib/plugins/openapi/ParameterList.tsx +++ b/packages/zudoku/src/lib/plugins/openapi/ParameterList.tsx @@ -21,14 +21,16 @@ export const ParameterList = ({
    - {parameters.map((parameter) => ( - - ))} + {parameters + .sort((a, b) => (a.required === b.required ? 0 : a.required ? -1 : 1)) + .map((parameter) => ( + + ))}
diff --git a/packages/zudoku/src/lib/plugins/openapi/SchemaListView.tsx b/packages/zudoku/src/lib/plugins/openapi/SchemaListView.tsx deleted file mode 100644 index 62835cd6..00000000 --- a/packages/zudoku/src/lib/plugins/openapi/SchemaListView.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Markdown } from "../../components/Markdown.js"; -import { SchemaObject } from "../../oas/parser/index.js"; -import { Card } from "../../ui/Card.js"; -import { groupBy } from "../../util/groupBy.js"; -import { objectEntries } from "../../util/objectEntries.js"; -import { SchemaListViewItem } from "./SchemaListViewItem.js"; -import { SchemaListViewItemGroup } from "./SchemaListViewItemGroup.js"; -import { SchemaProseClasses } from "./util/prose.js"; - -export const SchemaListView = ({ - name, - schema, - level = 0, - defaultOpen = false, -}: { - level?: number; - defaultOpen?: boolean; - name?: string; - schema: SchemaObject; -}) => { - const properties = Object.entries(schema.properties ?? {}); - const additionalProperties = - typeof schema.additionalProperties === "object" - ? Object.entries(schema.additionalProperties) - : []; - - const combinedProperties = properties.concat( - Array.isArray(additionalProperties) ? additionalProperties : [], - ); - - const groups = groupBy(combinedProperties, ([propertyName, property]) => { - return property.deprecated - ? "deprecated" - : schema.required?.includes(propertyName) - ? "required" - : "optional"; - }); - - if (schema.type === "array") { - return ( - - - - ); - } - - return ( -
- {(schema.title ?? name) && ( -
{schema.title ?? name}
- )} - {level === 0 && schema.description && ( - - )} - - {objectEntries(groups).map(([group, properties]) => ( - - ))} - -
- ); -}; diff --git a/packages/zudoku/src/lib/plugins/openapi/SchemaListViewItem.tsx b/packages/zudoku/src/lib/plugins/openapi/SchemaListViewItem.tsx deleted file mode 100644 index b0cb8def..00000000 --- a/packages/zudoku/src/lib/plugins/openapi/SchemaListViewItem.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import * as Collapsible from "@radix-ui/react-collapsible"; -import { ListPlusIcon } from "lucide-react"; -import { Markdown } from "../../components/Markdown.js"; -import { SchemaObject } from "../../oas/parser/index.js"; -import { Button } from "../../ui/Button.js"; -import { cn } from "../../util/cn.js"; -import { SchemaListView } from "./SchemaListView.js"; - -export const SchemaListViewItem = ({ - propertyName, - property, - nestingLevel, - isRequired, - defaultOpen = false, -}: { - propertyName?: string; - isRequired: boolean; - property: SchemaObject; - nestingLevel: number; - defaultOpen?: boolean; -}) => { - if (!property) { - return
no property
; - } - - const title = - propertyName || property.title - ? [propertyName, property.title].filter(Boolean).join(" ") - : null; - - return ( -
-
- {title && {title}} - - {property.type && ( - {property.type} - )} - {property.deprecated && ( - Deprecated - )} - {!isRequired && ( - - optional {property.required} - - )} -
- {property.description && ( - - )} - - {property.enum && ( - - Possible values - {/* Make values unique, some schemas have duplicates */} - {[...new Set(property.enum.filter((value) => value))] - .map((value) => ( - - {value} - - )) - .slice(0, 4)} - {property.enum.length > 4 && ( - - ... - - )} - - )} - - {(property.type === "object" && - (property.properties?.length ?? - Object.entries(property.additionalProperties ?? {}).length > 0)) || - (property.type === "array" && - // this check is needed because the `items` can be undefined despite the type being defined - typeof property.items !== "undefined" && - property.items.type === "object") ? ( - - - - - - - {property.type === "object" && ( -
- -
- )} - {property.type === "array" && property.items.type === "object" && ( -
- -
- )} -
-
- ) : null} -
- ); -}; diff --git a/packages/zudoku/src/lib/plugins/openapi/SchemaListViewItemGroup.tsx b/packages/zudoku/src/lib/plugins/openapi/SchemaListViewItemGroup.tsx deleted file mode 100644 index 6cf3be69..00000000 --- a/packages/zudoku/src/lib/plugins/openapi/SchemaListViewItemGroup.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import * as Collapsible from "@radix-ui/react-collapsible"; -import { useState } from "react"; -import { SchemaObject } from "../../oas/parser/index.js"; -import { cn } from "../../util/cn.js"; -import { SchemaListViewItem } from "./SchemaListViewItem.js"; - -export const SchemaListViewItemGroup = ({ - group, - properties, - nestingLevel, - required, - defaultOpen = false, -}: { - group: "optional" | "required" | "deprecated"; - defaultOpen?: boolean; - properties: [string, SchemaObject][]; - nestingLevel: number; - required: string[]; -}) => { - const notCollapsible = - defaultOpen || - group === "required" || - properties.length === 1 || - nestingLevel === 0; - - const [open, setOpen] = useState(notCollapsible); - - if (properties.length === 0) { - return; - } - - return ( - - {!open && ( - - {properties.length} {group} fields - - )} - - - {properties.map(([propertyName, property]) => ( - - ))} - - - ); -}; diff --git a/packages/zudoku/src/lib/plugins/openapi/Sidecar.tsx b/packages/zudoku/src/lib/plugins/openapi/Sidecar.tsx index 059172a4..b7b8b31c 100644 --- a/packages/zudoku/src/lib/plugins/openapi/Sidecar.tsx +++ b/packages/zudoku/src/lib/plugins/openapi/Sidecar.tsx @@ -105,8 +105,9 @@ export const Sidecar = ({ const requestBodyContent = operation.requestBody?.content; - const path = operation.path.split("/").map((part) => ( - + const path = operation.path.split("/").map((part, i, arr) => ( + // eslint-disable-next-line react/no-array-index-key + {part.startsWith("{") && part.endsWith("}") ? ( )); diff --git a/packages/zudoku/src/lib/plugins/openapi/schema/SchemaComponents.tsx b/packages/zudoku/src/lib/plugins/openapi/schema/SchemaComponents.tsx new file mode 100644 index 00000000..1b6d6e43 --- /dev/null +++ b/packages/zudoku/src/lib/plugins/openapi/schema/SchemaComponents.tsx @@ -0,0 +1,126 @@ +import * as Collapsible from "@radix-ui/react-collapsible"; +import { ListPlusIcon } from "lucide-react"; +import { useState } from "react"; +import { Markdown, ProseClasses } from "../../../components/Markdown.js"; +import type { SchemaObject } from "../../../oas/parser/index.js"; +import { Button } from "../../../ui/Button.js"; +import { cn } from "../../../util/cn.js"; +import { SchemaView } from "./SchemaView.js"; +import { hasLogicalGroupings, isComplexType } from "./utils.js"; + +export const SchemaLogicalGroup = ({ + schema, + level, +}: { + schema: SchemaObject; + level: number; +}) => { + const renderLogicalGroup = ( + group: SchemaObject[], + groupName: string, + separator: string, + ) => { + return group.map((subSchema, index) => ( +
+ {groupName} +
+ + {index < group.length - 1 && ( +
{separator}
+ )} +
+
+ )); + }; + + if (schema.oneOf) return renderLogicalGroup(schema.oneOf, "One of", "OR"); + if (schema.allOf) return renderLogicalGroup(schema.allOf, "All of", "AND"); + if (schema.anyOf) return renderLogicalGroup(schema.anyOf, "Any of", "OR"); + + return null; +}; + +export const SchemaPropertyItem = ({ + name, + value, + group, + level, + defaultOpen = false, + showCollapseButton = true, +}: { + name: string; + value: SchemaObject; + group: "required" | "optional" | "deprecated"; + level: number; + defaultOpen?: boolean; + showCollapseButton?: boolean; +}) => { + const [isOpen, setIsOpen] = useState(defaultOpen); + + return ( +
  • +
    +
    + {name} + + {value.type === "array" && value.items?.type ? ( + {value.items.type}[] + ) : Array.isArray(value.type) ? ( + {value.type.join(" | ")} + ) : ( + {value.type} + )} + + {group === "optional" && ( + + optional + + )} +
    + + {value.description && ( + + )} + + {(hasLogicalGroupings(value) ?? isComplexType(value)) && ( + setIsOpen(!isOpen)} + > + {showCollapseButton && ( + + + + )} + +
    + {hasLogicalGroupings(value) && ( + + )} + {value.type === "object" && ( + + )} + {value.type === "array" && typeof value.items === "object" && ( + + )} +
    +
    +
    + )} +
    +
  • + ); +}; diff --git a/packages/zudoku/src/lib/plugins/openapi/schema/SchemaView.tsx b/packages/zudoku/src/lib/plugins/openapi/schema/SchemaView.tsx new file mode 100644 index 00000000..d5d07a8f --- /dev/null +++ b/packages/zudoku/src/lib/plugins/openapi/schema/SchemaView.tsx @@ -0,0 +1,110 @@ +import type { SchemaObject } from "../../../oas/parser/index.js"; +import { Card, CardContent, CardHeader, CardTitle } from "../../../ui/Card.js"; +import { groupBy } from "../../../util/groupBy.js"; +import { SchemaLogicalGroup, SchemaPropertyItem } from "./SchemaComponents.js"; +import { hasLogicalGroupings } from "./utils.js"; + +export const SchemaView = ({ + schema, + level = 0, + defaultOpen = false, +}: { + schema: SchemaObject; + level?: number; + defaultOpen?: boolean; +}) => { + const renderSchema = (schema: SchemaObject, level: number) => { + if (schema.oneOf || schema.allOf || schema.anyOf) { + return ; + } + + if (schema.type === "array" && schema.items) { + const itemsSchema = schema.items as SchemaObject; + + if ( + typeof itemsSchema.type === "string" && + ["string", "number", "boolean", "integer"].includes(itemsSchema.type) + ) { + return ( + + + {`array<${itemsSchema.type}>`} + {schema.description &&

    {schema.description}

    } +
    +
    + ); + } else if ( + itemsSchema.type === "object" || + hasLogicalGroupings(itemsSchema) + ) { + return ( + + object[] + {renderSchema(itemsSchema, level + 1)} + + ); + } else { + return renderSchema(itemsSchema, level + 1); + } + } + + if (schema.properties) { + const groupedProperties = groupBy( + Object.entries(schema.properties), + ([propertyName, property]) => { + return property.deprecated + ? "deprecated" + : schema.required?.includes(propertyName) + ? "required" + : "optional"; + }, + ); + + const isTopLevelSingleItem = + level === 0 && Object.keys(groupedProperties).length === 1; + + return ( + + {(["required", "optional", "deprecated"] as const).map( + (group) => + groupedProperties[group] && ( +
      + {groupedProperties[group].map(([key, value]) => ( + + ))} +
    + ), + )} +
    + ); + } + + if (schema.additionalProperties) { + return ( + + + Additional Properties: + + + {renderSchema( + schema.additionalProperties as SchemaObject, + level + 1, + )} + + + ); + } + + return null; + }; + + return renderSchema(schema, level); +}; diff --git a/packages/zudoku/src/lib/plugins/openapi/schema/utils.ts b/packages/zudoku/src/lib/plugins/openapi/schema/utils.ts new file mode 100644 index 00000000..972a704e --- /dev/null +++ b/packages/zudoku/src/lib/plugins/openapi/schema/utils.ts @@ -0,0 +1,10 @@ +import type { SchemaObject } from "../../../oas/parser/index.js"; + +export const isComplexType = (value: SchemaObject) => + value.type === "object" || + (value.type === "array" && + typeof value.items === "object" && + (!value.items?.type || value.items?.type === "object")); + +export const hasLogicalGroupings = (value: SchemaObject) => + value.oneOf || value.allOf || value.anyOf;