Support for tag editor dynamic variable args (#512)

This commit is contained in:
Gregory Schier 2017-10-10 19:54:42 +02:00 committed by GitHub
parent 14bdea8b34
commit de08895092
7 changed files with 131 additions and 36 deletions

View File

@ -27,10 +27,11 @@ describe('ResponseExtension General', async () => {
}); });
it('fails on empty filter', async () => { it('fails on empty filter', async () => {
await models.response.create({parentId: 'req_test'}, '{"foo": "bar"}'); const request = await models.request.create({parentId: 'foo'});
await models.response.create({parentId: request._id, statusCode: 200}, '{"foo": "bar"}');
try { try {
await templating.render(`{% response "body", "req_test", "" %}`); await templating.render(`{% response "body", "${request._id}", "" %}`);
fail('Should have failed'); fail('Should have failed');
} catch (err) { } catch (err) {
expect(err.message).toContain('No body filter specified'); expect(err.message).toContain('No body filter specified');

View File

@ -49,10 +49,6 @@ export default ({
throw new Error('No request specified'); throw new Error('No request specified');
} }
if (field !== 'raw' && !filter) {
throw new Error(`No ${field} filter specified`);
}
const request = await context.util.models.request.getById(id); const request = await context.util.models.request.getById(id);
if (!request) { if (!request) {
throw new Error(`Could not find request ${id}`); throw new Error(`Could not find request ${id}`);
@ -65,7 +61,11 @@ export default ({
} }
if (!response.statusCode) { if (!response.statusCode) {
throw new Error('No responses for request'); throw new Error('No successful responses for request');
}
if (field !== 'raw' && !filter) {
throw new Error(`No ${field} filter specified`);
} }
const sanitizedFilter = filter.trim(); const sanitizedFilter = filter.trim();

View File

@ -3,6 +3,7 @@
export type NunjucksParsedTagArg = { export type NunjucksParsedTagArg = {
type: 'string' | 'number' | 'boolean' | 'number' | 'variable' | 'expression', type: 'string' | 'number' | 'boolean' | 'number' | 'variable' | 'expression',
value: string | number | boolean, value: string | number | boolean,
forceVariable?: boolean,
quotedBy?: '"' | "'" quotedBy?: '"' | "'"
}; };

View File

@ -67,13 +67,16 @@ class CookieList extends React.PureComponent<Props> {
return ( return (
<tr className="selectable" key={i}> <tr className="selectable" key={i}>
<RenderedText render={handleRender} component="td"> <td>
{cookie.domain} <RenderedText render={handleRender}>
</RenderedText> {cookie.domain}
<RenderedText render={handleRender} component="td" </RenderedText>
props={{className: 'force-wrap wide'}}> </td>
{cookieString} <td className="force-wrap wide">
</RenderedText> <RenderedText render={handleRender}>
{cookieString}
</RenderedText>
</td>
<td onClick={null} className="text-right no-wrap"> <td onClick={null} className="text-right no-wrap">
<button className="btn btn--super-compact btn--outlined" <button className="btn btn--super-compact btn--outlined"
onClick={e => handleShowModifyCookieModal(cookie)} onClick={e => handleShowModifyCookieModal(cookie)}

View File

@ -79,6 +79,7 @@ class NunjucksModal extends PureComponent {
onChange={this._handleTemplateChange} onChange={this._handleTemplateChange}
defaultValue={defaultTemplate} defaultValue={defaultTemplate}
handleRender={handleRender} handleRender={handleRender}
handleGetRenderContext={handleGetRenderContext}
workspace={workspace} workspace={workspace}
/> />
); );

View File

@ -2,10 +2,8 @@
import * as React from 'react'; import * as React from 'react';
type Props = { type Props = {
component: string,
children: string, children: string,
render: Function, render: Function
props?: Object
}; };
type State = { type State = {
@ -35,8 +33,7 @@ class RenderedText extends React.PureComponent<Props, State> {
} }
render () { render () {
const {component, props} = this.props; return this.state.renderedText;
return React.createElement(component, props || {}, this.state.renderedText);
} }
} }

View File

@ -14,9 +14,11 @@ import {trackEvent} from '../../../analytics/index';
import type {BaseModel} from '../../../models/index'; import type {BaseModel} from '../../../models/index';
import type {Workspace} from '../../../models/workspace'; import type {Workspace} from '../../../models/workspace';
import type {PluginArgumentEnumOption} from '../../../templating/extensions/index'; import type {PluginArgumentEnumOption} from '../../../templating/extensions/index';
import {Dropdown, DropdownButton, DropdownDivider, DropdownItem} from '../base/dropdown/index';
type Props = { type Props = {
handleRender: Function, handleRender: Function,
handleGetRenderContext: Function,
defaultValue: string, defaultValue: string,
onChange: Function, onChange: Function,
workspace: Workspace workspace: Workspace
@ -30,7 +32,8 @@ type State = {
allDocs: {[string]: Array<BaseModel>}, allDocs: {[string]: Array<BaseModel>},
rendering: boolean, rendering: boolean,
preview: string, preview: string,
error: string error: string,
variables: Array<{name: string, value: string}>
}; };
@autobind @autobind
@ -48,7 +51,8 @@ class TagEditor extends React.PureComponent<Props, State> {
allDocs: {}, allDocs: {},
rendering: true, rendering: true,
preview: '', preview: '',
error: '' error: '',
variables: []
}; };
} }
@ -68,8 +72,15 @@ class TagEditor extends React.PureComponent<Props, State> {
await this._update(tagDefinitions, activeTagDefinition, activeTagData, true); await this._update(tagDefinitions, activeTagDefinition, activeTagData, true);
} }
async loadVariables () {
const context = await this.props.handleGetRenderContext();
const variables = context.keys;
this.setState({variables});
}
componentDidMount () { componentDidMount () {
this.load(); this.load();
this.loadVariables();
} }
componentWillReceiveProps (nextProps: Props) { componentWillReceiveProps (nextProps: Props) {
@ -104,7 +115,11 @@ class TagEditor extends React.PureComponent<Props, State> {
this.setState({allDocs, loadingDocs: false}); this.setState({allDocs, loadingDocs: false});
} }
_updateArg (argValue: string | number, argIndex: number) { _updateArg (
argValue: string | number | boolean,
argIndex: number,
forceNewType: string | null = null
) {
const {tagDefinitions, activeTagData, activeTagDefinition} = this.state; const {tagDefinitions, activeTagData, activeTagDefinition} = this.state;
if (!activeTagData) { if (!activeTagData) {
@ -112,10 +127,13 @@ class TagEditor extends React.PureComponent<Props, State> {
return; return;
} }
if (!activeTagDefinition) {
console.warn('No active tag definition to update', {state: this.state});
return;
}
// Ensure all arguments exist // Ensure all arguments exist
const defaultArgs = activeTagDefinition const defaultArgs = this._getDefaultTagData(activeTagDefinition).args;
? this._getDefaultTagData(activeTagDefinition).args
: [];
for (let i = 0; i < defaultArgs.length; i++) { for (let i = 0; i < defaultArgs.length; i++) {
if (activeTagData.args[i]) { if (activeTagData.args[i]) {
continue; continue;
@ -135,10 +153,42 @@ class TagEditor extends React.PureComponent<Props, State> {
// Update it // Update it
argData.value = argValue; argData.value = argValue;
// Update type if we need to
if (forceNewType) {
// Ugh, what a hack (because it's enum)
(argData: any).type = forceNewType;
}
this._update(tagDefinitions, activeTagDefinition, tagData, false); this._update(tagDefinitions, activeTagDefinition, tagData, false);
} }
_handleChange (e: SyntheticEvent<HTMLInputElement>) { async _handleChangeArgVariable (options: {argIndex: number, variable: boolean}) {
const {variable, argIndex} = options;
const {activeTagData, activeTagDefinition, variables} = this.state;
if (!activeTagData || !activeTagDefinition) {
console.warn('Failed to change arg variable', {state: this.state});
return;
}
const argData = activeTagData.args[argIndex];
const argDef = activeTagDefinition.args[argIndex];
const existingValue = argData ? argData.value : '';
if (variable) {
const variable = variables.find(v => v.value === existingValue);
const firstVariable = variables.length ? variables[0].name : '';
const value = variable ? variable.name : firstVariable;
return this._updateArg(value || 'my_variable', argIndex, 'variable');
} else {
const initialType = argDef ? argDef.type : 'string';
const variable = variables.find(v => v.name === existingValue);
const value = variable ? variable.value : '';
return this._updateArg(value, argIndex, initialType);
}
}
_handleChange (e: SyntheticEvent<HTMLInputElement>, forceVariable: boolean = false) {
const parent = e.currentTarget.parentNode; const parent = e.currentTarget.parentNode;
let argIndex = -1; let argIndex = -1;
if (parent instanceof HTMLElement) { if (parent instanceof HTMLElement) {
@ -250,6 +300,19 @@ class TagEditor extends React.PureComponent<Props, State> {
} }
} }
renderArgVariable (path: string) {
const {variables} = this.state;
return (
<select type="text" defaultValue={path || ''} onChange={this._handleChange}>
{variables.map((v, i) => (
<option key={`${i}::${v.name}`} value={v.name}>
{v.name}
</option>
))}
</select>
);
}
renderArgString (value: string, placeholder: string) { renderArgString (value: string, placeholder: string) {
return ( return (
<input <input
@ -355,9 +418,13 @@ class TagEditor extends React.PureComponent<Props, State> {
} }
const value = argData.value.toString(); const value = argData.value.toString();
const isVariable = argData.type === 'variable';
const argInputVariable = isVariable ? this.renderArgVariable(value) : null;
let argInput; let argInput;
let isVariableAllowed = false;
if (argDefinition.type === 'string') { if (argDefinition.type === 'string') {
isVariableAllowed = true;
const placeholder = typeof argDefinition.placeholder === 'string' const placeholder = typeof argDefinition.placeholder === 'string'
? argDefinition.placeholder ? argDefinition.placeholder
: ''; : '';
@ -370,6 +437,7 @@ class TagEditor extends React.PureComponent<Props, State> {
const modelId = typeof value === 'string' ? value : 'unknown'; const modelId = typeof value === 'string' ? value : 'unknown';
argInput = this.renderArgModel(modelId, model); argInput = this.renderArgModel(modelId, model);
} else if (argDefinition.type === 'number') { } else if (argDefinition.type === 'number') {
isVariableAllowed = true;
const placeholder = typeof argDefinition.placeholder === 'string' const placeholder = typeof argDefinition.placeholder === 'string'
? argDefinition.placeholder ? argDefinition.placeholder
: ''; : '';
@ -385,16 +453,39 @@ class TagEditor extends React.PureComponent<Props, State> {
) ? fnOrString(argDefinition.displayName, argDatas) : ''; ) ? fnOrString(argDefinition.displayName, argDatas) : '';
return ( return (
<div key={argIndex} className="form-control form-control--outlined"> <div key={argIndex} className="form-row">
<label> <div className="form-control form-control--outlined">
{fnOrString(displayName, argDatas)} <label>
{help && ( {fnOrString(displayName, argDatas)}
<HelpTooltip className="space-left">{help}</HelpTooltip> {argData.type === 'variable' ? (
)} <span className="faded space-left">(Variable)</span>
<div data-arg-index={argIndex}> ) : null}
{argInput} {help && (
<HelpTooltip className="space-left">{help}</HelpTooltip>
)}
<div data-arg-index={argIndex}>
{argInputVariable || argInput}
</div>
</label>
</div>
{isVariableAllowed ? (
<div className="form-control form-control--outlined form-control--no-label width-auto">
<Dropdown right>
<DropdownButton className="btn btn--clicky">
<i className="fa fa-gear"/>
</DropdownButton>
<DropdownDivider>Input Type</DropdownDivider>
<DropdownItem value={{variable: false, argIndex}}
onClick={this._handleChangeArgVariable}>
<i className={'fa ' + (isVariable ? '' : 'fa-check')}/> Static Value
</DropdownItem>
<DropdownItem value={{variable: true, argIndex}}
onClick={this._handleChangeArgVariable}>
<i className={'fa ' + (isVariable ? 'fa-check' : '')}/> Environment Variable
</DropdownItem>
</Dropdown>
</div> </div>
</label> ) : null}
</div> </div>
); );
} }
@ -422,7 +513,8 @@ class TagEditor extends React.PureComponent<Props, State> {
} else { } else {
previewElement = ( previewElement = (
<pre> <pre>
<code className="block selectable scrollable force-wrap">{preview || <span>&nbsp;</span>}</code> <code className="block selectable scrollable force-wrap">{preview ||
<span>&nbsp;</span>}</code>
</pre> </pre>
); );
} }