customFields.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. CustomFields = new Mongo.Collection('customFields');
  3. /**
  4. * A custom field on a card in the board
  5. */
  6. CustomFields.attachSchema(
  7. new SimpleSchema({
  8. boardIds: {
  9. /**
  10. * the ID of the board
  11. */
  12. type: [String],
  13. },
  14. name: {
  15. /**
  16. * name of the custom field
  17. */
  18. type: String,
  19. },
  20. type: {
  21. /**
  22. * type of the custom field
  23. */
  24. type: String,
  25. allowedValues: [
  26. 'text',
  27. 'number',
  28. 'date',
  29. 'dropdown',
  30. 'checkbox',
  31. 'currency',
  32. 'stringtemplate',
  33. ],
  34. },
  35. settings: {
  36. /**
  37. * settings of the custom field
  38. */
  39. type: Object,
  40. },
  41. 'settings.currencyCode': {
  42. type: String,
  43. optional: true,
  44. },
  45. 'settings.dropdownItems': {
  46. /**
  47. * list of drop down items objects
  48. */
  49. type: [Object],
  50. optional: true,
  51. },
  52. 'settings.dropdownItems.$': {
  53. type: new SimpleSchema({
  54. _id: {
  55. /**
  56. * ID of the drop down item
  57. */
  58. type: String,
  59. },
  60. name: {
  61. /**
  62. * name of the drop down item
  63. */
  64. type: String,
  65. },
  66. }),
  67. },
  68. 'settings.stringtemplateFormat': {
  69. type: String,
  70. optional: true,
  71. },
  72. 'settings.stringtemplateSeparator': {
  73. type: String,
  74. optional: true,
  75. },
  76. showOnCard: {
  77. /**
  78. * should we show on the cards this custom field
  79. */
  80. type: Boolean,
  81. defaultValue: false,
  82. },
  83. automaticallyOnCard: {
  84. /**
  85. * should the custom fields automatically be added on cards?
  86. */
  87. type: Boolean,
  88. defaultValue: false,
  89. },
  90. alwaysOnCard: {
  91. /**
  92. * should the custom field be automatically added to all cards?
  93. */
  94. type: Boolean,
  95. defaultValue: false,
  96. },
  97. showLabelOnMiniCard: {
  98. /**
  99. * should the label of the custom field be shown on minicards?
  100. */
  101. type: Boolean,
  102. defaultValue: false,
  103. },
  104. showSumAtTopOfList: {
  105. /**
  106. * should the sum of the custom fields be shown at top of list?
  107. */
  108. type: Boolean,
  109. defaultValue: false,
  110. },
  111. createdAt: {
  112. type: Date,
  113. optional: true,
  114. // eslint-disable-next-line consistent-return
  115. autoValue() {
  116. if (this.isInsert) {
  117. return new Date();
  118. } else if (this.isUpsert) {
  119. return { $setOnInsert: new Date() };
  120. } else {
  121. this.unset();
  122. }
  123. },
  124. },
  125. modifiedAt: {
  126. type: Date,
  127. denyUpdate: false,
  128. // eslint-disable-next-line consistent-return
  129. autoValue() {
  130. if (this.isInsert || this.isUpsert || this.isUpdate) {
  131. return new Date();
  132. } else {
  133. this.unset();
  134. }
  135. },
  136. },
  137. }),
  138. );
  139. CustomFields.addToAllCards = cf => {
  140. Cards.update(
  141. {
  142. boardId: { $in: cf.boardIds },
  143. customFields: { $not: { $elemMatch: { _id: cf._id } } },
  144. },
  145. {
  146. $push: { customFields: { _id: cf._id, value: null } },
  147. },
  148. { multi: true },
  149. );
  150. };
  151. CustomFields.mutations({
  152. addBoard(boardId) {
  153. if (boardId) {
  154. return {
  155. $push: {
  156. boardIds: boardId,
  157. },
  158. };
  159. } else {
  160. return null;
  161. }
  162. },
  163. });
  164. CustomFields.allow({
  165. insert(userId, doc) {
  166. return allowIsAnyBoardMember(
  167. userId,
  168. ReactiveCache.getBoards({
  169. _id: { $in: doc.boardIds },
  170. }),
  171. );
  172. },
  173. update(userId, doc) {
  174. return allowIsAnyBoardMember(
  175. userId,
  176. ReactiveCache.getBoards({
  177. _id: { $in: doc.boardIds },
  178. }),
  179. );
  180. },
  181. remove(userId, doc) {
  182. return allowIsAnyBoardMember(
  183. userId,
  184. ReactiveCache.getBoards({
  185. _id: { $in: doc.boardIds },
  186. }),
  187. );
  188. },
  189. fetch: ['userId', 'boardIds'],
  190. });
  191. // not sure if we need this?
  192. //CustomFields.hookOptions.after.update = { fetchPrevious: false };
  193. function customFieldCreation(userId, doc) {
  194. Activities.insert({
  195. userId,
  196. activityType: 'createCustomField',
  197. boardId: doc.boardIds[0], // We are creating a customField, it has only one boardId
  198. customFieldId: doc._id,
  199. });
  200. }
  201. function customFieldDeletion(userId, doc) {
  202. Activities.insert({
  203. userId,
  204. activityType: 'deleteCustomField',
  205. boardId: doc.boardIds[0], // We are creating a customField, it has only one boardId
  206. customFieldId: doc._id,
  207. });
  208. }
  209. // This has some bug, it does not show edited customField value at Outgoing Webhook,
  210. // instead it shows undefined, and no listId and swimlaneId.
  211. function customFieldEdit(userId, doc) {
  212. const card = ReactiveCache.getCard(doc.cardId);
  213. const customFieldValue = ReactiveCache.getActivity({ customFieldId: doc._id }).value;
  214. Activities.insert({
  215. userId,
  216. activityType: 'setCustomField',
  217. boardId: doc.boardIds[0], // We are creating a customField, it has only one boardId
  218. customFieldId: doc._id,
  219. customFieldValue,
  220. listId: doc.listId,
  221. swimlaneId: doc.swimlaneId,
  222. });
  223. }
  224. if (Meteor.isServer) {
  225. Meteor.startup(() => {
  226. CustomFields._collection.createIndex({ modifiedAt: -1 });
  227. CustomFields._collection.createIndex({ boardIds: 1 });
  228. });
  229. CustomFields.after.insert((userId, doc) => {
  230. customFieldCreation(userId, doc);
  231. if (doc.alwaysOnCard) {
  232. CustomFields.addToAllCards(doc);
  233. }
  234. });
  235. CustomFields.before.update((userId, doc, fieldNames, modifier) => {
  236. if (_.contains(fieldNames, 'boardIds') && modifier.$pull) {
  237. Cards.update(
  238. { boardId: modifier.$pull.boardIds, 'customFields._id': doc._id },
  239. { $pull: { customFields: { _id: doc._id } } },
  240. { multi: true },
  241. );
  242. customFieldEdit(userId, doc);
  243. Activities.remove({
  244. customFieldId: doc._id,
  245. boardId: modifier.$pull.boardIds,
  246. listId: doc.listId,
  247. swimlaneId: doc.swimlaneId,
  248. });
  249. } else if (_.contains(fieldNames, 'boardIds') && modifier.$push) {
  250. Activities.insert({
  251. userId,
  252. activityType: 'createCustomField',
  253. boardId: modifier.$push.boardIds,
  254. customFieldId: doc._id,
  255. });
  256. }
  257. });
  258. CustomFields.after.update((userId, doc) => {
  259. if (doc.alwaysOnCard) {
  260. CustomFields.addToAllCards(doc);
  261. }
  262. });
  263. CustomFields.before.remove((userId, doc) => {
  264. customFieldDeletion(userId, doc);
  265. Activities.remove({
  266. customFieldId: doc._id,
  267. });
  268. Cards.update(
  269. { boardId: { $in: doc.boardIds }, 'customFields._id': doc._id },
  270. { $pull: { customFields: { _id: doc._id } } },
  271. { multi: true },
  272. );
  273. });
  274. }
  275. //CUSTOM FIELD REST API
  276. if (Meteor.isServer) {
  277. /**
  278. * @operation get_all_custom_fields
  279. * @summary Get the list of Custom Fields attached to a board
  280. *
  281. * @param {string} boardID the ID of the board
  282. * @return_type [{_id: string,
  283. * name: string,
  284. * type: string}]
  285. */
  286. JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function(
  287. req,
  288. res,
  289. ) {
  290. const paramBoardId = req.params.boardId;
  291. Authentication.checkBoardAccess(req.userId, paramBoardId);
  292. JsonRoutes.sendResult(res, {
  293. code: 200,
  294. data: ReactiveCache.getCustomFields({ boardIds: { $in: [paramBoardId] } }).map(
  295. function(cf) {
  296. return {
  297. _id: cf._id,
  298. name: cf.name,
  299. type: cf.type,
  300. };
  301. },
  302. ),
  303. });
  304. });
  305. /**
  306. * @operation get_custom_field
  307. * @summary Get a Custom Fields attached to a board
  308. *
  309. * @param {string} boardID the ID of the board
  310. * @param {string} customFieldId the ID of the custom field
  311. * @return_type [{_id: string,
  312. * boardIds: string}]
  313. */
  314. JsonRoutes.add(
  315. 'GET',
  316. '/api/boards/:boardId/custom-fields/:customFieldId',
  317. function(req, res) {
  318. const paramBoardId = req.params.boardId;
  319. const paramCustomFieldId = req.params.customFieldId;
  320. Authentication.checkBoardAccess(req.userId, paramBoardId);
  321. JsonRoutes.sendResult(res, {
  322. code: 200,
  323. data: ReactiveCache.getCustomField({
  324. _id: paramCustomFieldId,
  325. boardIds: { $in: [paramBoardId] },
  326. }),
  327. });
  328. },
  329. );
  330. /**
  331. * @operation new_custom_field
  332. * @summary Create a Custom Field
  333. *
  334. * @param {string} boardID the ID of the board
  335. * @param {string} name the name of the custom field
  336. * @param {string} type the type of the custom field
  337. * @param {string} settings the settings object of the custom field
  338. * @param {boolean} showOnCard should we show the custom field on cards?
  339. * @param {boolean} automaticallyOnCard should the custom fields automatically be added on cards?
  340. * @param {boolean} showLabelOnMiniCard should the label of the custom field be shown on minicards?
  341. * @param {boolean} showSumAtTopOfList should the sum of the custom fields be shown at top of list?
  342. * @return_type {_id: string}
  343. */
  344. JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function(
  345. req,
  346. res,
  347. ) {
  348. const paramBoardId = req.params.boardId;
  349. Authentication.checkBoardAccess(req.userId, paramBoardId);
  350. const board = ReactiveCache.getBoard(paramBoardId);
  351. const id = CustomFields.direct.insert({
  352. name: req.body.name,
  353. type: req.body.type,
  354. settings: req.body.settings,
  355. showOnCard: req.body.showOnCard,
  356. automaticallyOnCard: req.body.automaticallyOnCard,
  357. showLabelOnMiniCard: req.body.showLabelOnMiniCard,
  358. showSumAtTopOfList: req.body.showSumAtTopOfList,
  359. boardIds: [board._id],
  360. });
  361. const customField = ReactiveCache.getCustomField({
  362. _id: id,
  363. boardIds: { $in: [paramBoardId] },
  364. });
  365. customFieldCreation(req.body.authorId, customField);
  366. JsonRoutes.sendResult(res, {
  367. code: 200,
  368. data: {
  369. _id: id,
  370. },
  371. });
  372. });
  373. /**
  374. * @operation edit_custom_field
  375. * @summary Update a Custom Field
  376. *
  377. * @param {string} name the name of the custom field
  378. * @param {string} type the type of the custom field
  379. * @param {string} settings the settings object of the custom field
  380. * @param {boolean} showOnCard should we show the custom field on cards
  381. * @param {boolean} automaticallyOnCard should the custom fields automatically be added on cards
  382. * @param {boolean} showLabelOnMiniCard should the label of the custom field be shown on minicards
  383. * @param {boolean} showSumAtTopOfList should the sum of the custom fields be shown at top of list
  384. * @return_type {_id: string}
  385. */
  386. JsonRoutes.add(
  387. 'PUT',
  388. '/api/boards/:boardId/custom-fields/:customFieldId',
  389. (req, res) => {
  390. const paramBoardId = req.params.boardId;
  391. const paramFieldId = req.params.customFieldId;
  392. Authentication.checkBoardAccess(req.userId, paramBoardId);
  393. if (req.body.hasOwnProperty('name')) {
  394. CustomFields.direct.update(
  395. { _id: paramFieldId },
  396. { $set: { name: req.body.name } },
  397. );
  398. }
  399. if (req.body.hasOwnProperty('type')) {
  400. CustomFields.direct.update(
  401. { _id: paramFieldId },
  402. { $set: { type: req.body.type } },
  403. );
  404. }
  405. if (req.body.hasOwnProperty('settings')) {
  406. CustomFields.direct.update(
  407. { _id: paramFieldId },
  408. { $set: { settings: req.body.settings } },
  409. );
  410. }
  411. if (req.body.hasOwnProperty('showOnCard')) {
  412. CustomFields.direct.update(
  413. { _id: paramFieldId },
  414. { $set: { showOnCard: req.body.showOnCard } },
  415. );
  416. }
  417. if (req.body.hasOwnProperty('automaticallyOnCard')) {
  418. CustomFields.direct.update(
  419. { _id: paramFieldId },
  420. { $set: { automaticallyOnCard: req.body.automaticallyOnCard } },
  421. );
  422. }
  423. if (req.body.hasOwnProperty('alwaysOnCard')) {
  424. CustomFields.direct.update(
  425. { _id: paramFieldId },
  426. { $set: { alwaysOnCard: req.body.alwaysOnCard } },
  427. );
  428. }
  429. if (req.body.hasOwnProperty('showLabelOnMiniCard')) {
  430. CustomFields.direct.update(
  431. { _id: paramFieldId },
  432. { $set: { showLabelOnMiniCard: req.body.showLabelOnMiniCard } },
  433. );
  434. }
  435. if (req.body.hasOwnProperty('showSumAtTopOfList')) {
  436. CustomFields.direct.update(
  437. { _id: paramFieldId },
  438. { $set: { showSumAtTopOfList: req.body.showSumAtTopOfList } },
  439. );
  440. }
  441. JsonRoutes.sendResult(res, {
  442. code: 200,
  443. data: { _id: paramFieldId },
  444. });
  445. },
  446. );
  447. /**
  448. * @operation add_custom_field_dropdown_items
  449. * @summary Update a Custom Field's dropdown items
  450. *
  451. * @param {string} [items] names of the custom field
  452. * @return_type {_id: string}
  453. */
  454. JsonRoutes.add(
  455. 'POST',
  456. '/api/boards/:boardId/custom-fields/:customFieldId/dropdown-items',
  457. (req, res) => {
  458. const paramBoardId = req.params.boardId;
  459. const paramCustomFieldId = req.params.customFieldId;
  460. Authentication.checkBoardAccess(req.userId, paramBoardId);
  461. const paramItems = req.body.items;
  462. if (req.body.hasOwnProperty('items')) {
  463. if (Array.isArray(paramItems)) {
  464. CustomFields.direct.update(
  465. { _id: paramCustomFieldId },
  466. {
  467. $push: {
  468. 'settings.dropdownItems': {
  469. $each: paramItems
  470. .filter(name => typeof name === 'string')
  471. .map(name => ({
  472. _id: Random.id(6),
  473. name,
  474. })),
  475. },
  476. },
  477. },
  478. );
  479. }
  480. }
  481. JsonRoutes.sendResult(res, {
  482. code: 200,
  483. data: { _id: paramCustomFieldId },
  484. });
  485. },
  486. );
  487. /**
  488. * @operation edit_custom_field_dropdown_item
  489. * @summary Update a Custom Field's dropdown item
  490. *
  491. * @param {string} name names of the custom field
  492. * @return_type {_id: string}
  493. */
  494. JsonRoutes.add(
  495. 'PUT',
  496. '/api/boards/:boardId/custom-fields/:customFieldId/dropdown-items/:dropdownItemId',
  497. (req, res) => {
  498. const paramBoardId = req.params.boardId;
  499. const paramDropdownItemId = req.params.dropdownItemId;
  500. const paramCustomFieldId = req.params.customFieldId;
  501. Authentication.checkBoardAccess(req.userId, paramBoardId);
  502. const paramName = req.body.name;
  503. if (req.body.hasOwnProperty('name')) {
  504. CustomFields.direct.update(
  505. {
  506. _id: paramCustomFieldId,
  507. 'settings.dropdownItems._id': paramDropdownItemId,
  508. },
  509. {
  510. $set: {
  511. 'settings.dropdownItems.$': {
  512. _id: paramDropdownItemId,
  513. name: paramName,
  514. },
  515. },
  516. },
  517. );
  518. }
  519. JsonRoutes.sendResult(res, {
  520. code: 200,
  521. data: { _id: paramDropdownItemId },
  522. });
  523. },
  524. );
  525. /**
  526. * @operation delete_custom_field_dropdown_item
  527. * @summary Update a Custom Field's dropdown items
  528. *
  529. * @param {string} itemId ID of the dropdown item
  530. * @return_type {_id: string}
  531. */
  532. JsonRoutes.add(
  533. 'DELETE',
  534. '/api/boards/:boardId/custom-fields/:customFieldId/dropdown-items/:dropdownItemId',
  535. (req, res) => {
  536. const paramBoardId = req.params.boardId;
  537. paramCustomFieldId = req.params.customFieldId;
  538. paramDropdownItemId = req.params.dropdownItemId;
  539. Authentication.checkBoardAccess(req.userId, paramBoardId);
  540. CustomFields.direct.update(
  541. { _id: paramCustomFieldId },
  542. {
  543. $pull: {
  544. 'settings.dropdownItems': { _id: paramDropdownItemId },
  545. },
  546. },
  547. );
  548. JsonRoutes.sendResult(res, {
  549. code: 200,
  550. data: { _id: paramCustomFieldId },
  551. });
  552. },
  553. );
  554. /**
  555. * @operation delete_custom_field
  556. * @summary Delete a Custom Fields attached to a board
  557. *
  558. * @description The Custom Field can't be retrieved after this operation
  559. *
  560. * @param {string} boardID the ID of the board
  561. * @param {string} customFieldId the ID of the custom field
  562. * @return_type {_id: string}
  563. */
  564. JsonRoutes.add(
  565. 'DELETE',
  566. '/api/boards/:boardId/custom-fields/:customFieldId',
  567. function(req, res) {
  568. const paramBoardId = req.params.boardId;
  569. Authentication.checkBoardAccess(req.userId, paramBoardId);
  570. const id = req.params.customFieldId;
  571. CustomFields.remove({ _id: id, boardIds: { $in: [paramBoardId] } });
  572. JsonRoutes.sendResult(res, {
  573. code: 200,
  574. data: {
  575. _id: id,
  576. },
  577. });
  578. },
  579. );
  580. }
  581. export default CustomFields;