filter.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. // Filtered view manager
  2. // We define local filter objects for each different type of field (SetFilter,
  3. // RangeFilter, dateFilter, etc.). We then define a global `Filter` object whose
  4. // goal is to filter complete documents by using the local filters for each
  5. // fields.
  6. function showFilterSidebar() {
  7. Sidebar.setView('filter');
  8. }
  9. // Use a "set" filter for a field that is a set of documents uniquely
  10. // identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`.
  11. class SetFilter {
  12. constructor() {
  13. this._dep = new Tracker.Dependency();
  14. this._selectedElements = [];
  15. }
  16. isSelected(val) {
  17. this._dep.depend();
  18. return this._selectedElements.indexOf(val) > -1;
  19. }
  20. add(val) {
  21. if (this._indexOfVal(val) === -1) {
  22. this._selectedElements.push(val);
  23. this._dep.changed();
  24. showFilterSidebar();
  25. }
  26. }
  27. remove(val) {
  28. const indexOfVal = this._indexOfVal(val);
  29. if (this._indexOfVal(val) !== -1) {
  30. this._selectedElements.splice(indexOfVal, 1);
  31. this._dep.changed();
  32. }
  33. }
  34. toggle(val) {
  35. if (this._indexOfVal(val) === -1) {
  36. this.add(val);
  37. } else {
  38. this.remove(val);
  39. }
  40. }
  41. reset() {
  42. this._selectedElements = [];
  43. this._dep.changed();
  44. }
  45. _indexOfVal(val) {
  46. return this._selectedElements.indexOf(val);
  47. }
  48. _isActive() {
  49. this._dep.depend();
  50. return this._selectedElements.length !== 0;
  51. }
  52. _getMongoSelector() {
  53. this._dep.depend();
  54. return { $in: this._selectedElements };
  55. }
  56. _getEmptySelector() {
  57. this._dep.depend();
  58. let includeEmpty = false;
  59. this._selectedElements.forEach((el) => {
  60. if (el === undefined) {
  61. includeEmpty = true;
  62. }
  63. });
  64. return includeEmpty ? { $eq: [] } : null;
  65. }
  66. }
  67. // The global Filter object.
  68. // XXX It would be possible to re-write this object more elegantly, and removing
  69. // the need to provide a list of `_fields`. We also should move methods into the
  70. // object prototype.
  71. Filter = {
  72. // XXX I would like to rename this field into `labels` to be consistent with
  73. // the rest of the schema, but we need to set some migrations architecture
  74. // before changing the schema.
  75. labelIds: new SetFilter(),
  76. members: new SetFilter(),
  77. customFields: new SetFilter(),
  78. _fields: ['labelIds', 'members', 'customFields'],
  79. // We don't filter cards that have been added after the last filter change. To
  80. // implement this we keep the id of these cards in this `_exceptions` fields
  81. // and use a `$or` condition in the mongo selector we return.
  82. _exceptions: [],
  83. _exceptionsDep: new Tracker.Dependency(),
  84. isActive() {
  85. return _.any(this._fields, (fieldName) => {
  86. return this[fieldName]._isActive();
  87. });
  88. },
  89. _getMongoSelector() {
  90. if (!this.isActive())
  91. return {};
  92. const filterSelector = {};
  93. const emptySelector = {};
  94. let includeEmptySelectors = false;
  95. this._fields.forEach((fieldName) => {
  96. const filter = this[fieldName];
  97. if (filter._isActive()) {
  98. if (fieldName === 'customFields'){
  99. filterSelector[fieldName] = {_id: filter._getMongoSelector()};
  100. }
  101. else
  102. {
  103. filterSelector[fieldName] = filter._getMongoSelector();
  104. }
  105. emptySelector[fieldName] = filter._getEmptySelector();
  106. if (emptySelector[fieldName] !== null) {
  107. includeEmptySelectors = true;
  108. }
  109. }
  110. });
  111. const exceptionsSelector = {_id: {$in: this._exceptions}};
  112. this._exceptionsDep.depend();
  113. if (includeEmptySelectors)
  114. return {$or: [filterSelector, exceptionsSelector, emptySelector]};
  115. else
  116. return {$or: [filterSelector, exceptionsSelector]};
  117. },
  118. mongoSelector(additionalSelector) {
  119. const filterSelector = this._getMongoSelector();
  120. if (_.isUndefined(additionalSelector))
  121. return filterSelector;
  122. else
  123. return {$and: [filterSelector, additionalSelector]};
  124. },
  125. reset() {
  126. this._fields.forEach((fieldName) => {
  127. const filter = this[fieldName];
  128. filter.reset();
  129. });
  130. this.resetExceptions();
  131. },
  132. addException(_id) {
  133. if (this.isActive()) {
  134. this._exceptions.push(_id);
  135. this._exceptionsDep.changed();
  136. Tracker.flush();
  137. }
  138. },
  139. resetExceptions() {
  140. this._exceptions = [];
  141. this._exceptionsDep.changed();
  142. },
  143. };
  144. Blaze.registerHelper('Filter', Filter);