Skip to content

Commit 34995d2

Browse files
authored
feat: consume schema title property (#76)
* feat: pretty titles * fix: pr feedback
1 parent 1a18f25 commit 34995d2

File tree

12 files changed

+559
-129
lines changed

12 files changed

+559
-129
lines changed

src/__stories__/JsonSchemaViewer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ storiesOf('JsonSchemaViewer', module)
2020
name={text('name', 'my schema')}
2121
schema={schema as JSONSchema4}
2222
defaultExpandedDepth={number('defaultExpandedDepth', 0)}
23-
expanded={boolean('expanded', false)}
23+
expanded={boolean('expanded', true)}
2424
hideTopBar={boolean('hideTopBar', false)}
2525
shouldResolveEagerly={boolean('shouldResolveEagerly', false)}
2626
onGoToRef={action('onGoToRef')}

src/__stories__/_styles.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
@import "~@stoplight/tree-list/styles/_tree-list.scss";
22
@import "~@stoplight/ui-kit/styles/_ui-kit.scss";
3+
4+
.JsonSchemaViewer {
5+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', system-ui, sans-serif,
6+
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';}

src/components/__tests__/Property.spec.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,114 @@ describe('Property component', () => {
306306
expect(wrapper.find('div').first()).not.toExist();
307307
});
308308
});
309+
310+
describe('properties titles', () => {
311+
let treeNode: SchemaTreeListNode;
312+
313+
beforeEach(() => {
314+
treeNode = {
315+
id: 'foo',
316+
name: '',
317+
parent: null,
318+
};
319+
});
320+
321+
it('given object type, should render title', () => {
322+
const schema: JSONSchema4 = {
323+
title: 'User',
324+
type: 'object',
325+
properties: {
326+
name: {
327+
type: 'string',
328+
},
329+
},
330+
};
331+
332+
metadataStore.set(treeNode, {
333+
schemaNode: walk(schema).next().value.node,
334+
path: [],
335+
schema,
336+
});
337+
338+
const wrapper = shallow(<Property node={treeNode} />);
339+
expect(wrapper.find(Types)).toExist();
340+
expect(wrapper.find(Types)).toHaveProp('type', 'object');
341+
expect(wrapper.find(Types)).toHaveProp('subtype', void 0);
342+
expect(wrapper.find(Types)).toHaveProp('title', 'User');
343+
});
344+
345+
it('given array type with non-array items, should render title', () => {
346+
const schema: JSONSchema4 = {
347+
type: 'array',
348+
items: {
349+
title: 'User',
350+
type: 'object',
351+
properties: {
352+
name: {
353+
type: 'string',
354+
},
355+
},
356+
},
357+
};
358+
359+
metadataStore.set(treeNode, {
360+
schemaNode: walk(schema).next().value.node,
361+
path: [],
362+
schema,
363+
});
364+
365+
const wrapper = shallow(<Property node={treeNode} />);
366+
expect(wrapper.find(Types)).toExist();
367+
expect(wrapper.find(Types)).toHaveProp('type', 'array');
368+
expect(wrapper.find(Types)).toHaveProp('subtype', 'object');
369+
expect(wrapper.find(Types)).toHaveProp('title', 'User');
370+
});
371+
372+
it('given array with no items, should render title', () => {
373+
const schema: JSONSchema4 = {
374+
type: 'array',
375+
title: 'User',
376+
};
377+
378+
metadataStore.set(treeNode, {
379+
schemaNode: walk(schema).next().value.node,
380+
path: [],
381+
schema,
382+
});
383+
384+
const wrapper = shallow(<Property node={treeNode} />);
385+
expect(wrapper.find(Types)).toExist();
386+
expect(wrapper.find(Types)).toHaveProp('type', 'array');
387+
expect(wrapper.find(Types)).toHaveProp('subtype', void 0);
388+
expect(wrapper.find(Types)).toHaveProp('title', 'User');
389+
});
390+
391+
it('given array with defined items, should not render title', () => {
392+
const schema: JSONSchema4 = {
393+
type: 'array',
394+
items: [
395+
{
396+
title: 'foo',
397+
type: 'string',
398+
},
399+
{
400+
title: 'bar',
401+
type: 'number',
402+
},
403+
],
404+
};
405+
406+
metadataStore.set(treeNode, {
407+
schemaNode: walk(schema).next().value.node,
408+
path: [],
409+
schema,
410+
});
411+
412+
const wrapper = shallow(<Property node={treeNode} />);
413+
expect(wrapper.find(Types)).toExist();
414+
expect(wrapper.find(Types)).toHaveProp('type', 'array');
415+
expect(wrapper.find(Types)).toHaveProp('subtype', void 0);
416+
expect(wrapper.find(Types)).toHaveProp('title', void 0);
417+
});
418+
});
309419
});
Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,89 @@
11
import { shallow } from 'enzyme';
22
import 'jest-enzyme';
33
import * as React from 'react';
4+
import { SchemaKind } from '../../types';
45
import { IType, PropertyTypeColors, Type } from '../shared/Types';
56

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

