Skip to content

Commit ddcbfd3

Browse files
casserniP0lip
authored andcommitted
feat: detail dialog
1 parent 00f2a61 commit ddcbfd3

File tree

7 files changed

+603
-6
lines changed

7 files changed

+603
-6
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@
4444
"@emotion/core": "^10.0.10",
4545
"@fortawesome/free-solid-svg-icons": "5.6.x",
4646
"@stoplight/json": "1.9.x",
47+
"@stoplight/markdown-viewer": "^3.0.0",
4748
"@stoplight/tree-list": "^4.0.0",
4849
"@types/json-schema": "^7.0.3",
4950
"classnames": "^2.2.6",
5051
"lodash": "4.17.x",
5152
"mobx": "^5.9.4",
53+
"mobx-react-lite": "^1.3.1",
5254
"pluralize": "^7.0.0",
5355
"json-schema-merge-allof": "^0.6.0"
5456
},

src/components/DetailDialog.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { MarkdownViewer } from '@stoplight/markdown-viewer';
2+
import { ITreeListNode, TreeStore } from '@stoplight/tree-list';
3+
import { Dialog } from '@stoplight/ui-kit';
4+
import * as cn from 'classnames';
5+
import _get = require('lodash/get');
6+
import _isEmpty = require('lodash/isEmpty');
7+
import * as React from 'react';
8+
9+
import { SchemaNodeWithMeta } from '../types';
10+
import { isCombiner, isRef } from '../utils';
11+
import { Types } from './';
12+
13+
export interface IDetailDialog extends React.HTMLAttributes<HTMLDivElement> {
14+
node: ITreeListNode<SchemaNodeWithMeta>;
15+
treeStore: TreeStore;
16+
}
17+
18+
export const DetailDialog: React.FunctionComponent<IDetailDialog> = ({ node, treeStore }) => {
19+
if (!node) return null;
20+
21+
const meta = node.metadata as SchemaNodeWithMeta;
22+
const { name, subtype, $ref, required } = meta;
23+
24+
const type = isRef(meta) ? '$ref' : isCombiner(meta) ? meta.combiner : meta.type;
25+
const description = _get(meta, 'annotations.description', 'No further description.');
26+
27+
const validations = 'validations' in meta && meta.validations ? meta.validations : [];
28+
const validationElems = [];
29+
for (const key in validations) {
30+
validationElems.push(
31+
<div className="flex py-1">
32+
<div className="flex-1">{key}:</div>
33+
<div className="pl-10">{validations[key] as any}</div>
34+
</div>
35+
);
36+
}
37+
38+
return (
39+
<Dialog
40+
isOpen
41+
onClose={() => treeStore.setActiveNode()}
42+
title={
43+
<div className="py-3">
44+
<div className="flex items-center text-base">
45+
{name && <span className="mr-3 te">{name}</span>}
46+
47+
<Types type={type} subtype={subtype}>
48+
{type === '$ref' ? `[${$ref}]` : null}
49+
</Types>
50+
</div>
51+
52+
<div className={cn('text-xs font-semibold', required ? 'text-red-6' : 'text-darken-7')}>
53+
{required ? 'REQUIRED' : 'OPTIONAL'}
54+
</div>
55+
</div>
56+
}
57+
>
58+
<div className="px-6 text-sm flex">
59+
{description && (
60+
<div className="flex-1">
61+
<MarkdownViewer className="mt-6" markdown={description} />
62+
</div>
63+
)}
64+
65+
{!_isEmpty(validationElems) && <div className="mt-4 pl-4 border-l py-2">{validationElems}</div>}
66+
</div>
67+
</Dialog>
68+
);
69+
};

