diff --git a/frontend/src/metabase/hoc/NamedColumn.jsx b/frontend/src/metabase/hoc/NamedColumn.jsx deleted file mode 100644 index d94032b14f673ef26148272d7c67bd46a6bda62c..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/hoc/NamedColumn.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, { Component, PropTypes } from "react"; - -import Popover from "metabase/components/Popover.jsx"; - -import { NamedClause } from "metabase/lib/query"; - -import cx from "classnames"; - -const NamedColumn = ({ valueProp, updaterProp, nameIsEditable }) => (ComposedComponent) => class NamedWidget extends Component { - constructor(props, context) { - super(props, context); - this.state = { - isHovered: false - }; - } - render() { - const name = NamedClause.getName(this.props[valueProp]); - const clause = NamedClause.getContent(this.props[valueProp]); - - const props = { - ...this.props, - name: name, - [valueProp]: clause, - [updaterProp]: (clause) => this.props[updaterProp](name ? ["named", clause, name] : clause) - }; - - const isEditable = this.state.isHovered && this.props[updaterProp] && nameIsEditable(props); - - return ( - <div - className={cx(this.props.className, "relative")} - onMouseEnter={() => this.setState({ isHovered: true })} - onMouseLeave={() => this.setState({ isHovered: false })} - > - <ComposedComponent {...props} className="spread" /> - <Popover isOpen={isEditable}> - <input - className="input m1" - value={name || ""} - onChange={(e) => this.props[updaterProp](e.target.value ? ["named", clause, e.target.value] : clause)} - autoFocus - /> - </Popover> - </div> - ); - } -} - -export default NamedColumn; diff --git a/frontend/src/metabase/lib/query.js b/frontend/src/metabase/lib/query.js index 5f82f535ba907dbcc2c9d2ccec75be77932a82fc..3d742a5ed38e574d19af11afe85a00051bbc7080 100644 --- a/frontend/src/metabase/lib/query.js +++ b/frontend/src/metabase/lib/query.js @@ -628,6 +628,14 @@ export const NamedClause = { }, getContent(clause) { return NamedClause.isNamed(clause) ? clause[1] : clause; + }, + setName(clause, name) { + return ["named", NamedClause.getContent(clause), name]; + }, + setContent(clause, content) { + return NamedClause.isNamed(clause) ? + ["named", content, NamedClause.getName(clause)] : + content; } } @@ -672,7 +680,8 @@ export const AggregationClause = { }, isCustom(aggregation) { - return aggregation && isMath(aggregation) || ( + // for now treal all named clauses as custom + return aggregation && NamedClause.isNamed(aggregation) || isMath(aggregation) || ( AggregationClause.isStandard(aggregation) && _.any(aggregation.slice(1), (arg) => isMath(arg)) ); }, diff --git a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx index f49057a2d2947be076a5077333b3612ad2f148fa..f03a36667dd77471d629aa76d2884391964d4954 100644 --- a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx +++ b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx @@ -9,7 +9,7 @@ import Tooltip from "metabase/components/Tooltip.jsx"; import Button from "metabase/components/Button.jsx"; import Query from "metabase/lib/query"; -import { AggregationClause } from "metabase/lib/query"; +import { AggregationClause, NamedClause } from "metabase/lib/query"; import _ from "underscore"; @@ -23,8 +23,8 @@ export default class AggregationPopover extends Component { this.state = { aggregation: (props.isNew ? [] : props.aggregation), - choosingField: (props.aggregation && props.aggregation.length > 1 && AggregationClause.isStandard(props.aggregation)), - editingAggregation: (props.aggregation && props.aggregation.length > 1 && AggregationClause.isCustom(props.aggregation)) + choosingField: AggregationClause.isStandard(props.aggregation), + editingAggregation: AggregationClause.isCustom(props.aggregation) }; _.bindAll(this, "commitAggregation", "onPickAggregation", "onPickField", "onClearAggregation"); @@ -85,7 +85,7 @@ export default class AggregationPopover extends Component { itemIsSelected(item) { const { aggregation } = this.props; - return item.isSelected(aggregation); + return item.isSelected(NamedClause.getContent(aggregation)); } renderItemExtra(item, itemIndex) { @@ -115,7 +115,8 @@ export default class AggregationPopover extends Component { render() { const { availableAggregations, tableMetadata, isNew } = this.props; - const { aggregation, choosingField, editingAggregation } = this.state; + const { choosingField, editingAggregation } = this.state; + const aggregation = NamedClause.getContent(this.state.aggregation); let selectedAggregation; if (AggregationClause.isMetric(aggregation)) { @@ -180,8 +181,13 @@ export default class AggregationPopover extends Component { expression={aggregation} tableMetadata={tableMetadata} customFields={this.props.customFields} - onChange={(parsedExpression) => this.setState({aggregation: parsedExpression, error: null})} - onError={(errorMessage) => this.setState({error: errorMessage})} + onChange={(parsedExpression) => this.setState({ + aggregation: NamedClause.setContent(this.state.aggregation, parsedExpression), + error: null + })} + onError={(errorMessage) => this.setState({ + error: errorMessage + })} /> { this.state.error != null && ( Array.isArray(this.state.error) ? @@ -191,6 +197,16 @@ export default class AggregationPopover extends Component { : <div className="text-error mb1">{this.state.error.message}</div> )} + <input + className="input block full my1" + value={NamedClause.getName(this.state.aggregation)} + onChange={(e) => this.setState({ + aggregation: e.target.value ? + NamedClause.setName(aggregation, e.target.value) : + aggregation + })} + placeholder="Aggregation name (optional)" + /> <Button className="full" primary disabled={this.state.error} onClick={() => this.commitAggregation(this.state.aggregation)}> {isNew ? "Add" : "Update"} Aggregation </Button> diff --git a/frontend/src/metabase/query_builder/components/AggregationWidget.jsx b/frontend/src/metabase/query_builder/components/AggregationWidget.jsx index 965b81cbe922ee56cab8d8efb16c6966cafe62a1..01a8782828cbd2d63ce8f8e9750a0621a413cae7 100644 --- a/frontend/src/metabase/query_builder/components/AggregationWidget.jsx +++ b/frontend/src/metabase/query_builder/components/AggregationWidget.jsx @@ -5,21 +5,15 @@ import FieldName from './FieldName.jsx'; import Clearable from './Clearable.jsx'; import Popover from "metabase/components/Popover.jsx"; -import NamedColumn from "metabase/hoc/NamedColumn.jsx"; import Query from "metabase/lib/query"; -import { AggregationClause } from "metabase/lib/query"; +import { AggregationClause, NamedClause } from "metabase/lib/query"; import { getAggregator } from "metabase/lib/schema_metadata"; import { format } from "metabase/lib/expressions/formatter"; import cx from "classnames"; import _ from "underscore"; -@NamedColumn({ - valueProp: "aggregation", - updaterProp: "updateAggregation", - nameIsEditable: (props) => props.aggregation && props.aggregation[0] && props.aggregation[0] != "rows" -}) export default class AggregationWidget extends Component { constructor(props, context) { super(props, context); @@ -121,7 +115,9 @@ export default class AggregationWidget extends Component { render() { const { aggregation, addButton, name } = this.props; if (aggregation && aggregation.length > 0) { - let aggregationName = AggregationClause.isCustom(aggregation) ? + let aggregationName = NamedClause.isNamed(aggregation) ? + NamedClause.getName(aggregation) + : AggregationClause.isCustom(aggregation) ? this.renderCustomAggregation() : AggregationClause.isMetric(aggregation) ? this.renderMetricAggregation() diff --git a/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx b/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx index fe1993633de4a9dae090ef057c818dd8b5343fd1..7546d10bf52ca87da4e6fee78d4e2326f47926fa 100644 --- a/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx +++ b/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx @@ -103,7 +103,10 @@ export default class TokenizedInput extends Component { inputNode.removeChild(inputNode.firstChild); } ReactDOM.render(<TokenizedExpression source={this._getValue()} />, inputNode); - restore(); + + if (document.activeElement === inputNode) { + restore(); + } } render() { const { className, onFocus, onBlur, onClick } = this.props;