2
0

activities.js 11 KB

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