swimlanes.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. const { calculateIndex } = Utils;
  2. function currentListIsInThisSwimlane(swimlaneId) {
  3. const currentList = Lists.findOne(Session.get('currentList'));
  4. return (
  5. currentList &&
  6. (currentList.swimlaneId === swimlaneId || currentList.swimlaneId === '')
  7. );
  8. }
  9. function currentCardIsInThisList(listId, swimlaneId) {
  10. const currentCard = Utils.getCurrentCard();
  11. //const currentUser = Meteor.user();
  12. if (
  13. //currentUser &&
  14. //currentUser.profile &&
  15. Utils.boardView() === 'board-view-swimlanes'
  16. )
  17. return (
  18. currentCard &&
  19. currentCard.listId === listId &&
  20. currentCard.swimlaneId === swimlaneId
  21. );
  22. else if (
  23. //currentUser &&
  24. //currentUser.profile &&
  25. Utils.boardView() === 'board-view-lists'
  26. )
  27. return (
  28. currentCard &&
  29. currentCard.listId === listId
  30. );
  31. // https://github.com/wekan/wekan/issues/1623
  32. // https://github.com/ChronikEwok/wekan/commit/cad9b20451bb6149bfb527a99b5001873b06c3de
  33. // TODO: In public board, if you would like to switch between List/Swimlane view, you could
  34. // 1) If there is no view cookie, save to cookie board-view-lists
  35. // board-view-lists / board-view-swimlanes / board-view-cal
  36. // 2) If public user changes clicks board-view-lists then change view and
  37. // then change view and save cookie with view value
  38. // without using currentuser above, because currentuser is null.
  39. }
  40. function initSortable(boardComponent, $listsDom) {
  41. // We want to animate the card details window closing. We rely on CSS
  42. // transition for the actual animation.
  43. $listsDom._uihooks = {
  44. removeElement(node) {
  45. const removeNode = _.once(() => {
  46. node.parentNode.removeChild(node);
  47. });
  48. if ($(node).hasClass('js-card-details')) {
  49. $(node).css({
  50. flexBasis: 0,
  51. padding: 0,
  52. });
  53. $listsDom.one(CSSEvents.transitionend, removeNode);
  54. } else {
  55. removeNode();
  56. }
  57. },
  58. };
  59. $listsDom.sortable({
  60. connectWith: '.board-canvas',
  61. tolerance: 'pointer',
  62. helper: 'clone',
  63. items: '.js-list:not(.js-list-composer)',
  64. placeholder: 'js-list placeholder',
  65. distance: 7,
  66. start(evt, ui) {
  67. ui.placeholder.height(ui.helper.height());
  68. ui.placeholder.width(ui.helper.width());
  69. EscapeActions.executeUpTo('popup-close');
  70. boardComponent.setIsDragging(true);
  71. },
  72. stop(evt, ui) {
  73. // To attribute the new index number, we need to get the DOM element
  74. // of the previous and the following card -- if any.
  75. const prevListDom = ui.item.prev('.js-list').get(0);
  76. const nextListDom = ui.item.next('.js-list').get(0);
  77. const sortIndex = calculateIndex(prevListDom, nextListDom, 1);
  78. $listsDom.sortable('cancel');
  79. const listDomElement = ui.item.get(0);
  80. const list = Blaze.getData(listDomElement);
  81. /*
  82. Reverted incomplete change list width,
  83. removed from below Lists.update:
  84. https://github.com/wekan/wekan/issues/4558
  85. $set: {
  86. width: list._id.width(),
  87. height: list._id.height(),
  88. */
  89. Lists.update(list._id, {
  90. $set: {
  91. sort: sortIndex.base,
  92. },
  93. });
  94. boardComponent.setIsDragging(false);
  95. },
  96. });
  97. //function userIsMember() {
  98. // return (
  99. // Meteor.user() &&
  100. // Meteor.user().isBoardMember() &&
  101. // !Meteor.user().isCommentOnly() &&
  102. // !Meteor.user().isWorker()
  103. // );
  104. //}
  105. boardComponent.autorun(() => {
  106. if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
  107. $listsDom.sortable({
  108. handle: '.js-list-handle',
  109. });
  110. } else {
  111. $listsDom.sortable({
  112. handle: '.js-list-header',
  113. });
  114. }
  115. const $listDom = $listsDom;
  116. if ($listDom.data('uiSortable') || $listDom.data('sortable')) {
  117. $listsDom.sortable(
  118. 'option',
  119. 'disabled',
  120. // Disable drag-dropping when user is not member/is worker
  121. //!userIsMember() || Meteor.user().isWorker(),
  122. !Meteor.user() || !Meteor.user().isBoardAdmin(),
  123. // Not disable drag-dropping while in multi-selection mode
  124. // MultiSelection.isActive() || !userIsMember(),
  125. );
  126. }
  127. });
  128. }
  129. BlazeComponent.extendComponent({
  130. onRendered() {
  131. const boardComponent = this.parentComponent();
  132. const $listsDom = this.$('.js-lists');
  133. if (!Utils.getCurrentCardId()) {
  134. boardComponent.scrollLeft();
  135. }
  136. initSortable(boardComponent, $listsDom);
  137. },
  138. onCreated() {
  139. this.draggingActive = new ReactiveVar(false);
  140. this._isDragging = false;
  141. this._lastDragPositionX = 0;
  142. },
  143. id() {
  144. return this._id;
  145. },
  146. currentCardIsInThisList(listId, swimlaneId) {
  147. return currentCardIsInThisList(listId, swimlaneId);
  148. },
  149. currentListIsInThisSwimlane(swimlaneId) {
  150. return currentListIsInThisSwimlane(swimlaneId);
  151. },
  152. visible(list) {
  153. if (list.archived) {
  154. // Show archived list only when filter archive is on
  155. if (!Filter.archive.isSelected()) {
  156. return false;
  157. }
  158. }
  159. if (Filter.lists._isActive()) {
  160. if (!list.title.match(Filter.lists.getRegexSelector())) {
  161. return false;
  162. }
  163. }
  164. if (Filter.hideEmpty.isSelected()) {
  165. const swimlaneId = this.parentComponent()
  166. .parentComponent()
  167. .data()._id;
  168. const cards = list.cards(swimlaneId);
  169. if (cards.count() === 0) {
  170. return false;
  171. }
  172. }
  173. return true;
  174. },
  175. events() {
  176. return [
  177. {
  178. // Click-and-drag action
  179. 'mousedown .board-canvas'(evt) {
  180. // Translating the board canvas using the click-and-drag action can
  181. // conflict with the build-in browser mechanism to select text. We
  182. // define a list of elements in which we disable the dragging because
  183. // the user will legitimately expect to be able to select some text with
  184. // his mouse.
  185. const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
  186. Utils.isTouchScreenOrShowDesktopDragHandles()
  187. ? ['.js-list-handle', '.js-swimlane-header-handle']
  188. : ['.js-list-header'],
  189. );
  190. if (
  191. $(evt.target).closest(noDragInside.join(',')).length === 0 &&
  192. this.$('.swimlane').prop('clientHeight') > evt.offsetY
  193. ) {
  194. this._isDragging = true;
  195. this._lastDragPositionX = evt.clientX;
  196. }
  197. },
  198. mouseup() {
  199. if (this._isDragging) {
  200. this._isDragging = false;
  201. }
  202. },
  203. mousemove(evt) {
  204. if (this._isDragging) {
  205. // Update the canvas position
  206. this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX;
  207. this._lastDragPositionX = evt.clientX;
  208. // Disable browser text selection while dragging
  209. evt.stopPropagation();
  210. evt.preventDefault();
  211. // Don't close opened card or inlined form at the end of the
  212. // click-and-drag.
  213. EscapeActions.executeUpTo('popup-close');
  214. EscapeActions.preventNextClick();
  215. }
  216. },
  217. },
  218. ];
  219. },
  220. }).register('swimlane');
  221. BlazeComponent.extendComponent({
  222. onCreated() {
  223. this.currentBoard = Boards.findOne(Session.get('currentBoard'));
  224. this.isListTemplatesSwimlane =
  225. this.currentBoard.isTemplatesBoard() &&
  226. this.currentData().isListTemplatesSwimlane();
  227. this.currentSwimlane = this.currentData();
  228. },
  229. // Proxy
  230. open() {
  231. this.childComponents('inlinedForm')[0].open();
  232. },
  233. events() {
  234. return [
  235. {
  236. submit(evt) {
  237. evt.preventDefault();
  238. const lastList = this.currentBoard.getLastList();
  239. const titleInput = this.find('.list-name-input');
  240. const title = titleInput.value.trim();
  241. let sortIndex = 0
  242. if (lastList) {
  243. const positionInput = this.find('.list-position-input');
  244. const position = positionInput.value.trim();
  245. const ret = Lists.findOne({ boardId: Session.get('currentBoard'), _id: position, archived: false })
  246. sortIndex = parseInt(JSON.stringify(ret['sort']))
  247. sortIndex = sortIndex+1
  248. } else {
  249. sortIndex = Utils.calculateIndexData(lastList, null).base;
  250. }
  251. if (title) {
  252. Lists.insert({
  253. title,
  254. boardId: Session.get('currentBoard'),
  255. sort: sortIndex,
  256. type: this.isListTemplatesSwimlane ? 'template-list' : 'list',
  257. swimlaneId: this.currentBoard.isTemplatesBoard()
  258. ? this.currentSwimlane._id
  259. : '',
  260. });
  261. titleInput.value = '';
  262. titleInput.focus();
  263. }
  264. },
  265. 'click .js-list-template': Popup.open('searchElement'),
  266. },
  267. ];
  268. },
  269. }).register('addListForm');
  270. Template.swimlane.helpers({
  271. canSeeAddList() {
  272. return Meteor.user().isBoardAdmin();
  273. /*
  274. Meteor.user() &&
  275. Meteor.user().isBoardMember() &&
  276. !Meteor.user().isCommentOnly() &&
  277. !Meteor.user().isWorker()
  278. */
  279. },
  280. });
  281. BlazeComponent.extendComponent({
  282. currentCardIsInThisList(listId, swimlaneId) {
  283. return currentCardIsInThisList(listId, swimlaneId);
  284. },
  285. visible(list) {
  286. if (list.archived) {
  287. // Show archived list only when filter archive is on
  288. if (!Filter.archive.isSelected()) {
  289. return false;
  290. }
  291. }
  292. if (Filter.lists._isActive()) {
  293. if (!list.title.match(Filter.lists.getRegexSelector())) {
  294. return false;
  295. }
  296. }
  297. if (Filter.hideEmpty.isSelected()) {
  298. const swimlaneId = this.parentComponent()
  299. .parentComponent()
  300. .data()._id;
  301. const cards = list.cards(swimlaneId);
  302. if (cards.count() === 0) {
  303. return false;
  304. }
  305. }
  306. return true;
  307. },
  308. onRendered() {
  309. const boardComponent = this.parentComponent();
  310. const $listsDom = this.$('.js-lists');
  311. if (!Utils.getCurrentCardId()) {
  312. boardComponent.scrollLeft();
  313. }
  314. initSortable(boardComponent, $listsDom);
  315. },
  316. }).register('listsGroup');
  317. class MoveSwimlaneComponent extends BlazeComponent {
  318. serverMethod = 'moveSwimlane';
  319. onCreated() {
  320. this.currentSwimlane = this.currentData();
  321. }
  322. board() {
  323. return Boards.findOne(Session.get('currentBoard'));
  324. }
  325. toBoardsSelector() {
  326. return {
  327. archived: false,
  328. 'members.userId': Meteor.userId(),
  329. type: 'board',
  330. _id: { $ne: this.board()._id },
  331. };
  332. }
  333. toBoards() {
  334. return Boards.find(this.toBoardsSelector(), { sort: { title: 1 } });
  335. }
  336. events() {
  337. return [
  338. {
  339. 'click .js-done'() {
  340. // const swimlane = Swimlanes.findOne(this.currentSwimlane._id);
  341. const bSelect = $('.js-select-boards')[0];
  342. let boardId;
  343. if (bSelect) {
  344. boardId = bSelect.options[bSelect.selectedIndex].value;
  345. Meteor.call(this.serverMethod, this.currentSwimlane._id, boardId);
  346. }
  347. Popup.back();
  348. },
  349. },
  350. ];
  351. }
  352. }
  353. MoveSwimlaneComponent.register('moveSwimlanePopup');
  354. (class extends MoveSwimlaneComponent {
  355. serverMethod = 'copySwimlane';
  356. toBoardsSelector() {
  357. const selector = super.toBoardsSelector();
  358. delete selector._id;
  359. return selector;
  360. }
  361. }.register('copySwimlanePopup'));