Skip to content
Merged
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
97 changes: 97 additions & 0 deletions src/__fixtures__/extensions/simple.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{
"title": "User",
"type": "object",
"x-stoplight": { "id": "root-id" },
"properties": {
"name": {
"type": "string",
"const": "Constant name",
"examples": ["Example name", "Different name"],
"x-stoplight": { "id": "name-id" }
},
"age": {
"type": "number",
"minimum": 10,
"maximum": 40,
"x-stoplight": { "id": "age-id" }
},
"completed_at": {
"type": "string",
"format": "date-time",
"x-stoplight": { "id": "completed_at-id" }
},
"list": {
"type": ["null", "array"],
"items": {
"type": ["string", "number"],
"x-stoplight": { "id": "list-items-id" }
},
"minItems": 1,
"maxItems": 4,
"x-stoplight": { "id": "list-id" }
},
"email": {
"type": "string",
"format": "email",
"examples": ["[email protected]", "[email protected]"],
"deprecated": true,
"default": "[email protected]",
"minLength": 2,
"x-stoplight": { "id": "email-id" }
},
"list-of-objects": {
"type": "array",
"items": {
"type": "object",
"x-stoplight": { "id": "list-of-objects-items-id" },
"properties": {
"id": {
"type": "string",
"x-stoplight": { "id": "list-of-objects-items-id-id" }
},
"friend": {
"type": "object",
"x-stoplight": { "id": "list-of-objects-items-friend-id" },
"properties": {
"id": {
"type": "string",
"x-stoplight": { "id": "list-of-objects-items-friend-id-id" }
},
"name": {
"type": "object",
"x-stoplight": { "id": "list-of-objects-items-friend-name-id" },
"properties": {
"first": {
"type": "string",
"x-stoplight": { "id": "list-of-objects-items-friend-name-first-id" }
},
"last": {
"type": "string",
"x-stoplight": { "id": "list-of-objects-items-friend-name-last-id" }
}
}
}
}
}
}
},
"minItems": 1,
"maxItems": 4,
"x-stoplight": { "id": "list-of-objects-id" }
},
"friend": {
"type": "object",
"x-stoplight": { "id": "friend-id" },
"properties": {
"id": {
"type": "string",
"x-stoplight": { "id": "friend-id-id" }
},
"name": {
"type": "string",
"x-stoplight": { "id": "friend-name-id" }
}
}
}
}
}
7 changes: 7 additions & 0 deletions src/__stories__/Default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ CustomRowAddon.args = {
),
};

export const Expansions = Template.bind({});
Expansions.args = {
schema: arrayOfComplexObjects as JSONSchema4,
renderRootTreeLines: true,
defaultExpandedDepth: 0,
};

export const ArrayOfObjects = Template.bind({});
ArrayOfObjects.args = { schema: arrayOfComplexObjects as JSONSchema4, renderRootTreeLines: true };

Expand Down
40 changes: 40 additions & 0 deletions src/__stories__/VendorExtensions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Flex } from '@stoplight/mosaic';
import { Story } from '@storybook/react';
import { JSONSchema4 } from 'json-schema';
import React from 'react';

import { JsonSchemaProps, JsonSchemaViewer } from '../components/JsonSchemaViewer';

const defaultSchema = require('../__fixtures__/default-schema.json');
const extensionsSchema = require('../__fixtures__/extensions/simple.json');

export default {
component: JsonSchemaViewer,
argTypes: {},
};

const Template: Story<JsonSchemaProps> = ({ schema = defaultSchema as JSONSchema4, ...args }) => (
<JsonSchemaViewer schema={schema} {...args} />
);

