cardDetails.js 56 KB

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