minicard.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import { TAPi18n } from '/imports/i18n';
  3. import { CustomFieldStringTemplate } from '/client/lib/customFields';
  4. import { handleFileUpload } from './attachments';
  5. // Template.cards.events({
  6. // 'click .member': Popup.open('cardMember')
  7. // });
  8. BlazeComponent.extendComponent({
  9. template() {
  10. return 'minicard';
  11. },
  12. formattedCurrencyCustomFieldValue(definition) {
  13. const customField = this.data()
  14. .customFieldsWD()
  15. .find(f => f._id === definition._id);
  16. const customFieldTrueValue =
  17. customField && customField.trueValue ? customField.trueValue : '';
  18. const locale = TAPi18n.getLanguage();
  19. return new Intl.NumberFormat(locale, {
  20. style: 'currency',
  21. currency: definition.settings.currencyCode,
  22. }).format(customFieldTrueValue);
  23. },
  24. formattedStringtemplateCustomFieldValue(definition) {
  25. const customField = this.data()
  26. .customFieldsWD()
  27. .find(f => f._id === definition._id);
  28. const customFieldTrueValue =
  29. customField && customField.trueValue ? customField.trueValue : [];
  30. const ret = new CustomFieldStringTemplate(definition).getFormattedValue(customFieldTrueValue);
  31. return ret;
  32. },
  33. showCreatorOnMinicard() {
  34. // cache "board" to reduce the mini-mongodb access
  35. const board = this.data().board();
  36. let ret = false;
  37. if (board) {
  38. ret = board.allowsCreatorOnMinicard ?? false;
  39. }
  40. return ret;
  41. },
  42. isWatching() {
  43. const card = this.currentData();
  44. return card.findWatcher(Meteor.userId());
  45. },
  46. showMembers() {
  47. // cache "board" to reduce the mini-mongodb access
  48. const board = this.data().board();
  49. let ret = false;
  50. if (board) {
  51. ret =
  52. board.allowsMembers === null ||
  53. board.allowsMembers === undefined ||
  54. board.allowsMembers
  55. ;
  56. }
  57. return ret;
  58. },
  59. showAssignee() {
  60. // cache "board" to reduce the mini-mongodb access
  61. const board = this.data().board();
  62. let ret = false;
  63. if (board) {
  64. ret =
  65. board.allowsAssignee === null ||
  66. board.allowsAssignee === undefined ||
  67. board.allowsAssignee
  68. ;
  69. }
  70. return ret;
  71. },
  72. /** opens the card label popup only if clicked onto a label
  73. * <li> this is necessary to have the data context of the minicard.
  74. * if .js-card-label is used at click event, then only the data context of the label itself is available at this.currentData()
  75. */
  76. cardLabelsPopup(event) {
  77. if (this.find('.js-card-label:hover')) {
  78. Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: this.currentData()});
  79. }
  80. },
  81. events() {
  82. return [
  83. {
  84. 'click .js-linked-link'() {
  85. if (this.data().isLinkedCard()) Utils.goCardId(this.data().linkedId);
  86. else if (this.data().isLinkedBoard())
  87. Utils.goBoardId(this.data().linkedId);
  88. },
  89. 'click .js-toggle-minicard-label-text'() {
  90. if (window.localStorage.getItem('hiddenMinicardLabelText')) {
  91. window.localStorage.removeItem('hiddenMinicardLabelText'); //true
  92. } else {
  93. window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
  94. }
  95. },
  96. 'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
  97. 'click .minicard-labels' : this.cardLabelsPopup,
  98. 'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
  99. // Drag and drop file upload handlers
  100. 'dragover .minicard'(event) {
  101. event.preventDefault();
  102. event.stopPropagation();
  103. },
  104. 'dragenter .minicard'(event) {
  105. event.preventDefault();
  106. event.stopPropagation();
  107. const card = this.data();
  108. const board = card.board();
  109. // Only allow drag-and-drop if user can modify card and board allows attachments
  110. if (card.canModifyCard() && board && board.allowsAttachments) {
  111. // Check if the drag contains files
  112. const dataTransfer = event.originalEvent.dataTransfer;
  113. if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
  114. $(event.currentTarget).addClass('is-dragging-over');
  115. }
  116. }
  117. },
  118. 'dragleave .minicard'(event) {
  119. event.preventDefault();
  120. event.stopPropagation();
  121. $(event.currentTarget).removeClass('is-dragging-over');
  122. },
  123. 'drop .minicard'(event) {
  124. event.preventDefault();
  125. event.stopPropagation();
  126. $(event.currentTarget).removeClass('is-dragging-over');
  127. const card = this.data();
  128. const board = card.board();
  129. // Check permissions
  130. if (!card.canModifyCard() || !board || !board.allowsAttachments) {
  131. return;
  132. }
  133. // Check if this is a file drop (not a card reorder)
  134. const dataTransfer = event.originalEvent.dataTransfer;
  135. if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
  136. return;
  137. }
  138. // Check if the drop contains files (not just text/HTML)
  139. if (!dataTransfer.types.includes('Files')) {
  140. return;
  141. }
  142. const files = dataTransfer.files;
  143. if (files && files.length > 0) {
  144. handleFileUpload(card, files);
  145. }
  146. },
  147. }
  148. ];
  149. },
  150. }).register('minicard');
  151. Template.minicard.helpers({
  152. hiddenMinicardLabelText() {
  153. const currentUser = ReactiveCache.getCurrentUser();
  154. if (currentUser) {
  155. return (currentUser.profile || {}).hiddenMinicardLabelText;
  156. } else if (window.localStorage.getItem('hiddenMinicardLabelText')) {
  157. return true;
  158. } else {
  159. return false;
  160. }
  161. },
  162. // XXX resolve this nasty hack for https://github.com/veliovgroup/Meteor-Files/issues/763
  163. sess() {
  164. return Meteor.connection && Meteor.connection._lastSessionId
  165. ? Meteor.connection._lastSessionId
  166. : null;
  167. },
  168. isWatching() {
  169. return this.findWatcher(Meteor.userId());
  170. }
  171. });
  172. BlazeComponent.extendComponent({
  173. events() {
  174. return [
  175. {
  176. 'keydown input.js-edit-card-sort-popup'(evt) {
  177. // enter = save
  178. if (evt.keyCode === 13) {
  179. this.find('button[type=submit]').click();
  180. }
  181. },
  182. 'click button.js-submit-edit-card-sort-popup'(event) {
  183. // save button pressed
  184. event.preventDefault();
  185. const sort = this.$('.js-edit-card-sort-popup')[0]
  186. .value
  187. .trim();
  188. if (!Number.isNaN(sort)) {
  189. let card = this.data();
  190. card.move(card.boardId, card.swimlaneId, card.listId, sort);
  191. Popup.back();
  192. }
  193. },
  194. }
  195. ]
  196. }
  197. }).register('editCardSortOrderPopup');
  198. Template.minicardDetailsActionsPopup.events({
  199. 'click .js-due-date': Popup.open('editCardDueDate'),
  200. 'click .js-move-card': Popup.open('moveCard'),
  201. 'click .js-copy-card': Popup.open('copyCard'),
  202. 'click .js-set-card-color': Popup.open('setCardColor'),
  203. 'click .js-add-labels': Popup.open('cardLabels'),
  204. 'click .js-link': Popup.open('linkCard'),
  205. 'click .js-move-card-to-top'(event) {
  206. event.preventDefault();
  207. const minOrder = this.getMinSort();
  208. this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
  209. Popup.back();
  210. },
  211. 'click .js-move-card-to-bottom'(event) {
  212. event.preventDefault();
  213. const maxOrder = this.getMaxSort();
  214. this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
  215. Popup.back();
  216. },
  217. 'click .js-archive': Popup.afterConfirm('cardArchive', function () {
  218. Popup.close();
  219. this.archive();
  220. Utils.goBoardId(this.boardId);
  221. }),
  222. 'click .js-toggle-watch-card'() {
  223. const currentCard = this;
  224. const level = currentCard.findWatcher(Meteor.userId()) ? null : 'watching';
  225. Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => {
  226. if (!err && ret) Popup.back();
  227. });
  228. },
  229. });