query-classes.js 14 KB

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