2
0

filter.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  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. // use "subField" for searching inside object Fields.
  12. // For instance '{ 'customFields._id': ['field1','field2']} (subField would be: _id)
  13. class SetFilter {
  14. constructor(subField = '') {
  15. this._dep = new Tracker.Dependency();
  16. this._selectedElements = [];
  17. this.subField = subField;
  18. }
  19. isSelected(val) {
  20. this._dep.depend();
  21. return this._selectedElements.indexOf(val) > -1;
  22. }
  23. add(val) {
  24. if (this._indexOfVal(val) === -1) {
  25. this._selectedElements.push(val);
  26. this._dep.changed();
  27. showFilterSidebar();
  28. }
  29. }
  30. remove(val) {
  31. const indexOfVal = this._indexOfVal(val);
  32. if (this._indexOfVal(val) !== -1) {
  33. this._selectedElements.splice(indexOfVal, 1);
  34. this._dep.changed();
  35. }
  36. }
  37. toggle(val) {
  38. if (this._indexOfVal(val) === -1) {
  39. this.add(val);
  40. } else {
  41. this.remove(val);
  42. }
  43. }
  44. reset() {
  45. this._selectedElements = [];
  46. this._dep.changed();
  47. }
  48. _indexOfVal(val) {
  49. return this._selectedElements.indexOf(val);
  50. }
  51. _isActive() {
  52. this._dep.depend();
  53. return this._selectedElements.length !== 0;
  54. }
  55. _getMongoSelector() {
  56. this._dep.depend();
  57. return {
  58. $in: this._selectedElements,
  59. };
  60. }
  61. _getEmptySelector() {
  62. this._dep.depend();
  63. let includeEmpty = false;
  64. this._selectedElements.forEach((el) => {
  65. if (el === undefined) {
  66. includeEmpty = true;
  67. }
  68. });
  69. return includeEmpty ? {
  70. $eq: [],
  71. } : null;
  72. }
  73. }
  74. // Advanced filter forms a MongoSelector from a users String.
  75. // Build by: Ignatz 19.05.2018 (github feuerball11)
  76. class AdvancedFilter {
  77. constructor() {
  78. this._dep = new Tracker.Dependency();
  79. this._filter = '';
  80. this._lastValide = {};
  81. }
  82. set(str) {
  83. this._filter = str;
  84. this._dep.changed();
  85. }
  86. reset() {
  87. this._filter = '';
  88. this._lastValide = {};
  89. this._dep.changed();
  90. }
  91. _isActive() {
  92. this._dep.depend();
  93. return this._filter !== '';
  94. }
  95. _filterToCommands() {
  96. const commands = [];
  97. let current = '';
  98. let string = false;
  99. let regex = false;
  100. let wasString = false;
  101. let ignore = false;
  102. for (let i = 0; i < this._filter.length; i++) {
  103. const char = this._filter.charAt(i);
  104. if (ignore) {
  105. ignore = false;
  106. current += char;
  107. continue;
  108. }
  109. if (char === '/') {
  110. string = !string;
  111. if (string) regex = true;
  112. current += char;
  113. continue;
  114. }
  115. if (char === '\'') {
  116. string = !string;
  117. if (string) wasString = true;
  118. continue;
  119. }
  120. if (char === '\\' && !string) {
  121. ignore = true;
  122. continue;
  123. }
  124. if (char === ' ' && !string) {
  125. commands.push({
  126. 'cmd': current,
  127. 'string': wasString,
  128. regex,
  129. });
  130. wasString = false;
  131. current = '';
  132. continue;
  133. }
  134. current += char;
  135. }
  136. if (current !== '') {
  137. commands.push({
  138. 'cmd': current,
  139. 'string': wasString,
  140. regex,
  141. });
  142. }
  143. return commands;
  144. }
  145. _fieldNameToId(field) {
  146. const found = CustomFields.findOne({
  147. 'name': field,
  148. });
  149. return found._id;
  150. }
  151. _fieldValueToId(field, value) {
  152. const found = CustomFields.findOne({
  153. 'name': field,
  154. });
  155. if (found.settings.dropdownItems && found.settings.dropdownItems.length > 0) {
  156. for (let i = 0; i < found.settings.dropdownItems.length; i++) {
  157. if (found.settings.dropdownItems[i].name === value) {
  158. return found.settings.dropdownItems[i]._id;
  159. }
  160. }
  161. }
  162. return value;
  163. }
  164. _arrayToSelector(commands) {
  165. try {
  166. //let changed = false;
  167. this._processSubCommands(commands);
  168. } catch (e) {
  169. return this._lastValide;
  170. }
  171. this._lastValide = {
  172. $or: commands,
  173. };
  174. return {
  175. $or: commands,
  176. };
  177. }
  178. _processSubCommands(commands) {
  179. const subcommands = [];
  180. let level = 0;
  181. let start = -1;
  182. for (let i = 0; i < commands.length; i++) {
  183. if (commands[i].cmd) {
  184. switch (commands[i].cmd) {
  185. case '(':
  186. {
  187. level++;
  188. if (start === -1) start = i;
  189. continue;
  190. }
  191. case ')':
  192. {
  193. level--;
  194. commands.splice(i, 1);
  195. i--;
  196. continue;
  197. }
  198. default:
  199. {
  200. if (level > 0) {
  201. subcommands.push(commands[i]);
  202. commands.splice(i, 1);
  203. i--;
  204. continue;
  205. }
  206. }
  207. }
  208. }
  209. }
  210. if (start !== -1) {
  211. this._processSubCommands(subcommands);
  212. if (subcommands.length === 1)
  213. commands.splice(start, 0, subcommands[0]);
  214. else
  215. commands.splice(start, 0, subcommands);
  216. }
  217. this._processConditions(commands);
  218. this._processLogicalOperators(commands);
  219. }
  220. _processConditions(commands) {
  221. for (let i = 0; i < commands.length; i++) {
  222. if (!commands[i].string && commands[i].cmd) {
  223. switch (commands[i].cmd) {
  224. case '=':
  225. case '==':
  226. case '===':
  227. {
  228. const field = commands[i - 1].cmd;
  229. const str = commands[i + 1].cmd;
  230. if (commands[i + 1].regex) {
  231. const match = str.match(new RegExp('^/(.*?)/([gimy]*)$'));
  232. let regex = null;
  233. if (match.length > 2)
  234. regex = new RegExp(match[1], match[2]);
  235. else
  236. regex = new RegExp(match[1]);
  237. commands[i] = {
  238. 'customFields._id': this._fieldNameToId(field),
  239. 'customFields.value': regex,
  240. };
  241. } else {
  242. commands[i] = {
  243. 'customFields._id': this._fieldNameToId(field),
  244. 'customFields.value': {
  245. $in: [this._fieldValueToId(field, str), parseInt(str, 10)],
  246. },
  247. };
  248. }
  249. commands.splice(i - 1, 1);
  250. commands.splice(i, 1);
  251. //changed = true;
  252. i--;
  253. break;
  254. }
  255. case '!=':
  256. case '!==':
  257. {
  258. const field = commands[i - 1].cmd;
  259. const str = commands[i + 1].cmd;
  260. if (commands[i + 1].regex) {
  261. const match = str.match(new RegExp('^/(.*?)/([gimy]*)$'));
  262. let regex = null;
  263. if (match.length > 2)
  264. regex = new RegExp(match[1], match[2]);
  265. else
  266. regex = new RegExp(match[1]);
  267. commands[i] = {
  268. 'customFields._id': this._fieldNameToId(field),
  269. 'customFields.value': {
  270. $not: regex,
  271. },
  272. };
  273. } else {
  274. commands[i] = {
  275. 'customFields._id': this._fieldNameToId(field),
  276. 'customFields.value': {
  277. $not: {
  278. $in: [this._fieldValueToId(field, str), parseInt(str, 10)],
  279. },
  280. },
  281. };
  282. }
  283. commands.splice(i - 1, 1);
  284. commands.splice(i, 1);
  285. //changed = true;
  286. i--;
  287. break;
  288. }
  289. case '>':
  290. case 'gt':
  291. case 'Gt':
  292. case 'GT':
  293. {
  294. const field = commands[i - 1].cmd;
  295. const str = commands[i + 1].cmd;
  296. commands[i] = {
  297. 'customFields._id': this._fieldNameToId(field),
  298. 'customFields.value': {
  299. $gt: parseInt(str, 10),
  300. },
  301. };
  302. commands.splice(i - 1, 1);
  303. commands.splice(i, 1);
  304. //changed = true;
  305. i--;
  306. break;
  307. }
  308. case '>=':
  309. case '>==':
  310. case 'gte':
  311. case 'Gte':
  312. case 'GTE':
  313. {
  314. const field = commands[i - 1].cmd;
  315. const str = commands[i + 1].cmd;
  316. commands[i] = {
  317. 'customFields._id': this._fieldNameToId(field),
  318. 'customFields.value': {
  319. $gte: parseInt(str, 10),
  320. },
  321. };
  322. commands.splice(i - 1, 1);
  323. commands.splice(i, 1);
  324. //changed = true;
  325. i--;
  326. break;
  327. }
  328. case '<':
  329. case 'lt':
  330. case 'Lt':
  331. case 'LT':
  332. {
  333. const field = commands[i - 1].cmd;
  334. const str = commands[i + 1].cmd;
  335. commands[i] = {
  336. 'customFields._id': this._fieldNameToId(field),
  337. 'customFields.value': {
  338. $lt: parseInt(str, 10),
  339. },
  340. };
  341. commands.splice(i - 1, 1);
  342. commands.splice(i, 1);
  343. //changed = true;
  344. i--;
  345. break;
  346. }
  347. case '<=':
  348. case '<==':
  349. case 'lte':
  350. case 'Lte':
  351. case 'LTE':
  352. {
  353. const field = commands[i - 1].cmd;
  354. const str = commands[i + 1].cmd;
  355. commands[i] = {
  356. 'customFields._id': this._fieldNameToId(field),
  357. 'customFields.value': {
  358. $lte: parseInt(str, 10),
  359. },
  360. };
  361. commands.splice(i - 1, 1);
  362. commands.splice(i, 1);
  363. //changed = true;
  364. i--;
  365. break;
  366. }
  367. }
  368. }
  369. }
  370. }
  371. _processLogicalOperators(commands) {
  372. for (let i = 0; i < commands.length; i++) {
  373. if (!commands[i].string && commands[i].cmd) {
  374. switch (commands[i].cmd) {
  375. case 'or':
  376. case 'Or':
  377. case 'OR':
  378. case '|':
  379. case '||':
  380. {
  381. const op1 = commands[i - 1];
  382. const op2 = commands[i + 1];
  383. commands[i] = {
  384. $or: [op1, op2],
  385. };
  386. commands.splice(i - 1, 1);
  387. commands.splice(i, 1);
  388. //changed = true;
  389. i--;
  390. break;
  391. }
  392. case 'and':
  393. case 'And':
  394. case 'AND':
  395. case '&':
  396. case '&&':
  397. {
  398. const op1 = commands[i - 1];
  399. const op2 = commands[i + 1];
  400. commands[i] = {
  401. $and: [op1, op2],
  402. };
  403. commands.splice(i - 1, 1);
  404. commands.splice(i, 1);
  405. //changed = true;
  406. i--;
  407. break;
  408. }
  409. case 'not':
  410. case 'Not':
  411. case 'NOT':
  412. case '!':
  413. {
  414. const op1 = commands[i + 1];
  415. commands[i] = {
  416. $not: op1,
  417. };
  418. commands.splice(i + 1, 1);
  419. //changed = true;
  420. i--;
  421. break;
  422. }
  423. }
  424. }
  425. }
  426. }
  427. _getMongoSelector() {
  428. this._dep.depend();
  429. const commands = this._filterToCommands();
  430. return this._arrayToSelector(commands);
  431. }
  432. }
  433. // The global Filter object.
  434. // XXX It would be possible to re-write this object more elegantly, and removing
  435. // the need to provide a list of `_fields`. We also should move methods into the
  436. // object prototype.
  437. Filter = {
  438. // XXX I would like to rename this field into `labels` to be consistent with
  439. // the rest of the schema, but we need to set some migrations architecture
  440. // before changing the schema.
  441. labelIds: new SetFilter(),
  442. members: new SetFilter(),
  443. customFields: new SetFilter('_id'),
  444. advanced: new AdvancedFilter(),
  445. _fields: ['labelIds', 'members', 'customFields'],
  446. // We don't filter cards that have been added after the last filter change. To
  447. // implement this we keep the id of these cards in this `_exceptions` fields
  448. // and use a `$or` condition in the mongo selector we return.
  449. _exceptions: [],
  450. _exceptionsDep: new Tracker.Dependency(),
  451. isActive() {
  452. return _.any(this._fields, (fieldName) => {
  453. return this[fieldName]._isActive();
  454. }) || this.advanced._isActive();
  455. },
  456. _getMongoSelector() {
  457. if (!this.isActive())
  458. return {};
  459. const filterSelector = {};
  460. const emptySelector = {};
  461. let includeEmptySelectors = false;
  462. this._fields.forEach((fieldName) => {
  463. const filter = this[fieldName];
  464. if (filter._isActive()) {
  465. if (filter.subField !== '') {
  466. filterSelector[`${fieldName}.${filter.subField}`] = filter._getMongoSelector();
  467. } else {
  468. filterSelector[fieldName] = filter._getMongoSelector();
  469. }
  470. emptySelector[fieldName] = filter._getEmptySelector();
  471. if (emptySelector[fieldName] !== null) {
  472. includeEmptySelectors = true;
  473. }
  474. }
  475. });
  476. const exceptionsSelector = {
  477. _id: {
  478. $in: this._exceptions,
  479. },
  480. };
  481. this._exceptionsDep.depend();
  482. const selectors = [exceptionsSelector];
  483. if (_.any(this._fields, (fieldName) => {
  484. return this[fieldName]._isActive();
  485. })) selectors.push(filterSelector);
  486. if (includeEmptySelectors) selectors.push(emptySelector);
  487. if (this.advanced._isActive()) selectors.push(this.advanced._getMongoSelector());
  488. return {
  489. $or: selectors,
  490. };
  491. },
  492. mongoSelector(additionalSelector) {
  493. const filterSelector = this._getMongoSelector();
  494. if (_.isUndefined(additionalSelector))
  495. return filterSelector;
  496. else
  497. return {
  498. $and: [filterSelector, additionalSelector],
  499. };
  500. },
  501. reset() {
  502. this._fields.forEach((fieldName) => {
  503. const filter = this[fieldName];
  504. filter.reset();
  505. });
  506. this.advanced.reset();
  507. this.resetExceptions();
  508. },
  509. addException(_id) {
  510. if (this.isActive()) {
  511. this._exceptions.push(_id);
  512. this._exceptionsDep.changed();
  513. Tracker.flush();
  514. }
  515. },
  516. resetExceptions() {
  517. this._exceptions = [];
  518. this._exceptionsDep.changed();
  519. },
  520. };
  521. Blaze.registerHelper('Filter', Filter);