Skip to content

Commit

Permalink
feat: consume schema title property (#76)
Browse files Browse the repository at this point in the history
* feat: pretty titles

* fix: pr feedback
  • Loading branch information
P0lip authored May 25, 2020
1 parent 1a18f25 commit 34995d2
Show file tree
Hide file tree
Showing 12 changed files with 559 additions and 129 deletions.
2 changes: 1 addition & 1 deletion src/__stories__/JsonSchemaViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ storiesOf('JsonSchemaViewer', module)
name={text('name', 'my schema')}
schema={schema as JSONSchema4}
defaultExpandedDepth={number('defaultExpandedDepth', 0)}
expanded={boolean('expanded', false)}
expanded={boolean('expanded', true)}
hideTopBar={boolean('hideTopBar', false)}
shouldResolveEagerly={boolean('shouldResolveEagerly', false)}
onGoToRef={action('onGoToRef')}
Expand Down
4 changes: 4 additions & 0 deletions src/__stories__/_styles.scss
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
@import "~@stoplight/tree-list/styles/_tree-list.scss";
@import "~@stoplight/ui-kit/styles/_ui-kit.scss";

.JsonSchemaViewer {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';}
110 changes: 110 additions & 0 deletions src/components/__tests__/Property.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,114 @@ describe('Property component', () => {
expect(wrapper.find('div').first()).not.toExist();
});
});

describe('properties titles', () => {
let treeNode: SchemaTreeListNode;

beforeEach(() => {
treeNode = {
id: 'foo',
name: '',
parent: null,
};
});

it('given object type, should render title', () => {
const schema: JSONSchema4 = {
title: 'User',
type: 'object',
properties: {
name: {
type: 'string',
},
},
};

metadataStore.set(treeNode, {
schemaNode: walk(schema).next().value.node,
path: [],
schema,
});

const wrapper = shallow(<Property node={treeNode} />);
expect(wrapper.find(Types)).toExist();
expect(wrapper.find(Types)).toHaveProp('type', 'object');
expect(wrapper.find(Types)).toHaveProp('subtype', void 0);
expect(wrapper.find(Types)).toHaveProp('title', 'User');
});

it('given array type with non-array items, should render title', () => {
const schema: JSONSchema4 = {
type: 'array',
items: {
title: 'User',
type: 'object',
properties: {
name: {
type: 'string',
},
},
},
};

metadataStore.set(treeNode, {
schemaNode: walk(schema).next().value.node,
path: [],
schema,
});

const wrapper = shallow(<Property node={treeNode} />);
expect(wrapper.find(Types)).toExist();
expect(wrapper.find(Types)).toHaveProp('type', 'array');
expect(wrapper.find(Types)).toHaveProp('subtype', 'object');
expect(wrapper.find(Types)).toHaveProp('title', 'User');
});

it('given array with no items, should render title', () => {
const schema: JSONSchema4 = {
type: 'array',
title: 'User',
};

metadataStore.set(treeNode, {
schemaNode: walk(schema).next().value.node,
path: [],
schema,
});

const wrapper = shallow(<Property node={treeNode} />);
expect(wrapper.find(Types)).toExist();
expect(wrapper.find(Types)).toHaveProp('type', 'array');
expect(wrapper.find(Types)).toHaveProp('subtype', void 0);
expect(wrapper.find(Types)).toHaveProp('title', 'User');
});

it('given array with defined items, should not render title', () => {
const schema: JSONSchema4 = {
type: 'array',
items: [
{
title: 'foo',
type: 'string',
},
{
title: 'bar',
type: 'number',
},
],
};

metadataStore.set(treeNode, {
schemaNode: walk(schema).next().value.node,
path: [],
schema,
});

const wrapper = shallow(<Property node={treeNode} />);
expect(wrapper.find(Types)).toExist();
expect(wrapper.find(Types)).toHaveProp('type', 'array');
expect(wrapper.find(Types)).toHaveProp('subtype', void 0);
expect(wrapper.find(Types)).toHaveProp('title', void 0);
});
});
});
66 changes: 62 additions & 4 deletions src/components/__tests__/Type.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,89 @@
import { shallow } from 'enzyme';
import 'jest-enzyme';
import * as React from 'react';
import { SchemaKind } from '../../types';
import { IType, PropertyTypeColors, Type } from '../shared/Types';

