-
Notifications
You must be signed in to change notification settings - Fork 45
feat: introduce rowRenderer #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0e58299
cd35650
73bfc48
4af053b
3f700a1
bf28a83
3f92763
f38117c
6fe32c9
04b42e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,192 +1,67 @@ | ||
| import { MarkdownViewer } from '@stoplight/markdown-viewer'; | ||
| import { IRowRendererOptions } from '@stoplight/tree-list'; | ||
| import { Icon, Popover } from '@stoplight/ui-kit'; | ||
| import * as cn from 'classnames'; | ||
| import cn from 'classnames'; | ||
| import * as React from 'react'; | ||
| import { Divider } from './shared/Divider'; | ||
|
|
||
| import get = require('lodash/get'); | ||
| import map = require('lodash/map'); | ||
| import size = require('lodash/size'); | ||
|
|
||
| import { GoToRefHandler, IExtendableRenderers, SchemaNodeWithMeta, SchemaTreeListNode } from '../types'; | ||
| import { isCombiner, isRef } from '../utils'; | ||
| import { Types } from './'; | ||
| import { GoToRefHandler, SchemaNodeWithMeta, SchemaTreeListNode } from '../types'; | ||
| import { Caret } from './shared/Caret'; | ||
| import { Description } from './shared/Description'; | ||
| import { Property } from './shared/Property'; | ||
| import { Validations } from './shared/Validations'; | ||
|
|
||
| export interface ISchemaRow extends IExtendableRenderers { | ||
| export interface ISchemaRow { | ||
| className?: string; | ||
| node: SchemaTreeListNode; | ||
| rowOptions: IRowRendererOptions; | ||
| onGoToRef?: GoToRefHandler; | ||
| toggleExpand: () => void; | ||
| } | ||
|
|
||
| const ICON_SIZE = 12; | ||
| const ICON_DIMENSION = 20; | ||
| const ROW_OFFSET = 7; | ||
|
|
||
| export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({ | ||
| node, | ||
| rowOptions, | ||
| onGoToRef, | ||
| rowRendererRight, | ||
| toggleExpand, | ||
| }) => { | ||
| export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({ className, node, rowOptions, onGoToRef }) => { | ||
| const schemaNode = node.metadata as SchemaNodeWithMeta; | ||
| const { name, $ref, subtype, required } = schemaNode; | ||
|
|
||
| const type = isRef(schemaNode) ? '$ref' : isCombiner(schemaNode) ? schemaNode.combiner : schemaNode.type; | ||
| const description = get(schemaNode, 'annotations.description'); | ||
| const childrenCount = | ||
| type === 'object' | ||
| ? size(get(schemaNode, 'properties')) | ||
| : subtype === 'object' | ||
| ? size(get(schemaNode, 'items.properties')) | ||
| : size(get(schemaNode, 'items')); | ||
|
|
||
| const nodeValidations = { | ||
| ...('annotations' in schemaNode && schemaNode.annotations.default | ||
| ? { default: schemaNode.annotations.default } | ||
| : {}), | ||
| ...get(schemaNode, 'validations', {}), | ||
| }; | ||
| const validationCount = Object.keys(nodeValidations).length; | ||
| const handleGoToRef = React.useCallback<React.MouseEventHandler>( | ||
| () => { | ||
| if (onGoToRef) { | ||
| onGoToRef($ref!, node); | ||
| } | ||
| }, | ||
| [onGoToRef, node, $ref], | ||
| ); | ||
|
|
||
| const requiredElem = ( | ||
| <div className={cn('ml-2', required ? 'font-medium' : 'text-darken-7 dark:text-lighten-6')}> | ||
| {required ? 'required' : 'optional'} | ||
| {validationCount ? `+${validationCount}` : ''} | ||
| </div> | ||
| ); | ||
|
|
||
| const combinerOffset = ICON_DIMENSION * node.level; | ||
| return ( | ||
| <div onClick={toggleExpand} className="px-6 flex-1 w-full"> | ||
| {/* 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. */} | ||
| <div className={cn('px-2 flex-1 w-full', className)}> | ||
| <div | ||
| className="flex items-center text-sm" | ||
| className="flex items-center text-sm relative" | ||
| style={{ | ||
| marginLeft: combinerOffset, | ||
| marginLeft: ICON_DIMENSION * node.level, // offset for spacing | ||
P0lip marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }} | ||
| > | ||
| {node.canHaveChildren && | ||
| node.level > 0 && ( | ||
| <div | ||
| className="absolute flex justify-center cursor-pointer p-1 rounded hover:bg-darken-3" | ||
| <Caret | ||
| isExpanded={!!rowOptions.isExpanded} | ||
| style={{ | ||
| left: combinerOffset, | ||
| left: ICON_DIMENSION * -1 + ROW_OFFSET / -2, | ||
| width: ICON_DIMENSION, | ||
| height: ICON_DIMENSION, | ||
| }} | ||
| > | ||
| <Icon | ||
| iconSize={ICON_SIZE} | ||
| icon={rowOptions.isExpanded ? 'caret-down' : 'caret-right'} | ||
| className="text-darken-9 dark:text-lighten-9" | ||
| /> | ||
| </div> | ||
| size={ICON_SIZE} | ||
| /> | ||
| )} | ||
|
|
||
| {schemaNode.divider && ( | ||
| <div | ||
| className="flex items-center absolute" | ||
| style={{ | ||
| top: 0, | ||
| height: 1, | ||
| width: `calc(100% - ${combinerOffset}px - 1.5rem)`, | ||
| }} | ||
| > | ||
| <div className="text-darken-7 dark:text-lighten-8 uppercase text-xs pr-2 -ml-4">{schemaNode.divider}</div> | ||
| <div className="flex-1 bg-darken-5 dark:bg-lighten-5" style={{ height: 1 }} /> | ||
| </div> | ||
| )} | ||
| {schemaNode.divider && <Divider>{schemaNode.divider}</Divider>} | ||
P0lip marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| <div className="flex-1 flex truncate"> | ||
| {name && <div className="mr-2">{name}</div>} | ||
|
|
||
| <Types type={type} subtype={subtype}> | ||
| {type === '$ref' ? `[${$ref}]` : null} | ||
| </Types> | ||
|
|
||
| {type === '$ref' && onGoToRef ? ( | ||
| <a role="button" className="text-blue-4 ml-2" onClick={handleGoToRef}> | ||
| (go to ref) | ||
| </a> | ||
| ) : null} | ||
|
|
||
| {node.canHaveChildren && <div className="ml-2 text-darken-7 dark:text-lighten-7">{`{${childrenCount}}`}</div>} | ||
|
|
||
| {'pattern' in schemaNode && schemaNode.pattern ? ( | ||
| <div className="ml-2 text-darken-7 dark:text-lighten-7 truncate">(pattern property)</div> | ||
| ) : null} | ||
|
|
||
| {description && ( | ||
| <Popover | ||
| boundary="window" | ||
| interactionKind="hover" | ||
| className="ml-2 flex-1 truncate flex items-baseline" | ||
| target={<div className="text-darken-7 dark:text-lighten-7 w-full truncate">{description}</div>} | ||
| targetClassName="text-darken-7 dark:text-lighten-6 w-full truncate" | ||
| content={ | ||
| <div className="p-5" style={{ maxHeight: 500, maxWidth: 400 }}> | ||
| <MarkdownViewer markdown={description} /> | ||
| </div> | ||
| } | ||
| /> | ||
| )} | ||
| <Property node={schemaNode} onGoToRef={onGoToRef} /> | ||
| {description && <Description value={description} />} | ||
| </div> | ||
|
|
||
| {validationCount ? ( | ||
| <Popover | ||
| boundary="window" | ||
| interactionKind="hover" | ||
| content={ | ||
| <div className="p-5" style={{ maxHeight: 500, maxWidth: 400 }}> | ||
| {map(Object.keys(nodeValidations), (key, index) => { | ||
| const validation = nodeValidations[key]; | ||
|
|
||
| let elem = null; | ||
| if (Array.isArray(validation)) { | ||
| elem = validation.map((v, i) => ( | ||
| <div key={i} className="mt-1 mr-1 flex items-center"> | ||
| <div className="px-1 bg-gray-2 dark:bg-gray-8 font-bold text-sm rounded">{String(v)}</div> | ||
| {i < validation.length - 1 ? <div>,</div> : null} | ||
| </div> | ||
| )); | ||
| } else if (typeof validation === 'object') { | ||
| elem = ( | ||
| <div className="m-1 px-1 bg-gray-2 dark:bg-gray-8 font-bold text-sm rounded" key={index}> | ||
| {'{...}'} | ||
| </div> | ||
| ); | ||
| } else { | ||
| elem = ( | ||
| <div className="m-1 px-1 bg-gray-2 dark:bg-gray-8 font-bold text-sm rounded" key={index}> | ||
| {JSON.stringify(validation)} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div key={index} className="py-1 flex items-baseline"> | ||
| <div className="font-medium pr-2 w-24">{key}:</div> | ||
| <div className="flex-1 flex flex-wrap text-center">{elem}</div> | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
| } | ||
| target={requiredElem} | ||
| /> | ||
| ) : ( | ||
| requiredElem | ||
| )} | ||
| {rowRendererRight && <div className="ml-2">{rowRendererRight(node)}</div>} | ||
| <Validations | ||
| required={!!schemaNode.required} | ||
| validations={{ | ||
| ...('annotations' in schemaNode && | ||
| schemaNode.annotations.default && { default: schemaNode.annotations.default }), | ||
| ...('validations' in schemaNode && schemaNode.validations), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @P0lip it might be just me but I find it a bit intimidating to read. Would you mind wrapping this with a small helper function? E.g.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good to me. |
||
| }} | ||
| /> | ||
| </div> | ||
| </div> | ||
| ); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.