cardDetails.js 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import { TAPi18n } from '/imports/i18n';
  3. import { DatePicker } from '/client/lib/datepicker';
  4. import {
  5. formatDateTime,
  6. formatDate,
  7. formatTime,
  8. getISOWeek,
  9. isValidDate,
  10. isBefore,
  11. isAfter,
  12. isSame,
  13. add,
  14. subtract,
  15. startOf,
  16. endOf,
  17. format,
  18. parseDate,
  19. now,
  20. createDate,
  21. fromNow,
  22. calendar
  23. } from '/imports/lib/dateUtils';
  24. import Cards from '/models/cards';
  25. import Boards from '/models/boards';
  26. import Checklists from '/models/checklists';
  27. import Integrations from '/models/integrations';
  28. import Users from '/models/users';
  29. import Lists from '/models/lists';
  30. import CardComments from '/models/cardComments';
  31. import { ALLOWED_COLORS } from '/config/const';
  32. import { UserAvatar } from '../users/userAvatar';
  33. import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
  34. import { handleFileUpload } from './attachments';
  35. import uploadProgressManager from '../../lib/uploadProgressManager';
  36. const subManager = new SubsManager();
  37. const { calculateIndexData } = Utils;
  38. BlazeComponent.extendComponent({
  39. mixins() {
  40. return [Mixins.InfiniteScrolling];
  41. },
  42. calculateNextPeak() {
  43. const cardElement = this.find('.js-card-details');
  44. if (cardElement) {
  45. const altitude = cardElement.scrollHeight;
  46. this.callFirstWith(this, 'setNextPeak', altitude);
  47. }
  48. },
  49. reachNextPeak() {
  50. const activitiesComponent = this.childComponents('activities')[0];
  51. activitiesComponent.loadNextPage();
  52. },
  53. onCreated() {
  54. this.currentBoard = Utils.getCurrentBoard();
  55. this.isLoaded = new ReactiveVar(false);
  56. if (this.parentComponent() && this.parentComponent().parentComponent()) {
  57. const boardBody = this.parentComponent().parentComponent();
  58. //in Miniview parent is Board, not BoardBody.
  59. if (boardBody !== null) {
  60. boardBody.showOverlay.set(true);
  61. boardBody.mouseHasEnterCardDetails = false;
  62. }
  63. }
  64. this.calculateNextPeak();
  65. Meteor.subscribe('unsaved-edits');
  66. // this.findUsersOptions = new ReactiveVar({});
  67. // this.page = new ReactiveVar(1);
  68. // this.autorun(() => {
  69. // const limitUsers = this.page.get() * Number.MAX_SAFE_INTEGER;
  70. // this.subscribe('people', this.findUsersOptions.get(), limitUsers, () => {});
  71. // });
  72. },
  73. isWatching() {
  74. const card = this.currentData();
  75. return card.findWatcher(Meteor.userId());
  76. },
  77. customFieldsGrid() {
  78. return ReactiveCache.getCurrentUser().hasCustomFieldsGrid();
  79. },
  80. cardMaximized() {
  81. return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
  82. },
  83. presentParentTask() {
  84. let result = this.currentBoard.presentParentTask;
  85. if (result === null || result === undefined) {
  86. result = 'no-parent';
  87. }
  88. return result;
  89. },
  90. linkForCard() {
  91. const card = this.currentData();
  92. let result = '#';
  93. if (card) {
  94. const board = ReactiveCache.getBoard(card.boardId);
  95. if (board) {
  96. result = FlowRouter.path('card', {
  97. boardId: card.boardId,
  98. slug: board.slug,
  99. cardId: card._id,
  100. });
  101. }
  102. }
  103. return result;
  104. },
  105. showVotingButtons() {
  106. const card = this.currentData();
  107. return (
  108. (currentUser.isBoardMember() ||
  109. (currentUser && card.voteAllowNonBoardMembers())) &&
  110. !card.expiredVote()
  111. );
  112. },
  113. showPlanningPokerButtons() {
  114. const card = this.currentData();
  115. return (
  116. (currentUser.isBoardMember() ||
  117. (currentUser && card.pokerAllowNonBoardMembers())) &&
  118. !card.expiredPoker()
  119. );
  120. },
  121. isVerticalScrollbars() {
  122. const user = ReactiveCache.getCurrentUser();
  123. return user && user.isVerticalScrollbars();
  124. },
  125. /** returns if the list id is the current list id
  126. * @param listId list id to check
  127. * @return is the list id the current list id ?
  128. */
  129. isCurrentListId(listId) {
  130. const ret = this.data().listId == listId;
  131. return ret;
  132. },
  133. onRendered() {
  134. if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) {
  135. // Send Webhook but not create Activities records ---
  136. const card = this.currentData();
  137. const userId = Meteor.userId();
  138. const params = {
  139. userId,
  140. cardId: card._id,
  141. boardId: card.boardId,
  142. listId: card.listId,
  143. user: ReactiveCache.getCurrentUser().username,
  144. url: '',
  145. };
  146. const integrations = ReactiveCache.getIntegrations({
  147. boardId: { $in: [card.boardId, Integrations.Const.GLOBAL_WEBHOOK_ID] },
  148. enabled: true,
  149. activities: { $in: ['CardDetailsRendered', 'all'] },
  150. });
  151. if (integrations.length > 0) {
  152. integrations.forEach((integration) => {
  153. Meteor.call(
  154. 'outgoingWebhooks',
  155. integration,
  156. 'CardSelected',
  157. params,
  158. () => { },
  159. );
  160. });
  161. }
  162. //-------------
  163. }
  164. const $checklistsDom = this.$('.card-checklist-items');
  165. $checklistsDom.sortable({
  166. tolerance: 'pointer',
  167. helper: 'clone',
  168. handle: '.checklist-title',
  169. items: '.js-checklist',
  170. placeholder: 'checklist placeholder',
  171. distance: 7,
  172. start(evt, ui) {
  173. ui.placeholder.height(ui.helper.height());
  174. EscapeActions.clickExecute(evt.target, 'inlinedForm');
  175. },
  176. stop(evt, ui) {
  177. let prevChecklist = ui.item.prev('.js-checklist').get(0);
  178. if (prevChecklist) {
  179. prevChecklist = Blaze.getData(prevChecklist).checklist;
  180. }
  181. let nextChecklist = ui.item.next('.js-checklist').get(0);
  182. if (nextChecklist) {
  183. nextChecklist = Blaze.getData(nextChecklist).checklist;
  184. }
  185. const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1);
  186. $checklistsDom.sortable('cancel');
  187. const checklist = Blaze.getData(ui.item.get(0)).checklist;
  188. Checklists.update(checklist._id, {
  189. $set: {
  190. sort: sortIndex.base,
  191. },
  192. });
  193. },
  194. });
  195. const $subtasksDom = this.$('.card-subtasks-items');
  196. $subtasksDom.sortable({
  197. tolerance: 'pointer',
  198. helper: 'clone',
  199. handle: '.subtask-title',
  200. items: '.js-subtasks',
  201. placeholder: 'subtasks placeholder',
  202. distance: 7,
  203. start(evt, ui) {
  204. ui.placeholder.height(ui.helper.height());
  205. EscapeActions.executeUpTo('popup-close');
  206. },
  207. stop(evt, ui) {
  208. let prevChecklist = ui.item.prev('.js-subtasks').get(0);
  209. if (prevChecklist) {
  210. prevChecklist = Blaze.getData(prevChecklist).subtask;
  211. }
  212. let nextChecklist = ui.item.next('.js-subtasks').get(0);
  213. if (nextChecklist) {
  214. nextChecklist = Blaze.getData(nextChecklist).subtask;
  215. }
  216. const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1);
  217. $subtasksDom.sortable('cancel');
  218. const subtask = Blaze.getData(ui.item.get(0)).subtask;
  219. Subtasks.update(subtask._id, {
  220. $set: {
  221. subtaskSort: sortIndex.base,
  222. },
  223. });
  224. },
  225. });
  226. function userIsMember() {
  227. return ReactiveCache.getCurrentUser()?.isBoardMember();
  228. }
  229. // Disable sorting if the current user is not a board member
  230. this.autorun(() => {
  231. const disabled = !userIsMember();
  232. if (
  233. $checklistsDom.data('uiSortable') ||
  234. $checklistsDom.data('sortable')
  235. ) {
  236. $checklistsDom.sortable('option', 'disabled', disabled);
  237. if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
  238. $checklistsDom.sortable({ handle: '.checklist-handle' });
  239. }
  240. }
  241. if ($subtasksDom.data('uiSortable') || $subtasksDom.data('sortable')) {
  242. $subtasksDom.sortable('option', 'disabled', disabled);
  243. }
  244. });
  245. },
  246. onDestroyed() {
  247. if (this.parentComponent() === null) return;
  248. const parentComponent = this.parentComponent().parentComponent();
  249. //on mobile view parent is Board, not board body.
  250. if (parentComponent === null) return;
  251. parentComponent.showOverlay.set(false);
  252. },
  253. events() {
  254. const events = {
  255. [`${CSSEvents.transitionend} .js-card-details`]() {
  256. this.isLoaded.set(true);
  257. },
  258. [`${CSSEvents.animationend} .js-card-details`]() {
  259. this.isLoaded.set(true);
  260. },
  261. };
  262. return [
  263. {
  264. ...events,
  265. 'click .js-close-card-details'() {
  266. Utils.goBoardId(this.data().boardId);
  267. },
  268. 'click .js-copy-link'(event) {
  269. event.preventDefault();
  270. const promise = Utils.copyTextToClipboard(event.target.href);
  271. const $tooltip = this.$('.card-details-header .copied-tooltip');
  272. Utils.showCopied(promise, $tooltip);
  273. },
  274. 'change .js-date-format-selector'(event) {
  275. const dateFormat = event.target.value;
  276. Meteor.call('changeDateFormat', dateFormat);
  277. },
  278. 'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
  279. 'submit .js-card-description'(event) {
  280. event.preventDefault();
  281. const description = this.currentComponent().getValue();
  282. this.data().setDescription(description);
  283. },
  284. 'submit .js-card-details-title'(event) {
  285. event.preventDefault();
  286. const title = this.currentComponent().getValue().trim();
  287. if (title) {
  288. this.data().setTitle(title);
  289. } else {
  290. this.data().setTitle('');
  291. }
  292. },
  293. 'submit .js-card-details-assigner'(event) {
  294. event.preventDefault();
  295. const assigner = this.currentComponent().getValue().trim();
  296. if (assigner) {
  297. this.data().setAssignedBy(assigner);
  298. } else {
  299. this.data().setAssignedBy('');
  300. }
  301. },
  302. 'submit .js-card-details-requester'(event) {
  303. event.preventDefault();
  304. const requester = this.currentComponent().getValue().trim();
  305. if (requester) {
  306. this.data().setRequestedBy(requester);
  307. } else {
  308. this.data().setRequestedBy('');
  309. }
  310. },
  311. 'keydown input.js-edit-card-sort'(evt) {
  312. // enter = save
  313. if (evt.keyCode === 13) {
  314. this.find('button[type=submit]').click();
  315. }
  316. },
  317. 'submit .js-card-details-sort'(event) {
  318. event.preventDefault();
  319. const sort = parseFloat(this.currentComponent()
  320. .getValue()
  321. .trim());
  322. if (!Number.isNaN(sort)) {
  323. let card = this.data();
  324. card.move(card.boardId, card.swimlaneId, card.listId, sort);
  325. }
  326. },
  327. 'change .js-select-card-details-lists'(event) {
  328. let card = this.data();
  329. const listSelect = this.$('.js-select-card-details-lists')[0];
  330. const listId = listSelect.options[listSelect.selectedIndex].value;
  331. const minOrder = card.getMinSort(listId, card.swimlaneId);
  332. card.move(card.boardId, card.swimlaneId, listId, minOrder - 1);
  333. },
  334. 'click .js-go-to-linked-card'() {
  335. Utils.goCardId(this.data().linkedId);
  336. },
  337. 'click .js-member': Popup.open('cardMember'),
  338. 'click .js-add-members': Popup.open('cardMembers'),
  339. 'click .js-assignee': Popup.open('cardAssignee'),
  340. 'click .js-add-assignees': Popup.open('cardAssignees'),
  341. 'click .js-add-labels': Popup.open('cardLabels'),
  342. 'click .js-received-date': Popup.open('editCardReceivedDate'),
  343. 'click .js-start-date': Popup.open('editCardStartDate'),
  344. 'click .js-due-date': Popup.open('editCardDueDate'),
  345. 'click .js-end-date': Popup.open('editCardEndDate'),
  346. 'click .js-show-positive-votes': Popup.open('positiveVoteMembers'),
  347. 'click .js-show-negative-votes': Popup.open('negativeVoteMembers'),
  348. 'click .js-custom-fields': Popup.open('cardCustomFields'),
  349. 'mouseenter .js-card-details'() {
  350. if (this.parentComponent() === null) return;
  351. const parentComponent = this.parentComponent().parentComponent();
  352. //on mobile view parent is Board, not BoardBody.
  353. if (parentComponent === null) return;
  354. parentComponent.showOverlay.set(true);
  355. parentComponent.mouseHasEnterCardDetails = true;
  356. },
  357. 'mousedown .js-card-details'() {
  358. Session.set('cardDetailsIsDragging', false);
  359. Session.set('cardDetailsIsMouseDown', true);
  360. },
  361. 'mousemove .js-card-details'() {
  362. if (Session.get('cardDetailsIsMouseDown')) {
  363. Session.set('cardDetailsIsDragging', true);
  364. }
  365. },
  366. 'mouseup .js-card-details'() {
  367. Session.set('cardDetailsIsDragging', false);
  368. Session.set('cardDetailsIsMouseDown', false);
  369. },
  370. 'click #toggleShowActivitiesCard'() {
  371. this.data().toggleShowActivities();
  372. },
  373. 'click #toggleHideCheckedChecklistItems'() {
  374. this.data().toggleHideCheckedChecklistItems();
  375. },
  376. 'click #toggleCustomFieldsGridButton'() {
  377. Meteor.call('toggleCustomFieldsGrid');
  378. },
  379. 'click .js-maximize-card-details'() {
  380. Meteor.call('toggleCardMaximized');
  381. autosize($('.card-details'));
  382. },
  383. 'click .js-minimize-card-details'() {
  384. Meteor.call('toggleCardMaximized');
  385. autosize($('.card-details'));
  386. },
  387. 'click .js-vote'(e) {
  388. const forIt = $(e.target).hasClass('js-vote-positive');
  389. let newState = null;
  390. if (
  391. this.data().voteState() === null ||
  392. (this.data().voteState() === false && forIt) ||
  393. (this.data().voteState() === true && !forIt)
  394. ) {
  395. newState = forIt;
  396. }
  397. // Use secure server method; direct client updates to vote are blocked
  398. Meteor.call('cards.vote', this.data()._id, newState);
  399. },
  400. 'click .js-poker'(e) {
  401. let newState = null;
  402. if ($(e.target).hasClass('js-poker-vote-one')) {
  403. newState = 'one';
  404. Meteor.call('cards.pokerVote', this.data()._id, newState);
  405. }
  406. if ($(e.target).hasClass('js-poker-vote-two')) {
  407. newState = 'two';
  408. Meteor.call('cards.pokerVote', this.data()._id, newState);
  409. }
  410. if ($(e.target).hasClass('js-poker-vote-three')) {
  411. newState = 'three';
  412. Meteor.call('cards.pokerVote', this.data()._id, newState);
  413. }
  414. if ($(e.target).hasClass('js-poker-vote-five')) {
  415. newState = 'five';
  416. Meteor.call('cards.pokerVote', this.data()._id, newState);
  417. }
  418. if ($(e.target).hasClass('js-poker-vote-eight')) {
  419. newState = 'eight';
  420. Meteor.call('cards.pokerVote', this.data()._id, newState);
  421. }
  422. if ($(e.target).hasClass('js-poker-vote-thirteen')) {
  423. newState = 'thirteen';
  424. Meteor.call('cards.pokerVote', this.data()._id, newState);
  425. }
  426. if ($(e.target).hasClass('js-poker-vote-twenty')) {
  427. newState = 'twenty';
  428. Meteor.call('cards.pokerVote', this.data()._id, newState);
  429. }
  430. if ($(e.target).hasClass('js-poker-vote-forty')) {
  431. newState = 'forty';
  432. Meteor.call('cards.pokerVote', this.data()._id, newState);
  433. }
  434. if ($(e.target).hasClass('js-poker-vote-one-hundred')) {
  435. newState = 'oneHundred';
  436. Meteor.call('cards.pokerVote', this.data()._id, newState);
  437. }
  438. if ($(e.target).hasClass('js-poker-vote-unsure')) {
  439. newState = 'unsure';
  440. Meteor.call('cards.pokerVote', this.data()._id, newState);
  441. }
  442. },
  443. 'click .js-poker-finish'(e) {
  444. if ($(e.target).hasClass('js-poker-finish')) {
  445. e.preventDefault();
  446. const now = new Date();
  447. Meteor.call('cards.setPokerEnd', this.data()._id, now);
  448. }
  449. },
  450. 'click .js-poker-replay'(e) {
  451. if ($(e.target).hasClass('js-poker-replay')) {
  452. e.preventDefault();
  453. this.currentCard = this.currentData();
  454. Meteor.call('cards.replayPoker', this.currentCard._id);
  455. Meteor.call('cards.unsetPokerEnd', this.currentCard._id);
  456. Meteor.call('cards.unsetPokerEstimation', this.currentCard._id);
  457. }
  458. },
  459. 'click .js-poker-estimation'(event) {
  460. event.preventDefault();
  461. const ruleTitle = this.find('#pokerEstimation').value;
  462. if (ruleTitle !== undefined && ruleTitle !== '') {
  463. this.find('#pokerEstimation').value = '';
  464. if (ruleTitle) {
  465. Meteor.call('cards.setPokerEstimation', this.data()._id, parseInt(ruleTitle, 10));
  466. } else {
  467. Meteor.call('cards.unsetPokerEstimation', this.data()._id);
  468. }
  469. }
  470. },
  471. // Drag and drop file upload handlers
  472. 'dragover .js-card-details'(event) {
  473. // Only prevent default for file drags to avoid interfering with other drag operations
  474. const dataTransfer = event.originalEvent.dataTransfer;
  475. if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
  476. event.preventDefault();
  477. event.stopPropagation();
  478. }
  479. },
  480. 'dragenter .js-card-details'(event) {
  481. const dataTransfer = event.originalEvent.dataTransfer;
  482. if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
  483. event.preventDefault();
  484. event.stopPropagation();
  485. const card = this.data();
  486. const board = card.board();
  487. // Only allow drag-and-drop if user can modify card and board allows attachments
  488. if (Utils.canModifyCard() && board && board.allowsAttachments) {
  489. $(event.currentTarget).addClass('is-dragging-over');
  490. }
  491. }
  492. },
  493. 'dragleave .js-card-details'(event) {
  494. const dataTransfer = event.originalEvent.dataTransfer;
  495. if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
  496. event.preventDefault();
  497. event.stopPropagation();
  498. $(event.currentTarget).removeClass('is-dragging-over');
  499. }
  500. },
  501. 'drop .js-card-details'(event) {
  502. const dataTransfer = event.originalEvent.dataTransfer;
  503. if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
  504. event.preventDefault();
  505. event.stopPropagation();
  506. $(event.currentTarget).removeClass('is-dragging-over');
  507. const card = this.data();
  508. const board = card.board();
  509. // Check permissions
  510. if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
  511. return;
  512. }
  513. // Check if this is a file drop (not a checklist item reorder)
  514. if (!dataTransfer.files || dataTransfer.files.length === 0) {
  515. return;
  516. }
  517. const files = dataTransfer.files;
  518. if (files && files.length > 0) {
  519. handleFileUpload(card, files);
  520. }
  521. }
  522. },
  523. },
  524. ];
  525. },
  526. }).register('cardDetails');
  527. Template.cardDetails.helpers({
  528. isPopup() {
  529. let ret = !!Utils.getPopupCardId();
  530. return ret;
  531. },
  532. isDateFormat(format) {
  533. const currentUser = ReactiveCache.getCurrentUser();
  534. if (!currentUser) return format === 'YYYY-MM-DD';
  535. return currentUser.getDateFormat() === format;
  536. },
  537. // Upload progress helpers
  538. hasActiveUploads() {
  539. return uploadProgressManager.hasActiveUploads(this._id);
  540. },
  541. uploads() {
  542. return uploadProgressManager.getUploadsForCard(this._id);
  543. },
  544. uploadCount() {
  545. return uploadProgressManager.getUploadCountForCard(this._id);
  546. }
  547. });
  548. Template.cardDetailsPopup.onDestroyed(() => {
  549. Session.delete('popupCardId');
  550. Session.delete('popupCardBoardId');
  551. });
  552. Template.cardDetailsPopup.helpers({
  553. popupCard() {
  554. const ret = Utils.getPopupCard();
  555. return ret;
  556. },
  557. });
  558. BlazeComponent.extendComponent({
  559. template() {
  560. return 'exportCard';
  561. },
  562. withApi() {
  563. return Template.instance().apiEnabled.get();
  564. },
  565. exportUrlCardPDF() {
  566. const params = {
  567. boardId: Session.get('currentBoard'),
  568. listId: this.listId,
  569. cardId: this.cardId,
  570. };
  571. const queryParams = {
  572. authToken: Accounts._storedLoginToken(),
  573. };
  574. return FlowRouter.path(
  575. '/api/boards/:boardId/lists/:listId/cards/:cardId/exportPDF',
  576. params,
  577. queryParams,
  578. );
  579. },
  580. exportFilenameCardPDF() {
  581. //const boardId = Session.get('currentBoard');
  582. //return `export-card-pdf-${boardId}.xlsx`;
  583. return `export-card.pdf`;
  584. },
  585. }).register('exportCardPopup');
  586. // only allow number input
  587. Template.editCardSortOrderForm.onRendered(function () {
  588. this.$('input').on("keypress paste", function (event) {
  589. let keyCode = event.keyCode;
  590. let charCode = String.fromCharCode(keyCode);
  591. let regex = new RegExp('[-0-9.]');
  592. let ret = regex.test(charCode);
  593. // only working here, defining in events() doesn't handle the return value correctly
  594. return ret;
  595. });
  596. });
  597. // We extends the normal InlinedForm component to support UnsavedEdits draft
  598. // feature.
  599. (class extends InlinedForm {
  600. _getUnsavedEditKey() {
  601. return {
  602. fieldName: 'cardDescription',
  603. // XXX Recovering the currentCard identifier form a session variable is
  604. // fragile because this variable may change for instance if the route
  605. // change. We should use some component props instead.
  606. docId: Utils.getCurrentCardId(),
  607. };
  608. }
  609. close(isReset = false) {
  610. if (this.isOpen.get() && !isReset) {
  611. const draft = this.getValue().trim();
  612. let card = Utils.getCurrentCard();
  613. if (card && draft !== card.getDescription()) {
  614. UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue());
  615. }
  616. }
  617. super.close();
  618. }
  619. reset() {
  620. UnsavedEdits.reset(this._getUnsavedEditKey());
  621. this.close(true);
  622. }
  623. events() {
  624. const parentEvents = InlinedForm.prototype.events()[0];
  625. return [
  626. {
  627. ...parentEvents,
  628. 'click .js-close-inlined-form': this.reset,
  629. },
  630. ];
  631. }
  632. }.register('inlinedCardDescription'));
  633. Template.cardDetailsActionsPopup.helpers({
  634. isWatching() {
  635. return this.findWatcher(Meteor.userId());
  636. },
  637. isBoardAdmin() {
  638. return ReactiveCache.getCurrentUser().isBoardAdmin();
  639. },
  640. showListOnMinicard() {
  641. return this.showListOnMinicard;
  642. },
  643. });
  644. Template.cardDetailsActionsPopup.events({
  645. 'click .js-export-card': Popup.open('exportCard'),
  646. 'click .js-members': Popup.open('cardMembers'),
  647. 'click .js-assignees': Popup.open('cardAssignees'),
  648. 'click .js-attachments': Popup.open('cardAttachments'),
  649. 'click .js-start-voting': Popup.open('cardStartVoting'),
  650. 'click .js-start-planning-poker': Popup.open('cardStartPlanningPoker'),
  651. 'click .js-custom-fields': Popup.open('cardCustomFields'),
  652. 'click .js-received-date': Popup.open('editCardReceivedDate'),
  653. 'click .js-start-date': Popup.open('editCardStartDate'),
  654. 'click .js-due-date': Popup.open('editCardDueDate'),
  655. 'click .js-end-date': Popup.open('editCardEndDate'),
  656. 'click .js-spent-time': Popup.open('editCardSpentTime'),
  657. 'click .js-move-card': Popup.open('moveCard'),
  658. 'click .js-copy-card': Popup.open('copyCard'),
  659. 'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
  660. 'click .js-copy-checklist-cards': Popup.open('copyManyCards'),
  661. 'click .js-set-card-color': Popup.open('setCardColor'),
  662. 'click .js-move-card-to-top'(event) {
  663. event.preventDefault();
  664. const minOrder = this.getMinSort();
  665. this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
  666. Popup.back();
  667. },
  668. 'click .js-move-card-to-bottom'(event) {
  669. event.preventDefault();
  670. const maxOrder = this.getMaxSort();
  671. this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
  672. Popup.back();
  673. },
  674. 'click .js-archive': Popup.afterConfirm('cardArchive', function () {
  675. Popup.close();
  676. this.archive();
  677. Utils.goBoardId(this.boardId);
  678. }),
  679. 'click .js-more': Popup.open('cardMore'),
  680. 'click .js-toggle-watch-card'() {
  681. const currentCard = this;
  682. const level = currentCard.findWatcher(Meteor.userId()) ? null : 'watching';
  683. Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => {
  684. if (!err && ret) Popup.close();
  685. });
  686. },
  687. 'click .js-toggle-show-list-on-minicard'() {
  688. const currentCard = this;
  689. const newValue = !currentCard.showListOnMinicard;
  690. Cards.update(currentCard._id, { $set: { showListOnMinicard: newValue } });
  691. Popup.close();
  692. },
  693. });
  694. BlazeComponent.extendComponent({
  695. onRendered() {
  696. autosize(this.$('textarea.js-edit-card-title'));
  697. },
  698. events() {
  699. return [
  700. {
  701. 'click a.fa.fa-copy'(event) {
  702. const $editor = this.$('textarea');
  703. const promise = Utils.copyTextToClipboard($editor[0].value);
  704. const $tooltip = this.$('.copied-tooltip');
  705. Utils.showCopied(promise, $tooltip);
  706. },
  707. 'keydown .js-edit-card-title'(event) {
  708. // If enter key was pressed, submit the data
  709. // Unless the shift key is also being pressed
  710. if (event.keyCode === 13 && !event.shiftKey) {
  711. $('.js-submit-edit-card-title-form').click();
  712. }
  713. },
  714. }
  715. ];
  716. }
  717. }).register('editCardTitleForm');
  718. Template.cardMembersPopup.onCreated(function () {
  719. let currBoard = Utils.getCurrentBoard();
  720. let members = currBoard.activeMembers();
  721. this.members = new ReactiveVar(members);
  722. });
  723. Template.cardMembersPopup.events({
  724. 'keyup .card-members-filter'(event) {
  725. const members = filterMembers(event.target.value);
  726. Template.instance().members.set(members);
  727. }
  728. });
  729. Template.cardMembersPopup.helpers({
  730. members() {
  731. return _.sortBy(Template.instance().members.get(),'fullname');
  732. },
  733. });
  734. const filterMembers = (filterTerm) => {
  735. let currBoard = Utils.getCurrentBoard();
  736. let members = currBoard.activeMembers();
  737. if (filterTerm) {
  738. members = members
  739. .map(member => ({
  740. member,
  741. user: ReactiveCache.getUser(member.userId)
  742. }))
  743. .filter(({ user }) =>
  744. (user.profile.fullname !== undefined && user.profile.fullname.toLowerCase().indexOf(filterTerm.toLowerCase()) !== -1)
  745. || user.profile.fullname === undefined && user.profile.username !== undefined && user.profile.username.toLowerCase().indexOf(filterTerm.toLowerCase()) !== -1)
  746. .map(({ member }) => member);
  747. }
  748. return members;
  749. }
  750. Template.editCardRequesterForm.onRendered(function () {
  751. autosize(this.$('.js-edit-card-requester'));
  752. });
  753. Template.editCardRequesterForm.events({
  754. 'keydown .js-edit-card-requester'(event) {
  755. // If enter key was pressed, submit the data
  756. if (event.keyCode === 13) {
  757. $('.js-submit-edit-card-requester-form').click();
  758. }
  759. },
  760. });
  761. Template.editCardAssignerForm.onRendered(function () {
  762. autosize(this.$('.js-edit-card-assigner'));
  763. });
  764. Template.editCardAssignerForm.events({
  765. 'keydown .js-edit-card-assigner'(event) {
  766. // If enter key was pressed, submit the data
  767. if (event.keyCode === 13) {
  768. $('.js-submit-edit-card-assigner-form').click();
  769. }
  770. },
  771. });
  772. /** Move Card Dialog */
  773. (class extends DialogWithBoardSwimlaneList {
  774. getDialogOptions() {
  775. const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
  776. return ret;
  777. }
  778. setDone(boardId, swimlaneId, listId, options) {
  779. ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
  780. const card = this.data();
  781. const minOrder = card.getMinSort(listId, swimlaneId);
  782. card.move(boardId, swimlaneId, listId, minOrder - 1);
  783. }
  784. }).register('moveCardPopup');
  785. /** Copy Card Dialog */
  786. (class extends DialogWithBoardSwimlaneList {
  787. getDialogOptions() {
  788. const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
  789. return ret;
  790. }
  791. setDone(boardId, swimlaneId, listId, options) {
  792. ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
  793. const card = this.data();
  794. // const textarea = $('#copy-card-title');
  795. const textarea = this.$('#copy-card-title');
  796. const title = textarea.val().trim();
  797. if (title) {
  798. // insert new card to the top of new list
  799. const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, true, {title: title});
  800. // In case the filter is active we need to add the newly inserted card in
  801. // the list of exceptions -- cards that are not filtered. Otherwise the
  802. // card will disappear instantly.
  803. // See https://github.com/wekan/wekan/issues/80
  804. Filter.addException(newCardId);
  805. }
  806. }
  807. }).register('copyCardPopup');
  808. /** Convert Checklist-Item to card dialog */
  809. (class extends DialogWithBoardSwimlaneList {
  810. getDialogOptions() {
  811. const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
  812. return ret;
  813. }
  814. setDone(boardId, swimlaneId, listId, options) {
  815. ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
  816. const card = this.data();
  817. const textarea = this.$('#copy-card-title');
  818. const title = textarea.val().trim();
  819. if (title) {
  820. const _id = Cards.insert({
  821. title: title,
  822. listId: listId,
  823. boardId: boardId,
  824. swimlaneId: swimlaneId,
  825. sort: 0,
  826. });
  827. const card = ReactiveCache.getCard(_id);
  828. const minOrder = card.getMinSort();
  829. card.move(card.boardId, card.swimlaneId, card.listId, minOrder - 1);
  830. Filter.addException(_id);
  831. }
  832. }
  833. }).register('convertChecklistItemToCardPopup');
  834. /** Copy many cards dialog */
  835. (class extends DialogWithBoardSwimlaneList {
  836. getDialogOptions() {
  837. const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
  838. return ret;
  839. }
  840. setDone(boardId, swimlaneId, listId, options) {
  841. ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
  842. const card = this.data();
  843. const textarea = this.$('#copy-card-title');
  844. const title = textarea.val().trim();
  845. if (title) {
  846. const titleList = JSON.parse(title);
  847. for (const obj of titleList) {
  848. const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, false, {title: obj.title, description: obj.description});
  849. // In case the filter is active we need to add the newly inserted card in
  850. // the list of exceptions -- cards that are not filtered. Otherwise the
  851. // card will disappear instantly.
  852. // See https://github.com/wekan/wekan/issues/80
  853. Filter.addException(newCardId);
  854. }
  855. }
  856. }
  857. }).register('copyManyCardsPopup');
  858. BlazeComponent.extendComponent({
  859. onCreated() {
  860. this.currentCard = this.currentData();
  861. this.currentColor = new ReactiveVar(this.currentCard.color);
  862. },
  863. colors() {
  864. return ALLOWED_COLORS.map((color) => ({ color, name: '' }));
  865. },
  866. isSelected(color) {
  867. if (this.currentColor.get() === null) {
  868. return color === 'white';
  869. }
  870. return this.currentColor.get() === color;
  871. },
  872. events() {
  873. return [
  874. {
  875. 'click .js-palette-color'() {
  876. this.currentColor.set(this.currentData().color);
  877. },
  878. 'click .js-submit'(event) {
  879. event.preventDefault();
  880. this.currentCard.setColor(this.currentColor.get());
  881. Popup.back();
  882. },
  883. 'click .js-remove-color'(event) {
  884. event.preventDefault();
  885. this.currentCard.setColor(null);
  886. Popup.back();
  887. },
  888. },
  889. ];
  890. },
  891. }).register('setCardColorPopup');
  892. BlazeComponent.extendComponent({
  893. onCreated() {
  894. this.currentCard = this.currentData();
  895. this.parentBoard = new ReactiveVar(null);
  896. this.parentCard = this.currentCard.parentCard();
  897. if (this.parentCard) {
  898. const list = $('.js-field-parent-card');
  899. list.val(this.parentCard._id);
  900. this.parentBoard.set(this.parentCard.board()._id);
  901. } else {
  902. this.parentBoard.set(null);
  903. }
  904. },
  905. boards() {
  906. const ret = ReactiveCache.getBoards(
  907. {
  908. archived: false,
  909. 'members.userId': Meteor.userId(),
  910. _id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
  911. },
  912. {
  913. sort: { sort: 1 /* boards default sorting */ },
  914. },
  915. );
  916. return ret;
  917. },
  918. cards() {
  919. const currentId = Utils.getCurrentCardId();
  920. if (this.parentBoard.get()) {
  921. const ret = ReactiveCache.getCards({
  922. boardId: this.parentBoard.get(),
  923. _id: { $ne: currentId },
  924. });
  925. return ret;
  926. } else {
  927. return [];
  928. }
  929. },
  930. isParentBoard() {
  931. const board = this.currentData();
  932. if (this.parentBoard.get()) {
  933. return board._id === this.parentBoard.get();
  934. }
  935. return false;
  936. },
  937. isParentCard() {
  938. const card = this.currentData();
  939. if (this.parentCard) {
  940. return card._id === this.parentCard;
  941. }
  942. return false;
  943. },
  944. setParentCardId(cardId) {
  945. if (cardId) {
  946. this.parentCard = ReactiveCache.getCard(cardId);
  947. } else {
  948. this.parentCard = null;
  949. }
  950. this.currentCard.setParentId(cardId);
  951. },
  952. events() {
  953. return [
  954. {
  955. 'click .js-copy-card-link-to-clipboard'(event) {
  956. const promise = Utils.copyTextToClipboard(location.origin + document.getElementById('cardURL').value);
  957. const $tooltip = this.$('.copied-tooltip');
  958. Utils.showCopied(promise, $tooltip);
  959. },
  960. 'click .js-delete': Popup.afterConfirm('cardDelete', function () {
  961. Popup.close();
  962. // verify that there are no linked cards
  963. if (ReactiveCache.getCards({ linkedId: this._id }).length === 0) {
  964. Cards.remove(this._id);
  965. } else {
  966. // TODO: Maybe later we can list where the linked cards are.
  967. // Now here is popup with a hint that the card cannot be deleted
  968. // as there are linked cards.
  969. // Related:
  970. // client/components/lists/listHeader.js about line 248
  971. // https://github.com/wekan/wekan/issues/2785
  972. const message = `${TAPi18n.__(
  973. 'delete-linked-card-before-this-card',
  974. )} linkedId: ${this._id
  975. } at client/components/cards/cardDetails.js and https://github.com/wekan/wekan/issues/2785`;
  976. alert(message);
  977. }
  978. Utils.goBoardId(this.boardId);
  979. }),
  980. 'change .js-field-parent-board'(event) {
  981. const selection = $(event.currentTarget).val();
  982. const list = $('.js-field-parent-card');
  983. if (selection === 'none') {
  984. this.parentBoard.set(null);
  985. } else {
  986. subManager.subscribe('board', $(event.currentTarget).val(), false);
  987. this.parentBoard.set(selection);
  988. list.prop('disabled', false);
  989. }
  990. this.setParentCardId(null);
  991. },
  992. 'change .js-field-parent-card'(event) {
  993. const selection = $(event.currentTarget).val();
  994. this.setParentCardId(selection);
  995. },
  996. },
  997. ];
  998. },
  999. }).register('cardMorePopup');
  1000. BlazeComponent.extendComponent({
  1001. onCreated() {
  1002. this.currentCard = this.currentData();
  1003. this.voteQuestion = new ReactiveVar(this.currentCard.voteQuestion);
  1004. },
  1005. events() {
  1006. return [
  1007. {
  1008. 'click .js-end-date': Popup.open('editVoteEndDate'),
  1009. 'submit .edit-vote-question'(evt) {
  1010. evt.preventDefault();
  1011. const voteQuestion = evt.target.vote.value;
  1012. const publicVote = $('#vote-public').hasClass('is-checked');
  1013. const allowNonBoardMembers = $('#vote-allow-non-members').hasClass(
  1014. 'is-checked',
  1015. );
  1016. const endString = this.currentCard.getVoteEnd();
  1017. Meteor.call('cards.setVoteQuestion', this.currentCard._id, voteQuestion, publicVote, allowNonBoardMembers);
  1018. if (endString) {
  1019. Meteor.call('cards.setVoteEnd', this.currentCard._id, endString);
  1020. }
  1021. Popup.back();
  1022. },
  1023. 'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
  1024. event.preventDefault();
  1025. Meteor.call('cards.unsetVote', this.currentCard._id);
  1026. Popup.back();
  1027. }),
  1028. 'click a.js-toggle-vote-public'(event) {
  1029. event.preventDefault();
  1030. $('#vote-public').toggleClass('is-checked');
  1031. },
  1032. 'click a.js-toggle-vote-allow-non-members'(event) {
  1033. event.preventDefault();
  1034. $('#vote-allow-non-members').toggleClass('is-checked');
  1035. },
  1036. },
  1037. ];
  1038. },
  1039. }).register('cardStartVotingPopup');
  1040. // editVoteEndDatePopup
  1041. (class extends DatePicker {
  1042. onCreated() {
  1043. super.onCreated(formatDateTime(now()));
  1044. this.data().getVoteEnd() && this.date.set(new Date(this.data().getVoteEnd()));
  1045. }
  1046. events() {
  1047. return [
  1048. {
  1049. 'submit .edit-date'(evt) {
  1050. evt.preventDefault();
  1051. // if no time was given, init with 12:00
  1052. const time =
  1053. evt.target.time.value ||
  1054. formatTime(new Date().setHours(12, 0, 0));
  1055. const dateString = `${evt.target.date.value} ${time}`;
  1056. /*
  1057. const newDate = parseDate(dateString, ['L LT'], true);
  1058. if (newDate.isValid()) {
  1059. // if active vote - store it
  1060. if (this.currentData().getVoteQuestion()) {
  1061. this._storeDate(newDate.toDate());
  1062. Popup.back();
  1063. } else {
  1064. this.currentData().vote = { end: newDate.toDate() }; // set vote end temp
  1065. Popup.back();
  1066. }
  1067. */
  1068. // Try to parse different date formats using native Date parsing
  1069. const formats = [
  1070. 'YYYY-MM-DD HH:mm',
  1071. 'MM/DD/YYYY HH:mm',
  1072. 'DD.MM.YYYY HH:mm',
  1073. 'DD/MM/YYYY HH:mm',
  1074. 'DD-MM-YYYY HH:mm'
  1075. ];
  1076. let parsedDate = null;
  1077. for (const format of formats) {
  1078. parsedDate = parseDate(dateString, [format], true);
  1079. if (parsedDate) break;
  1080. }
  1081. // Fallback to native Date parsing
  1082. if (!parsedDate) {
  1083. parsedDate = new Date(dateString);
  1084. }
  1085. if (isValidDate(parsedDate)) {
  1086. // if active poker - store it
  1087. if (this.currentData().getPokerQuestion()) {
  1088. this._storeDate(usaDate.toDate());
  1089. } else {
  1090. this.currentData().poker = { end: usaDate.toDate() }; // set poker end temp
  1091. }
  1092. Popup.back();
  1093. } else if (euroAmDate.isValid()) {
  1094. // if active poker - store it
  1095. if (this.currentData().getPokerQuestion()) {
  1096. this._storeDate(euroAmDate.toDate());
  1097. } else {
  1098. this.currentData().poker = { end: euroAmDate.toDate() }; // set poker end temp
  1099. }
  1100. Popup.back();
  1101. } else if (euro24hDate.isValid()) {
  1102. // if active poker - store it
  1103. if (this.currentData().getPokerQuestion()) {
  1104. this._storeDate(euro24hDate.toDate());
  1105. this.card.setPokerEnd(euro24hDate.toDate());
  1106. } else {
  1107. this.currentData().poker = { end: euro24hDate.toDate() }; // set poker end temp
  1108. }
  1109. Popup.back();
  1110. } else if (eurodotDate.isValid()) {
  1111. // if active poker - store it
  1112. if (this.currentData().getPokerQuestion()) {
  1113. this._storeDate(eurodotDate.toDate());
  1114. this.card.setPokerEnd(eurodotDate.toDate());
  1115. } else {
  1116. this.currentData().poker = { end: eurodotDate.toDate() }; // set poker end temp
  1117. }
  1118. Popup.back();
  1119. } else if (minusDate.isValid()) {
  1120. // if active poker - store it
  1121. if (this.currentData().getPokerQuestion()) {
  1122. this._storeDate(minusDate.toDate());
  1123. this.card.setPokerEnd(minusDate.toDate());
  1124. } else {
  1125. this.currentData().poker = { end: minusDate.toDate() }; // set poker end temp
  1126. }
  1127. Popup.back();
  1128. } else if (slashDate.isValid()) {
  1129. // if active poker - store it
  1130. if (this.currentData().getPokerQuestion()) {
  1131. this._storeDate(slashDate.toDate());
  1132. this.card.setPokerEnd(slashDate.toDate());
  1133. } else {
  1134. this.currentData().poker = { end: slashDate.toDate() }; // set poker end temp
  1135. }
  1136. Popup.back();
  1137. } else if (dotDate.isValid()) {
  1138. // if active poker - store it
  1139. if (this.currentData().getPokerQuestion()) {
  1140. this._storeDate(dotDate.toDate());
  1141. this.card.setPokerEnd(dotDate.toDate());
  1142. } else {
  1143. this.currentData().poker = { end: dotDate.toDate() }; // set poker end temp
  1144. }
  1145. Popup.back();
  1146. } else if (brezhonegDate.isValid()) {
  1147. // if active poker - store it
  1148. if (this.currentData().getPokerQuestion()) {
  1149. this._storeDate(brezhonegDate.toDate());
  1150. this.card.setPokerEnd(brezhonegDate.toDate());
  1151. } else {
  1152. this.currentData().poker = { end: brezhonegDate.toDate() }; // set poker end temp
  1153. }
  1154. Popup.back();
  1155. } else if (hrvatskiDate.isValid()) {
  1156. // if active poker - store it
  1157. if (this.currentData().getPokerQuestion()) {
  1158. this._storeDate(hrvatskiDate.toDate());
  1159. this.card.setPokerEnd(hrvatskiDate.toDate());
  1160. } else {
  1161. this.currentData().poker = { end: hrvatskiDate.toDate() }; // set poker end temp
  1162. Popup.back();
  1163. }
  1164. } else if (latviaDate.isValid()) {
  1165. // if active poker - store it
  1166. if (this.currentData().getPokerQuestion()) {
  1167. this._storeDate(latviaDate.toDate());
  1168. this.card.setPokerEnd(latviaDate.toDate());
  1169. } else {
  1170. this.currentData().poker = { end: latviaDate.toDate() }; // set poker end temp
  1171. }
  1172. Popup.back();
  1173. } else if (nederlandsDate.isValid()) {
  1174. // if active poker - store it
  1175. if (this.currentData().getPokerQuestion()) {
  1176. this._storeDate(nederlandsDate.toDate());
  1177. this.card.setPokerEnd(nederlandsDate.toDate());
  1178. } else {
  1179. this.currentData().poker = { end: nederlandsDate.toDate() }; // set poker end temp
  1180. }
  1181. Popup.back();
  1182. } else if (greekDate.isValid()) {
  1183. // if active poker - store it
  1184. if (this.currentData().getPokerQuestion()) {
  1185. this._storeDate(greekDate.toDate());
  1186. this.card.setPokerEnd(greekDate.toDate());
  1187. } else {
  1188. this.currentData().poker = { end: greekDate.toDate() }; // set poker end temp
  1189. }
  1190. Popup.back();
  1191. } else if (macedonianDate.isValid()) {
  1192. // if active poker - store it
  1193. if (this.currentData().getPokerQuestion()) {
  1194. this._storeDate(macedonianDate.toDate());
  1195. this.card.setPokerEnd(macedonianDate.toDate());
  1196. } else {
  1197. this.currentData().poker = { end: macedonianDate.toDate() }; // set poker end temp
  1198. }
  1199. Popup.back();
  1200. } else {
  1201. this.error.set('invalid-date');
  1202. evt.target.date.focus();
  1203. }
  1204. },
  1205. 'click .js-delete-date'(evt) {
  1206. evt.preventDefault();
  1207. this._deleteDate();
  1208. Popup.back();
  1209. },
  1210. },
  1211. ];
  1212. }
  1213. _storeDate(newDate) {
  1214. Meteor.call('cards.setVoteEnd', this.card._id, newDate);
  1215. }
  1216. _deleteDate() {
  1217. Meteor.call('cards.unsetVoteEnd', this.card._id);
  1218. }
  1219. }.register('editVoteEndDatePopup'));
  1220. BlazeComponent.extendComponent({
  1221. onCreated() {
  1222. this.currentCard = this.currentData();
  1223. this.pokerQuestion = new ReactiveVar(this.currentCard.pokerQuestion);
  1224. },
  1225. events() {
  1226. return [
  1227. {
  1228. 'click .js-end-date': Popup.open('editPokerEndDate'),
  1229. 'submit .edit-poker-question'(evt) {
  1230. evt.preventDefault();
  1231. const pokerQuestion = true;
  1232. const allowNonBoardMembers = $('#poker-allow-non-members').hasClass(
  1233. 'is-checked',
  1234. );
  1235. const endString = this.currentCard.getPokerEnd();
  1236. Meteor.call('cards.setPokerQuestion', this.currentCard._id, pokerQuestion, allowNonBoardMembers);
  1237. if (endString) {
  1238. Meteor.call('cards.setPokerEnd', this.currentCard._id, new Date(endString));
  1239. }
  1240. Popup.back();
  1241. },
  1242. 'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => {
  1243. Meteor.call('cards.unsetPoker', this.currentCard._id);
  1244. Popup.back();
  1245. }),
  1246. 'click a.js-toggle-poker-allow-non-members'(event) {
  1247. event.preventDefault();
  1248. $('#poker-allow-non-members').toggleClass('is-checked');
  1249. },
  1250. },
  1251. ];
  1252. },
  1253. }).register('cardStartPlanningPokerPopup');
  1254. // editPokerEndDatePopup
  1255. (class extends DatePicker {
  1256. onCreated() {
  1257. super.onCreated(formatDateTime(now()));
  1258. this.data().getPokerEnd() &&
  1259. this.date.set(new Date(this.data().getPokerEnd()));
  1260. }
  1261. /*
  1262. Tried to use dateFormat and timeFormat from client/components/lib/datepicker.js
  1263. to make detecting all date formats not necessary,
  1264. but got error "language mk does not exist".
  1265. Maybe client/components/lib/datepicker.jade could have hidden input field for
  1266. datepicker format that could be used to detect date format?
  1267. dateFormat() {
  1268. return moment.localeData().longDateFormat('L');
  1269. }
  1270. timeFormat() {
  1271. return moment.localeData().longDateFormat('LT');
  1272. }
  1273. const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
  1274. */
  1275. events() {
  1276. return [
  1277. {
  1278. 'submit .edit-date'(evt) {
  1279. evt.preventDefault();
  1280. // if no time was given, init with 12:00
  1281. const time =
  1282. evt.target.time.value ||
  1283. formatTime(new Date().setHours(12, 0, 0));
  1284. const dateString = `${evt.target.date.value} ${time}`;
  1285. /*
  1286. Tried to use dateFormat and timeFormat from client/components/lib/datepicker.js
  1287. to make detecting all date formats not necessary,
  1288. but got error "language mk does not exist".
  1289. Maybe client/components/lib/datepicker.jade could have hidden input field for
  1290. datepicker format that could be used to detect date format?
  1291. const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
  1292. if (newDate.isValid()) {
  1293. // if active poker - store it
  1294. if (this.currentData().getPokerQuestion()) {
  1295. this._storeDate(newDate.toDate());
  1296. Popup.back();
  1297. } else {
  1298. this.currentData().poker = { end: newDate.toDate() }; // set poker end temp
  1299. Popup.back();
  1300. }
  1301. */
  1302. // Try to parse different date formats using native Date parsing
  1303. const formats = [
  1304. 'YYYY-MM-DD HH:mm',
  1305. 'MM/DD/YYYY HH:mm',
  1306. 'DD.MM.YYYY HH:mm',
  1307. 'DD/MM/YYYY HH:mm',
  1308. 'DD-MM-YYYY HH:mm'
  1309. ];
  1310. let parsedDate = null;
  1311. for (const format of formats) {
  1312. parsedDate = parseDate(dateString, [format], true);
  1313. if (parsedDate) break;
  1314. }
  1315. // Fallback to native Date parsing
  1316. if (!parsedDate) {
  1317. parsedDate = new Date(dateString);
  1318. }
  1319. if (isValidDate(parsedDate)) {
  1320. // if active poker - store it
  1321. if (this.currentData().getPokerQuestion()) {
  1322. this._storeDate(usaDate.toDate());
  1323. } else {
  1324. this.currentData().poker = { end: usaDate.toDate() }; // set poker end temp
  1325. }
  1326. Popup.back();
  1327. } else if (euroAmDate.isValid()) {
  1328. // if active poker - store it
  1329. if (this.currentData().getPokerQuestion()) {
  1330. this._storeDate(euroAmDate.toDate());
  1331. } else {
  1332. this.currentData().poker = { end: euroAmDate.toDate() }; // set poker end temp
  1333. }
  1334. Popup.back();
  1335. } else if (euro24hDate.isValid()) {
  1336. // if active poker - store it
  1337. if (this.currentData().getPokerQuestion()) {
  1338. this._storeDate(euro24hDate.toDate());
  1339. this.card.setPokerEnd(euro24hDate.toDate());
  1340. } else {
  1341. this.currentData().poker = { end: euro24hDate.toDate() }; // set poker end temp
  1342. }
  1343. Popup.back();
  1344. } else if (eurodotDate.isValid()) {
  1345. // if active poker - store it
  1346. if (this.currentData().getPokerQuestion()) {
  1347. this._storeDate(eurodotDate.toDate());
  1348. this.card.setPokerEnd(eurodotDate.toDate());
  1349. } else {
  1350. this.currentData().poker = { end: eurodotDate.toDate() }; // set poker end temp
  1351. }
  1352. Popup.back();
  1353. } else if (minusDate.isValid()) {
  1354. // if active poker - store it
  1355. if (this.currentData().getPokerQuestion()) {
  1356. this._storeDate(minusDate.toDate());
  1357. this.card.setPokerEnd(minusDate.toDate());
  1358. } else {
  1359. this.currentData().poker = { end: minusDate.toDate() }; // set poker end temp
  1360. }
  1361. Popup.back();
  1362. } else if (slashDate.isValid()) {
  1363. // if active poker - store it
  1364. if (this.currentData().getPokerQuestion()) {
  1365. this._storeDate(slashDate.toDate());
  1366. this.card.setPokerEnd(slashDate.toDate());
  1367. } else {
  1368. this.currentData().poker = { end: slashDate.toDate() }; // set poker end temp
  1369. }
  1370. Popup.back();
  1371. } else if (dotDate.isValid()) {
  1372. // if active poker - store it
  1373. if (this.currentData().getPokerQuestion()) {
  1374. this._storeDate(dotDate.toDate());
  1375. this.card.setPokerEnd(dotDate.toDate());
  1376. } else {
  1377. this.currentData().poker = { end: dotDate.toDate() }; // set poker end temp
  1378. }
  1379. Popup.back();
  1380. } else if (brezhonegDate.isValid()) {
  1381. // if active poker - store it
  1382. if (this.currentData().getPokerQuestion()) {
  1383. this._storeDate(brezhonegDate.toDate());
  1384. this.card.setPokerEnd(brezhonegDate.toDate());
  1385. } else {
  1386. this.currentData().poker = { end: brezhonegDate.toDate() }; // set poker end temp
  1387. }
  1388. Popup.back();
  1389. } else if (hrvatskiDate.isValid()) {
  1390. // if active poker - store it
  1391. if (this.currentData().getPokerQuestion()) {
  1392. this._storeDate(hrvatskiDate.toDate());
  1393. this.card.setPokerEnd(hrvatskiDate.toDate());
  1394. } else {
  1395. this.currentData().poker = { end: hrvatskiDate.toDate() }; // set poker end temp
  1396. }
  1397. Popup.back();
  1398. } else if (latviaDate.isValid()) {
  1399. // if active poker - store it
  1400. if (this.currentData().getPokerQuestion()) {
  1401. this._storeDate(latviaDate.toDate());
  1402. this.card.setPokerEnd(latviaDate.toDate());
  1403. } else {
  1404. this.currentData().poker = { end: latviaDate.toDate() }; // set poker end temp
  1405. }
  1406. Popup.back();
  1407. } else if (nederlandsDate.isValid()) {
  1408. // if active poker - store it
  1409. if (this.currentData().getPokerQuestion()) {
  1410. this._storeDate(nederlandsDate.toDate());
  1411. this.card.setPokerEnd(nederlandsDate.toDate());
  1412. } else {
  1413. this.currentData().poker = { end: nederlandsDate.toDate() }; // set poker end temp
  1414. }
  1415. Popup.back();
  1416. } else if (greekDate.isValid()) {
  1417. // if active poker - store it
  1418. if (this.currentData().getPokerQuestion()) {
  1419. this._storeDate(greekDate.toDate());
  1420. this.card.setPokerEnd(greekDate.toDate());
  1421. } else {
  1422. this.currentData().poker = { end: greekDate.toDate() }; // set poker end temp
  1423. }
  1424. Popup.back();
  1425. } else if (macedonianDate.isValid()) {
  1426. // if active poker - store it
  1427. if (this.currentData().getPokerQuestion()) {
  1428. this._storeDate(macedonianDate.toDate());
  1429. this.card.setPokerEnd(macedonianDate.toDate());
  1430. } else {
  1431. this.currentData().poker = { end: macedonianDate.toDate() }; // set poker end temp
  1432. }
  1433. Popup.back();
  1434. } else {
  1435. // this.error.set('invalid-date);
  1436. this.error.set('invalid-date' + ' ' + dateString);
  1437. evt.target.date.focus();
  1438. }
  1439. },
  1440. 'click .js-delete-date'(evt) {
  1441. evt.preventDefault();
  1442. this._deleteDate();
  1443. Popup.back();
  1444. },
  1445. },
  1446. ];
  1447. }
  1448. _storeDate(newDate) {
  1449. Meteor.call('cards.setPokerEnd', this.card._id, newDate);
  1450. }
  1451. _deleteDate() {
  1452. Meteor.call('cards.unsetPokerEnd', this.card._id);
  1453. }
  1454. }.register('editPokerEndDatePopup'));
  1455. // Close the card details pane by pressing escape
  1456. EscapeActions.register(
  1457. 'detailsPane',
  1458. () => {
  1459. // if card description diverges from database due to editing
  1460. // ask user whether changes should be applied
  1461. if (ReactiveCache.getCurrentUser()) {
  1462. if (ReactiveCache.getCurrentUser().profile.rescueCardDescription == true) {
  1463. currentDescription = document.getElementsByClassName("editor js-new-description-input").item(0)
  1464. if (currentDescription?.value && !(currentDescription.value === Utils.getCurrentCard().getDescription())) {
  1465. if (confirm(TAPi18n.__('rescue-card-description-dialogue'))) {
  1466. Utils.getCurrentCard().setDescription(document.getElementsByClassName("editor js-new-description-input").item(0).value);
  1467. // Save it!
  1468. console.log(document.getElementsByClassName("editor js-new-description-input").item(0).value);
  1469. console.log("current description", Utils.getCurrentCard().getDescription());
  1470. } else {
  1471. // Do nothing!
  1472. console.log('Description changes were not saved to the database.');
  1473. }
  1474. }
  1475. }
  1476. }
  1477. if (Session.get('cardDetailsIsDragging')) {
  1478. // Reset dragging status as the mouse landed outside the cardDetails template area and this will prevent a mousedown event from firing
  1479. Session.set('cardDetailsIsDragging', false);
  1480. Session.set('cardDetailsIsMouseDown', false);
  1481. } else {
  1482. // Prevent close card when the user is selecting text and moves the mouse cursor outside the card detail area
  1483. Utils.goBoardId(Session.get('currentBoard'));
  1484. }
  1485. },
  1486. () => {
  1487. return !Session.equals('currentCard', null);
  1488. },
  1489. {
  1490. noClickEscapeOn: '.js-card-details,.board-sidebar,#header',
  1491. },
  1492. );
  1493. Template.cardAssigneesPopup.onCreated(function () {
  1494. let currBoard = Utils.getCurrentBoard();
  1495. let members = currBoard.activeMembers();
  1496. this.members = new ReactiveVar(members);
  1497. });
  1498. Template.cardAssigneesPopup.events({
  1499. 'click .js-select-assignee'(event) {
  1500. const card = Utils.getCurrentCard();
  1501. const assigneeId = this.userId;
  1502. card.toggleAssignee(assigneeId);
  1503. event.preventDefault();
  1504. },
  1505. 'keyup .card-assignees-filter'(event) {
  1506. const members = filterMembers(event.target.value);
  1507. Template.instance().members.set(members);
  1508. },
  1509. });
  1510. Template.cardAssigneesPopup.helpers({
  1511. isCardAssignee() {
  1512. const card = Template.parentData();
  1513. const cardAssignees = card.getAssignees();
  1514. return _.contains(cardAssignees, this.userId);
  1515. },
  1516. members() {
  1517. return _.sortBy(Template.instance().members.get(),'fullname');
  1518. },
  1519. user() {
  1520. return ReactiveCache.getUser(this.userId);
  1521. },
  1522. });
  1523. Template.cardAssigneePopup.helpers({
  1524. userData() {
  1525. return ReactiveCache.getUser(this.userId, {
  1526. fields: {
  1527. profile: 1,
  1528. username: 1,
  1529. },
  1530. });
  1531. },
  1532. memberType() {
  1533. const user = ReactiveCache.getUser(this.userId);
  1534. return user && user.isBoardAdmin() ? 'admin' : 'normal';
  1535. },
  1536. isCardAssignee() {
  1537. const card = Template.parentData();
  1538. const cardAssignees = card.getAssignees();
  1539. return _.contains(cardAssignees, this.userId);
  1540. },
  1541. user() {
  1542. return ReactiveCache.getUser(this.userId);
  1543. },
  1544. });
  1545. Template.cardAssigneePopup.events({
  1546. 'click .js-remove-assignee'() {
  1547. ReactiveCache.getCard(this.cardId).unassignAssignee(this.userId);
  1548. Popup.back();
  1549. },
  1550. 'click .js-edit-profile': Popup.open('editProfile'),
  1551. });