query-classes.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. import {
  2. OPERATOR_ASSIGNEE,
  3. OPERATOR_BOARD,
  4. OPERATOR_COMMENT,
  5. OPERATOR_CREATED_AT,
  6. OPERATOR_CREATOR,
  7. OPERATOR_DUE,
  8. OPERATOR_HAS,
  9. OPERATOR_LABEL,
  10. OPERATOR_LIMIT,
  11. OPERATOR_LIST,
  12. OPERATOR_MEMBER,
  13. OPERATOR_MODIFIED_AT,
  14. OPERATOR_SORT,
  15. OPERATOR_STATUS,
  16. OPERATOR_SWIMLANE,
  17. OPERATOR_UNKNOWN,
  18. OPERATOR_USER,
  19. ORDER_ASCENDING,
  20. ORDER_DESCENDING,
  21. PREDICATE_ALL,
  22. PREDICATE_ARCHIVED,
  23. PREDICATE_ASSIGNEES,
  24. PREDICATE_ATTACHMENT,
  25. PREDICATE_CHECKLIST,
  26. PREDICATE_CREATED_AT,
  27. PREDICATE_DESCRIPTION,
  28. PREDICATE_DUE_AT,
  29. PREDICATE_END_AT,
  30. PREDICATE_ENDED,
  31. PREDICATE_MEMBERS,
  32. PREDICATE_MODIFIED_AT,
  33. PREDICATE_MONTH,
  34. PREDICATE_OPEN,
  35. PREDICATE_OVERDUE,
  36. PREDICATE_PRIVATE,
  37. PREDICATE_PUBLIC,
  38. PREDICATE_QUARTER,
  39. PREDICATE_START_AT,
  40. PREDICATE_WEEK,
  41. PREDICATE_YEAR,
  42. } from './search-const';
  43. import Boards from '../models/boards';
  44. import moment from 'moment';
  45. export class QueryParams {
  46. text = '';
  47. constructor(params = {}, text = '') {
  48. this.params = params;
  49. this.text = text;
  50. }
  51. hasOperator(operator) {
  52. return (
  53. this.params[operator] !== undefined &&
  54. (this.params[operator].length === undefined ||
  55. this.params[operator].length > 0)
  56. );
  57. }
  58. addPredicate(operator, predicate) {
  59. if (!this.hasOperator(operator)) {
  60. this.params[operator] = [];
  61. }
  62. this.params[operator].push(predicate);
  63. }
  64. setPredicate(operator, predicate) {
  65. this.params[operator] = predicate;
  66. }
  67. getPredicate(operator) {
  68. if (typeof this.params[operator] === 'object') {
  69. return this.params[operator][0];
  70. } else {
  71. return this.params[operator];
  72. }
  73. }
  74. getPredicates(operator) {
  75. return this.params[operator];
  76. }
  77. getParams() {
  78. return this.params;
  79. }
  80. }
  81. export class QueryErrors {
  82. operatorTagMap = [
  83. [OPERATOR_BOARD, 'board-title-not-found'],
  84. [OPERATOR_SWIMLANE, 'swimlane-title-not-found'],
  85. [
  86. OPERATOR_LABEL,
  87. label => {
  88. if (Boards.labelColors().includes(label)) {
  89. return {
  90. tag: 'label-color-not-found',
  91. value: label,
  92. color: true,
  93. };
  94. } else {
  95. return {
  96. tag: 'label-not-found',
  97. value: label,
  98. color: false,
  99. };
  100. }
  101. },
  102. ],
  103. [OPERATOR_LIST, 'list-title-not-found'],
  104. [OPERATOR_COMMENT, 'comment-not-found'],
  105. [OPERATOR_USER, 'user-username-not-found'],
  106. [OPERATOR_ASSIGNEE, 'user-username-not-found'],
  107. [OPERATOR_MEMBER, 'user-username-not-found'],
  108. [OPERATOR_CREATOR, 'user-username-not-found'],
  109. ];
  110. constructor() {
  111. this._errors = {};
  112. this.operatorTags = {};
  113. this.operatorTagMap.forEach(([operator, tag]) => {
  114. this.operatorTags[operator] = tag;
  115. });
  116. this.colorMap = Boards.colorMap();
  117. }
  118. addError(operator, error) {
  119. if (!this._errors[operator]) {
  120. this._errors[operator] = [];
  121. }
  122. this._errors[operator].push(error);
  123. }
  124. addNotFound(operator, value) {
  125. if (typeof this.operatorTags[operator] === 'function') {
  126. this.addError(operator, this.operatorTags[operator](value));
  127. } else {
  128. this.addError(operator, { tag: this.operatorTags[operator], value });
  129. }
  130. }
  131. hasErrors() {
  132. return Object.entries(this._errors).length > 0;
  133. }
  134. errors() {
  135. const errs = [];
  136. // eslint-disable-next-line no-unused-vars
  137. Object.entries(this._errors).forEach(([, errors]) => {
  138. errors.forEach(err => {
  139. errs.push(err);
  140. });
  141. });
  142. return errs;
  143. }
  144. errorMessages() {
  145. const messages = [];
  146. // eslint-disable-next-line no-unused-vars
  147. Object.entries(this._errors).forEach(([, errors]) => {
  148. errors.forEach(err => {
  149. messages.push(TAPi18n.__(err.tag, err.value));
  150. });
  151. });
  152. return messages;
  153. }
  154. }
  155. export class Query {
  156. selector = {};
  157. projection = {};
  158. constructor(selector, projection) {
  159. this._errors = new QueryErrors();
  160. this.queryParams = new QueryParams();
  161. this.colorMap = Boards.colorMap();
  162. if (selector) {
  163. this.selector = selector;
  164. }
  165. if (projection) {
  166. this.projection = projection;
  167. }
  168. }
  169. hasErrors() {
  170. return this._errors.hasErrors();
  171. }
  172. errors() {
  173. return this._errors.errors();
  174. }
  175. errorMessages() {
  176. return this._errors.errorMessages();
  177. }
  178. getQueryParams() {
  179. return this.queryParams;
  180. }
  181. setQueryParams(queryParams) {
  182. this.queryParams = queryParams;
  183. }
  184. addPredicate(operator, predicate) {
  185. this.queryParams.addPredicate(operator, predicate);
  186. }
  187. buildParams(queryText) {
  188. queryText = queryText.trim();
  189. // eslint-disable-next-line no-console
  190. //console.log('query:', query);
  191. if (!queryText) {
  192. return;
  193. }
  194. const reOperator1 = new RegExp(
  195. '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<value>[\\p{Letter}\\p{Mark}]+)(\\s+|$)',
  196. 'iu',
  197. );
  198. const reOperator2 = new RegExp(
  199. '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<quote>["\']*)(?<value>.*?)\\k<quote>(\\s+|$)',
  200. 'iu',
  201. );
  202. const reText = new RegExp('^(?<text>\\S+)(\\s+|$)', 'u');
  203. const reQuotedText = new RegExp(
  204. '^(?<quote>["\'])(?<text>.*?)\\k<quote>(\\s+|$)',
  205. 'u',
  206. );
  207. const reNegatedOperator = new RegExp('^-(?<operator>.*)$');
  208. const operators = {
  209. 'operator-board': OPERATOR_BOARD,
  210. 'operator-board-abbrev': OPERATOR_BOARD,
  211. 'operator-swimlane': OPERATOR_SWIMLANE,
  212. 'operator-swimlane-abbrev': OPERATOR_SWIMLANE,
  213. 'operator-list': OPERATOR_LIST,
  214. 'operator-list-abbrev': OPERATOR_LIST,
  215. 'operator-label': OPERATOR_LABEL,
  216. 'operator-label-abbrev': OPERATOR_LABEL,
  217. 'operator-user': OPERATOR_USER,
  218. 'operator-user-abbrev': OPERATOR_USER,
  219. 'operator-member': OPERATOR_MEMBER,
  220. 'operator-member-abbrev': OPERATOR_MEMBER,
  221. 'operator-assignee': OPERATOR_ASSIGNEE,
  222. 'operator-creator': OPERATOR_CREATOR,
  223. 'operator-assignee-abbrev': OPERATOR_ASSIGNEE,
  224. 'operator-status': OPERATOR_STATUS,
  225. 'operator-due': OPERATOR_DUE,
  226. 'operator-created': OPERATOR_CREATED_AT,
  227. 'operator-modified': OPERATOR_MODIFIED_AT,
  228. 'operator-comment': OPERATOR_COMMENT,
  229. 'operator-has': OPERATOR_HAS,
  230. 'operator-sort': OPERATOR_SORT,
  231. 'operator-limit': OPERATOR_LIMIT,
  232. };
  233. const predicates = {
  234. due: {
  235. 'predicate-overdue': PREDICATE_OVERDUE,
  236. },
  237. durations: {
  238. 'predicate-week': PREDICATE_WEEK,
  239. 'predicate-month': PREDICATE_MONTH,
  240. 'predicate-quarter': PREDICATE_QUARTER,
  241. 'predicate-year': PREDICATE_YEAR,
  242. },
  243. status: {
  244. 'predicate-archived': PREDICATE_ARCHIVED,
  245. 'predicate-all': PREDICATE_ALL,
  246. 'predicate-open': PREDICATE_OPEN,
  247. 'predicate-ended': PREDICATE_ENDED,
  248. 'predicate-public': PREDICATE_PUBLIC,
  249. 'predicate-private': PREDICATE_PRIVATE,
  250. },
  251. sorts: {
  252. 'predicate-due': PREDICATE_DUE_AT,
  253. 'predicate-created': PREDICATE_CREATED_AT,
  254. 'predicate-modified': PREDICATE_MODIFIED_AT,
  255. },
  256. has: {
  257. 'predicate-description': PREDICATE_DESCRIPTION,
  258. 'predicate-checklist': PREDICATE_CHECKLIST,
  259. 'predicate-attachment': PREDICATE_ATTACHMENT,
  260. 'predicate-start': PREDICATE_START_AT,
  261. 'predicate-end': PREDICATE_END_AT,
  262. 'predicate-due': PREDICATE_DUE_AT,
  263. 'predicate-assignee': PREDICATE_ASSIGNEES,
  264. 'predicate-member': PREDICATE_MEMBERS,
  265. },
  266. };
  267. const predicateTranslations = {};
  268. Object.entries(predicates).forEach(([category, catPreds]) => {
  269. predicateTranslations[category] = {};
  270. Object.entries(catPreds).forEach(([tag, value]) => {
  271. predicateTranslations[category][TAPi18n.__(tag)] = value;
  272. });
  273. });
  274. // eslint-disable-next-line no-console
  275. // console.log('predicateTranslations:', predicateTranslations);
  276. const operatorMap = {};
  277. Object.entries(operators).forEach(([key, value]) => {
  278. operatorMap[TAPi18n.__(key).toLowerCase()] = value;
  279. });
  280. // eslint-disable-next-line no-console
  281. // console.log('operatorMap:', operatorMap);
  282. let text = '';
  283. while (queryText) {
  284. let m = queryText.match(reOperator1);
  285. if (!m) {
  286. m = queryText.match(reOperator2);
  287. if (m) {
  288. queryText = queryText.replace(reOperator2, '');
  289. }
  290. } else {
  291. queryText = queryText.replace(reOperator1, '');
  292. }
  293. if (m) {
  294. let op;
  295. if (m.groups.operator) {
  296. op = m.groups.operator.toLowerCase();
  297. } else {
  298. op = m.groups.abbrev.toLowerCase();
  299. }
  300. // eslint-disable-next-line no-prototype-builtins
  301. if (operatorMap.hasOwnProperty(op)) {
  302. const operator = operatorMap[op];
  303. let value = m.groups.value;
  304. if (operator === OPERATOR_LABEL) {
  305. if (value in this.colorMap) {
  306. value = this.colorMap[value];
  307. // console.log('found color:', value);
  308. }
  309. } else if (
  310. [OPERATOR_DUE, OPERATOR_CREATED_AT, OPERATOR_MODIFIED_AT].includes(
  311. operator,
  312. )
  313. ) {
  314. const days = parseInt(value, 10);
  315. let duration = null;
  316. if (isNaN(days)) {
  317. // duration was specified as text
  318. if (predicateTranslations.durations[value]) {
  319. duration = predicateTranslations.durations[value];
  320. let date = null;
  321. switch (duration) {
  322. case PREDICATE_WEEK:
  323. // eslint-disable-next-line no-case-declarations
  324. const week = moment().week();
  325. if (week === 52) {
  326. date = moment(1, 'W');
  327. date.set('year', date.year() + 1);
  328. } else {
  329. date = moment(week + 1, 'W');
  330. }
  331. break;
  332. case PREDICATE_MONTH:
  333. // eslint-disable-next-line no-case-declarations
  334. const month = moment().month();
  335. // .month() is zero indexed
  336. if (month === 11) {
  337. date = moment(1, 'M');
  338. date.set('year', date.year() + 1);
  339. } else {
  340. date = moment(month + 2, 'M');
  341. }
  342. break;
  343. case PREDICATE_QUARTER:
  344. // eslint-disable-next-line no-case-declarations
  345. const quarter = moment().quarter();
  346. if (quarter === 4) {
  347. date = moment(1, 'Q');
  348. date.set('year', date.year() + 1);
  349. } else {
  350. date = moment(quarter + 1, 'Q');
  351. }
  352. break;
  353. case PREDICATE_YEAR:
  354. date = moment(moment().year() + 1, 'YYYY');
  355. break;
  356. }
  357. if (date) {
  358. value = {
  359. operator: '$lt',
  360. value: date.format('YYYY-MM-DD'),
  361. };
  362. }
  363. } else if (
  364. operator === OPERATOR_DUE &&
  365. value === PREDICATE_OVERDUE
  366. ) {
  367. value = {
  368. operator: '$lt',
  369. value: moment().format('YYYY-MM-DD'),
  370. };
  371. } else {
  372. this.errors.addError(OPERATOR_DUE, {
  373. tag: 'operator-number-expected',
  374. value: { operator: op, value },
  375. });
  376. continue;
  377. }
  378. } else if (operator === OPERATOR_DUE) {
  379. value = {
  380. operator: '$lt',
  381. value: moment(moment().format('YYYY-MM-DD'))
  382. .add(days + 1, duration ? duration : 'days')
  383. .format(),
  384. };
  385. } else {
  386. value = {
  387. operator: '$gte',
  388. value: moment(moment().format('YYYY-MM-DD'))
  389. .subtract(days, duration ? duration : 'days')
  390. .format(),
  391. };
  392. }
  393. } else if (operator === OPERATOR_SORT) {
  394. let negated = false;
  395. const m = value.match(reNegatedOperator);
  396. if (m) {
  397. value = m.groups.operator;
  398. negated = true;
  399. }
  400. if (!predicateTranslations.sorts[value]) {
  401. this.errors.addError(OPERATOR_SORT, {
  402. tag: 'operator-sort-invalid',
  403. value,
  404. });
  405. continue;
  406. } else {
  407. value = {
  408. name: predicateTranslations.sorts[value],
  409. order: negated ? ORDER_DESCENDING : ORDER_ASCENDING,
  410. };
  411. }
  412. } else if (operator === OPERATOR_STATUS) {
  413. if (!predicateTranslations.status[value]) {
  414. this.errors.addError(OPERATOR_STATUS, {
  415. tag: 'operator-status-invalid',
  416. value,
  417. });
  418. continue;
  419. } else {
  420. value = predicateTranslations.status[value];
  421. }
  422. } else if (operator === OPERATOR_HAS) {
  423. let negated = false;
  424. const m = value.match(reNegatedOperator);
  425. if (m) {
  426. value = m.groups.operator;
  427. negated = true;
  428. }
  429. if (!predicateTranslations.has[value]) {
  430. this.errors.addError(OPERATOR_HAS, {
  431. tag: 'operator-has-invalid',
  432. value,
  433. });
  434. continue;
  435. } else {
  436. value = {
  437. field: predicateTranslations.has[value],
  438. exists: !negated,
  439. };
  440. }
  441. } else if (operator === OPERATOR_LIMIT) {
  442. const limit = parseInt(value, 10);
  443. if (isNaN(limit) || limit < 1) {
  444. this.errors.addError(OPERATOR_LIMIT, {
  445. tag: 'operator-limit-invalid',
  446. value,
  447. });
  448. continue;
  449. } else {
  450. value = limit;
  451. }
  452. }
  453. this.queryParams.addPredicate(operator, value);
  454. } else {
  455. this.errors.addError(OPERATOR_UNKNOWN, {
  456. tag: 'operator-unknown-error',
  457. value: op,
  458. });
  459. }
  460. continue;
  461. }
  462. m = queryText.match(reQuotedText);
  463. if (!m) {
  464. m = queryText.match(reText);
  465. if (m) {
  466. queryText = queryText.replace(reText, '');
  467. }
  468. } else {
  469. queryText = queryText.replace(reQuotedText, '');
  470. }
  471. if (m) {
  472. text += (text ? ' ' : '') + m.groups.text;
  473. }
  474. }
  475. // eslint-disable-next-line no-console
  476. // console.log('text:', text);
  477. this.queryParams.text = text;
  478. // eslint-disable-next-line no-console
  479. //console.log('queryParams:', this.queryParams);
  480. }
  481. }