mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-24 05:05:39 +00:00
241 lines
8.0 KiB
JavaScript
241 lines
8.0 KiB
JavaScript
import React, { Component } from 'react';
|
|
import isEqual from 'lodash/isEqual';
|
|
import { getAllVariables } from 'utils/collections';
|
|
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
|
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
|
|
import { MaskedEditor } from 'utils/common/masked-editor';
|
|
import StyledWrapper from './StyledWrapper';
|
|
import { setupLinkAware } from 'utils/codemirror/linkAware';
|
|
import { IconEye, IconEyeOff } from '@tabler/icons';
|
|
|
|
const CodeMirror = require('codemirror');
|
|
|
|
class MultiLineEditor extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
// Keep a cached version of the value, this cache will be updated when the
|
|
// editor is updated, which can later be used to protect the editor from
|
|
// unnecessary updates during the update lifecycle.
|
|
this.cachedValue = props.value || '';
|
|
this.editorRef = React.createRef();
|
|
this.variables = {};
|
|
this.readOnly = props.readOnly || false;
|
|
|
|
this.state = {
|
|
maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
// Initialize CodeMirror as a single line editor
|
|
/** @type {import("codemirror").Editor} */
|
|
const variables = getAllVariables(this.props.collection, this.props.item);
|
|
|
|
this.editor = CodeMirror(this.editorRef.current, {
|
|
lineWrapping: false,
|
|
lineNumbers: false,
|
|
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
|
placeholder: this.props.placeholder,
|
|
mode: 'brunovariables',
|
|
brunoVarInfo: this.props.enableBrunoVarInfo !== false ? {
|
|
variables,
|
|
collection: this.props.collection,
|
|
item: this.props.item
|
|
} : false,
|
|
readOnly: this.props.readOnly,
|
|
tabindex: 0,
|
|
extraKeys: {
|
|
'Ctrl-Enter': () => {
|
|
if (this.props.onRun) {
|
|
this.props.onRun();
|
|
}
|
|
},
|
|
'Cmd-Enter': () => {
|
|
if (this.props.onRun) {
|
|
this.props.onRun();
|
|
}
|
|
},
|
|
'Cmd-S': () => {
|
|
if (this.props.onSave) {
|
|
this.props.onSave();
|
|
}
|
|
},
|
|
'Ctrl-S': () => {
|
|
if (this.props.onSave) {
|
|
this.props.onSave();
|
|
}
|
|
},
|
|
'Cmd-F': () => {},
|
|
'Ctrl-F': () => {},
|
|
// Tabbing disabled to make tabindex work
|
|
'Tab': false,
|
|
'Shift-Tab': false
|
|
}
|
|
});
|
|
|
|
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
|
|
const getAnywordAutocompleteHints = () => this.props.autocomplete || [];
|
|
|
|
// Setup AutoComplete Helper
|
|
const autoCompleteOptions = {
|
|
showHintsFor: ['variables'],
|
|
getAllVariables: getAllVariablesHandler,
|
|
getAnywordAutocompleteHints
|
|
};
|
|
|
|
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
|
this.editor,
|
|
autoCompleteOptions
|
|
);
|
|
|
|
setupLinkAware(this.editor);
|
|
|
|
this.editor.setValue(String(this.props.value) || '');
|
|
this.editor.on('change', this._onEdit);
|
|
this.addOverlay(variables);
|
|
|
|
// Initialize masking if this is a secret field
|
|
this.setState({ maskInput: this.props.isSecret });
|
|
this._enableMaskedEditor(this.props.isSecret);
|
|
}
|
|
|
|
_onEdit = () => {
|
|
if (!this.ignoreChangeEvent && this.editor) {
|
|
this.cachedValue = this.editor.getValue();
|
|
if (this.props.onChange) {
|
|
this.props.onChange(this.cachedValue);
|
|
}
|
|
}
|
|
};
|
|
|
|
/** Enable or disable masking the rendered content of the editor */
|
|
_enableMaskedEditor = (enabled) => {
|
|
if (typeof enabled !== 'boolean') return;
|
|
|
|
if (enabled == true) {
|
|
if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');
|
|
this.maskedEditor.enable();
|
|
} else {
|
|
if (this.maskedEditor) {
|
|
this.maskedEditor.disable();
|
|
this.maskedEditor.destroy();
|
|
this.maskedEditor = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
componentDidUpdate(prevProps) {
|
|
// Ensure the changes caused by this update are not interpreted as
|
|
// user-input changes which could otherwise result in an infinite
|
|
// event loop.
|
|
this.ignoreChangeEvent = true;
|
|
|
|
let variables = getAllVariables(this.props.collection, this.props.item);
|
|
if (!isEqual(variables, this.variables)) {
|
|
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
|
|
this.editor.options.brunoVarInfo.variables = variables;
|
|
}
|
|
this.addOverlay(variables);
|
|
}
|
|
|
|
// Update collection and item when they change
|
|
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
|
|
if (!isEqual(this.props.collection, this.editor.options.brunoVarInfo.collection)) {
|
|
this.editor.options.brunoVarInfo.collection = this.props.collection;
|
|
}
|
|
if (!isEqual(this.props.item, this.editor.options.brunoVarInfo.item)) {
|
|
this.editor.options.brunoVarInfo.item = this.props.item;
|
|
}
|
|
}
|
|
if (this.props.theme !== prevProps.theme && this.editor) {
|
|
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
|
}
|
|
if (this.props.readOnly !== prevProps.readOnly && this.editor) {
|
|
this.editor.setOption('readOnly', this.props.readOnly);
|
|
}
|
|
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
|
// TODO: temporary fix for keeping cursor state when auto save and new line insertion collide PR#7098
|
|
const nextValue = String(this.props.value ?? '');
|
|
const currentValue = this.editor.getValue();
|
|
if (this.editor.hasFocus?.() && currentValue !== nextValue) {
|
|
this.cachedValue = currentValue;
|
|
} else {
|
|
const cursor = this.editor.getCursor();
|
|
this.cachedValue = nextValue;
|
|
this.editor.setValue(nextValue);
|
|
this.editor.setCursor(cursor);
|
|
// Re-apply masking after setValue() since it destroys all CodeMirror marks
|
|
if (this.maskedEditor && this.maskedEditor.isEnabled()) {
|
|
this.maskedEditor.update();
|
|
}
|
|
}
|
|
}
|
|
if (!isEqual(this.props.isSecret, prevProps.isSecret)) {
|
|
// If the secret flag has changed, update the editor to reflect the change
|
|
this._enableMaskedEditor(this.props.isSecret);
|
|
// also set the maskInput flag to the new value
|
|
this.setState({ maskInput: this.props.isSecret });
|
|
}
|
|
if (this.props.readOnly !== prevProps.readOnly && this.editor) {
|
|
this.editor.setOption('readOnly', this.props.readOnly || false);
|
|
}
|
|
this.ignoreChangeEvent = false;
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.brunoAutoCompleteCleanup) {
|
|
this.brunoAutoCompleteCleanup();
|
|
}
|
|
if (this.editor?._destroyLinkAware) {
|
|
this.editor._destroyLinkAware();
|
|
}
|
|
if (this.maskedEditor) {
|
|
this.maskedEditor.destroy();
|
|
this.maskedEditor = null;
|
|
}
|
|
this.editor.getWrapperElement().remove();
|
|
}
|
|
|
|
addOverlay = (variables) => {
|
|
this.variables = variables;
|
|
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', false, true);
|
|
this.editor.setOption('mode', 'brunovariables');
|
|
};
|
|
|
|
/**
|
|
* @brief Toggle the visibility of the secret value
|
|
*/
|
|
toggleVisibleSecret = () => {
|
|
const isVisible = !this.state.maskInput;
|
|
this.setState({ maskInput: isVisible });
|
|
this._enableMaskedEditor(isVisible);
|
|
};
|
|
|
|
/**
|
|
* @brief Eye icon to show/hide the secret value
|
|
* @returns ReactComponent The eye icon
|
|
*/
|
|
secretEye = (isSecret) => {
|
|
return isSecret === true ? (
|
|
<button className="mx-2" onClick={() => this.toggleVisibleSecret()}>
|
|
{this.state.maskInput === true ? (
|
|
<IconEyeOff size={18} strokeWidth={2} />
|
|
) : (
|
|
<IconEye size={18} strokeWidth={2} />
|
|
)}
|
|
</button>
|
|
) : null;
|
|
};
|
|
|
|
render() {
|
|
const wrapperClass = `multi-line-editor grow ${this.props.readOnly ? 'read-only' : ''}`;
|
|
return (
|
|
<div className={`flex flex-row justify-between w-full overflow-x-auto ${this.props.className}`}>
|
|
<StyledWrapper ref={this.editorRef} className={wrapperClass} />
|
|
{this.secretEye(this.props.isSecret)}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
export default MultiLineEditor;
|