globalSearch.js 19 KB

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