globalSearch.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. const subManager = new SubsManager();
  2. BlazeComponent.extendComponent({
  3. events() {
  4. return [
  5. {
  6. 'click .js-due-cards-view-change': Popup.open('globalSearchViewChange'),
  7. },
  8. ];
  9. },
  10. }).register('globalSearchHeaderBar');
  11. Template.globalSearch.helpers({
  12. userId() {
  13. return Meteor.userId();
  14. },
  15. });
  16. BlazeComponent.extendComponent({
  17. events() {
  18. return [
  19. {
  20. 'click .js-due-cards-view-me'() {
  21. Utils.setDueCardsView('me');
  22. Popup.close();
  23. },
  24. 'click .js-due-cards-view-all'() {
  25. Utils.setDueCardsView('all');
  26. Popup.close();
  27. },
  28. },
  29. ];
  30. },
  31. }).register('globalSearchViewChangePopup');
  32. BlazeComponent.extendComponent({
  33. onCreated() {
  34. this.searching = new ReactiveVar(false);
  35. this.hasResults = new ReactiveVar(false);
  36. this.hasQueryErrors = new ReactiveVar(false);
  37. this.query = new ReactiveVar('');
  38. this.resultsHeading = new ReactiveVar('');
  39. this.queryParams = null;
  40. this.parsingErrors = [];
  41. this.resultsCount = 0;
  42. this.totalHits = 0;
  43. this.queryErrors = null;
  44. Meteor.subscribe('setting');
  45. if (Session.get('globalQuery')) {
  46. // eslint-disable-next-line no-console
  47. // console.log(Session.get('globalQuery'));
  48. this.searchAllBoards(Session.get('globalQuery'));
  49. }
  50. },
  51. resetSearch() {
  52. this.searching.set(false);
  53. this.hasResults.set(false);
  54. this.hasQueryErrors.set(false);
  55. this.resultsHeading.set('');
  56. this.parsingErrors = [];
  57. this.resultsCount = 0;
  58. this.totalHits = 0;
  59. this.queryErrors = null;
  60. },
  61. results() {
  62. // eslint-disable-next-line no-console
  63. console.log('getting results');
  64. if (this.queryParams) {
  65. const results = Cards.globalSearch(this.queryParams);
  66. this.queryErrors = results.errors;
  67. // eslint-disable-next-line no-console
  68. console.log('errors:', this.queryErrors);
  69. if (this.errorMessages().length) {
  70. this.hasQueryErrors.set(true);
  71. return null;
  72. }
  73. if (results.cards) {
  74. const sessionData = SessionData.findOne({ userId: Meteor.userId() });
  75. this.totalHits = sessionData.totalHits;
  76. this.resultsCount = results.cards.count();
  77. this.resultsHeading.set(this.getResultsHeading());
  78. return results.cards;
  79. }
  80. }
  81. this.resultsCount = 0;
  82. return [];
  83. },
  84. errorMessages() {
  85. const messages = [];
  86. if (this.queryErrors) {
  87. this.queryErrors.notFound.boards.forEach(board => {
  88. messages.push({ tag: 'board-title-not-found', value: board });
  89. });
  90. this.queryErrors.notFound.swimlanes.forEach(swim => {
  91. messages.push({ tag: 'swimlane-title-not-found', value: swim });
  92. });
  93. this.queryErrors.notFound.lists.forEach(list => {
  94. messages.push({ tag: 'list-title-not-found', value: list });
  95. });
  96. this.queryErrors.notFound.labels.forEach(label => {
  97. messages.push({ tag: 'label-not-found', value: label });
  98. });
  99. this.queryErrors.notFound.users.forEach(user => {
  100. messages.push({ tag: 'user-username-not-found', value: user });
  101. });
  102. this.queryErrors.notFound.members.forEach(user => {
  103. messages.push({ tag: 'user-username-not-found', value: user });
  104. });
  105. this.queryErrors.notFound.assignees.forEach(user => {
  106. messages.push({ tag: 'user-username-not-found', value: user });
  107. });
  108. }
  109. if (this.parsingErrors.length) {
  110. this.parsingErrors.forEach(err => {
  111. messages.push(err);
  112. });
  113. }
  114. return messages;
  115. },
  116. searchAllBoards(query) {
  117. this.query.set(query);
  118. this.resetSearch();
  119. if (!query) {
  120. return;
  121. }
  122. this.searching.set(true);
  123. // eslint-disable-next-line no-console
  124. // console.log('query:', query);
  125. const reOperator1 = /^((?<operator>\w+):|(?<abbrev>[#@]))(?<value>\w+)(\s+|$)/;
  126. const reOperator2 = /^((?<operator>\w+):|(?<abbrev>[#@]))(?<quote>["']*)(?<value>.*?)\k<quote>(\s+|$)/;
  127. const reText = /^(?<text>\S+)(\s+|$)/;
  128. const reQuotedText = /^(?<quote>["'])(?<text>\w+)\k<quote>(\s+|$)/;
  129. const operatorMap = {};
  130. operatorMap[TAPi18n.__('operator-board')] = 'boards';
  131. operatorMap[TAPi18n.__('operator-board-abbrev')] = 'boards';
  132. operatorMap[TAPi18n.__('operator-swimlane')] = 'swimlanes';
  133. operatorMap[TAPi18n.__('operator-swimlane-abbrev')] = 'swimlanes';
  134. operatorMap[TAPi18n.__('operator-list')] = 'lists';
  135. operatorMap[TAPi18n.__('operator-list-abbrev')] = 'lists';
  136. operatorMap[TAPi18n.__('operator-label')] = 'labels';
  137. operatorMap[TAPi18n.__('operator-label-abbrev')] = 'labels';
  138. operatorMap[TAPi18n.__('operator-user')] = 'users';
  139. operatorMap[TAPi18n.__('operator-user-abbrev')] = 'users';
  140. operatorMap[TAPi18n.__('operator-member')] = 'members';
  141. operatorMap[TAPi18n.__('operator-member-abbrev')] = 'members';
  142. operatorMap[TAPi18n.__('operator-assignee')] = 'assignees';
  143. operatorMap[TAPi18n.__('operator-assignee-abbrev')] = 'assignees';
  144. operatorMap[TAPi18n.__('operator-is')] = 'is';
  145. // eslint-disable-next-line no-console
  146. // console.log('operatorMap:', operatorMap);
  147. const params = {
  148. boards: [],
  149. swimlanes: [],
  150. lists: [],
  151. users: [],
  152. members: [],
  153. assignees: [],
  154. labels: [],
  155. is: [],
  156. };
  157. let text = '';
  158. while (query) {
  159. m = query.match(reOperator1);
  160. if (!m) {
  161. m = query.match(reOperator2);
  162. if (m) {
  163. query = query.replace(reOperator2, '');
  164. }
  165. } else {
  166. query = query.replace(reOperator1, '');
  167. }
  168. if (m) {
  169. let op;
  170. if (m.groups.operator) {
  171. op = m.groups.operator.toLowerCase();
  172. } else {
  173. op = m.groups.abbrev;
  174. }
  175. if (op in operatorMap) {
  176. params[operatorMap[op]].push(m.groups.value);
  177. } else {
  178. this.parsingErrors.push({
  179. tag: 'operator-unknown-error',
  180. value: op,
  181. });
  182. }
  183. continue;
  184. }
  185. m = query.match(reQuotedText);
  186. if (!m) {
  187. m = query.match(reText);
  188. if (m) {
  189. query = query.replace(reText, '');
  190. }
  191. } else {
  192. query = query.replace(reQuotedText, '');
  193. }
  194. if (m) {
  195. text += (text ? ' ' : '') + m.groups.text;
  196. }
  197. }
  198. // eslint-disable-next-line no-console
  199. // console.log('text:', text);
  200. params.text = text;
  201. // eslint-disable-next-line no-console
  202. // console.log('params:', params);
  203. this.queryParams = params;
  204. this.autorun(() => {
  205. const handle = subManager.subscribe('globalSearch', params);
  206. Tracker.nonreactive(() => {
  207. Tracker.autorun(() => {
  208. // eslint-disable-next-line no-console
  209. // console.log('ready:', handle.ready());
  210. if (handle.ready()) {
  211. this.searching.set(false);
  212. this.hasResults.set(true);
  213. }
  214. });
  215. });
  216. });
  217. },
  218. getResultsHeading() {
  219. if (this.resultsCount === 0) {
  220. return TAPi18n.__('no-cards-found');
  221. } else if (this.resultsCount === 1) {
  222. return TAPi18n.__('one-card-found');
  223. } else if (this.resultsCount === this.totalHits) {
  224. return TAPi18n.__('n-cards-found', this.resultsCount);
  225. }
  226. return TAPi18n.__('n-n-of-n-cards-found', {
  227. start: 1,
  228. end: this.resultsCount,
  229. total: this.totalHits,
  230. });
  231. },
  232. searchInstructions() {
  233. tags = {
  234. operator_board: TAPi18n.__('operator-board'),
  235. operator_list: TAPi18n.__('operator-list'),
  236. operator_swimlane: TAPi18n.__('operator-swimlane'),
  237. operator_label: TAPi18n.__('operator-label'),
  238. operator_label_abbrev: TAPi18n.__('operator-label-abbrev'),
  239. operator_user: TAPi18n.__('operator-user'),
  240. operator_user_abbrev: TAPi18n.__('operator-user-abbrev'),
  241. operator_member: TAPi18n.__('operator-member'),
  242. operator_member_abbrev: TAPi18n.__('operator-member-abbrev'),
  243. operator_assignee: TAPi18n.__('operator-assignee'),
  244. operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'),
  245. };
  246. text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`;
  247. text += `\n${TAPi18n.__('globalSearch-instructions-description', tags)}`;
  248. text += `\n${TAPi18n.__('globalSearch-instructions-operators', tags)}`;
  249. text += `\n* ${TAPi18n.__(
  250. 'globalSearch-instructions-operator-board',
  251. tags,
  252. )}`;
  253. text += `\n* ${TAPi18n.__(
  254. 'globalSearch-instructions-operator-list',
  255. tags,
  256. )}`;
  257. text += `\n* ${TAPi18n.__(
  258. 'globalSearch-instructions-operator-swimlane',
  259. tags,
  260. )}`;
  261. text += `\n* ${TAPi18n.__(
  262. 'globalSearch-instructions-operator-label',
  263. tags,
  264. )}`;
  265. text += `\n* ${TAPi18n.__(
  266. 'globalSearch-instructions-operator-hash',
  267. tags,
  268. )}`;
  269. text += `\n* ${TAPi18n.__(
  270. 'globalSearch-instructions-operator-user',
  271. tags,
  272. )}`;
  273. text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-at', tags)}`;
  274. text += `\n* ${TAPi18n.__(
  275. 'globalSearch-instructions-operator-member',
  276. tags,
  277. )}`;
  278. text += `\n* ${TAPi18n.__(
  279. 'globalSearch-instructions-operator-assignee',
  280. tags,
  281. )}`;
  282. text += `\n## ${TAPi18n.__('heading-notes')}`;
  283. text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`;
  284. text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-2', tags)}`;
  285. text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-3', tags)}`;
  286. text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-4', tags)}`;
  287. text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-5', tags)}`;
  288. return text;
  289. },
  290. events() {
  291. return [
  292. {
  293. 'submit .js-search-query-form'(evt) {
  294. evt.preventDefault();
  295. this.searchAllBoards(evt.target.searchQuery.value);
  296. },
  297. },
  298. ];
  299. },
  300. }).register('globalSearch');