Skip to content

Commit eff3288

Browse files
authored
feat: integrate tree-list component [STU-123] (#14)
* feat: initial version * fix: can have children * style: even rows * fix: typings + minor divider adjustment * style: masking dialog * feat: drop limitPropertyCount * refactor: assign id * feat: collapsing * style: text-overflow * fix: handle expanded prop * chore: stress-test story * fix: make top bar display and configurable * fix: make jsv react to schema changes * test: snapshots :P * test: cover SchemaView * chore: tslint --fix
1 parent b0eff89 commit eff3288

33 files changed

+16474
-1090
lines changed

.storybook/preview-head.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@
33
html {
44
font-family: aktiv-grotesk, -apple-system, BlinkMacSystemFont, Roboto, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
55
}
6+
html, body {
7+
padding: 0;
8+
margin: 0;
9+
}
610
</style>

.storybook/theme.js

Lines changed: 4 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { createThemedModule } from '@stoplight/ui-kit';
2+
import { dark } from '../src/themes/dark';
3+
import { light } from '../src/themes/light';
24

35
const { useTheme, ThemeProvider } = createThemedModule();
46

@@ -7,72 +9,6 @@ export { useTheme, ThemeProvider };
79
export const themes = ['dark', 'light'];
810

911
export const zones = {
10-
'json-schema-viewer': ({ base }) =>
11-
base === 'dark'
12-
? {
13-
canvas: {
14-
bg: '#111',
15-
fg: '#fff',
16-
error: 'red',
17-
muted: 'rgba(255, 255, 255, 0.6)',
18-
},
19-
20-
divider: {
21-
bg: '#bababa',
22-
},
23-
24-
row: {
25-
hoverBg: '#333',
26-
hoverFg: '#fff',
27-
evenBg: '#232222',
28-
},
29-
30-
types: {
31-
object: '#83c1ff',
32-
array: '#7dff75',
33-
allOf: '#b89826',
34-
oneOf: '#b89826',
35-
anyOf: '#b89826',
36-
null: '#ff7f50',
37-
integer: '#e03b36',
38-
number: '#e03b36',
39-
boolean: '#ff69b4',
40-
binary: '#8ccda3',
41-
string: '#19c5a0',
42-
$ref: '#a359e2',
43-
},
44-
}
45-
: {
46-
canvas: {
47-
bg: '#fff',
48-
fg: '#111',
49-
error: 'red',
50-
muted: 'rgba(0, 0, 0, 0.5)',
51-
},
52-
53-
divider: {
54-
bg: '#dae1e7',
55-
},
56-
57-
row: {
58-
hoverBg: '#e9e9e9',
59-
hoverFg: '#111',
60-
evenBg: '#f3f3f3',
61-
},
62-
63-
types: {
64-
object: '#00f',
65-
array: '#008000',
66-
allOf: '#B8860B',
67-
oneOf: '#B8860B',
68-
anyOf: '#B8860B',
69-
null: '#ff7f50',
70-
integer: '#a52a2a',
71-
number: '#a52a2a',
72-
boolean: '#ff69b4',
73-
binary: '#66cdaa',
74-
string: '#008080',
75-
$ref: '#8a2be2',
76-
},
77-
},
12+
'tree-list': ({ base }) => base === 'dark' ? dark['tree-list'] : light['tree-list'],
13+
'json-schema-viewer': ({ base }) => base === 'dark' ? dark['json-schema-viewer'] : light['json-schema-viewer'],
7814
};

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@
4343
"@emotion/core": "^10.0.10",
4444
"@fortawesome/free-solid-svg-icons": "5.6.x",
4545
"@stoplight/json": "1.9.x",
46+
"@stoplight/tree-list": "^3.6.0",
4647
"@types/json-schema": "^7.0.3",
47-
"lodash": "4.17.x"
48+
"lodash": "4.17.x",
49+
"mobx": "^5.9.4"
4850
},
4951
"devDependencies": {
5052
"@sambego/storybook-state": "^1.3.4",

src/JsonSchemaViewer.tsx

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
import { TreeStore } from '@stoplight/tree-list';
12
import { Omit } from '@stoplight/types';
3+
import { runInAction } from 'mobx';
24
import * as React from 'react';
35
import { ErrorMessage } from './components/common/ErrorMessage';
46
import { MutedText } from './components/common/MutedText';
57
import { ISchemaView, SchemaView } from './SchemaView';
68
import { ThemeZone } from './theme';
79
import { isSchemaViewerEmpty } from './utils/isSchemaViewerEmpty';
10+
import { renderSchema } from './utils/renderSchema';
811

9-
export interface IJsonSchemaViewer extends Omit<ISchemaView, 'emptyText'> {
12+
export interface IJsonSchemaViewer extends Omit<ISchemaView, 'emptyText' | 'treeStore'> {
1013
emptyText?: string;
14+
defaultExpandedDepth?: number;
1115
}
1216

1317
export interface IJsonSchemaViewerState {
@@ -19,23 +23,51 @@ export class JsonSchemaViewer extends React.PureComponent<IJsonSchemaViewer, IJs
1923
error: null,
2024
};
2125

26+
protected treeStore: TreeStore;
27+
28+
constructor(props: IJsonSchemaViewer) {
29+
super(props);
30+
31+
this.treeStore = new TreeStore({
32+
defaultExpandedDepth: this.expandedDepth,
33+
nodes: Array.from(renderSchema(props.schema, props.dereferencedSchema)),
34+
});
35+
}
36+
2237
// there is no error hook yet, see https://reactjs.org/docs/hooks-faq.html#how-do-lifecycle-methods-correspond-to-hooks
2338
public static getDerivedStateFromError(error: Error): { error: IJsonSchemaViewerState['error'] } {
2439
return { error: `Error rendering schema. ${error.message}` };
2540
}
2641

42+
protected get expandedDepth(): number {
43+
if (this.props.expanded) {
44+
return 2 ** 31 - 3; // tree-list kind of equivalent of expanded: all
45+
}
46+
47+
if (this.props.defaultExpandedDepth !== undefined) {
48+
return this.props.defaultExpandedDepth;
49+
}
50+
51+
return 1;
52+
}
53+
54+
public componentDidUpdate(prevProps: Readonly<IJsonSchemaViewer>) {
55+
if (this.treeStore.defaultExpandedDepth !== this.expandedDepth) {
56+
runInAction(() => {
57+
this.treeStore.defaultExpandedDepth = this.expandedDepth;
58+
});
59+
}
60+
61+
if (prevProps.schema !== this.props.schema || prevProps.dereferencedSchema !== this.props.dereferencedSchema) {
62+
runInAction(() => {
63+
this.treeStore.nodes = Array.from(renderSchema(this.props.schema, this.props.dereferencedSchema));
64+
});
65+
}
66+
}
67+
2768
public render() {
2869
const {
29-
props: {
30-
emptyText = 'No schema defined',
31-
name,
32-
schema,
33-
schemas,
34-
limitPropertyCount,
35-
expanded,
36-
defaultExpandedDepth,
37-
...props
38-
},
70+
props: { emptyText = 'No schema defined', name, schema, schemas, expanded, defaultExpandedDepth, ...props },
3971
state: { error },
4072
} = this;
4173

@@ -58,15 +90,7 @@ export class JsonSchemaViewer extends React.PureComponent<IJsonSchemaViewer, IJs
5890

5991
return (
6092
<ThemeZone name="json-schema-viewer">
61-
<SchemaView
62-
emptyText={emptyText}
63-
defaultExpandedDepth={defaultExpandedDepth}
64-
expanded={expanded}
65-
limitPropertyCount={limitPropertyCount}
66-
name={name}
67-
schema={schema}
68-
{...props}
69-
/>
93+
<SchemaView expanded={expanded} name={name} schema={schema} treeStore={this.treeStore} {...props} />
7094
</ThemeZone>
7195
);
7296
}

src/SchemaView.tsx

Lines changed: 40 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,92 @@
1-
import { Dictionary, Omit } from '@stoplight/types';
2-
import { Box, Button, IBox } from '@stoplight/ui-kit';
1+
import { TreeList, TreeListMouseEventHandler, TreeStore } from '@stoplight/tree-list';
2+
import { Omit } from '@stoplight/types';
3+
import { Box, IBox, ThemeZone } from '@stoplight/ui-kit';
34
import { JSONSchema4 } from 'json-schema';
4-
import _get = require('lodash/get');
5+
import _isEmpty = require('lodash/isEmpty');
56
import * as React from 'react';
6-
import { MutedText } from './components/common/MutedText';
77
import { MaskedSchema } from './components/MaskedSchema';
88
import { IProperty, Property } from './components/Property';
99
import { TopBar } from './components/TopBar';
1010
import { useMetadata } from './hooks/useMetadata';
11-
import { useProperties } from './hooks/useProperties';
11+
import { IJsonSchemaViewer } from './JsonSchemaViewer';
1212
import { useTheme } from './theme';
13-
import { IMasking } from './types';
14-
import { isExpanded } from './utils/isExpanded';
15-
import { pathToString } from './utils/pathToString';
13+
import { IMasking, SchemaNodeWithMeta } from './types';
14+
import { lookupRef } from './utils/lookupRef';
15+
16+
const canDrag = () => false;
1617

1718
export interface ISchemaView extends Omit<IBox, 'onSelect'>, IMasking {
1819
name?: string;
19-
defaultExpandedDepth?: number;
2020
dereferencedSchema?: JSONSchema4;
2121
schema: JSONSchema4;
22-
limitPropertyCount?: number;
2322
expanded?: boolean;
24-
emptyText: string;
23+
hideTopBar?: boolean;
24+
treeStore: TreeStore;
2525
}
2626

2727
export const SchemaView: React.FunctionComponent<ISchemaView> = props => {
2828
const {
29-
defaultExpandedDepth = 1,
3029
emptyText,
3130
expanded = false,
32-
limitPropertyCount,
3331
schema,
3432
dereferencedSchema,
33+
hideTopBar,
3534
selected,
3635
canSelect,
3736
onSelect,
3837
name,
38+
treeStore,
3939
...rest
4040
} = props;
4141

42-
const theme = useTheme();
43-
const [showExtra, setShowExtra] = React.useState<boolean>(false);
42+
const theme = useTheme() as IJsonSchemaViewer;
4443
const [maskedSchema, setMaskedSchema] = React.useState<JSONSchema4 | null>(null);
45-
const [expandedRows, setExpandedRows] = React.useState<Dictionary<boolean>>({ all: expanded });
46-
const { properties, isOverflow } = useProperties(schema, dereferencedSchema, {
47-
expandedRows,
48-
defaultExpandedDepth,
49-
...(!showExtra && { limitPropertyCount }),
50-
...(canSelect && selected && { selected }),
51-
});
44+
5245
const metadata = useMetadata(schema);
5346

54-
const toggleExpandRow = React.useCallback<IProperty['onClick']>(
47+
const handleMaskEdit = React.useCallback<IProperty['onMaskEdit']>(
5548
node => {
56-
if (node.path.length > 0) {
57-
setExpandedRows({
58-
...expandedRows,
59-
[pathToString(node)]: !isExpanded(node, defaultExpandedDepth, expandedRows),
60-
});
61-
}
49+
setMaskedSchema(lookupRef(node.path, dereferencedSchema));
6250
},
63-
[expandedRows, defaultExpandedDepth]
51+
[dereferencedSchema]
6452
);
6553

66-
const toggleShowExtra = React.useCallback<React.MouseEventHandler<HTMLElement>>(
67-
() => {
68-
setShowExtra(!showExtra);
54+
const handleNodeClick = React.useCallback<TreeListMouseEventHandler>(
55+
(e, node) => {
56+
treeStore.toggleExpand(node);
6957
},
70-
[showExtra]
58+
[treeStore]
7159
);
7260

73-
const handleMaskEdit = React.useCallback<IProperty['onMaskEdit']>(node => {
74-
setMaskedSchema(_get(dereferencedSchema, node.path));
75-
}, []);
76-
7761
const handleMaskedSchemaClose = React.useCallback(() => {
7862
setMaskedSchema(null);
7963
}, []);
8064

81-
if (properties.length === 0) {
82-
return <MutedText>{emptyText}</MutedText>;
83-
}
65+
const shouldRenderTopBar = !hideTopBar && (name || !_isEmpty(metadata));
66+
67+
const itemData = {
68+
onSelect,
69+
onMaskEdit: handleMaskEdit,
70+
selected,
71+
canSelect,
72+
};
8473

8574
return (
8675
<Box backgroundColor={theme.canvas.bg} color={theme.canvas.fg} {...rest}>
8776
{maskedSchema && (
8877
<MaskedSchema onClose={handleMaskedSchemaClose} onSelect={onSelect} selected={selected} schema={maskedSchema} />
8978
)}
90-
<TopBar name={name} metadata={metadata} />
91-
{properties.map((node, i) => (
92-
<Property
93-
key={i}
94-
node={node}
95-
onClick={toggleExpandRow}
96-
onSelect={onSelect}
97-
onMaskEdit={handleMaskEdit}
98-
selected={selected}
99-
canSelect={canSelect}
79+
{shouldRenderTopBar && <TopBar name={name} metadata={metadata} />}
80+
<ThemeZone name="tree-list">
81+
<TreeList
82+
top={shouldRenderTopBar ? '40px' : 0}
83+
rowHeight={40}
84+
canDrag={canDrag}
85+
store={treeStore}
86+
onNodeClick={handleNodeClick}
87+
rowRenderer={node => <Property node={node.metadata as SchemaNodeWithMeta} {...itemData} />}
10088
/>
101-
))}
102-
{showExtra || isOverflow ? (
103-
<Button onClick={toggleShowExtra}>{showExtra ? 'collapse' : `...show more properties`}</Button>
104-
) : null}
89+
</ThemeZone>
10590
</Box>
10691
);
10792
};

0 commit comments

Comments
 (0)