Merge branch 'edge' into meteor-1.8

This commit is contained in:
Lauri Ojansivu 2019-02-27 21:03:54 +02:00
commit 82a728df71
34 changed files with 811 additions and 191 deletions

View file

@ -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:

View file

@ -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

View file

@ -1,9 +1,3 @@
Template.boardListHeaderBar.events({
'click .js-open-archived-board'() {
Modal.open('archivedBoards');
},
});
BlazeComponent.extendComponent({
onCreated() {
this.subscribe('archivedBoards');

View file

@ -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")

View file

@ -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

View file

@ -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');

View file

@ -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'}}

View file

@ -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'],
});

View file

@ -0,0 +1,8 @@
template(name="miniboard")
.minicard(
class="minicard-{{colorClass}}")
.minicard-title
.handle
.fa.fa-arrows
+viewer
= title

View file

@ -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();

View file

@ -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)

View file

@ -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');

View file

@ -0,0 +1,8 @@
template(name="minilist")
.minicard(
class="minicard-{{colorClass}}")
.minicard-title
.handle
.fa.fa-arrows
+viewer
= title

View file

@ -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) {

View file

@ -0,0 +1,8 @@
template(name="miniswimlane")
.minicard(
class="minicard-{{colorClass}}")
.minicard-title
.handle
.fa.fa-arrows
+viewer
= title

View file

@ -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

View file

@ -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');

View file

@ -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

View file

@ -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');

View file

@ -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

View file

@ -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'),

View file

@ -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.",

View file

@ -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": "Количество задач в этом списке превышает установленный вами лимит",

View file

@ -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);

View file

@ -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);
},

View file

@ -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,

View file

@ -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();
},

View file

@ -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 } };
},

View file

@ -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 } };
},

View file

@ -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) => {

View file

@ -1,6 +1,6 @@
{
"name": "wekan",
"version": "v2.28.0",
"version": "v2.29.0",
"description": "Open-Source kanban",
"private": true,
"scripts": {

View file

@ -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,

View file

@ -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}});
});
});
});
});

View file

@ -32,6 +32,7 @@ Meteor.publish('boards', function() {
color: 1,
members: 1,
permission: 1,
type: 1,
},
});
});