1011
expect(wrapper).toHaveText(type);
1112
});
1213

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

1718
expect(wrapper).toHaveText('foo');
1819
});
1920

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

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

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

2930
expect(wrapper).toHaveText('array');
3031
});
32+
33+
describe('titles', () => {
34+
describe('when main type equals array', () => {
35+
it('given object type, should display title', () => {
36+
const wrapper = shallow(<Type type={SchemaKind.Array} subtype={SchemaKind.Object} title="foo" />);
37+
38+
expect(wrapper).toHaveText('foo[]');
39+
});
40+
41+
it('given array type, should display title', () => {
42+
const wrapper = shallow(<Type type={SchemaKind.Array} subtype={SchemaKind.Array} title="foo" />);
43+
44+
expect(wrapper).toHaveText('foo[]');
45+
});
46+
47+
it('given primitive type, should not display title', () => {
48+
const wrapper = shallow(<Type type={SchemaKind.Array} subtype={SchemaKind.String} title="foo" />);
49+
50+
expect(wrapper).toHaveText('array[string]');
51+
});
52+
53+
it('given mixed types, should not display title', () => {
54+
const wrapper = shallow(
55+
<Type type={SchemaKind.Array} subtype={[SchemaKind.String, SchemaKind.Object]} title="foo" />,
56+
);
57+
58+
expect(wrapper).toHaveText('array[string,object]');
59+
});
60+
61+
it('given $ref type, should display title', () => {
62+
const wrapper = shallow(<Type type={SchemaKind.Array} subtype="$ref" title="foo" />);
63+
64+
expect(wrapper).toHaveText('foo[]');
65+
});
66+
});
67+
68+
it('given object type, should always display title', () => {
69+
const wrapper = shallow(<Type type={SchemaKind.Object} subtype={void 0} title="foo" />);
70+
71+
expect(wrapper).toHaveText('foo');
72+
});
73+
74+
it('given $ref type, should always display title', () => {
75+
const wrapper = shallow(<Type type="$ref" subtype={void 0} title="foo" />);
76+
77+
expect(wrapper).toHaveText('foo');
78+
});
79+
80+
it.each([SchemaKind.Null, SchemaKind.Integer, SchemaKind.Number, SchemaKind.Boolean, SchemaKind.String])(
81+
'given primitive %s type, should not display title',
82+
type => {
83+
const wrapper = shallow(<Type type={type} subtype={void 0} title="foo" />);
84+
85+
expect(wrapper).toHaveText(type);
86+
},
87+
);
88+
});
3189
});

src/components/shared/Property.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,35 @@ function isExternalRefSchemaNode(schemaNode: SchemaNode) {
4747
return isRefNode(schemaNode) && schemaNode.$ref !== null && !isLocalRef(schemaNode.$ref);
4848
}
4949

