diff --git a/frontend/src/metabase/hoc/NamedColumn.jsx b/frontend/src/metabase/hoc/NamedColumn.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d94032b14f673ef26148272d7c67bd46a6bda62c --- /dev/null +++ b/frontend/src/metabase/hoc/NamedColumn.jsx @@ -0,0 +1,49 @@ +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 b7e811cbb7f74b345801ccd0a0ffe54b5455cdad..5f82f535ba907dbcc2c9d2ccec75be77932a82fc 100644 --- a/frontend/src/metabase/lib/query.js +++ b/frontend/src/metabase/lib/query.js @@ -619,6 +619,18 @@ for (const prop in Q) { import { isMath } from "metabase/lib/expressions"; +export const NamedClause = { + isNamed(clause) { + return Array.isArray(clause) && mbqlEq(clause[0], "named"); + }, + getName(clause) { + return NamedClause.isNamed(clause) ? clause[2] : null; + }, + getContent(clause) { + return NamedClause.isNamed(clause) ? clause[1] : clause; + } +} + export const AggregationClause = { // predicate function to test if a given aggregation clause is fully formed diff --git a/frontend/src/metabase/query_builder/components/AggregationWidget.jsx b/frontend/src/metabase/query_builder/components/AggregationWidget.jsx index e383f40c7cce60b7d321dec4ea28d17ededba536..965b81cbe922ee56cab8d8efb16c6966cafe62a1 100644 --- a/frontend/src/metabase/query_builder/components/AggregationWidget.jsx +++ b/frontend/src/metabase/query_builder/components/AggregationWidget.jsx @@ -5,6 +5,7 @@ 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"; @@ -14,7 +15,11 @@ 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);