Skip to content

Commit dcc8eea

Browse files
P0lipmarbemac
authored andcommitted
feat: introduce rowRenderer (#40)
* refactor: use inferType * revert: "feat: introduce rowRendererRight " This reverts commit dd89bc4. * feat: make allOf merging optional * refactor: split components * fix: minor tweaks * chore: rexport components + add a story * feat: expose treeStore * feat: make SchemaRow more flexible * chore: improve storybook example * fix: lint --fix
1 parent 1a08e63 commit dcc8eea

File tree

17 files changed

+320
-224
lines changed

17 files changed

+320
-224
lines changed

src/__stories__/JsonSchemaViewer.tsx

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import * as React from 'react';
22

33
import { State, Store } from '@sambego/storybook-state';
4+
import { Button, Checkbox, Icon } from '@stoplight/ui-kit';
45
import { action } from '@storybook/addon-actions';
56
import { boolean, number, object, select, text, withKnobs } from '@storybook/addon-knobs';
67
import { storiesOf } from '@storybook/react';
7-
import { JsonSchemaViewer } from '../components';
8-
9-
import { Checkbox } from '@stoplight/ui-kit';
108
import { JSONSchema4 } from 'json-schema';
9+
import { JsonSchemaViewer, SchemaRow } from '../components';
10+
1111
import * as allOfSchemaResolved from '../__fixtures__/allOf/allOf-resolved.json';
1212
import * as allOfSchema from '../__fixtures__/allOf/allOf-schema.json';
1313
import * as schema from '../__fixtures__/default-schema.json';
1414
import * as schemaWithRefs from '../__fixtures__/ref/original.json';
1515
import * as dereferencedSchema from '../__fixtures__/ref/resolved.json';
1616
import * as stressSchema from '../__fixtures__/stress-schema.json';
17+
import { RowRenderer } from '../types';
1718
import { Wrapper } from './utils/Wrapper';
1819

1920
storiesOf('JsonSchemaViewer', module)
@@ -44,6 +45,7 @@ storiesOf('JsonSchemaViewer', module)
4445
expanded={boolean('expanded', true)}
4546
hideTopBar={boolean('hideTopBar', false)}
4647
onGoToRef={action('onGoToRef')}
48+
mergeAllOf={boolean('mergeAllOf', true)}
4749
/>
4850
</State>
4951
);
@@ -56,8 +58,35 @@ storiesOf('JsonSchemaViewer', module)
5658
hideTopBar={boolean('hideTopBar', false)}
5759
onGoToRef={action('onGoToRef')}
5860
maxRows={number('maxRows', 5)}
61+
mergeAllOf={boolean('mergeAllOf', true)}
5962
/>
6063
))
64+
.add('custom row renderer', () => {
65+
const customRowRenderer: RowRenderer = (node, rowOptions) => {
66+
return (
67+
<>
68+
<SchemaRow node={node} rowOptions={rowOptions} />
69+
<div className="flex h-full items-center">
70+
<Button className="pl-1 mr-1" small minimal icon={<Icon color="grey" iconSize={12} icon="issue" />} />
71+
<Checkbox className="mb-0" />
72+
</div>
73+
</>
74+
);
75+
};
76+
77+
return (
78+
<JsonSchemaViewer
79+
name={text('name', 'my schema')}
80+
schema={object('schema', schema as JSONSchema4)}
81+
expanded={boolean('expanded', true)}
82+
hideTopBar={boolean('hideTopBar', false)}
83+
onGoToRef={action('onGoToRef')}
84+
maxRows={number('maxRows', 5)}
85+
mergeAllOf={boolean('mergeAllOf', true)}
86+
rowRenderer={customRowRenderer}
87+
/>
88+
);
89+
})
6190
.add('stress-test schema', () => (
6291
<JsonSchemaViewer
6392
name={text('name', 'my stress schema')}
@@ -67,6 +96,7 @@ storiesOf('JsonSchemaViewer', module)
6796
hideTopBar={boolean('hideTopBar', false)}
6897
onGoToRef={action('onGoToRef')}
6998
maxRows={number('maxRows', 10)}
99+
mergeAllOf={boolean('mergeAllOf', true)}
70100
/>
71101
))
72102
.add('allOf-schema', () => (
@@ -76,6 +106,7 @@ storiesOf('JsonSchemaViewer', module)
76106
defaultExpandedDepth={number('defaultExpandedDepth', 2)}
77107
expanded={boolean('expanded', false)}
78108
hideTopBar={boolean('hideTopBar', false)}
109+
mergeAllOf={boolean('mergeAllOf', true)}
79110
onGoToRef={action('onGoToRef')}
80111
/>
81112
))
@@ -96,6 +127,7 @@ storiesOf('JsonSchemaViewer', module)
96127
defaultExpandedDepth={number('defaultExpandedDepth', 2)}
97128
hideTopBar={boolean('hideTopBar', false)}
98129
onGoToRef={action('onGoToRef')}
130+
mergeAllOf={boolean('mergeAllOf', true)}
99131
/>
100132
))
101133
.add('dark', () => (
@@ -107,21 +139,7 @@ storiesOf('JsonSchemaViewer', module)
107139
expanded={boolean('expanded', false)}
108140
hideTopBar={boolean('hideTopBar', false)}
109141
onGoToRef={action('onGoToRef')}
142+
mergeAllOf={boolean('mergeAllOf', true)}
110143
/>
111144
</div>
112-
))
113-
.add('with rowRendererRight', () => (
114-
<JsonSchemaViewer
115-
rowRendererRight={() => (
116-
<span style={{ position: 'relative', top: '5px' }}>
117-
<Checkbox />
118-
</span>
119-
)}
120-
name={text('name', 'my schema')}
121-
schema={schema as JSONSchema4}
122-
defaultExpandedDepth={number('defaultExpandedDepth', 2)}
123-
expanded={boolean('expanded', false)}
124-
hideTopBar={boolean('hideTopBar', false)}
125-
onGoToRef={action('onGoToRef')}
126-
/>
127145
));

src/components/JsonSchemaViewer.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import { runInAction } from 'mobx';
44
import * as React from 'react';
55

66
import { JSONSchema4 } from 'json-schema';
7-
import { GoToRefHandler, IExtendableRenderers } from '../types';
7+
import { GoToRefHandler, RowRenderer } from '../types';
88
import { isSchemaViewerEmpty, renderSchema } from '../utils';
99
import { SchemaTree } from './SchemaTree';
1010

1111
export type FallbackComponent = React.ComponentType<{ error: Error | null }>;
1212

13-
export interface IJsonSchemaViewer extends IExtendableRenderers {
13+
export interface IJsonSchemaViewer {
1414
schema: JSONSchema4;
1515
dereferencedSchema?: JSONSchema4;
1616
style?: object;
@@ -24,6 +24,7 @@ export interface IJsonSchemaViewer extends IExtendableRenderers {
2424
onGoToRef?: GoToRefHandler;
2525
mergeAllOf?: boolean;
2626
FallbackComponent?: FallbackComponent;
27+
rowRenderer?: RowRenderer;
2728
}
2829

2930
export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaViewer> {
@@ -39,9 +40,7 @@ export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaVi
3940
props.dereferencedSchema || props.schema,
4041
0,
4142
{ path: [] },
42-
{
43-
mergeAllOf: props.mergeAllOf === undefined ? true : props.mergeAllOf,
44-
},
43+
{ mergeAllOf: props.mergeAllOf !== false },
4544
),
4645
),
4746
});
@@ -66,10 +65,19 @@ export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaVi
6665
});
6766
}
6867

69-
if (prevProps.schema !== this.props.schema || prevProps.dereferencedSchema !== this.props.dereferencedSchema) {
68+
if (
69+
prevProps.schema !== this.props.schema ||
70+
prevProps.dereferencedSchema !== this.props.dereferencedSchema ||
71+
prevProps.mergeAllOf !== this.props.mergeAllOf
72+
) {
7073
runInAction(() => {
7174
this.treeStore.nodes = Array.from(
72-
renderSchema(this.props.dereferencedSchema || this.props.schema, 0, { path: [] }, { mergeAllOf: true }),
75+
renderSchema(
76+
this.props.dereferencedSchema || this.props.schema,
77+
0,
78+
{ path: [] },
79+
{ mergeAllOf: this.props.mergeAllOf !== false },
80+
),
7381
);
7482
});
7583
}

src/components/SchemaRow.tsx

Lines changed: 30 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,192 +1,67 @@
1-
import { MarkdownViewer } from '@stoplight/markdown-viewer';
21
import { IRowRendererOptions } from '@stoplight/tree-list';
3-
import { Icon, Popover } from '@stoplight/ui-kit';
4-
import * as cn from 'classnames';
2+
import cn from 'classnames';
53
import * as React from 'react';
4+
import { Divider } from './shared/Divider';
65

76
import get = require('lodash/get');
8-
import map = require('lodash/map');
9-
import size = require('lodash/size');
107

11-
import { GoToRefHandler, IExtendableRenderers, SchemaNodeWithMeta, SchemaTreeListNode } from '../types';
12-
import { isCombiner, isRef } from '../utils';
13-
import { Types } from './';
8+
import { GoToRefHandler, SchemaNodeWithMeta, SchemaTreeListNode } from '../types';
9+
import { Caret } from './shared/Caret';
10+
import { Description } from './shared/Description';
11+
import { Property } from './shared/Property';
12+
import { Validations } from './shared/Validations';
1413

15-
export interface ISchemaRow extends IExtendableRenderers {
14+
export interface ISchemaRow {
15+
className?: string;
1616
node: SchemaTreeListNode;
1717
rowOptions: IRowRendererOptions;
1818
onGoToRef?: GoToRefHandler;
19-
toggleExpand: () => void;
2019
}
2120

2221
const ICON_SIZE = 12;
2322
const ICON_DIMENSION = 20;
23+
const ROW_OFFSET = 7;
2424

25-
export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({
26-
node,
27-
rowOptions,
28-
onGoToRef,
29-
rowRendererRight,
30-
toggleExpand,
31-
}) => {
25+
export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({ className, node, rowOptions, onGoToRef }) => {
3226
const schemaNode = node.metadata as SchemaNodeWithMeta;
33-
const { name, $ref, subtype, required } = schemaNode;
34-
35-
const type = isRef(schemaNode) ? '$ref' : isCombiner(schemaNode) ? schemaNode.combiner : schemaNode.type;
3627
const description = get(schemaNode, 'annotations.description');
37-
const childrenCount =
38-
type === 'object'
39-
? size(get(schemaNode, 'properties'))
40-
: subtype === 'object'
41-
? size(get(schemaNode, 'items.properties'))
42-
: size(get(schemaNode, 'items'));
43-
44-
const nodeValidations = {
45-
...('annotations' in schemaNode && schemaNode.annotations.default
46-
? { default: schemaNode.annotations.default }
47-
: {}),
48-
...get(schemaNode, 'validations', {}),
49-
};
50-
const validationCount = Object.keys(nodeValidations).length;
51-
const handleGoToRef = React.useCallback<React.MouseEventHandler>(
52-
() => {
53-
if (onGoToRef) {
54-
onGoToRef($ref!, node);
55-
}
56-
},
57-
[onGoToRef, node, $ref],
58-
);
59-
60-
const requiredElem = (
61-
<div className={cn('ml-2', required ? 'font-medium' : 'text-darken-7 dark:text-lighten-6')}>
62-
{required ? 'required' : 'optional'}
63-
{validationCount ? `+${validationCount}` : ''}
64-
</div>
65-
);
6628

67-
const combinerOffset = ICON_DIMENSION * node.level;
6829
return (
69-
<div onClick={toggleExpand} className="px-6 flex-1 w-full">
70-
{/* 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. */}
30+
<div className={cn('px-2 flex-1 w-full', className)}>
7131
<div
72-
className="flex items-center text-sm"
32+
className="flex items-center text-sm relative"
7333
style={{
74-
marginLeft: combinerOffset,
34+
marginLeft: ICON_DIMENSION * node.level, // offset for spacing
7535
}}
7636
>
7737
{node.canHaveChildren &&
7838
node.level > 0 && (
79-
<div
80-
className="absolute flex justify-center cursor-pointer p-1 rounded hover:bg-darken-3"
39+
<Caret
40+
isExpanded={!!rowOptions.isExpanded}
8141
style={{
82-
left: combinerOffset,
42+
left: ICON_DIMENSION * -1 + ROW_OFFSET / -2,
8343
width: ICON_DIMENSION,
8444
height: ICON_DIMENSION,
8545
}}
86-
>
87-
<Icon
88-
iconSize={ICON_SIZE}
89-
icon={rowOptions.isExpanded ? 'caret-down' : 'caret-right'}
90-
className="text-darken-9 dark:text-lighten-9"
91-
/>
92-
</div>
46+
size={ICON_SIZE}
47+
/>
9348
)}
9449

95-
{schemaNode.divider && (
96-
<div
97-
className="flex items-center absolute"
98-
style={{
99-
top: 0,
100-
height: 1,
101-
width: `calc(100% - ${combinerOffset}px - 1.5rem)`,
102-
}}
103-
>
104-
<div className="text-darken-7 dark:text-lighten-8 uppercase text-xs pr-2 -ml-4">{schemaNode.divider}</div>
105-
<div className="flex-1 bg-darken-5 dark:bg-lighten-5" style={{ height: 1 }} />
106-
</div>
107-
)}
50+
{schemaNode.divider && <Divider>{schemaNode.divider}</Divider>}
10851

10952
<div className="flex-1 flex truncate">
110-
{name && <div className="mr-2">{name}</div>}
111-
112-
<Types type={type} subtype={subtype}>
113-
{type === '$ref' ? `[${$ref}]` : null}
114-
</Types>
115-
116-
{type === '$ref' && onGoToRef ? (
117-
<a role="button" className="text-blue-4 ml-2" onClick={handleGoToRef}>
118-
(go to ref)
119-
</a>
120-
) : null}
121-
122-
{node.canHaveChildren && <div className="ml-2 text-darken-7 dark:text-lighten-7">{`{${childrenCount}}`}</div>}
123-
124-
{'pattern' in schemaNode && schemaNode.pattern ? (
125-
<div className="ml-2 text-darken-7 dark:text-lighten-7 truncate">(pattern property)</div>
126-
) : null}
127-
128-
{description && (
129-
<Popover
130-
boundary="window"
131-
interactionKind="hover"
132-
className="ml-2 flex-1 truncate flex items-baseline"
133-
target={<div className="text-darken-7 dark:text-lighten-7 w-full truncate">{description}</div>}
134-
targetClassName="text-darken-7 dark:text-lighten-6 w-full truncate"
135-
content={
136-
<div className="p-5" style={{ maxHeight: 500, maxWidth: 400 }}>
137-
<MarkdownViewer markdown={description} />
138-
</div>
139-
}
140-
/>
141-
)}
53+
<Property node={schemaNode} onGoToRef={onGoToRef} />
54+
{description && <Description value={description} />}
14255
</div>
14356

144-
{validationCount ? (
145-
<Popover
146-
boundary="window"
147-
interactionKind="hover"
148-
content={
149-
<div className="p-5" style={{ maxHeight: 500, maxWidth: 400 }}>
150-
{map(Object.keys(nodeValidations), (key, index) => {
151-
const validation = nodeValidations[key];
152-
153-
let elem = null;
154-
if (Array.isArray(validation)) {
155-
elem = validation.map((v, i) => (
156-
<div key={i} className="mt-1 mr-1 flex items-center">
157-
<div className="px-1 bg-gray-2 dark:bg-gray-8 font-bold text-sm rounded">{String(v)}</div>
158-
{i < validation.length - 1 ? <div>,</div> : null}
159-
</div>
160-
));
161-
} else if (typeof validation === 'object') {
162-
elem = (
163-
<div className="m-1 px-1 bg-gray-2 dark:bg-gray-8 font-bold text-sm rounded" key={index}>
164-
{'{...}'}
165-
</div>
166-
);
167-
} else {
168-
elem = (
169-
<div className="m-1 px-1 bg-gray-2 dark:bg-gray-8 font-bold text-sm rounded" key={index}>
170-
{JSON.stringify(validation)}
171-
</div>
172-
);
173-
}
174-
175-
return (
176-
<div key={index} className="py-1 flex items-baseline">
177-
<div className="font-medium pr-2 w-24">{key}:</div>
178-
<div className="flex-1 flex flex-wrap text-center">{elem}</div>
179-
</div>
180-
);
181-
})}
182-
</div>
183-
}
184-
target={requiredElem}
185-
/>
186-
) : (
187-
requiredElem
188-
)}
189-
{rowRendererRight && <div className="ml-2">{rowRendererRight(node)}</div>}
57+
<Validations
58+
required={!!schemaNode.required}
59+
validations={{
60+
...('annotations' in schemaNode &&
61+
schemaNode.annotations.default && { default: schemaNode.annotations.default }),
62+
...('validations' in schemaNode && schemaNode.validations),
63+
}}
64+
/>
19065
</div>
19166
</div>
19267
);

0 commit comments

Comments
 (0)