describe('Type component', () => {
it.each(Object.keys(PropertyTypeColors))('should handle $s type', type => {
const wrapper = shallow(<Type type={type as IType['type']} subtype={void 0} />);
const wrapper = shallow(<Type type={type as IType['type']} subtype={void 0} title={void 0} />);

expect(wrapper).toHaveText(type);
});

it('should handle unknown types', () => {
// @ts-ignore
const wrapper = shallow(<Type type="foo" subtype={void 0} />);
const wrapper = shallow(<Type type="foo" subtype={void 0} title={void 0} />);

expect(wrapper).toHaveText('foo');
});

it('should display non-array subtype for array', () => {
const wrapper = shallow(<Type type="array" subtype="object" />);
const wrapper = shallow(<Type type={SchemaKind.Array} subtype={SchemaKind.Object} title={void 0} />);

expect(wrapper).toHaveText('array[object]');
});

it('should not display array subtype for array', () => {
const wrapper = shallow(<Type type="array" subtype="array" />);
const wrapper = shallow(<Type type={SchemaKind.Array} subtype={SchemaKind.Array} title={void 0} />);

expect(wrapper).toHaveText('array');
});

describe('titles', () => {
describe('when main type equals array', () => {
it('given object type, should display title', () => {
const wrapper = shallow(<Type type={SchemaKind.Array} subtype={SchemaKind.Object} title="foo" />);

expect(wrapper).toHaveText('foo[]');
});

it('given array type, should display title', () => {
const wrapper = shallow(<Type type={SchemaKind.Array} subtype={SchemaKind.Array} title="foo" />);

expect(wrapper).toHaveText('foo[]');
});

it('given primitive type, should not display title', () => {
const wrapper = shallow(<Type type={SchemaKind.Array} subtype={SchemaKind.String} title="foo" />);

expect(wrapper).toHaveText('array[string]');
});

it('given mixed types, should not display title', () => {
const wrapper = shallow(
<Type type={SchemaKind.Array} subtype={[SchemaKind.String, SchemaKind.Object]} title="foo" />,
);

expect(wrapper).toHaveText('array[string,object]');
});

it('given $ref type, should display title', () => {
const wrapper = shallow(<Type type={SchemaKind.Array} subtype="$ref" title="foo" />);

expect(wrapper).toHaveText('foo[]');
});
});

it('given object type, should always display title', () => {
const wrapper = shallow(<Type type={SchemaKind.Object} subtype={void 0} title="foo" />);

expect(wrapper).toHaveText('foo');
});

it('given $ref type, should always display title', () => {
const wrapper = shallow(<Type type="$ref" subtype={void 0} title="foo" />);

expect(wrapper).toHaveText('foo');
});

it.each([SchemaKind.Null, SchemaKind.Integer, SchemaKind.Number, SchemaKind.Boolean, SchemaKind.String])(
'given primitive %s type, should not display title',
type => {
const wrapper = shallow(<Type type={type} subtype={void 0} title="foo" />);

expect(wrapper).toHaveText(type);
},
);
});
});
30 changes: 26 additions & 4 deletions src/components/shared/Property.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,35 @@ function isExternalRefSchemaNode(schemaNode: SchemaNode) {
return isRefNode(schemaNode) && schemaNode.$ref !== null && !isLocalRef(schemaNode.$ref);
}

function retrieve$ref(node: SchemaNode): Optional<string> {
if (isRefNode(node) && node.$ref !== null) {
return node.$ref;
}

if (hasRefItems(node) && node.items.$ref !== null) {
return `$ref(${node.items.$ref})`;
}

return;
}

function getTitle(node: SchemaNode): Optional<string> {
if (isArrayNodeWithItems(node)) {
if (Array.isArray(node.items) || !node.items.title) {
return retrieve$ref(node);
}

return node.items.title;
}

return node.title || retrieve$ref(node);
}