export const ExtensionRowSchema = Template.bind({});
ExtensionRowSchema.args = {
schema: extensionsSchema as JSONSchema4,
defaultExpandedDepth: Infinity,
renderRootTreeLines: true,
renderExtensionAddon: ({ nestingLevel, vendorExtensions }) => {
if (nestingLevel < 1) {
return null;
}

if (typeof vendorExtensions['x-stoplight'] === 'undefined') {
return null;
}

return (
<Flex h="full" alignItems="center">
<strong>{JSON.stringify(vendorExtensions['x-stoplight'], null, 2)}</strong>
</Flex>
);
},
};
3 changes: 3 additions & 0 deletions src/components/JsonSchemaViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const JsonSchemaViewerComponent = ({
defaultExpandedDepth = 1,
onGoToRef,
renderRowAddon,
renderExtensionAddon,
hideExamples,
renderRootTreeLines,
disableCrumbs,
Expand All @@ -49,6 +50,7 @@ const JsonSchemaViewerComponent = ({
viewMode,
onGoToRef,
renderRowAddon,
renderExtensionAddon,
hideExamples,
renderRootTreeLines,
disableCrumbs,
Expand All @@ -59,6 +61,7 @@ const JsonSchemaViewerComponent = ({
viewMode,
onGoToRef,
renderRowAddon,
renderExtensionAddon,
hideExamples,
renderRootTreeLines,
disableCrumbs,
Expand Down
20 changes: 11 additions & 9 deletions src/components/SchemaRow/SchemaRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { COMBINER_NAME_MAP } from '../../consts';
import { useJSVOptionsContext } from '../../contexts';
import { getNodeId, getOriginalNodeId } from '../../hash';
import { isPropertyRequired, visibleChildren } from '../../tree';
import { extractVendorExtensions } from '../../utils/extractVendorExtensions';
import { Caret, Description, getValidationsFromSchema, Types, Validations } from '../shared';
import { ChildStack } from '../shared/ChildStack';
import { Error } from '../shared/Error';
Expand All @@ -30,6 +31,7 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = React.memo(
const {
defaultExpandedDepth,
renderRowAddon,
renderExtensionAddon,
onGoToRef,
hideExamples,
renderRootTreeLines,
Expand Down Expand Up @@ -65,6 +67,12 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = React.memo(
const validations = isRegularNode(schemaNode) ? schemaNode.validations : {};
const hasProperties = useHasProperties({ required, deprecated, validations });

const [totalVendorExtensions, vendorExtensions] = React.useMemo(
() => extractVendorExtensions(schemaNode.fragment),
[schemaNode.fragment],
);
const hasVendorProperties = totalVendorExtensions > 0;

const annotationRootOffset = renderRootTreeLines ? 0 : 8;
let annotationLeftOffset = -20 - annotationRootOffset;
if (nestingLevel > 1) {
Expand Down Expand Up @@ -100,11 +108,9 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = React.memo(
}}
>
{!isRootLevel && <Box borderT w={isCollapsible ? 1 : 3} ml={-3} mr={3} mt={2} />}

{parentChangeType !== 'added' && parentChangeType !== 'removed' ? (
<NodeAnnotation change={hasChanged} style={{ left: annotationLeftOffset }} />
) : null}

<VStack spacing={1} maxW="full" flex={1} ml={isCollapsible && !isRootLevel ? 2 : undefined}>
<Flex
alignItems="center"
Expand All @@ -113,7 +119,6 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = React.memo(
cursor={isCollapsible ? 'pointer' : undefined}
>
{isCollapsible ? <Caret isExpanded={isExpanded} /> : null}

<Flex alignItems="baseline" fontSize="base">
{schemaNode.subpath.length > 0 && shouldShowPropertyName(schemaNode) && (
<Box
Expand Down Expand Up @@ -167,22 +172,19 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = React.memo(
/>
)}
</Flex>

{hasProperties && <Divider atom={isNodeHoveredAtom(schemaNode)} />}

<Properties required={required} deprecated={deprecated} validations={validations} />
</Flex>

{typeof description === 'string' && description.length > 0 && <Description value={description} />}

<Validations
validations={isRegularNode(schemaNode) ? getValidationsFromSchema(schemaNode) : {}}
hideExamples={hideExamples}
/>
{hasVendorProperties && renderExtensionAddon ? (
<Box>{renderExtensionAddon({ schemaNode, nestingLevel, vendorExtensions })}</Box>
) : null}
</VStack>

<Error schemaNode={schemaNode} />

{renderRowAddon ? <Box>{renderRowAddon({ schemaNode, nestingLevel })}</Box> : null}
</Flex>
{isCollapsible && isExpanded ? (
Expand Down
14 changes: 12 additions & 2 deletions src/components/SchemaRow/TopLevelSchemaRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { isEmpty } from 'lodash';
import * as React from 'react';

import { COMBINER_NAME_MAP } from '../../consts';
import { useJSVOptionsContext } from '../../contexts';
import { useIsOnScreen } from '../../hooks/useIsOnScreen';
import { isComplexArray, isDictionaryNode, visibleChildren } from '../../tree';
import { extractVendorExtensions } from '../../utils/extractVendorExtensions';
import { showPathCrumbsAtom } from '../PathCrumbs/state';
import { Description, getValidationsFromSchema, Validations } from '../shared';
import { ChildStack } from '../shared/ChildStack';
Expand All @@ -18,18 +20,28 @@ export const TopLevelSchemaRow = ({
schemaNode,
skipDescription,
}: Pick<SchemaRowProps, 'schemaNode'> & { skipDescription?: boolean }) => {
const { renderExtensionAddon } = useJSVOptionsContext();

const { selectedChoice, setSelectedChoice, choices } = useChoices(schemaNode);
const childNodes = React.useMemo(() => visibleChildren(selectedChoice.type), [selectedChoice.type]);
const nestingLevel = 0;

const nodeId = schemaNode.fragment?.['x-stoplight']?.id;
const [totalVendorExtensions, vendorExtensions] = React.useMemo(
() => extractVendorExtensions(schemaNode.fragment),
[schemaNode.fragment],
);
const hasVendorProperties = totalVendorExtensions > 0;

// regular objects are flattened at the top level
if (isRegularNode(schemaNode) && isPureObjectNode(schemaNode)) {
return (
<>
<ScrollCheck />
{!skipDescription ? <Description value={schemaNode.annotations.description} /> : null}
{hasVendorProperties && renderExtensionAddon
? renderExtensionAddon({ schemaNode, nestingLevel, vendorExtensions })
: null}
<ChildStack
schemaNode={schemaNode}
childNodes={childNodes}
Expand All @@ -48,7 +60,6 @@ export const TopLevelSchemaRow = ({
<>
<ScrollCheck />
<Description value={schemaNode.annotations.description} />

<HStack spacing={3} pb={4}>
<Menu
aria-label="Pick a type"
Expand Down Expand Up @@ -77,7 +88,6 @@ export const TopLevelSchemaRow = ({
</Flex>
) : null}
</HStack>

{childNodes.length > 0 ? (
<ChildStack
schemaNode={schemaNode}
Expand Down
3 changes: 2 additions & 1 deletion src/contexts/jsvOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { NodeHasChangedFn } from '@stoplight/types';
import * as React from 'react';

import { GoToRefHandler, RowAddonRenderer, ViewMode } from '../types';
import { ExtensionAddonRenderer, GoToRefHandler, RowAddonRenderer, ViewMode } from '../types';

export type JSVOptions = {
defaultExpandedDepth: number;
viewMode: ViewMode;
onGoToRef?: GoToRefHandler;
renderRowAddon?: RowAddonRenderer;
renderExtensionAddon?: ExtensionAddonRenderer;
hideExamples?: boolean;
renderRootTreeLines?: boolean;
disableCrumbs?: boolean;
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export interface SchemaRowProps {

export type RowAddonRenderer = (props: SchemaRowProps) => React.ReactNode;

export interface ExtensionRowProps {
schemaNode: SchemaNode;
nestingLevel: number;
vendorExtensions: Record<string, unknown>;
}

export type ExtensionAddonRenderer = (props: ExtensionRowProps) => React.ReactNode;

export type ViewMode = 'read' | 'write' | 'standalone';

export type JSONSchema = JSONSchema4 | JSONSchema6 | JSONSchema7;
26 changes: 26 additions & 0 deletions src/utils/extractVendorExtensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SchemaFragment } from '@stoplight/json-schema-tree';

export type VendorExtensionsList = {
[keyof: string]: unknown;
};

export type VendorExtensionsResult = [number, VendorExtensionsList];

/**
* Extract all vendor extensions or properties prefix with 'x-' from the schema definition
* @param fragment The fragment to extract the vendor extensions from
* @returns VendorExtensionsResult
*/
export function extractVendorExtensions(fragment: SchemaFragment | boolean): VendorExtensionsResult {
if (typeof fragment === 'boolean') {
return [0, {}];
}

const extensionKeys = Object.keys(fragment).filter(key => key.startsWith('x-'));
let vendorExtensions = {};
extensionKeys.forEach(key => {
vendorExtensions[key] = fragment[key];
});

return [extensionKeys.length, vendorExtensions];
}
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './extractVendorExtensions';
export * from './printName';