diff --git a/src/Explorer.js b/src/Explorer.js index 8e3afcb..e2b192a 100644 --- a/src/Explorer.js +++ b/src/Explorer.js @@ -989,6 +989,82 @@ class AbstractView extends React.PureComponent { } } +function unvisitedChildFieldEntries(parentFields, field, visited) { + const type = unwrapOutputType(field.type); + if (isObjectType(type) || isInterfaceType(type)) { + return Object.values(type.getFields()) + .filter(childField => !visited.has(childField)) + .map(childField => [childField, parentFields.concat([field])]); + } else { + return []; + } +}; + +function searchOneGeneration(generationEntries, searchTerm, visited) { + let grandChildFieldEntries = []; + let matches = []; + + for (const [field, parentFields] of generationEntries) { + visited.add(field); + if (field.name.includes(searchTerm)) { + matches.push([field, parentFields]); + } else { + grandChildFieldEntries.push(...unvisitedChildFieldEntries(parentFields, field, visited)); + } + } + + return [matches, grandChildFieldEntries]; +}; + +function searchOneGenerationAsync(generationEntries, searchTerm, visited, matches, grandChildFieldEntries, getLastSearch, callback) { + if (!generationEntries.length) { + callback([matches, grandChildFieldEntries]); + return; + } + + const [field, parentFields] = generationEntries.pop(); + visited.add(field); + if (field.name.includes(searchTerm)) { + matches.push([field, parentFields]); + } else { + grandChildFieldEntries.push(...unvisitedChildFieldEntries(parentFields, field, visited)); + } + + if (getLastSearch() !== searchTerm) { + return; + } + + setTimeout(() => searchOneGenerationAsync(generationEntries, searchTerm, visited, matches, grandChildFieldEntries, getLastSearch, callback, 0)); +}; + +function searchRemainingGenerationsAsync(generationEntries, searchTerm, visited, callback, getLastSearch, depth) { + if (!generationEntries.length) { + callback([]); + return; + } + + searchOneGenerationAsync( + generationEntries, searchTerm, visited, [], [], getLastSearch, + ([matches, nextGenEntries]) => { + if (getLastSearch() !== searchTerm) { + return; + } + + if (matches.length) { + callback(matches); + } else { + setTimeout(() => searchRemainingGenerationsAsync(nextGenEntries, searchTerm, visited, callback, getLastSearch, depth + 1), 0); + } + }); +}; + +function childFieldsMatchingSearchTerm(field, searchTerm, callback, getLastSearch) { + const visited = new Set(); + + let generationEntries = [[field, []]]; + searchRemainingGenerationsAsync(generationEntries, searchTerm, visited, callback, getLastSearch, 0); +} + type FieldViewProps = {| field: Field, selections: Selections, @@ -999,6 +1075,7 @@ type FieldViewProps = {| makeDefaultArg: ?MakeDefaultArg, onRunOperation: void => void, styleConfig: StyleConfig, + searchTerm: string, |}; function defaultInputObjectFields( @@ -1081,6 +1158,14 @@ function defaultArgs( } class FieldView extends React.PureComponent { + constructor (props) { + super(props) + this.state = { + childMatches: [], + lastSearch: "" + } + } + _previousSelection: ?SelectionNode; _addAllFieldsToSelections = rawSubfields => { const subFields: Array = !!rawSubfields @@ -1122,10 +1207,8 @@ class FieldView extends React.PureComponent { this.props.modifySelections(nextSelections); }; - _addFieldToSelections = rawSubfields => { - const nextSelections = [ - ...this.props.selections, - this._previousSelection || { + _makeSelection = () => { + return this._previousSelection || { kind: 'Field', name: {kind: 'Name', value: this.props.field.name}, arguments: defaultArgs( @@ -1133,7 +1216,13 @@ class FieldView extends React.PureComponent { this.props.makeDefaultArg, this.props.field, ), - }, + } + }; + + _addFieldToSelections = () => { + const nextSelections = [ + ...this.props.selections, + this._makeSelection() ]; this.props.modifySelections(nextSelections); @@ -1151,7 +1240,7 @@ class FieldView extends React.PureComponent { shouldSelectAllSubfields ? this._addAllFieldsToSelections(rawSubfields) - : this._addFieldToSelections(rawSubfields); + : this._addFieldToSelections(); } }; @@ -1201,8 +1290,10 @@ class FieldView extends React.PureComponent { }; _modifyChildSelections = (selections: Selections) => { + const selection = this._getSelection(); + const oldSelections = this.props.selections.concat(!!selection ? [] : [this._makeSelection()]); this.props.modifySelections( - this.props.selections.map(selection => { + oldSelections.map(selection => { if ( selection.kind === 'Field' && this.props.field.name === selection.name.value @@ -1227,8 +1318,18 @@ class FieldView extends React.PureComponent { ); }; + componentDidUpdate() { + if (this.props.searchTerm.length && this.state.lastSearch !== this.props.searchTerm) { + this.setState( + {...this.state, lastSearch: this.props.searchTerm}, + () => { + childFieldsMatchingSearchTerm(this.props.field, this.props.searchTerm, (x)=>this.setState({...this.state, childMatches: x}), () => this.state.lastSearch); + }); + } + }; + render() { - const {field, schema, getDefaultFieldNames, styleConfig} = this.props; + const {field, schema, getDefaultFieldNames, styleConfig, searchTerm} = this.props; const selection = this._getSelection(); const type = unwrapOutputType(field.type); const args = field.args.sort((a, b) => a.name.localeCompare(b.name)); @@ -1282,26 +1383,17 @@ class FieldView extends React.PureComponent { ); - if ( - selection && - (isObjectType(type) || isInterfaceType(type) || isUnionType(type)) - ) { - const fields = isUnionType(type) ? {} : type.getFields(); - const childSelections = selection - ? selection.selectionSet - ? selection.selectionSet.selections - : [] - : []; - return ( -
- {node} -
- {Object.keys(fields) - .sort() - .map(fieldName => ( - { makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} - /> - ))} + searchTerm={""} + searchResult={this.props.searchResult} + elidedParents={this.props.elidedParents.slice(1)} + />); + } else { + return (); + } + } else if (isObjectType(type) || + isInterfaceType(type) || + isUnionType(type)) { + if (!!selection) { + const fields = isUnionType(type) ? {} : type.getFields(); + const children = ( +
+ {Object.keys(fields) + .sort() + .map(fieldName => { + return (); + }) + } + {isInterfaceType(type) || isUnionType(type) ? schema - .getPossibleTypes(type) - .map(type => ( - - )) - : null} + .getPossibleTypes(type) + .map(type => ( + + )) + : null}
-
- ); + ); + + return ( +
+ {node} + {children} +
+ ); + + } else if (searchTerm.length) { + if (this.state.childMatches.length) { + const children = ( +
+ {this.state.childMatches + .filter(searchResult => searchResult[1].length) + .map((searchResult, index) => { + if (searchResult[1].length > 1) { + return ( +
+ ... {searchResult[1].length - 1} elided ... +
+ +
+
); + } else { + return ( + ); + } + })} +
+ ); + + return ( +
+ {node} + {children} +
+ ); + + } + } } + return node; } } +type SearchProps = { + value: string, +}; + +class Search extends React.PureComponent { + _ref: ?any; + + render() { + return ( + { + this._ref = ref; + }} + type="text" + onChange={this.props.onChange} + value={this.props.value} + /> + ); + } +} + function parseQuery(text: string): ?DocumentNode | Error { try { if (!text.trim()) { @@ -1437,6 +1660,13 @@ type RootViewProps = {| |}; class RootView extends React.PureComponent { + constructor (props) { + super(props) + this.state = { + searchTerm: '' + } + } + _previousOperationDef: ?OperationDefinitionNode | ?FragmentDefinitionNode; _modifySelections = (selections: Selections) => { @@ -1485,6 +1715,11 @@ class RootView extends React.PureComponent { } }; + _handleSearchChange = (event, fields) => { + const searchTerm = event.target.value; + this.setState({...this.state, searchTerm}); + }; + render() { const { operation, @@ -1536,6 +1771,10 @@ class RootView extends React.PureComponent { '' )}
+ this._handleSearchChange(event, fields)} + /> {Object.keys(fields) .sort() @@ -1551,6 +1790,8 @@ class RootView extends React.PureComponent { makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} + searchTerm={this.state.searchTerm} + isParentSelected={true} /> ))}