diff --git a/CHANGELOG.md b/CHANGELOG.md index f105915fa..f87199c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# v2.29 2019-02-27 Wekan release + +This release adds the following new features: + +- Swimlane/List/Board/Card templates. In Progress, please test and [add comment if you find not listed bugs](https://github.com/wekan/wekan/issues/2165). + Thanks to GitHub user andresmanelli. + # v2.28 2019-02-27 Wekan release This release adds the following new Sandstorm features and fixes: diff --git a/Stackerfile.yml b/Stackerfile.yml index 294bdb9a3..031953fef 100644 --- a/Stackerfile.yml +++ b/Stackerfile.yml @@ -1,5 +1,5 @@ appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928 -appVersion: "v2.28.0" +appVersion: "v2.29.0" files: userUploads: - README.md diff --git a/client/components/boards/boardArchive.js b/client/components/boards/boardArchive.js index 8f4d54345..c8bbb3419 100644 --- a/client/components/boards/boardArchive.js +++ b/client/components/boards/boardArchive.js @@ -1,9 +1,3 @@ -Template.boardListHeaderBar.events({ - 'click .js-open-archived-board'() { - Modal.open('archivedBoards'); - }, -}); - BlazeComponent.extendComponent({ onCreated() { this.subscribe('archivedBoards'); diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade index 3a40921d0..32f8629f3 100644 --- a/client/components/boards/boardBody.jade +++ b/client/components/boards/boardBody.jade @@ -20,12 +20,15 @@ template(name="boardBody") class="{{#if draggingActive.get}}is-dragging-active{{/if}}") if showOverlay.get .board-overlay - if isViewSwimlanes + if currentBoard.isTemplatesBoard each currentBoard.swimlanes +swimlane(this) - if isViewLists - +listsGroup - if isViewCalendar + else if isViewSwimlanes + each currentBoard.swimlanes + +swimlane(this) + else if isViewLists + +listsGroup(currentBoard) + else if isViewCalendar +calendarView template(name="calendarView") diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index 746dae09d..c4c9eeef3 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -94,10 +94,11 @@ template(name="boardHeaderBar") i.fa.fa-search span {{_ 'search'}} - a.board-header-btn.js-toggle-board-view( - title="{{_ 'board-view'}}") - i.fa.fa-th-large - span {{_ currentUser.profile.boardView}} + unless currentBoard.isTemplatesBoard + a.board-header-btn.js-toggle-board-view( + title="{{_ 'board-view'}}") + i.fa.fa-th-large + span {{_ currentUser.profile.boardView}} if canModifyBoard a.board-header-btn.js-multiselection-activate( @@ -130,7 +131,8 @@ template(name="boardMenuPopup") hr ul.pop-over-list li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}} - li: a.js-archive-board {{_ 'archive-board'}} + unless currentBoard.isTemplatesBoard + li: a.js-archive-board {{_ 'archive-board'}} li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}} hr ul.pop-over-list @@ -275,7 +277,10 @@ template(name="createBoard") input.primary.wide(type="submit" value="{{_ 'create'}}") span.quiet | {{_ 'or'}} - a.js-import-board {{_ 'import-board'}} + a.js-import-board {{_ 'import'}} + span.quiet + | / + a.js-board-template {{_ 'template'}} template(name="chooseBoardSource") ul.pop-over-list diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index 89f686ab4..492fda40d 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -304,6 +304,7 @@ const CreateBoard = BlazeComponent.extendComponent({ 'click .js-import': Popup.open('boardImportBoard'), submit: this.onSubmit, 'click .js-import-board': Popup.open('chooseBoardSource'), + 'click .js-board-template': Popup.open('searchElement'), }]; }, }).register('createBoardPopup'); diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade index 89852570d..e36b8fc6e 100644 --- a/client/components/boards/boardsList.jade +++ b/client/components/boards/boardsList.jade @@ -36,3 +36,6 @@ template(name="boardListHeaderBar") a.board-header-btn.js-open-archived-board i.fa.fa-archive span {{_ 'archives'}} + a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") + i.fa.fa-clone + span {{_ 'templates'}} diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js index 1ed881467..df495bb14 100644 --- a/client/components/boards/boardsList.js +++ b/client/components/boards/boardsList.js @@ -1,5 +1,20 @@ const subManager = new SubsManager(); +Template.boardListHeaderBar.events({ + 'click .js-open-archived-board'() { + Modal.open('archivedBoards'); + }, +}); + +Template.boardListHeaderBar.helpers({ + templatesBoardId() { + return Meteor.user().getTemplatesBoardId(); + }, + templatesBoardSlug() { + return Meteor.user().getTemplatesBoardSlug(); + }, +}); + BlazeComponent.extendComponent({ onCreated() { Meteor.subscribe('setting'); @@ -9,6 +24,7 @@ BlazeComponent.extendComponent({ return Boards.find({ archived: false, 'members.userId': Meteor.userId(), + type: 'board', }, { sort: ['title'], }); diff --git a/client/components/boards/miniboard.jade b/client/components/boards/miniboard.jade new file mode 100644 index 000000000..d1fb0b075 --- /dev/null +++ b/client/components/boards/miniboard.jade @@ -0,0 +1,8 @@ +template(name="miniboard") + .minicard( + class="minicard-{{colorClass}}") + .minicard-title + .handle + .fa.fa-arrows + +viewer + = title diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index a571e21a1..73a7a67d5 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -456,26 +456,9 @@ BlazeComponent.extendComponent({ }, }).register('boardsAndLists'); - -function cloneCheckList(_id, checklist) { - 'use strict'; - const checklistId = checklist._id; - checklist.cardId = _id; - checklist._id = null; - const newChecklistId = Checklists.insert(checklist); - ChecklistItems.find({checklistId}).forEach(function(item) { - item._id = null; - item.checklistId = newChecklistId; - item.cardId = _id; - ChecklistItems.insert(item); - }); -} - Template.copyCardPopup.events({ 'click .js-done'() { const card = Cards.findOne(Session.get('currentCard')); - const oldId = card._id; - card._id = null; const lSelect = $('.js-select-lists')[0]; card.listId = lSelect.options[lSelect.selectedIndex].value; const slSelect = $('.js-select-swimlanes')[0]; @@ -490,38 +473,13 @@ Template.copyCardPopup.events({ if (title) { card.title = title; card.coverId = ''; - const _id = Cards.insert(card); + const _id = card.copy(); // In case the filter is active we need to add the newly inserted card in // the list of exceptions -- cards that are not filtered. Otherwise the // card will disappear instantly. // See https://github.com/wekan/wekan/issues/80 Filter.addException(_id); - // copy checklists - let cursor = Checklists.find({cardId: oldId}); - cursor.forEach(function() { - cloneCheckList(_id, arguments[0]); - }); - - // copy subtasks - cursor = Cards.find({parentId: oldId}); - cursor.forEach(function() { - 'use strict'; - const subtask = arguments[0]; - subtask.parentId = _id; - subtask._id = null; - /* const newSubtaskId = */ Cards.insert(subtask); - }); - - // copy card comments - cursor = CardComments.find({cardId: oldId}); - cursor.forEach(function () { - 'use strict'; - const comment = arguments[0]; - comment.cardId = _id; - comment._id = null; - CardComments.insert(comment); - }); Popup.close(); } }, @@ -558,9 +516,8 @@ Template.copyChecklistToManyCardsPopup.events({ Filter.addException(_id); // copy checklists - let cursor = Checklists.find({cardId: oldId}); - cursor.forEach(function() { - cloneCheckList(_id, arguments[0]); + Checklists.find({cardId: oldId}).forEach((ch) => { + ch.copy(_id); }); // copy subtasks @@ -574,13 +531,8 @@ Template.copyChecklistToManyCardsPopup.events({ }); // copy card comments - cursor = CardComments.find({cardId: oldId}); - cursor.forEach(function () { - 'use strict'; - const comment = arguments[0]; - comment.cardId = _id; - comment._id = null; - CardComments.insert(comment); + CardComments.find({cardId: oldId}).forEach((cmt) => { + cmt.copy(_id); }); } Popup.close(); diff --git a/client/components/lists/listBody.jade b/client/components/lists/listBody.jade index d31070bd4..9a9c322aa 100644 --- a/client/components/lists/listBody.jade +++ b/client/components/lists/listBody.jade @@ -44,13 +44,19 @@ template(name="addCardForm") .add-controls.clearfix button.primary.confirm(type="submit") {{_ 'add'}} - span.quiet - | {{_ 'or'}} - a.js-link {{_ 'link'}} - span.quiet - |   - | / - a.js-search {{_ 'search'}} + unless currentBoard.isTemplatesBoard + unless currentBoard.isTemplateBoard + span.quiet + | {{_ 'or'}} + a.js-link {{_ 'link'}} + span.quiet + |   + | / + a.js-search {{_ 'search'}} + span.quiet + |   + | / + a.js-card-template {{_ 'template'}} template(name="autocompleteLabelLine") .minicard-label(class="card-label-{{colorName}}" title=labelName) @@ -60,11 +66,9 @@ template(name="linkCardPopup") label {{_ 'boards'}}: .link-board-wrapper select.js-select-boards + option(value="") each boards - if $eq _id currentBoard._id - option(value="{{_id}}" selected) {{_ 'current'}} - else - option(value="{{_id}}") {{title}} + option(value="{{_id}}") {{title}} input.primary.confirm.js-link-board(type="button" value="{{_ 'link'}}") label {{_ 'swimlanes'}}: @@ -85,19 +89,35 @@ template(name="linkCardPopup") .edit-controls.clearfix input.primary.confirm.js-done(type="button" value="{{_ 'link'}}") -template(name="searchCardPopup") - label {{_ 'boards'}}: - .link-board-wrapper - select.js-select-boards - each boards - if $eq _id currentBoard._id - option(value="{{_id}}" selected) {{_ 'current'}} - else +template(name="searchElementPopup") + unless isTemplateSearch + label {{_ 'boards'}}: + .link-board-wrapper + select.js-select-boards + option(value="") + each boards option(value="{{_id}}") {{title}} form.js-search-term-form input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus) .list-body.js-perfect-scrollbar.search-card-results .minicards.clearfix.js-minicards - each results - a.minicard-wrapper.js-minicard - +minicard(this) + if isBoardTemplateSearch + each results + a.minicard-wrapper.js-minicard + +miniboard(this) + if isListTemplateSearch + each results + a.minicard-wrapper.js-minicard + +minilist(this) + if isSwimlaneTemplateSearch + each results + a.minicard-wrapper.js-minicard + +miniswimlane(this) + if isCardTemplateSearch + each results + a.minicard-wrapper.js-minicard + +minicard(this) + unless isTemplateSearch + each results + a.minicard-wrapper.js-minicard + +minicard(this) diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js index 0f5caac56..04c7eede8 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -67,25 +67,47 @@ BlazeComponent.extendComponent({ const labelIds = formComponent.labels.get(); const customFields = formComponent.customFields.get(); - const boardId = this.data().board(); + const board = this.data().board(); + let linkedId = ''; let swimlaneId = ''; const boardView = Meteor.user().profile.boardView; - if (boardView === 'board-view-swimlanes') - swimlaneId = this.parentComponent().parentComponent().data()._id; - else if ((boardView === 'board-view-lists') || (boardView === 'board-view-cal')) - swimlaneId = boardId.getDefaultSwimline()._id; - + let cardType = 'cardType-card'; if (title) { + if (board.isTemplatesBoard()) { + swimlaneId = this.parentComponent().parentComponent().data()._id; // Always swimlanes view + const swimlane = Swimlanes.findOne(swimlaneId); + // If this is the card templates swimlane, insert a card template + if (swimlane.isCardTemplatesSwimlane()) + cardType = 'template-card'; + // If this is the board templates swimlane, insert a board template and a linked card + else if (swimlane.isBoardTemplatesSwimlane()) { + linkedId = Boards.insert({ + title, + permission: 'private', + type: 'template-board', + }); + Swimlanes.insert({ + title: TAPi18n.__('default'), + boardId: linkedId, + }); + cardType = 'cardType-linkedBoard'; + } + } else if (boardView === 'board-view-swimlanes') + swimlaneId = this.parentComponent().parentComponent().data()._id; + else if ((boardView === 'board-view-lists') || (boardView === 'board-view-cal')) + swimlaneId = board.getDefaultSwimline()._id; + const _id = Cards.insert({ title, members, labelIds, customFields, listId: this.data()._id, - boardId: boardId._id, + boardId: board._id, sort: sortIndex, swimlaneId, - type: 'cardType-card', + type: cardType, + linkedId, }); // if the displayed card count is less than the total cards in the list, @@ -127,9 +149,9 @@ BlazeComponent.extendComponent({ const methodName = evt.shiftKey ? 'toggleRange' : 'toggle'; MultiSelection[methodName](this.currentData()._id); - // If the card is already selected, we want to de-select it. - // XXX We should probably modify the minicard href attribute instead of - // overwriting the event in case the card is already selected. + // If the card is already selected, we want to de-select it. + // XXX We should probably modify the minicard href attribute instead of + // overwriting the event in case the card is already selected. } else if (Session.equals('currentCard', this.currentData()._id)) { evt.stopImmediatePropagation(); evt.preventDefault(); @@ -149,7 +171,8 @@ BlazeComponent.extendComponent({ idOrNull(swimlaneId) { const currentUser = Meteor.user(); - if (currentUser.profile.boardView === 'board-view-swimlanes') + if (currentUser.profile.boardView === 'board-view-swimlanes' || + this.data().board().isTemplatesBoard()) return swimlaneId; return undefined; }, @@ -269,8 +292,8 @@ BlazeComponent.extendComponent({ // work. $form.find('button[type=submit]').click(); - // Pressing Tab should open the form of the next column, and Maj+Tab go - // in the reverse order + // Pressing Tab should open the form of the next column, and Maj+Tab go + // in the reverse order } else if (evt.keyCode === 9) { evt.preventDefault(); const isReverse = evt.shiftKey; @@ -292,7 +315,8 @@ BlazeComponent.extendComponent({ return [{ keydown: this.pressKey, 'click .js-link': Popup.open('linkCard'), - 'click .js-search': Popup.open('searchCard'), + 'click .js-search': Popup.open('searchElement'), + 'click .js-card-template': Popup.open('searchElement'), }]; }, @@ -330,7 +354,7 @@ BlazeComponent.extendComponent({ const currentBoard = Boards.findOne(Session.get('currentBoard')); callback($.map(currentBoard.labels, (label) => { if (label.name.indexOf(term) > -1 || - label.color.indexOf(term) > -1) { + label.color.indexOf(term) > -1) { return label; } return null; @@ -367,17 +391,7 @@ BlazeComponent.extendComponent({ BlazeComponent.extendComponent({ onCreated() { - // Prefetch first non-current board id - const boardId = Boards.findOne({ - archived: false, - 'members.userId': Meteor.userId(), - _id: {$ne: Session.get('currentBoard')}, - }, { - sort: ['title'], - })._id; - // Subscribe to this board - subManager.subscribe('board', boardId); - this.selectedBoardId = new ReactiveVar(boardId); + this.selectedBoardId = new ReactiveVar(''); this.selectedSwimlaneId = new ReactiveVar(''); this.selectedListId = new ReactiveVar(''); @@ -403,6 +417,7 @@ BlazeComponent.extendComponent({ archived: false, 'members.userId': Meteor.userId(), _id: {$ne: Session.get('currentBoard')}, + type: 'board', }, { sort: ['title'], }); @@ -410,7 +425,7 @@ BlazeComponent.extendComponent({ }, swimlanes() { - if (!this.selectedBoardId) { + if (!this.selectedBoardId.get()) { return []; } const swimlanes = Swimlanes.find({boardId: this.selectedBoardId.get()}); @@ -420,7 +435,7 @@ BlazeComponent.extendComponent({ }, lists() { - if (!this.selectedBoardId) { + if (!this.selectedBoardId.get()) { return []; } const lists = Lists.find({boardId: this.selectedBoardId.get()}); @@ -441,6 +456,7 @@ BlazeComponent.extendComponent({ archived: false, linkedId: {$nin: ownCardsIds}, _id: {$nin: ownCardsIds}, + type: {$nin: ['template-card']}, }); }, @@ -508,12 +524,25 @@ BlazeComponent.extendComponent({ }, onCreated() { - // Prefetch first non-current board id - let board = Boards.findOne({ - archived: false, - 'members.userId': Meteor.userId(), - _id: {$ne: Session.get('currentBoard')}, - }); + this.isCardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-card-template'); + this.isListTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-list-template'); + this.isSwimlaneTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-open-add-swimlane-menu'); + this.isBoardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-add-board'); + this.isTemplateSearch = this.isCardTemplateSearch || + this.isListTemplateSearch || + this.isSwimlaneTemplateSearch || + this.isBoardTemplateSearch; + let board = {}; + if (this.isTemplateSearch) { + board = Boards.findOne(Meteor.user().profile.templatesBoardId); + } else { + // Prefetch first non-current board id + board = Boards.findOne({ + archived: false, + 'members.userId': Meteor.userId(), + _id: {$nin: [Session.get('currentBoard'), Meteor.user().profile.templatesBoardId]}, + }); + } if (!board) { Popup.close(); return; @@ -523,20 +552,21 @@ BlazeComponent.extendComponent({ subManager.subscribe('board', boardId); this.selectedBoardId = new ReactiveVar(boardId); - this.boardId = Session.get('currentBoard'); - // In order to get current board info - subManager.subscribe('board', this.boardId); - board = Boards.findOne(this.boardId); - // List where to insert card - const list = $(Popup._getTopStack().openerElement).closest('.js-list'); - this.listId = Blaze.getData(list[0])._id; - // Swimlane where to insert card - const swimlane = $(Popup._getTopStack().openerElement).closest('.js-swimlane'); - this.swimlaneId = ''; - if (board.view === 'board-view-swimlanes') - this.swimlaneId = Blaze.getData(swimlane[0])._id; - else - this.swimlaneId = Swimlanes.findOne({boardId: this.boardId})._id; + if (!this.isBoardTemplateSearch) { + this.boardId = Session.get('currentBoard'); + // In order to get current board info + subManager.subscribe('board', this.boardId); + this.swimlaneId = ''; + // Swimlane where to insert card + const swimlane = $(Popup._getTopStack().openerElement).parents('.js-swimlane'); + if (Meteor.user().profile.boardView === 'board-view-swimlanes') + this.swimlaneId = Blaze.getData(swimlane[0])._id; + else + this.swimlaneId = Swimlanes.findOne({boardId: this.boardId})._id; + // List where to insert card + const list = $(Popup._getTopStack().openerElement).closest('.js-list'); + this.listId = Blaze.getData(list[0])._id; + } this.term = new ReactiveVar(''); }, @@ -545,6 +575,7 @@ BlazeComponent.extendComponent({ archived: false, 'members.userId': Meteor.userId(), _id: {$ne: Session.get('currentBoard')}, + type: 'board', }, { sort: ['title'], }); @@ -556,7 +587,21 @@ BlazeComponent.extendComponent({ return []; } const board = Boards.findOne(this.selectedBoardId.get()); - return board.searchCards(this.term.get(), false); + if (!this.isTemplateSearch || this.isCardTemplateSearch) { + return board.searchCards(this.term.get(), false); + } else if (this.isListTemplateSearch) { + return board.searchLists(this.term.get()); + } else if (this.isSwimlaneTemplateSearch) { + return board.searchSwimlanes(this.term.get()); + } else if (this.isBoardTemplateSearch) { + const boards = board.searchBoards(this.term.get()); + boards.forEach((board) => { + subManager.subscribe('board', board.linkedId); + }); + return boards; + } else { + return []; + } }, events() { @@ -570,20 +615,50 @@ BlazeComponent.extendComponent({ this.term.set(evt.target.searchTerm.value); }, 'click .js-minicard'(evt) { - // LINK CARD - const card = Blaze.getData(evt.currentTarget); - const _id = Cards.insert({ - title: card.title, //dummy - listId: this.listId, - swimlaneId: this.swimlaneId, - boardId: this.boardId, - sort: Lists.findOne(this.listId).cards().count(), - type: 'cardType-linkedCard', - linkedId: card.linkedId || card._id, - }); - Filter.addException(_id); + // 0. Common + const element = Blaze.getData(evt.currentTarget); + let _id = ''; + if (!this.isTemplateSearch || this.isCardTemplateSearch) { + // Card insertion + // 1. Common + element.boardId = this.boardId; + element.listId = this.listId; + element.swimlaneId = this.swimlaneId; + element.sort = Lists.findOne(this.listId).cards().count(); + // 1.A From template + if (this.isTemplateSearch) { + element.type = 'cardType-card'; + element.linkedId = ''; + _id = element.copy(); + // 1.B Linked card + } else { + delete element._id; + element.type = 'cardType-linkedCard'; + element.linkedId = element.linkedId || element._id; + _id = Cards.insert(element); + } + Filter.addException(_id); + // List insertion + } else if (this.isListTemplateSearch) { + element.boardId = this.boardId; + element.sort = Swimlanes.findOne(this.swimlaneId).lists().count(); + element.type = 'list'; + _id = element.copy(this.swimlaneId); + } else if (this.isSwimlaneTemplateSearch) { + element.boardId = this.boardId; + element.sort = Boards.findOne(this.boardId).swimlanes().count(); + element.type = 'swimlalne'; + _id = element.copy(); + } else if (this.isBoardTemplateSearch) { + board = Boards.findOne(element.linkedId); + board.sort = Boards.find({archived: false}).count(); + board.type = 'board'; + delete board.slug; + delete board.members; + _id = board.copy(); + } Popup.close(); }, }]; }, -}).register('searchCardPopup'); +}).register('searchElementPopup'); diff --git a/client/components/lists/minilist.jade b/client/components/lists/minilist.jade new file mode 100644 index 000000000..e34214c40 --- /dev/null +++ b/client/components/lists/minilist.jade @@ -0,0 +1,8 @@ +template(name="minilist") + .minicard( + class="minicard-{{colorClass}}") + .minicard-title + .handle + .fa.fa-arrows + +viewer + = title diff --git a/client/components/main/editor.js b/client/components/main/editor.js index 20ece5622..88d8abf02 100755 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -36,7 +36,10 @@ import sanitizeXss from 'xss'; const at = HTML.CharRef({html: '@', str: '@'}); Blaze.Template.registerHelper('mentions', new Template('mentions', function() { const view = this; + let content = Blaze.toHTML(view.templateContentBlock); const currentBoard = Boards.findOne(Session.get('currentBoard')); + if (!currentBoard) + return HTML.Raw(sanitizeXss(content)); const knowedUsers = currentBoard.members.map((member) => { const u = Users.findOne(member.userId); if(u){ @@ -45,7 +48,6 @@ Blaze.Template.registerHelper('mentions', new Template('mentions', function() { return member; }); const mentionRegex = /\B@([\w.]*)/gi; - let content = Blaze.toHTML(view.templateContentBlock); let currentMention; while ((currentMention = mentionRegex.exec(content)) !== null) { diff --git a/client/components/swimlanes/miniswimlane.jade b/client/components/swimlanes/miniswimlane.jade new file mode 100644 index 000000000..d4be85994 --- /dev/null +++ b/client/components/swimlanes/miniswimlane.jade @@ -0,0 +1,8 @@ +template(name="miniswimlane") + .minicard( + class="minicard-{{colorClass}}") + .minicard-title + .handle + .fa.fa-arrows + +viewer + = title diff --git a/client/components/swimlanes/swimlaneHeader.jade b/client/components/swimlanes/swimlaneHeader.jade index 33eb5731f..de9621d52 100644 --- a/client/components/swimlanes/swimlaneHeader.jade +++ b/client/components/swimlanes/swimlaneHeader.jade @@ -1,15 +1,21 @@ template(name="swimlaneHeader") .swimlane-header-wrap.js-swimlane-header(class='{{#if colorClass}}swimlane-{{colorClass}}{{/if}}') - +inlinedForm - +editSwimlaneTitleForm + if this.isTemplateContainer + +swimlaneFixedHeader(this) else - .swimlane-header( - class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}") - = title - .swimlane-header-menu - unless currentUser.isCommentOnly - a.fa.fa-plus.js-open-add-swimlane-menu.swimlane-header-plus-icon - a.fa.fa-navicon.js-open-swimlane-menu + +inlinedForm + +editSwimlaneTitleForm + else + +swimlaneFixedHeader(this) + +template(name="swimlaneFixedHeader") + .swimlane-header( + class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}") + = title + .swimlane-header-menu + unless currentUser.isCommentOnly + a.fa.fa-plus.js-open-add-swimlane-menu.swimlane-header-plus-icon + a.fa.fa-navicon.js-open-swimlane-menu template(name="editSwimlaneTitleForm") .list-composer @@ -22,9 +28,10 @@ template(name="swimlaneActionPopup") unless currentUser.isCommentOnly ul.pop-over-list li: a.js-set-swimlane-color {{_ 'select-color'}} - hr - ul.pop-over-list - li: a.js-close-swimlane {{_ 'archive-swimlane'}} + unless this.isTemplateContainer + hr + ul.pop-over-list + li: a.js-close-swimlane {{_ 'archive-swimlane'}} template(name="swimlaneAddPopup") unless currentUser.isCommentOnly @@ -33,6 +40,11 @@ template(name="swimlaneAddPopup") autocomplete="off" autofocus) .edit-controls.clearfix button.primary.confirm(type="submit") {{_ 'add'}} + unless currentBoard.isTemplatesBoard + unless currentBoard.isTemplateBoard + span.quiet + | {{_ 'or'}} + a.js-swimlane-template {{_ 'template'}} template(name="setSwimlaneColorPopup") form.edit-label diff --git a/client/components/swimlanes/swimlaneHeader.js b/client/components/swimlanes/swimlaneHeader.js index 1004cb254..e7f3cc761 100644 --- a/client/components/swimlanes/swimlaneHeader.js +++ b/client/components/swimlanes/swimlaneHeader.js @@ -47,12 +47,14 @@ BlazeComponent.extendComponent({ const titleInput = this.find('.swimlane-name-input'); const title = titleInput.value.trim(); const sortValue = calculateIndexData(this.currentSwimlane, nextSwimlane, 1); + const swimlaneType = (currentBoard.isTemplatesBoard())?'template-swimlane':'swimlane'; if (title) { Swimlanes.insert({ title, boardId: Session.get('currentBoard'), sort: sortValue.base, + type: swimlaneType, }); titleInput.value = ''; @@ -63,6 +65,7 @@ BlazeComponent.extendComponent({ // with a minimum of interactions Popup.close(); }, + 'click .js-swimlane-template': Popup.open('searchElement'), }]; }, }).register('swimlaneAddPopup'); diff --git a/client/components/swimlanes/swimlanes.jade b/client/components/swimlanes/swimlanes.jade index 34177a022..c56834df4 100644 --- a/client/components/swimlanes/swimlanes.jade +++ b/client/components/swimlanes/swimlanes.jade @@ -3,15 +3,15 @@ template(name="swimlane") +swimlaneHeader .swimlane.js-lists.js-swimlane if isMiniScreen - if currentList + if currentListIsInThisSwimlane _id +list(currentList) - else - each currentBoard.lists + unless currentList + each lists +miniList(this) if currentUser.isBoardMember +addListForm else - each currentBoard.lists + each lists +list(this) if currentCardIsInThisList _id ../_id +cardDetails(currentCard) @@ -24,12 +24,12 @@ template(name="listsGroup") if currentList +list(currentList) else - each currentBoard.lists + each lists +miniList(this) if currentUser.isBoardMember +addListForm else - each currentBoard.lists + each lists +list(this) if currentCardIsInThisList _id null +cardDetails(currentCard) @@ -44,7 +44,11 @@ template(name="addListForm") autocomplete="off" autofocus) .edit-controls.clearfix button.primary.confirm(type="submit") {{_ 'save'}} - a.fa.fa-times-thin.js-close-inlined-form + unless currentBoard.isTemplatesBoard + unless currentBoard.isTemplateBoard + span.quiet + | {{_ 'or'}} + a.js-list-template {{_ 'template'}} else a.open-list-composer.js-open-inlined-form i.fa.fa-plus diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index ce327f54d..519b00d2e 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -1,5 +1,10 @@ const { calculateIndex, enableClickOnTouch } = Utils; +function currentListIsInThisSwimlane(swimlaneId) { + const currentList = Lists.findOne(Session.get('currentList')); + return currentList && (currentList.swimlaneId === swimlaneId || currentList.swimlaneId === ''); +} + function currentCardIsInThisList(listId, swimlaneId) { const currentCard = Cards.findOne(Session.get('currentCard')); const currentUser = Meteor.user(); @@ -114,6 +119,10 @@ BlazeComponent.extendComponent({ return currentCardIsInThisList(listId, swimlaneId); }, + currentListIsInThisSwimlane(swimlaneId) { + return currentListIsInThisSwimlane(swimlaneId); + }, + events() { return [{ // Click-and-drag action @@ -153,6 +162,12 @@ BlazeComponent.extendComponent({ }).register('swimlane'); BlazeComponent.extendComponent({ + onCreated() { + this.currentBoard = Boards.findOne(Session.get('currentBoard')); + this.isListTemplatesSwimlane = this.currentBoard.isTemplatesBoard() && this.currentData().isListTemplatesSwimlane(); + this.currentSwimlane = this.currentData(); + }, + // Proxy open() { this.childComponents('inlinedForm')[0].open(); @@ -169,12 +184,15 @@ BlazeComponent.extendComponent({ title, boardId: Session.get('currentBoard'), sort: $('.list').length, + type: (this.isListTemplatesSwimlane)?'template-list':'list', + swimlaneId: (this.currentBoard.isTemplatesBoard())?this.currentSwimlane._id:'', }); titleInput.value = ''; titleInput.focus(); } }, + 'click .js-list-template': Popup.open('searchElement'), }]; }, }).register('addListForm'); diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade index af120045a..c55b65c28 100644 --- a/client/components/users/userHeader.jade +++ b/client/components/users/userHeader.jade @@ -21,6 +21,9 @@ template(name="memberMenuPopup") li: a.js-change-language {{_ 'changeLanguagePopup-title'}} if currentUser.isAdmin li: a.js-go-setting(href="{{pathFor 'setting'}}") {{_ 'admin-panel'}} + hr + ul.pop-over-list + li: a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") {{_ 'templates'}} unless isSandstorm hr ul.pop-over-list diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js index 63cbb14f4..6a2397a4b 100644 --- a/client/components/users/userHeader.js +++ b/client/components/users/userHeader.js @@ -3,6 +3,15 @@ Template.headerUserBar.events({ 'click .js-change-avatar': Popup.open('changeAvatar'), }); +Template.memberMenuPopup.helpers({ + templatesBoardId() { + return Meteor.user().getTemplatesBoardId(); + }, + templatesBoardSlug() { + return Meteor.user().getTemplatesBoardSlug(); + }, +}); + Template.memberMenuPopup.events({ 'click .js-edit-profile': Popup.open('editProfile'), 'click .js-change-settings': Popup.open('changeSettings'), diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index d4e817ea0..94666c16c 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -92,6 +92,8 @@ "restore-board": "Restore Board", "no-archived-boards": "No Boards in Archive.", "archives": "Archive", + "template": "Template", + "templates": "Templates", "assign-member": "Assign member", "attached": "attached", "attachment": "Attachment", @@ -143,6 +145,7 @@ "cardLabelsPopup-title": "Labels", "cardMembersPopup-title": "Members", "cardMorePopup-title": "More", + "cardTemplatePopup-title": "Create template", "cards": "Cards", "cards-count": "Cards", "casSignIn" : "Sign In with CAS", @@ -453,6 +456,9 @@ "welcome-swimlane": "Milestone 1", "welcome-list1": "Basics", "welcome-list2": "Advanced", + "card-templates-swimlane": "Card Templates", + "list-templates-swimlane": "List Templates", + "board-templates-swimlane": "Board Templates", "what-to-do": "What do you want to do?", "wipLimitErrorPopup-title": "Invalid WIP Limit", "wipLimitErrorPopup-dialog-pt1": "The number of tasks in this list is higher than the WIP limit you've defined.", diff --git a/i18n/ru.i18n.json b/i18n/ru.i18n.json index 6fde47f97..082cc0069 100644 --- a/i18n/ru.i18n.json +++ b/i18n/ru.i18n.json @@ -92,8 +92,8 @@ "restore-board": "Востановить доску", "no-archived-boards": "Нет досок в архиве.", "archives": "Архив", - "template": "Template", - "templates": "Templates", + "template": "Шаблон", + "templates": "Шаблоны", "assign-member": "Назначить участника", "attached": "прикреплено", "attachment": "Вложение", @@ -145,7 +145,7 @@ "cardLabelsPopup-title": "Метки", "cardMembersPopup-title": "Участники", "cardMorePopup-title": "Поделиться", - "cardTemplatePopup-title": "Create template", + "cardTemplatePopup-title": "Создать шаблон", "cards": "Карточки", "cards-count": "Карточки", "casSignIn": "Войти через CAS", @@ -456,9 +456,9 @@ "welcome-swimlane": "Этап 1", "welcome-list1": "Основы", "welcome-list2": "Расширенно", - "card-templates-swimlane": "Card Templates", - "list-templates-swimlane": "List Templates", - "board-templates-swimlane": "Board Templates", + "card-templates-swimlane": "Шаблоны карточек", + "list-templates-swimlane": "Шаблоны списков", + "board-templates-swimlane": "Шаблоны досок", "what-to-do": "Что вы хотите сделать?", "wipLimitErrorPopup-title": "Некорректный лимит на кол-во задач", "wipLimitErrorPopup-dialog-pt1": "Количество задач в этом списке превышает установленный вами лимит", diff --git a/models/boards.js b/models/boards.js index 71831a630..0db2e48e3 100644 --- a/models/boards.js +++ b/models/boards.js @@ -304,10 +304,32 @@ Boards.attachSchema(new SimpleSchema({ defaultValue: false, optional: true, }, + type: { + /** + * The type of board + */ + type: String, + defaultValue: 'board', + }, })); Boards.helpers({ + copy() { + const oldId = this._id; + delete this._id; + const _id = Boards.insert(this); + + // Copy all swimlanes in board + Swimlanes.find({ + boardId: oldId, + archived: false, + }).forEach((swimlane) => { + swimlane.type = 'swimlane'; + swimlane.boardId = _id; + swimlane.copy(oldId); + }); + }, /** * Is supplied user authorized to view this board? */ @@ -456,6 +478,75 @@ Boards.helpers({ return _id; }, + searchBoards(term) { + check(term, Match.OneOf(String, null, undefined)); + + const query = { boardId: this._id }; + query.type = 'cardType-linkedBoard'; + query.archived = false; + + const projection = { limit: 10, sort: { createdAt: -1 } }; + + if (term) { + const regex = new RegExp(term, 'i'); + + query.$or = [ + { title: regex }, + { description: regex }, + ]; + } + + return Cards.find(query, projection); + }, + + searchSwimlanes(term) { + check(term, Match.OneOf(String, null, undefined)); + + const query = { boardId: this._id }; + if (this.isTemplatesBoard()) { + query.type = 'template-swimlane'; + query.archived = false; + } else { + query.type = {$nin: ['template-swimlane']}; + } + const projection = { limit: 10, sort: { createdAt: -1 } }; + + if (term) { + const regex = new RegExp(term, 'i'); + + query.$or = [ + { title: regex }, + { description: regex }, + ]; + } + + return Swimlanes.find(query, projection); + }, + + searchLists(term) { + check(term, Match.OneOf(String, null, undefined)); + + const query = { boardId: this._id }; + if (this.isTemplatesBoard()) { + query.type = 'template-list'; + query.archived = false; + } else { + query.type = {$nin: ['template-list']}; + } + const projection = { limit: 10, sort: { createdAt: -1 } }; + + if (term) { + const regex = new RegExp(term, 'i'); + + query.$or = [ + { title: regex }, + { description: regex }, + ]; + } + + return Lists.find(query, projection); + }, + searchCards(term, excludeLinked) { check(term, Match.OneOf(String, null, undefined)); @@ -463,6 +554,12 @@ Boards.helpers({ if (excludeLinked) { query.linkedId = null; } + if (this.isTemplatesBoard()) { + query.type = 'template-card'; + query.archived = false; + } else { + query.type = {$nin: ['template-card']}; + } const projection = { limit: 10, sort: { createdAt: -1 } }; if (term) { @@ -559,6 +656,13 @@ Boards.helpers({ }); }, + isTemplateBoard() { + return this.type === 'template-board'; + }, + + isTemplatesBoard() { + return this.type === 'template-container'; + }, }); @@ -907,7 +1011,7 @@ if (Meteor.isServer) { * @param {string} userId the ID of the user to retrieve the data * @return_type [{_id: string, title: string}] - */ + */ JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res) { try { Authentication.checkLoggedIn(req.userId); @@ -944,7 +1048,7 @@ if (Meteor.isServer) { * * @return_type [{_id: string, title: string}] - */ + */ JsonRoutes.add('GET', '/api/boards', function (req, res) { try { Authentication.checkUserId(req.userId); @@ -1015,7 +1119,7 @@ if (Meteor.isServer) { * * @return_type {_id: string, defaultSwimlaneId: string} - */ + */ JsonRoutes.add('POST', '/api/boards', function (req, res) { try { Authentication.checkUserId(req.userId); diff --git a/models/cardComments.js b/models/cardComments.js index 974c5ec93..fcb97104d 100644 --- a/models/cardComments.js +++ b/models/cardComments.js @@ -67,6 +67,12 @@ CardComments.allow({ }); CardComments.helpers({ + copy(newCardId) { + this.cardId = newCardId; + delete this._id; + CardComments.insert(this); + }, + user() { return Users.findOne(this.userId); }, diff --git a/models/cards.js b/models/cards.js index ff19a9a0e..c733c7f8b 100644 --- a/models/cards.js +++ b/models/cards.js @@ -246,7 +246,7 @@ Cards.attachSchema(new SimpleSchema({ * type of the card */ type: String, - defaultValue: '', + defaultValue: 'cardType-card', }, linkedId: { /** @@ -272,6 +272,31 @@ Cards.allow({ }); Cards.helpers({ + copy() { + const oldId = this._id; + delete this._id; + const _id = Cards.insert(this); + + // copy checklists + Checklists.find({cardId: oldId}).forEach((ch) => { + ch.copy(_id); + }); + + // copy subtasks + Cards.find({parentId: oldId}).forEach((subtask) => { + subtask.parentId = _id; + subtask._id = null; + Cards.insert(subtask); + }); + + // copy card comments + CardComments.find({cardId: oldId}).forEach((cmt) => { + cmt.copy(_id); + }); + + return _id; + }, + list() { return Lists.findOne(this.listId); }, @@ -930,6 +955,10 @@ Cards.helpers({ return this.assignedBy; } }, + + isTemplateCard() { + return this.type === 'template-card'; + }, }); Cards.mutations({ @@ -1230,7 +1259,7 @@ Cards.mutations({ function cardMove(userId, doc, fieldNames, oldListId, oldSwimlaneId) { if ((_.contains(fieldNames, 'listId') && doc.listId !== oldListId) || - (_.contains(fieldNames, 'swimlaneId') && doc.swimlaneId !== oldSwimlaneId)){ + (_.contains(fieldNames, 'swimlaneId') && doc.swimlaneId !== oldSwimlaneId)){ Activities.insert({ userId, oldListId, diff --git a/models/checklists.js b/models/checklists.js index a372fafa4..9e763f1ad 100644 --- a/models/checklists.js +++ b/models/checklists.js @@ -48,6 +48,19 @@ Checklists.attachSchema(new SimpleSchema({ })); Checklists.helpers({ + copy(newCardId) { + const oldChecklistId = this._id; + this._id = null; + this.cardId = newCardId; + const newChecklistId = Checklists.insert(this); + ChecklistItems.find({checklistId: oldChecklistId}).forEach((item) => { + item._id = null; + item.checklistId = newChecklistId; + item.cardId = newCardId; + ChecklistItems.insert(item); + }); + }, + itemCount() { return ChecklistItems.find({ checklistId: this._id }).count(); }, diff --git a/models/lists.js b/models/lists.js index 54e7d0373..d76c961c0 100644 --- a/models/lists.js +++ b/models/lists.js @@ -27,6 +27,13 @@ Lists.attachSchema(new SimpleSchema({ */ type: String, }, + swimlaneId: { + /** + * the swimlane associated to this list. Used for templates + */ + type: String, + defaultValue: '', + }, createdAt: { /** * creation date @@ -107,6 +114,13 @@ Lists.attachSchema(new SimpleSchema({ 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', ], }, + type: { + /** + * The type of list + */ + type: String, + defaultValue: 'list', + }, })); Lists.allow({ @@ -123,6 +137,37 @@ Lists.allow({ }); Lists.helpers({ + copy(swimlaneId) { + const oldId = this._id; + const oldSwimlaneId = this.swimlaneId || null; + let _id = null; + existingListWithSameName = Lists.findOne({ + boardId: this.boardId, + title: this.title, + archived: false, + }); + if (existingListWithSameName) { + _id = existingListWithSameName._id; + } else { + delete this._id; + delete this.swimlaneId; + _id = Lists.insert(this); + } + + // Copy all cards in list + Cards.find({ + swimlaneId: oldSwimlaneId, + listId: oldId, + archived: false, + }).forEach((card) => { + card.type = 'cardType-card'; + card.listId = _id; + card.boardId = this.boardId; + card.swimlaneId = swimlaneId; + card.copy(); + }); + }, + cards(swimlaneId) { const selector = { listId: this._id, @@ -169,6 +214,10 @@ Lists.helpers({ return this.color; return ''; }, + + isTemplateList() { + return this.type === 'template-list'; + }, }); Lists.mutations({ @@ -177,10 +226,20 @@ Lists.mutations({ }, archive() { + if (this.isTemplateList()) { + this.cards().forEach((card) => { + return card.archive(); + }); + } return { $set: { archived: true } }; }, restore() { + if (this.isTemplateList()) { + this.allCards().forEach((card) => { + return card.restore(); + }); + } return { $set: { archived: false } }; }, diff --git a/models/swimlanes.js b/models/swimlanes.js index e2c3925c4..a3427fc62 100644 --- a/models/swimlanes.js +++ b/models/swimlanes.js @@ -78,6 +78,13 @@ Swimlanes.attachSchema(new SimpleSchema({ } }, }, + type: { + /** + * The type of swimlane + */ + type: String, + defaultValue: 'swimlane', + }, })); Swimlanes.allow({ @@ -94,6 +101,28 @@ Swimlanes.allow({ }); Swimlanes.helpers({ + copy(oldBoardId) { + const oldId = this._id; + delete this._id; + const _id = Swimlanes.insert(this); + + const query = { + swimlaneId: {$in: [oldId, '']}, + archived: false, + }; + if (oldBoardId) { + query.boardId = oldBoardId; + } + + // Copy all lists in swimlane + Lists.find(query).forEach((list) => { + list.type = 'list'; + list.swimlaneId = oldId; + list.boardId = this.boardId; + list.copy(_id); + }); + }, + cards() { return Cards.find(Filter.mongoSelector({ swimlaneId: this._id, @@ -101,6 +130,18 @@ Swimlanes.helpers({ }), { sort: ['sort'] }); }, + lists() { + return Lists.find(Filter.mongoSelector({ + boardId: this.boardId, + swimlaneId: {$in: [this._id, '']}, + archived: false, + }), { sort: ['sort'] }); + }, + + allLists() { + return Lists.find({ swimlaneId: this._id }); + }, + allCards() { return Cards.find({ swimlaneId: this._id }); }, @@ -114,6 +155,29 @@ Swimlanes.helpers({ return this.color; return ''; }, + + isTemplateSwimlane() { + return this.type === 'template-swimlane'; + }, + + isTemplateContainer() { + return this.type === 'template-container'; + }, + + isListTemplatesSwimlane() { + const user = Users.findOne(Meteor.userId()); + return user.profile.listTemplatesSwimlaneId === this._id; + }, + + isCardTemplatesSwimlane() { + const user = Users.findOne(Meteor.userId()); + return user.profile.cardTemplatesSwimlaneId === this._id; + }, + + isBoardTemplatesSwimlane() { + const user = Users.findOne(Meteor.userId()); + return user.profile.boardTemplatesSwimlaneId === this._id; + }, }); Swimlanes.mutations({ @@ -122,10 +186,20 @@ Swimlanes.mutations({ }, archive() { + if (this.isTemplateSwimlane()) { + this.lists().forEach((list) => { + return list.archive(); + }); + } return { $set: { archived: true } }; }, restore() { + if (this.isTemplateSwimlane()) { + this.allLists().forEach((list) => { + return list.restore(); + }); + } return { $set: { archived: false } }; }, diff --git a/models/users.js b/models/users.js index c6c0f857f..0dd9c1d67 100644 --- a/models/users.js +++ b/models/users.js @@ -159,6 +159,34 @@ Users.attachSchema(new SimpleSchema({ 'board-view-cal', ], }, + 'profile.templatesBoardId': { + /** + * Reference to the templates board + */ + type: String, + defaultValue: '', + }, + 'profile.cardTemplatesSwimlaneId': { + /** + * Reference to the card templates swimlane Id + */ + type: String, + defaultValue: '', + }, + 'profile.listTemplatesSwimlaneId': { + /** + * Reference to the list templates swimlane Id + */ + type: String, + defaultValue: '', + }, + 'profile.boardTemplatesSwimlaneId': { + /** + * Reference to the board templates swimlane Id + */ + type: String, + defaultValue: '', + }, services: { /** * services field of the user @@ -328,6 +356,14 @@ Users.helpers({ const profile = this.profile || {}; return profile.language || 'en'; }, + + getTemplatesBoardId() { + return this.profile.templatesBoardId; + }, + + getTemplatesBoardSlug() { + return Boards.findOne(this.profile.templatesBoardId).slug; + }, }); Users.mutations({ @@ -675,7 +711,6 @@ if (Meteor.isServer) { CollectionHooks.getUserId = () => { return fakeUserId.get() || getUserId(); }; - /* if (!isSandstorm) { Users.after.insert((userId, doc) => { const fakeUser = { @@ -685,6 +720,7 @@ if (Meteor.isServer) { }; fakeUserId.withValue(doc._id, () => { + /* // Insert the Welcome Board Boards.insert({ title: TAPi18n.__('welcome-board'), @@ -701,10 +737,56 @@ if (Meteor.isServer) { Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser); }); }); + */ + + Boards.insert({ + title: TAPi18n.__('templates'), + permission: 'private', + type: 'template-container', + }, fakeUser, (err, boardId) => { + + // Insert the reference to our templates board + Users.update(fakeUserId.get(), {$set: {'profile.templatesBoardId': boardId}}); + + // Insert the card templates swimlane + Swimlanes.insert({ + title: TAPi18n.__('card-templates-swimlane'), + boardId, + sort: 1, + type: 'template-container', + }, fakeUser, (err, swimlaneId) => { + + // Insert the reference to out card templates swimlane + Users.update(fakeUserId.get(), {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}}); + }); + + // Insert the list templates swimlane + Swimlanes.insert({ + title: TAPi18n.__('list-templates-swimlane'), + boardId, + sort: 2, + type: 'template-container', + }, fakeUser, (err, swimlaneId) => { + + // Insert the reference to out list templates swimlane + Users.update(fakeUserId.get(), {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}}); + }); + + // Insert the board templates swimlane + Swimlanes.insert({ + title: TAPi18n.__('board-templates-swimlane'), + boardId, + sort: 3, + type: 'template-container', + }, fakeUser, (err, swimlaneId) => { + + // Insert the reference to out board templates swimlane + Users.update(fakeUserId.get(), {$set: {'profile.boardTemplatesSwimlaneId': swimlaneId}}); + }); + }); }); }); } - */ Users.after.insert((userId, doc) => { diff --git a/package.json b/package.json index 79f192195..a17f36c22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wekan", - "version": "v2.28.0", + "version": "v2.29.0", "description": "Open-Source kanban", "private": true, "scripts": { diff --git a/sandstorm-pkgdef.capnp b/sandstorm-pkgdef.capnp index 11cf0c31e..0d7064c5c 100644 --- a/sandstorm-pkgdef.capnp +++ b/sandstorm-pkgdef.capnp @@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = ( appTitle = (defaultText = "Wekan"), # The name of the app as it is displayed to the user. - appVersion = 230, + appVersion = 231, # Increment this for every release. - appMarketingVersion = (defaultText = "2.28.0~2019-02-27"), + appMarketingVersion = (defaultText = "2.29.0~2019-02-27"), # Human-readable presentation of the app version. minUpgradableAppVersion = 0, diff --git a/server/migrations.js b/server/migrations.js index 8dcd892ad..cb64b7e8a 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -422,3 +422,98 @@ Migrations.add('add-defaultAuthenticationMethod', () => { }, }, noValidateMulti); }); + +Migrations.add('add-templates', () => { + Boards.update({ + type: { + $exists: false, + }, + }, { + $set: { + type: 'board', + }, + }, noValidateMulti); + Swimlanes.update({ + type: { + $exists: false, + }, + }, { + $set: { + type: 'swimlane', + }, + }, noValidateMulti); + Lists.update({ + type: { + $exists: false, + }, + swimlaneId: { + $exists: false, + }, + }, { + $set: { + type: 'list', + swimlaneId: '', + }, + }, noValidateMulti); + Users.find({ + 'profile.templatesBoardId': { + $exists: false, + }, + }).forEach((user) => { + // Create board and swimlanes + Boards.insert({ + title: TAPi18n.__('templates'), + permission: 'private', + type: 'template-container', + members: [ + { + userId: user._id, + isAdmin: true, + isActive: true, + isNoComments: false, + isCommentOnly: false, + }, + ], + }, (err, boardId) => { + + // Insert the reference to our templates board + Users.update(user._id, {$set: {'profile.templatesBoardId': boardId}}); + + // Insert the card templates swimlane + Swimlanes.insert({ + title: TAPi18n.__('card-templates-swimlane'), + boardId, + sort: 1, + type: 'template-container', + }, (err, swimlaneId) => { + + // Insert the reference to out card templates swimlane + Users.update(user._id, {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}}); + }); + + // Insert the list templates swimlane + Swimlanes.insert({ + title: TAPi18n.__('list-templates-swimlane'), + boardId, + sort: 2, + type: 'template-container', + }, (err, swimlaneId) => { + + // Insert the reference to out list templates swimlane + Users.update(user._id, {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}}); + }); + + // Insert the board templates swimlane + Swimlanes.insert({ + title: TAPi18n.__('board-templates-swimlane'), + boardId, + sort: 3, + type: 'template-container', + }, (err, swimlaneId) => { + + // Insert the reference to out board templates swimlane + Users.update(user._id, {$set: {'profile.boardTemplatesSwimlaneId': swimlaneId}}); + }); + }); + }); +}); diff --git a/server/publications/boards.js b/server/publications/boards.js index fb4c8c84c..71c53612d 100644 --- a/server/publications/boards.js +++ b/server/publications/boards.js @@ -32,6 +32,7 @@ Meteor.publish('boards', function() { color: 1, members: 1, permission: 1, + type: 1, }, }); });