export const Property: React.FunctionComponent<IProperty> = ({ node: treeNode, onGoToRef }) => {
const { path, schemaNode: node } = getSchemaNodeMetadata(treeNode);
const type = isRefNode(node) ? '$ref' : isCombinerNode(node) ? node.combiner : node.type;
const subtype = isArrayNodeWithItems(node) ? (hasRefItems(node) ? '$ref' : inferType(node.items)) : void 0;
const title = getTitle(node);

const childrenCount = React.useMemo<number | null>(() => {
if (type === SchemaKind.Object || (Array.isArray(type) && type.includes(SchemaKind.Object))) {
Expand Down Expand Up @@ -78,10 +103,7 @@ export const Property: React.FunctionComponent<IProperty> = ({ node: treeNode, o
<>
{path.length > 0 && shouldShowPropertyName(treeNode) && <div className="mr-2">{path[path.length - 1]}</div>}

<Types type={type} subtype={subtype}>
{isRefNode(node) && node.$ref !== null ? `[${node.$ref}]` : null}
{hasRefItems(node) && node.items.$ref !== null ? `[$ref(${node.items.$ref})]` : null}
</Types>
<Types type={type} subtype={subtype} title={title} />

{onGoToRef && isExternalRefSchemaNode(node) ? (
<a role="button" className="text-blue-4 ml-2" onClick={handleGoToRef}>
Expand Down
48 changes: 40 additions & 8 deletions src/components/shared/Types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import cn from 'classnames';
import { JSONSchema4TypeName } from 'json-schema';
import * as React from 'react';

import { JSONSchema4CombinerName } from '../../types';
import { JSONSchema4CombinerName, SchemaKind } from '../../types';

/**
* TYPE
Expand All @@ -12,14 +12,45 @@ export interface IType {
type: JSONSchema4TypeName | JSONSchema4CombinerName | 'binary' | '$ref';
subtype: Optional<JSONSchema4TypeName | JSONSchema4TypeName[]> | '$ref';
className?: string;
title: Optional<string>;
}

export const Type: React.FunctionComponent<IType> = ({ className, children, type, subtype }) => {
function shouldRenderTitle(type: string): boolean {
return type === SchemaKind.Array || type === SchemaKind.Object || type === '$ref';
}

function getPrintableArrayType(subtype: IType['subtype'], title: IType['title']): string {
if (!subtype) return SchemaKind.Array;

if (Array.isArray(subtype)) {
return `${SchemaKind.Array}[${subtype.join(',')}]`;
}

if (title && shouldRenderTitle(subtype)) {
return `${title}[]`;
}

if (subtype !== SchemaKind.Array && subtype !== '$ref') {
return `${SchemaKind.Array}[${subtype}]`;
}

return SchemaKind.Array;
}

function getPrintableType(type: IType['type'], subtype: IType['subtype'], title: IType['title']): string {
if (type === SchemaKind.Array) {
return getPrintableArrayType(subtype, title);
} else if (title && shouldRenderTitle(type)) {
return title;
} else {
return type;
}
}

export const Type: React.FunctionComponent<IType> = ({ className, title, type, subtype }) => {
return (
<span className={cn(className, PropertyTypeColors[type], 'truncate')}>
{type === 'array' && subtype && subtype !== 'array' && subtype !== '$ref' ? `array[${subtype}]` : type}

{children}
{getPrintableType(type, subtype, title)}
</span>
);
};
Expand All @@ -32,21 +63,22 @@ interface ITypes {
className?: string;
type: Optional<JSONSchema4TypeName | JSONSchema4TypeName[] | JSONSchema4CombinerName | '$ref'>;
subtype: Optional<JSONSchema4TypeName | JSONSchema4TypeName[] | '$ref'>;
title: Optional<string>;
}

export const Types: React.FunctionComponent<ITypes> = ({ className, type, subtype, children }) => {
export const Types: React.FunctionComponent<ITypes> = ({ className, title, type, subtype }) => {
if (type === void 0) return null;

if (!Array.isArray(type)) {
return <Type className={className} type={type} subtype={subtype} children={children} />;
return <Type className={className} type={type} subtype={subtype} title={title} />;
}

return (
<div className={cn(className, 'truncate')}>
<>
{type.map((name, i, { length }) => (
<React.Fragment key={i}>
<Type key={i} type={name} subtype={subtype} />
<Type key={i} type={name} subtype={subtype} title={title} />

{i < length - 1 && (
<span key={`${i}-sep`} className="text-darken-7 dark:text-lighten-6">
Expand Down
Loading

0 comments on commit 34995d2

Please sign in to comment.