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

Refactor filter fields/metadata, add custom placeholder texts

parent 3a5557e5
No related branches found
No related tags found
No related merge requests found
......@@ -8,6 +8,7 @@ export const NUMBER = 'NUMBER';
export const STRING = 'STRING';
export const BOOL = 'BOOL';
export const LOCATION = 'LOCATION';
export const UNKNOWN = 'UNKNOWN';
const DateBaseTypes = ['DateTimeField', 'DateField'];
const NumberBaseTypes = ['IntegerField', 'DecimalField', 'FloatField', 'BigIntegerField'];
......@@ -171,118 +172,112 @@ function longitudeFieldSelectArgument(field, table) {
};
}
var FilterOperators = {
'IS': {
'name': "=",
'verbose_name': "Is",
'validArgumentsFilters': [equivalentArgument],
'multi': true
const OPERATORS = {
"=": {
validArgumentsFilters: [equivalentArgument],
multi: true
},
'IS_NOT': {
'name': "!=",
'verbose_name': "Is Not",
'validArgumentsFilters': [equivalentArgument],
'multi': true
"!=": {
validArgumentsFilters: [equivalentArgument],
multi: true
},
'IS_NULL': {
'name': "IS_NULL",
'verbose_name': "Is Null",
'validArgumentsFilters': []
"IS_NULL": {
validArgumentsFilters: []
},
'IS_NOT_NULL': {
'name': "NOT_NULL",
'verbose_name': "Is Not Null",
'validArgumentsFilters': []
"NOT_NULL": {
validArgumentsFilters: []
},
'LESS_THAN': {
'name': "<",
'verbose_name': "Less Than",
'validArgumentsFilters': [comparableArgument]
"<": {
validArgumentsFilters: [comparableArgument]
},
'LESS_THAN_OR_EQUAL': {
'name': "<=",
'verbose_name': "Less Than or Equal To",
'validArgumentsFilters': [comparableArgument]
"<=": {
validArgumentsFilters: [comparableArgument]
},
'GREATER_THAN': {
'name': ">",
'verbose_name': "Greater Than",
'validArgumentsFilters': [comparableArgument]
">": {
validArgumentsFilters: [comparableArgument]
},
'GREATER_THAN_OR_EQUAL': {
'name': ">=",
'verbose_name': "Greater Than or Equal To",
'validArgumentsFilters': [comparableArgument]
">=": {
validArgumentsFilters: [comparableArgument]
},
'INSIDE': {
'name': "INSIDE",
'verbose_name': "Inside - (Lat,Long) for upper left, (Lat,Long) for lower right",
'validArgumentsFilters': [longitudeFieldSelectArgument, numberArgument, numberArgument, numberArgument, numberArgument]
"INSIDE": {
validArgumentsFilters: [longitudeFieldSelectArgument, numberArgument, numberArgument, numberArgument, numberArgument],
placeholders: ["Select longitude field", "Enter upper latitude", "Enter left longitude", "Enter lower latitude", "Enter right latitude"]
},
'BETWEEN': {
'name': "BETWEEN",
'verbose_name': "Between - Min, Max",
'validArgumentsFilters': [comparableArgument, comparableArgument]
"BETWEEN": {
validArgumentsFilters: [comparableArgument, comparableArgument]
},
'STARTS_WITH': {
'name': "STARTS_WITH",
'verbose_name': "Starts With",
'validArgumentsFilters': [freeformArgument]
"STARTS_WITH": {
validArgumentsFilters: [freeformArgument]
},
'ENDS_WITH': {
'name': "ENDS_WITH",
'verbose_name': "Ends With",
'validArgumentsFilters': [freeformArgument]
"ENDS_WITH": {
validArgumentsFilters: [freeformArgument]
},
'CONTAINS': {
'name': "CONTAINS",
'verbose_name': "Contains",
'validArgumentsFilters': [freeformArgument]
"CONTAINS": {
validArgumentsFilters: [freeformArgument]
}
};
var BaseOperators = ['IS', 'IS_NOT', 'IS_NULL', 'IS_NOT_NULL'];
var AdditionalOperators = {
'CharField': ['STARTS_WITH', 'ENDS_WITH', 'CONTAINS'],
'TextField': ['STARTS_WITH', 'ENDS_WITH', 'CONTAINS'],
'IntegerField': ['LESS_THAN', 'LESS_THAN_OR_EQUAL', 'GREATER_THAN', 'GREATER_THAN_OR_EQUAL', 'BETWEEN'],
'BigIntegerField': ['LESS_THAN', 'LESS_THAN_OR_EQUAL', 'GREATER_THAN', 'GREATER_THAN_OR_EQUAL', 'BETWEEN'],
'DecimalField': ['LESS_THAN', 'LESS_THAN_OR_EQUAL', 'GREATER_THAN', 'GREATER_THAN_OR_EQUAL', 'BETWEEN'],
'FloatField': ['LESS_THAN', 'LESS_THAN_OR_EQUAL', 'GREATER_THAN', 'GREATER_THAN_OR_EQUAL', 'BETWEEN'],
'DateTimeField': ['LESS_THAN', 'LESS_THAN_OR_EQUAL', 'GREATER_THAN', 'GREATER_THAN_OR_EQUAL', 'BETWEEN'],
'DateField': ['LESS_THAN', 'LESS_THAN_OR_EQUAL', 'GREATER_THAN', 'GREATER_THAN_OR_EQUAL', 'BETWEEN'],
'LatLongField': ['INSIDE'],
'latitude': ['INSIDE']
// ordered list of operators and metadata per type
const OPERATORS_BY_TYPE_ORDERED = {
[NUMBER]: [
{ name: "=", verboseName: "Equal" },
{ name: "!=", verboseName: "Not equal" },
{ name: ">", verboseName: "Greater than" },
{ name: "<", verboseName: "Less than" },
{ name: "BETWEEN", verboseName: "Between" },
{ name: ">=", verboseName: "Greater than or equal to", advanced: true },
{ name: "<=", verboseName: "Less than or equal to", advanced: true },
{ name: "IS_NULL", verboseName: "Is empty", advanced: true },
{ name: "NOT_NULL",verboseName: "Not empty", advanced: true }
],
[STRING]: [
{ name: "=", verboseName: "Is" },
{ name: "!=", verboseName: "Is not" },
{ name: "IS_NULL", verboseName: "Is empty", advanced: true },
{ name: "NOT_NULL",verboseName: "Not empty", advanced: true }
],
[TIME]: [
{ name: "=", verboseName: "Is" },
{ name: "<", verboseName: "Before" },
{ name: ">", verboseName: "After" },
{ name: "BETWEEN", verboseName: "Between" }
],
[LOCATION]: [
{ name: "=", verboseName: "Is" },
{ name: "!=", verboseName: "Is not" },
{ name: "INSIDE", verboseName: "Inside" }
],
[BOOL]: [
],
[UNKNOWN]: [
{ name: "=", verboseName: "Is" },
{ name: "!=", verboseName: "Is not" }
]
};
function formatOperator(cls, field, table) {
return {
'name': cls.name,
'verbose_name': cls.verbose_name,
'validArgumentsFilters': cls.validArgumentsFilters,
'fields': _.map(cls.validArgumentsFilters, function(validArgumentsFilter) {
return validArgumentsFilter(field, table);
}),
'multi': !!cls.multi
};
const MORE_VERBOSE_NAMES = {
"equal": "is equal to",
"not equal": "is not equal to",
"before": "is before",
"after": "is afer",
"not empty": "is not empty",
"less than": "is less than",
"greater than": "is greater than",
"less than or equal to": "is less than or equal to",
"greater than or equal to": "is greater than or equal to",
}
function getOperators(field, table) {
// All fields have the base operators
var validOperators = BaseOperators;
// Check to see if the field's base type offers additional operators
if (field.base_type in AdditionalOperators) {
validOperators = validOperators.concat(AdditionalOperators[field.base_type]);
}
// Check to see if the field's semantic type offers additional operators
if (field.special_type in AdditionalOperators) {
validOperators = validOperators.concat(AdditionalOperators[field.special_type]);
}
// Wrap them up and send them back
return _.map(validOperators, function(operator) {
return formatOperator(FilterOperators[operator], field, table);
let type = getUmbrellaType(field) || UNKNOWN;
return OPERATORS_BY_TYPE_ORDERED[type].map(operatorForType => {
let operator = OPERATORS[operatorForType.name];
let verboseNameLower = operatorForType.verboseName.toLowerCase();
return {
...operator,
...operatorForType,
moreVerboseName: MORE_VERBOSE_NAMES[verboseNameLower] || verboseNameLower,
fields: operator.validArgumentsFilters.map(validArgumentsFilter => validArgumentsFilter(field, table))
};
});
}
......
......@@ -96,7 +96,7 @@ export default class FilterPopover extends Component {
for (let i = 0; i < oldFilter.length - 2; i++) {
let field = operator.multi ? operator.fields[0] : operator.fields[i];
let oldField = oldOperator.multi ? oldOperator.fields[0] : oldOperator.fields[i];
if (field && oldField && field.type === oldField.type) {
if (field && oldField && field.type === oldField.type && oldFilter[i + 2] !== undefined) {
filter[i + 2] = oldFilter[i + 2];
}
}
......@@ -140,6 +140,7 @@ export default class FilterPopover extends Component {
let operator = field.operators_lookup[filter[0]];
return operator.fields.map((operatorField, index) => {
let values, onValuesChange;
let placeholder = operator.placeholders && operator.placeholders[index] || undefined;
if (operator.multi) {
values = this.state.filter.slice(2);
onValuesChange = (values) => this.setValues(values);
......@@ -153,8 +154,8 @@ export default class FilterPopover extends Component {
options={operatorField.values}
values={values}
onValuesChange={onValuesChange}
placeholder={placeholder}
multi={operator.multi}
index={index}
/>
);
} else if (operatorField.type === "text") {
......@@ -162,8 +163,8 @@ export default class FilterPopover extends Component {
<TextPicker
values={values}
onValuesChange={onValuesChange}
placeholder={placeholder}
multi={operator.multi}
index={index}
/>
);
} else if (operatorField.type === "number") {
......@@ -171,8 +172,8 @@ export default class FilterPopover extends Component {
<NumberPicker
values={values}
onValuesChange={onValuesChange}
placeholder={placeholder}
multi={operator.multi}
index={index}
/>
);
}
......
......@@ -78,7 +78,7 @@ export default class FilterWidget extends Component {
return (
<div className="Filter-section Filter-section-operator" onClick={this.open}>
&nbsp;
<a className="QueryOption flex align-center">{operatorDef && operatorDef.verbose_name}</a>
<a className="QueryOption flex align-center">{operatorDef && operatorDef.moreVerboseName}</a>
</div>
);
}
......
......@@ -4,37 +4,8 @@ import React, { Component, PropTypes } from "react";
import Icon from "metabase/components/Icon.react";
import { getUmbrellaType, NUMBER, STRING, TIME } from "metabase/lib/schema_metadata";
import cx from "classnames";
// TODO: merge into schema_metadata?
const OPERATORS = {
[NUMBER]: [
{ name: "=", verbose_name: "Equal" },
{ name: "!=", verbose_name: "Not equal" },
{ name: ">", verbose_name: "Greater than" },
{ name: "<", verbose_name: "Less than" },
{ name: "BETWEEN", verbose_name: "Between" },
{ name: ">=", verbose_name: "Greater than or equal to", advanced: true },
{ name: "<=", verbose_name: "Less than or equal to", advanced: true },
{ name: "IS_NULL", verbose_name: "Is empty", advanced: true },
{ name: "NOT_NULL",verbose_name: "Not empty", advanced: true }
],
[STRING]: [
{ name: "=", verbose_name: "Is" },
{ name: "!=", verbose_name: "Is not" },
{ name: "IS_NULL", verbose_name: "Is empty", advanced: true },
{ name: "NOT_NULL",verbose_name: "Not empty", advanced: true }
],
[TIME]: [
{ name: "=", verbose_name: "Is" },
{ name: "<", verbose_name: "Before" },
{ name: ">", verbose_name: "After" },
{ name: "BETWEEN", verbose_name: "Between" }
]
};
export default class OperatorSelector extends Component {
constructor(props) {
super(props);
......@@ -49,12 +20,6 @@ export default class OperatorSelector extends Component {
let operators = field.valid_operators;
// use overide order/name/visibility
let type = getUmbrellaType(field);
if (type in OPERATORS) {
operators = OPERATORS[type].map(o => ({ ...field.operators_lookup[o.name], ...o }))
}
let defaultOperators = operators.filter(o => !o.advanced);
let expandedOperators = operators.filter(o => o.advanced);
......@@ -71,7 +36,7 @@ export default class OperatorSelector extends Component {
className={cx("Button Button-normal Button--medium mr1 mb1", { "Button--purple": operator.name === filter[0] })}
onClick={() => this.props.onOperatorChange(operator.name)}
>
{operator.verbose_name}
{operator.verboseName}
</button>
)}
{ !expanded && expandedOperators.length > 0 ?
......
......@@ -38,9 +38,14 @@ export default class NumberPicker extends Component {
}
}
TextPicker.propTypes = {
NumberPicker.propTypes = {
values: PropTypes.array.isRequired,
onValuesChange: PropTypes.func.isRequired,
multi: PropTypes.bool,
index: PropTypes.number
placeholder: PropTypes.string,
validations: PropTypes.array,
multi: PropTypes.bool
};
NumberPicker.defaultProps = {
placeholder: "Enter desired number"
};
......@@ -60,7 +60,7 @@ export default class RelativeDatePicker extends Component {
<div className="p1 pt2">
<section>
{ SHORTCUTS.map((s, index) =>
<span className={cx("inline-block half pb1", { "pr1": index % 2 === 0 })}>
<span key={index} className={cx("inline-block half pb1", { "pr1": index % 2 === 0 })}>
<button
key={index}
className={cx("Button Button-normal Button--medium text-normal text-centered full", { "Button--purple": this.isSelectedShortcut(s) })}
......
......@@ -16,7 +16,7 @@ export default class SelectPicker extends Component {
}
render() {
let { values, options } = this.props;
let { values, options, placeholder } = this.props;
let checked = {};
for (let value of values) {
......@@ -24,18 +24,23 @@ export default class SelectPicker extends Component {
}
return (
<ul className="px1 pt1" style={{maxHeight: '200px', overflowY: 'scroll'}}>
{options.map((option, index) => {
return (
<li key={index}>
<label className="flex align-center full cursor-pointer p1" onClick={(e) => this.selectValue(option.key, !checked[option.key])}>
<CheckBox checked={checked[option.key]} />
<h4 className="ml1">{option.name}</h4>
</label>
</li>
)
})}
</ul>
<div className="px1 pt1" style={{maxHeight: '200px', overflowY: 'scroll'}}>
{ placeholder ?
<h5>{placeholder}</h5>
: null }
<ul>
{options.map((option, index) => {
return (
<li key={index}>
<label className="flex align-center full cursor-pointer p1" onClick={(e) => this.selectValue(option.key, !checked[option.key])}>
<CheckBox checked={checked[option.key]} />
<h4 className="ml1">{option.name}</h4>
</label>
</li>
)
})}
</ul>
</div>
);
}
}
......@@ -44,6 +49,6 @@ SelectPicker.propTypes = {
options: PropTypes.object.isRequired,
values: PropTypes.array.isRequired,
onValuesChange: PropTypes.func.isRequired,
multi: PropTypes.bool,
index: PropTypes.number
placeholder: PropTypes.string,
multi: PropTypes.bool
};
......@@ -38,7 +38,7 @@ export default class TextPicker extends Component {
type="text"
value={value}
onChange={(e) => this.setValue(index, e.target.value)}
placeholder="Enter desired value"
placeholder={this.props.placeholder}
autoFocus={true}
/>
{ index > 0 ?
......@@ -64,10 +64,12 @@ export default class TextPicker extends Component {
TextPicker.propTypes = {
values: PropTypes.array.isRequired,
onValuesChange: PropTypes.func.isRequired,
multi: PropTypes.bool,
validations: PropTypes.array
placeholder: PropTypes.string,
validations: PropTypes.array,
multi: PropTypes.bool
};
TextPicker.defaultProps = {
validations: []
validations: [],
placeholder: "Enter desired text"
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment