mirror of
https://github.com/wekan/wekan.git
synced 2025-04-23 13:37:09 -04:00
Add checklist feature
This commit is contained in:
parent
38c143b8bf
commit
59731af139
15 changed files with 462 additions and 2 deletions
|
@ -117,6 +117,8 @@
|
|||
"Notifications": true,
|
||||
"allowIsBoardAdmin": true,
|
||||
"allowIsBoardMember": true,
|
||||
"Emoji": true
|
||||
"allowIsBoardMemberByCard": true,
|
||||
"Emoji": true,
|
||||
"Checklists": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,12 @@ template(name="boardActivities")
|
|||
+viewer
|
||||
= comment.text
|
||||
|
||||
if($eq activityType 'addChecklist')
|
||||
| {{{_ 'activity-checklist-added' cardLink}}}.
|
||||
.activity-checklist(href="{{ card.absoluteUrl }}")
|
||||
+viewer
|
||||
= checklist.title
|
||||
|
||||
if($eq activityType 'archivedCard')
|
||||
| {{{_ 'activity-archived' cardLink}}}.
|
||||
|
||||
|
@ -103,6 +109,11 @@ template(name="cardActivities")
|
|||
| {{{_ 'activity-attached' attachmentLink cardLabel}}}.
|
||||
if attachment.isImage
|
||||
img.attachment-image-preview(src=attachment.url)
|
||||
if($eq activityType 'addChecklist')
|
||||
| {{{_ 'activity-checklist-added' cardLabel}}}.
|
||||
.activity-checklist
|
||||
+viewer
|
||||
= checklist.title
|
||||
|
||||
if($eq activityType 'addComment')
|
||||
+inlinedForm(classNames='js-edit-comment')
|
||||
|
|
|
@ -26,6 +26,14 @@
|
|||
margin-top: 5px
|
||||
padding: 5px
|
||||
|
||||
.activity-checklist
|
||||
display: block
|
||||
border-radius: 3px
|
||||
background: white
|
||||
text-decoration: none
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.2)
|
||||
margin-top: 5px
|
||||
padding: 5px
|
||||
.activity-meta
|
||||
font-size: 0.8em
|
||||
color: darken(white, 40%)
|
||||
|
|
|
@ -72,6 +72,10 @@ template(name="cardDetails")
|
|||
h3.card-details-item-title {{_ 'description'}}
|
||||
+viewer
|
||||
= description
|
||||
|
||||
hr
|
||||
+checklists(cardId = _id)
|
||||
|
||||
if attachments.count
|
||||
hr
|
||||
h2
|
||||
|
|
61
client/components/cards/checklists.jade
Normal file
61
client/components/cards/checklists.jade
Normal file
|
@ -0,0 +1,61 @@
|
|||
template(name="checklists")
|
||||
h2 {{_ 'checklists'}}
|
||||
.card-checklist-items
|
||||
each checklist in currentCard.checklists
|
||||
+checklistDetail(checklist = checklist)
|
||||
+inlinedForm(classNames="js-add-checklist" cardId = cardId)
|
||||
+addChecklistItemForm
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-checklist'}}...
|
||||
|
||||
template(name="checklistDetail")
|
||||
+inlinedForm(classNames="js-edit-checklist-title")
|
||||
+editChecklistItemForm(checklist = checklist)
|
||||
else
|
||||
.checklist-title
|
||||
.checkbox.fa.fa-check-square-o
|
||||
a.js-delete-checklist {{_ "delete"}}...
|
||||
span.checklist-stat(class="{{#if checklist.isFinished}}is-finished{{/if}}") {{checklist.finishedCount}}/{{checklist.itemCount}}
|
||||
h2.title.js-open-inlined-form.is-editable {{checklist.title}}
|
||||
+checklistItems(checklist = checklist)
|
||||
|
||||
template(name="addChecklistItemForm")
|
||||
textarea.js-add-checklist-item(rows='1' autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
|
||||
template(name="editChecklistItemForm")
|
||||
textarea.js-edit-checklist-item(rows='1' autofocus)
|
||||
if $eq type 'item'
|
||||
= item.title
|
||||
else
|
||||
= checklist.title
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
span(title=createdAt) {{ moment createdAt }}
|
||||
if currentUser.isBoardMember
|
||||
a.js-delete-checklist-item {{_ "delete"}}...
|
||||
|
||||
template(name="checklistItems")
|
||||
.checklist-items
|
||||
each item in checklist.items
|
||||
+inlinedForm(classNames="js-edit-checklist-item")
|
||||
+editChecklistItemForm(type = 'item' item = item checklist = checklist)
|
||||
else
|
||||
+itemDetail(item = item checklist = checklist)
|
||||
if currentUser.isBoardMember
|
||||
+inlinedForm(classNames="js-add-checklist-item" checklist = checklist)
|
||||
+addChecklistItemForm
|
||||
else
|
||||
a.add-checklist-item.js-open-inlined-form
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-checklist-item'}}...
|
||||
|
||||
template(name='itemDetail')
|
||||
.item
|
||||
.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}}") {{item.title}}
|
74
client/components/cards/checklists.js
Normal file
74
client/components/cards/checklists.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
BlazeComponent.extendComponent({
|
||||
addChecklist(event) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-add-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const cardId = this.currentData().cardId;
|
||||
Checklists.insert({
|
||||
cardId,
|
||||
title,
|
||||
});
|
||||
},
|
||||
|
||||
addChecklistItem(event) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-add-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const checklist = this.currentData().checklist;
|
||||
checklist.addItem(title);
|
||||
},
|
||||
|
||||
editChecklist(event) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-edit-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const checklist = this.currentData().checklist;
|
||||
checklist.setTitle(title);
|
||||
},
|
||||
|
||||
editChecklistItem(event) {
|
||||
event.preventDefault();
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
deleteItem() {
|
||||
const checklist = this.currentData().checklist;
|
||||
const item = this.currentData().item;
|
||||
if (checklist && item && item._id) {
|
||||
checklist.removeItem(item._id);
|
||||
}
|
||||
},
|
||||
|
||||
deleteChecklist() {
|
||||
const checklist = this.currentData().checklist;
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
},
|
||||
|
||||
pressKey(event) {
|
||||
//If user press enter key inside a form, submit it, so user doesn't have to leave keyboard to submit a form.
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
const $form = $(event.currentTarget).closest('form');
|
||||
$form.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'submit .js-add-checklist': this.addChecklist,
|
||||
'submit .js-edit-checklist-title': this.editChecklist,
|
||||
'submit .js-add-checklist-item': this.addChecklistItem,
|
||||
'submit .js-edit-checklist-item': this.editChecklistItem,
|
||||
'click .js-delete-checklist-item': this.deleteItem,
|
||||
'click .js-delete-checklist': this.deleteChecklist,
|
||||
keydown: this.pressKey,
|
||||
}];
|
||||
},
|
||||
}).register('checklists');
|
68
client/components/cards/checklists.styl
Normal file
68
client/components/cards/checklists.styl
Normal file
|
@ -0,0 +1,68 @@
|
|||
.js-add-checklist
|
||||
color: #8c8c8c
|
||||
|
||||
textarea.js-add-checklist-item, textarea.js-edit-checklist-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
|
||||
|
||||
.checklist-title
|
||||
.checkbox
|
||||
float: left
|
||||
width: 30px
|
||||
height 30px
|
||||
font-size: 18px
|
||||
line-height: 30px
|
||||
|
||||
.title
|
||||
font-size: 18px
|
||||
line-height: 30px
|
||||
|
||||
.checklist-stat
|
||||
margin: 0 0.5em
|
||||
float: right
|
||||
padding-top: 6px
|
||||
&.is-finished
|
||||
color: #3cb500
|
||||
|
||||
.js-delete-checklist
|
||||
@extends .delete-text
|
||||
|
||||
.checklist-items
|
||||
margin: 0 0 0.5em 1.33em
|
||||
|
||||
.item
|
||||
line-height: 25px
|
||||
font-size: 1.1em
|
||||
margin-top: 3px
|
||||
display: flex
|
||||
|
||||
.check-box
|
||||
margin-top: 5px
|
||||
&.is-checked
|
||||
border-bottom: 2px solid #3cb500
|
||||
border-right: 2px solid #3cb500
|
||||
|
||||
.item-title
|
||||
padding-left: 10px;
|
||||
&.is-checked
|
||||
color: #8c8c8c
|
||||
font-style: italic
|
||||
|
||||
.js-delete-checklist-item
|
||||
@extends .delete-text
|
||||
padding: 12px 0 0 0
|
||||
|
||||
.add-checklist-item
|
||||
padding-top: 0.5em
|
||||
display: inline-block
|
|
@ -14,7 +14,7 @@ template(name="minicard")
|
|||
.badges
|
||||
if comments.count
|
||||
.badge(title="{{_ 'card-comments-title' comments.count }}")
|
||||
span.badge-icon.fa.fa-comment-o
|
||||
span.badge-icon.fa.fa-comment-o.badge-comment
|
||||
span.badge-text= comments.count
|
||||
if description
|
||||
.badge.badge-state-image-only(title=description)
|
||||
|
@ -29,3 +29,8 @@ template(name="minicard")
|
|||
if dueAt
|
||||
.badge
|
||||
+minicardDueDate
|
||||
if checklists.count
|
||||
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
|
||||
span.badge-icon.fa.fa-check-square-o
|
||||
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
|
||||
|
||||
|
|
|
@ -99,10 +99,26 @@
|
|||
.badge-text
|
||||
vertical-align: middle
|
||||
|
||||
&.is-finished
|
||||
background: #3cb500
|
||||
padding: 0px 3px
|
||||
border-radius: 3px
|
||||
color: white
|
||||
|
||||
.badge-icon,
|
||||
.badge-text
|
||||
vertical-align: middle//didn't figure why use top, it'd be easier to fill bg if it's middle. This was introduced in commit "91cfcf7b12b5e7c137c2e765b2c378dde6b82966" & "* Improve the design of the minicards badges" was mentioned.
|
||||
&.badge-comment
|
||||
margin-bottom: 0.1rem
|
||||
|
||||
.badge-text
|
||||
font-size: 0.9em
|
||||
padding-left: 2px
|
||||
line-height: 14px
|
||||
.check-list-text
|
||||
padding-left: 0px
|
||||
line-height: 12px
|
||||
|
||||
|
||||
.minicard-members
|
||||
float: right
|
||||
|
|
|
@ -36,10 +36,13 @@
|
|||
"activity-removed": "removed %s from %s",
|
||||
"activity-sent": "sent %s to %s",
|
||||
"activity-unjoined": "unjoined %s",
|
||||
"activity-checklist-added": "added checklist to %s",
|
||||
"add": "Add",
|
||||
"add-attachment": "Add an attachment",
|
||||
"add-board": "Add a new board",
|
||||
"add-card": "Add a card",
|
||||
"add-checklist": "Add a checklist",
|
||||
"add-checklist-item": "Add an item to checklist",
|
||||
"add-cover": "Add Cover",
|
||||
"add-label": "Add the label",
|
||||
"add-list": "Add a list",
|
||||
|
@ -115,6 +118,7 @@
|
|||
"changePasswordPopup-title": "Change Password",
|
||||
"changePermissionsPopup-title": "Change Permissions",
|
||||
"changeSettingsPopup-title": "Change Settings",
|
||||
"checklists": "Checklists",
|
||||
"click-to-star": "Click to star this board.",
|
||||
"click-to-unstar": "Click to unstar this board.",
|
||||
"clipboard" : "Clipboard or drag & drop",
|
||||
|
|
|
@ -35,6 +35,9 @@ Activities.helpers({
|
|||
attachment() {
|
||||
return Attachments.findOne(this.attachmentId);
|
||||
},
|
||||
checklist() {
|
||||
return Checklists.findOne(this.checklistId);
|
||||
},
|
||||
});
|
||||
|
||||
Activities.before.insert((userId, doc) => {
|
||||
|
@ -102,6 +105,10 @@ if (Meteor.isServer) {
|
|||
const attachment = activity.attachment();
|
||||
params.attachment = attachment._id;
|
||||
}
|
||||
if (activity.checklistId) {
|
||||
const checklist = activity.checklist();
|
||||
params.checklist = checklist.title;
|
||||
}
|
||||
if (board) {
|
||||
const watchingUsers = _.pluck(_.where(board.watchers, {level: 'watching'}), 'userId');
|
||||
const trackingUsers = _.pluck(_.where(board.watchers, {level: 'tracking'}), 'userId');
|
||||
|
|
|
@ -141,6 +141,36 @@ Cards.helpers({
|
|||
return cover && cover.url() && cover;
|
||||
},
|
||||
|
||||
checklists() {
|
||||
return Checklists.find({ cardId: this._id }, { sort: { createdAt: 1 }});
|
||||
},
|
||||
|
||||
checklistItemCount() {
|
||||
const checklists = this.checklists().fetch();
|
||||
return checklists.map((checklist) => {
|
||||
return checklist.itemCount();
|
||||
}).reduce((prev, next) => {
|
||||
return prev + next;
|
||||
}, 0);
|
||||
},
|
||||
|
||||
checklistFinishedCount() {
|
||||
const checklists = this.checklists().fetch();
|
||||
return checklists.map((checklist) => {
|
||||
return checklist.finishedCount();
|
||||
}).reduce((prev, next) => {
|
||||
return prev + next;
|
||||
}, 0);
|
||||
},
|
||||
|
||||
checklistFinished() {
|
||||
return this.hasChecklist() && this.checklistItemCount() === this.checklistFinishedCount();
|
||||
},
|
||||
|
||||
hasChecklist() {
|
||||
return this.checklistItemCount() !== 0;
|
||||
},
|
||||
|
||||
absoluteUrl() {
|
||||
const board = this.board();
|
||||
return FlowRouter.url('card', {
|
||||
|
|
164
models/checklists.js
Normal file
164
models/checklists.js
Normal file
|
@ -0,0 +1,164 @@
|
|||
Checklists = new Mongo.Collection('checklists');
|
||||
|
||||
Checklists.attachSchema(new SimpleSchema({
|
||||
cardId: {
|
||||
type: String,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
items: {
|
||||
type: [Object],
|
||||
defaultValue: [],
|
||||
},
|
||||
'items.$._id': {
|
||||
type: String,
|
||||
},
|
||||
'items.$.title': {
|
||||
type: String,
|
||||
},
|
||||
'items.$.isFinished': {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
finishedAt: {
|
||||
type: Date,
|
||||
optional: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
denyUpdate: false,
|
||||
},
|
||||
}));
|
||||
|
||||
Checklists.helpers({
|
||||
itemCount () {
|
||||
return this.items.length;
|
||||
},
|
||||
finishedCount () {
|
||||
return this.items.filter((item) => {
|
||||
return item.isFinished;
|
||||
}).length;
|
||||
},
|
||||
isFinished () {
|
||||
return 0 !== this.itemCount() && this.itemCount() === this.finishedCount();
|
||||
},
|
||||
getItem (_id) {
|
||||
return _.findWhere(this.items, { _id });
|
||||
},
|
||||
itemIndex(itemId) {
|
||||
return _.pluck(this.items, '_id').indexOf(itemId);
|
||||
},
|
||||
});
|
||||
|
||||
Checklists.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'],
|
||||
});
|
||||
|
||||
Checklists.before.insert((userId, doc) => {
|
||||
doc.createdAt = new Date();
|
||||
if (!doc.userId) {
|
||||
doc.userId = userId;
|
||||
}
|
||||
});
|
||||
|
||||
Checklists.mutations({
|
||||
//for checklist itself
|
||||
setTitle(title){
|
||||
return { $set: { title }};
|
||||
},
|
||||
//for items in checklist
|
||||
addItem(title) {
|
||||
const itemCount = this.itemCount();
|
||||
const _id = `${this._id}${itemCount}`;
|
||||
return { $addToSet: {items: {_id, title, isFinished: false}} };
|
||||
},
|
||||
removeItem(itemId) {
|
||||
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 {};
|
||||
},
|
||||
});
|
||||
|
||||
if (Meteor.isServer) {
|
||||
Checklists.after.insert((userId, doc) => {
|
||||
Activities.insert({
|
||||
userId,
|
||||
activityType: 'addChecklist',
|
||||
cardId: doc.cardId,
|
||||
boardId: Cards.findOne(doc.cardId).boardId,
|
||||
checklistId: doc._id,
|
||||
});
|
||||
});
|
||||
|
||||
//TODO: so there will be no activity for adding item into checklist, maybe will be implemented in the future.
|
||||
// Checklists.after.update((userId, doc) => {
|
||||
// console.log('update:', doc)
|
||||
// Activities.insert({
|
||||
// userId,
|
||||
// activityType: 'addChecklist',
|
||||
// boardId: doc.boardId,
|
||||
// cardId: doc.cardId,
|
||||
// checklistId: doc._id,
|
||||
// });
|
||||
// });
|
||||
|
||||
Checklists.before.remove((userId, doc) => {
|
||||
const activity = Activities.findOne({ checklistId: doc._id });
|
||||
if (activity) {
|
||||
Activities.remove(activity._id);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -5,3 +5,8 @@ allowIsBoardAdmin = function(userId, board) {
|
|||
allowIsBoardMember = function(userId, board) {
|
||||
return board && board.hasMember(userId);
|
||||
};
|
||||
|
||||
allowIsBoardMemberByCard = function(userId, card) {
|
||||
const board = card.board();
|
||||
return board && board.hasMember(userId);
|
||||
};
|
||||
|
|
|
@ -98,6 +98,7 @@ Meteor.publishRelations('board', function(boardId) {
|
|||
this.cursor(Cards.find({ boardId }), function(cardId) {
|
||||
this.cursor(CardComments.find({ cardId }));
|
||||
this.cursor(Attachments.find({ cardId }));
|
||||
this.cursor(Checklists.find({ cardId }));
|
||||
});
|
||||
|
||||
if (board.members) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue