Skip to content
Snippets Groups Projects
Commit b053ec17 authored by Tom Robinson's avatar Tom Robinson Committed by GitHub
Browse files

Merge pull request #4344 from metabase/sql-editor-improvments

Native/SQL editor improvements
parents 77896bb3 c11cb912
Branches
Tags
No related merge requests found
......@@ -10,3 +10,5 @@ export function createMultiwordSearchRegex(input) {
"i");
}
}
export const countLines = (str) => str.split(/\n/g).length
.NativeQueryEditor .ace_editor {
height: 200px;
height: 100%;
}
.NativeQueryEditor .react-resizable {
position: relative;
}
.NativeQueryEditor .react-resizable-handle {
position: absolute;
width: 100%;
height: 10px;
bottom: -5px;
cursor: ns-resize;
}
.NativeQueryEditor .ace_editor.read-only .ace_cursor {
......
......@@ -6,6 +6,9 @@ import ReactDOM from "react-dom";
import "./NativeQueryEditor.css";
import { ResizableBox } from 'react-resizable';
import { countLines } from "metabase/lib/string";
import 'ace/ace';
import 'ace/ext-language_tools';
......@@ -50,14 +53,26 @@ function getModeInfo(query, databases) {
};
}
const SCROLL_MARGIN = 8;
const LINE_HEIGHT = 16;
const MIN_HEIGHT_LINES = 1;
const MAX_AUTO_SIZE_LINES = 12;
const getEditorLineHeight = (lines) => lines * LINE_HEIGHT + 2 * SCROLL_MARGIN;
export default class NativeQueryEditor extends Component {
constructor(props, context) {
super(props, context);
const lines = props.query.native.query ?
Math.min(MAX_AUTO_SIZE_LINES, countLines(props.query.native.query)) :
MAX_AUTO_SIZE_LINES;
this.state = {
showEditor: true,//this.props.isOpen,
modeInfo: getModeInfo(props.query, props.databases)
showEditor: !(props.card && props.card.id),
modeInfo: getModeInfo(props.query, props.databases),
initialHeight: getEditorLineHeight(lines)
};
this.localUpdate = false;
......@@ -102,32 +117,31 @@ export default class NativeQueryEditor extends Component {
componentDidUpdate() {
const { modeInfo } = this.state;
let editorElement = ReactDOM.findDOMNode(this.refs.editor);
let editor = ace.edit(editorElement);
if (editor.getValue() !== this.props.query.native.query) {
if (this._editor.getValue() !== this.props.query.native.query) {
// This is a weird hack, but the purpose is to avoid an infinite loop caused by the fact that calling editor.setValue()
// will trigger the editor 'change' event, update the query, and cause another rendering loop which we don't want, so
// we need a way to update the editor without causing the onChange event to go through as well
this.localUpdate = true;
editor.setValue(this.props.query.native.query);
editor.clearSelection();
this._editor.setValue(this.props.query.native.query);
this._editor.clearSelection();
this.localUpdate = false;
}
if (modeInfo) {
let editorElement = ReactDOM.findDOMNode(this.refs.editor);
if (!modeInfo.database || modeInfo.database.native_permissions !== "write") {
editor.setReadOnly(true);
this._editor.setReadOnly(true);
editorElement.classList.add("read-only");
} else {
editor.setReadOnly(false);
this._editor.setReadOnly(false);
editorElement.classList.remove("read-only");
}
if (editor.getSession().$modeId !== modeInfo.mode) {
editor.getSession().setMode(modeInfo.mode);
if (this._editor.getSession().$modeId !== modeInfo.mode) {
this._editor.getSession().setMode(modeInfo.mode);
// monkey patch the mode to add our bracket/paren/braces-matching behavior
if (this.state.modeInfo.mode.indexOf("sql") >= 0) {
editor.getSession().$mode.$behaviour = new SQLBehaviour();
this._editor.getSession().$mode.$behaviour = new SQLBehaviour();
}
}
}
......@@ -135,26 +149,25 @@ export default class NativeQueryEditor extends Component {
loadAceEditor() {
let editorElement = ReactDOM.findDOMNode(this.refs.editor);
let editor = ace.edit(editorElement);
this._editor = ace.edit(editorElement);
// listen to onChange events
editor.getSession().on('change', this.onChange);
this._editor.getSession().on('change', this.onChange);
// initialize the content
editor.setValue(this.props.query.native.query);
const querySource = this.props.query.native.query;
this._editor.setValue(querySource);
this._editor.renderer.setScrollMargin(SCROLL_MARGIN, SCROLL_MARGIN);
// clear the editor selection, otherwise we start with the whole editor selected
editor.clearSelection();
this._editor.clearSelection();
// hmmm, this could be dangerous
editor.focus();
this.setState({
editor: editor
});
this._editor.focus();
let aceLanguageTools = ace.require('ace/ext/language_tools');
editor.setOptions({
this._editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
......@@ -190,12 +203,22 @@ export default class NativeQueryEditor extends Component {
});
}
_updateSize() {
const doc = this._editor.getSession().getDocument();
const element = ReactDOM.findDOMNode(this.refs.resizeBox);
const newHeight = getEditorLineHeight(doc.getLength());
if (newHeight > element.offsetHeight && newHeight <= getEditorLineHeight(MAX_AUTO_SIZE_LINES)) {
element.style.height = newHeight + "px";
this._editor.resize();
}
}
onChange(event) {
if (this.state.editor && !this.localUpdate) {
if (this._editor && !this.localUpdate) {
this._updateSize();
const { query } = this.props;
const { editor } = this.state;
if (query.native.query !== editor.getValue()) {
this.props.setQueryFn(assocIn(query, ["native", "query"], editor.getValue()));
if (query.native.query !== this._editor.getValue()) {
this.props.setQueryFn(assocIn(query, ["native", "query"], this._editor.getValue()));
}
}
}
......@@ -305,9 +328,18 @@ export default class NativeQueryEditor extends Component {
<Icon name={toggleEditorIcon} size={20}/>
</a>
</div>
<div className={"border-top " + editorClasses}>
<ResizableBox
ref="resizeBox"
className={"border-top " + editorClasses}
height={this.state.initialHeight}
minConstraints={[Infinity, getEditorLineHeight(MIN_HEIGHT_LINES)]}
axis="y"
onResizeStop={(e, data) => {
this._editor.resize();
}}
>
<div id="id_sql" ref="editor"></div>
</div>
</ResizableBox>
</div>
</div>
);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment