Skip to content

Commit

Permalink
feat: detail dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
casserni authored and P0lip committed May 17, 2019
1 parent 00f2a61 commit ddcbfd3
Show file tree
Hide file tree
Showing 7 changed files with 603 additions and 6 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@
"@emotion/core": "^10.0.10",
"@fortawesome/free-solid-svg-icons": "5.6.x",
"@stoplight/json": "1.9.x",
"@stoplight/markdown-viewer": "^3.0.0",
"@stoplight/tree-list": "^4.0.0",
"@types/json-schema": "^7.0.3",
"classnames": "^2.2.6",
"lodash": "4.17.x",
"mobx": "^5.9.4",
"mobx-react-lite": "^1.3.1",
"pluralize": "^7.0.0",
"json-schema-merge-allof": "^0.6.0"
},
Expand Down
69 changes: 69 additions & 0 deletions src/components/DetailDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { MarkdownViewer } from '@stoplight/markdown-viewer';
import { ITreeListNode, TreeStore } from '@stoplight/tree-list';
import { Dialog } from '@stoplight/ui-kit';
import * as cn from 'classnames';
import _get = require('lodash/get');
import _isEmpty = require('lodash/isEmpty');
import * as React from 'react';

import { SchemaNodeWithMeta } from '../types';
import { isCombiner, isRef } from '../utils';
import { Types } from './';

export interface IDetailDialog extends React.HTMLAttributes<HTMLDivElement> {
node: ITreeListNode<SchemaNodeWithMeta>;
treeStore: TreeStore;
}

export const DetailDialog: React.FunctionComponent<IDetailDialog> = ({ node, treeStore }) => {
if (!node) return null;

const meta = node.metadata as SchemaNodeWithMeta;
const { name, subtype, $ref, required } = meta;

const type = isRef(meta) ? '$ref' : isCombiner(meta) ? meta.combiner : meta.type;
const description = _get(meta, 'annotations.description', 'No further description.');

const validations = 'validations' in meta && meta.validations ? meta.validations : [];
const validationElems = [];
for (const key in validations) {
validationElems.push(
<div className="flex py-1">
<div className="flex-1">{key}:</div>
<div className="pl-10">{validations[key] as any}</div>
</div>
);
}

return (
<Dialog
isOpen
onClose={() => treeStore.setActiveNode()}
title={
<div className="py-3">
<div className="flex items-center text-base">
{name && <span className="mr-3 te">{name}</span>}

<Types type={type} subtype={subtype}>
{type === '$ref' ? `[${$ref}]` : null}
</Types>
</div>

<div className={cn('text-xs font-semibold', required ? 'text-red-6' : 'text-darken-7')}>
{required ? 'REQUIRED' : 'OPTIONAL'}
</div>
</div>
}
>
<div className="px-6 text-sm flex">
{description && (
<div className="flex-1">
<MarkdownViewer className="mt-6" markdown={description} />
</div>
)}

{!_isEmpty(validationElems) && <div className="mt-4 pl-4 border-l py-2">{validationElems}</div>}
</div>
</Dialog>
);
};
107 changes: 107 additions & 0 deletions src/components/SchemaRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { ITreeListNode, TreeStore } from '@stoplight/tree-list';
import { Omit } from '@stoplight/types';
import { Button, Checkbox, Icon } from '@stoplight/ui-kit';
import * as cn from 'classnames';
import * as pluralize from 'pluralize';
import * as React from 'react';

import { IMasking, SchemaNodeWithMeta } from '../types';
import { formatRef, isCombiner, isRef, pathToString } from '../utils';
import { Divider, Types } from './';

export interface ISchemaRow extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick' | 'onSelect'>, IMasking {
node: ITreeListNode<object>;
onMaskEdit(node: SchemaNodeWithMeta): void;
treeStore: TreeStore;
}

export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({
node,
treeStore,
canSelect,
onSelect,
onMaskEdit,
selected,
}) => {
const schemaNode = node.metadata as SchemaNodeWithMeta;
const { showDivider, name, $ref, subtype, required, path, inheritedFrom } = schemaNode;

const handleChange = React.useCallback(
() => {
if (onSelect !== undefined) {
onSelect(pathToString(path));
}
},
[onSelect]
);

const handleEditMask = React.useCallback<React.MouseEventHandler<HTMLButtonElement>>(
e => {
e.stopPropagation();
onMaskEdit(schemaNode);
},
[onMaskEdit]
);

const type = isRef(schemaNode) ? '$ref' : isCombiner(schemaNode) ? schemaNode.combiner : schemaNode.type;
const description = 'annotations' in schemaNode && schemaNode.annotations.description;

const validationCount = 'validations' in schemaNode ? Object.keys(schemaNode.validations).length : 0;

return (
<div className="flex flex-1 items-center text-sm leading-tight relative select-none mr-3">
{showDivider && <Divider>or</Divider>}

<div className="flex-1 truncate">
<div className="flex items-baseline">
{name && <span className="mr-3">{name}</span>}

<Types type={type} subtype={subtype}>
{type === '$ref' ? `[${$ref}]` : null}
</Types>

{inheritedFrom ? (
<>
<span className="text-darken-7 mx-2">{`{${formatRef(inheritedFrom)}}`}</span>
{onMaskEdit !== undefined && <span onClick={handleEditMask}>(edit mask)</span>}
</>
) : null}
</div>

{description && <span className="text-darken-7 text-xs">{description}</span>}
</div>

{(canSelect || validationCount || required) && (
<div className="items-center text-right ml-auto text-xs">
{canSelect ? (
<Checkbox onChange={handleChange} checked={selected && selected.includes(pathToString(path))} />
) : (
<>
{validationCount ? (
<span className="mr-2 text-darken-7">
{validationCount} {pluralize('validation', validationCount)}
</span>
) : null}

{required && <span className="font-semibold">required</span>}
</>
)}
</div>
)}

{(validationCount || description) &&
node.canHaveChildren && (
<Button
small
className={cn(required && 'ml-2')}
id={`${node.id}-showMore`}
icon={<Icon icon="info-sign" className="opacity-75" iconSize={12} />}
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
treeStore.setActiveNode(node.id);
}}
/>
)}
</div>
);
};
93 changes: 93 additions & 0 deletions src/components/SchemaTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { ITreeListNode, TreeList, TreeListEvents, TreeStore } from '@stoplight/tree-list';
import { Omit } from '@stoplight/types';

import * as cn from 'classnames';
import { JSONSchema4 } from 'json-schema';
import { observer } from 'mobx-react-lite';
import * as React from 'react';

import _isEmpty = require('lodash/isEmpty');

import { useMetadata } from '../hooks';
import { IMasking, SchemaNodeWithMeta } from '../types';
import { lookupRef } from '../utils';
import { DetailDialog, ISchemaRow, MaskedSchema, SchemaRow, TopBar } from './';

const canDrag = () => false;

export interface ISchemaTree extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'>, IMasking {
name?: string;
dereferencedSchema?: JSONSchema4;
schema: JSONSchema4;
expanded?: boolean;
hideTopBar?: boolean;
treeStore: TreeStore;
}

// @ts-ignore
export const SchemaTree: React.NamedExoticComponent<ISchemaTree> = observer((props: ISchemaTree) => {
const {
expanded = false,
schema,
dereferencedSchema,
hideTopBar,
selected,
canSelect,
onSelect,
name,
treeStore,
className,
...rest
} = props;

const [maskedSchema, setMaskedSchema] = React.useState<JSONSchema4 | null>(null);

const metadata = useMetadata(schema);
const activeNode = treeStore.nodes.find(node => node.id === treeStore.activeNodeId);

const handleMaskEdit = React.useCallback<ISchemaRow['onMaskEdit']>(
node => {
setMaskedSchema(lookupRef(node.path, dereferencedSchema));
},
[dereferencedSchema]
);

treeStore.on(TreeListEvents.NodeClick, (e, node) => {
if (node.canHaveChildren) {
treeStore.toggleExpand(node);
} else {
treeStore.setActiveNode(node.id);
}
});

const handleMaskedSchemaClose = React.useCallback(() => {
setMaskedSchema(null);
}, []);

const shouldRenderTopBar = !hideTopBar && (name || !_isEmpty(metadata));

const itemData = {
onSelect,
onMaskEdit: handleMaskEdit,
selected,
canSelect,
treeStore,
};

return (
<div className={cn(className, 'h-full w-full')} {...rest}>
{maskedSchema && (
<MaskedSchema onClose={handleMaskedSchemaClose} onSelect={onSelect} selected={selected} schema={maskedSchema} />
)}
{shouldRenderTopBar && <TopBar name={name} metadata={metadata} />}
<DetailDialog node={activeNode as ITreeListNode<SchemaNodeWithMeta>} treeStore={treeStore} />
<TreeList
rowHeight={40}
canDrag={canDrag}
striped
store={treeStore}
rowRenderer={node => <SchemaRow node={node} {...itemData} />}
/>
</div>
);
});
2 changes: 1 addition & 1 deletion src/components/Types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const Types: React.FunctionComponent<ITypes> = ({ type, subtype }) => {
}

return (
<div>
<div className="truncate">
{type.map((name, i, { length }) => (
<>
<Type type={name} subtype={subtype} />
Expand Down
1 change: 1 addition & 0 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './SchemaTree';
export * from './TopBar';
export * from './Type';
export * from './Types';
export * from './DetailDialog';
Loading

0 comments on commit ddcbfd3

Please sign in to comment.