50+
function retrieve$ref(node: SchemaNode): Optional<string> {
51+
if (isRefNode(node) && node.$ref !== null) {
52+
return node.$ref;
53+
}
54+
55+
if (hasRefItems(node) && node.items.$ref !== null) {
56+
return `$ref(${node.items.$ref})`;
57+
}
58+
59+
return;
60+
}
61+
62+
function getTitle(node: SchemaNode): Optional<string> {
63+
if (isArrayNodeWithItems(node)) {
64+
if (Array.isArray(node.items) || !node.items.title) {
65+
return retrieve$ref(node);
66+
}
67+
68+
return node.items.title;
69+
}
70+
71+
return node.title || retrieve$ref(node);
72+
}
73+
5074
export const Property: React.FunctionComponent<IProperty> = ({ node: treeNode, onGoToRef }) => {
5175
const { path, schemaNode: node } = getSchemaNodeMetadata(treeNode);
5276
const type = isRefNode(node) ? '$ref' : isCombinerNode(node) ? node.combiner : node.type;
5377
const subtype = isArrayNodeWithItems(node) ? (hasRefItems(node) ? '$ref' : inferType(node.items)) : void 0;
78+
const title = getTitle(node);
5479

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

81-
<Types type={type} subtype={subtype}>
82-
{isRefNode(node) && node.$ref !== null ? `[${node.$ref}]` : null}
83-
{hasRefItems(node) && node.items.$ref !== null ? `[$ref(${node.items.$ref})]` : null}
84-
</Types>
106+
<Types type={type} subtype={subtype} title={title} />
85107

86108
{onGoToRef && isExternalRefSchemaNode(node) ? (
87109
<a role="button" className="text-blue-4 ml-2" onClick={handleGoToRef}>

src/components/shared/Types.tsx

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import cn from 'classnames';
33
import { JSONSchema4TypeName } from 'json-schema';
44
import * as React from 'react';
55

6-
import { JSONSchema4CombinerName } from '../../types';
6+
import { JSONSchema4CombinerName, SchemaKind } from '../../types';
77

88
/**
99
* TYPE
@@ -12,14 +12,45 @@ export interface IType {
1212
type: JSONSchema4TypeName | JSONSchema4CombinerName | 'binary' | '$ref';
1313
subtype: Optional<JSONSchema4TypeName | JSONSchema4TypeName[]> | '$ref';
1414
className?: string;
15+
title: Optional<string>;
1516
}
1617

17-
export const Type: React.FunctionComponent<IType> = ({ className, children, type, subtype }) => {
18+
function shouldRenderTitle(type: string): boolean {
19+
return type === SchemaKind.Array || type === SchemaKind.Object || type === '$ref';
20+
}
21+
22+
function getPrintableArrayType(subtype: IType['subtype'], title: IType['title']): string {
23+
if (!subtype) return SchemaKind.Array;
24+
25+
if (Array.isArray(subtype)) {
26+
return `${SchemaKind.Array}[${subtype.join(',')}]`;
27+
}
28+
29+
if (title && shouldRenderTitle(subtype)) {
30+
return `${title}[]`;
31+
}
32+
33+
if (subtype !== SchemaKind.Array && subtype !== '$ref') {
34+
return `${SchemaKind.Array}[${subtype}]`;
35+
}
36+
37+
return SchemaKind.Array;
38+
}
39+
40+
function getPrintableType(type: IType['type'], subtype: IType['subtype'], title: IType['title']): string {
41+
if (type === SchemaKind.Array) {
42+
return getPrintableArrayType(subtype, title);
43+
} else if (title && shouldRenderTitle(type)) {
44+
return title;
45+
} else {
46+
return type;
47+
}
48+
}
49+
50+
export const Type: React.FunctionComponent<IType> = ({ className, title, type, subtype }) => {
1851
return (
1952
<span className={cn(className, PropertyTypeColors[type], 'truncate')}>
20-
{type === 'array' && subtype && subtype !== 'array' && subtype !== '$ref' ? `array[${subtype}]` : type}
21-
22-
{children}
53+
{getPrintableType(type, subtype, title)}
2354
</span>
2455
);
2556
};
@@ -32,21 +63,22 @@ interface ITypes {
3263
className?: string;
3364
type: Optional<JSONSchema4TypeName | JSONSchema4TypeName[] | JSONSchema4CombinerName | '$ref'>;
3465
subtype: Optional<JSONSchema4TypeName | JSONSchema4TypeName[] | '$ref'>;
66+
title: Optional<string>;
3567
}
3668

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

4072
if (!Array.isArray(type)) {
41-
return <Type className={className} type={type} subtype={subtype} children={children} />;
73+
return <Type className={className} type={type} subtype={subtype} title={title} />;
4274
}
4375

4476
return (
4577
<div className={cn(className, 'truncate')}>
4678
<>
4779
{type.map((name, i, { length }) => (
4880
<React.Fragment key={i}>
49-
<Type key={i} type={name} subtype={subtype} />
81+
<Type key={i} type={name} subtype={subtype} title={title} />
5082

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

0 commit comments

Comments
 (0)