mirror of
https://github.com/wekan/wekan.git
synced 2026-01-01 15:18:49 +01:00
Merge branch 'nested-tasks' of https://github.com/TNick/wekan into TNick-nested-tasks
This commit is contained in:
commit
06423164fb
21 changed files with 1163 additions and 83 deletions
|
|
@ -134,6 +134,7 @@
|
|||
"Announcements": true,
|
||||
"Swimlanes": true,
|
||||
"ChecklistItems": true,
|
||||
"Subtasks": true,
|
||||
"Npm": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,10 @@ template(name="boardMenuPopup")
|
|||
li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
|
||||
li: a.js-archive-board {{_ 'archive-board'}}
|
||||
li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a.js-subtask-settings {{_ 'subtask-settings'}}
|
||||
|
||||
if isSandstorm
|
||||
hr
|
||||
ul.pop-over-list
|
||||
|
|
@ -193,6 +197,56 @@ template(name="boardChangeColorPopup")
|
|||
if isSelected
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="boardSubtaskSettingsPopup")
|
||||
form.board-subtask-settings
|
||||
h3 {{_ 'show-parent-in-minicard'}}
|
||||
a#prefix-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
|
||||
span {{_ 'prefix-with-full-path'}}
|
||||
a#prefix-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
|
||||
span {{_ 'prefix-with-parent'}}
|
||||
a#subtext-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
|
||||
span {{_ 'subtext-with-full-path'}}
|
||||
a#subtext-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
|
||||
span {{_ 'subtext-with-parent'}}
|
||||
a#no-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
|
||||
span {{_ 'no-parent'}}
|
||||
div
|
||||
hr
|
||||
|
||||
div.check-div
|
||||
a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
||||
span {{_ 'show-subtasks-field'}}
|
||||
|
||||
label
|
||||
| {{_ 'deposit-subtasks-board'}}
|
||||
select.js-field-deposit-board(disabled="{{#unless allowsSubtasks}}disabled{{/unless}}")
|
||||
each boards
|
||||
if isBoardSelected
|
||||
option(value=_id selected="selected") {{title}}
|
||||
else
|
||||
option(value=_id) {{title}}
|
||||
if isNullBoardSelected
|
||||
option(value='null' selected="selected") {{_ 'custom-field-dropdown-none'}}
|
||||
else
|
||||
option(value='null') {{_ 'custom-field-dropdown-none'}}
|
||||
div
|
||||
hr
|
||||
|
||||
label
|
||||
| {{_ 'deposit-subtasks-list'}}
|
||||
select.js-field-deposit-list(disabled="{{#unless hasLists}}disabled{{/unless}}")
|
||||
each lists
|
||||
if isListSelected
|
||||
option(value=_id selected="selected") {{title}}
|
||||
else
|
||||
option(value=_id) {{title}}
|
||||
|
||||
template(name="createBoard")
|
||||
form
|
||||
label
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ Template.boardMenuPopup.events({
|
|||
}),
|
||||
'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
|
||||
});
|
||||
|
||||
Template.boardMenuPopup.helpers({
|
||||
|
|
@ -153,6 +154,102 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('boardChangeColorPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
},
|
||||
|
||||
allowsSubtasks() {
|
||||
return this.currentBoard.allowsSubtasks;
|
||||
},
|
||||
|
||||
isBoardSelected() {
|
||||
return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
|
||||
},
|
||||
|
||||
isNullBoardSelected() {
|
||||
return (this.currentBoard.subtasksDefaultBoardId === null) || (this.currentBoard.subtasksDefaultBoardId === undefined);
|
||||
},
|
||||
|
||||
boards() {
|
||||
return Boards.find({
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
}, {
|
||||
sort: ['title'],
|
||||
});
|
||||
},
|
||||
|
||||
lists() {
|
||||
return Lists.find({
|
||||
boardId: this.currentBoard._id,
|
||||
archived: false,
|
||||
}, {
|
||||
sort: ['title'],
|
||||
});
|
||||
},
|
||||
|
||||
hasLists() {
|
||||
return this.lists().count() > 0;
|
||||
},
|
||||
|
||||
isListSelected() {
|
||||
return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
|
||||
},
|
||||
|
||||
presentParentTask() {
|
||||
let result = this.currentBoard.presentParentTask;
|
||||
if ((result === null) || (result === undefined)) {
|
||||
result = 'no-parent';
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'click .js-field-has-subtasks'(evt) {
|
||||
evt.preventDefault();
|
||||
this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks;
|
||||
this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks);
|
||||
$('.js-field-has-subtasks .materialCheckBox').toggleClass('is-checked', this.currentBoard.allowsSubtasks);
|
||||
$('.js-field-has-subtasks').toggleClass('is-checked', this.currentBoard.allowsSubtasks);
|
||||
$('.js-field-deposit-board').prop('disabled', !this.currentBoard.allowsSubtasks);
|
||||
},
|
||||
'change .js-field-deposit-board'(evt) {
|
||||
let value = evt.target.value;
|
||||
if (value === 'null') {
|
||||
value = null;
|
||||
}
|
||||
this.currentBoard.setSubtasksDefaultBoardId(value);
|
||||
evt.preventDefault();
|
||||
},
|
||||
'change .js-field-deposit-list'(evt) {
|
||||
this.currentBoard.setSubtasksDefaultListId(evt.target.value);
|
||||
evt.preventDefault();
|
||||
},
|
||||
'click .js-field-show-parent-in-minicard'(evt) {
|
||||
const value = evt.target.id || $(evt.target).parent()[0].id || $(evt.target).parent()[0].parent()[0].id;
|
||||
const options = [
|
||||
'prefix-with-full-path',
|
||||
'prefix-with-parent',
|
||||
'subtext-with-full-path',
|
||||
'subtext-with-parent',
|
||||
'no-parent'];
|
||||
options.forEach(function(element) {
|
||||
if (element !== value) {
|
||||
$(`#${element} .materialCheckBox`).toggleClass('is-checked', false);
|
||||
$(`#${element}`).toggleClass('is-checked', false);
|
||||
}
|
||||
});
|
||||
$(`#${value} .materialCheckBox`).toggleClass('is-checked', true);
|
||||
$(`#${value}`).toggleClass('is-checked', true);
|
||||
this.currentBoard.setPresentParentTask(value);
|
||||
evt.preventDefault();
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('boardSubtaskSettingsPopup');
|
||||
|
||||
const CreateBoard = BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'createBoard';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,22 @@
|
|||
.integration-form
|
||||
padding: 5px
|
||||
border-bottom: 1px solid #ccc
|
||||
|
||||
.flex
|
||||
display: -webkit-box
|
||||
display: -moz-box
|
||||
display: -webkit-flex
|
||||
display: -moz-flex
|
||||
display: -ms-flexbox
|
||||
display: flex
|
||||
|
||||
.option
|
||||
@extends .flex
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
text-decoration: none;
|
||||
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
margin-top: 5px;
|
||||
padding: 5px;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ template(name="cardDetails")
|
|||
= title
|
||||
if isWatching
|
||||
i.fa.fa-eye.card-details-watch
|
||||
.card-details-path
|
||||
each parentList
|
||||
| >
|
||||
a.js-parent-card(href=linkForCard) {{title}}
|
||||
// else
|
||||
{{_ 'top-level-card'}}
|
||||
|
||||
if archived
|
||||
p.warning {{_ 'card-archived'}}
|
||||
|
|
@ -144,6 +150,10 @@ template(name="cardDetails")
|
|||
hr
|
||||
+checklists(cardId = _id)
|
||||
|
||||
if currentBoard.allowsSubtasks
|
||||
hr
|
||||
+subtasks(cardId = _id)
|
||||
|
||||
hr
|
||||
h3
|
||||
i.fa.fa-paperclip
|
||||
|
|
@ -273,10 +283,37 @@ template(name="cardMorePopup")
|
|||
button.js-copy-card-link-to-clipboard(class="btn") {{_ 'copy-card-link-to-clipboard'}}
|
||||
span.clearfix
|
||||
br
|
||||
h2 {{_ 'change-card-parent'}}
|
||||
label {{_ 'source-board'}}:
|
||||
select.js-field-parent-board
|
||||
each boards
|
||||
if isParentBoard
|
||||
option(value="{{_id}}" selected) {{title}}
|
||||
else
|
||||
option(value="{{_id}}") {{title}}
|
||||
if isTopLevel
|
||||
option(value="none" selected) {{_ 'custom-field-dropdown-none'}}
|
||||
else
|
||||
option(value="none") {{_ 'custom-field-dropdown-none'}}
|
||||
|
||||
label {{_ 'parent-card'}}:
|
||||
select.js-field-parent-card
|
||||
if isTopLevel
|
||||
option(value="none" selected) {{_ 'custom-field-dropdown-none'}}
|
||||
else
|
||||
each cards
|
||||
if isParentCard
|
||||
option(value="{{_id}}" selected) {{title}}
|
||||
else
|
||||
option(value="{{_id}}") {{title}}
|
||||
option(value="none") {{_ 'custom-field-dropdown-none'}}
|
||||
br
|
||||
| {{_ 'added'}}
|
||||
span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
|
||||
a.js-delete(title="{{_ 'card-delete-notice'}}") {{_ 'delete'}}
|
||||
|
||||
|
||||
|
||||
template(name="cardDeletePopup")
|
||||
p {{_ "card-delete-pop"}}
|
||||
unless archived
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
|
||||
onCreated() {
|
||||
this.currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
this.isLoaded = new ReactiveVar(false);
|
||||
const boardBody = this.parentComponent().parentComponent();
|
||||
//in Miniview parent is Board, not BoardBody.
|
||||
if (boardBody !== null){
|
||||
if (boardBody !== null) {
|
||||
boardBody.showOverlay.set(true);
|
||||
boardBody.mouseHasEnterCardDetails = false;
|
||||
}
|
||||
|
|
@ -70,6 +71,30 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
|
||||
presentParentTask() {
|
||||
let result = this.currentBoard.presentParentTask;
|
||||
if ((result === null) || (result === undefined)) {
|
||||
result = 'no-parent';
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
linkForCard() {
|
||||
const card = this.currentData();
|
||||
let result = '#';
|
||||
if (card) {
|
||||
const board = Boards.findOne(card.boardId);
|
||||
if (board) {
|
||||
result = FlowRouter.url('card', {
|
||||
boardId: card.boardId,
|
||||
slug: board.slug,
|
||||
cardId: card._id,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
if (!Utils.isMiniScreen()) this.scrollParentContainer();
|
||||
const $checklistsDom = this.$('.card-checklist-items');
|
||||
|
|
@ -107,6 +132,41 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
});
|
||||
|
||||
const $subtasksDom = this.$('.card-subtasks-items');
|
||||
|
||||
$subtasksDom.sortable({
|
||||
tolerance: 'pointer',
|
||||
helper: 'clone',
|
||||
handle: '.subtask-title',
|
||||
items: '.js-subtasks',
|
||||
placeholder: 'subtasks placeholder',
|
||||
distance: 7,
|
||||
start(evt, ui) {
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
},
|
||||
stop(evt, ui) {
|
||||
let prevChecklist = ui.item.prev('.js-subtasks').get(0);
|
||||
if (prevChecklist) {
|
||||
prevChecklist = Blaze.getData(prevChecklist).subtask;
|
||||
}
|
||||
let nextChecklist = ui.item.next('.js-subtasks').get(0);
|
||||
if (nextChecklist) {
|
||||
nextChecklist = Blaze.getData(nextChecklist).subtask;
|
||||
}
|
||||
const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1);
|
||||
|
||||
$subtasksDom.sortable('cancel');
|
||||
const subtask = Blaze.getData(ui.item.get(0)).subtask;
|
||||
|
||||
Subtasks.update(subtask._id, {
|
||||
$set: {
|
||||
subtaskSort: sortIndex.base,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function userIsMember() {
|
||||
return Meteor.user() && Meteor.user().isBoardMember();
|
||||
}
|
||||
|
|
@ -116,6 +176,9 @@ BlazeComponent.extendComponent({
|
|||
if ($checklistsDom.data('sortable')) {
|
||||
$checklistsDom.sortable('option', 'disabled', !userIsMember());
|
||||
}
|
||||
if ($subtasksDom.data('sortable')) {
|
||||
$subtasksDom.sortable('option', 'disabled', !userIsMember());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -327,7 +390,6 @@ Template.moveCardPopup.events({
|
|||
Popup.close();
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
subManager.subscribe('board', Session.get('currentBoard'));
|
||||
|
|
@ -364,6 +426,21 @@ 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'));
|
||||
|
|
@ -392,19 +469,18 @@ Template.copyCardPopup.events({
|
|||
|
||||
// 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 checklist = arguments[0];
|
||||
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);
|
||||
});
|
||||
const subtask = arguments[0];
|
||||
subtask.parentId = _id;
|
||||
subtask._id = null;
|
||||
/* const newSubtaskId = */ Cards.insert(subtask);
|
||||
});
|
||||
|
||||
// copy card comments
|
||||
|
|
@ -453,19 +529,18 @@ Template.copyChecklistToManyCardsPopup.events({
|
|||
|
||||
// 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 checklist = arguments[0];
|
||||
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);
|
||||
});
|
||||
const subtask = arguments[0];
|
||||
subtask.parentId = _id;
|
||||
subtask._id = null;
|
||||
/* const newSubtaskId = */ Cards.insert(subtask);
|
||||
});
|
||||
|
||||
// copy card comments
|
||||
|
|
@ -483,36 +558,119 @@ Template.copyChecklistToManyCardsPopup.events({
|
|||
},
|
||||
});
|
||||
|
||||
|
||||
Template.cardMorePopup.events({
|
||||
'click .js-copy-card-link-to-clipboard' () {
|
||||
// Clipboard code from:
|
||||
// https://stackoverflow.com/questions/6300213/copy-selected-text-to-the-clipboard-without-using-flash-must-be-cross-browser
|
||||
const StringToCopyElement = document.getElementById('cardURL');
|
||||
StringToCopyElement.select();
|
||||
if (document.execCommand('copy')) {
|
||||
StringToCopyElement.blur();
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentCard = this.currentData();
|
||||
this.parentCard = this.currentCard.parentCard();
|
||||
if (this.parentCard) {
|
||||
this.parentBoard = this.parentCard.board();
|
||||
} else {
|
||||
document.getElementById('cardURL').selectionStart = 0;
|
||||
document.getElementById('cardURL').selectionEnd = 999;
|
||||
document.execCommand('copy');
|
||||
if (window.getSelection) {
|
||||
if (window.getSelection().empty) { // Chrome
|
||||
window.getSelection().empty();
|
||||
} else if (window.getSelection().removeAllRanges) { // Firefox
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
} else if (document.selection) { // IE?
|
||||
document.selection.empty();
|
||||
}
|
||||
this.parentBoard = null;
|
||||
}
|
||||
},
|
||||
'click .js-delete': Popup.afterConfirm('cardDelete', function () {
|
||||
Popup.close();
|
||||
Cards.remove(this._id);
|
||||
Utils.goBoardId(this.boardId);
|
||||
}),
|
||||
});
|
||||
|
||||
boards() {
|
||||
const boards = Boards.find({
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
}, {
|
||||
sort: ['title'],
|
||||
});
|
||||
return boards;
|
||||
},
|
||||
|
||||
cards() {
|
||||
if (this.parentBoard) {
|
||||
return this.parentBoard.cards();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
isParentBoard() {
|
||||
const board = this.currentData();
|
||||
if (this.parentBoard) {
|
||||
return board._id === this.parentBoard;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isParentCard() {
|
||||
const card = this.currentData();
|
||||
if (this.parentCard) {
|
||||
return card._id === this.parentCard;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
setParentCardId(cardId) {
|
||||
if (cardId === 'null') {
|
||||
cardId = null;
|
||||
this.parentCard = null;
|
||||
} else {
|
||||
this.parentCard = Cards.findOne(cardId);
|
||||
}
|
||||
this.currentCard.setParentId(cardId);
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'click .js-copy-card-link-to-clipboard' () {
|
||||
// Clipboard code from:
|
||||
// https://stackoverflow.com/questions/6300213/copy-selected-text-to-the-clipboard-without-using-flash-must-be-cross-browser
|
||||
const StringToCopyElement = document.getElementById('cardURL');
|
||||
StringToCopyElement.select();
|
||||
if (document.execCommand('copy')) {
|
||||
StringToCopyElement.blur();
|
||||
} else {
|
||||
document.getElementById('cardURL').selectionStart = 0;
|
||||
document.getElementById('cardURL').selectionEnd = 999;
|
||||
document.execCommand('copy');
|
||||
if (window.getSelection) {
|
||||
if (window.getSelection().empty) { // Chrome
|
||||
window.getSelection().empty();
|
||||
} else if (window.getSelection().removeAllRanges) { // Firefox
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
} else if (document.selection) { // IE?
|
||||
document.selection.empty();
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-delete': Popup.afterConfirm('cardDelete', function () {
|
||||
Popup.close();
|
||||
Cards.remove(this._id);
|
||||
Utils.goBoardId(this.boardId);
|
||||
}),
|
||||
'change .js-field-parent-board'(evt) {
|
||||
const selection = $(evt.currentTarget).val();
|
||||
const list = $('.js-field-parent-card');
|
||||
list.empty();
|
||||
if (selection === 'none') {
|
||||
this.parentBoard = null;
|
||||
list.prop('disabled', true);
|
||||
} else {
|
||||
this.parentBoard = Boards.findOne(selection);
|
||||
this.parentBoard.cards().forEach(function(card) {
|
||||
list.append(
|
||||
$('<option></option>').val(card._id).html(card.title)
|
||||
);
|
||||
});
|
||||
list.prop('disabled', false);
|
||||
}
|
||||
list.append(
|
||||
`<option value='none' selected='selected'>${TAPi18n.__('custom-field-dropdown-none')}</option>`
|
||||
);
|
||||
this.setParentCardId('null');
|
||||
},
|
||||
'change .js-field-parent-card'(evt) {
|
||||
const selection = $(evt.currentTarget).val();
|
||||
this.setParentCardId(selection);
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('cardMorePopup');
|
||||
|
||||
|
||||
// Close the card details pane by pressing escape
|
||||
EscapeActions.register('detailsPane',
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ template(name="checklistDetail")
|
|||
if canModifyCard
|
||||
a.js-delete-checklist.toggle-delete-checklist-dialog {{_ "delete"}}...
|
||||
|
||||
span.checklist-stat(class="{{#if checklist.isFinished}}is-finished{{/if}}") {{checklist.finishedCount}}/{{checklist.itemCount}}
|
||||
if canModifyCard
|
||||
h2.title.js-open-inlined-form.is-editable
|
||||
+viewer
|
||||
|
|
@ -75,7 +74,7 @@ template(name="checklistItems")
|
|||
+inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
|
||||
+editChecklistItemForm(type = 'item' item = item checklist = checklist)
|
||||
else
|
||||
+itemDetail(item = item checklist = checklist)
|
||||
+checklistItemDetail(item = item checklist = checklist)
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist)
|
||||
+addChecklistItemForm
|
||||
|
|
@ -84,7 +83,7 @@ template(name="checklistItems")
|
|||
i.fa.fa-plus
|
||||
| {{_ 'add-checklist-item'}}...
|
||||
|
||||
template(name='itemDetail')
|
||||
template(name='checklistItemDetail')
|
||||
.js-checklist-item.checklist-item
|
||||
if canModifyCard
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ Template.checklistDeleteDialog.onDestroyed(() => {
|
|||
$cardDetails.animate( { scrollTop: this.scrollState.position });
|
||||
});
|
||||
|
||||
Template.itemDetail.helpers({
|
||||
Template.checklistItemDetail.helpers({
|
||||
canModifyCard() {
|
||||
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
|
||||
},
|
||||
|
|
@ -223,4 +223,4 @@ BlazeComponent.extendComponent({
|
|||
'click .js-checklist-item .check-box': this.toggleItem,
|
||||
}];
|
||||
},
|
||||
}).register('itemDetail');
|
||||
}).register('checklistItemDetail');
|
||||
|
|
|
|||
|
|
@ -7,8 +7,21 @@ template(name="minicard")
|
|||
each labels
|
||||
.minicard-label(class="card-label-{{color}}" title="{{name}}")
|
||||
.minicard-title
|
||||
if $eq 'prefix-with-full-path' currentBoard.presentParentTask
|
||||
.parent-prefix
|
||||
| {{ parentString ' > ' }}
|
||||
if $eq 'prefix-with-parent' currentBoard.presentParentTask
|
||||
.parent-prefix
|
||||
| {{ parentCardName }}
|
||||
+viewer
|
||||
= title
|
||||
{{ title }}
|
||||
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentString ' > ' }}
|
||||
if $eq 'subtext-with-parent' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentCardName }}
|
||||
|
||||
.dates
|
||||
if receivedAt
|
||||
unless startAt
|
||||
|
|
|
|||
|
|
@ -162,6 +162,13 @@
|
|||
margin-bottom: 20px
|
||||
overflow-y: auto
|
||||
|
||||
.parent-prefix
|
||||
color: darken(white, 30%)
|
||||
font-size: 0.9em
|
||||
.parent-subtext
|
||||
color: darken(white, 30%)
|
||||
font-size: 0.9em
|
||||
|
||||
@media screen and (max-width: 800px)
|
||||
.minicard
|
||||
.is-selected &
|
||||
|
|
|
|||
97
client/components/cards/subtasks.jade
Normal file
97
client/components/cards/subtasks.jade
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
template(name="subtasks")
|
||||
h3 {{_ 'subtasks'}}
|
||||
if toggleDeleteDialog.get
|
||||
.board-overlay#card-details-overlay
|
||||
+subtaskDeleteDialog(subtask = subtaskToDelete)
|
||||
|
||||
|
||||
.card-subtasks-items
|
||||
each subtask in currentCard.subtasks
|
||||
+subtaskDetail(subtask = subtask)
|
||||
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-subtask" cardId = cardId)
|
||||
+addSubtaskItemForm
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-subtask'}}...
|
||||
|
||||
template(name="subtaskDetail")
|
||||
.js-subtasks.subtask
|
||||
+inlinedForm(classNames="js-edit-subtask-title" subtask = subtask)
|
||||
+editSubtaskItemForm(subtask = subtask)
|
||||
else
|
||||
.subtask-title
|
||||
span
|
||||
a.js-view-subtask(title="{{ subtask.title }}") {{_ "view-it"}}
|
||||
if canModifyCard
|
||||
a.js-delete-subtask.toggle-delete-subtask-dialog {{_ "delete"}}...
|
||||
|
||||
if canModifyCard
|
||||
h2.title.js-open-inlined-form.is-editable
|
||||
+viewer
|
||||
= subtask.title
|
||||
else
|
||||
h2.title
|
||||
+viewer
|
||||
= subtask.title
|
||||
|
||||
template(name="subtaskDeleteDialog")
|
||||
.js-confirm-subtask-delete
|
||||
p
|
||||
i(class="fa fa-exclamation-triangle" aria-hidden="true")
|
||||
p
|
||||
| {{_ 'confirm-subtask-delete-dialog'}}
|
||||
span {{subtask.title}}
|
||||
| ?
|
||||
.js-subtask-delete-buttons
|
||||
button.confirm-subtask-delete(type="button") {{_ 'delete'}}
|
||||
button.toggle-delete-subtask-dialog(type="button") {{_ 'cancel'}}
|
||||
|
||||
template(name="addSubtaskItemForm")
|
||||
textarea.js-add-subtask-item(rows='1' autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-add-subtask-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
|
||||
template(name="editSubtaskItemForm")
|
||||
textarea.js-edit-subtask-item(rows='1' autofocus)
|
||||
if $eq type 'item'
|
||||
= item.title
|
||||
else
|
||||
= subtask.title
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-subtask-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
span(title=createdAt) {{ moment createdAt }}
|
||||
if canModifyCard
|
||||
a.js-delete-subtask-item {{_ "delete"}}...
|
||||
|
||||
template(name="subtasksItems")
|
||||
.subtasks-items.js-subtasks-items
|
||||
each item in subtasks.items
|
||||
+inlinedForm(classNames="js-edit-subtask-item" item = item subtasks = subtasks)
|
||||
+editSubtaskItemForm(type = 'item' item = item subtasks = subtasks)
|
||||
else
|
||||
+subtaskItemDetail(item = item subtasks = subtasks)
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-subtask-item" subtasks = subtasks)
|
||||
+addSubtaskItemForm
|
||||
else
|
||||
a.add-subtask-item.js-open-inlined-form
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-subtask-item'}}...
|
||||
|
||||
template(name='subtaskItemDetail')
|
||||
.js-subtasks-item.subtasks-item
|
||||
if canModifyCard
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
else
|
||||
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
145
client/components/cards/subtasks.js
Normal file
145
client/components/cards/subtasks.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
BlazeComponent.extendComponent({
|
||||
canModifyCard() {
|
||||
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
|
||||
},
|
||||
}).register('subtaskDetail');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
|
||||
addSubtask(event) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-add-subtask-item');
|
||||
const title = textarea.value.trim();
|
||||
const cardId = this.currentData().cardId;
|
||||
const card = Cards.findOne(cardId);
|
||||
const sortIndex = -1;
|
||||
const crtBoard = Boards.findOne(card.boardId);
|
||||
const targetBoard = crtBoard.getDefaultSubtasksBoard();
|
||||
const listId = targetBoard.getDefaultSubtasksListId();
|
||||
const swimlaneId = targetBoard.getDefaultSwimline()._id;
|
||||
|
||||
if (title) {
|
||||
const _id = Cards.insert({
|
||||
title,
|
||||
parentId: cardId,
|
||||
members: [],
|
||||
labelIds: [],
|
||||
customFields: [],
|
||||
listId,
|
||||
boardId: targetBoard._id,
|
||||
sort: sortIndex,
|
||||
swimlaneId,
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
this.$('.add-subtask-item').last().click();
|
||||
}, 100);
|
||||
}
|
||||
textarea.value = '';
|
||||
textarea.focus();
|
||||
},
|
||||
|
||||
canModifyCard() {
|
||||
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
|
||||
},
|
||||
|
||||
deleteSubtask() {
|
||||
const subtask = this.currentData().subtask;
|
||||
if (subtask && subtask._id) {
|
||||
subtask.archive();
|
||||
this.toggleDeleteDialog.set(false);
|
||||
}
|
||||
},
|
||||
|
||||
editSubtask(event) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-edit-subtask-item');
|
||||
const title = textarea.value.trim();
|
||||
const subtask = this.currentData().subtask;
|
||||
subtask.setTitle(title);
|
||||
},
|
||||
|
||||
onCreated() {
|
||||
this.toggleDeleteDialog = new ReactiveVar(false);
|
||||
this.subtaskToDelete = null; //Store data context to pass to subtaskDeleteDialog template
|
||||
},
|
||||
|
||||
pressKey(event) {
|
||||
//If user press enter key inside a form, submit it
|
||||
//Unless the user is also holding down the 'shift' key
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
const $form = $(event.currentTarget).closest('form');
|
||||
$form.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
const events = {
|
||||
'click .toggle-delete-subtask-dialog'(event) {
|
||||
if($(event.target).hasClass('js-delete-subtask')){
|
||||
this.subtaskToDelete = this.currentData().subtask; //Store data context
|
||||
}
|
||||
this.toggleDeleteDialog.set(!this.toggleDeleteDialog.get());
|
||||
},
|
||||
'click .js-view-subtask'(event) {
|
||||
if($(event.target).hasClass('js-view-subtask')){
|
||||
const subtask = this.currentData().subtask;
|
||||
const board = subtask.board();
|
||||
FlowRouter.go('card', {
|
||||
boardId: board._id,
|
||||
slug: board.slug,
|
||||
cardId: subtask._id,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return [{
|
||||
...events,
|
||||
'submit .js-add-subtask': this.addSubtask,
|
||||
'submit .js-edit-subtask-title': this.editSubtask,
|
||||
'click .confirm-subtask-delete': this.deleteSubtask,
|
||||
keydown: this.pressKey,
|
||||
}];
|
||||
},
|
||||
}).register('subtasks');
|
||||
|
||||
Template.subtaskDeleteDialog.onCreated(() => {
|
||||
const $cardDetails = this.$('.card-details');
|
||||
this.scrollState = { position: $cardDetails.scrollTop(), //save current scroll position
|
||||
top: false, //required for smooth scroll animation
|
||||
};
|
||||
//Callback's purpose is to only prevent scrolling after animation is complete
|
||||
$cardDetails.animate({ scrollTop: 0 }, 500, () => { this.scrollState.top = true; });
|
||||
|
||||
//Prevent scrolling while dialog is open
|
||||
$cardDetails.on('scroll', () => {
|
||||
if(this.scrollState.top) { //If it's already in position, keep it there. Otherwise let animation scroll
|
||||
$cardDetails.scrollTop(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.subtaskDeleteDialog.onDestroyed(() => {
|
||||
const $cardDetails = this.$('.card-details');
|
||||
$cardDetails.off('scroll'); //Reactivate scrolling
|
||||
$cardDetails.animate( { scrollTop: this.scrollState.position });
|
||||
});
|
||||
|
||||
Template.subtaskItemDetail.helpers({
|
||||
canModifyCard() {
|
||||
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
// ...
|
||||
}).register('subtaskItemDetail');
|
||||
142
client/components/cards/subtasks.styl
Normal file
142
client/components/cards/subtasks.styl
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
.js-add-subtask
|
||||
color: #8c8c8c
|
||||
|
||||
textarea.js-add-subtask-item, textarea.js-edit-subtask-item
|
||||
overflow: hidden
|
||||
word-wrap: break-word
|
||||
resize: none
|
||||
height: 34px
|
||||
|
||||
.delete-text
|
||||
color: #8c8c8c
|
||||
text-decoration: underline
|
||||
word-wrap: break-word
|
||||
float: right
|
||||
padding-top: 6px
|
||||
&:hover
|
||||
color: inherit
|
||||
|
||||
.subtask-title
|
||||
.checkbox
|
||||
float: left
|
||||
width: 30px
|
||||
height 30px
|
||||
font-size: 18px
|
||||
line-height: 30px
|
||||
|
||||
.title
|
||||
font-size: 18px
|
||||
line-height: 25px
|
||||
|
||||
.subtasks-stat
|
||||
margin: 0 0.5em
|
||||
float: right
|
||||
padding-top: 6px
|
||||
&.is-finished
|
||||
color: #3cb500
|
||||
|
||||
.js-delete-subtask
|
||||
@extends .delete-text
|
||||
margin: 0 0.5em
|
||||
|
||||
.js-view-subtask
|
||||
@extends .delete-text
|
||||
|
||||
.js-confirm-subtask-delete
|
||||
background-color: darken(white, 3%)
|
||||
position: absolute
|
||||
float: left;
|
||||
width: 60%
|
||||
margin-top: 0
|
||||
margin-left: 13%
|
||||
padding-bottom: 2%
|
||||
padding-left: 3%
|
||||
padding-right: 3%
|
||||
z-index: 17
|
||||
border-radius: 3px
|
||||
|
||||
p
|
||||
position: relative
|
||||
margin-top: 3%
|
||||
width: 100%
|
||||
text-align: center
|
||||
span
|
||||
font-weight: bold
|
||||
|
||||
i
|
||||
font-size: 2em
|
||||
|
||||
.js-subtask-delete-buttons
|
||||
position: relative
|
||||
padding: left 2% right 2%
|
||||
.confirm-subtask-delete
|
||||
margin-left: 12%
|
||||
float: left
|
||||
.toggle-delete-subtask-dialog
|
||||
margin-right: 12%
|
||||
float: right
|
||||
|
||||
#card-details-overlay
|
||||
top: 0
|
||||
bottom: -600px
|
||||
right: 0
|
||||
|
||||
.subtasks
|
||||
background: darken(white, 3%)
|
||||
|
||||
&.placeholder
|
||||
background: darken(white, 20%)
|
||||
border-radius: 2px
|
||||
|
||||
&.ui-sortable-helper
|
||||
box-shadow: -2px 2px 8px rgba(0, 0, 0, .3),
|
||||
0 0 1px rgba(0, 0, 0, .5)
|
||||
transform: rotate(4deg)
|
||||
cursor: grabbing
|
||||
|
||||
|
||||
.subtasks-item
|
||||
margin: 0 0 0 0.1em
|
||||
line-height: 18px
|
||||
font-size: 1.1em
|
||||
margin-top: 3px
|
||||
display: flex
|
||||
background: darken(white, 3%)
|
||||
|
||||
&.placeholder
|
||||
background: darken(white, 20%)
|
||||
border-radius: 2px
|
||||
|
||||
&.ui-sortable-helper
|
||||
box-shadow: -2px 2px 8px rgba(0, 0, 0, .3),
|
||||
0 0 1px rgba(0, 0, 0, .5)
|
||||
transform: rotate(4deg)
|
||||
cursor: grabbing
|
||||
|
||||
&:hover
|
||||
background-color: darken(white, 8%)
|
||||
|
||||
.check-box
|
||||
margin: 0.1em 0 0 0;
|
||||
&.is-checked
|
||||
border-bottom: 2px solid #3cb500
|
||||
border-right: 2px solid #3cb500
|
||||
|
||||
.item-title
|
||||
flex: 1
|
||||
padding-left: 10px;
|
||||
&.is-checked
|
||||
color: #8c8c8c
|
||||
font-style: italic
|
||||
& .viewer
|
||||
p
|
||||
margin-bottom: 2px
|
||||
|
||||
.js-delete-subtask-item
|
||||
margin: 0 0 0.5em 1.33em
|
||||
@extends .delete-text
|
||||
padding: 12px 0 0 0
|
||||
|
||||
.add-subtask-item
|
||||
margin: 0.2em 0 0.5em 1.33em
|
||||
display: inline-block
|
||||
|
|
@ -36,17 +36,14 @@ BlazeComponent.extendComponent({
|
|||
const members = formComponent.members.get();
|
||||
const labelIds = formComponent.labels.get();
|
||||
const customFields = formComponent.customFields.get();
|
||||
//console.log('members', members);
|
||||
//console.log('labelIds', labelIds);
|
||||
//console.log('customFields', customFields);
|
||||
|
||||
const boardId = this.data().board()._id;
|
||||
const boardId = this.data().board();
|
||||
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 = Swimlanes.findOne({boardId})._id;
|
||||
swimlaneId = boardId.getDefaultSwimline()._id;
|
||||
|
||||
if (title) {
|
||||
const _id = Cards.insert({
|
||||
|
|
@ -55,7 +52,7 @@ BlazeComponent.extendComponent({
|
|||
labelIds,
|
||||
customFields,
|
||||
listId: this.data()._id,
|
||||
boardId: this.data().board()._id,
|
||||
boardId: boardId._id,
|
||||
sort: sortIndex,
|
||||
swimlaneId,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"accept": "Accept",
|
||||
"act-activity-notify": "[Wekan] Activity Notification",
|
||||
"act-addAttachment": "attached __attachment__ to __card__",
|
||||
"act-addSubtask": "added subtask __checklist__ to __card__",
|
||||
"act-addChecklist": "added checklist __checklist__ to __card__",
|
||||
"act-addChecklistItem": "added __checklistItem__ to checklist __checklist__ on __card__",
|
||||
"act-addComment": "commented on __card__: __comment__",
|
||||
|
|
@ -41,6 +42,7 @@
|
|||
"activity-removed": "removed %s from %s",
|
||||
"activity-sent": "sent %s to %s",
|
||||
"activity-unjoined": "unjoined %s",
|
||||
"activity-subtask-added": "added subtask to %s",
|
||||
"activity-checklist-added": "added checklist to %s",
|
||||
"activity-checklist-item-added": "added checklist item to '%s' in %s",
|
||||
"add": "Add",
|
||||
|
|
@ -48,6 +50,7 @@
|
|||
"add-board": "Add Board",
|
||||
"add-card": "Add Card",
|
||||
"add-swimlane": "Add Swimlane",
|
||||
"add-subtask": "Add Subtask",
|
||||
"add-checklist": "Add Checklist",
|
||||
"add-checklist-item": "Add an item to checklist",
|
||||
"add-cover": "Add Cover",
|
||||
|
|
@ -141,6 +144,7 @@
|
|||
"changePasswordPopup-title": "Change Password",
|
||||
"changePermissionsPopup-title": "Change Permissions",
|
||||
"changeSettingsPopup-title": "Change Settings",
|
||||
"subtasks": "Subtasks",
|
||||
"checklists": "Checklists",
|
||||
"click-to-star": "Click to star this board.",
|
||||
"click-to-unstar": "Click to unstar this board.",
|
||||
|
|
@ -163,6 +167,7 @@
|
|||
"comment-only": "Comment only",
|
||||
"comment-only-desc": "Can comment on cards only.",
|
||||
"computer": "Computer",
|
||||
"confirm-subtask-delete-dialog": "Are you sure you want to delete subtask",
|
||||
"confirm-checklist-delete-dialog": "Are you sure you want to delete checklist",
|
||||
"copy-card-link-to-clipboard": "Copy card link to clipboard",
|
||||
"copyCardPopup-title": "Copy Card",
|
||||
|
|
@ -475,5 +480,23 @@
|
|||
"board-delete-notice": "Deleting is permanent. You will lose all lists, cards and actions associated with this board.",
|
||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||
"boardDeletePopup-title": "Delete Board?",
|
||||
"delete-board": "Delete Board"
|
||||
"delete-board": "Delete Board",
|
||||
"default-subtasks-board": "Subtasks for __board__ board",
|
||||
"default": "Default",
|
||||
"queue": "Queue",
|
||||
"subtask-settings": "Subtasks Settings",
|
||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
||||
"show-subtasks-field": "Cards can have subtasks",
|
||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
|
||||
"show-parent-in-minicard": "Show parent in minicard:",
|
||||
"prefix-with-full-path": "Prefix with full path",
|
||||
"prefix-with-parent": "Prefix with parent",
|
||||
"subtext-with-full-path": "Subtext with full path",
|
||||
"subtext-with-parent": "Subtext with parent",
|
||||
"change-card-parent": "Change card's parent",
|
||||
"parent-card": "Parent card",
|
||||
"source-board": "Source board",
|
||||
"no-parent": "Don't show parent"
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ Activities.helpers({
|
|||
checklistItem() {
|
||||
return ChecklistItems.findOne(this.checklistItemId);
|
||||
},
|
||||
subtasks() {
|
||||
return Cards.findOne(this.subtaskId);
|
||||
},
|
||||
customField() {
|
||||
return CustomFields.findOne(this.customFieldId);
|
||||
},
|
||||
|
|
|
|||
102
models/boards.js
102
models/boards.js
|
|
@ -151,6 +151,32 @@ Boards.attachSchema(new SimpleSchema({
|
|||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
subtasksDefaultBoardId: {
|
||||
type: String,
|
||||
optional: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
subtasksDefaultListId: {
|
||||
type: String,
|
||||
optional: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
allowsSubtasks: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
},
|
||||
presentParentTask: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
'prefix-with-full-path',
|
||||
'prefix-with-parent',
|
||||
'subtext-with-full-path',
|
||||
'subtext-with-parent',
|
||||
'no-parent',
|
||||
],
|
||||
optional: true,
|
||||
defaultValue: 'no-parent',
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
|
|
@ -194,6 +220,10 @@ Boards.helpers({
|
|||
return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
|
||||
},
|
||||
|
||||
cards() {
|
||||
return Cards.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
|
||||
},
|
||||
|
||||
hasOvertimeCards(){
|
||||
const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} );
|
||||
return card !== undefined;
|
||||
|
|
@ -284,6 +314,61 @@ Boards.helpers({
|
|||
|
||||
return Cards.find(query, projection);
|
||||
},
|
||||
// A board alwasy has another board where it deposits subtasks of thasks
|
||||
// that belong to itself.
|
||||
getDefaultSubtasksBoardId() {
|
||||
if ((this.subtasksDefaultBoardId === null) || (this.subtasksDefaultBoardId === undefined)) {
|
||||
this.subtasksDefaultBoardId = Boards.insert({
|
||||
title: `^${this.title}^`,
|
||||
permission: this.permission,
|
||||
members: this.members,
|
||||
color: this.color,
|
||||
description: TAPi18n.__('default-subtasks-board', {board: this.title}),
|
||||
});
|
||||
|
||||
Swimlanes.insert({
|
||||
title: TAPi18n.__('default'),
|
||||
boardId: this.subtasksDefaultBoardId,
|
||||
});
|
||||
Boards.update(this._id, {$set: {
|
||||
subtasksDefaultBoardId: this.subtasksDefaultBoardId,
|
||||
}});
|
||||
}
|
||||
return this.subtasksDefaultBoardId;
|
||||
},
|
||||
|
||||
getDefaultSubtasksBoard() {
|
||||
return Boards.findOne(this.getDefaultSubtasksBoardId());
|
||||
},
|
||||
|
||||
getDefaultSubtasksListId() {
|
||||
if ((this.subtasksDefaultListId === null) || (this.subtasksDefaultListId === undefined)) {
|
||||
this.subtasksDefaultListId = Lists.insert({
|
||||
title: TAPi18n.__('queue'),
|
||||
boardId: this._id,
|
||||
});
|
||||
Boards.update(this._id, {$set: {
|
||||
subtasksDefaultListId: this.subtasksDefaultListId,
|
||||
}});
|
||||
}
|
||||
return this.subtasksDefaultListId;
|
||||
},
|
||||
|
||||
getDefaultSubtasksList() {
|
||||
return Lists.findOne(this.getDefaultSubtasksListId());
|
||||
},
|
||||
|
||||
getDefaultSwimline() {
|
||||
let result = Swimlanes.findOne({boardId: this._id});
|
||||
if (result === undefined) {
|
||||
Swimlanes.insert({
|
||||
title: TAPi18n.__('default'),
|
||||
boardId: this._id,
|
||||
});
|
||||
result = Swimlanes.findOne({boardId: this._id});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
cardsInInterval(start, end) {
|
||||
return Cards.find({
|
||||
|
|
@ -313,6 +398,7 @@ Boards.helpers({
|
|||
|
||||
});
|
||||
|
||||
|
||||
Boards.mutations({
|
||||
archive() {
|
||||
return { $set: { archived: true } };
|
||||
|
|
@ -434,6 +520,22 @@ Boards.mutations({
|
|||
},
|
||||
};
|
||||
},
|
||||
|
||||
setAllowsSubtasks(allowsSubtasks) {
|
||||
return { $set: { allowsSubtasks } };
|
||||
},
|
||||
|
||||
setSubtasksDefaultBoardId(subtasksDefaultBoardId) {
|
||||
return { $set: { subtasksDefaultBoardId } };
|
||||
},
|
||||
|
||||
setSubtasksDefaultListId(subtasksDefaultListId) {
|
||||
return { $set: { subtasksDefaultListId } };
|
||||
},
|
||||
|
||||
setPresentParentTask(presentParentTask) {
|
||||
return { $set: { presentParentTask } };
|
||||
},
|
||||
});
|
||||
|
||||
if (Meteor.isServer) {
|
||||
|
|
|
|||
131
models/cards.js
131
models/cards.js
|
|
@ -15,6 +15,11 @@ Cards.attachSchema(new SimpleSchema({
|
|||
}
|
||||
},
|
||||
},
|
||||
parentId: {
|
||||
type: String,
|
||||
optional: true,
|
||||
defaultValue: '',
|
||||
},
|
||||
listId: {
|
||||
type: String,
|
||||
},
|
||||
|
|
@ -122,6 +127,12 @@ Cards.attachSchema(new SimpleSchema({
|
|||
type: Number,
|
||||
decimal: true,
|
||||
},
|
||||
subtaskSort: {
|
||||
type: Number,
|
||||
decimal: true,
|
||||
defaultValue: -1,
|
||||
optional: true,
|
||||
},
|
||||
}));
|
||||
|
||||
Cards.allow({
|
||||
|
|
@ -215,6 +226,42 @@ Cards.helpers({
|
|||
return this.checklistItemCount() !== 0;
|
||||
},
|
||||
|
||||
subtasks() {
|
||||
return Cards.find({
|
||||
parentId: this._id,
|
||||
archived: false,
|
||||
}, {sort: { sort: 1 } });
|
||||
},
|
||||
|
||||
allSubtasks() {
|
||||
return Cards.find({
|
||||
parentId: this._id,
|
||||
archived: false,
|
||||
}, {sort: { sort: 1 } });
|
||||
},
|
||||
|
||||
subtasksCount() {
|
||||
return Cards.find({
|
||||
parentId: this._id,
|
||||
archived: false,
|
||||
}).count();
|
||||
},
|
||||
|
||||
subtasksFinishedCount() {
|
||||
return Cards.find({
|
||||
parentId: this._id,
|
||||
archived: true}).count();
|
||||
},
|
||||
|
||||
subtasksFinished() {
|
||||
const finishCount = this.subtasksFinishedCount();
|
||||
return finishCount > 0 && this.subtasksCount() === finishCount;
|
||||
},
|
||||
|
||||
allowsSubtasks() {
|
||||
return this.subtasksCount() !== 0;
|
||||
},
|
||||
|
||||
customFieldIndex(customFieldId) {
|
||||
return _.pluck(this.customFields, '_id').indexOf(customFieldId);
|
||||
},
|
||||
|
|
@ -271,14 +318,90 @@ Cards.helpers({
|
|||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
parentCard() {
|
||||
if (this.parentId === '') {
|
||||
return null;
|
||||
}
|
||||
return Cards.findOne(this.parentId);
|
||||
},
|
||||
|
||||
parentCardName() {
|
||||
let result = '';
|
||||
if (this.parentId !== '') {
|
||||
const card = Cards.findOne(this.parentId);
|
||||
if (card) {
|
||||
result = card.title;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
parentListId() {
|
||||
const result = [];
|
||||
let crtParentId = this.parentId;
|
||||
while (crtParentId !== '') {
|
||||
const crt = Cards.findOne(crtParentId);
|
||||
if ((crt === null) || (crt === undefined)) {
|
||||
// maybe it has been deleted
|
||||
break;
|
||||
}
|
||||
if (crtParentId in result) {
|
||||
// circular reference
|
||||
break;
|
||||
}
|
||||
result.unshift(crtParentId);
|
||||
crtParentId = crt.parentId;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
parentList() {
|
||||
const resultId = [];
|
||||
const result = [];
|
||||
let crtParentId = this.parentId;
|
||||
while (crtParentId !== '') {
|
||||
const crt = Cards.findOne(crtParentId);
|
||||
if ((crt === null) || (crt === undefined)) {
|
||||
// maybe it has been deleted
|
||||
break;
|
||||
}
|
||||
if (crtParentId in resultId) {
|
||||
// circular reference
|
||||
break;
|
||||
}
|
||||
resultId.unshift(crtParentId);
|
||||
result.unshift(crt);
|
||||
crtParentId = crt.parentId;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
parentString(sep) {
|
||||
return this.parentList().map(function(elem){
|
||||
return elem.title;
|
||||
}).join(sep);
|
||||
},
|
||||
|
||||
isTopLevel() {
|
||||
return this.parentId === '';
|
||||
},
|
||||
});
|
||||
|
||||
Cards.mutations({
|
||||
applyToChildren(funct) {
|
||||
Cards.find({ parentId: this._id }).forEach((card) => {
|
||||
funct(card);
|
||||
});
|
||||
},
|
||||
|
||||
archive() {
|
||||
this.applyToChildren((card) => { return card.archive(); });
|
||||
return {$set: {archived: true}};
|
||||
},
|
||||
|
||||
restore() {
|
||||
this.applyToChildren((card) => { return card.restore(); });
|
||||
return {$set: {archived: false}};
|
||||
},
|
||||
|
||||
|
|
@ -422,6 +545,11 @@ Cards.mutations({
|
|||
unsetSpentTime() {
|
||||
return {$unset: {spentTime: '', isOvertime: false}};
|
||||
},
|
||||
|
||||
setParentId(parentId) {
|
||||
return {$set: {parentId}};
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -513,6 +641,9 @@ function cardRemover(userId, doc) {
|
|||
Checklists.remove({
|
||||
cardId: doc._id,
|
||||
});
|
||||
Subtasks.remove({
|
||||
cardId: doc._id,
|
||||
});
|
||||
CardComments.remove({
|
||||
cardId: doc._id,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,9 +58,11 @@ class Exporter {
|
|||
result.activities = Activities.find(byBoard, noBoardId).fetch();
|
||||
result.checklists = [];
|
||||
result.checklistItems = [];
|
||||
result.subtaskItems = [];
|
||||
result.cards.forEach((card) => {
|
||||
result.checklists.push(...Checklists.find({ cardId: card._id }).fetch());
|
||||
result.checklistItems.push(...ChecklistItems.find({ cardId: card._id }).fetch());
|
||||
result.subtaskItems.push(...Cards.find({ parentid: card._id }).fetch());
|
||||
});
|
||||
|
||||
// [Old] for attachments we only export IDs and absolute url to original doc
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ Migrations.add('lowercase-board-permission', () => {
|
|||
// Security migration: see https://github.com/wekan/wekan/issues/99
|
||||
Migrations.add('change-attachments-type-for-non-images', () => {
|
||||
const newTypeForNonImage = 'application/octet-stream';
|
||||
Attachments.find().forEach((file) => {
|
||||
Attachments.forEach((file) => {
|
||||
if (!file.isImage()) {
|
||||
Attachments.update(file._id, {
|
||||
$set: {
|
||||
|
|
@ -68,7 +68,7 @@ Migrations.add('change-attachments-type-for-non-images', () => {
|
|||
});
|
||||
|
||||
Migrations.add('card-covers', () => {
|
||||
Cards.find().forEach((card) => {
|
||||
Cards.forEach((card) => {
|
||||
const cover = Attachments.findOne({ cardId: card._id, cover: true });
|
||||
if (cover) {
|
||||
Cards.update(card._id, {$set: {coverId: cover._id}}, noValidate);
|
||||
|
|
@ -86,7 +86,7 @@ Migrations.add('use-css-class-for-boards-colors', () => {
|
|||
'#2C3E50': 'midnight',
|
||||
'#E67E22': 'pumpkin',
|
||||
};
|
||||
Boards.find().forEach((board) => {
|
||||
Boards.forEach((board) => {
|
||||
const oldBoardColor = board.background.color;
|
||||
const newBoardColor = associationTable[oldBoardColor];
|
||||
Boards.update(board._id, {
|
||||
|
|
@ -97,7 +97,7 @@ Migrations.add('use-css-class-for-boards-colors', () => {
|
|||
});
|
||||
|
||||
Migrations.add('denormalize-star-number-per-board', () => {
|
||||
Boards.find().forEach((board) => {
|
||||
Boards.forEach((board) => {
|
||||
const nStars = Users.find({'profile.starredBoards': board._id}).count();
|
||||
Boards.update(board._id, {$set: {stars: nStars}}, noValidate);
|
||||
});
|
||||
|
|
@ -132,7 +132,7 @@ Migrations.add('add-member-isactive-field', () => {
|
|||
});
|
||||
|
||||
Migrations.add('add-sort-checklists', () => {
|
||||
Checklists.find().forEach((checklist, index) => {
|
||||
Checklists.forEach((checklist, index) => {
|
||||
if (!checklist.hasOwnProperty('sort')) {
|
||||
Checklists.direct.update(
|
||||
checklist._id,
|
||||
|
|
@ -153,17 +153,8 @@ Migrations.add('add-sort-checklists', () => {
|
|||
});
|
||||
|
||||
Migrations.add('add-swimlanes', () => {
|
||||
Boards.find().forEach((board) => {
|
||||
const swimlane = Swimlanes.findOne({ boardId: board._id });
|
||||
let swimlaneId = '';
|
||||
if (swimlane)
|
||||
swimlaneId = swimlane._id;
|
||||
else
|
||||
swimlaneId = Swimlanes.direct.insert({
|
||||
boardId: board._id,
|
||||
title: 'Default',
|
||||
});
|
||||
|
||||
Boards.forEach((board) => {
|
||||
const swimlaneId = board.getDefaultSwimline()._id;
|
||||
Cards.find({ boardId: board._id }).forEach((card) => {
|
||||
if (!card.hasOwnProperty('swimlaneId')) {
|
||||
Cards.direct.update(
|
||||
|
|
@ -177,7 +168,7 @@ Migrations.add('add-swimlanes', () => {
|
|||
});
|
||||
|
||||
Migrations.add('add-views', () => {
|
||||
Boards.find().forEach((board) => {
|
||||
Boards.forEach((board) => {
|
||||
if (!board.hasOwnProperty('view')) {
|
||||
Boards.direct.update(
|
||||
{ _id: board._id },
|
||||
|
|
@ -189,7 +180,7 @@ Migrations.add('add-views', () => {
|
|||
});
|
||||
|
||||
Migrations.add('add-checklist-items', () => {
|
||||
Checklists.find().forEach((checklist) => {
|
||||
Checklists.forEach((checklist) => {
|
||||
// Create new items
|
||||
_.sortBy(checklist.items, 'sort').forEach((item, index) => {
|
||||
ChecklistItems.direct.insert({
|
||||
|
|
@ -210,7 +201,7 @@ Migrations.add('add-checklist-items', () => {
|
|||
});
|
||||
|
||||
Migrations.add('add-profile-view', () => {
|
||||
Users.find().forEach((user) => {
|
||||
Users.forEach((user) => {
|
||||
if (!user.hasOwnProperty('profile.boardView')) {
|
||||
// Set default view
|
||||
Users.direct.update(
|
||||
|
|
@ -258,3 +249,64 @@ Migrations.add('add-assigner-field', () => {
|
|||
}, noValidateMulti);
|
||||
});
|
||||
|
||||
Migrations.add('add-parent-field-to-cards', () => {
|
||||
Cards.update({
|
||||
parentId: {
|
||||
$exists: false,
|
||||
},
|
||||
}, {
|
||||
$set: {
|
||||
parentId:'',
|
||||
},
|
||||
}, noValidateMulti);
|
||||
});
|
||||
|
||||
Migrations.add('add-subtasks-boards', () => {
|
||||
Boards.update({
|
||||
subtasksDefaultBoardId: {
|
||||
$exists: false,
|
||||
},
|
||||
}, {
|
||||
$set: {
|
||||
subtasksDefaultBoardId: null,
|
||||
subtasksDefaultListId: null,
|
||||
},
|
||||
}, noValidateMulti);
|
||||
});
|
||||
|
||||
Migrations.add('add-subtasks-sort', () => {
|
||||
Boards.update({
|
||||
subtaskSort: {
|
||||
$exists: false,
|
||||
},
|
||||
}, {
|
||||
$set: {
|
||||
subtaskSort: -1,
|
||||
},
|
||||
}, noValidateMulti);
|
||||
});
|
||||
|
||||
Migrations.add('add-subtasks-allowed', () => {
|
||||
Boards.update({
|
||||
allowsSubtasks: {
|
||||
$exists: false,
|
||||
},
|
||||
}, {
|
||||
$set: {
|
||||
allowsSubtasks: true,
|
||||
},
|
||||
}, noValidateMulti);
|
||||
});
|
||||
|
||||
Migrations.add('add-subtasks-allowed', () => {
|
||||
Boards.update({
|
||||
presentParentTask: {
|
||||
$exists: false,
|
||||
},
|
||||
}, {
|
||||
$set: {
|
||||
presentParentTask: 'no-parent',
|
||||
},
|
||||
}, noValidateMulti);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ Meteor.publishRelations('board', function(boardId) {
|
|||
this.cursor(Attachments.find({ cardId }));
|
||||
this.cursor(Checklists.find({ cardId }));
|
||||
this.cursor(ChecklistItems.find({ cardId }));
|
||||
this.cursor(Cards.find({ parentId: cardId }));
|
||||
});
|
||||
|
||||
if (board.members) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue