Merge branch 'devel'

This commit is contained in:
Lauri Ojansivu 2018-03-20 22:58:59 +02:00
commit 578bdf9e67
13 changed files with 342 additions and 262 deletions

View file

@ -131,6 +131,7 @@
"AccountSettings": true,
"Announcements": true,
"Swimlanes": true,
"ChecklistItems": true,
"Npm": true
}
}

View file

@ -1,3 +1,11 @@
# Upcoming Wekan release
This release adds the following new features:
- [Checklist items sort fix, and checklist sort capability](https://github.com/wekan/wekan/pull/1543).
Thanks to GitHub user andresmanelli for contributions.
# v0.78 2018-03-17 Wekan release
This release adds the following new features:

View file

@ -1,4 +1,5 @@
const subManager = new SubsManager();
const { calculateIndexData } = Utils;
BlazeComponent.extendComponent({
mixins() {
@ -66,6 +67,51 @@ BlazeComponent.extendComponent({
onRendered() {
if (!Utils.isMiniScreen()) this.scrollParentContainer();
const $checklistsDom = this.$('.card-checklist-items');
$checklistsDom.sortable({
tolerance: 'pointer',
helper: 'clone',
handle: '.checklist-title',
items: '.js-checklist',
placeholder: 'checklist 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-checklist').get(0);
if (prevChecklist) {
prevChecklist = Blaze.getData(prevChecklist).checklist;
}
let nextChecklist = ui.item.next('.js-checklist').get(0);
if (nextChecklist) {
nextChecklist = Blaze.getData(nextChecklist).checklist;
}
const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1);
$checklistsDom.sortable('cancel');
const checklist = Blaze.getData(ui.item.get(0)).checklist;
Checklists.update(checklist._id, {
$set: {
sort: sortIndex.base,
},
});
},
});
function userIsMember() {
return Meteor.user() && Meteor.user().isBoardMember();
}
// Disable sorting if the current user is not a board member
this.autorun(() => {
if ($checklistsDom.data('sortable')) {
$checklistsDom.sortable('option', 'disabled', !userIsMember());
}
});
},
onDestroyed() {

View file

@ -18,24 +18,25 @@ template(name="checklists")
| {{_ 'add-checklist'}}...
template(name="checklistDetail")
+inlinedForm(classNames="js-edit-checklist-title" checklist = checklist)
+editChecklistItemForm(checklist = checklist)
else
.checklist-title
.checkbox.fa.fa-check-square-o
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
= checklist.title
else
h2.title
+viewer
.js-checklist.checklist
+inlinedForm(classNames="js-edit-checklist-title" checklist = checklist)
+editChecklistItemForm(checklist = checklist)
else
.checklist-title
.checkbox.fa.fa-check-square-o
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
= checklist.title
+checklistItems(checklist = checklist)
else
h2.title
+viewer
= checklist.title
+checklistItems(checklist = checklist)
template(name="checklistDeleteDialog")
.js-confirm-checklist-delete
@ -70,7 +71,7 @@ template(name="editChecklistItemForm")
template(name="checklistItems")
.checklist-items.js-checklist-items
each item in checklist.getItemsSorted
each item in checklist.items
+inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
+editChecklistItemForm(type = 'item' item = item checklist = checklist)
else
@ -84,7 +85,7 @@ template(name="checklistItems")
| {{_ 'add-checklist-item'}}...
template(name='itemDetail')
.item.js-checklist-item
.js-checklist-item.checklist-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}}")

View file

@ -1,11 +1,14 @@
const { calculateIndexData } = Utils;
function initSorting(items) {
items.sortable({
tolerance: 'pointer',
helper: 'clone',
items: '.js-checklist-item:not(.placeholder)',
axis: 'y',
connectWith: '.js-checklist-items',
appendTo: '.board-canvas',
distance: 7,
placeholder: 'placeholder',
placeholder: 'checklist-item placeholder',
scroll: false,
start(evt, ui) {
ui.placeholder.height(ui.helper.height());
@ -13,57 +16,54 @@ function initSorting(items) {
},
stop(evt, ui) {
const parent = ui.item.parents('.js-checklist-items');
const orderedItems = [];
parent.find('.js-checklist-item').each(function(i, item) {
const checklistItem = Blaze.getData(item).item;
orderedItems.push(checklistItem._id);
});
items.sortable('cancel');
const formerParent = ui.item.parents('.js-checklist-items');
const checklist = Blaze.getData(parent.get(0)).checklist;
const oldChecklist = Blaze.getData(formerParent.get(0)).checklist;
if (oldChecklist._id !== checklist._id) {
const currentItem = Blaze.getData(ui.item.get(0)).item;
for (let i = 0; i < orderedItems.length; i++) {
const itemId = orderedItems[i];
if (itemId !== currentItem._id) continue;
const newItem = {
_id: checklist.getNewItemId(),
title: currentItem.title,
sort: i,
isFinished: currentItem.isFinished,
};
checklist.addFullItem(newItem);
orderedItems[i] = currentItem._id;
oldChecklist.removeItem(itemId);
}
} else {
checklist.sortItems(orderedItems);
const checklistId = Blaze.getData(parent.get(0)).checklist._id;
let prevItem = ui.item.prev('.js-checklist-item').get(0);
if (prevItem) {
prevItem = Blaze.getData(prevItem).item;
}
let nextItem = ui.item.next('.js-checklist-item').get(0);
if (nextItem) {
nextItem = Blaze.getData(nextItem).item;
}
const nItems = 1;
const sortIndex = calculateIndexData(prevItem, nextItem, nItems);
const checklistDomElement = ui.item.get(0);
const checklistData = Blaze.getData(checklistDomElement);
const checklistItem = checklistData.item;
items.sortable('cancel');
checklistItem.move(checklistId, sortIndex.base);
},
});
}
Template.checklists.onRendered(function () {
const self = BlazeComponent.getComponentForElement(this.firstNode);
self.itemsDom = this.$('.card-checklist-items');
initSorting(self.itemsDom);
self.itemsDom.mousedown(function(evt) {
evt.stopPropagation();
});
BlazeComponent.extendComponent({
onRendered() {
const self = this;
self.itemsDom = this.$('.js-checklist-items');
initSorting(self.itemsDom);
self.itemsDom.mousedown(function(evt) {
evt.stopPropagation();
});
function userIsMember() {
return Meteor.user() && Meteor.user().isBoardMember();
}
// Disable sorting if the current user is not a board member
self.autorun(() => {
const $itemsDom = $(self.itemsDom);
if ($itemsDom.data('sortable')) {
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
function userIsMember() {
return Meteor.user() && Meteor.user().isBoardMember();
}
});
});
// Disable sorting if the current user is not a board member
self.autorun(() => {
const $itemsDom = $(self.itemsDom);
if ($itemsDom.data('sortable')) {
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
}
});
},
canModifyCard() {
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
},
}).register('checklistDetail');
BlazeComponent.extendComponent({
@ -95,7 +95,12 @@ BlazeComponent.extendComponent({
const checklist = this.currentData().checklist;
if (title) {
checklist.addItem(title);
ChecklistItems.insert({
title,
checklistId: checklist._id,
cardId: checklist.cardId,
sort: checklist.itemCount(),
});
}
// We keep the form opened, empty it.
textarea.value = '';
@ -118,7 +123,7 @@ BlazeComponent.extendComponent({
const checklist = this.currentData().checklist;
const item = this.currentData().item;
if (checklist && item && item._id) {
checklist.removeItem(item._id);
ChecklistItems.remove(item._id);
}
},
@ -135,9 +140,8 @@ BlazeComponent.extendComponent({
const textarea = this.find('textarea.js-edit-checklist-item');
const title = textarea.value.trim();
const itemId = this.currentData().item._id;
const checklist = this.currentData().checklist;
checklist.editItem(itemId, title);
const item = this.currentData().item;
item.setTitle(title);
},
onCreated() {
@ -211,12 +215,12 @@ BlazeComponent.extendComponent({
const checklist = this.currentData().checklist;
const item = this.currentData().item;
if (checklist && item && item._id) {
checklist.toggleItem(item._id);
item.toggleItem();
}
},
events() {
return [{
'click .item .check-box': this.toggleItem,
'click .js-checklist-item .check-box': this.toggleItem,
}];
},
}).register('itemDetail');

View file

@ -78,34 +78,60 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
bottom: -600px
right: 0
.checklist-items
.checklist
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
.checklist-item
margin: 0 0 0.5em 1.33em
line-height: 25px
font-size: 1.1em
margin-top: 3px
display: flex
background: darken(white, 3%)
.item
line-height: 25px
font-size: 1.1em
margin-top: 3px
display: flex
&:hover
background-color: darken(white, 8%)
&.placeholder
background: darken(white, 20%)
border-radius: 2px
.check-box
margin-top: 5px
&.is-checked
border-bottom: 2px solid #3cb500
border-right: 2px solid #3cb500
&.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
.item-title
flex: 1
padding-left: 10px;
&.is-checked
color: #8c8c8c
font-style: italic
&:hover
background-color: darken(white, 8%)
.js-delete-checklist-item
@extends .delete-text
padding: 12px 0 0 0
.check-box
margin-top: 5px
&.is-checked
border-bottom: 2px solid #3cb500
border-right: 2px solid #3cb500
.add-checklist-item
padding-top: 0.5em
display: inline-block
.item-title
flex: 1
padding-left: 10px;
&.is-checked
color: #8c8c8c
font-style: italic
.js-delete-checklist-item
margin: 0 0 0.5em 1.33em
@extends .delete-text
padding: 12px 0 0 0
.add-checklist-item
margin: 0 0 0.5em 1.33em
padding-top: 0.5em
display: inline-block

View file

@ -33,6 +33,37 @@ Utils = {
return $(window).width() <= 800;
},
calculateIndexData(prevData, nextData, nItems = 1) {
let base, increment;
// If we drop the card to an empty column
if (!prevData && !nextData) {
base = 0;
increment = 1;
// If we drop the card in the first position
} else if (!prevData) {
base = nextData.sort - 1;
increment = -1;
// If we drop the card in the last position
} else if (!nextData) {
base = prevData.sort + 1;
increment = 1;
}
// In the general case take the average of the previous and next element
// sort indexes.
else {
const prevSortIndex = prevData.sort;
const nextSortIndex = nextData.sort;
increment = (nextSortIndex - prevSortIndex) / (nItems + 1);
base = prevSortIndex + increment;
}
// XXX Return a generator that yield values instead of a base with a
// increment number.
return {
base,
increment,
};
},
// Determine the new sort index
calculateIndex(prevCardDomElement, nextCardDomElement, nCards = 1) {
let base, increment;

View file

@ -42,7 +42,7 @@ Activities.helpers({
return Checklists.findOne(this.checklistId);
},
checklistItem() {
return Checklists.findOne(this.checklistId).getItem(this.checklistItemId);
return ChecklistItems.findOne(this.checklistItemId);
},
});

View file

@ -155,7 +155,7 @@ Cards.helpers({
},
checklists() {
return Checklists.find({cardId: this._id}, {sort: {createdAt: 1}});
return Checklists.find({cardId: this._id}, {sort: { sort: 1 } });
},
checklistItemCount() {

95
models/checklistItems.js Normal file
View file

@ -0,0 +1,95 @@
ChecklistItems = new Mongo.Collection('checklistItems');
ChecklistItems.attachSchema(new SimpleSchema({
title: {
type: String,
},
sort: {
type: Number,
decimal: true,
},
isFinished: {
type: Boolean,
defaultValue: false,
},
checklistId: {
type: String,
},
cardId: {
type: String,
},
}));
ChecklistItems.allow({
insert(userId, doc) {
return allowIsBoardMemberByCard(userId, Cards.findOne(doc.cardId));
},
update(userId, doc) {
return allowIsBoardMemberByCard(userId, Cards.findOne(doc.cardId));
},
remove(userId, doc) {
return allowIsBoardMemberByCard(userId, Cards.findOne(doc.cardId));
},
fetch: ['userId', 'cardId'],
});
ChecklistItems.before.insert((userId, doc) => {
if (!doc.userId) {
doc.userId = userId;
}
});
// Mutations
ChecklistItems.mutations({
setTitle(title) {
return { $set: { title } };
},
toggleItem() {
return { $set: { isFinished: !this.isFinished } };
},
move(checklistId, sortIndex) {
const cardId = Checklists.findOne(checklistId).cardId;
const mutatedFields = {
cardId,
checklistId,
sort: sortIndex,
};
return {$set: mutatedFields};
},
});
// Activities helper
function itemCreation(userId, doc) {
const card = Cards.findOne(doc.cardId);
const boardId = card.boardId;
Activities.insert({
userId,
activityType: 'addChecklistItem',
cardId: doc.cardId,
boardId,
checklistId: doc.checklistId,
checklistItemId: doc._id,
});
}
function itemRemover(userId, doc) {
Activities.remove({
checklistItemId: doc._id,
});
}
// Activities
if (Meteor.isServer) {
Meteor.startup(() => {
ChecklistItems._collection._ensureIndex({ checklistId: 1 });
});
ChecklistItems.after.insert((userId, doc) => {
itemCreation(userId, doc);
});
ChecklistItems.after.remove((userId, doc) => {
itemRemover(userId, doc);
});
}

View file

@ -7,24 +7,6 @@ Checklists.attachSchema(new SimpleSchema({
title: {
type: String,
},
items: {
type: [Object],
defaultValue: [],
},
'items.$._id': {
type: String,
},
'items.$.title': {
type: String,
},
'items.$.sort': {
type: Number,
decimal: true,
},
'items.$.isFinished': {
type: Boolean,
defaultValue: false,
},
finishedAt: {
type: Date,
optional: true,
@ -46,40 +28,28 @@ Checklists.attachSchema(new SimpleSchema({
},
}));
const self = Checklists;
Checklists.helpers({
itemCount() {
return this.items.length;
return ChecklistItems.find({ checklistId: this._id }).count();
},
getItemsSorted() {
return _.sortBy(this.items, 'sort');
items() {
return ChecklistItems.find(Filter.mongoSelector({
checklistId: this._id,
}), { sort: ['sort'] });
},
finishedCount() {
return this.items.filter((item) => {
return item.isFinished;
}).length;
return ChecklistItems.find({
checklistId: this._id,
isFinished: true,
}).count();
},
isFinished() {
return 0 !== this.itemCount() && this.itemCount() === this.finishedCount();
},
getItem(_id) {
return _.findWhere(this.items, { _id });
},
itemIndex(itemId) {
const items = self.findOne({_id : this._id}).items;
return _.pluck(items, '_id').indexOf(itemId);
},
getNewItemId() {
const itemCount = this.itemCount();
let idx = 0;
if (itemCount > 0) {
const lastId = this.items[itemCount - 1]._id;
const lastIdSuffix = lastId.substr(this._id.length);
idx = parseInt(lastIdSuffix, 10) + 1;
}
return `${this._id}${idx}`;
},
});
Checklists.allow({
@ -103,108 +73,9 @@ Checklists.before.insert((userId, doc) => {
});
Checklists.mutations({
//for checklist itself
setTitle(title) {
return { $set: { title } };
},
//for items in checklist
addItem(title) {
const _id = this.getNewItemId();
return {
$addToSet: {
items: {
_id, title,
isFinished: false,
sort: this.itemCount(),
},
},
};
},
addFullItem(item) {
const itemsUpdate = {};
this.items.forEach(function(iterItem, index) {
if (iterItem.sort >= item.sort) {
itemsUpdate[`items.${index}.sort`] = iterItem.sort + 1;
}
});
if (!_.isEmpty(itemsUpdate)) {
self.direct.update({ _id: this._id }, { $set: itemsUpdate });
}
return { $addToSet: { items: item } };
},
removeItem(itemId) {
const item = this.getItem(itemId);
const itemsUpdate = {};
this.items.forEach(function(iterItem, index) {
if (iterItem.sort > item.sort) {
itemsUpdate[`items.${index}.sort`] = iterItem.sort - 1;
}
});
if (!_.isEmpty(itemsUpdate)) {
self.direct.update({ _id: this._id }, { $set: itemsUpdate });
}
return { $pull: { items: { _id: itemId } } };
},
editItem(itemId, title) {
if (this.getItem(itemId)) {
const itemIndex = this.itemIndex(itemId);
return {
$set: {
[`items.${itemIndex}.title`]: title,
},
};
}
return {};
},
finishItem(itemId) {
if (this.getItem(itemId)) {
const itemIndex = this.itemIndex(itemId);
return {
$set: {
[`items.${itemIndex}.isFinished`]: true,
},
};
}
return {};
},
resumeItem(itemId) {
if (this.getItem(itemId)) {
const itemIndex = this.itemIndex(itemId);
return {
$set: {
[`items.${itemIndex}.isFinished`]: false,
},
};
}
return {};
},
toggleItem(itemId) {
const item = this.getItem(itemId);
if (item) {
const itemIndex = this.itemIndex(itemId);
return {
$set: {
[`items.${itemIndex}.isFinished`]: !item.isFinished,
},
};
}
return {};
},
sortItems(itemIDs) {
const validItems = [];
itemIDs.forEach((itemID) => {
if (this.getItem(itemID)) {
validItems.push(this.itemIndex(itemID));
}
});
const modifiedValues = {};
for (let i = 0; i < validItems.length; i++) {
modifiedValues[`items.${validItems[i]}.sort`] = i;
}
return {
$set: modifiedValues,
};
},
});
if (Meteor.isServer) {
@ -222,30 +93,6 @@ if (Meteor.isServer) {
});
});
//TODO: so there will be no activity for adding item into checklist, maybe will be implemented in the future.
// The future is now
Checklists.after.update((userId, doc, fieldNames, modifier) => {
if (fieldNames.includes('items')) {
if (modifier.$addToSet) {
Activities.insert({
userId,
activityType: 'addChecklistItem',
cardId: doc.cardId,
boardId: Cards.findOne(doc.cardId).boardId,
checklistId: doc._id,
checklistItemId: modifier.$addToSet.items._id,
});
} else if (modifier.$pull) {
const activity = Activities.findOne({
checklistItemId: modifier.$pull.items._id,
});
if (activity) {
Activities.remove(activity._id);
}
}
}
});
Checklists.before.remove((userId, doc) => {
const activities = Activities.find({ checklistId: doc._id });
if (activities) {
@ -256,7 +103,6 @@ if (Meteor.isServer) {
});
}
//CARD COMMENT REST API
if (Meteor.isServer) {
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
try {

View file

@ -187,3 +187,24 @@ Migrations.add('add-views', () => {
}
});
});
Migrations.add('add-checklist-items', () => {
Checklists.find().forEach((checklist) => {
// Create new items
_.sortBy(checklist.items, 'sort').forEach((item, index) => {
ChecklistItems.direct.insert({
title: item.title,
sort: index,
isFinished: item.isFinished,
checklistId: checklist._id,
cardId: checklist.cardId,
});
});
// Delete old ones
Checklists.direct.update({ _id: checklist._id },
{ $unset: { items : 1 } },
noValidate
);
});
});

View file

@ -101,6 +101,7 @@ Meteor.publishRelations('board', function(boardId) {
this.cursor(CardComments.find({ cardId }));
this.cursor(Attachments.find({ cardId }));
this.cursor(Checklists.find({ cardId }));
this.cursor(ChecklistItems.find({ cardId }));
});
if (board.members) {