globalSearch.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. import { CardSearchPagedComponent } from '../../lib/cardSearch';
  2. import moment from 'moment';
  3. // const subManager = new SubsManager();
  4. BlazeComponent.extendComponent({
  5. events() {
  6. return [
  7. {
  8. 'click .js-due-cards-view-change': Popup.open('globalSearchViewChange'),
  9. },
  10. ];
  11. },
  12. }).register('globalSearchHeaderBar');
  13. Template.globalSearch.helpers({
  14. userId() {
  15. return Meteor.userId();
  16. },
  17. });
  18. BlazeComponent.extendComponent({
  19. events() {
  20. return [
  21. {
  22. 'click .js-due-cards-view-me'() {
  23. Utils.setDueCardsView('me');
  24. Popup.close();
  25. },
  26. 'click .js-due-cards-view-all'() {
  27. Utils.setDueCardsView('all');
  28. Popup.close();
  29. },
  30. },
  31. ];
  32. },
  33. }).register('globalSearchViewChangePopup');
  34. class GlobalSearchComponent extends CardSearchPagedComponent {
  35. onCreated() {
  36. super.onCreated();
  37. this.myLists = new ReactiveVar([]);
  38. this.myLabelNames = new ReactiveVar([]);
  39. this.myBoardNames = new ReactiveVar([]);
  40. this.parsingErrors = [];
  41. this.colorMap = null;
  42. this.queryParams = null;
  43. Meteor.call('myLists', (err, data) => {
  44. if (!err) {
  45. this.myLists.set(data);
  46. }
  47. });
  48. Meteor.call('myLabelNames', (err, data) => {
  49. if (!err) {
  50. this.myLabelNames.set(data);
  51. }
  52. });
  53. Meteor.call('myBoardNames', (err, data) => {
  54. if (!err) {
  55. this.myBoardNames.set(data);
  56. }
  57. });
  58. }
  59. onRendered() {
  60. Meteor.subscribe('setting');
  61. // eslint-disable-next-line no-console
  62. //console.log('lang:', TAPi18n.getLanguage());
  63. this.colorMap = Boards.colorMap();
  64. // eslint-disable-next-line no-console
  65. // console.log('colorMap:', this.colorMap);
  66. if (Session.get('globalQuery')) {
  67. this.searchAllBoards(Session.get('globalQuery'));
  68. }
  69. }
  70. resetSearch() {
  71. super.resetSearch();
  72. this.parsingErrors = [];
  73. }
  74. errorMessages() {
  75. if (this.parsingErrors.length) {
  76. return this.parsingErrorMessages();
  77. }
  78. return this.queryErrorMessages();
  79. }
  80. parsingErrorMessages() {
  81. const messages = [];
  82. if (this.parsingErrors.length) {
  83. this.parsingErrors.forEach(err => {
  84. messages.push(TAPi18n.__(err.tag, err.value));
  85. });
  86. }
  87. return messages;
  88. }
  89. searchAllBoards(query) {
  90. query = query.trim();
  91. // eslint-disable-next-line no-console
  92. //console.log('query:', query);
  93. this.query.set(query);
  94. this.resetSearch();
  95. if (!query) {
  96. return;
  97. }
  98. this.searching.set(true);
  99. const reOperator1 = new RegExp(
  100. '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<value>[\\p{Letter}\\p{Mark}]+)(\\s+|$)',
  101. 'iu',
  102. );
  103. const reOperator2 = new RegExp(
  104. '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<quote>["\']*)(?<value>.*?)\\k<quote>(\\s+|$)',
  105. 'iu',
  106. );
  107. const reText = new RegExp('^(?<text>\\S+)(\\s+|$)', 'u');
  108. const reQuotedText = new RegExp(
  109. '^(?<quote>["\'])(?<text>.*?)\\k<quote>(\\s+|$)',
  110. 'u',
  111. );
  112. const reNegatedOperator = new RegExp('^-(?<operator>.*)$');
  113. const operators = {
  114. 'operator-board': 'boards',
  115. 'operator-board-abbrev': 'boards',
  116. 'operator-swimlane': 'swimlanes',
  117. 'operator-swimlane-abbrev': 'swimlanes',
  118. 'operator-list': 'lists',
  119. 'operator-list-abbrev': 'lists',
  120. 'operator-label': 'labels',
  121. 'operator-label-abbrev': 'labels',
  122. 'operator-user': 'users',
  123. 'operator-user-abbrev': 'users',
  124. 'operator-member': 'members',
  125. 'operator-member-abbrev': 'members',
  126. 'operator-assignee': 'assignees',
  127. 'operator-assignee-abbrev': 'assignees',
  128. 'operator-status': 'status',
  129. 'operator-due': 'dueAt',
  130. 'operator-created': 'createdAt',
  131. 'operator-modified': 'modifiedAt',
  132. 'operator-comment': 'comments',
  133. 'operator-has': 'has',
  134. 'operator-sort': 'sort',
  135. 'operator-limit': 'limit',
  136. };
  137. const predicates = {
  138. due: {
  139. 'predicate-overdue': 'overdue',
  140. },
  141. durations: {
  142. 'predicate-week': 'week',
  143. 'predicate-month': 'month',
  144. 'predicate-quarter': 'quarter',
  145. 'predicate-year': 'year',
  146. },
  147. status: {
  148. 'predicate-archived': 'archived',
  149. 'predicate-all': 'all',
  150. 'predicate-open': 'open',
  151. 'predicate-ended': 'ended',
  152. 'predicate-public': 'public',
  153. 'predicate-private': 'private',
  154. },
  155. sorts: {
  156. 'predicate-due': 'dueAt',
  157. 'predicate-created': 'createdAt',
  158. 'predicate-modified': 'modifiedAt',
  159. },
  160. has: {
  161. 'predicate-description': 'description',
  162. 'predicate-checklist': 'checklist',
  163. 'predicate-attachment': 'attachment',
  164. 'predicate-start': 'startAt',
  165. 'predicate-end': 'endAt',
  166. 'predicate-due': 'dueAt',
  167. 'predicate-assignee': 'assignees',
  168. 'predicate-member': 'members',
  169. },
  170. };
  171. const predicateTranslations = {};
  172. Object.entries(predicates).forEach(([category, catPreds]) => {
  173. predicateTranslations[category] = {};
  174. Object.entries(catPreds).forEach(([tag, value]) => {
  175. predicateTranslations[category][TAPi18n.__(tag)] = value;
  176. });
  177. });
  178. // eslint-disable-next-line no-console
  179. // console.log('predicateTranslations:', predicateTranslations);
  180. const operatorMap = {};
  181. Object.entries(operators).forEach(([key, value]) => {
  182. operatorMap[TAPi18n.__(key).toLowerCase()] = value;
  183. });
  184. // eslint-disable-next-line no-console
  185. // console.log('operatorMap:', operatorMap);
  186. const params = {
  187. limit: this.resultsPerPage,
  188. boards: [],
  189. swimlanes: [],
  190. lists: [],
  191. users: [],
  192. members: [],
  193. assignees: [],
  194. labels: [],
  195. status: [],
  196. dueAt: null,
  197. createdAt: null,
  198. modifiedAt: null,
  199. comments: [],
  200. has: [],
  201. };
  202. let text = '';
  203. while (query) {
  204. let m = query.match(reOperator1);
  205. if (!m) {
  206. m = query.match(reOperator2);
  207. if (m) {
  208. query = query.replace(reOperator2, '');
  209. }
  210. } else {
  211. query = query.replace(reOperator1, '');
  212. }
  213. if (m) {
  214. let op;
  215. if (m.groups.operator) {
  216. op = m.groups.operator.toLowerCase();
  217. } else {
  218. op = m.groups.abbrev.toLowerCase();
  219. }
  220. // eslint-disable-next-line no-prototype-builtins
  221. if (operatorMap.hasOwnProperty(op)) {
  222. const operator = operatorMap[op];
  223. let value = m.groups.value;
  224. if (operator === 'labels') {
  225. if (value in this.colorMap) {
  226. value = this.colorMap[value];
  227. // console.log('found color:', value);
  228. }
  229. } else if (['dueAt', 'createdAt', 'modifiedAt'].includes(operator)) {
  230. const days = parseInt(value, 10);
  231. let duration = null;
  232. if (isNaN(days)) {
  233. // duration was specified as text
  234. if (predicateTranslations.durations[value]) {
  235. duration = predicateTranslations.durations[value];
  236. let date = null;
  237. switch (duration) {
  238. case 'week':
  239. // eslint-disable-next-line no-case-declarations
  240. const week = moment().week();
  241. if (week === 52) {
  242. date = moment(1, 'W');
  243. date.set('year', date.year() + 1);
  244. } else {
  245. date = moment(week + 1, 'W');
  246. }
  247. break;
  248. case 'month':
  249. // eslint-disable-next-line no-case-declarations
  250. const month = moment().month();
  251. // .month() is zero indexed
  252. if (month === 11) {
  253. date = moment(1, 'M');
  254. date.set('year', date.year() + 1);
  255. } else {
  256. date = moment(month + 2, 'M');
  257. }
  258. break;
  259. case 'quarter':
  260. // eslint-disable-next-line no-case-declarations
  261. const quarter = moment().quarter();
  262. if (quarter === 4) {
  263. date = moment(1, 'Q');
  264. date.set('year', date.year() + 1);
  265. } else {
  266. date = moment(quarter + 1, 'Q');
  267. }
  268. break;
  269. case 'year':
  270. date = moment(moment().year() + 1, 'YYYY');
  271. break;
  272. }
  273. if (date) {
  274. value = {
  275. operator: '$lt',
  276. value: date.format('YYYY-MM-DD'),
  277. };
  278. }
  279. } else if (operator === 'dueAt' && value === 'overdue') {
  280. value = {
  281. operator: '$lt',
  282. value: moment().format('YYYY-MM-DD'),
  283. };
  284. } else {
  285. this.parsingErrors.push({
  286. tag: 'operator-number-expected',
  287. value: { operator: op, value },
  288. });
  289. value = null;
  290. }
  291. } else if (operator === 'dueAt') {
  292. value = {
  293. operator: '$lt',
  294. value: moment(moment().format('YYYY-MM-DD'))
  295. .add(days + 1, duration ? duration : 'days')
  296. .format(),
  297. };
  298. } else {
  299. value = {
  300. operator: '$gte',
  301. value: moment(moment().format('YYYY-MM-DD'))
  302. .subtract(days, duration ? duration : 'days')
  303. .format(),
  304. };
  305. }
  306. } else if (operator === 'sort') {
  307. let negated = false;
  308. const m = value.match(reNegatedOperator);
  309. if (m) {
  310. value = m.groups.operator;
  311. negated = true;
  312. }
  313. if (!predicateTranslations.sorts[value]) {
  314. this.parsingErrors.push({
  315. tag: 'operator-sort-invalid',
  316. value,
  317. });
  318. } else {
  319. value = {
  320. name: predicateTranslations.sorts[value],
  321. order: negated ? 'des' : 'asc',
  322. };
  323. }
  324. } else if (operator === 'status') {
  325. if (!predicateTranslations.status[value]) {
  326. this.parsingErrors.push({
  327. tag: 'operator-status-invalid',
  328. value,
  329. });
  330. } else {
  331. value = predicateTranslations.status[value];
  332. }
  333. } else if (operator === 'has') {
  334. let negated = false;
  335. const m = value.match(reNegatedOperator);
  336. if (m) {
  337. value = m.groups.operator;
  338. negated = true;
  339. }
  340. if (!predicateTranslations.has[value]) {
  341. this.parsingErrors.push({
  342. tag: 'operator-has-invalid',
  343. value,
  344. });
  345. } else {
  346. value = {
  347. field: predicateTranslations.has[value],
  348. exists: !negated,
  349. };
  350. }
  351. } else if (operator === 'limit') {
  352. const limit = parseInt(value, 10);
  353. if (isNaN(limit) || limit < 1) {
  354. this.parsingErrors.push({
  355. tag: 'operator-limit-invalid',
  356. value,
  357. });
  358. } else {
  359. value = limit;
  360. }
  361. }
  362. if (Array.isArray(params[operator])) {
  363. params[operator].push(value);
  364. } else {
  365. params[operator] = value;
  366. }
  367. } else {
  368. this.parsingErrors.push({
  369. tag: 'operator-unknown-error',
  370. value: op,
  371. });
  372. }
  373. continue;
  374. }
  375. m = query.match(reQuotedText);
  376. if (!m) {
  377. m = query.match(reText);
  378. if (m) {
  379. query = query.replace(reText, '');
  380. }
  381. } else {
  382. query = query.replace(reQuotedText, '');
  383. }
  384. if (m) {
  385. text += (text ? ' ' : '') + m.groups.text;
  386. }
  387. }
  388. // eslint-disable-next-line no-console
  389. // console.log('text:', text);
  390. params.text = text;
  391. // eslint-disable-next-line no-console
  392. console.log('params:', params);
  393. this.queryParams = params;
  394. if (this.parsingErrors.length) {
  395. this.searching.set(false);
  396. this.queryErrors = this.parsingErrorMessages();
  397. this.hasResults.set(true);
  398. this.hasQueryErrors.set(true);
  399. return;
  400. }
  401. this.runGlobalSearch(params);
  402. }
  403. searchInstructions() {
  404. const tags = {
  405. operator_board: TAPi18n.__('operator-board'),
  406. operator_list: TAPi18n.__('operator-list'),
  407. operator_swimlane: TAPi18n.__('operator-swimlane'),
  408. operator_comment: TAPi18n.__('operator-comment'),
  409. operator_label: TAPi18n.__('operator-label'),
  410. operator_label_abbrev: TAPi18n.__('operator-label-abbrev'),
  411. operator_user: TAPi18n.__('operator-user'),
  412. operator_user_abbrev: TAPi18n.__('operator-user-abbrev'),
  413. operator_member: TAPi18n.__('operator-member'),
  414. operator_member_abbrev: TAPi18n.__('operator-member-abbrev'),
  415. operator_assignee: TAPi18n.__('operator-assignee'),
  416. operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'),
  417. operator_due: TAPi18n.__('operator-due'),
  418. operator_created: TAPi18n.__('operator-created'),
  419. operator_modified: TAPi18n.__('operator-modified'),
  420. operator_status: TAPi18n.__('operator-status'),
  421. operator_has: TAPi18n.__('operator-has'),
  422. operator_sort: TAPi18n.__('operator-sort'),
  423. operator_limit: TAPi18n.__('operator-limit'),
  424. predicate_overdue: TAPi18n.__('predicate-overdue'),
  425. predicate_archived: TAPi18n.__('predicate-archived'),
  426. predicate_all: TAPi18n.__('predicate-all'),
  427. predicate_ended: TAPi18n.__('predicate-ended'),
  428. predicate_week: TAPi18n.__('predicate-week'),
  429. predicate_month: TAPi18n.__('predicate-month'),
  430. predicate_quarter: TAPi18n.__('predicate-quarter'),
  431. predicate_year: TAPi18n.__('predicate-year'),
  432. predicate_attachment: TAPi18n.__('predicate-attachment'),
  433. predicate_description: TAPi18n.__('predicate-description'),
  434. predicate_checklist: TAPi18n.__('predicate-checklist'),
  435. predicate_public: TAPi18n.__('predicate-public'),
  436. predicate_private: TAPi18n.__('predicate-private'),
  437. predicate_due: TAPi18n.__('predicate-due'),
  438. predicate_created: TAPi18n.__('predicate-created'),
  439. predicate_modified: TAPi18n.__('predicate-modified'),
  440. predicate_start: TAPi18n.__('predicate-start'),
  441. predicate_end: TAPi18n.__('predicate-end'),
  442. predicate_assignee: TAPi18n.__('predicate-assignee'),
  443. predicate_member: TAPi18n.__('predicate-member'),
  444. };
  445. let text = '';
  446. [
  447. ['# ', 'globalSearch-instructions-heading'],
  448. ['\n', 'globalSearch-instructions-description'],
  449. ['\n\n', 'globalSearch-instructions-operators'],
  450. ['\n* ', 'globalSearch-instructions-operator-board'],
  451. ['\n* ', 'globalSearch-instructions-operator-list'],
  452. ['\n* ', 'globalSearch-instructions-operator-swimlane'],
  453. ['\n* ', 'globalSearch-instructions-operator-comment'],
  454. ['\n* ', 'globalSearch-instructions-operator-label'],
  455. ['\n* ', 'globalSearch-instructions-operator-hash'],
  456. ['\n* ', 'globalSearch-instructions-operator-user'],
  457. ['\n* ', 'globalSearch-instructions-operator-at'],
  458. ['\n* ', 'globalSearch-instructions-operator-member'],
  459. ['\n* ', 'globalSearch-instructions-operator-assignee'],
  460. ['\n* ', 'globalSearch-instructions-operator-due'],
  461. ['\n* ', 'globalSearch-instructions-operator-created'],
  462. ['\n* ', 'globalSearch-instructions-operator-modified'],
  463. ['\n* ', 'globalSearch-instructions-operator-status'],
  464. ['\n * ', 'globalSearch-instructions-status-archived'],
  465. ['\n * ', 'globalSearch-instructions-status-public'],
  466. ['\n * ', 'globalSearch-instructions-status-private'],
  467. ['\n * ', 'globalSearch-instructions-status-all'],
  468. ['\n * ', 'globalSearch-instructions-status-ended'],
  469. ['\n* ', 'globalSearch-instructions-operator-has'],
  470. ['\n* ', 'globalSearch-instructions-operator-sort'],
  471. ['\n* ', 'globalSearch-instructions-operator-limit'],
  472. ['\n## ', 'heading-notes'],
  473. ['\n* ', 'globalSearch-instructions-notes-1'],
  474. ['\n* ', 'globalSearch-instructions-notes-2'],
  475. ['\n* ', 'globalSearch-instructions-notes-3'],
  476. ['\n* ', 'globalSearch-instructions-notes-3-2'],
  477. ['\n* ', 'globalSearch-instructions-notes-4'],
  478. ['\n* ', 'globalSearch-instructions-notes-5'],
  479. ].forEach(([prefix, instruction]) => {
  480. text += `${prefix}${TAPi18n.__(instruction, tags)}`;
  481. });
  482. return text;
  483. }
  484. labelColors() {
  485. return Boards.simpleSchema()._schema['labels.$.color'].allowedValues.map(
  486. color => {
  487. return { color, name: TAPi18n.__(`color-${color}`) };
  488. },
  489. );
  490. }
  491. events() {
  492. return [
  493. {
  494. ...super.events()[0],
  495. 'submit .js-search-query-form'(evt) {
  496. evt.preventDefault();
  497. this.searchAllBoards(evt.target.searchQuery.value);
  498. },
  499. 'click .js-label-color'(evt) {
  500. evt.preventDefault();
  501. const input = document.getElementById('global-search-input');
  502. this.query.set(
  503. `${input.value} ${TAPi18n.__('operator-label')}:"${
  504. evt.currentTarget.textContent
  505. }"`,
  506. );
  507. document.getElementById('global-search-input').focus();
  508. },
  509. 'click .js-board-title'(evt) {
  510. evt.preventDefault();
  511. const input = document.getElementById('global-search-input');
  512. this.query.set(
  513. `${input.value} ${TAPi18n.__('operator-board')}:"${
  514. evt.currentTarget.textContent
  515. }"`,
  516. );
  517. document.getElementById('global-search-input').focus();
  518. },
  519. 'click .js-list-title'(evt) {
  520. evt.preventDefault();
  521. const input = document.getElementById('global-search-input');
  522. this.query.set(
  523. `${input.value} ${TAPi18n.__('operator-list')}:"${
  524. evt.currentTarget.textContent
  525. }"`,
  526. );
  527. document.getElementById('global-search-input').focus();
  528. },
  529. 'click .js-label-name'(evt) {
  530. evt.preventDefault();
  531. const input = document.getElementById('global-search-input');
  532. this.query.set(
  533. `${input.value} ${TAPi18n.__('operator-label')}:"${
  534. evt.currentTarget.textContent
  535. }"`,
  536. );
  537. document.getElementById('global-search-input').focus();
  538. },
  539. },
  540. ];
  541. }
  542. }
  543. GlobalSearchComponent.register('globalSearch');