activities.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import DOMPurify from 'dompurify';
  3. import { sanitizeHTML, sanitizeText } from '/client/lib/secureDOMPurify';
  4. import { TAPi18n } from '/imports/i18n';
  5. const activitiesPerPage = 500;
  6. BlazeComponent.extendComponent({
  7. onCreated() {
  8. // XXX Should we use ReactiveNumber?
  9. this.page = new ReactiveVar(1);
  10. this.loadNextPageLocked = false;
  11. // TODO is sidebar always available? E.g. on small screens/mobile devices
  12. const sidebar = Sidebar;
  13. sidebar && sidebar.callFirstWith(null, 'resetNextPeak');
  14. this.autorun(() => {
  15. let mode = this.data()?.mode;
  16. if (mode) {
  17. const capitalizedMode = Utils.capitalize(mode);
  18. let searchId;
  19. const showActivities = this.showActivities();
  20. if (mode === 'linkedcard' || mode === 'linkedboard') {
  21. const currentCard = Utils.getCurrentCard();
  22. searchId = currentCard.linkedId;
  23. mode = mode.replace('linked', '');
  24. } else if (mode === 'card') {
  25. searchId = Utils.getCurrentCardId();
  26. } else {
  27. searchId = Session.get(`current${capitalizedMode}`);
  28. }
  29. const limit = this.page.get() * activitiesPerPage;
  30. if (searchId === null) return;
  31. this.subscribe('activities', mode, searchId, limit, showActivities, () => {
  32. this.loadNextPageLocked = false;
  33. // TODO the guard can be removed as soon as the TODO above is resolved
  34. if (!sidebar) return;
  35. // If the sibear peak hasn't increased, that mean that there are no more
  36. // activities, and we can stop calling new subscriptions.
  37. // XXX This is hacky! We need to know excatly and reactively how many
  38. // activities there are, we probably want to denormalize this number
  39. // dirrectly into card and board documents.
  40. const nextPeakBefore = sidebar.callFirstWith(null, 'getNextPeak');
  41. sidebar.calculateNextPeak();
  42. const nextPeakAfter = sidebar.callFirstWith(null, 'getNextPeak');
  43. if (nextPeakBefore === nextPeakAfter) {
  44. sidebar.callFirstWith(null, 'resetNextPeak');
  45. }
  46. });
  47. }
  48. });
  49. },
  50. loadNextPage() {
  51. if (this.loadNextPageLocked === false) {
  52. this.page.set(this.page.get() + 1);
  53. this.loadNextPageLocked = true;
  54. }
  55. },
  56. showActivities() {
  57. let ret = false;
  58. let mode = this.data()?.mode;
  59. if (mode) {
  60. if (mode === 'linkedcard' || mode === 'linkedboard') {
  61. const currentCard = Utils.getCurrentCard();
  62. ret = currentCard.showActivities ?? false;
  63. } else if (mode === 'card') {
  64. ret = this.data()?.card?.showActivities ?? false;
  65. } else {
  66. ret = Utils.getCurrentBoard().showActivities ?? false;
  67. }
  68. }
  69. return ret;
  70. },
  71. activities() {
  72. const ret = this.data().card.activities();
  73. return ret;
  74. },
  75. }).register('activities');
  76. BlazeComponent.extendComponent({
  77. checkItem() {
  78. const checkItemId = this.currentData().activity.checklistItemId;
  79. const checkItem = ReactiveCache.getChecklistItem(checkItemId);
  80. return checkItem && checkItem.title;
  81. },
  82. boardLabelLink() {
  83. const data = this.currentData();
  84. const currentBoardId = Session.get('currentBoard');
  85. if (data.mode !== 'board') {
  86. // data.mode: card, linkedcard, linkedboard
  87. return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
  88. }
  89. else if (currentBoardId != data.activity.boardId) {
  90. // data.mode: board
  91. // current activitie is linked
  92. return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
  93. }
  94. return TAPi18n.__('this-board');
  95. },
  96. cardLabelLink() {
  97. const data = this.currentData();
  98. const currentBoardId = Session.get('currentBoard');
  99. if (data.mode == 'card') {
  100. // data.mode: card
  101. return TAPi18n.__('this-card');
  102. }
  103. else if (data.mode !== 'board') {
  104. // data.mode: linkedcard, linkedboard
  105. return createCardLink(data.activity.card(), null);
  106. }
  107. else if (currentBoardId != data.activity.boardId) {
  108. // data.mode: board
  109. // current activitie is linked
  110. return createCardLink(data.activity.card(), data.activity.board().title);
  111. }
  112. return createCardLink(this.currentData().activity.card(), null);
  113. },
  114. cardLink() {
  115. const data = this.currentData();
  116. const currentBoardId = Session.get('currentBoard');
  117. if (data.mode !== 'board') {
  118. // data.mode: card, linkedcard, linkedboard
  119. return createCardLink(data.activity.card(), null);
  120. }
  121. else if (currentBoardId != data.activity.boardId) {
  122. // data.mode: board
  123. // current activitie is linked
  124. return createCardLink(data.activity.card(), data.activity.board().title);
  125. }
  126. return createCardLink(this.currentData().activity.card(), null);
  127. },
  128. receivedDate() {
  129. const receivedDate = this.currentData().activity.card();
  130. if (!receivedDate) return null;
  131. return receivedDate.receivedAt;
  132. },
  133. startDate() {
  134. const startDate = this.currentData().activity.card();
  135. if (!startDate) return null;
  136. return startDate.startAt;
  137. },
  138. dueDate() {
  139. const dueDate = this.currentData().activity.card();
  140. if (!dueDate) return null;
  141. return dueDate.dueAt;
  142. },
  143. endDate() {
  144. const endDate = this.currentData().activity.card();
  145. if (!endDate) return null;
  146. return endDate.endAt;
  147. },
  148. lastLabel() {
  149. const lastLabelId = this.currentData().activity.labelId;
  150. if (!lastLabelId) return null;
  151. const lastLabel = ReactiveCache.getBoard(
  152. this.currentData().activity.boardId,
  153. ).getLabelById(lastLabelId);
  154. if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) {
  155. return lastLabel.color;
  156. } else if (lastLabel.name !== undefined && lastLabel.name !== '') {
  157. return lastLabel.name;
  158. } else {
  159. return null;
  160. }
  161. },
  162. lastCustomField() {
  163. const lastCustomField = ReactiveCache.getCustomField(
  164. this.currentData().activity.customFieldId,
  165. );
  166. if (!lastCustomField) return null;
  167. return lastCustomField.name;
  168. },
  169. lastCustomFieldValue() {
  170. const lastCustomField = ReactiveCache.getCustomField(
  171. this.currentData().activity.customFieldId,
  172. );
  173. if (!lastCustomField) return null;
  174. const value = this.currentData().activity.value;
  175. if (
  176. lastCustomField.settings.dropdownItems &&
  177. lastCustomField.settings.dropdownItems.length > 0
  178. ) {
  179. const dropDownValue = _.find(
  180. lastCustomField.settings.dropdownItems,
  181. item => {
  182. return item._id === value;
  183. },
  184. );
  185. if (dropDownValue) return dropDownValue.name;
  186. }
  187. return value;
  188. },
  189. listLabel() {
  190. const activity = this.currentData().activity;
  191. const list = activity.list();
  192. return (list && list.title) || activity.title;
  193. },
  194. sourceLink() {
  195. const source = this.currentData().activity.source;
  196. if (source) {
  197. if (source.url) {
  198. return Blaze.toHTML(
  199. HTML.A(
  200. {
  201. href: source.url,
  202. },
  203. sanitizeHTML(source.system),
  204. ),
  205. );
  206. } else {
  207. return sanitizeHTML(source.system);
  208. }
  209. }
  210. return null;
  211. },
  212. memberLink() {
  213. return Blaze.toHTMLWithData(Template.memberName, {
  214. user: this.currentData().activity.member(),
  215. });
  216. },
  217. attachmentLink() {
  218. const attachment = this.currentData().activity.attachment();
  219. // trying to display url before file is stored generates js errors
  220. return (
  221. (attachment &&
  222. attachment.path &&
  223. Blaze.toHTML(
  224. HTML.A(
  225. {
  226. href: `${attachment.link()}?download=true`,
  227. target: '_blank',
  228. },
  229. sanitizeText(attachment.name),
  230. ),
  231. )) ||
  232. sanitizeText(this.currentData().activity.attachmentName)
  233. );
  234. },
  235. customField() {
  236. const customField = this.currentData().activity.customField();
  237. if (!customField) return null;
  238. return customField.name;
  239. },
  240. }).register('activity');
  241. Template.activity.helpers({
  242. sanitize(value) {
  243. return sanitizeHTML(value);
  244. },
  245. });
  246. Template.commentReactions.events({
  247. 'click .reaction'(event) {
  248. if (ReactiveCache.getCurrentUser().isBoardMember()) {
  249. const codepoint = event.currentTarget.dataset['codepoint'];
  250. const commentId = Template.instance().data.commentId;
  251. const cardComment = ReactiveCache.getCardComment(commentId);
  252. cardComment.toggleReaction(codepoint);
  253. }
  254. },
  255. 'click .open-comment-reaction-popup': Popup.open('addReaction'),
  256. })
  257. Template.addReactionPopup.events({
  258. 'click .add-comment-reaction'(event) {
  259. if (ReactiveCache.getCurrentUser().isBoardMember()) {
  260. const codepoint = event.currentTarget.dataset['codepoint'];
  261. const commentId = Template.instance().data.commentId;
  262. const cardComment = ReactiveCache.getCardComment(commentId);
  263. cardComment.toggleReaction(codepoint);
  264. }
  265. Popup.back();
  266. },
  267. })
  268. Template.addReactionPopup.helpers({
  269. codepoints() {
  270. // Starting set of unicode codepoints as comment reactions
  271. return [
  272. '👍',
  273. '👎',
  274. '👀',
  275. '✅',
  276. '❌',
  277. '🙏',
  278. '👏',
  279. '🎉',
  280. '🚀',
  281. '😊',
  282. '🤔',
  283. '😔'];
  284. }
  285. })
  286. Template.commentReactions.helpers({
  287. isSelected(userIds) {
  288. return Meteor.userId() && userIds.includes(Meteor.userId());
  289. },
  290. userNames(userIds) {
  291. const ret = ReactiveCache.getUsers({_id: {$in: userIds}})
  292. .map(user => user.profile.fullname)
  293. .join(', ');
  294. return ret;
  295. }
  296. })
  297. function createCardLink(card, board) {
  298. if (!card) return '';
  299. let text = card.title;
  300. if (board) text = `${board} > ` + text;
  301. return (
  302. card &&
  303. Blaze.toHTML(
  304. HTML.A(
  305. {
  306. href: card.originRelativeUrl(),
  307. class: 'action-card',
  308. },
  309. sanitizeHTML(text),
  310. ),
  311. )
  312. );
  313. }
  314. function createBoardLink(board, list) {
  315. let text = board.title;
  316. if (list) text += `: ${list}`;
  317. return (
  318. board &&
  319. Blaze.toHTML(
  320. HTML.A(
  321. {
  322. href: board.originRelativeUrl(),
  323. class: 'action-board',
  324. },
  325. sanitizeHTML(text),
  326. ),
  327. )
  328. );
  329. }