Skip to content

Commit f40a5a4

Browse files
author
Jakub Jankowski
authored
feat: conditionaly render readOnly writeOnly nodes (#83)
* feat: conditional readOnly and writeOnly nodes * fix: shpw node if both validations are present * chore: simplify validations * chore: clean updating component * chore: viewMode instead of context
1 parent 757d300 commit f40a5a4

File tree

7 files changed

+79
-7
lines changed

7 files changed

+79
-7
lines changed

src/__fixtures__/default-schema.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
"maximum": 150,
1313
"multipleOf": 10,
1414
"exclusiveMinimum": true,
15-
"exclusiveMaximum": true
15+
"exclusiveMaximum": true,
16+
"readOnly": true
1617
},
1718
"completed_at": {
1819
"type": "string",
19-
"format": "date-time"
20+
"format": "date-time",
21+
"writeOnly": true
2022
},
2123
"items": {
2224
"type": ["null", "array"],

src/__stories__/JsonSchemaViewer.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ storiesOf('JsonSchemaViewer', module)
2424
hideTopBar={boolean('hideTopBar', false)}
2525
shouldResolveEagerly={boolean('shouldResolveEagerly', false)}
2626
onGoToRef={action('onGoToRef')}
27+
viewMode={select(
28+
'viewMode',
29+
{
30+
standalone: 'standalone',
31+
read: 'read',
32+
write: 'write',
33+
},
34+
'standalone',
35+
)}
2736
/>
2837
))
2938
.add('custom schema', () => (

src/components/JsonSchemaViewer.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as React from 'react';
66

77
import { JSONSchema4 } from 'json-schema';
88
import { SchemaTree, SchemaTreeOptions, SchemaTreePopulateHandler, SchemaTreeRefDereferenceFn } from '../tree/tree';
9-
import { GoToRefHandler, RowRenderer } from '../types';
9+
import { GoToRefHandler, RowRenderer, ViewMode } from '../types';
1010
import { isSchemaViewerEmpty } from '../utils/isSchemaViewerEmpty';
1111
import { SchemaTree as SchemaTreeComponent } from './SchemaTree';
1212

@@ -27,8 +27,12 @@ export interface IJsonSchemaViewer {
2727
onTreePopulate?: SchemaTreePopulateHandler;
2828
resolveRef?: SchemaTreeRefDereferenceFn;
2929
shouldResolveEagerly?: boolean;
30+
viewMode?: ViewMode;
3031
}
3132

33+
export const ViewModeContext = React.createContext<ViewMode>('standalone');
34+
ViewModeContext.displayName = 'ViewModeContext';
35+
3236
export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaViewer & ErrorBoundaryForwardedProps> {
3337
protected readonly treeStore: TreeStore;
3438
protected readonly tree: SchemaTree;
@@ -51,6 +55,7 @@ export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaVi
5155
resolveRef: this.props.resolveRef,
5256
shouldResolveEagerly: !!this.props.shouldResolveEagerly,
5357
onPopulate: this.props.onTreePopulate,
58+
viewMode: this.props.viewMode,
5459
};
5560
}
5661

@@ -96,7 +101,8 @@ export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaVi
96101
this.treeStore.defaultExpandedDepth !== this.expandedDepth ||
97102
prevProps.schema !== this.props.schema ||
98103
prevProps.mergeAllOf !== this.props.mergeAllOf ||
99-
prevProps.shouldResolveEagerly !== this.props.shouldResolveEagerly
104+
prevProps.shouldResolveEagerly !== this.props.shouldResolveEagerly ||
105+
prevProps.viewMode !== this.props.viewMode
100106
) {
101107
this.treeStore.defaultExpandedDepth = this.expandedDepth;
102108
this.tree.treeOptions = this.treeOptions;
@@ -117,7 +123,9 @@ export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaVi
117123

118124
return (
119125
<div className={cn(className, 'JsonSchemaViewer flex flex-col relative')}>
120-
<SchemaTreeComponent expanded={expanded} name={name} schema={schema} treeStore={this.treeStore} {...props} />
126+
<ViewModeContext.Provider value={this.props.viewMode ?? 'standalone'}>
127+
<SchemaTreeComponent expanded={expanded} name={name} schema={schema} treeStore={this.treeStore} {...props} />
128+
</ViewModeContext.Provider>
121129
</div>
122130
);
123131
}

src/components/shared/Validations.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ import { Dictionary } from '@stoplight/types';
33
import { Popover } from '@stoplight/ui-kit';
44
import cn from 'classnames';
55
import * as React from 'react';
6+
import { ViewModeContext } from '../JsonSchemaViewer';
67

78
export interface IValidations {
89
required: boolean;
9-
validations: (Dictionary<unknown> | {}) & { deprecated?: boolean };
10+
validations: (Dictionary<unknown> | {}) & { deprecated?: boolean; readOnly?: unknown; writeOnly?: unknown };
1011
}
1112

1213
export const Validations: React.FunctionComponent<IValidations> = ({
1314
required,
14-
validations: { deprecated, ...validations },
15+
validations: { deprecated, readOnly, writeOnly, ...validations },
1516
}) => {
17+
const viewMode = React.useContext(ViewModeContext);
1618
const validationCount = Object.keys(validations).length;
1719

1820
const requiredElem = (
@@ -22,9 +24,20 @@ export const Validations: React.FunctionComponent<IValidations> = ({
2224
</div>
2325
);
2426

27+
// Show readOnly writeOnly validations only in standalone mode and only if just one of them is present
28+
const showVisibilityValidations = viewMode === 'standalone' && !!readOnly !== !!writeOnly;
29+
const visibility = showVisibilityValidations ? (
30+
readOnly ? (
31+
<span className="ml-2 text-darken-7 dark:text-lighten-6">read-only</span>
32+
) : (
33+
<span className="ml-2 text-darken-7 dark:text-lighten-6">write-only</span>
34+
)
35+
) : null;
36+
2537
return (
2638
<>
2739
{deprecated ? <span className="ml-2 text-orange-7 dark:text-orange-6">deprecated</span> : null}
40+
{visibility}
2841
{validationCount ? (
2942
<Popover
3043
boundary="window"

src/tree/__tests__/tree.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { TreeListParentNode } from '@stoplight/tree-list';
22
import { JSONSchema4 } from 'json-schema';
33
import { ResolvingError } from '../../errors';
4+
import { ViewMode } from '../../types';
45
import { getNodeMetadata } from '../metadata';
56
import { SchemaTree, SchemaTreeState } from '../tree';
67
import { printTree } from './utils/printTree';
@@ -1365,6 +1366,34 @@ describe('SchemaTree', () => {
13651366
expect(printTree(tree)).toMatchSnapshot();
13661367
});
13671368
});
1369+
1370+
test.each(['standalone', 'read', 'write'])('given %s mode, should populate proper nodes', mode => {
1371+
const schema: JSONSchema4 = {
1372+
type: ['string', 'object'],
1373+
properties: {
1374+
id: {
1375+
type: 'string',
1376+
readOnly: true,
1377+
},
1378+
description: {
1379+
type: 'string',
1380+
writeOnly: true,
1381+
},
1382+
},
1383+
};
1384+
1385+
const tree = new SchemaTree(schema, new SchemaTreeState(), {
1386+
expandedDepth: Infinity,
1387+
mergeAllOf: true,
1388+
resolveRef: void 0,
1389+
shouldResolveEagerly: true,
1390+
onPopulate: void 0,
1391+
viewMode: mode as ViewMode,
1392+
});
1393+
1394+
tree.populate();
1395+
expect(tree.count).toEqual(mode === 'standalone' ? 3 : 2);
1396+
});
13681397
});
13691398

13701399
test('given visible $ref node, should try to inject the title immediately', () => {

src/tree/tree.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { JsonPath, Optional } from '@stoplight/types';
44
import { JSONSchema4 } from 'json-schema';
55
import { get as _get, isEqual as _isEqual, isObject as _isObject } from 'lodash';
66
import { ResolvingError } from '../errors';
7+
import { ViewMode } from '../types';
78
import { hasRefItems, isRefNode } from '../utils/guards';
89
import { getSchemaNodeMetadata } from './metadata';
910
import { canStepIn } from './utils/canStepIn';
@@ -29,6 +30,7 @@ export type SchemaTreeOptions = {
2930
resolveRef: Optional<SchemaTreeRefDereferenceFn>;
3031
shouldResolveEagerly: boolean;
3132
onPopulate: Optional<SchemaTreePopulateHandler>;
33+
viewMode?: ViewMode;
3234
};
3335

3436
export { TreeState as SchemaTreeState };
@@ -53,6 +55,13 @@ export class SchemaTree extends Tree {
5355
populateTree(this.schema, this.root, 0, [], {
5456
mergeAllOf: this.treeOptions.mergeAllOf,
5557
onNode: (fragment, node, parentTreeNode, level): boolean => {
58+
if (
59+
!!fragment.writeOnly !== !!fragment.readOnly &&
60+
((this.treeOptions.viewMode === 'read' && fragment.writeOnly) ||
61+
(this.treeOptions.viewMode === 'write' && fragment.readOnly))
62+
) {
63+
return false;
64+
}
5665
if (
5766
!this.treeOptions.shouldResolveEagerly &&
5867
((isRefNode(node) && node.$ref !== null) || (hasRefItems(node) && node.items.$ref !== null))

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,5 @@ export type RowRenderer = (
6565
rowOptions: IRowRendererOptions,
6666
treeStore: TreeStore,
6767
) => React.ReactNode;
68+
69+
export type ViewMode = 'read' | 'write' | 'standalone';

0 commit comments

Comments
 (0)