浏览代码

Support filtering by due date

Mike Crute 4 年之前
父节点
当前提交
e7186b47b0
共有 4 个文件被更改,包括 189 次插入0 次删除
  1. 37 0
      client/components/sidebar/sidebarFilters.jade
  2. 25 0
      client/components/sidebar/sidebarFilters.js
  3. 121 0
      client/lib/filter.js
  4. 6 0
      i18n/en.i18n.json

+ 37 - 0
client/components/sidebar/sidebarFilters.jade

@@ -72,6 +72,43 @@ template(name="filterSidebar")
               | (<span class="username">{{ username }}</span>)
             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

+ 25 - 0
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);

+ 121 - 0
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',
   ],
 

+ 6 - 0
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",