Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/__tests__/__snapshots__/index.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ exports[`HTML Output given anyOf combiner placed next to allOf given allOf mergi
<div>type</div>
<span>string</span>
</div>
<div></div>
<span>required</span>
</div>
<div>
Expand Down Expand Up @@ -425,6 +426,7 @@ exports[`HTML Output given oneOf combiner placed next to allOf given allOf mergi
<div>type</div>
<span>string</span>
</div>
<div></div>
<span>required</span>
</div>
<div>
Expand Down Expand Up @@ -548,6 +550,7 @@ exports[`HTML Output given standalone mode, should populate proper nodes 1`] = `
<div>id</div>
<span>string</span>
</div>
<div></div>
<span>read-only</span>
</div>
</div>
Expand All @@ -559,6 +562,7 @@ exports[`HTML Output given standalone mode, should populate proper nodes 1`] = `
<div>description</div>
<span>string</span>
</div>
<div></div>
<span>write-only</span>
</div>
</div>
Expand Down
9 changes: 5 additions & 4 deletions src/components/JsonSchemaViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import * as React from 'react';

import { JSVOptions, JSVOptionsContextProvider } from '../contexts';
import type { JSONSchema } from '../types';
import { PathCrumbs, pathCrumbsAtom } from './PathCrumbs';
import { PathCrumbs } from './PathCrumbs';
import { TopLevelSchemaRow } from './SchemaRow';
import { hoveredNodeAtom } from './SchemaRow/state';

export type JsonSchemaProps = Partial<JSVOptions> & {
schema: JSONSchema;
Expand Down Expand Up @@ -74,10 +75,10 @@ const JsonSchemaViewerInner = ({
JsonSchemaProps,
'schema' | 'viewMode' | 'className' | 'resolveRef' | 'emptyText' | 'onTreePopulated' | 'maxHeight' | 'parentCrumbs'
>) => {
const setPathCrumbs = useUpdateAtom(pathCrumbsAtom);
const setHoveredNode = useUpdateAtom(hoveredNodeAtom);
const onMouseLeave = React.useCallback(() => {
setPathCrumbs([]);
}, [setPathCrumbs]);
setHoveredNode(null);
}, [setHoveredNode]);

const { jsonSchemaTreeRoot, nodeCount } = React.useMemo(() => {
const jsonSchemaTree = new JsonSchemaTree(schema, {
Expand Down
39 changes: 2 additions & 37 deletions src/components/PathCrumbs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { isRegularNode, isRootNode, SchemaNode } from '@stoplight/json-schema-tree';
import { Box, HStack } from '@stoplight/mosaic';
import { atom, useAtom } from 'jotai';
import { useAtom } from 'jotai';
import * as React from 'react';

import { useJSVOptionsContext } from '../../contexts';

export const showPathCrumbsAtom = atom<boolean>(false);

export const pathCrumbsAtom = atom([], (_get, set, node) => {
set(pathCrumbsAtom, propertyPathToObjectPath(node as SchemaNode));
});
import { pathCrumbsAtom, showPathCrumbsAtom } from './state';

export const PathCrumbs = ({ parentCrumbs = [] }: { parentCrumbs?: string[] }) => {
const [showPathCrumbs] = useAtom(showPathCrumbsAtom);
Expand Down Expand Up @@ -67,32 +61,3 @@ export const PathCrumbs = ({ parentCrumbs = [] }: { parentCrumbs?: string[] }) =
</HStack>
);
};

function propertyPathToObjectPath(node: SchemaNode) {
const objectPath: string[] = [];

let currentNode: SchemaNode | null = node;
while (currentNode && !isRootNode(currentNode)) {
if (isRegularNode(currentNode)) {
const pathPart = currentNode.subpath[currentNode.subpath.length - 1];

if (currentNode.primaryType === 'array') {
const key = `${pathPart || ''}[]`;
if (objectPath[objectPath.length - 1]) {
objectPath[objectPath.length - 1] = key;
} else {
objectPath.push(key);
}
} else if (
pathPart &&
(currentNode.subpath.length !== 2 || !['allOf', 'oneOf', 'anyOf'].includes(currentNode.subpath[0]))
) {
objectPath.push(currentNode.subpath[currentNode.subpath.length - 1]);
}
}

currentNode = currentNode.parent;
}

return objectPath.reverse();
}
43 changes: 43 additions & 0 deletions src/components/PathCrumbs/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { isRegularNode, isRootNode, SchemaNode } from '@stoplight/json-schema-tree';
import { atom } from 'jotai';

import { hoveredNodeAtom } from '../SchemaRow/state';

export const showPathCrumbsAtom = atom<boolean>(false);

export const pathCrumbsAtom = atom(get => {
const node = get(hoveredNodeAtom);

if (!node) return [];

return propertyPathToObjectPath(node as SchemaNode);
});

function propertyPathToObjectPath(node: SchemaNode) {
const objectPath: string[] = [];

let currentNode: SchemaNode | null = node;
while (currentNode && !isRootNode(currentNode)) {
if (isRegularNode(currentNode)) {
const pathPart = currentNode.subpath[currentNode.subpath.length - 1];

if (currentNode.primaryType === 'array') {
const key = `${pathPart || ''}[]`;
if (objectPath[objectPath.length - 1]) {
objectPath[objectPath.length - 1] = key;
} else {
objectPath.push(key);
}
} else if (
pathPart &&
(currentNode.subpath.length !== 2 || !['allOf', 'oneOf', 'anyOf'].includes(currentNode.subpath[0]))
) {
objectPath.push(currentNode.subpath[currentNode.subpath.length - 1]);
}
}

currentNode = currentNode.parent;
}

return objectPath.reverse();
}
40 changes: 25 additions & 15 deletions src/components/SchemaRow/SchemaRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,32 @@ import {
SchemaNode,
SchemaNodeKind,
} from '@stoplight/json-schema-tree';
import { Box, Flex, Icon, Select, VStack } from '@stoplight/mosaic';
import { useUpdateAtom } from 'jotai/utils';
import { Box, Flex, Icon, Select, SpaceVals, VStack } from '@stoplight/mosaic';
import { useAtomValue, useUpdateAtom } from 'jotai/utils';
import last from 'lodash/last.js';
import * as React from 'react';

import { COMBINER_NAME_MAP } from '../../consts';
import { useJSVOptionsContext } from '../../contexts';
import { calculateChildrenToShow, isFlattenableNode, isPropertyRequired } from '../../tree';
import { pathCrumbsAtom } from '../PathCrumbs';
import { Caret, Description, Format, getValidationsFromSchema, Types, Validations } from '../shared';
import { ChildStack } from '../shared/ChildStack';
import { Properties } from '../shared/Properties';
import { Properties, useHasProperties } from '../shared/Properties';
import { hoveredNodeAtom, isNodeHoveredAtom } from './state';
import { useChoices } from './useChoices';

export interface SchemaRowProps {
schemaNode: SchemaNode;
nestingLevel: number;
pl?: SpaceVals;
}

export const SchemaRow: React.FunctionComponent<SchemaRowProps> = ({ schemaNode, nestingLevel }) => {
export const SchemaRow: React.FunctionComponent<SchemaRowProps> = React.memo(({ schemaNode, nestingLevel, pl }) => {
const { defaultExpandedDepth, renderRowAddon, onGoToRef, hideExamples, renderRootTreeLines } = useJSVOptionsContext();

const setPathCrumbs = useUpdateAtom(pathCrumbsAtom);
const setHoveredNode = useUpdateAtom(hoveredNodeAtom);
const isHovering = useAtomValue(isNodeHoveredAtom(schemaNode));

const [isExpanded, setExpanded] = React.useState<boolean>(
!isMirroredNode(schemaNode) && nestingLevel <= defaultExpandedDepth,
);
Expand Down Expand Up @@ -62,13 +65,20 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = ({ schemaNode,
const isCollapsible = childNodes.length > 0;
const isRootLevel = nestingLevel < rootLevel;

const required = isPropertyRequired(schemaNode);
const deprecated = isRegularNode(schemaNode) && schemaNode.deprecated;
const validations = isRegularNode(schemaNode) ? schemaNode.validations : {};
const hasProperties = useHasProperties({ required, deprecated, validations });

return (
<>
<Flex
maxW="full"
pl={pl}
py={2}
onMouseEnter={(e: any) => {
e.stopPropagation();
setPathCrumbs(selectedChoice.type);
setHoveredNode(selectedChoice.type);
}}
>
{!isRootLevel && <Box borderT w={isCollapsible ? 1 : 3} ml={-3} mr={3} mt={2} />}
Expand All @@ -82,7 +92,7 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = ({ schemaNode,
>
{isCollapsible ? <Caret isExpanded={isExpanded} /> : null}

<Flex alignItems="baseline" fontSize="base" flex={1} pos="sticky" top={0}>
<Flex alignItems="baseline" fontSize="base">
{schemaNode.subpath.length > 0 && shouldShowPropertyName(schemaNode) && (
<Box mr={2} fontFamily="mono" fontWeight="semibold">
{last(schemaNode.subpath)}
Expand Down Expand Up @@ -136,11 +146,9 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = ({ schemaNode,
)}
</Flex>

<Properties
required={isPropertyRequired(schemaNode)}
deprecated={isRegularNode(schemaNode) && schemaNode.deprecated}
validations={isRegularNode(schemaNode) ? schemaNode.validations : {}}
/>
{hasProperties && <Box bg={isHovering ? 'canvas-200' : undefined} h="px" flex={1} mx={3} />}

<Properties required={required} deprecated={deprecated} validations={validations} />
</Flex>

{typeof description === 'string' && description.length > 0 && <Description value={description} />}
Expand All @@ -159,10 +167,12 @@ export const SchemaRow: React.FunctionComponent<SchemaRowProps> = ({ schemaNode,
{renderRowAddon ? <Box>{renderRowAddon({ schemaNode, nestingLevel })}</Box> : null}
</Flex>

{isCollapsible && isExpanded ? <ChildStack childNodes={childNodes} currentNestingLevel={nestingLevel} /> : null}
{isCollapsible && isExpanded ? (
<ChildStack schemaNode={schemaNode} childNodes={childNodes} currentNestingLevel={nestingLevel} />
) : null}
</>
);
};
});

function shouldShowPropertyName(schemaNode: SchemaNode) {
return (
Expand Down
12 changes: 8 additions & 4 deletions src/components/SchemaRow/TopLevelSchemaRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as React from 'react';
import { COMBINER_NAME_MAP } from '../../consts';
import { useIsOnScreen } from '../../hooks/useIsOnScreen';
import { calculateChildrenToShow, isComplexArray } from '../../tree';
import { showPathCrumbsAtom } from '../PathCrumbs';
import { showPathCrumbsAtom } from '../PathCrumbs/state';
import { ChildStack } from '../shared/ChildStack';
import { SchemaRow, SchemaRowProps } from './SchemaRow';
import { useChoices } from './useChoices';
Expand All @@ -22,7 +22,7 @@ export const TopLevelSchemaRow = ({ schemaNode }: Pick<SchemaRowProps, 'schemaNo
return (
<>
<ScrollCheck />
<ChildStack childNodes={childNodes} currentNestingLevel={nestingLevel} />
<ChildStack schemaNode={schemaNode} childNodes={childNodes} currentNestingLevel={nestingLevel} />
</>
);
}
Expand Down Expand Up @@ -63,7 +63,9 @@ export const TopLevelSchemaRow = ({ schemaNode }: Pick<SchemaRowProps, 'schemaNo
) : null}
</HStack>

{childNodes.length > 0 ? <ChildStack childNodes={childNodes} currentNestingLevel={nestingLevel} /> : null}
{childNodes.length > 0 ? (
<ChildStack schemaNode={schemaNode} childNodes={childNodes} currentNestingLevel={nestingLevel} />
) : null}
</>
);
}
Expand All @@ -77,7 +79,9 @@ export const TopLevelSchemaRow = ({ schemaNode }: Pick<SchemaRowProps, 'schemaNo
array of:
</Box>

{childNodes.length > 0 ? <ChildStack childNodes={childNodes} currentNestingLevel={nestingLevel} /> : null}
{childNodes.length > 0 ? (
<ChildStack schemaNode={schemaNode} childNodes={childNodes} currentNestingLevel={nestingLevel} />
) : null}
</>
);
}
Expand Down
15 changes: 15 additions & 0 deletions src/components/SchemaRow/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SchemaNode } from '@stoplight/json-schema-tree';
import { atom } from 'jotai';
import { atomFamily } from 'jotai/utils';

export const hoveredNodeAtom = atom<SchemaNode | null>(null);
export const isNodeHoveredAtom = atomFamily((node: SchemaNode) => atom(get => node === get(hoveredNodeAtom)));
export const isChildNodeHoveredAtom = atomFamily((parent: SchemaNode) =>
atom(get => {
const hoveredNode = get(hoveredNodeAtom);

if (!hoveredNode || hoveredNode === parent) return false;

return hoveredNode.parent === parent;
}),
);
61 changes: 31 additions & 30 deletions src/components/shared/ChildStack.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,47 @@
import { SchemaNode } from '@stoplight/json-schema-tree';
import { SpaceVals, VStack } from '@stoplight/mosaic';
import { Box, SpaceVals } from '@stoplight/mosaic';
import * as React from 'react';

import { NESTING_OFFSET } from '../../consts';
import { useJSVOptionsContext } from '../../contexts';
import { SchemaRow, SchemaRowProps } from '../SchemaRow';

type ChildStackProps = {
schemaNode: SchemaNode;
childNodes: readonly SchemaNode[];
currentNestingLevel: number;
className?: string;
RowComponent?: React.FC<SchemaRowProps>;
};

export const ChildStack = ({
childNodes,
currentNestingLevel,
className,
RowComponent = SchemaRow,
}: ChildStackProps) => {
const { renderRootTreeLines } = useJSVOptionsContext();
const rootLevel = renderRootTreeLines ? 0 : 1;
const isRootLevel = currentNestingLevel < rootLevel;
export const ChildStack = React.memo(
({ childNodes, currentNestingLevel, className, RowComponent = SchemaRow }: ChildStackProps) => {
const { renderRootTreeLines } = useJSVOptionsContext();
const rootLevel = renderRootTreeLines ? 0 : 1;
const isRootLevel = currentNestingLevel < rootLevel;

let ml: SpaceVals | undefined;
if (!isRootLevel) {
ml = currentNestingLevel === rootLevel ? 'px' : 4;
}
let ml: SpaceVals | undefined;
if (!isRootLevel) {
ml = currentNestingLevel === rootLevel ? 'px' : 7;
}

return (
<VStack
className={className}
pl={isRootLevel ? undefined : NESTING_OFFSET}
ml={ml}
spacing={4}
fontSize="sm"
borderL={isRootLevel ? undefined : true}
data-level={currentNestingLevel}
>
{childNodes.map((childNode: SchemaNode) => (
<RowComponent key={childNode.id} schemaNode={childNode} nestingLevel={currentNestingLevel + 1} />
))}
</VStack>
);
};
return (
<Box
className={className}
ml={ml}
fontSize="sm"
borderL={isRootLevel ? undefined : true}
data-level={currentNestingLevel}
>
{childNodes.map((childNode: SchemaNode) => (
<RowComponent
key={childNode.id}
schemaNode={childNode}
nestingLevel={currentNestingLevel + 1}
pl={isRootLevel ? undefined : NESTING_OFFSET}
/>
))}
</Box>
);
},
);
Loading