summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/components/boards/boardArchive.js6
-rw-r--r--client/components/boards/boardBody.jade11
-rw-r--r--client/components/boards/boardHeader.jade17
-rw-r--r--client/components/boards/boardHeader.js1
-rw-r--r--client/components/boards/boardsList.jade3
-rw-r--r--client/components/boards/boardsList.js16
-rw-r--r--client/components/boards/miniboard.jade8
-rw-r--r--client/components/cards/cardDetails.js58
-rw-r--r--client/components/lists/listBody.jade64
-rw-r--r--client/components/lists/listBody.js201
-rw-r--r--client/components/lists/minilist.jade8
-rwxr-xr-xclient/components/main/editor.js4
-rw-r--r--client/components/swimlanes/miniswimlane.jade8
-rw-r--r--client/components/swimlanes/swimlaneHeader.jade36
-rw-r--r--client/components/swimlanes/swimlaneHeader.js3
-rw-r--r--client/components/swimlanes/swimlanes.jade18
-rw-r--r--client/components/swimlanes/swimlanes.js18
-rw-r--r--client/components/users/userHeader.jade3
-rw-r--r--client/components/users/userHeader.js9
-rw-r--r--i18n/en.i18n.json6
-rw-r--r--models/boards.js110
-rw-r--r--models/cardComments.js6
-rw-r--r--models/cards.js33
-rw-r--r--models/checklists.js13
-rw-r--r--models/lists.js59
-rw-r--r--models/swimlanes.js74
-rw-r--r--models/users.js82
-rw-r--r--server/migrations.js95
-rw-r--r--server/publications/boards.js1
29 files changed, 792 insertions, 179 deletions
diff --git a/client/components/boards/boardArchive.js b/client/components/boards/boardArchive.js
index 8f4d5434..c8bbb341 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 3a40921d..32f8629f 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 75b2f02b..1f6462bd 100644
--- a/client/components/boards/boardHeader.jade
+++ b/client/components/boards/boardHeader.jade
@@ -96,10 +96,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(
@@ -132,7 +133,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 89f686ab..492fda40 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 89852570..e36b8fc6 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 1ed88146..df495bb1 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 00000000..d1fb0b07
--- /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 a571e21a..73a7a67d 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 f030833b..b8e2adc7 100644
--- a/client/components/lists/listBody.jade
+++ b/client/components/lists/listBody.jade
@@ -45,13 +45,19 @@ template(name="addCardForm")
.add-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
unless isSandstorm
- 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)
@@ -61,11 +67,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'}}:
@@ -87,19 +91,35 @@ template(name="linkCardPopup")
unless isSandstorm
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 0f5caac5..04c7eede 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 00000000..e34214c4
--- /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 20ece562..88d8abf0 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 00000000..d4be8599
--- /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 33eb5731..de9621d5 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 1004cb25..e7f3cc76 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 34177a02..c56834df 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 ce327f54..519b00d2 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 b6e10d8a..a4704933 100644
--- a/client/components/users/userHeader.jade
+++ b/client/components/users/userHeader.jade
@@ -21,6 +21,9 @@ template(name="memberMenuPopup")
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'}}
+ hr
+ ul.pop-over-list
li: a.js-logout {{_ 'log-out'}}
template(name="editProfilePopup")
diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js
index 63cbb14f..6a2397a4 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 d4e817ea..94666c16 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/models/boards.js b/models/boards.js
index 71831a63..0db2e48e 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 974c5ec9..fcb97104 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 ff19a9a0..c733c7f8 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 a372fafa..9e763f1a 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 54e7d037..d76c961c 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 e2c3925c..a3427fc6 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 0fdf21a8..9bc4f175 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({
@@ -701,6 +737,52 @@ 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}});
+ });
+ });
});
});
}
diff --git a/server/migrations.js b/server/migrations.js
index 8dcd892a..cb64b7e8 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 fb4c8c84..71c53612 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,
},
});
});