mirror of
https://github.com/wekan/wekan.git
synced 2025-04-24 05:57:13 -04:00
Merge branch 'edge' into meteor-1.8
This commit is contained in:
commit
82a728df71
34 changed files with 811 additions and 191 deletions
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
Template.boardListHeaderBar.events({
|
||||
'click .js-open-archived-board'() {
|
||||
Modal.open('archivedBoards');
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.subscribe('archivedBoards');
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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'}}
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
|
|
8
client/components/boards/miniboard.jade
Normal file
8
client/components/boards/miniboard.jade
Normal file
|
@ -0,0 +1,8 @@
|
|||
template(name="miniboard")
|
||||
.minicard(
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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');
|
||||
|
|
8
client/components/lists/minilist.jade
Normal file
8
client/components/lists/minilist.jade
Normal file
|
@ -0,0 +1,8 @@
|
|||
template(name="minilist")
|
||||
.minicard(
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
|
@ -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) {
|
||||
|
|
8
client/components/swimlanes/miniswimlane.jade
Normal file
8
client/components/swimlanes/miniswimlane.jade
Normal file
|
@ -0,0 +1,8 @@
|
|||
template(name="miniswimlane")
|
||||
.minicard(
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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": "Количество задач в этом списке превышает установленный вами лимит",
|
||||
|
|
110
models/boards.js
110
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);
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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 } };
|
||||
},
|
||||
|
||||
|
|
|
@ -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 } };
|
||||
},
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "wekan",
|
||||
"version": "v2.28.0",
|
||||
"version": "v2.29.0",
|
||||
"description": "Open-Source kanban",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,6 +32,7 @@ Meteor.publish('boards', function() {
|
|||
color: 1,
|
||||
members: 1,
|
||||
permission: 1,
|
||||
type: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue