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

Relative date shortcuts

parent 00014c58
Branches
Tags
No related merge requests found
......@@ -35,3 +35,25 @@
color: white !important;
background-color: var(--purple-light-color);
}
.Calendar--range .Calendar-day--selected,
.Calendar--range .Calendar-day--selected-end {
background-color: var(--purple-color);
}
.Calendar--range .Calendar-day--week-start,
.Calendar--range .Calendar-day--selected {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.Calendar--range .Calendar-day--week-end,
.Calendar--range .Calendar-day--selected-end {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.Calendar-day--in-range {
color: white !important;
background-color: var(--purple-light-color);
}
......@@ -2,7 +2,7 @@
import _ from "underscore";
import SchemaMetadata from "metabase/lib/schema_metadata";
import * as SchemaMetadata from "metabase/lib/schema_metadata";
function compareNumbers(a, b) {
return a - b;
......
......@@ -2,13 +2,14 @@
import React, { Component, PropTypes } from 'react';
import cx from 'classnames';
import moment from "moment";
import Icon from 'metabase/components/Icon.react';
export default class Calendar extends Component {
constructor(props) {
super(props);
let month = this.props.selected.clone();
let month = this.props.selected ? this.props.selected.clone() : moment();
const modes = ['month', 'year', 'decade']
this.state = {
month: month,
......@@ -18,6 +19,21 @@ export default class Calendar extends Component {
this.previous = this.previous.bind(this);
this.next = this.next.bind(this);
this.cycleMode = this.cycleMode.bind(this);
this.onClickDay = this.onClickDay.bind(this);
}
onClickDay(date, e) {
let { selected, selectedEnd } = this.props;
if (!selected || selectedEnd) {
this.props.onChange(date.format("YYYY-MM-DD"), null);
} else if (!selectedEnd) {
if (date.isAfter(selected)) {
this.props.onChange(selected.format("YYYY-MM-DD"), date.format("YYYY-MM-DD"));
} else {
this.props.onChange(date.format("YYYY-MM-DD"), selected.format("YYYY-MM-DD"));
}
}
}
cycleMode() {
......@@ -44,7 +60,7 @@ export default class Calendar extends Component {
renderMonthHeader() {
return (
<div className="Calendar-header flex align-center">
<div className="bordered rounded p1 cursor-pointer transition-border border-hover px1"onClick={this.previous}>
<div className="bordered rounded p1 cursor-pointer transition-border border-hover px1" onClick={this.previous}>
<Icon name="chevronleft" width="10" height="12" />
</div>
<span className="flex-full" />
......@@ -79,8 +95,9 @@ export default class Calendar extends Component {
key={date.toString()}
date={date.clone()}
month={this.state.month}
onChange={this.props.onChange}
onClickDay={this.onClickDay}
selected={this.props.selected}
selectedEnd={this.props.selectedEnd}
/>
);
date.add(1, "w");
......@@ -94,7 +111,7 @@ export default class Calendar extends Component {
}
render() {
return (
<div className="Calendar">
<div className={cx("Calendar", { "Calendar--range": this.props.selected && this.props.selectedEnd })}>
{this.renderMonthHeader()}
{this.renderDayNames()}
{this.renderWeeks()}
......@@ -104,10 +121,17 @@ export default class Calendar extends Component {
}
Calendar.propTypes = {
selected: PropTypes.object.isRequired
selected: PropTypes.object,
selectedEnd: PropTypes.object,
onChange: PropTypes.func.isRequired
};
class Week extends Component {
_dayIsSelected(day) {
return
}
render() {
let days = [];
let { date, month } = this.props;
......@@ -116,15 +140,21 @@ class Week extends Component {
let classes = cx({
'p1': true,
'cursor-pointer': true,
'bordered': true,
'text-centered': true,
"Calendar-day": true,
"Calendar-day--today": date.isSame(new Date(), "day"),
"Calendar-day--this-month": date.month() === month.month(),
"Calendar-day--selected": date.isSame(this.props.selected)
"Calendar-day--selected": date.isSame(this.props.selected),
"Calendar-day--selected-end": date.isSame(this.props.selectedEnd),
"Calendar-day--week-start": i === 0,
"Calendar-day--week-end": i === 6,
"Calendar-day--in-range": (
date.isSame(this.props.selected) || date.isSame(this.props.selectedEnd) ||
(this.props.selectedEnd && this.props.selectedEnd.isAfter(date) && date.isAfter(this.props.selected))
)
});
days.push(
<span key={date.toString()} className={classes} onClick={this.props.onChange.bind(null, date)}>
<span key={date.toString()} className={classes} onClick={this.props.onClickDay.bind(null, date)}>
{date.date()}
</span>
);
......@@ -140,6 +170,8 @@ class Week extends Component {
}
}
Week.defaultProps = {
onChange: () => {}
Week.propTypes = {
selected: PropTypes.object,
selectedEnd: PropTypes.object,
onClickDay: PropTypes.func.isRequired
}
......@@ -12,6 +12,7 @@ import DatePicker from "./pickers/DatePicker.react";
import Icon from "metabase/components/Icon.react";
import Query from "metabase/lib/query";
import * as SchemaMetadata from "metabase/lib/schema_metadata";
import cx from "classnames";
......@@ -27,9 +28,14 @@ export default class FilterPopover extends Component {
this.setField = this.setField.bind(this);
this.setOperator = this.setOperator.bind(this);
this.setValues = this.setValues.bind(this);
this.setFilter = this.setFilter.bind(this);
this.commitFilter = this.commitFilter.bind(this);
}
componentDidUpdate() {
console.log("FILTER", this.state.filter);
}
commitFilter() {
this.props.onCommitFilter(this.state.filter);
this.props.onClose();
......@@ -53,6 +59,10 @@ export default class FilterPopover extends Component {
this.setState({ filter, pane: "filter" });
}
setFilter(filter) {
this.setState({ filter });
}
setOperator(operator) {
let { filter } = this.state;
if (filter[0] !== operator) {
......@@ -78,8 +88,11 @@ export default class FilterPopover extends Component {
// update the operator
filter = [operatorName, filter[1]];
for (var i = 0; i < operator.fields.length; i++) {
filter.push(undefined);
if (operator) {
for (var i = 0; i < operator.fields.length; i++) {
filter.push(undefined);
}
}
return filter;
}
......@@ -107,12 +120,11 @@ export default class FilterPopover extends Component {
// field/operator combo is valid
let { field } = this._getTarget(filter);
let operator = field.operators_lookup[filter[0]];
if (!operator) {
return false;
}
// has the mininum number of arguments
if (filter.length - 2 < operator.fields.length) {
return false;
if (operator) {
// has the mininum number of arguments
if (filter.length - 2 < operator.fields.length) {
return false;
}
}
// arguments are non-null/undefined
for (var i = 2; i < filter.length; i++) {
......@@ -124,7 +136,19 @@ export default class FilterPopover extends Component {
return true;
}
renderPicker(field, operator) {
renderPicker(filter, field) {
let operator = field.operators_lookup[filter[0]];
// HACK: special case for dates
if (SchemaMetadata.isDate(field)) {
return (
<DatePicker
filter={this.state.filter}
onFilterChange={this.setFilter}
/>
);
}
return operator.fields.map((operatorField, index) => {
if (operatorField.type === "select") {
return (
......@@ -154,10 +178,6 @@ export default class FilterPopover extends Component {
index={index}
/>
);
} else if (operatorField.type === "date") {
return (
<DatePicker />
)
}
return <span>not implemented {operatorField.type} {operator.multi ? "true" : "false"}</span>;
});
......@@ -179,7 +199,6 @@ export default class FilterPopover extends Component {
} else {
let { filter } = this.state;
let { table, field } = this._getTarget(filter);
let selectedOperator = field.operators_lookup[filter[0]];
return (
<div style={{width: 300}}>
......@@ -197,7 +216,7 @@ export default class FilterPopover extends Component {
field={field}
onOperatorChange={this.setOperator}
/>
{ selectedOperator && this.renderPicker(field, selectedOperator) }
{ this.renderPicker(filter, field) }
</div>
<div className="FilterPopover-footer p1">
<button className={cx("Button", "Button--purple", "full", { "disabled": !this.isValid() })} onClick={this.commitFilter}>
......
......@@ -26,7 +26,8 @@ const OPERATORS = {
[TIME]: [
{ name: "=", verbose_name: "Is" },
{ name: "<", verbose_name: "Before" },
{ name: ">", verbose_name: "After" }
{ name: ">", verbose_name: "After" },
{ name: "BETWEEN", verbose_name: "Between" }
]
};
......
'use strict';
import React, { Component } from 'react';
import React, { Component, PropTypes } from 'react';
import moment from 'moment';
import _ from "underscore";
import cx from "classnames";
import Calendar from '../../Calendar.react';
class DateShortcuts extends Component {
isCurrentShortcut(shortcut) {
return this.props.shortcut === shortcut;
const SHORTCUTS = [
{ name: "Today", operator: "TIME_INTERVAL", values: ["current", "day"]},
{ name: "Yesterday", operator: "TIME_INTERVAL", values: ["last", "day"]},
{ name: "Past 7 days", operator: "TIME_INTERVAL", values: [-7, "day"]},
{ name: "Past 30 days", operator: "TIME_INTERVAL", values: [-30, "day"]}
];
const RELATIVE_SHORTCUTS = {
"Last": [
{ name: "Week", operator: "TIME_INTERVAL", values: ["last", "week"]},
{ name: "Month", operator: "TIME_INTERVAL", values: ["last", "month"]},
{ name: "Year", operator: "TIME_INTERVAL", values: ["last", "year"]}
],
"This": [
{ name: "Week", operator: "TIME_INTERVAL", values: ["current", "week"]},
{ name: "Month", operator: "TIME_INTERVAL", values: ["current", "month"]},
{ name: "Year", operator: "TIME_INTERVAL", values: ["current", "year"]}
]
};
export default class DatePicker extends Component {
constructor(props) {
super(props);
_.bindAll(this, "onChange", "isSelectedShortcut", "onSetShortcut");
}
onChange(start, end) {
let { filter } = this.props;
if (end) {
this.props.onFilterChange(["BETWEEN", filter[1], start, end]);
} else {
let operator = _.contains(["=", "<", ">"], filter[0]) ? filter[0] : "=";
this.props.onFilterChange([operator, filter[1], start]);
}
}
render() {
let { filter } = this.props
let start, end;
if (filter[0] !== "TIME_INTERVAL") {
start = filter[2] && moment(filter[2], "YYYY-MM-DD");
end = filter[3] && moment(filter[3], "YYYY-MM-DD");
}
return (
<div>
<div className="mx1 mt1">
<Calendar
selected={start}
selectedEnd={end}
onChange={this.onChange}
/>
</div>
<div className="px1 pt1">
<DateShortcuts filter={this.props.filter} isSelectedShortcut={this.isSelectedShortcut} onSetShortcut={this.onSetShortcut} />
<RelativeDates filter={this.props.filter} isSelectedShortcut={this.isSelectedShortcut} onSetShortcut={this.onSetShortcut} />
</div>
</div>
)
}
shortcuts() {
return ['Today', 'Yesterday', 'Past 7 days', 'Past 30 days'];
isSelectedShortcut(shortcut) {
let { filter } = this.props;
return filter[0] === shortcut.operator && _.isEqual(filter.slice(2), shortcut.values);
}
onSetShortcut(shortcut) {
let { filter } = this.props;
this.props.onFilterChange([shortcut.operator, filter[1], ...shortcut.values])
}
}
DatePicker.propTypes = {
filter: PropTypes.array.isRequired,
onFilterChange: PropTypes.func.isRequired
};
class DateShortcuts extends Component {
selectedStyles() {
return {
'text-purple': true
......@@ -23,21 +93,51 @@ class DateShortcuts extends Component {
render() {
return (
<ul className="bordered rounded">
{ this.shortcuts().map((shortcut, index) => {
return <li key={index} className="cursor-pointer text-bold py1 text-purple text-centered inline-block half">{shortcut}</li>
})}
{ SHORTCUTS.map((s, index) =>
<li
key={index}
className={cx("cursor-pointer text-bold py1 text-purple text-centered inline-block half", { "bg-brand": this.props.isSelectedShortcut(s) })}
onClick={() => this.props.onSetShortcut(s)}
>
{s.name}
</li>
)}
</ul>
)
);
}
}
DateShortcuts.propTypes = {
isSelectedShortcut: PropTypes.func.isRequired,
onSetShortcut: PropTypes.func.isRequired
}
class RelativeDates extends Component {
constructor() {
super();
constructor(props) {
super(props);
this.state = {
selected: 'last'
tab: this._findTabWithSelection(props) || 'Last'
}
}
componentWillReceiveProps(nextProps) {
let tab = this._findTabWithSelection(nextProps);
if (tab && tab !== this.state.tab) {
this.setState({ tab });
}
}
_findTabWithSelection() {
for (let tab in RELATIVE_SHORTCUTS) {
for (let shortcut of RELATIVE_SHORTCUTS[tab]) {
if (this.props.isSelectedShortcut(shortcut)) {
return tab;
}
}
}
return null;
}
render() {
const tabStyles = function (state, condition) {
return {
......@@ -51,37 +151,31 @@ class RelativeDates extends Component {
}
}
return (
<div className="px1 pt1">
<DateShortcuts selected="today" />
<div>
<div style={{display: 'flex', justifyContent: 'center'}} className="mt1">
<a style={tabStyles(this.state.selected, 'last')} className="py1 px2 cursor-pointer bordered" onClick={() => this.setState({ selected: 'last' })}>Last</a>
<a style={tabStyles(this.state.selected, 'this')} className="py1 px2 cursor-pointer bordered" onClick={() => this.setState({ selected: 'this' })}>This</a>
</div>
<div style={{marginTop: '-1px', display: 'flex', justifyContent: 'center'}} className="border-top pt1">
<h4 className="mr1 cursor-pointer bordered border-hover rounded p1 inline-block">Week</h4>
<h4 className="mr1 cursor-pointer bordered border-hover rounded p1 inline-block">Month</h4>
<h4 className="cursor-pointer bordered border-hover rounded p1 inline-block">Year</h4>
</div>
</div>
</div>
)
}
}
export default class DatePicker extends Component {
setDateValue(index, date) {
this.setValue(index, date.format('YYYY-MM-DD'));
}
render() {
return (
<div>
<div className="mx1 mt1">
<Calendar selected={moment()} />
<div style={{display: 'flex', justifyContent: 'center'}} className="mt1">
{ Object.keys(RELATIVE_SHORTCUTS).map(tab =>
<a style={tabStyles(this.state.tab, tab)} className="py1 px2 cursor-pointer bordered" onClick={() => this.setState({ tab })}>{tab}</a>
)}
</div>
<RelativeDates />
<ul style={{marginTop: '-1px', display: 'flex', justifyContent: 'center'}} className="border-top pt1">
{ RELATIVE_SHORTCUTS[this.state.tab].map((s, index) =>
<li
key={index}
className={cx("h4 mr1 cursor-pointer bordered border-hover rounded p1 inline-block", { "bg-brand": this.props.isSelectedShortcut(s)})}
onClick={() => this.props.onSetShortcut(s)}
>
{s.name}
</li>
)}
</ul>
</div>
)
}
}
RelativeDates.propTypes = {
filter: PropTypes.array.isRequired,
isSelectedShortcut: PropTypes.func.isRequired,
onSetShortcut: PropTypes.func.isRequired
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment