Skip to content

Commit e59a5f2

Browse files
committed
fix: step-in
1 parent 670f0e5 commit e59a5f2

File tree

10 files changed

+199
-60
lines changed

10 files changed

+199
-60
lines changed

src/components/__tests__/Property.spec.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { JSONSchema4 } from 'json-schema';
55
import * as React from 'react';
66
import { SchemaTree } from '../../tree';
77
import { metadataStore } from '../../tree/metadata';
8-
import { walk } from '../../tree/walk';
8+
import { walk } from '../../tree/utils/walk';
99
import { SchemaTreeListNode } from '../../types';
1010
import { Property, Types } from '../shared';
1111

@@ -25,7 +25,7 @@ describe('Property component', () => {
2525
};
2626

2727
metadataStore.set(treeNode, {
28-
schemaNode: walk(schema).next().value,
28+
schemaNode: walk(schema).next().value.node,
2929
path: [],
3030
schema,
3131
});
@@ -49,7 +49,7 @@ describe('Property component', () => {
4949
};
5050

5151
metadataStore.set(treeNode, {
52-
schemaNode: walk(schema).next().value,
52+
schemaNode: walk(schema).next().value.node,
5353
path: [],
5454
schema,
5555
});
@@ -70,7 +70,7 @@ describe('Property component', () => {
7070
};
7171

7272
metadataStore.set(treeNode, {
73-
schemaNode: walk(schema).next().value,
73+
schemaNode: walk(schema).next().value.node,
7474
path: [],
7575
schema,
7676
});
@@ -92,7 +92,7 @@ describe('Property component', () => {
9292
};
9393

9494
metadataStore.set(treeNode, {
95-
schemaNode: walk(schema).next().value,
95+
schemaNode: walk(schema).next().value.node,
9696
path: [],
9797
schema,
9898
});
@@ -114,7 +114,7 @@ describe('Property component', () => {
114114
};
115115

116116
metadataStore.set(treeNode, {
117-
schemaNode: walk(schema).next().value,
117+
schemaNode: walk(schema).next().value.node,
118118
path: [],
119119
schema,
120120
});
@@ -136,7 +136,7 @@ describe('Property component', () => {
136136
};
137137

138138
metadataStore.set(treeNode, {
139-
schemaNode: walk(schema).next().value,
139+
schemaNode: walk(schema).next().value.node,
140140
path: [],
141141
schema,
142142
});
@@ -164,7 +164,7 @@ describe('Property component', () => {
164164

165165
tree.populate();
166166

167-
const wrapper = shallow(<Property node={Array.from(tree)[1]} />);
167+
const wrapper = shallow(<Property node={tree.itemAt(1)!} />);
168168
expect(wrapper.find('div').first()).toHaveText('foo');
169169
});
170170

@@ -188,7 +188,7 @@ describe('Property component', () => {
188188

189189
tree.populate();
190190

191-
const wrapper = shallow(<Property node={Array.from(tree)[1]} />);
191+
const wrapper = shallow(<Property node={tree.itemAt(1)!} />);
192192
expect(wrapper.find('div').first()).toHaveText('foo');
193193
});
194194

@@ -213,11 +213,11 @@ describe('Property component', () => {
213213
tree.populate();
214214
tree.unwrap(Array.from(tree)[1] as TreeListParentNode);
215215

216-
const wrapper = shallow(<Property node={Array.from(tree)[2]} />);
216+
const wrapper = shallow(<Property node={tree.itemAt(2)!} />);
217217
expect(wrapper.find('div').first()).not.toExist();
218218
});
219219

220-
xtest('given a ref pointing at complex type, should not display property name', () => {
220+
test('given a ref pointing at complex type, should not display property name', () => {
221221
const schema: JSONSchema4 = {
222222
properties: {
223223
foo: {
@@ -238,7 +238,7 @@ describe('Property component', () => {
238238
tree.populate();
239239
tree.unwrap(Array.from(tree)[1] as TreeListParentNode);
240240

241-
const wrapper = shallow(<Property node={Array.from(tree)[2]} />);
241+
const wrapper = shallow(<Property node={tree.itemAt(2)!} />);
242242
expect(wrapper.find('div').first()).not.toExist();
243243
});
244244
});

src/components/__tests__/SchemaRow.spec.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,28 +81,28 @@ describe('SchemaRow component', () => {
8181
});
8282

8383
tree.populate();
84-
tree.unwrap(Array.from(tree)[1] as TreeListParentNode);
84+
tree.unwrap(tree.itemAt(1) as TreeListParentNode);
8585
});
8686

8787
test('should preserve the required validation', () => {
88-
const wrapper = shallow(<SchemaRow node={Array.from(tree)[5]} rowOptions={{}} />);
88+
const wrapper = shallow(<SchemaRow node={tree.itemAt(6)!} rowOptions={{}} />);
8989
expect(wrapper.find(Validations)).toHaveProp('required', true);
9090
});
9191

9292
test('should preserve the optional validation', () => {
93-
const wrapper = shallow(<SchemaRow node={Array.from(tree)[6]} rowOptions={{}} />);
93+
const wrapper = shallow(<SchemaRow node={tree.itemAt(7)!} rowOptions={{}} />);
9494
expect(wrapper.find(Validations)).toHaveProp('required', false);
9595
});
9696

9797
describe('given a referenced object', () => {
9898
test('should preserve the required validation', () => {
99-
const wrapper = shallow(<SchemaRow node={Array.from(tree)[2]} rowOptions={{}} />);
99+
const wrapper = shallow(<SchemaRow node={tree.itemAt(3)!} rowOptions={{}} />);
100100

101101
expect(wrapper.find(Validations)).toHaveProp('required', true);
102102
});
103103

104104
test('should preserve the optional validation', () => {
105-
const wrapper = shallow(<SchemaRow node={Array.from(tree)[3]} rowOptions={{}} />);
105+
const wrapper = shallow(<SchemaRow node={tree.itemAt(4)!} rowOptions={{}} />);
106106
expect(wrapper.find(Validations)).toHaveProp('required', false);
107107
});
108108
});

src/__tests__/SchemaTree.spec.tsx renamed to src/components/__tests__/SchemaTree.spec.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { shallow } from 'enzyme';
33
import 'jest-enzyme';
44
import { JSONSchema4 } from 'json-schema';
55
import * as React from 'react';
6-
import { SchemaTree } from '../components';
7-
import { useMetadata } from '../hooks/useMetadata';
6+
import { useMetadata } from '../../hooks/useMetadata';
7+
import { SchemaTree } from '../index';
88

99
jest.mock('mobx-react-lite', () => ({
1010
observer: (children: any) => children,
1111
}));
12-
jest.mock('../hooks/useMetadata');
12+
jest.mock('../../hooks/useMetadata');
1313

1414
const schema: JSONSchema4 = {
1515
type: 'object',

src/components/shared/Property.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function count(obj: Optional<JSONSchema4 | null>): number | null {
2626
function shouldShowPropertyName(treeNode: SchemaTreeListNode) {
2727
if (treeNode.parent === null) return false;
2828
try {
29-
const schema = getNodeMetadata(treeNode.parent).schema;
29+
const { schema } = getNodeMetadata(treeNode.parent);
3030
let type = getPrimaryType(schema);
3131

3232
if (type === SchemaKind.Array && schema.items) {

src/tree/__tests__/populateTree.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Tree } from '@stoplight/tree-list';
22
import * as fs from 'fs';
33
import * as path from 'path';
44
import { generateId } from '../../utils/generateId';
5-
import { populateTree } from '../populateTree';
5+
import { populateTree } from '../utils/populateTree';
66

77
const BASE_PATH = path.resolve(__dirname, '../../__fixtures__/');
88

src/tree/__tests__/tree.spec.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { TreeListParentNode } from '@stoplight/tree-list';
2+
import { JSONSchema4 } from 'json-schema';
3+
import { getNodeMetadata } from '../metadata';
4+
import { SchemaTree, SchemaTreeState } from '../tree';
5+
6+
describe('SchemaTree', () => {
7+
describe('expanding', () => {
8+
describe('oneOf combiner', () => {
9+
let tree: SchemaTree;
10+
let schema: JSONSchema4;
11+
12+
beforeEach(() => {
13+
schema = {
14+
type: 'object',
15+
properties: {
16+
user: {
17+
$ref: '#/properties/id',
18+
},
19+
id: {
20+
oneOf: [
21+
{
22+
type: 'object',
23+
required: ['foo'],
24+
properties: {
25+
foo: {
26+
type: 'string',
27+
},
28+
},
29+
},
30+
],
31+
},
32+
},
33+
};
34+
35+
tree = new SchemaTree(schema, new SchemaTreeState(), {
36+
expandedDepth: 0,
37+
mergeAllOf: false,
38+
resolveRef: void 0,
39+
});
40+
41+
tree.populate();
42+
});
43+
44+
test('upon expanded $ref, should insert only oneOf combiner', () => {
45+
expect(tree.count).toEqual(3);
46+
47+
tree.unwrap(tree.itemAt(1) as TreeListParentNode);
48+
49+
expect(tree.count).toEqual(4);
50+
expect(getNodeMetadata(tree.itemAt(2)!)).toHaveProperty(
51+
'schema',
52+
expect.objectContaining({
53+
oneOf: [
54+
{
55+
type: 'object',
56+
required: ['foo'],
57+
properties: {
58+
foo: {
59+
type: 'string',
60+
},
61+
},
62+
},
63+
],
64+
}),
65+
);
66+
});
67+
68+
test('upon expanded $ref and expanded oneOf combiner, should insert object', () => {
69+
expect(tree.count).toEqual(3);
70+
71+
tree.unwrap(tree.itemAt(1) as TreeListParentNode);
72+
tree.unwrap(tree.itemAt(2) as TreeListParentNode);
73+
74+
expect(tree.count).toEqual(5);
75+
expect(getNodeMetadata(tree.itemAt(3)!)).toHaveProperty(
76+
'schema',
77+
expect.objectContaining({
78+
type: 'object',
79+
required: ['foo'],
80+
properties: {
81+
foo: {
82+
type: 'string',
83+
},
84+
},
85+
}),
86+
);
87+
});
88+
89+
test('upon expanded id property, should insert object', () => {
90+
expect(tree.count).toEqual(3);
91+
92+
tree.unwrap(tree.itemAt(2) as TreeListParentNode);
93+
94+
expect(tree.count).toEqual(4);
95+
expect(getNodeMetadata(tree.itemAt(3)!)).toHaveProperty(
96+
'schema',
97+
expect.objectContaining({
98+
type: 'object',
99+
required: ['foo'],
100+
properties: {
101+
foo: {
102+
type: 'string',
103+
},
104+
},
105+
}),
106+
);
107+
});
108+
});
109+
});
110+
});

src/tree/tree.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { JSONSchema4 } from 'json-schema';
55
import { get as _get, isEqual as _isEqual, isObject as _isObject } from 'lodash';
66
import { isRefNode } from '../utils/guards';
77
import { getNodeMetadata, metadataStore } from './metadata';
8-
import { populateTree } from './populateTree';
8+
import { canStepIn } from './utils/canStepIn';
9+
import { populateTree } from './utils/populateTree';
910

1011
export type SchemaTreeRefDereferenceFn = (path: JsonPath, schema: JSONSchema4) => Optional<JSONSchema4>;
1112

@@ -36,7 +37,7 @@ export class SchemaTree extends Tree {
3637
const expanded = {};
3738
populateTree(this.schema, this.root, 0, [], {
3839
mergeAllOf: this.mergeAllOf,
39-
onNode: (node, parentTreeNode, level): boolean => {
40+
onNode: (fragment, node, parentTreeNode, level): boolean => {
4041
if (isRefNode(node) && node.$ref !== null && isLocalRef(node.$ref)) {
4142
expanded[node.id] = false;
4243
}
@@ -51,27 +52,34 @@ export class SchemaTree extends Tree {
5152
this.invalidate();
5253
}
5354

54-
public populateTreeFragment(parent: TreeListParentNode, schema: JSONSchema4, path: JsonPath) {
55+
public populateTreeFragment(parent: TreeListParentNode, schema: JSONSchema4, path: JsonPath, stepIn: boolean) {
5556
const initialLevel = Tree.getLevel(parent);
5657
const artificialRoot = Tree.createArtificialRoot();
5758
populateTree(schema, artificialRoot, initialLevel, path, {
5859
mergeAllOf: this.mergeAllOf,
59-
onNode: (node, parentTreeNode, level) => level <= this.expandedDepth + 1 || level <= initialLevel + 1,
60+
onNode: (fragment, node, parentTreeNode, level) => {
61+
if (level <= this.expandedDepth || level <= initialLevel) return true;
62+
return stepIn && level <= initialLevel + 1 && canStepIn(getNodeMetadata(parentTreeNode).schema);
63+
},
6064
});
6165

6266
if (artificialRoot.children.length === 0) {
6367
throw new Error(`Could not expand node ${path.join('.')}`);
6468
}
6569

66-
// todo: improve walk, i.e. add stepIn so that this is not required
70+
this.insertTreeFragment(stepIn ? this.stepIn(artificialRoot, parent) : artificialRoot.children, parent);
71+
}
72+
73+
protected stepIn(root: TreeListParentNode, parent: TreeListParentNode) {
6774
if (
68-
'children' in artificialRoot.children[0] &&
69-
_isEqual(getNodeMetadata(parent).path, getNodeMetadata(artificialRoot.children[0]).path)
75+
root.children.length > 0 &&
76+
'children' in root.children[0] &&
77+
_isEqual(getNodeMetadata(parent).path, getNodeMetadata(root.children[0]).path)
7078
) {
71-
this.insertTreeFragment(artificialRoot.children[0].children, parent);
72-
} else {
73-
this.insertTreeFragment(artificialRoot.children, parent);
79+
return root.children[0].children;
7480
}
81+
82+
return root.children;
7583
}
7684

7785
public unwrap(node: TreeListParentNode) {
@@ -82,16 +90,15 @@ export class SchemaTree extends Tree {
8290
const metadata = getNodeMetadata(node);
8391
const { path, schemaNode, schema } = metadata;
8492
if (!isRefNode(schemaNode)) {
85-
this.populateTreeFragment(node, schema, path);
93+
this.populateTreeFragment(node, schema, path, true);
8694
} else if (schemaNode.$ref !== null) {
8795
const refPath = pointerToPath(schemaNode.$ref);
8896
const schemaFragment = this.resolveRef ? this.resolveRef(refPath, this.schema) : _get(this.schema, refPath);
8997
if (!_isObject(schemaFragment)) {
9098
throw new ReferenceError(`Could not dereference ${refPath.join('.')}`);
9199
}
92100

93-
this.populateTreeFragment(node, schemaFragment, path);
94-
metadata.schema = schemaFragment;
101+
this.populateTreeFragment(node, schemaFragment, path, false);
95102
} else {
96103
throw new Error(`I do know not how not expand node ${path.join('.')}`);
97104
}

src/tree/utils/canStepIn.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { JSONSchema4 } from 'json-schema';
2+
import { SchemaKind } from '../../types';
3+
import { getCombiner } from '../../utils/getCombiner';
4+
import { getPrimaryType } from '../../utils/getPrimaryType';
5+
6+
export const canStepIn = (fragment: JSONSchema4) => {
7+
if (getCombiner(fragment)) {
8+
return true;
9+
}
10+
11+
const type = getPrimaryType(fragment);
12+
return type === SchemaKind.Array || type === SchemaKind.Object;
13+
};

0 commit comments

Comments
 (0)