src/components/SchemaRow.tsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { ITreeListNode, TreeStore } from '@stoplight/tree-list';
2+
import { Omit } from '@stoplight/types';
3+
import { Button, Checkbox, Icon } from '@stoplight/ui-kit';
4+
import * as cn from 'classnames';
5+
import * as pluralize from 'pluralize';
6+
import * as React from 'react';
7+
8+
import { IMasking, SchemaNodeWithMeta } from '../types';
9+
import { formatRef, isCombiner, isRef, pathToString } from '../utils';
10+
import { Divider, Types } from './';
11+
12+
export interface ISchemaRow extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick' | 'onSelect'>, IMasking {
13+
node: ITreeListNode<object>;
14+
onMaskEdit(node: SchemaNodeWithMeta): void;
15+
treeStore: TreeStore;
16+
}
17+
18+
export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({
19+
node,
20+
treeStore,
21+
canSelect,
22+
onSelect,
23+
onMaskEdit,
24+
selected,
25+
}) => {
26+
const schemaNode = node.metadata as SchemaNodeWithMeta;
27+
const { showDivider, name, $ref, subtype, required, path, inheritedFrom } = schemaNode;
28+
29+
const handleChange = React.useCallback(
30+
() => {
31+
if (onSelect !== undefined) {
32+
onSelect(pathToString(path));
33+
}
34+
},
35+
[onSelect]
36+
);
37+
38+
const handleEditMask = React.useCallback<React.MouseEventHandler<HTMLButtonElement>>(
39+
e => {
40+
e.stopPropagation();
41+
onMaskEdit(schemaNode);
42+
},
43+
[onMaskEdit]
44+
);
45+
46+
const type = isRef(schemaNode) ? '$ref' : isCombiner(schemaNode) ? schemaNode.combiner : schemaNode.type;
47+
const description = 'annotations' in schemaNode && schemaNode.annotations.description;
48+
49+
const validationCount = 'validations' in schemaNode ? Object.keys(schemaNode.validations).length : 0;
50+
51+
return (
52+
<div className="flex flex-1 items-center text-sm leading-tight relative select-none mr-3">
53+
{showDivider && <Divider>or</Divider>}
54+
55+
<div className="flex-1 truncate">
56+
<div className="flex items-baseline">
57+
{name && <span className="mr-3">{name}</span>}
58+
59+
<Types type={type} subtype={subtype}>
60+
{type === '$ref' ? `[${$ref}]` : null}
61+
</Types>
62+
63+
{inheritedFrom ? (
64+
<>
65+
<span className="text-darken-7 mx-2">{`{${formatRef(inheritedFrom)}}`}</span>
66+
{onMaskEdit !== undefined && <span onClick={handleEditMask}>(edit mask)</span>}
67+
</>
68+
) : null}
69+
</div>
70+
71+
{description && <span className="text-darken-7 text-xs">{description}</span>}
72+
</div>
73+
74+
{(canSelect || validationCount || required) && (
75+
<div className="items-center text-right ml-auto text-xs">
76+
{canSelect ? (
77+
<Checkbox onChange={handleChange} checked={selected && selected.includes(pathToString(path))} />
78+
) : (
79+
<>
80+
{validationCount ? (
81+
<span className="mr-2 text-darken-7">
82+
{validationCount} {pluralize('validation', validationCount)}
83+
</span>
84+
) : null}
85+
86+
{required && <span className="font-semibold">required</span>}
87+
</>
88+
)}
89+
</div>
90+
)}
91+
92+
{(validationCount || description) &&
93+
node.canHaveChildren && (
94+
<Button
95+
small
96+
className={cn(required && 'ml-2')}
97+
id={`${node.id}-showMore`}
98+
icon={<Icon icon="info-sign" className="opacity-75" iconSize={12} />}
99+
onClick={(e: React.MouseEvent) => {
100+
e.stopPropagation();
101+
treeStore.setActiveNode(node.id);
102+
}}
103+
/>
104+
)}
105+
</div>
106+
);
107+
};

src/components/SchemaTree.tsx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { ITreeListNode, TreeList, TreeListEvents, TreeStore } from '@stoplight/tree-list';
2+
import { Omit } from '@stoplight/types';
3+
4+
import * as cn from 'classnames';
5+
import { JSONSchema4 } from 'json-schema';
6+
import { observer } from 'mobx-react-lite';
7+
import * as React from 'react';
8+
9+
import _isEmpty = require('lodash/isEmpty');
10+
11+
import { useMetadata } from '../hooks';
12+
import { IMasking, SchemaNodeWithMeta } from '../types';
13+
import { lookupRef } from '../utils';
14+
import { DetailDialog, ISchemaRow, MaskedSchema, SchemaRow, TopBar } from './';
15+
16+
const canDrag = () => false;
17+
18+
export interface ISchemaTree extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'>, IMasking {
19+
name?: string;
20+
dereferencedSchema?: JSONSchema4;
21+
schema: JSONSchema4;
22+
expanded?: boolean;
23+
hideTopBar?: boolean;
24+
treeStore: TreeStore;
25+
}
26+
27+
// @ts-ignore
28+
export const SchemaTree: React.NamedExoticComponent<ISchemaTree> = observer((props: ISchemaTree) => {
29+
const {
30+
expanded = false,
31+
schema,
32+
dereferencedSchema,
33+
hideTopBar,
34+
selected,
35+
canSelect,
36+
onSelect,
37+
name,
38+
treeStore,
39+
className,
40+
...rest
41+
} = props;
42+
43+
const [maskedSchema, setMaskedSchema] = React.useState<JSONSchema4 | null>(null);
44+
45+
const metadata = useMetadata(schema);
46+
const activeNode = treeStore.nodes.find(node => node.id === treeStore.activeNodeId);
47+
48+
const handleMaskEdit = React.useCallback<ISchemaRow['onMaskEdit']>(
49+
node => {
50+
setMaskedSchema(lookupRef(node.path, dereferencedSchema));
51+
},
52+
[dereferencedSchema]
53+
);
54+
55+
treeStore.on(TreeListEvents.NodeClick, (e, node) => {
56+
if (node.canHaveChildren) {
57+
treeStore.toggleExpand(node);
58+
} else {
59+
treeStore.setActiveNode(node.id);
60+
}
61+
});
62+
63+
const handleMaskedSchemaClose = React.useCallback(() => {
64+
setMaskedSchema(null);
65+
}, []);
66+
67+
const shouldRenderTopBar = !hideTopBar && (name || !_isEmpty(metadata));
68+
69+
const itemData = {
70+
onSelect,
71+
onMaskEdit: handleMaskEdit,
72+
selected,
73+
canSelect,
74+
treeStore,
75+
};
76+
77+
return (
78+
<div className={cn(className, 'h-full w-full')} {...rest}>
79+
{maskedSchema && (
80+
<MaskedSchema onClose={handleMaskedSchemaClose} onSelect={onSelect} selected={selected} schema={maskedSchema} />
81+
)}
82+
{shouldRenderTopBar && <TopBar name={name} metadata={metadata} />}
83+
<DetailDialog node={activeNode as ITreeListNode<SchemaNodeWithMeta>} treeStore={treeStore} />
84+
<TreeList
85+
rowHeight={40}
86+
canDrag={canDrag}
87+
striped
88+
store={treeStore}
89+
rowRenderer={node => <SchemaRow node={node} {...itemData} />}
90+
/>
91+
</div>
92+
);
93+
});

src/components/Types.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const Types: React.FunctionComponent<ITypes> = ({ type, subtype }) => {
1717
}
1818

1919
return (
20-
<div>
20+
<div className="truncate">
2121
{type.map((name, i, { length }) => (
2222
<>
2323
<Type type={name} subtype={subtype} />

src/components/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './SchemaTree';
66
export * from './TopBar';
77
export * from './Type';
88
export * from './Types';
9+
export * from './DetailDialog';

0 commit comments

Comments
 (0)