diff --git a/src/__stories__/JsonSchemaViewer.tsx b/src/__stories__/JsonSchemaViewer.tsx
index cde18580..205aa808 100644
--- a/src/__stories__/JsonSchemaViewer.tsx
+++ b/src/__stories__/JsonSchemaViewer.tsx
@@ -1,19 +1,20 @@
import * as React from 'react';
import { State, Store } from '@sambego/storybook-state';
+import { Button, Checkbox, Icon } from '@stoplight/ui-kit';
import { action } from '@storybook/addon-actions';
import { boolean, number, object, select, text, withKnobs } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
-import { JsonSchemaViewer } from '../components';
-
-import { Checkbox } from '@stoplight/ui-kit';
import { JSONSchema4 } from 'json-schema';
+import { JsonSchemaViewer, SchemaRow } from '../components';
+
import * as allOfSchemaResolved from '../__fixtures__/allOf/allOf-resolved.json';
import * as allOfSchema from '../__fixtures__/allOf/allOf-schema.json';
import * as schema from '../__fixtures__/default-schema.json';
import * as schemaWithRefs from '../__fixtures__/ref/original.json';
import * as dereferencedSchema from '../__fixtures__/ref/resolved.json';
import * as stressSchema from '../__fixtures__/stress-schema.json';
+import { RowRenderer } from '../types';
import { Wrapper } from './utils/Wrapper';
storiesOf('JsonSchemaViewer', module)
@@ -44,6 +45,7 @@ storiesOf('JsonSchemaViewer', module)
expanded={boolean('expanded', true)}
hideTopBar={boolean('hideTopBar', false)}
onGoToRef={action('onGoToRef')}
+ mergeAllOf={boolean('mergeAllOf', true)}
/>
);
@@ -56,8 +58,35 @@ storiesOf('JsonSchemaViewer', module)
hideTopBar={boolean('hideTopBar', false)}
onGoToRef={action('onGoToRef')}
maxRows={number('maxRows', 5)}
+ mergeAllOf={boolean('mergeAllOf', true)}
/>
))
+ .add('custom row renderer', () => {
+ const customRowRenderer: RowRenderer = (node, rowOptions) => {
+ return (
+ <>
+
+
+ } />
+
+
+ >
+ );
+ };
+
+ return (
+
+ );
+ })
.add('stress-test schema', () => (
))
.add('allOf-schema', () => (
@@ -76,6 +106,7 @@ storiesOf('JsonSchemaViewer', module)
defaultExpandedDepth={number('defaultExpandedDepth', 2)}
expanded={boolean('expanded', false)}
hideTopBar={boolean('hideTopBar', false)}
+ mergeAllOf={boolean('mergeAllOf', true)}
onGoToRef={action('onGoToRef')}
/>
))
@@ -96,6 +127,7 @@ storiesOf('JsonSchemaViewer', module)
defaultExpandedDepth={number('defaultExpandedDepth', 2)}
hideTopBar={boolean('hideTopBar', false)}
onGoToRef={action('onGoToRef')}
+ mergeAllOf={boolean('mergeAllOf', true)}
/>
))
.add('dark', () => (
@@ -107,21 +139,7 @@ storiesOf('JsonSchemaViewer', module)
expanded={boolean('expanded', false)}
hideTopBar={boolean('hideTopBar', false)}
onGoToRef={action('onGoToRef')}
+ mergeAllOf={boolean('mergeAllOf', true)}
/>
- ))
- .add('with rowRendererRight', () => (
- (
-
-
-
- )}
- name={text('name', 'my schema')}
- schema={schema as JSONSchema4}
- defaultExpandedDepth={number('defaultExpandedDepth', 2)}
- expanded={boolean('expanded', false)}
- hideTopBar={boolean('hideTopBar', false)}
- onGoToRef={action('onGoToRef')}
- />
));
diff --git a/src/components/JsonSchemaViewer.tsx b/src/components/JsonSchemaViewer.tsx
index 562ac176..cbbd968a 100644
--- a/src/components/JsonSchemaViewer.tsx
+++ b/src/components/JsonSchemaViewer.tsx
@@ -4,13 +4,13 @@ import { runInAction } from 'mobx';
import * as React from 'react';
import { JSONSchema4 } from 'json-schema';
-import { GoToRefHandler, IExtendableRenderers } from '../types';
+import { GoToRefHandler, RowRenderer } from '../types';
import { isSchemaViewerEmpty, renderSchema } from '../utils';
import { SchemaTree } from './SchemaTree';
export type FallbackComponent = React.ComponentType<{ error: Error | null }>;
-export interface IJsonSchemaViewer extends IExtendableRenderers {
+export interface IJsonSchemaViewer {
schema: JSONSchema4;
dereferencedSchema?: JSONSchema4;
style?: object;
@@ -24,6 +24,7 @@ export interface IJsonSchemaViewer extends IExtendableRenderers {
onGoToRef?: GoToRefHandler;
mergeAllOf?: boolean;
FallbackComponent?: FallbackComponent;
+ rowRenderer?: RowRenderer;
}
export class JsonSchemaViewerComponent extends React.PureComponent {
@@ -39,9 +40,7 @@ export class JsonSchemaViewerComponent extends React.PureComponent {
this.treeStore.nodes = Array.from(
- renderSchema(this.props.dereferencedSchema || this.props.schema, 0, { path: [] }, { mergeAllOf: true }),
+ renderSchema(
+ this.props.dereferencedSchema || this.props.schema,
+ 0,
+ { path: [] },
+ { mergeAllOf: this.props.mergeAllOf !== false },
+ ),
);
});
}
diff --git a/src/components/SchemaRow.tsx b/src/components/SchemaRow.tsx
index 7930bf72..4bef0040 100644
--- a/src/components/SchemaRow.tsx
+++ b/src/components/SchemaRow.tsx
@@ -1,192 +1,67 @@
-import { MarkdownViewer } from '@stoplight/markdown-viewer';
import { IRowRendererOptions } from '@stoplight/tree-list';
-import { Icon, Popover } from '@stoplight/ui-kit';
-import * as cn from 'classnames';
+import cn from 'classnames';
import * as React from 'react';
+import { Divider } from './shared/Divider';
import get = require('lodash/get');
-import map = require('lodash/map');
-import size = require('lodash/size');
-import { GoToRefHandler, IExtendableRenderers, SchemaNodeWithMeta, SchemaTreeListNode } from '../types';
-import { isCombiner, isRef } from '../utils';
-import { Types } from './';
+import { GoToRefHandler, SchemaNodeWithMeta, SchemaTreeListNode } from '../types';
+import { Caret } from './shared/Caret';
+import { Description } from './shared/Description';
+import { Property } from './shared/Property';
+import { Validations } from './shared/Validations';
-export interface ISchemaRow extends IExtendableRenderers {
+export interface ISchemaRow {
+ className?: string;
node: SchemaTreeListNode;
rowOptions: IRowRendererOptions;
onGoToRef?: GoToRefHandler;
- toggleExpand: () => void;
}
const ICON_SIZE = 12;
const ICON_DIMENSION = 20;
+const ROW_OFFSET = 7;
-export const SchemaRow: React.FunctionComponent = ({
- node,
- rowOptions,
- onGoToRef,
- rowRendererRight,
- toggleExpand,
-}) => {
+export const SchemaRow: React.FunctionComponent = ({ className, node, rowOptions, onGoToRef }) => {
const schemaNode = node.metadata as SchemaNodeWithMeta;
- const { name, $ref, subtype, required } = schemaNode;
-
- const type = isRef(schemaNode) ? '$ref' : isCombiner(schemaNode) ? schemaNode.combiner : schemaNode.type;
const description = get(schemaNode, 'annotations.description');
- const childrenCount =
- type === 'object'
- ? size(get(schemaNode, 'properties'))
- : subtype === 'object'
- ? size(get(schemaNode, 'items.properties'))
- : size(get(schemaNode, 'items'));
-
- const nodeValidations = {
- ...('annotations' in schemaNode && schemaNode.annotations.default
- ? { default: schemaNode.annotations.default }
- : {}),
- ...get(schemaNode, 'validations', {}),
- };
- const validationCount = Object.keys(nodeValidations).length;
- const handleGoToRef = React.useCallback(
- () => {
- if (onGoToRef) {
- onGoToRef($ref!, node);
- }
- },
- [onGoToRef, node, $ref],
- );
-
- const requiredElem = (
-
- {required ? 'required' : 'optional'}
- {validationCount ? `+${validationCount}` : ''}
-
- );
- const combinerOffset = ICON_DIMENSION * node.level;
return (
-
- {/* Do not set position: relative. Divider must be relative to the parent container in order to avoid bugs related to this container calculated height changes. */}
+
{node.canHaveChildren &&
node.level > 0 && (
-
-
-
+ size={ICON_SIZE}
+ />
)}
- {schemaNode.divider && (
-
-
{schemaNode.divider}
-
-
- )}
+ {schemaNode.divider &&
{schemaNode.divider} }
- {name &&
{name}
}
-
-
- {type === '$ref' ? `[${$ref}]` : null}
-
-
- {type === '$ref' && onGoToRef ? (
-
- (go to ref)
-
- ) : null}
-
- {node.canHaveChildren &&
{`{${childrenCount}}`}
}
-
- {'pattern' in schemaNode && schemaNode.pattern ? (
-
(pattern property)
- ) : null}
-
- {description && (
-
{description} }
- targetClassName="text-darken-7 dark:text-lighten-6 w-full truncate"
- content={
-
-
-
- }
- />
- )}
+
+ {description &&
}
- {validationCount ? (
-
- {map(Object.keys(nodeValidations), (key, index) => {
- const validation = nodeValidations[key];
-
- let elem = null;
- if (Array.isArray(validation)) {
- elem = validation.map((v, i) => (
-
-
{String(v)}
- {i < validation.length - 1 ?
,
: null}
-
- ));
- } else if (typeof validation === 'object') {
- elem = (
-
- {'{...}'}
-
- );
- } else {
- elem = (
-
- {JSON.stringify(validation)}
-
- );
- }
-
- return (
-
- );
- })}
-
- }
- target={requiredElem}
- />
- ) : (
- requiredElem
- )}
- {rowRendererRight &&
{rowRendererRight(node)}
}
+
);
diff --git a/src/components/SchemaTree.tsx b/src/components/SchemaTree.tsx
index b62a1486..f38ed05c 100644
--- a/src/components/SchemaTree.tsx
+++ b/src/components/SchemaTree.tsx
@@ -1,12 +1,13 @@
-import { TreeList, TreeStore } from '@stoplight/tree-list';
+import { TreeList, TreeListEvents, TreeStore } from '@stoplight/tree-list';
import * as cn from 'classnames';
import { JSONSchema4 } from 'json-schema';
import { observer } from 'mobx-react-lite';
import * as React from 'react';
-import { GoToRefHandler, IExtendableRenderers, SchemaTreeListNode } from '../types';
+
+import { GoToRefHandler, RowRenderer } from '../types';
import { SchemaRow } from './';
-export interface ISchemaTree extends IExtendableRenderers {
+export interface ISchemaTree {
treeStore: TreeStore;
schema: JSONSchema4;
className?: string;
@@ -16,18 +17,37 @@ export interface ISchemaTree extends IExtendableRenderers {
expanded?: boolean;
maxRows?: number;
onGoToRef?: GoToRefHandler;
+ rowRenderer?: RowRenderer;
}
const canDrag = () => false;
export const SchemaTree = observer(props => {
- const { hideTopBar, name, treeStore, maxRows, className, onGoToRef } = props;
+ const { hideTopBar, name, treeStore, maxRows, className, onGoToRef, rowRenderer: customRowRenderer } = props;
+
+ React.useEffect(
+ () => {
+ treeStore.on(TreeListEvents.NodeClick, (e, node) => {
+ treeStore.toggleExpand(node);
+ });
+
+ return () => {
+ treeStore.dispose();
+ };
+ },
+ [treeStore],
+ );
- const itemData = {
- treeStore,
- count: treeStore.nodes.length,
- onGoToRef,
- };
+ const rowRenderer = React.useCallback(
+ (node, rowOptions) => {
+ if (customRowRenderer !== undefined) {
+ return customRowRenderer(node, rowOptions, treeStore);
+ }
+
+ return ;
+ },
+ [onGoToRef, customRowRenderer, treeStore],
+ );
return (
@@ -42,24 +62,9 @@ export const SchemaTree = observer(props => {
striped
maxRows={maxRows !== undefined ? maxRows + 0.5 : maxRows}
store={treeStore}
- rowRenderer={(node, rowOptions) => {
- // TODO: add a React.useCallback to rerender only when either itemData.count or maskProps (to be found in studio) change
-
- return (
- {
- treeStore.toggleExpand(node);
- }}
- rowRendererRight={props.rowRendererRight}
- node={node as SchemaTreeListNode}
- rowOptions={rowOptions}
- {...itemData}
- />
- );
- }}
+ rowRenderer={rowRenderer}
canDrag={canDrag}
/>
- {props.schemaControlsRenderer && props.schemaControlsRenderer()}
);
});
diff --git a/src/components/__tests__/SchemaRow.spec.tsx b/src/components/__tests__/SchemaRow.spec.tsx
index 14aa4d76..cf42f3de 100644
--- a/src/components/__tests__/SchemaRow.spec.tsx
+++ b/src/components/__tests__/SchemaRow.spec.tsx
@@ -1,9 +1,9 @@
-import { Popover } from '@stoplight/ui-kit';
import { shallow } from 'enzyme';
import 'jest-enzyme';
import * as React from 'react';
import { SchemaTreeListNode } from '../../types';
import { SchemaRow } from '../SchemaRow';
+import { Validations } from '../shared/Validations';
describe('SchemaRow component', () => {
test('should render falsy validations', () => {
@@ -27,10 +27,9 @@ describe('SchemaRow component', () => {
isExpanded: true,
};
- const wrapper = shallow(shallow(
- null} node={node as SchemaTreeListNode} rowOptions={rowOptions} />,
- )
- .find(Popover)
+ const wrapper = shallow(shallow( )
+ .find(Validations)
+ .shallow()
.prop('content') as React.ReactElement);
expect(wrapper).toHaveText('enum:null,0,false');
diff --git a/src/components/__tests__/Type.spec.tsx b/src/components/__tests__/Type.spec.tsx
index b0ac77b2..b1cce12a 100644
--- a/src/components/__tests__/Type.spec.tsx
+++ b/src/components/__tests__/Type.spec.tsx
@@ -1,7 +1,7 @@
import { shallow } from 'enzyme';
import 'jest-enzyme';
import * as React from 'react';
-import { IType, PropertyTypeColors, Type } from '../Types';
+import { IType, PropertyTypeColors, Type } from '../shared/Types';
describe('Type component', () => {
it.each(Object.keys(PropertyTypeColors))('should handle $s type', type => {
diff --git a/src/components/index.tsx b/src/components/index.tsx
index 01cc319c..f77b5926 100644
--- a/src/components/index.tsx
+++ b/src/components/index.tsx
@@ -1,4 +1,4 @@
export * from './JsonSchemaViewer';
export * from './SchemaRow';
export * from './SchemaTree';
-export * from './Types';
+export * from './shared';
diff --git a/src/components/shared/Caret.tsx b/src/components/shared/Caret.tsx
new file mode 100644
index 00000000..704c3cc7
--- /dev/null
+++ b/src/components/shared/Caret.tsx
@@ -0,0 +1,22 @@
+import { Icon, IIconProps } from '@stoplight/ui-kit';
+import * as React from 'react';
+
+export interface ICaret {
+ isExpanded: boolean;
+ style?: React.CSSProperties;
+ size?: IIconProps['iconSize'];
+}
+
+export const Caret: React.FunctionComponent = ({ style, size, isExpanded }) => (
+
+
+
+);
diff --git a/src/components/shared/Description.tsx b/src/components/shared/Description.tsx
new file mode 100644
index 00000000..1f486f6d
--- /dev/null
+++ b/src/components/shared/Description.tsx
@@ -0,0 +1,18 @@
+import { MarkdownViewer } from '@stoplight/markdown-viewer';
+import { Popover } from '@stoplight/ui-kit';
+import * as React from 'react';
+
+export const Description: React.FunctionComponent<{ value: string }> = ({ value }) => (
+ {value}}
+ targetClassName="text-darken-7 dark:text-lighten-6 w-full truncate"
+ content={
+
+
+
+ }
+ />
+);
diff --git a/src/components/shared/Divider.tsx b/src/components/shared/Divider.tsx
new file mode 100644
index 00000000..7800e0d1
--- /dev/null
+++ b/src/components/shared/Divider.tsx
@@ -0,0 +1,8 @@
+import * as React from 'react';
+
+export const Divider: React.FunctionComponent = ({ children }) => (
+
+);
diff --git a/src/components/shared/Property.tsx b/src/components/shared/Property.tsx
new file mode 100644
index 00000000..851d8528
--- /dev/null
+++ b/src/components/shared/Property.tsx
@@ -0,0 +1,69 @@
+import { size } from 'lodash';
+import * as React from 'react';
+import { GoToRefHandler, IArrayNode, IObjectNode, SchemaKind, SchemaNodeWithMeta } from '../../types';
+import { isCombiner, isRef } from '../../utils';
+import { inferType } from '../../utils/inferType';
+import { Types } from './Types';
+
+export interface IProperty {
+ node: SchemaNodeWithMeta;
+ onGoToRef?: GoToRefHandler;
+}
+
+export const Property: React.FunctionComponent = ({ node, onGoToRef }) => {
+ const type = isRef(node) ? '$ref' : isCombiner(node) ? node.combiner : node.type;
+ const subtype =
+ type === SchemaKind.Array && (node as IArrayNode).items !== undefined
+ ? inferType((node as IArrayNode).items!)
+ : undefined;
+
+ const childrenCount = React.useMemo(
+ () => {
+ if (type === SchemaKind.Object) {
+ return size((node as IObjectNode).properties);
+ }
+
+ if (subtype === SchemaKind.Object) {
+ return size(((node as IArrayNode).items as IObjectNode).properties);
+ }
+
+ if (subtype === SchemaKind.Array) {
+ return size((node as IArrayNode).items as IArrayNode);
+ }
+
+ return null;
+ },
+ [node],
+ );
+
+ const handleGoToRef = React.useCallback(
+ () => {
+ if (onGoToRef) {
+ onGoToRef(node.$ref!, node);
+ }
+ },
+ [onGoToRef, node],
+ );
+
+ return (
+ <>
+ {node.name && {node.name}
}
+
+
+ {type === '$ref' ? `[${node.$ref}]` : null}
+
+
+ {type === '$ref' && onGoToRef ? (
+
+ (go to ref)
+
+ ) : null}
+
+ {childrenCount !== null && {`{${childrenCount}}`}
}
+
+ {'pattern' in node && node.pattern ? (
+ (pattern property)
+ ) : null}
+ >
+ );
+};
diff --git a/src/components/Types.tsx b/src/components/shared/Types.tsx
similarity index 96%
rename from src/components/Types.tsx
rename to src/components/shared/Types.tsx
index 5910d5b4..d7b0494d 100644
--- a/src/components/Types.tsx
+++ b/src/components/shared/Types.tsx
@@ -3,7 +3,7 @@ import cn from 'classnames';
import { JSONSchema4TypeName } from 'json-schema';
import * as React from 'react';
-import { ITreeNodeMeta, JSONSchema4CombinerName } from '../types';
+import { ITreeNodeMeta, JSONSchema4CombinerName } from '../../types';
/**
* TYPE
diff --git a/src/components/shared/Validations.tsx b/src/components/shared/Validations.tsx
new file mode 100644
index 00000000..c6dccae8
--- /dev/null
+++ b/src/components/shared/Validations.tsx
@@ -0,0 +1,66 @@
+import { Dictionary } from '@stoplight/types';
+import { Popover } from '@stoplight/ui-kit';
+import cn from 'classnames';
+import * as React from 'react';
+
+export interface IValidations {
+ required: boolean;
+ validations: Dictionary;
+}
+
+export const Validations: React.FunctionComponent = ({ required, validations }) => {
+ const validationCount = Object.keys(validations).length;
+
+ const requiredElem = (
+
+ {required ? 'required' : 'optional'}
+ {validationCount ? `+${validationCount}` : ''}
+
+ );
+
+ return validationCount ? (
+
+ {Object.keys(validations).map((key, index) => {
+ const validation = validations[key];
+
+ let elem = null;
+ if (Array.isArray(validation)) {
+ elem = validation.map((v, i) => (
+
+
{String(v)}
+ {i < validation.length - 1 ?
,
: null}
+
+ ));
+ } else if (typeof validation === 'object') {
+ elem = (
+
+ {'{...}'}
+
+ );
+ } else {
+ elem = (
+
+ {JSON.stringify(validation)}
+
+ );
+ }
+
+ return (
+
+ );
+ })}
+
+ }
+ target={requiredElem}
+ />
+ ) : (
+ requiredElem
+ );
+};
diff --git a/src/components/shared/index.ts b/src/components/shared/index.ts
new file mode 100644
index 00000000..2fb6949c
--- /dev/null
+++ b/src/components/shared/index.ts
@@ -0,0 +1,6 @@
+export * from './Caret';
+export * from './Description';
+export * from './Description';
+export * from './Property';
+export * from './Types';
+export * from './Validations';
diff --git a/src/index.ts b/src/index.ts
index bfc7ee1f..195d95f5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1 +1,2 @@
-export * from './components/JsonSchemaViewer';
+export * from './components';
+export * from './types';
diff --git a/src/types.ts b/src/types.ts
index 8bd7adbe..169ddf5c 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,11 +1,7 @@
-import { TreeListNode } from '@stoplight/tree-list';
+import { IRowRendererOptions, TreeListNode, TreeStore } from '@stoplight/tree-list';
import { Dictionary, JsonPath } from '@stoplight/types';
import { JSONSchema4, JSONSchema4TypeName } from 'json-schema';
-
-export interface IExtendableRenderers {
- rowRendererRight?: (node: SchemaTreeListNode) => React.ReactElement;
- schemaControlsRenderer?: () => React.ReactElement;
-}
+import * as React from 'react';
export const enum SchemaKind {
Any = 'any',
@@ -73,4 +69,10 @@ export type SchemaNodeWithMeta = SchemaNode & ITreeNodeMeta;
export type SchemaTreeListNode = TreeListNode;
-export type GoToRefHandler = (path: string, node: SchemaTreeListNode) => void;
+export type GoToRefHandler = (path: string, node: SchemaNodeWithMeta) => void;
+
+export type RowRenderer = (
+ node: SchemaTreeListNode,
+ rowOptions: IRowRendererOptions,
+ treeStore: TreeStore,
+) => React.ReactNode;
diff --git a/src/utils/renderSchema.ts b/src/utils/renderSchema.ts
index 40d0f7c1..f6e16e53 100644
--- a/src/utils/renderSchema.ts
+++ b/src/utils/renderSchema.ts
@@ -11,6 +11,7 @@ import { walk } from './walk';
// @ts-ignore no typings
import * as resolveAllOf from 'json-schema-merge-allof';
+import { inferType } from './inferType';
type Walker = (
schema: JSONSchema4,
@@ -80,9 +81,7 @@ export const renderSchema: Walker = function*(schema, level = 0, meta = { path:
subtype:
'$ref' in parsedSchema.items
? `$ref( ${parsedSchema.items.$ref} )`
- : parsedSchema.items.type ||
- (parsedSchema.items.properties && 'object') ||
- (parsedSchema.items.items && 'array'),
+ : parsedSchema.items.type || inferType(parsedSchema.items),
}),
path,
},