insomnia/packages/insomnia-app/app/ui/components/templating/variable-editor.tsx
2021-12-04 09:08:28 +13:00

154 lines
4.3 KiB
TypeScript

import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { createRef, FC, PureComponent } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import { HandleGetRenderContext, HandleRender } from '../../../common/render';
import { useNunjucks } from '../../context/nunjucks/use-nunjucks';
interface Props {
handleRender: HandleRender;
handleGetRenderContext: HandleGetRenderContext;
defaultValue: string;
onChange: Function;
}
interface State {
variables: any[];
value: string;
preview: string;
error: string;
variableSource: string;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
class VariableEditorInternal extends PureComponent<Props, State> {
textAreaRef = createRef<HTMLTextAreaElement>();
_select: HTMLSelectElement | null = null;
constructor(props: Props) {
super(props);
const inner = props.defaultValue.replace(/\s*}}$/, '').replace(/^{{\s*/, '');
this.state = {
variables: [],
value: `{{ ${inner} }}`,
preview: '',
error: '',
variableSource: '',
};
}
componentDidMount() {
this._update(this.state.value, true);
this._resize();
}
componentDidUpdate() {
this._resize();
}
_handleChange(e) {
const name = e.target.value;
this._update(name);
}
_resize() {
setTimeout(() => {
const element = this.textAreaRef.current;
// @ts-expect-error -- TSCONVERSION null coalesce
element.style.cssText = 'height:auto';
// @ts-expect-error -- TSCONVERSION null coalesce
element.style.cssText = `height:${element.scrollHeight}px;overflow:hidden`;
}, 200);
}
_setSelectRef(n: HTMLSelectElement) {
this._select = n;
// Let it render, then focus the input
setTimeout(() => {
this._select?.focus();
}, 100);
}
async _update(value, noCallback = false) {
const { handleRender } = this.props;
const cleanedValue = value
.replace(/^{%/, '')
.replace(/%}$/, '')
.replace(/^{{/, '')
.replace(/}}$/, '')
.trim();
let preview = '';
let error = '';
try {
preview = await handleRender(value);
} catch (err) {
error = err.message;
}
const context = await this.props.handleGetRenderContext();
const variables = context.keys.sort((a, b) => (a.name < b.name ? -1 : 1));
const variableSource = context.context.getKeysContext().keyContext[cleanedValue] || '';
// Hack to skip updating if we unmounted for some reason
if (this._select) {
this.setState({
preview,
error,
variables,
value,
variableSource,
});
}
// Call the callback if we need to
if (!noCallback) {
this.props.onChange(value);
}
}
render() {
const { error, value, preview, variables, variableSource } = this.state;
const isOther = !variables.find(v => value === `{{ ${v.name} }}`);
return (
<div>
<div className="form-control form-control--outlined">
<label>
Environment Variable
<select ref={this._setSelectRef} value={value} onChange={this._handleChange}>
<option value={"{{ 'my custom template logic' | urlencode }}"}>-- Custom --</option>
{variables.map((v, i) => (
<option key={`${i}::${v.name}`} value={`{{ ${v.name} }}`}>
{v.name}
</option>
))}
</select>
</label>
</div>
{isOther && (
<div className="form-control form-control--outlined">
<input type="text" defaultValue={value} onChange={this._handleChange} />
</div>
)}
<div className="form-control form-control--outlined">
<label>
Live Preview {variableSource && ` - {source: ${variableSource} }`}
{error ? (
<textarea className="danger" value={error || 'Error'} readOnly />
) : (
<textarea ref={this.textAreaRef} value={preview || ''} readOnly />
)}
</label>
</div>
</div>
);
}
}
export const VariableEditor: FC<Omit<Props, 'handleRender' | 'handleGetRenderContext'>> = props => {
const { handleRender, handleGetRenderContext } = useNunjucks();
return <VariableEditorInternal {...props} handleRender={handleRender} handleGetRenderContext={handleGetRenderContext} />;
};