diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade index bfb091021..641193d6e 100644 --- a/client/components/sidebar/sidebarFilters.jade +++ b/client/components/sidebar/sidebarFilters.jade @@ -72,6 +72,43 @@ template(name="filterSidebar") | ({{ username }}) if Filter.assignees.isSelected _id i.fa.fa-check + + hr + h3 + i.fa.fa-list-alt + | {{_ 'filter-dates-label' }} + ul.sidebar-list + li(class="{{#if Filter.dueAt.isSelected 'noDate'}}active{{/if}}") + a.name.js-toggle-no-due-date-filter + span.sidebar-list-item-description + | {{_ 'filter-no-due-date' }} + if Filter.dueAt.isSelected 'noDate' + i.fa.fa-check + li(class="{{#if Filter.dueAt.isSelected 'past'}}active{{/if}}") + a.name.js-toggle-overdue-filter + span.sidebar-list-item-description + | {{_ 'filter-overdue' }} + if Filter.dueAt.isSelected 'past' + i.fa.fa-check + li(class="{{#if Filter.dueAt.isSelected 'today'}}active{{/if}}") + a.name.js-toggle-due-today-filter + span.sidebar-list-item-description + | {{_ 'filter-due-today' }} + if Filter.dueAt.isSelected 'today' + i.fa.fa-check + li(class="{{#if Filter.dueAt.isSelected 'tomorrow'}}active{{/if}}") + a.name.js-toggle-due-tomorrow-filter + span.sidebar-list-item-description + | {{_ 'filter-due-tomorrow' }} + if Filter.dueAt.isSelected 'tomorrow' + i.fa.fa-check + li(class="{{#if Filter.dueAt.isSelected 'week'}}active{{/if}}") + a.name.js-toggle-due-this-week-filter + span.sidebar-list-item-description + | {{_ 'filter-due-this-week' }} + if Filter.dueAt.isSelected 'week' + i.fa.fa-check + hr h3 i.fa.fa-list-alt diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js index 1e846e9c0..7f527b9f5 100644 --- a/client/components/sidebar/sidebarFilters.js +++ b/client/components/sidebar/sidebarFilters.js @@ -23,6 +23,31 @@ BlazeComponent.extendComponent({ Filter.assignees.toggle(this.currentData()._id); Filter.resetExceptions(); }, + 'click .js-toggle-no-due-date-filter'(evt) { + evt.preventDefault(); + Filter.dueAt.noDate(); + Filter.resetExceptions(); + }, + 'click .js-toggle-overdue-filter'(evt) { + evt.preventDefault(); + Filter.dueAt.past(); + Filter.resetExceptions(); + }, + 'click .js-toggle-due-today-filter'(evt) { + evt.preventDefault(); + Filter.dueAt.today(); + Filter.resetExceptions(); + }, + 'click .js-toggle-due-tomorrow-filter'(evt) { + evt.preventDefault(); + Filter.dueAt.tomorrow(); + Filter.resetExceptions(); + }, + 'click .js-toggle-due-this-week-filter'(evt) { + evt.preventDefault(); + Filter.dueAt.thisWeek(); + Filter.resetExceptions(); + }, 'click .js-toggle-archive-filter'(evt) { evt.preventDefault(); Filter.archive.toggle(this.currentData()._id); diff --git a/client/lib/filter.js b/client/lib/filter.js index 24ca320b7..cb5f56bf7 100644 --- a/client/lib/filter.js +++ b/client/lib/filter.js @@ -7,6 +7,125 @@ function showFilterSidebar() { Sidebar.setView('filter'); } +class DateFilter { + constructor() { + this._dep = new Tracker.Dependency(); + this.subField = ''; // Prevent name mangling in Filter + this._filter = null; + this._filterState = null; + } + + _updateState(state) { + this._filterState = state; + showFilterSidebar(); + this._dep.changed(); + } + + // past builds a filter for all dates before now + past() { + if (this._filterState == 'past') { this.reset(); return; } + this._filter = { $lte: moment().toDate() }; + this._updateState('past'); + } + + // today is a convenience method for calling relativeDay with 0 + today() { + if (this._filterState == 'today') { this.reset(); return; } + this.relativeDay(0); + this._updateState('today'); + } + + // tomorrow is a convenience method for calling relativeDay with 1 + tomorrow() { + if (this._filterState == 'tomorrow') { this.reset(); return; } + this.relativeDay(1); + this._updateState('tomorrow'); + } + + // thisWeek is a convenience method for calling relativeWeek with 1 + thisWeek() { + this.relativeWeek(1); + } + + // relativeDay builds a filter starting from now and including all + // days up to today +/- offset. + relativeDay(offset) { + if (this._filterState == 'day') { this.reset(); return; } + + var startDay = moment().startOf('day').toDate(), + endDay = moment().endOf('day').add(offset, 'day').toDate(); + + if (offset >= 0) { + this._filter = { $gte: startDay, $lte: endDay }; + } else { + this._filter = { $lte: startDay, $gte: endDay }; + } + + this._updateState('day'); + } + + // relativeWeek builds a filter starting from today and including all + // weeks up to today +/- offset. This considers the user's preferred + // start of week day (as defined by Meteor). + relativeWeek(offset) { + if (this._filterState == 'week') { this.reset(); return; } + + // getStartDayOfWeek returns the offset from Sunday of the user's + // preferred starting day of the week. This date should be added + // to the moment start of week to get the real start of week date. + // The default is 1, meaning Monday. + const currentUser = Meteor.user(); + const weekStartDay = currentUser ? currentUser.getStartDayOfWeek() : 1; + + // Moments are mutable so they must be cloned before modification + var thisWeekStart = moment().startOf('day').startOf('week').add(weekStartDay, 'days'); + var thisWeekEnd = thisWeekStart.clone().add(offset, 'week').endOf('day'); + var startDate = thisWeekStart.toDate(); + var endDate = thisWeekEnd.toDate(); + + if (offset >= 0) { + this._filter = { $gte: startDate, $lte: endDate }; + } else { + this._filter = { $lte: startDate, $gte: endDate }; + } + + this._updateState('week'); + } + + // noDate builds a filter for items where date is not set + noDate() { + if (this._filterState == 'noDate') { this.reset(); return; } + this._filter = null; + this._updateState('noDate'); + } + + reset() { + this._filter = null; + this._filterState = null; + this._dep.changed(); + } + + isSelected(val) { + this._dep.depend(); + return this._filterState == val; + } + + _isActive() { + this._dep.depend(); + return this._filterState !== null; + } + + _getMongoSelector() { + this._dep.depend(); + return this._filter; + } + + _getEmptySelector() { + this._dep.depend(); + return null; + } +} + // Use a "set" filter for a field that is a set of documents uniquely // identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`. // use "subField" for searching inside object Fields. @@ -462,6 +581,7 @@ Filter = { assignees: new SetFilter(), archive: new SetFilter(), hideEmpty: new SetFilter(), + dueAt: new DateFilter(), customFields: new SetFilter('_id'), advanced: new AdvancedFilter(), lists: new AdvancedFilter(), // we need the ability to filter list by name as well @@ -472,6 +592,7 @@ Filter = { 'assignees', 'archive', 'hideEmpty', + 'dueAt', 'customFields', ], diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index dae814f82..2ea022302 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -344,6 +344,12 @@ "list-label-short-sort": "(M)", "filter": "Filter", "filter-cards": "Filter Cards or Lists", + "filter-dates-label": "Filter by date", + "filter-no-due-date": "No due date", + "filter-overdue": "Overdue", + "filter-due-today": "Due today", + "filter-due-this-week": "Due this week", + "filter-due-tomorrow": "Due tomorrow", "list-filter-label": "Filter List by Title", "filter-clear": "Clear filter", "filter-labels-label": "Filter by label",