Skip to content

Commit 2d0554e

Browse files
committed
feat: make use of error boundaries
1 parent 6db0cf9 commit 2d0554e

File tree

5 files changed

+200
-139
lines changed

5 files changed

+200
-139
lines changed

src/JsonSchemaViewer.tsx

Lines changed: 55 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,159 +1,79 @@
1-
import * as React from 'react';
2-
3-
import { safeParse } from '@stoplight/json';
4-
import { Dictionary, ISchema } from '@stoplight/types';
5-
import { Box } from '@stoplight/ui-kit';
6-
import dropRight = require('lodash/dropRight');
7-
import isEmpty = require('lodash/isEmpty');
1+
/* @jsx jsx */
82

3+
import { jsx } from '@emotion/core';
4+
import { Box, IBox } from '@stoplight/ui-kit';
5+
import { Component } from 'react';
6+
import { ErrorMessage } from './common/ErrorMessage';
97
import { MutedText } from './common/MutedText';
10-
import { Row } from './common/Row';
11-
import { dereferenceSchema } from './dereferenceSchema';
12-
import { renderSchema } from './renderers/renderSchema';
13-
import { IProp } from './types';
14-
import { buildAllOfSchema } from './util/buildAllOfSchema';
8+
import { ISchemaView, SchemaView } from './Schema';
159
import { isSchemaViewerEmpty } from './util/isSchemaViewerEmpty';
1610

17-
export interface IJsonSchemaViewer {
18-
name?: string;
19-
dereferencedSchema?: string | ISchema;
20-
defaultExpandedDepth?: number;
21-
schemas: object;
22-
schema: string | ISchema;
23-
limitPropertyCount: number;
24-
hideRoot?: boolean;
25-
expanded?: boolean;
26-
emptyText?: string;
27-
hideInheritedFrom?: boolean;
28-
}
11+
export interface IJsonSchemaViewer extends ISchemaView, IBox {}
2912

3013
export interface IJsonSchemaViewerState {
31-
showExtra: boolean;
32-
expandedRows: Dictionary<boolean>;
14+
error: null | string;
3315
}
3416

35-
export class JsonSchemaViewer extends React.Component<IJsonSchemaViewer, IJsonSchemaViewerState> {
17+
export class JsonSchemaViewer extends Component<IJsonSchemaViewer, IJsonSchemaViewerState> {
3618
public state = {
37-
showExtra: false,
38-
expandedRows: {
39-
all: false,
40-
},
19+
error: null,
4120
};
4221

22+
// there is no error hook yet, see https://reactjs.org/docs/hooks-faq.html#how-do-lifecycle-methods-correspond-to-hooks
23+
public static getDerivedStateFromError(error: Error): { error: IJsonSchemaViewerState['error'] } {
24+
return { error: error.message };
25+
}
26+
4327
public render() {
4428
const {
45-
name,
46-
schema,
47-
dereferencedSchema,
48-
schemas = {},
49-
limitPropertyCount,
50-
hideRoot,
51-
expanded = false,
52-
defaultExpandedDepth = 1,
53-
emptyText,
54-
hideInheritedFrom = false,
55-
} = this.props;
56-
57-
const emptyElem = <MutedText className="u-none">{emptyText || 'No schema defined.'}</MutedText>;
58-
59-
// an empty array or object is still a valid response, schema is ONLY really empty when a combiner type has no information
60-
if (isSchemaViewerEmpty(schema)) {
61-
return <div>{emptyElem}</div>;
62-
}
63-
64-
let parsed: IProp;
65-
if (typeof (dereferencedSchema || schema) === 'string') {
66-
parsed = safeParse((dereferencedSchema || schema) as string) as IProp;
67-
} else {
68-
parsed = (dereferencedSchema || schema) as IProp;
69-
}
70-
71-
try {
72-
if (!dereferencedSchema || isEmpty(dereferencedSchema)) {
73-
parsed = dereferenceSchema(parsed, { definitions: schemas }, hideInheritedFrom);
74-
}
75-
} catch (e) {
76-
console.error('JsonSchemaViewer dereference error', e);
77-
return (
78-
<Box as="p" p={3} className="u-error">
79-
There is an error in your {name} schema definition.
80-
</Box>
81-
);
82-
}
83-
84-
if (!parsed || !Object.keys(parsed).length || (parsed.properties && !Object.keys(parsed.properties).length)) {
85-
return emptyElem;
86-
}
87-
88-
let rowElems: any[];
89-
90-
const { expandedRows } = this.state;
91-
expandedRows.all = expanded;
92-
93-
try {
94-
// resolve root allOfs, simplifies things later
95-
if (parsed.allOf) {
96-
const elems = parsed.allOf;
97-
98-
if (parsed.type) elems.push({ type: parsed.type });
99-
100-
parsed = buildAllOfSchema({ elems });
101-
}
102-
103-
rowElems = renderSchema({
29+
props: {
30+
name,
31+
schema,
32+
dereferencedSchema,
10433
schemas,
105-
expandedRows,
34+
limitPropertyCount,
35+
hideRoot,
36+
expanded,
10637
defaultExpandedDepth,
107-
schema: parsed,
108-
level: hideRoot && (parsed.type === 'object' || parsed.hasOwnProperty('allOf')) ? -1 : 0,
109-
name: 'root',
110-
rowElems: [],
111-
toggleExpandRow: this.toggleExpandRow,
11238
hideInheritedFrom,
113-
jsonPath: 'root',
114-
hideRoot,
115-
});
116-
} catch (e) {
117-
console.error('JSV:error', e);
118-
rowElems = [<Row className="u-error">{`Error rendering schema. ${e}`}</Row>];
119-
}
120-
121-
const { showExtra } = this.state;
122-
const propOverflowCount = rowElems.length - limitPropertyCount;
123-
124-
if (limitPropertyCount) {
125-
if (!showExtra && propOverflowCount > 0) {
126-
rowElems = dropRight(rowElems, propOverflowCount);
127-
}
39+
...props
40+
},
41+
state: { error },
42+
} = this;
43+
44+
if (error) {
45+
// todo: handle these:
46+
/*
47+
<Box as="p" p={3} className="u-error">
48+
There is an error in your {name} schema definition.
49+
</Box>
50+
*/
51+
52+
/*<Row className="u-error">{`Error rendering schema. ${e}`}</Row>]*/
53+
return <ErrorMessage>{error}</ErrorMessage>;
12854
}
12955

130-
if (isEmpty(rowElems)) {
131-
return emptyElem;
56+
// an empty array or object is still a valid response, schema is ONLY really empty when a combiner type has no information
57+
if (isSchemaViewerEmpty(schema)) {
58+
return <MutedText>No schema defined</MutedText>;
13259
}
13360

13461
return (
135-
<div className="JSV us-t u-schemaColors">
136-
{rowElems}
137-
138-
{showExtra || propOverflowCount > 0 ? (
139-
<div onClick={this.toggleShowExtra}>
140-
{/* className={cn('JSV-toggleExtra', { 'is-on': showExtra })} */}
141-
{showExtra ? 'collapse' : `...show ${propOverflowCount} more properties`}
142-
</div>
143-
) : null}
144-
</div>
62+
<Box {...props}>
63+
{(
64+
<SchemaView
65+
defaultExpandedDepth={defaultExpandedDepth}
66+
dereferencedSchema={dereferencedSchema}
67+
expanded={expanded}
68+
hideInheritedFrom={hideInheritedFrom}
69+
hideRoot={hideRoot}
70+
limitPropertyCount={limitPropertyCount}
71+
name={name}
72+
schema={schema}
73+
schemas={schemas}
74+
/>
75+
) || <MutedText>No schema defined</MutedText>}
76+
</Box>
14577
);
14678
}
147-
148-
public toggleShowExtra = () => {
149-
const { showExtra } = this.state;
150-
this.setState({ showExtra: !showExtra });
151-
};
152-
153-
public toggleExpandRow = (rowKey: string, expanded: boolean) => {
154-
const { expandedRows } = this.state;
155-
const update = {};
156-
update[rowKey] = expanded;
157-
this.setState({ expandedRows: Object.assign({}, expandedRows, update) });
158-
};
15979
}

src/Schema.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/* @jsx jsx */
2+
3+
import { jsx } from '@emotion/core';
4+
import { safeParse } from '@stoplight/json';
5+
import dropRight = require('lodash/dropRight');
6+
import isEmpty = require('lodash/isEmpty');
7+
import { Fragment, FunctionComponent, MouseEventHandler, ReactNodeArray, useCallback, useState } from 'react';
8+
9+
import { Dictionary, ISchema } from '@stoplight/types';
10+
import { Button } from '@stoplight/ui-kit';
11+
import { dereferenceSchema } from './dereferenceSchema';
12+
import { renderSchema } from './renderers/renderSchema';
13+
import { IProp } from './types';
14+
import { buildAllOfSchema } from './util/buildAllOfSchema';
15+
16+
export interface ISchemaView {
17+
name?: string;
18+
dereferencedSchema?: string | ISchema;
19+
defaultExpandedDepth?: number;
20+
schemas: object;
21+
schema: string | ISchema;
22+
limitPropertyCount?: number;
23+
hideRoot?: boolean;
24+
expanded?: boolean;
25+
hideInheritedFrom?: boolean;
26+
}
27+
28+
export const SchemaView: FunctionComponent<ISchemaView> = props => {
29+
const {
30+
defaultExpandedDepth = 1,
31+
dereferencedSchema,
32+
expanded = false,
33+
hideInheritedFrom = false,
34+
hideRoot,
35+
limitPropertyCount = 0,
36+
schema,
37+
schemas = {},
38+
} = props;
39+
40+
const [showExtra, setShowExtra] = useState<boolean>(false);
41+
const [expandedRows, setExpandedRows] = useState<Dictionary<boolean>>({ all: expanded });
42+
43+
const toggleExpandRow = useCallback<(rowKey: string, expanded: boolean) => void>((rowKey, expandRow) => {
44+
setExpandedRows({ ...expandedRows, [rowKey]: expandRow });
45+
}, []);
46+
47+
const toggleShowExtra = useCallback<MouseEventHandler<HTMLElement>>(() => {
48+
setShowExtra(!showExtra);
49+
}, []);
50+
51+
let parsedSchema: IProp;
52+
if (typeof (dereferencedSchema || schema) === 'string') {
53+
parsedSchema = safeParse((dereferencedSchema || schema) as string) as IProp;
54+
} else {
55+
parsedSchema = (dereferencedSchema || schema) as IProp;
56+
}
57+
58+
if (!dereferencedSchema || isEmpty(dereferencedSchema)) {
59+
parsedSchema = dereferenceSchema(parsedSchema, { definitions: schemas }, hideInheritedFrom);
60+
}
61+
62+
if (
63+
!parsedSchema ||
64+
!Object.keys(parsedSchema).length ||
65+
(parsedSchema.properties && !Object.keys(parsedSchema.properties).length)
66+
) {
67+
return null;
68+
}
69+
70+
if (parsedSchema.allOf) {
71+
const elems = parsedSchema.allOf;
72+
73+
if (parsedSchema.type) elems.push({ type: parsedSchema.type });
74+
75+
parsedSchema = buildAllOfSchema({ elems });
76+
}
77+
78+
let rowElems: ReactNodeArray = [];
79+
80+
renderSchema({
81+
schemas,
82+
expandedRows,
83+
defaultExpandedDepth,
84+
schema: parsedSchema,
85+
level: hideRoot && (parsedSchema.type === 'object' || parsedSchema.hasOwnProperty('allOf')) ? -1 : 0,
86+
name: 'root',
87+
rowElems,
88+
toggleExpandRow,
89+
hideInheritedFrom,
90+
jsonPath: 'root',
91+
hideRoot,
92+
});
93+
94+
const propOverflowCount = rowElems.length - Math.max(0, limitPropertyCount);
95+
96+
if (limitPropertyCount && !showExtra && propOverflowCount > 0) {
97+
rowElems = dropRight(rowElems, propOverflowCount);
98+
}
99+
100+
if (rowElems.length === 0) {
101+
return null;
102+
}
103+
104+
return (
105+
<Fragment>
106+
{rowElems}
107+
{showExtra || propOverflowCount > 0 ? (
108+
<Button onClick={toggleShowExtra}>
109+
{showExtra ? 'collapse' : `...show ${propOverflowCount} more properties`}
110+
</Button>
111+
) : null}
112+
</Fragment>
113+
);
114+
};

src/__stories__/JsonSchemaViewer.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,10 @@ const schema = {
2525
storiesOf('JsonSchemaViewer', module).add('with text', () => (
2626
<JsonSchemaViewer
2727
name={text('name', 'name')}
28-
schemas={{}}
28+
schemas={object('schemas', {})}
2929
schema={object('schema', schema)}
3030
limitPropertyCount={number('limitPropertyCount', 20)}
3131
hideRoot={boolean('hideRoot', false)}
3232
expanded={boolean('expanded', true)}
33-
emptyText={text('emptyText', 'empty')}
3433
/>
3534
));

src/common/ErrorMessage.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* @jsx jsx */
2+
3+
import { jsx } from '@emotion/core';
4+
import { Box, IBox } from '@stoplight/ui-kit';
5+
import { FunctionComponent } from 'react';
6+
7+
export const ErrorMessage: FunctionComponent<IErrorMessage> = props => {
8+
const { children, ...rest } = props;
9+
const css = errorMessageStyles();
10+
11+
return (
12+
<Box as="p" p={3} css={css} {...rest}>
13+
{children}
14+
</Box>
15+
);
16+
};
17+
18+
export interface IErrorMessageProps {}
19+
20+
export interface IErrorMessage extends IErrorMessageProps, IBox {}
21+
22+
export const errorMessageStyles = () => {
23+
// const theme = useTheme();
24+
return {
25+
// canvas.error
26+
color: 'red',
27+
};
28+
};

src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Dictionary, ISchema, Omit } from '@stoplight/types';
22
import { ICustomTheme } from '@stoplight/ui-kit';
3-
import { ReactElement } from 'react';
3+
import { ReactNodeArray } from 'react';
44

55
export interface IProp extends ISchema {
66
allOf?: IProp[];
@@ -26,7 +26,7 @@ export interface ICommonProps {
2626
defaultExpandedDepth: number;
2727
prop?: IProp | IResolvedProp;
2828
parentName?: string;
29-
rowElems: Array<ReactElement<any>>;
29+
rowElems: ReactNodeArray;
3030
expandedRows: Dictionary<boolean>;
3131
jsonPath: string;
3232
propName?: string;

0 commit comments

Comments
 (0)