summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/components/activities/activities.jade59
-rw-r--r--client/components/activities/activities.js31
-rw-r--r--client/components/activities/comments.js10
-rw-r--r--client/components/boards/boardArchive.js10
-rw-r--r--client/components/boards/boardBody.js23
-rw-r--r--client/components/boards/boardHeader.jade10
-rw-r--r--client/components/boards/boardHeader.js36
-rw-r--r--client/components/boards/boardHeader.styl2
-rw-r--r--client/components/boards/boardsList.jade26
-rw-r--r--client/components/boards/boardsList.js18
-rw-r--r--client/components/boards/boardsList.styl9
-rw-r--r--client/components/cards/attachments.jade12
-rw-r--r--client/components/cards/attachments.js85
-rw-r--r--client/components/cards/attachments.styl11
-rw-r--r--client/components/cards/cardDetails.js54
-rw-r--r--client/components/cards/labels.jade2
-rw-r--r--client/components/cards/labels.js55
-rw-r--r--client/components/cards/minicard.jade2
-rw-r--r--client/components/forms/forms.styl9
-rw-r--r--client/components/import/import.jade54
-rw-r--r--client/components/import/import.js271
-rw-r--r--client/components/import/import.styl17
-rw-r--r--client/components/lists/list.js29
-rw-r--r--client/components/lists/listBody.jade15
-rw-r--r--client/components/lists/listBody.js109
-rw-r--r--client/components/lists/listHeader.jade1
-rw-r--r--client/components/lists/listHeader.js45
-rw-r--r--client/components/main/editor.js36
-rw-r--r--client/components/main/header.jade4
-rw-r--r--client/components/main/keyboardShortcuts.styl5
-rw-r--r--client/components/main/layouts.jade13
-rw-r--r--client/components/main/layouts.js33
-rw-r--r--client/components/main/layouts.styl9
-rw-r--r--client/components/main/popup.styl6
-rw-r--r--client/components/sidebar/sidebar.jade78
-rw-r--r--client/components/sidebar/sidebar.js167
-rw-r--r--client/components/sidebar/sidebarArchives.js18
-rw-r--r--client/components/sidebar/sidebarFilters.jade6
-rw-r--r--client/components/sidebar/sidebarFilters.js49
-rw-r--r--client/components/users/userAvatar.jade4
-rw-r--r--client/components/users/userAvatar.js27
-rw-r--r--client/components/users/userAvatar.styl4
-rw-r--r--client/components/users/userForm.styl10
-rw-r--r--client/components/users/userHeader.js6
-rw-r--r--client/config/accounts.js48
-rw-r--r--client/config/blazeHelpers.js4
-rw-r--r--client/config/router.js23
-rw-r--r--client/lib/accessibility.js41
-rw-r--r--client/lib/dropImage.js62
-rw-r--r--client/lib/filter.js4
-rw-r--r--client/lib/keyboard.js34
-rw-r--r--client/lib/modal.js4
-rw-r--r--client/lib/multiSelection.js9
-rw-r--r--client/lib/pasteImage.js57
-rw-r--r--client/lib/popup.js4
-rw-r--r--client/lib/textComplete.js54
-rw-r--r--client/lib/unsavedEdits.js2
-rw-r--r--client/lib/utils.js14
58 files changed, 1345 insertions, 495 deletions
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade
index 85b1276e..28a9f9c9 100644
--- a/client/components/activities/activities.jade
+++ b/client/components/activities/activities.jade
@@ -14,32 +14,41 @@ template(name="boardActivities")
p.activity-desc
+memberName(user=user)
- if($eq activityType 'createBoard')
- | {{_ 'activity-created' boardLabel}}.
+ if($eq activityType 'addAttachment')
+ | {{{_ 'activity-attached' attachmentLink cardLink}}}.
- if($eq activityType 'createList')
- | {{_ 'activity-added' list.title boardLabel}}.
+ if($eq activityType 'addBoardMember')
+ | {{{_ 'activity-added' memberLink boardLabel}}}.
+
+ if($eq activityType 'addComment')
+ | {{{_ 'activity-on' cardLink}}}
+ a.activity-comment(href="{{ card.absoluteUrl }}")
+ +viewer
+ = comment.text
+
+ if($eq activityType 'archivedCard')
+ | {{{_ 'activity-archived' cardLink}}}.
if($eq activityType 'archivedList')
| {{_ 'activity-archived' list.title}}.
+ if($eq activityType 'createBoard')
+ | {{_ 'activity-created' boardLabel}}.
+
if($eq activityType 'createCard')
| {{{_ 'activity-added' cardLink boardLabel}}}.
- if($eq activityType 'archivedCard')
- | {{{_ 'activity-archived' cardLink}}}.
-
- if($eq activityType 'restoredCard')
- | {{{_ 'activity-sent' cardLink boardLabel}}}.
+ if($eq activityType 'createList')
+ | {{_ 'activity-added' list.title boardLabel}}.
- if($eq activityType 'moveCard')
- | {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
+ if($eq activityType 'importBoard')
+ | {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
- if($eq activityType 'addBoardMember')
- | {{{_ 'activity-added' memberLink boardLabel}}}.
+ if($eq activityType 'importCard')
+ | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
- if($eq activityType 'removeBoardMember')
- | {{{_ 'activity-excluded' memberLink boardLabel}}}.
+ if($eq activityType 'importList')
+ | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
if($eq activityType 'joinMember')
if($eq currentUser._id member._id)
@@ -47,21 +56,21 @@ template(name="boardActivities")
else
| {{{_ 'activity-added' memberLink cardLink}}}.
+ if($eq activityType 'moveCard')
+ | {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
+
+ if($eq activityType 'removeBoardMember')
+ | {{{_ 'activity-excluded' memberLink boardLabel}}}.
+
+ if($eq activityType 'restoredCard')
+ | {{{_ 'activity-sent' cardLink boardLabel}}}.
+
if($eq activityType 'unjoinMember')
if($eq currentUser._id member._id)
| {{{_ 'activity-unjoined' cardLink}}}.
else
| {{{_ 'activity-removed' memberLink cardLink}}}.
- if($eq activityType 'addComment')
- | {{{_ 'activity-on' cardLink}}}
- a.activity-comment(href="{{ card.absoluteUrl }}")
- +viewer
- = comment.text
-
- if($eq activityType 'addAttachment')
- | {{{_ 'activity-attached' attachmentLink cardLink}}}.
-
span.activity-meta {{ moment createdAt }}
template(name="cardActivities")
@@ -72,6 +81,8 @@ template(name="cardActivities")
+memberName(user=user)
if($eq activityType 'createCard')
| {{_ 'activity-added' cardLabel list.title}}.
+ if($eq activityType 'importCard')
+ | {{{_ 'activity-imported' cardLabel list.title sourceLink}}}.
if($eq activityType 'joinMember')
if($eq currentUser._id member._id)
| {{_ 'activity-joined' cardLabel}}.
diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js
index 5c5d8370..c1465b04 100644
--- a/client/components/activities/activities.js
+++ b/client/components/activities/activities.js
@@ -9,7 +9,7 @@ BlazeComponent.extendComponent({
// XXX Should we use ReactiveNumber?
this.page = new ReactiveVar(1);
this.loadNextPageLocked = false;
- const sidebar = this.componentParent(); // XXX for some reason not working
+ const sidebar = this.parentComponent(); // XXX for some reason not working
sidebar.callFirstWith(null, 'resetNextPeak');
this.autorun(() => {
const mode = this.data().mode;
@@ -55,11 +55,29 @@ BlazeComponent.extendComponent({
cardLink() {
const card = this.currentData().card();
return card && Blaze.toHTML(HTML.A({
- href: card.absoluteUrl(),
+ href: FlowRouter.path(card.absoluteUrl()),
'class': 'action-card',
}, card.title));
},
+ listLabel() {
+ return this.currentData().list().title;
+ },
+
+ sourceLink() {
+ const source = this.currentData().source;
+ if(source) {
+ if(source.url) {
+ return Blaze.toHTML(HTML.A({
+ href: source.url,
+ }, source.system));
+ } else {
+ return source.system;
+ }
+ }
+ return null;
+ },
+
memberLink() {
return Blaze.toHTMLWithData(Template.memberName, {
user: this.currentData().member(),
@@ -68,8 +86,9 @@ BlazeComponent.extendComponent({
attachmentLink() {
const attachment = this.currentData().attachment();
- return attachment && Blaze.toHTML(HTML.A({
- href: attachment.url({ download: true }),
+ // trying to display url before file is stored generates js errors
+ return attachment && attachment.url({ download: true }) && Blaze.toHTML(HTML.A({
+ href: FlowRouter.path(attachment.url({ download: true })),
target: '_blank',
}, attachment.name()));
},
@@ -83,9 +102,9 @@ BlazeComponent.extendComponent({
},
'submit .js-edit-comment'(evt) {
evt.preventDefault();
- const commentText = this.currentComponent().getValue();
+ const commentText = this.currentComponent().getValue().trim();
const commentId = Template.parentData().commentId;
- if ($.trim(commentText)) {
+ if (commentText) {
CardComments.update(commentId, {
$set: {
text: commentText,
diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js
index 08401caa..18bf9ef0 100644
--- a/client/components/activities/comments.js
+++ b/client/components/activities/comments.js
@@ -24,11 +24,12 @@ BlazeComponent.extendComponent({
},
'submit .js-new-comment-form'(evt) {
const input = this.getInput();
- if ($.trim(input.val())) {
+ const text = input.val().trim();
+ if (text) {
CardComments.insert({
+ text,
boardId: this.currentData().boardId,
cardId: this.currentData()._id,
- text: input.val(),
});
resetCommentInput(input);
Tracker.flush();
@@ -72,8 +73,9 @@ EscapeActions.register('inlinedForm',
docId: Session.get('currentCard'),
};
const commentInput = $('.js-new-comment-input');
- if ($.trim(commentInput.val())) {
- UnsavedEdits.set(draftKey, commentInput.val());
+ const draft = commentInput.val().trim();
+ if (draft) {
+ UnsavedEdits.set(draftKey, draft);
} else {
UnsavedEdits.reset(draftKey);
}
diff --git a/client/components/boards/boardArchive.js b/client/components/boards/boardArchive.js
index 9d7ca7f2..35f795f3 100644
--- a/client/components/boards/boardArchive.js
+++ b/client/components/boards/boardArchive.js
@@ -22,13 +22,9 @@ BlazeComponent.extendComponent({
events() {
return [{
'click .js-restore-board'() {
- const boardId = this.currentData()._id;
- Boards.update(boardId, {
- $set: {
- archived: false,
- },
- });
- Utils.goBoardId(boardId);
+ const board = this.currentData();
+ board.restore();
+ Utils.goBoardId(board._id);
},
}];
},
diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js
index 95590beb..a601bc2e 100644
--- a/client/components/boards/boardBody.js
+++ b/client/components/boards/boardBody.js
@@ -34,7 +34,7 @@ BlazeComponent.extendComponent({
},
openNewListForm() {
- this.componentChildren('addListForm')[0].open();
+ this.childComponents('addListForm')[0].open();
},
// XXX Flow components allow us to avoid creating these two setter methods by
@@ -45,7 +45,8 @@ BlazeComponent.extendComponent({
},
scrollLeft(position = 0) {
- this.$('.js-lists').animate({
+ const lists = this.$('.js-lists');
+ lists && lists.animate({
scrollLeft: position,
});
},
@@ -133,7 +134,7 @@ Template.boardBody.onRendered(function() {
if (!Meteor.user() || !Meteor.user().isBoardMember())
return;
- self.$(self.listsDom).sortable({
+ $(self.listsDom).sortable({
tolerance: 'pointer',
helper: 'clone',
handle: '.js-list-header',
@@ -145,7 +146,7 @@ Template.boardBody.onRendered(function() {
Popup.close();
},
stop() {
- self.$('.js-lists').find('.js-list:not(.js-list-composer)').each(
+ $(self.listsDom).find('.js-list:not(.js-list-composer)').each(
(i, list) => {
const data = Blaze.getData(list);
Lists.update(data._id, {
@@ -160,7 +161,7 @@ Template.boardBody.onRendered(function() {
// Disable drag-dropping while in multi-selection mode
self.autorun(() => {
- self.$(self.listsDom).sortable('option', 'disabled',
+ $(self.listsDom).sortable('option', 'disabled',
MultiSelection.isActive());
});
@@ -179,22 +180,24 @@ BlazeComponent.extendComponent({
// Proxy
open() {
- this.componentChildren('inlinedForm')[0].open();
+ this.childComponents('inlinedForm')[0].open();
},
events() {
return [{
submit(evt) {
evt.preventDefault();
- const title = this.find('.list-name-input');
- if ($.trim(title.value)) {
+ const titleInput = this.find('.list-name-input');
+ const title = titleInput.value.trim();
+ if (title) {
Lists.insert({
- title: title.value,
+ title,
boardId: Session.get('currentBoard'),
sort: $('.list').length,
});
- title.value = '';
+ titleInput.value = '';
+ titleInput.focus();
}
},
}];
diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade
index 94225730..a0160382 100644
--- a/client/components/boards/boardHeader.jade
+++ b/client/components/boards/boardHeader.jade
@@ -32,7 +32,7 @@ template(name="headerBoard")
title="{{#if MultiSelection.isActive}}{{_ 'filter-on-desc'}}{{/if}}"
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
i.fa.fa-check-square-o
- span Multi-Selection {{#if MultiSelection.isActive}}is on{{/if}}
+ span {{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
@@ -105,8 +105,11 @@ template(name="createBoardPopup")
span.fa.fa-lock.colorful
= " "
| {{{_ 'board-private-info'}}}
- a.js-change-visibility Change.
+ a.js-change-visibility {{_ 'change'}}.
input.primary.wide(type="submit" value="{{_ 'create'}}")
+ span.quiet
+ | {{_ 'or'}}
+ a.js-import {{_ 'import-board'}}
template(name="boardChangeTitlePopup")
@@ -114,6 +117,9 @@ template(name="boardChangeTitlePopup")
label
| {{_ 'title'}}
input.js-board-name(type="text" value=title autofocus)
+ label
+ | {{_ 'description'}}
+ textarea.js-board-desc= description
input.primary.wide(type="submit" value="{{_ 'rename'}}")
template(name="archiveBoardPopup")
diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js
index f259b2a6..3dc6d754 100644
--- a/client/components/boards/boardHeader.js
+++ b/client/components/boards/boardHeader.js
@@ -6,9 +6,9 @@ Template.boardMenuPopup.events({
},
'click .js-change-board-color': Popup.open('boardChangeColor'),
'click .js-change-language': Popup.open('changeLanguage'),
- 'click .js-archive-board ': Popup.afterConfirm('archiveBoard', () => {
- const boardId = Session.get('currentBoard');
- Boards.update(boardId, { $set: { archived: true }});
+ 'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ currentBoard.archive();
// XXX We should have some kind of notification on top of the page to
// confirm that the board was successfully archived.
FlowRouter.go('home');
@@ -17,13 +17,11 @@ Template.boardMenuPopup.events({
Template.boardChangeTitlePopup.events({
submit(evt, tpl) {
- const title = tpl.$('.js-board-name').val().trim();
- if (title) {
- Boards.update(this._id, {
- $set: {
- title,
- },
- });
+ const newTitle = tpl.$('.js-board-name').val().trim();
+ const newDesc = tpl.$('.js-board-desc').val().trim();
+ if (newTitle) {
+ this.rename(newTitle);
+ this.setDesciption(newDesc);
Popup.close();
}
evt.preventDefault();
@@ -95,12 +93,9 @@ BlazeComponent.extendComponent({
events() {
return [{
'click .js-select-background'(evt) {
- const currentBoardId = Session.get('currentBoard');
- Boards.update(currentBoardId, {
- $set: {
- color: this.currentData().toString(),
- },
- });
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ const newColor = this.currentData().toString();
+ currentBoard.setColor(newColor);
evt.preventDefault();
},
}];
@@ -152,6 +147,7 @@ BlazeComponent.extendComponent({
this.setVisibility(this.currentData());
},
'click .js-change-visibility': this.toggleVisibilityMenu,
+ 'click .js-import': Popup.open('boardImportBoard'),
submit: this.onSubmit,
}];
},
@@ -168,11 +164,9 @@ BlazeComponent.extendComponent({
},
selectBoardVisibility() {
- Boards.update(Session.get('currentBoard'), {
- $set: {
- permission: this.currentData(),
- },
- });
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ const visibility = this.currentData();
+ currentBoard.setVisibility(visibility);
Popup.close();
},
diff --git a/client/components/boards/boardHeader.styl b/client/components/boards/boardHeader.styl
new file mode 100644
index 00000000..adfe4b19
--- /dev/null
+++ b/client/components/boards/boardHeader.styl
@@ -0,0 +1,2 @@
+a.js-import
+ text-decoration underline
diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade
index 11333eee..7099cdc9 100644
--- a/client/components/boards/boardsList.jade
+++ b/client/components/boards/boardsList.jade
@@ -3,11 +3,23 @@ template(name="boardList")
ul.board-list.clearfix
each boards
li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
- a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
- span.details
- span.board-list-item-name= title
- i.fa.js-star-board(
- class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
- title="{{_ 'star-board-title'}}")
+ if isInvited
+ .board-list-item
+ span.details
+ span.board-list-item-name= title
+ i.fa.js-star-board(
+ class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
+ title="{{_ 'star-board-title'}}")
+ p.board-list-item-desc {{_ 'just-invited'}}
+ button.js-accept-invite.primary {{_ 'accept'}}
+ button.js-decline-invite {{_ 'decline'}}
+ else
+ a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
+ span.details
+ span.board-list-item-name= title
+ i.fa.js-star-board(
+ class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
+ title="{{_ 'star-board-title'}}")
+ p.board-list-item-desc= description
li.js-add-board
- a.label {{_ 'add-board'}}
+ a.board-list-item.label {{_ 'add-board'}}
diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js
index 1a2d3c9a..131adf9d 100644
--- a/client/components/boards/boardsList.js
+++ b/client/components/boards/boardsList.js
@@ -17,6 +17,11 @@ BlazeComponent.extendComponent({
return user && user.hasStarred(this.currentData()._id);
},
+ isInvited() {
+ const user = Meteor.user();
+ return user && user.isInvitedTo(this.currentData()._id);
+ },
+
events() {
return [{
'click .js-add-board': Popup.open('createBoard'),
@@ -25,6 +30,19 @@ BlazeComponent.extendComponent({
Meteor.user().toggleBoardStar(boardId);
evt.preventDefault();
},
+ 'click .js-accept-invite'() {
+ const boardId = this.currentData()._id;
+ Meteor.user().removeInvite(boardId);
+ },
+ 'click .js-decline-invite'() {
+ const boardId = this.currentData()._id;
+ Meteor.call('quitBoard', boardId, (err, ret) => {
+ if (!err && ret) {
+ Meteor.user().removeInvite(boardId);
+ FlowRouter.go('home');
+ }
+ });
+ },
}];
},
}).register('boardList');
diff --git a/client/components/boards/boardsList.styl b/client/components/boards/boardsList.styl
index 9978fab8..e24940a0 100644
--- a/client/components/boards/boardsList.styl
+++ b/client/components/boards/boardsList.styl
@@ -14,7 +14,7 @@ $spaceBetweenTiles = 16px
.fa-star-o
opacity: 1
- a
+ .board-list-item
background-color: #999
color: #f6f6f6
height: 90px
@@ -40,6 +40,13 @@ $spaceBetweenTiles = 16px
font-weight: 400
line-height: 22px
+ .board-list-item-desc
+ color: rgba(255, 255, 255, .5)
+ display: block
+ font-size: 10px
+ font-weight: 400
+ line-height: 18px
+
.js-add-board
text-align:center
diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade
index 59eaf077..2cb3bb85 100644
--- a/client/components/cards/attachments.jade
+++ b/client/components/cards/attachments.jade
@@ -3,6 +3,16 @@ template(name="cardAttachmentsPopup")
li
input.js-attach-file.hide(type="file" name="file" multiple)
a.js-computer-upload {{_ 'computer'}}
+ li
+ a.js-upload-clipboard-image {{_ 'clipboard'}}
+
+template(name="previewClipboardImagePopup")
+ p <kbd>Ctrl</kbd>+<kbd>V</kbd> {{_ "paste-or-dragdrop"}}
+ img.preview-clipboard-image()
+ button.primary.js-upload-pasted-image {{_ 'upload'}}
+
+template(name="previewAttachedImagePopup")
+ img.preview-large-image.js-large-image-clicked(src="{{pathFor url}}")
template(name="attachmentDeletePopup")
p {{_ "attachment-delete-pop"}}
@@ -15,7 +25,7 @@ template(name="attachmentsGalery")
.attachment-thumbnail
if isUploaded
if isImage
- img.attachment-thumbnail-img(src=url)
+ img.attachment-thumbnail-img.js-preview-image(src="{{pathFor url}}")
else
span.attachment-thumbnail-ext= extension
else
diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js
index ba56aa1a..1e5aa03b 100644
--- a/client/components/cards/attachments.js
+++ b/client/components/cards/attachments.js
@@ -1,7 +1,7 @@
Template.attachmentsGalery.events({
'click .js-add-attachment': Popup.open('cardAttachments'),
'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete',
- () => {
+ function() {
Attachments.remove(this._id);
Popup.close();
}
@@ -15,10 +15,43 @@ Template.attachmentsGalery.events({
// XXX Not implemented!
},
'click .js-add-cover'() {
- Cards.update(this.cardId, { $set: { coverId: this._id } });
+ Cards.findOne(this.cardId).setCover(this._id);
},
'click .js-remove-cover'() {
- Cards.update(this.cardId, { $unset: { coverId: '' } });
+ Cards.findOne(this.cardId).unsetCover();
+ },
+ 'click .js-preview-image'(evt) {
+ Popup.open('previewAttachedImage').call(this, evt);
+ // when multiple thumbnails, if click one then another very fast,
+ // we might get a wrong width from previous img.
+ // when popup reused, onRendered() won't be called, so we cannot get there.
+ // here make sure to get correct size when this img fully loaded.
+ const img = $('img.preview-large-image')[0];
+ if (!img) return;
+ const rePosPopup = () => {
+ const w = img.width;
+ const h = img.height;
+ // if the image is too large, we resize & center the popup.
+ if (w > 300) {
+ $('div.pop-over').css({
+ width: (w + 20),
+ position: 'absolute',
+ left: (window.innerWidth - w)/2,
+ top: (window.innerHeight - h)/2,
+ });
+ }
+ };
+ const url = $(evt.currentTarget).attr('src');
+ if (img.src === url && img.complete)
+ rePosPopup();
+ else
+ img.onload = rePosPopup;
+ },
+});
+
+Template.previewAttachedImagePopup.events({
+ 'click .js-large-image-clicked'(){
+ Popup.close();
},
});
@@ -28,7 +61,7 @@ Template.cardAttachmentsPopup.events({
FS.Utility.eachFile(evt, (f) => {
const file = new FS.File(f);
file.boardId = card.boardId;
- file.cardId = card._id;
+ file.cardId = card._id;
Attachments.insert(file);
Popup.close();
@@ -38,4 +71,48 @@ Template.cardAttachmentsPopup.events({
tpl.find('.js-attach-file').click();
evt.preventDefault();
},
+ 'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'),
+});
+
+let pastedResults = null;
+
+Template.previewClipboardImagePopup.onRendered(() => {
+ // we can paste image from clipboard
+ $(document.body).pasteImageReader((results) => {
+ if (results.dataURL.startsWith('data:image/')) {
+ $('img.preview-clipboard-image').attr('src', results.dataURL);
+ pastedResults = results;
+ }
+ });
+
+ // we can also drag & drop image file to it
+ $(document.body).dropImageReader((results) => {
+ if (results.dataURL.startsWith('data:image/')) {
+ $('img.preview-clipboard-image').attr('src', results.dataURL);
+ pastedResults = results;
+ }
+ });
+});
+
+Template.previewClipboardImagePopup.events({
+ 'click .js-upload-pasted-image'() {
+ const results = pastedResults;
+ if (results && results.file) {
+ const card = this;
+ const file = new FS.File(results.file);
+ if (!results.name) {
+ // if no filename, it's from clipboard. then we give it a name, with ext name from MIME type
+ if (typeof results.file.type === 'string') {
+ file.name(results.file.type.replace('image/', 'clipboard.'));
+ }
+ }
+ file.updatedAt(new Date());
+ file.boardId = card.boardId;
+ file.cardId = card._id;
+ Attachments.insert(file);
+ pastedResults = null;
+ $(document.body).pasteImageReader(() => {});
+ Popup.close();
+ }
+ },
});
diff --git a/client/components/cards/attachments.styl b/client/components/cards/attachments.styl
index 5cdf7386..a582f3af 100644
--- a/client/components/cards/attachments.styl
+++ b/client/components/cards/attachments.styl
@@ -45,3 +45,14 @@
display: block
box-shadow: 0 1px 2px rgba(0,0,0,.2)
+.preview-large-image
+ max-width: 1000px
+ display: block
+ box-shadow: 0 1px 2px rgba(0,0,0,.2)
+
+.preview-clipboard-image
+ width: 280px
+ height: 200px
+ display: block
+ border: 1px solid black
+ box-shadow: 0 1px 2px rgba(0,0,0,.2)
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index 09c99f4e..b4fdca52 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -13,19 +13,19 @@ BlazeComponent.extendComponent({
},
reachNextPeak() {
- const activitiesComponent = this.componentChildren('activities')[0];
+ const activitiesComponent = this.childComponents('activities')[0];
activitiesComponent.loadNextPage();
},
onCreated() {
this.isLoaded = new ReactiveVar(false);
- this.componentParent().showOverlay.set(true);
- this.componentParent().mouseHasEnterCardDetails = false;
+ this.parentComponent().showOverlay.set(true);
+ this.parentComponent().mouseHasEnterCardDetails = false;
},
scrollParentContainer() {
const cardPanelWidth = 510;
- const bodyBoardComponent = this.componentParent();
+ const bodyBoardComponent = this.parentComponent();
const $cardContainer = bodyBoardComponent.$('.js-lists');
const $cardView = this.$(this.firstNode());
@@ -52,13 +52,7 @@ BlazeComponent.extendComponent({
},
onDestroyed() {
- this.componentParent().showOverlay.set(false);
- },
-
- updateCard(modifier) {
- Cards.update(this.data()._id, {
- $set: modifier,
- });
+ this.parentComponent().showOverlay.set(false);
},
events() {
@@ -68,7 +62,8 @@ BlazeComponent.extendComponent({
},
};
- return [_.extend(events, {
+ return [{
+ ...events,
'click .js-close-card-details'() {
Utils.goBoardId(this.data().boardId);
},
@@ -76,23 +71,23 @@ BlazeComponent.extendComponent({
'submit .js-card-description'(evt) {
evt.preventDefault();
const description = this.currentComponent().getValue();
- this.updateCard({ description });
+ this.data().setDescription(description);
},
'submit .js-card-details-title'(evt) {
evt.preventDefault();
- const title = this.currentComponent().getValue();
- if ($.trim(title)) {
- this.updateCard({ title });
+ const title = this.currentComponent().getValue().trim();
+ if (title) {
+ this.data().setTitle(title);
}
},
'click .js-member': Popup.open('cardMember'),
'click .js-add-members': Popup.open('cardMembers'),
'click .js-add-labels': Popup.open('cardLabels'),
'mouseenter .js-card-details'() {
- this.componentParent().showOverlay.set(true);
- this.componentParent().mouseHasEnterCardDetails = true;
+ this.parentComponent().showOverlay.set(true);
+ this.parentComponent().mouseHasEnterCardDetails = true;
},
- })];
+ }];
},
}).register('cardDetails');
@@ -111,7 +106,7 @@ BlazeComponent.extendComponent({
close(isReset = false) {
if (this.isOpen.get() && !isReset) {
- const draft = $.trim(this.getValue());
+ const draft = this.getValue().trim();
if (draft !== Cards.findOne(Session.get('currentCard')).description) {
UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue());
}
@@ -138,14 +133,9 @@ Template.cardDetailsActionsPopup.events({
'click .js-labels': Popup.open('cardLabels'),
'click .js-attachments': Popup.open('cardAttachments'),
'click .js-move-card': Popup.open('moveCard'),
- // 'click .js-copy': Popup.open(),
'click .js-archive'(evt) {
evt.preventDefault();
- Cards.update(this._id, {
- $set: {
- archived: true,
- },
- });
+ this.archive();
Popup.close();
},
'click .js-more': Popup.open('cardMore'),
@@ -155,22 +145,18 @@ Template.moveCardPopup.events({
'click .js-select-list'() {
// XXX We should *not* get the currentCard from the global state, but
// instead from a “component” state.
- const cardId = Session.get('currentCard');
+ const card = Cards.findOne(Session.get('currentCard'));
const newListId = this._id;
- Cards.update(cardId, {
- $set: {
- listId: newListId,
- },
- });
+ card.move(newListId);
Popup.close();
},
});
Template.cardMorePopup.events({
- 'click .js-delete': Popup.afterConfirm('cardDelete', () => {
+ 'click .js-delete': Popup.afterConfirm('cardDelete', function() {
Popup.close();
Cards.remove(this._id);
- Utils.goBoardId(this.board()._id);
+ Utils.goBoardId(this.boardId);
}),
});
diff --git a/client/components/cards/labels.jade b/client/components/cards/labels.jade
index a868627c..31bd4d06 100644
--- a/client/components/cards/labels.jade
+++ b/client/components/cards/labels.jade
@@ -18,7 +18,7 @@ template(name="editLabelPopup")
form.edit-label
+formLabel
button.primary.wide.left(type="submit") {{_ 'save'}}
- span.right
+ button.js-delete-label.negate.wide.right {{_ 'delete'}}
template(name="deleteLabelPopup")
p {{_ "label-delete-pop"}}
diff --git a/client/components/cards/labels.js b/client/components/cards/labels.js
index 2da3b80b..4e61a0c6 100644
--- a/client/components/cards/labels.js
+++ b/client/components/cards/labels.js
@@ -13,7 +13,7 @@ BlazeComponent.extendComponent({
},
labels() {
- return _.map(labelColors, (color) => {
+ return labelColors.map((color) => {
return { color, name: '' };
});
},
@@ -45,19 +45,9 @@ Template.createLabelPopup.helpers({
Template.cardLabelsPopup.events({
'click .js-select-label'(evt) {
- const cardId = Template.parentData(2).data._id;
+ const card = Cards.findOne(Session.get('currentCard'));
const labelId = this._id;
- let operation;
- if (Cards.find({ _id: cardId, labelIds: labelId}).count() === 0)
- operation = '$addToSet';
- else
- operation = '$pull';
-
- Cards.update(cardId, {
- [operation]: {
- labelIds: labelId,
- },
- });
+ card.toggleLabel(labelId);
evt.preventDefault();
},
'click .js-edit-label': Popup.open('editLabel'),
@@ -79,52 +69,27 @@ Template.formLabel.events({
Template.createLabelPopup.events({
// Create the new label
'submit .create-label'(evt, tpl) {
+ evt.preventDefault();
+ const board = Boards.findOne(Session.get('currentBoard'));
const name = tpl.$('#labelName').val().trim();
- const boardId = Session.get('currentBoard');
const color = Blaze.getData(tpl.find('.fa-check')).color;
-
- Boards.update(boardId, {
- $push: {
- labels: {
- name,
- color,
- _id: Random.id(6),
- },
- },
- });
-
+ board.addLabel(name, color);
Popup.back();
- evt.preventDefault();
},
});
Template.editLabelPopup.events({
'click .js-delete-label': Popup.afterConfirm('deleteLabel', function() {
- const boardId = Session.get('currentBoard');
- Boards.update(boardId, {
- $pull: {
- labels: {
- _id: this._id,
- },
- },
- });
-
+ const board = Boards.findOne(Session.get('currentBoard'));
+ board.removeLabel(this._id);
Popup.back(2);
}),
'submit .edit-label'(evt, tpl) {
evt.preventDefault();
+ const board = Boards.findOne(Session.get('currentBoard'));
const name = tpl.$('#labelName').val().trim();
- const boardId = Session.get('currentBoard');
- const getLabel = Utils.getLabelIndex(boardId, this._id);
const color = Blaze.getData(tpl.find('.fa-check')).color;
-
- Boards.update(boardId, {
- $set: {
- [getLabel.key('name')]: name,
- [getLabel.key('color')]: color,
- },
- });
-
+ board.editLabel(this._id, name, color);
Popup.back();
},
});
diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade
index 660b0fa5..573b3da1 100644
--- a/client/components/cards/minicard.jade
+++ b/client/components/cards/minicard.jade
@@ -2,7 +2,7 @@ template(name="minicard")
.minicard
if cover
.minicard-cover
- img(src=cover.url)
+ img(src="{{pathFor cover.url}}")
if labels
.minicard-labels
each labels
diff --git a/client/components/forms/forms.styl b/client/components/forms/forms.styl
index 83d25370..9ae95140 100644
--- a/client/components/forms/forms.styl
+++ b/client/components/forms/forms.styl
@@ -617,8 +617,15 @@ button
margin-right: 5px
vertical-align: middle
+ .minicard-label
+ width: 11px
+ height: @width
+ border-radius: 2px
+ margin: 2px 7px -2px -2px
+ display: inline-block
+
&.active
background: #005377
- a
+ a, .quiet
color: white
diff --git a/client/components/import/import.jade b/client/components/import/import.jade
new file mode 100644
index 00000000..74b6ca13
--- /dev/null
+++ b/client/components/import/import.jade
@@ -0,0 +1,54 @@
+template(name="importPopup")
+ if error.get
+ .warning {{_ error.get}}
+ form
+ p: label(for='import-textarea') {{_ getLabel}}
+ textarea#import-textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
+ | {{jsonText}}
+ if membersMapping
+ div
+ a.show-mapping
+ | {{_ 'import-show-user-mapping'}}
+ input.primary.wide(type="submit" value="{{_ 'import'}}")
+
+template(name="mapMembersPopup")
+ .map-members
+ p {{_ 'import-members-map'}}
+ .mapping-list
+ each members
+ .mapping
+ a.source
+ div.full-name
+ = fullName
+ div.username
+ | ({{username}})
+ .wekan
+ if wekan
+ +userAvatar(userId=wekan._id)
+ else
+ a.member.add-member.js-add-members
+ i.fa.fa-plus
+ form
+ input.primary.wide(type="submit" value="{{_ 'done'}}")
+
+ template(name="addMemberPopup")
+
+template(name="mapMembersAddPopup")
+ .select-member
+ p
+ | {{_ 'import-user-select'}}
+ .js-map-member
+ +esInput(index="users")
+ ul.pop-over-list
+ +esEach(index="users")
+ li.item.js-member-item
+ a.name.js-select-import(title="{{profile.name}} ({{username}})" data-id="{{_id}}")
+ +userAvatar(userId=_id esSearch=true)
+ span.full-name
+ = profile.name
+ | (<span class="username">{{username}}</span>)
+ +ifEsIsSearching(index='users')
+ +spinner
+ +ifEsHasNoResults(index="users")
+ .manage-member-section
+ p.quiet {{_ 'no-results'}}
diff --git a/client/components/import/import.js b/client/components/import/import.js
new file mode 100644
index 00000000..63285e57
--- /dev/null
+++ b/client/components/import/import.js
@@ -0,0 +1,271 @@
+/// Abstract root for all import popup screens.
+/// Descendants must define:
+/// - getMethodName(): return the Meteor method to call for import, passing json
+/// data decoded as object and additional data (see below);
+/// - getAdditionalData(): return object containing additional data passed to
+/// Meteor method (like list ID and position for a card import);
+/// - getLabel(): i18n key for the text displayed in the popup, usually to
+/// explain how to get the data out of the source system.
+const ImportPopup = BlazeComponent.extendComponent({
+ template() {
+ return 'importPopup';
+ },
+
+ jsonText() {
+ return Session.get('import.text');
+ },
+
+ membersMapping() {
+ return Session.get('import.membersToMap');
+ },
+
+ onCreated() {
+ this.error = new ReactiveVar('');
+ this.dataToImport = '';
+ },
+
+ onFinish() {
+ Popup.close();
+ },
+
+ onShowMapping(evt) {
+ this._storeText(evt);
+ Popup.open('mapMembers')(evt);
+ },
+
+ onSubmit(evt){
+ evt.preventDefault();
+ const dataJson = this._storeText(evt);
+ let dataObject;
+ try {
+ dataObject = JSON.parse(dataJson);
+ this.setError('');
+ } catch (e) {
+ this.setError('error-json-malformed');
+ return;
+ }
+ if(this._hasAllNeededData(dataObject)) {
+ this._import(dataObject);
+ } else {
+ this._prepareAdditionalData(dataObject);
+ Popup.open(this._screenAdditionalData())(evt);
+
+ }
+ },
+
+ events() {
+ return [{
+ submit: this.onSubmit,
+ 'click .show-mapping': this.onShowMapping,
+ }];
+ },
+
+ setError(error) {
+ this.error.set(error);
+ },
+
+ _import(dataObject) {
+ const additionalData = this.getAdditionalData();
+ const membersMapping = this.membersMapping();
+ if (membersMapping) {
+ const mappingById = {};
+ membersMapping.forEach((member) => {
+ if (member.wekan) {
+ mappingById[member.id] = member.wekan._id;
+ }
+ });
+ additionalData.membersMapping = mappingById;
+ }
+ Session.set('import.membersToMap', null);
+ Session.set('import.text', null);
+ Meteor.call(this.getMethodName(), dataObject, additionalData,
+ (error, response) => {
+ if (error) {
+ this.setError(error.error);
+ } else {
+ // ensure will display what we just imported
+ Filter.addException(response);
+ this.onFinish(response);
+ }
+ }
+ );
+ },
+
+ _hasAllNeededData(dataObject) {
+ // import has no members or they are already mapped
+ return dataObject.members.length === 0 || this.membersMapping();
+ },
+
+ _prepareAdditionalData(dataObject) {
+ // we will work on the list itself (an ordered array of objects)
+ // when a mapping is done, we add a 'wekan' field to the object representing the imported member
+ const membersToMap = dataObject.members;
+ // auto-map based on username
+ membersToMap.forEach((importedMember) => {
+ const wekanUser = Users.findOne({username: importedMember.username});
+ if(wekanUser) {
+ importedMember.wekan = wekanUser;
+ }
+ });
+ // store members data and mapping in Session
+ // (we go deep and 2-way, so storing in data context is not a viable option)
+ Session.set('import.membersToMap', membersToMap);
+ return membersToMap;
+ },
+
+ _screenAdditionalData() {
+ return 'mapMembers';
+ },
+
+ _storeText() {
+ const dataJson = this.$('.js-import-json').val();
+ Session.set('import.text', dataJson);
+ return dataJson;
+ },
+});
+
+ImportPopup.extendComponent({
+ getAdditionalData() {
+ const listId = this.currentData()._id;
+ const selector = `#js-list-${this.currentData()._id} .js-minicard:first`;
+ const firstCardDom = $(selector).get(0);
+ const sortIndex = Utils.calculateIndex(null, firstCardDom).base;
+ const result = {listId, sortIndex};
+ return result;
+ },
+
+ getMethodName() {
+ return 'importTrelloCard';
+ },
+
+ getLabel() {
+ return 'import-card-trello-instruction';
+ },
+}).register('listImportCardPopup');
+
+ImportPopup.extendComponent({
+ getAdditionalData() {
+ const result = {};
+ return result;
+ },
+
+ getMethodName() {
+ return 'importTrelloBoard';
+ },
+
+ getLabel() {
+ return 'import-board-trello-instruction';
+ },
+
+ onFinish(response) {
+ Utils.goBoardId(response);
+ },
+}).register('boardImportBoardPopup');
+
+const ImportMapMembers = BlazeComponent.extendComponent({
+ members() {
+ return Session.get('import.membersToMap');
+ },
+ _refreshMembers(listOfMembers) {
+ Session.set('import.membersToMap', listOfMembers);
+ },
+ /**
+ * Will look into the list of members to import for the specified memberId,
+ * then set its property to the supplied value.
+ * If unset is true, it will remove the property from the rest of the list as well.
+ *
+ * use:
+ * - memberId = null to use selected member
+ * - value = null to unset a property
+ * - unset = true to ensure property is only set on 1 member at a time
+ */
+ _setPropertyForMember(property, value, memberId, unset = false) {
+ const listOfMembers = this.members();
+ let finder = null;
+ if(memberId) {
+ finder = (member) => member.id === memberId;
+ } else {
+ finder = (member) => member.selected;
+ }
+ listOfMembers.forEach((member) => {
+ if(finder(member)) {
+ if(value !== null) {
+ member[property] = value;
+ } else {
+ delete member[property];
+ }
+ if(!unset) {
+ // we shortcut if we don't care about unsetting the others
+ return false;
+ }
+ } else if(unset) {
+ delete member[property];
+ }
+ return true;
+ });
+ // Session.get gives us a copy, we have to set it back so it sticks
+ this._refreshMembers(listOfMembers);
+ },
+ setSelectedMember(memberId) {
+ return this._setPropertyForMember('selected', true, memberId, true);
+ },
+ /**
+ * returns the member with specified id,
+ * or the selected member if memberId is not specified
+ */
+ getMember(memberId = null) {
+ const allMembers = Session.get('import.membersToMap');
+ let finder = null;
+ if(memberId) {
+ finder = (user) => user.id === memberId;
+ } else {
+ finder = (user) => user.selected;
+ }
+ return allMembers.find(finder);
+ },
+ mapSelectedMember(wekan) {
+ return this._setPropertyForMember('wekan', wekan, null);
+ },
+ unmapMember(memberId){
+ return this._setPropertyForMember('wekan', null, memberId);
+ },
+});
+
+ImportMapMembers.extendComponent({
+ onMapMember(evt) {
+ const memberToMap = this.currentData();
+ if(memberToMap.wekan) {
+ // todo xxx ask for confirmation?
+ this.unmapMember(memberToMap.id);
+ } else {
+ this.setSelectedMember(memberToMap.id);
+ Popup.open('mapMembersAdd')(evt);
+ }
+ },
+ onSubmit(evt) {
+ evt.preventDefault();
+ Popup.back();
+ },
+ events() {
+ return [{
+ 'submit': this.onSubmit,
+ 'click .mapping': this.onMapMember,
+ }];
+ },
+}).register('mapMembersPopup');
+
+ImportMapMembers.extendComponent({
+ onSelectUser(){
+ this.mapSelectedMember(this.currentData());
+ Popup.back();
+ },
+ events() {
+ return [{
+ 'click .js-select-import': this.onSelectUser,
+ }];
+ },
+ onRendered() {
+ // todo XXX why do I not get the focus??
+ this.find('.js-map-member input').focus();
+ },
+}).register('mapMembersAddPopup');
diff --git a/client/components/import/import.styl b/client/components/import/import.styl
new file mode 100644
index 00000000..3c6cfdf3
--- /dev/null
+++ b/client/components/import/import.styl
@@ -0,0 +1,17 @@
+.map-members
+ .mapping:first-of-type
+ border-top: solid 1px #999
+ .mapping
+ padding: 10px 0
+ border-bottom: solid 1px #999
+ .source
+ display: inline-block
+ width: 80%
+ .wekan
+ display: inline-block
+ width: 35px
+ .member
+ float: none
+
+a.show-mapping
+ text-decoration underline
diff --git a/client/components/lists/list.js b/client/components/lists/list.js
index cdf30fc2..f5410ed0 100644
--- a/client/components/lists/list.js
+++ b/client/components/lists/list.js
@@ -7,7 +7,7 @@ BlazeComponent.extendComponent({
// Proxy
openForm(options) {
- this.componentChildren('listBody')[0].openForm(options);
+ this.childComponents('listBody')[0].openForm(options);
},
onCreated() {
@@ -25,7 +25,7 @@ BlazeComponent.extendComponent({
if (!Meteor.user() || !Meteor.user().isBoardMember())
return;
- const boardComponent = this.componentParent();
+ const boardComponent = this.parentComponent();
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
$cards.sortable({
@@ -73,23 +73,13 @@ BlazeComponent.extendComponent({
$cards.sortable('cancel');
if (MultiSelection.isActive()) {
- Cards.find(MultiSelection.getMongoSelector()).forEach((c, i) => {
- Cards.update(c._id, {
- $set: {
- listId,
- sort: sortIndex.base + i * sortIndex.increment,
- },
- });
+ Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
+ card.move(listId, sortIndex.base + i * sortIndex.increment);
});
} else {
const cardDomElement = ui.item.get(0);
- const cardId = Blaze.getData(cardDomElement)._id;
- Cards.update(cardId, {
- $set: {
- listId,
- sort: sortIndex.base,
- },
- });
+ const card = Blaze.getData(cardDomElement);
+ card.move(listId, sortIndex.base);
}
boardComponent.setIsDragging(false);
},
@@ -107,16 +97,15 @@ BlazeComponent.extendComponent({
accept: '.js-member,.js-label',
drop(event, ui) {
const cardId = Blaze.getData(this)._id;
- let addToSet;
+ const card = Cards.findOne(cardId);
if (ui.draggable.hasClass('js-member')) {
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
- addToSet = { members: memberId };
+ card.assignMember(memberId);
} else {
const labelId = Blaze.getData(ui.draggable.get(0))._id;
- addToSet = { labelIds: labelId };
+ card.addLabel(labelId);
}
- Cards.update(cardId, { $addToSet: addToSet });
},
});
});
diff --git a/client/components/lists/listBody.jade b/client/components/lists/listBody.jade
index b0a374ea..e659b179 100644
--- a/client/components/lists/listBody.jade
+++ b/client/components/lists/listBody.jade
@@ -22,9 +22,20 @@ template(name="listBody")
template(name="addCardForm")
.minicard.minicard-composer.js-composer
- .minicard-detailss.clearfix
- textarea.minicard-composer-textarea.js-card-title(autofocus)
+ if getLabels
+ .minicard-labels
+ each getLabels
+ .minicard-label(class="card-label-{{color}}" title="{{name}}")
+ textarea.minicard-composer-textarea.js-card-title(autofocus)
+ if members.get
.minicard-members.js-minicard-composer-members
+ each members.get
+ +userAvatar(userId=this)
+
.add-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
a.fa.fa-times-thin.js-close-inlined-form
+
+template(name="autocompleteLabelLine")
+ .minicard-label(class="card-label-{{colorName}}" title=labelName)
+ span(class="{{#if hasNoName}}quiet{{/if}}")= labelName
diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js
index 2e00cb4f..36b60d06 100644
--- a/client/components/lists/listBody.js
+++ b/client/components/lists/listBody.js
@@ -11,8 +11,8 @@ BlazeComponent.extendComponent({
options = options || {};
options.position = options.position || 'top';
- const forms = this.componentChildren('inlinedForm');
- let form = _.find(forms, (component) => {
+ const forms = this.childComponents('inlinedForm');
+ let form = forms.find((component) => {
return component.data().position === options.position;
});
if (!form && forms.length > 0) {
@@ -26,8 +26,10 @@ BlazeComponent.extendComponent({
const firstCardDom = this.find('.js-minicard:first');
const lastCardDom = this.find('.js-minicard:last');
const textarea = $(evt.currentTarget).find('textarea');
- const title = textarea.val();
- const position = Blaze.getData(evt.currentTarget).position;
+ const position = this.currentData().position;
+ const title = textarea.val().trim();
+
+ const formComponent = this.childComponents('addCardForm')[0];
let sortIndex;
if (position === 'top') {
sortIndex = Utils.calculateIndex(null, firstCardDom).base;
@@ -35,9 +37,14 @@ BlazeComponent.extendComponent({
sortIndex = Utils.calculateIndex(lastCardDom, null).base;
}
- if ($.trim(title)) {
+ const members = formComponent.members.get();
+ const labelIds = formComponent.labels.get();
+
+ if (title) {
const _id = Cards.insert({
title,
+ members,
+ labelIds,
listId: this.data()._id,
boardId: this.data().board()._id,
sort: sortIndex,
@@ -53,6 +60,8 @@ BlazeComponent.extendComponent({
if (position === 'bottom') {
this.scrollToBottom();
}
+
+ formComponent.reset();
}
},
@@ -100,11 +109,39 @@ BlazeComponent.extendComponent({
},
}).register('listBody');
+function toggleValueInReactiveArray(reactiveValue, value) {
+ const array = reactiveValue.get();
+ const valueIndex = array.indexOf(value);
+ if (valueIndex === -1) {
+ array.push(value);
+ } else {
+ array.splice(valueIndex, 1);
+ }
+ reactiveValue.set(array);
+}
+
BlazeComponent.extendComponent({
template() {
return 'addCardForm';
},
+ onCreated() {
+ this.labels = new ReactiveVar([]);
+ this.members = new ReactiveVar([]);
+ },
+
+ reset() {
+ this.labels.set([]);
+ this.members.set([]);
+ },
+
+ getLabels() {
+ const currentBoardId = Session.get('currentBoard');
+ return Boards.findOne(currentBoardId).labels.filter((label) => {
+ return this.labels.get().indexOf(label._id) > -1;
+ });
+ },
+
pressKey(evt) {
// Pressing Enter should submit the card
if (evt.keyCode === 13) {
@@ -140,4 +177,66 @@ BlazeComponent.extendComponent({
keydown: this.pressKey,
}];
},
+
+ onRendered() {
+ const editor = this;
+ this.$('textarea').escapeableTextComplete([
+ // User mentions
+ {
+ match: /\B@(\w*)$/,
+ search(term, callback) {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ callback($.map(currentBoard.members, (member) => {
+ const user = Users.findOne(member.userId);
+ return user.username.indexOf(term) === 0 ? user : null;
+ }));
+ },
+ template(user) {
+ return user.username;
+ },
+ replace(user) {
+ toggleValueInReactiveArray(editor.members, user._id);
+ return '';
+ },
+ index: 1,
+ },
+
+ // Labels
+ {
+ match: /\B#(\w*)$/,
+ search(term, callback) {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ callback($.map(currentBoard.labels, (label) => {
+ if (label.name.indexOf(term) > -1 ||
+ label.color.indexOf(term) > -1) {
+ return label;
+ }
+ }));
+ },
+ template(label) {
+ return Blaze.toHTMLWithData(Template.autocompleteLabelLine, {
+ hasNoName: !Boolean(label.name),
+ colorName: label.color,
+ labelName: label.name || label.color,
+ });
+ },
+ replace(label) {
+ toggleValueInReactiveArray(editor.labels, label._id);
+ return '';
+ },
+ index: 1,
+ },
+ ], {
+ // When the autocomplete menu is shown we want both a press of both `Tab`
+ // or `Enter` to validation the auto-completion. We also need to stop the
+ // event propagation to prevent the card from submitting (on `Enter`) or
+ // going on the next column (on `Tab`).
+ onKeydown(evt, commands) {
+ if (evt.keyCode === 9 || evt.keyCode === 13) {
+ evt.stopPropagation();
+ return commands.KEY_ENTER;
+ }
+ },
+ });
+ },
}).register('addCardForm');
diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade
index 7d01f1ba..72cd0fe9 100644
--- a/client/components/lists/listHeader.jade
+++ b/client/components/lists/listHeader.jade
@@ -25,6 +25,7 @@ template(name="listActionPopup")
li: a.js-archive-cards {{_ 'list-archive-cards'}}
hr
ul.pop-over-list
+ li: a.js-import-card {{_ 'import-card'}}
li: a.js-close-list {{_ 'archive-list'}}
template(name="listMoveCardsPopup")
diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js
index 9431b461..d660508a 100644
--- a/client/components/lists/listHeader.js
+++ b/client/components/lists/listHeader.js
@@ -5,14 +5,10 @@ BlazeComponent.extendComponent({
editTitle(evt) {
evt.preventDefault();
- const form = this.componentChildren('inlinedForm')[0];
- const newTitle = form.getValue();
- if ($.trim(newTitle)) {
- Lists.update(this.currentData()._id, {
- $set: {
- title: newTitle,
- },
- });
+ const newTitle = this.childComponents('inlinedForm')[0].getValue().trim();
+ const list = this.currentData();
+ if (newTitle) {
+ list.rename(newTitle.trim());
}
},
@@ -33,45 +29,32 @@ Template.listActionPopup.events({
},
'click .js-list-subscribe'() {},
'click .js-select-cards'() {
- const cardIds = Cards.find(
- {listId: this._id},
- {fields: { _id: 1 }}
- ).map((card) => card._id);
+ const cardIds = this.allCards().map((card) => card._id);
MultiSelection.add(cardIds);
Popup.close();
},
+ 'click .js-import-card': Popup.open('listImportCard'),
'click .js-move-cards': Popup.open('listMoveCards'),
- 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', () => {
- Cards.find({listId: this._id}).forEach((card) => {
- Cards.update(card._id, {
- $set: {
- archived: true,
- },
- });
+ 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() {
+ this.allCards().forEach((card) => {
+ card.archive();
});
Popup.close();
}),
+
'click .js-close-list'(evt) {
evt.preventDefault();
- Lists.update(this._id, {
- $set: {
- archived: true,
- },
- });
+ this.archive();
Popup.close();
},
});
Template.listMoveCardsPopup.events({
'click .js-select-list'() {
- const fromList = Template.parentData(2).data._id;
+ const fromList = Template.parentData(2).data;
const toList = this._id;
- Cards.find({ listId: fromList }).forEach((card) => {
- Cards.update(card._id, {
- $set: {
- listId: toList,
- },
- });
+ fromList.allCards().forEach((card) => {
+ card.move(toList);
});
Popup.close();
},
diff --git a/client/components/main/editor.js b/client/components/main/editor.js
index 1d88fe74..82fce641 100644
--- a/client/components/main/editor.js
+++ b/client/components/main/editor.js
@@ -1,17 +1,15 @@
-let dropdownMenuIsOpened = false;
-
Template.editor.onRendered(() => {
const $textarea = this.$('textarea');
autosize($textarea);
- $textarea.textcomplete([
+ $textarea.escapeableTextComplete([
// Emojies
{
match: /\B:([\-+\w]*)$/,
search(term, callback) {
- callback($.map(Emoji.values, (emoji) => {
- return emoji.indexOf(term) === 0 ? emoji : null;
+ callback(Emoji.values.map((emoji) => {
+ return emoji.includes(term) ? emoji : null;
}));
},
template(value) {
@@ -30,9 +28,9 @@ Template.editor.onRendered(() => {
match: /\B@(\w*)$/,
search(term, callback) {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
- callback($.map(currentBoard.members, (member) => {
+ callback(currentBoard.members.map((member) => {
const username = Users.findOne(member.userId).username;
- return username.indexOf(term) === 0 ? username : null;
+ return username.includes(term) ? username : null;
}));
},
template(value) {
@@ -44,30 +42,8 @@ Template.editor.onRendered(() => {
index: 1,
},
]);
-
- // Since commit d474017 jquery-textComplete automatically closes a potential
- // opened dropdown menu when the user press Escape. This behavior conflicts
- // with our EscapeActions system, but it's too complicated and hacky to
- // monkey-pach textComplete to disable it -- I tried. Instead we listen to
- // 'open' and 'hide' events, and create a ghost escapeAction when the dropdown
- // is opened (and rely on textComplete to execute the actual action).
- $textarea.on({
- 'textComplete:show'() {
- dropdownMenuIsOpened = true;
- },
- 'textComplete:hide'() {
- Tracker.afterFlush(() => {
- dropdownMenuIsOpened = false;
- });
- },
- });
});
-EscapeActions.register('textcomplete',
- () => {},
- () => dropdownMenuIsOpened
-);
-
// XXX I believe we should compute a HTML rendered field on the server that
// would handle markdown, emojies and user mentions. We can simply have two
// fields, one source, and one compiled version (in HTML) and send only the
@@ -78,7 +54,7 @@ const at = HTML.CharRef({html: '&commat;', str: '@'});
Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
const view = this;
const currentBoard = Boards.findOne(Session.get('currentBoard'));
- const knowedUsers = _.map(currentBoard.members, (member) => {
+ const knowedUsers = currentBoard.members.map((member) => {
member.username = Users.findOne(member.userId).username;
return member;
});
diff --git a/client/components/main/header.jade b/client/components/main/header.jade
index 4715bfc8..86dfd6a7 100644
--- a/client/components/main/header.jade
+++ b/client/components/main/header.jade
@@ -43,10 +43,10 @@ template(name="header")
the list of all boards.
if isSandstorm
.wekan-logo
- img(src="/wekan-logo-header.png" alt="Wekan")
+ img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan")
else
a.wekan-logo(href="{{pathFor 'home'}}" title="{{_ 'header-logo-title'}}")
- img(src="/wekan-logo-header.png" alt="Wekan")
+ img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan")
template(name="headerTitle")
h1 {{_ 'my-boards'}}
diff --git a/client/components/main/keyboardShortcuts.styl b/client/components/main/keyboardShortcuts.styl
index 42e0637b..f77d387f 100644
--- a/client/components/main/keyboardShortcuts.styl
+++ b/client/components/main/keyboardShortcuts.styl
@@ -14,11 +14,6 @@
padding: 5px 8px
margin: 5px
font-size: 18px
- font-weight: bold
- background: white
- border-radius: 3px
- border: 1px solid darken(white, 10%)
- box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.15)
.shortcuts-list-item-action
font-size: 1.4em
diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade
index f5a8db59..65b53f04 100644
--- a/client/components/main/layouts.jade
+++ b/client/components/main/layouts.jade
@@ -2,13 +2,24 @@ head
title Wekan
meta(name="viewport"
content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0")
+ //- XXX We should use pathFor in the following `href` to support the case
+ where the application is deployed with a path prefix, but it seems to be
+ difficult to do that cleanly with Blaze -- at least without adding extra
+ packages.
link(rel="shortcut icon" href="/wekan-favicon.png")
template(name="userFormsLayout")
section.auth-layout
h1.at-form-landing-logo
- img(src="/wekan-logo.png" alt="Wekan")
+ img(src="{{pathFor '/wekan-logo.png'}}" alt="Wekan")
+Template.dynamic(template=content)
+ div.at-form-lang
+ select.select-lang.js-userform-set-language
+ each languages
+ if isCurrentLanguage
+ option(value="{{tag}}" selected="selected") {{name}}
+ else
+ option(value="{{tag}}") {{name}}
template(name="defaultLayout")
+header
diff --git a/client/components/main/layouts.js b/client/components/main/layouts.js
index ab62e76a..3df17f41 100644
--- a/client/components/main/layouts.js
+++ b/client/components/main/layouts.js
@@ -2,10 +2,43 @@ Meteor.subscribe('boards');
BlazeLayout.setRoot('body');
+const i18nTagToT9n = (i18nTag) => {
+ // t9n/i18n tags are same now, see: https://github.com/softwarerero/meteor-accounts-t9n/pull/129
+ // but we keep this conversion function here, to be aware that that they are different system.
+ return i18nTag;
+};
+
Template.userFormsLayout.onRendered(() => {
+ const i18nTag = navigator.language;
+ if (i18nTag) {
+ T9n.setLanguage(i18nTagToT9n(i18nTag));
+ }
EscapeActions.executeAll();
});
+Template.userFormsLayout.helpers({
+ languages() {
+ return _.map(TAPi18n.getLanguages(), (lang, tag) => {
+ const name = lang.name;
+ return { tag, name };
+ });
+ },
+
+ isCurrentLanguage() {
+ const t9nTag = i18nTagToT9n(this.tag);
+ const curLang = T9n.getLanguage() || 'en';
+ return t9nTag === curLang;
+ },
+});
+
+Template.userFormsLayout.events({
+ 'change .js-userform-set-language'(evt) {
+ const i18nTag = $(evt.currentTarget).val();
+ T9n.setLanguage(i18nTagToT9n(i18nTag));
+ evt.preventDefault();
+ },
+});
+
Template.defaultLayout.events({
'click .js-close-modal': () => {
Modal.close();
diff --git a/client/components/main/layouts.styl b/client/components/main/layouts.styl
index 1dbefc20..fcc94251 100644
--- a/client/components/main/layouts.styl
+++ b/client/components/main/layouts.styl
@@ -172,6 +172,15 @@ dl, dt
dd
margin: 0 0 16px 24px
+kbd
+ padding: 1px 3px
+ margin: 3px
+ font-weight: bold
+ background: darken(white, 2%)
+ border-radius: 3px
+ border: 1px solid darken(white, 10%)
+ box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.15)
+
.clear
clear: both
diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl
index 3bef4f7d..8a685069 100644
--- a/client/components/main/popup.styl
+++ b/client/components/main/popup.styl
@@ -17,9 +17,11 @@ $popupWidth = 300px
margin: 4px -10px
width: $popupWidth
+ p,
+ textarea,
input[type="text"],
input[type="email"],
- input[type="password"]
+ input[type="password"],
input[type="file"]
margin: 4px 0 12px
width: 100%
@@ -30,8 +32,6 @@ $popupWidth = 300px
textarea
height: 72px
- margin: 4px 0 12px
- width: 100%
.header
height: 36px
diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade
index 7f7519c6..3a5c7fdb 100644
--- a/client/components/sidebar/sidebar.jade
+++ b/client/components/sidebar/sidebar.jade
@@ -33,6 +33,13 @@ template(name="membersWidget")
a.member.add-member.js-manage-board-members
i.fa.fa-plus
.clearfix
+ if isInvited
+ hr
+ p
+ i.fa.fa-exclamation-circle
+ | {{_ 'just-invited'}}
+ button.js-member-invite-accept.primary {{_ 'accept'}}
+ button.js-member-invite-decline {{_ 'decline'}}
template(name="labelsWidget")
.board-widget.board-widget-labels
@@ -56,51 +63,58 @@ template(name="memberPopup")
h3
.js-profile= user.profile.fullname
p.quiet @#{user.username}
+ if isInvited
+ p
+ i.fa.fa-exclamation-circle
+ | {{_ 'not-accepted-yet'}}
- if currentUser.isBoardMember
- ul.pop-over-list
- li
- a.js-filter-member Filter cards
+ ul.pop-over-list
+ li
+ a.js-filter-member {{_ 'filter-cards'}}
+ if currentUser.isBoardAdmin
unless isSandstorm
- if currentUser.isBoardAdmin
- li
- a.js-change-role
- | {{_ 'change-permissions'}}
- span.quiet (#{memberType})
- li
- if $eq currentUser._id userId
- //-
- XXX Not implemented!
- // a.js-leave-member {{_ 'leave-board'}}
- else
- a.js-remove-member {{_ 'remove-from-board'}}
+ li
+ a.js-change-role
+ | {{_ 'change-permissions'}}
+ span.quiet (#{memberType})
+ li
+ if $eq currentUser._id userId
+ a.js-leave-member {{_ 'leave-board'}}
+ else
+ a.js-remove-member {{_ 'remove-from-board'}}
template(name="removeMemberPopup")
- p {{_ 'remove-member-pop' name=user.profile.name username=user.username boardTitle=board.title}}
+ p {{_ 'remove-member-pop' name=user.profile.fullname username=user.username boardTitle=board.title}}
button.js-confirm.negate.full(type="submit") {{_ 'remove-member'}}
template(name="addMemberPopup")
.js-search-member
+esInput(index="users")
- ul.pop-over-list
- +esEach(index="users")
- li.item.js-member-item(class="{{#if isBoardMember}}disabled{{/if}}")
- a.name.js-select-member(title="{{profile.name}} ({{username}})")
- +userAvatar(userId=_id esSearch=true)
- span.full-name
- = profile.name
- | (<span class="username">{{username}}</span>)
- if isBoardMember
- .quiet ({{_ 'joined'}})
+ if loading.get
+ +spinner
+ else if error.get
+ .warning {{_ error.get}}
+ else
+ ul.pop-over-list
+ +esEach(index="users")
+ li.item.js-member-item(class="{{#if isBoardMember}}disabled{{/if}}")
+ a.name.js-select-member(title="{{profile.name}} ({{username}})")
+ +userAvatar(userId=_id esSearch=true)
+ span.full-name
+ = profile.fullname
+ | (<span class="username">{{username}}</span>)
+ if isBoardMember
+ .quiet ({{_ 'joined'}})
- +ifEsIsSearching(index='users')
- +spinner
+ +ifEsIsSearching(index='users')
+ +spinner
- +ifEsHasNoResults(index="users")
- .manage-member-section
- p.quiet {{_ 'no-results'}}
+ +ifEsHasNoResults(index="users")
+ .manage-member-section
+ p.quiet {{_ 'no-results'}}
+ button.js-email-invite.primary.full {{_ 'email-invite'}}
template(name="changePermissionsPopup")
ul.pop-over-list
diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js
index eff0ef52..5b58dbd9 100644
--- a/client/components/sidebar/sidebar.js
+++ b/client/components/sidebar/sidebar.js
@@ -54,7 +54,7 @@ BlazeComponent.extendComponent({
},
reachNextPeak() {
- const activitiesComponent = this.componentChildren('activities')[0];
+ const activitiesComponent = this.childComponents('activities')[0];
activitiesComponent.loadNextPage();
},
@@ -95,10 +95,10 @@ BlazeComponent.extendComponent({
events() {
// XXX Hacky, we need some kind of `super`
const mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events();
- return mixinEvents.concat([{
+ return [...mixinEvents, {
'click .js-toggle-sidebar': this.toggle,
'click .js-back-home': this.setView,
- }]);
+ }];
},
}).register('sidebar');
@@ -109,14 +109,6 @@ EscapeActions.register('sidebarView',
() => { return Sidebar && Sidebar.getView() !== defaultView; }
);
-function getMemberIndex(board, searchId) {
- for (let i = 0; i < board.members.length; i++) {
- if (board.members[i].userId === searchId)
- return i;
- }
- throw new Meteor.Error('Member not found');
-}
-
Template.memberPopup.helpers({
user() {
return Users.findOne(this.userId);
@@ -125,6 +117,9 @@ Template.memberPopup.helpers({
const type = Users.findOne(this.userId).isBoardAdmin() ? 'admin' : 'normal';
return TAPi18n.__(type).toLowerCase();
},
+ isInvited() {
+ return Users.findOne(this.userId).isInvitedTo(Session.get('currentBoard'));
+ },
});
Template.memberPopup.events({
@@ -135,24 +130,53 @@ Template.memberPopup.events({
'click .js-change-role': Popup.open('changePermissions'),
'click .js-remove-member': Popup.afterConfirm('removeMember', function() {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
- const memberIndex = getMemberIndex(currentBoard, this.userId);
-
- Boards.update(currentBoard._id, {
- $set: {
- [`members.${memberIndex}.isActive`]: false,
- },
- });
+ const memberId = this.userId;
+ currentBoard.removeMember(memberId);
Popup.close();
}),
'click .js-leave-member'() {
- // XXX Not implemented
- Popup.close();
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ Meteor.call('quitBoard', currentBoard, (err, ret) => {
+ if (!ret && ret) {
+ Popup.close();
+ FlowRouter.go('home');
+ }
+ });
+ },
+});
+
+Template.removeMemberPopup.helpers({
+ user() {
+ return Users.findOne(this.userId);
+ },
+ board() {
+ return Boards.findOne(Session.get('currentBoard'));
+ },
+});
+
+Template.membersWidget.helpers({
+ isInvited() {
+ const user = Meteor.user();
+ return user && user.isInvitedTo(Session.get('currentBoard'));
},
});
Template.membersWidget.events({
'click .js-member': Popup.open('member'),
'click .js-manage-board-members': Popup.open('addMember'),
+ 'click .js-member-invite-accept'() {
+ const boardId = Session.get('currentBoard');
+ Meteor.user().removeInvite(boardId);
+ },
+ 'click .js-member-invite-decline'() {
+ const boardId = Session.get('currentBoard');
+ Meteor.call('quitBoard', boardId, (err, ret) => {
+ if (!err && ret) {
+ Meteor.user().removeInvite(boardId);
+ FlowRouter.go('home');
+ }
+ });
+ },
});
Template.labelsWidget.events({
@@ -198,56 +222,83 @@ function draggableMembersLabelsWidgets() {
Template.membersWidget.onRendered(draggableMembersLabelsWidgets);
Template.labelsWidget.onRendered(draggableMembersLabelsWidgets);
-Template.addMemberPopup.helpers({
+BlazeComponent.extendComponent({
+ template() {
+ return 'addMemberPopup';
+ },
+
+ onCreated() {
+ this.error = new ReactiveVar('');
+ this.loading = new ReactiveVar(false);
+ },
+
+ onRendered() {
+ this.find('.js-search-member input').focus();
+ this.setLoading(false);
+ },
+
isBoardMember() {
- const user = Users.findOne(this._id);
+ const userId = this.currentData()._id;
+ const user = Users.findOne(userId);
return user && user.isBoardMember();
},
-});
-Template.addMemberPopup.events({
- 'click .js-select-member'() {
- const userId = this._id;
- const currentBoard = Boards.findOne(Session.get('currentBoard'));
- const currentMembersIds = _.pluck(currentBoard.members, 'userId');
- if (currentMembersIds.indexOf(userId) === -1) {
- Boards.update(currentBoard._id, {
- $push: {
- members: {
- userId,
- isAdmin: false,
- isActive: true,
- },
- },
- });
- } else {
- const memberIndex = getMemberIndex(currentBoard, userId);
+ isValidEmail(email) {
+ return SimpleSchema.RegEx.Email.test(email);
+ },
- Boards.update(currentBoard._id, {
- $set: {
- [`members.${memberIndex}.isActive`]: true,
- },
- });
- }
- Popup.close();
+ setError(error) {
+ this.error.set(error);
},
-});
-Template.addMemberPopup.onRendered(function() {
- this.find('.js-search-member input').focus();
-});
+ setLoading(w) {
+ this.loading.set(w);
+ },
+
+ isLoading() {
+ return this.loading.get();
+ },
+
+ inviteUser(idNameEmail) {
+ const boardId = Session.get('currentBoard');
+ this.setLoading(true);
+ const self = this;
+ Meteor.call('inviteUserToBoard', idNameEmail, boardId, (err, ret) => {
+ self.setLoading(false);
+ if (err) self.setError(err.error);
+ else if (ret.email) self.setError('email-sent');
+ else Popup.close();
+ });
+ },
+
+ events() {
+ return [{
+ 'keyup input'() {
+ this.setError('');
+ },
+ 'click .js-select-member'() {
+ const userId = this.currentData()._id;
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ if (currentBoard.memberIndex(userId)<0) {
+ this.inviteUser(userId);
+ }
+ },
+ 'click .js-email-invite'() {
+ const idNameEmail = $('.js-search-member input').val();
+ if (idNameEmail.indexOf('@')<0 || this.isValidEmail(idNameEmail)) {
+ this.inviteUser(idNameEmail);
+ } else this.setError('email-invalid');
+ },
+ }];
+ },
+}).register('addMemberPopup');
Template.changePermissionsPopup.events({
'click .js-set-admin, click .js-set-normal'(event) {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
- const memberIndex = getMemberIndex(currentBoard, this.userId);
+ const memberId = this.userId;
const isAdmin = $(event.currentTarget).hasClass('js-set-admin');
-
- Boards.update(currentBoard._id, {
- $set: {
- [`members.${memberIndex}.isAdmin`]: isAdmin,
- },
- });
+ currentBoard.setMemberPermission(memberId, isAdmin);
Popup.back(1);
},
});
diff --git a/client/components/sidebar/sidebarArchives.js b/client/components/sidebar/sidebarArchives.js
index f2597c3c..18970267 100644
--- a/client/components/sidebar/sidebarArchives.js
+++ b/client/components/sidebar/sidebarArchives.js
@@ -11,11 +11,17 @@ BlazeComponent.extendComponent({
},
archivedCards() {
- return Cards.find({ archived: true });
+ return Cards.find({
+ archived: true,
+ boardId: Session.get('currentBoard'),
+ });
},
archivedLists() {
- return Lists.find({ archived: true });
+ return Lists.find({
+ archived: true,
+ boardId: Session.get('currentBoard'),
+ });
},
cardIsInArchivedList() {
@@ -29,8 +35,8 @@ BlazeComponent.extendComponent({
events() {
return [{
'click .js-restore-card'() {
- const cardId = this.currentData()._id;
- Cards.update(cardId, {$set: {archived: false}});
+ const card = this.currentData();
+ card.restore();
},
'click .js-delete-card': Popup.afterConfirm('cardDelete', function() {
const cardId = this._id;
@@ -38,8 +44,8 @@ BlazeComponent.extendComponent({
Popup.close();
}),
'click .js-restore-list'() {
- const listId = this.currentData()._id;
- Lists.update(listId, {$set: {archived: false}});
+ const list = this.currentData();
+ list.restore();
},
}];
},
diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade
index c894bc8b..ef26ef76 100644
--- a/client/components/sidebar/sidebarFilters.jade
+++ b/client/components/sidebar/sidebarFilters.jade
@@ -13,7 +13,7 @@ template(name="filterSidebar")
if name
= name
else
- span.quiet {{_ "label-default" color}}
+ span.quiet {{_ "label-default" (_ (concat "color-" color))}}
if Filter.labelIds.isSelected _id
i.fa.fa-check
hr
@@ -75,8 +75,8 @@ template(name="multiselectionSidebar")
template(name="disambiguateMultiLabelPopup")
p {{_ 'what-to-do'}}
- button.wide.js-remove-label Remove {{_ 'remove-label'}}
- button.wide.js-add-label Add {{_ 'add-label'}}
+ button.wide.js-remove-label {{_ 'remove-label'}}
+ button.wide.js-add-label {{_ 'add-label'}}
template(name="disambiguateMultiMemberPopup")
p {{_ 'what-to-do'}}
diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js
index 335cc7d6..bdecd63e 100644
--- a/client/components/sidebar/sidebarFilters.js
+++ b/client/components/sidebar/sidebarFilters.js
@@ -30,9 +30,9 @@ BlazeComponent.extendComponent({
},
}).register('filterSidebar');
-function updateSelectedCards(query) {
+function mutateSelectedCards(mutationName, ...args) {
Cards.find(MultiSelection.getMongoSelector()).forEach((card) => {
- Cards.update(card._id, query);
+ card[mutationName](...args);
});
}
@@ -67,47 +67,34 @@ BlazeComponent.extendComponent({
'click .js-toggle-label-multiselection'(evt) {
const labelId = this.currentData()._id;
const mappedSelection = this.mapSelection('label', labelId);
- let operation;
- if (_.every(mappedSelection))
- operation = '$pull';
- else if (_.every(mappedSelection, (bool) => !bool))
- operation = '$addToSet';
- else {
+
+ if (_.every(mappedSelection)) {
+ mutateSelectedCards('removeLabel', labelId);
+ } else if (_.every(mappedSelection, (bool) => !bool)) {
+ mutateSelectedCards('addLabel', labelId);
+ } else {
const popup = Popup.open('disambiguateMultiLabel');
// XXX We need to have a better integration between the popup and the
// UI components systems.
return popup.call(this.currentData(), evt);
}
-
- updateSelectedCards({
- [operation]: {
- labelIds: labelId,
- },
- });
},
'click .js-toggle-member-multiselection'(evt) {
const memberId = this.currentData()._id;
const mappedSelection = this.mapSelection('member', memberId);
- let operation;
- if (_.every(mappedSelection))
- operation = '$pull';
- else if (_.every(mappedSelection, (bool) => !bool))
- operation = '$addToSet';
- else {
+ if (_.every(mappedSelection)) {
+ mutateSelectedCards('unassignMember', memberId);
+ } else if (_.every(mappedSelection, (bool) => !bool)) {
+ mutateSelectedCards('assignMember', memberId);
+ } else {
const popup = Popup.open('disambiguateMultiMember');
// XXX We need to have a better integration between the popup and the
// UI components systems.
return popup.call(this.currentData(), evt);
}
-
- updateSelectedCards({
- [operation]: {
- members: memberId,
- },
- });
},
'click .js-archive-selection'() {
- updateSelectedCards({$set: {archived: true}});
+ mutateSelectedCards('archive');
},
}];
},
@@ -115,22 +102,22 @@ BlazeComponent.extendComponent({
Template.disambiguateMultiLabelPopup.events({
'click .js-remove-label'() {
- updateSelectedCards({$pull: {labelIds: this._id}});
+ mutateSelectedCards('removeLabel', this._id);
Popup.close();
},
'click .js-add-label'() {
- updateSelectedCards({$addToSet: {labelIds: this._id}});
+ mutateSelectedCards('addLabel', this._id);
Popup.close();
},
});
Template.disambiguateMultiMemberPopup.events({
'click .js-unassign-member'() {
- updateSelectedCards({$pull: {members: this._id}});
+ mutateSelectedCards('assignMember', this._id);
Popup.close();
},
'click .js-assign-member'() {
- updateSelectedCards({$addToSet: {members: this._id}});
+ mutateSelectedCards('unassignMember', this._id);
Popup.close();
},
});
diff --git a/client/components/users/userAvatar.jade b/client/components/users/userAvatar.jade
index e08666e5..44e899a7 100644
--- a/client/components/users/userAvatar.jade
+++ b/client/components/users/userAvatar.jade
@@ -1,7 +1,7 @@
template(name="userAvatar")
a.member.js-member(title="{{userData.profile.fullname}} ({{userData.username}})")
- if userData.profile.avatarUrl
- img.avatar.avatar-image(src=userData.profile.avatarUrl)
+ if userData.getAvatarUrl
+ img.avatar.avatar-image(src=userData.getAvatarUrl)
else
+userAvatarInitials(userId=userData._id)
diff --git a/client/components/users/userAvatar.js b/client/components/users/userAvatar.js
index 04add0a6..1e531882 100644
--- a/client/components/users/userAvatar.js
+++ b/client/components/users/userAvatar.js
@@ -22,8 +22,11 @@ Template.userAvatar.helpers({
},
presenceStatusClassName() {
+ const user = Users.findOne(this.userId);
const userPresence = presences.findOne({ userId: this.userId });
- if (!userPresence)
+ if (user && user.isInvitedTo(Session.get('currentBoard')))
+ return 'pending';
+ else if (!userPresence)
return 'disconnected';
else if (Session.equals('currentBoard', userPresence.state.currentBoardId))
return 'active';
@@ -82,11 +85,7 @@ BlazeComponent.extendComponent({
},
setAvatar(avatarUrl) {
- Meteor.users.update(Meteor.userId(), {
- $set: {
- 'profile.avatarUrl': avatarUrl,
- },
- });
+ Meteor.user().setAvatarUrl(avatarUrl);
},
setError(error) {
@@ -151,19 +150,9 @@ Template.cardMembersPopup.helpers({
Template.cardMembersPopup.events({
'click .js-select-member'(evt) {
- const cardId = Template.parentData(2).data._id;
+ const card = Cards.findOne(Session.get('currentCard'));
const memberId = this.userId;
- let operation;
- if (Cards.find({ _id: cardId, members: memberId}).count() === 0)
- operation = '$addToSet';
- else
- operation = '$pull';
-
- Cards.update(cardId, {
- [operation]: {
- members: memberId,
- },
- });
+ card.toggleMember(memberId);
evt.preventDefault();
},
});
@@ -176,7 +165,7 @@ Template.cardMemberPopup.helpers({
Template.cardMemberPopup.events({
'click .js-remove-member'() {
- Cards.update(this.cardId, {$pull: {members: this.userId}});
+ Cards.findOne(this.cardId).unassignMember(this.userId);
Popup.close();
},
'click .js-edit-profile': Popup.open('editProfile'),
diff --git a/client/components/users/userAvatar.styl b/client/components/users/userAvatar.styl
index 83257792..b962b01c 100644
--- a/client/components/users/userAvatar.styl
+++ b/client/components/users/userAvatar.styl
@@ -56,6 +56,10 @@ avatar-radius = 50%
background: #bdbdbd
border-color: #ededed
+ &.pending
+ background: #e44242
+ border-color: #f1dada
+
.edit-avatar
position: absolute
top: 0
diff --git a/client/components/users/userForm.styl b/client/components/users/userForm.styl
index 9b6e86ce..dbe62b4e 100644
--- a/client/components/users/userForm.styl
+++ b/client/components/users/userForm.styl
@@ -45,3 +45,13 @@
.at-signUp,
.at-signIn
font-weight: bold
+
+ .at-form-lang
+ margin: auto
+ width: 275px
+ padding: 25px
+ padding-bottom: 10px
+
+ .select-lang
+ width: 275px
+ font-size: 1.0em
diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js
index 0f91fd15..a478da0c 100644
--- a/client/components/users/userHeader.js
+++ b/client/components/users/userHeader.js
@@ -18,9 +18,9 @@ Template.memberMenuPopup.events({
Template.editProfilePopup.events({
submit(evt, tpl) {
evt.preventDefault();
- const fullname = $.trim(tpl.find('.js-profile-fullname').value);
- const username = $.trim(tpl.find('.js-profile-username').value);
- const initials = $.trim(tpl.find('.js-profile-initials').value);
+ const fullname = tpl.find('.js-profile-fullname').value.trim();
+ const username = tpl.find('.js-profile-username').value.trim();
+ const initials = tpl.find('.js-profile-initials').value.trim();
Users.update(Meteor.userId(), {$set: {
'profile.fullname': fullname,
'profile.initials': initials,
diff --git a/client/config/accounts.js b/client/config/accounts.js
deleted file mode 100644
index df0935f7..00000000
--- a/client/config/accounts.js
+++ /dev/null
@@ -1,48 +0,0 @@
-const passwordField = AccountsTemplates.removeField('password');
-const emailField = AccountsTemplates.removeField('email');
-AccountsTemplates.addFields([{
- _id: 'username',
- type: 'text',
- displayName: 'username',
- required: true,
- minLength: 2,
-}, emailField, passwordField]);
-
-AccountsTemplates.configure({
- defaultLayout: 'userFormsLayout',
- defaultContentRegion: 'content',
- confirmPassword: false,
- enablePasswordChange: true,
- sendVerificationEmail: true,
- showForgotPasswordLink: true,
- onLogoutHook() {
- const homePage = 'home';
- if (FlowRouter.getRouteName() === homePage) {
- FlowRouter.reload();
- } else {
- FlowRouter.go(homePage);
- }
- },
-});
-
-_.each(['signIn', 'signUp', 'resetPwd', 'forgotPwd', 'enrollAccount'],
- (routeName) => AccountsTemplates.configureRoute(routeName));
-
-// We display the form to change the password in a popup window that already
-// have a title, so we unset the title automatically displayed by useraccounts.
-AccountsTemplates.configure({
- texts: {
- title: {
- changePwd: '',
- },
- },
-});
-
-AccountsTemplates.configureRoute('changePwd', {
- redirect() {
- // XXX We should emit a notification once we have a notification system.
- // Currently the user has no indication that his modification has been
- // applied.
- Popup.back();
- },
-});
diff --git a/client/config/blazeHelpers.js b/client/config/blazeHelpers.js
index 12990ed7..adf5ef6a 100644
--- a/client/config/blazeHelpers.js
+++ b/client/config/blazeHelpers.js
@@ -13,3 +13,7 @@ Blaze.registerHelper('currentCard', () => {
});
Blaze.registerHelper('getUser', (userId) => Users.findOne(userId));
+
+UI.registerHelper('concat', function (...args) {
+ return Array.prototype.slice.call(args, 0, -1).join('');
+});
diff --git a/client/config/router.js b/client/config/router.js
index 1cac43a0..0a6958d0 100644
--- a/client/config/router.js
+++ b/client/config/router.js
@@ -88,3 +88,26 @@ _.each(redirections, (newPath, oldPath) => {
}],
});
});
+
+// As it is not possible to use template helpers in the page <head> we create a
+// reactive function whose role is to set any page-specific tag in the <head>
+// using the `kadira:dochead` package. Currently we only use it to display the
+// board title if we are in a board page (see #364) but we may want to support
+// some <meta> tags in the future.
+const appTitle = 'Wekan';
+
+// XXX The `Meteor.startup` should not be necessary -- we don't need to wait for
+// the complete DOM to be ready to call `DocHead.setTitle`. But the problem is
+// that the global variable `Boards` is undefined when this file loads so we
+// wait a bit until hopefully all files are loaded. This will be fixed in a
+// clean way once Meteor will support ES6 modules -- hopefully in Meteor 1.3.
+Meteor.startup(() => {
+ Tracker.autorun(() => {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ const titleStack = [appTitle];
+ if (currentBoard) {
+ titleStack.push(currentBoard.title);
+ }
+ DocHead.setTitle(titleStack.reverse().join(' - '));
+ });
+});
diff --git a/client/lib/accessibility.js b/client/lib/accessibility.js
new file mode 100644
index 00000000..52b771d4
--- /dev/null
+++ b/client/lib/accessibility.js
@@ -0,0 +1,41 @@
+// In this file we define a set of DOM transformations that are specifically
+// intended for blind screen readers.
+//
+// See https://github.com/wekan/wekan/issues/337 for the general accessibility
+// considerations.
+
+// Without an href, links are non-keyboard-focusable and are not presented on
+// blind screen readers. We default to the empty anchor `#` href.
+function enforceHref(attributes) {
+ if (!_.has(attributes, 'href')) {
+ attributes.href = '#';
+ }
+ return attributes;
+}
+
+// `title` is inconsistently used on the web, and is thus inconsistently
+// presented by screen readers. `aria-label`, on the other hand, is specific to
+// accessibility and is presented in ways that title shouldn't be.
+function copyTitleInAriaLabel(attributes) {
+ if (!_.has(attributes, 'aria-label') && _.has(attributes, 'title')) {
+ attributes['aria-label'] = attributes.title;
+ }
+ return attributes;
+}
+
+// XXX Our implementation relies on overwriting Blaze virtual DOM functions,
+// which is a little bit hacky -- but still reasonable with our ES6 usage. If we
+// end up switching to React we will probably create lower level small
+// components to handle that without overwriting any build-in function.
+const {
+ A: superA,
+ I: superI,
+} = HTML;
+
+HTML.A = (attributes, ...others) => {
+ return superA(copyTitleInAriaLabel(enforceHref(attributes)), ...others);
+};
+
+HTML.I = (attributes, ...others) => {
+ return superI(copyTitleInAriaLabel(attributes), ...others);
+};
diff --git a/client/lib/dropImage.js b/client/lib/dropImage.js
new file mode 100644
index 00000000..592d5c8f
--- /dev/null
+++ b/client/lib/dropImage.js
@@ -0,0 +1,62 @@
+/* eslint-disable */
+
+// ------------------------------------------------------------------------
+// Created by STRd6
+// MIT License
+// https://github.com/distri/jquery-image_reader/blob/master/drop.coffee.md
+//
+// Raymond re-write it to javascript
+
+(function($) {
+ $.event.fix = (function(originalFix) {
+ return function(event) {
+ event = originalFix.apply(this, arguments);
+ if (event.type.indexOf('drag') === 0 || event.type.indexOf('drop') === 0) {
+ event.dataTransfer = event.originalEvent.dataTransfer;
+ }
+ return event;
+ };
+ })($.event.fix);
+
+ const defaults = {
+ callback: $.noop,
+ matchType: /image.*/,
+ };
+
+ return $.fn.dropImageReader = function(options) {
+ if (typeof options === 'function') {
+ options = {
+ callback: options,
+ };
+ }
+ options = $.extend({}, defaults, options);
+ const stopFn = function(event) {
+ event.stopPropagation();
+ return event.preventDefault();
+ };
+ return this.each(function() {
+ const element = this;
+ $(element).bind('dragenter dragover dragleave', stopFn);
+ return $(element).bind('drop', function(event) {
+ stopFn(event);
+ const files = event.dataTransfer.files;
+ for(let i=0; i<files.length; i++) {
+ const f = files[i];
+ if(f.type.match(options.matchType)) {
+ const reader = new FileReader();
+ reader.onload = function(evt) {
+ return options.callback.call(element, {
+ dataURL: evt.target.result,
+ event: evt,
+ file: f,
+ name: f.name,
+ });
+ };
+ reader.readAsDataURL(f);
+ return;
+ }
+ }
+ });
+ });
+ };
+})(jQuery);
diff --git a/client/lib/filter.js b/client/lib/filter.js
index f7baf480..74305284 100644
--- a/client/lib/filter.js
+++ b/client/lib/filter.js
@@ -95,7 +95,7 @@ Filter = {
return {};
const filterSelector = {};
- _.forEach(this._fields, (fieldName) => {
+ this._fields.forEach((fieldName) => {
const filter = this[fieldName];
if (filter._isActive())
filterSelector[fieldName] = filter._getMongoSelector();
@@ -116,7 +116,7 @@ Filter = {
},
reset() {
- _.forEach(this._fields, (fieldName) => {
+ this._fields.forEach((fieldName) => {
const filter = this[fieldName];
filter.reset();
});
diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js
index af5fb7a2..f8212c9b 100644
--- a/client/lib/keyboard.js
+++ b/client/lib/keyboard.js
@@ -23,6 +23,14 @@ Mousetrap.bind('x', () => {
}
});
+Mousetrap.bind('f', () => {
+ if (Sidebar.isOpen() && Sidebar.getView() === 'filter') {
+ Sidebar.toggle();
+ } else {
+ Sidebar.setView('filter');
+ }
+});
+
Mousetrap.bind(['down', 'up'], (evt, key) => {
if (!Session.get('currentCard')) {
return;
@@ -36,6 +44,26 @@ Mousetrap.bind(['down', 'up'], (evt, key) => {
}
});
+// XXX This shortcut should also work when hovering over a card in board view
+Mousetrap.bind('space', (evt) => {
+ if (!Session.get('currentCard')) {
+ return;
+ }
+
+ const currentUserId = Meteor.userId();
+ if (currentUserId === null) {
+ return;
+ }
+
+ if (Meteor.user().isBoardMember()) {
+ const card = Cards.findOne(Session.get('currentCard'));
+ card.toggleMember(currentUserId);
+ // We should prevent scrolling in card when spacebar is clicked
+ // This should do it according to Mousetrap docs, but it doesn't
+ evt.preventDefault();
+ }
+});
+
Template.keyboardShortcuts.helpers({
mapping: [{
keys: ['W'],
@@ -44,6 +72,9 @@ Template.keyboardShortcuts.helpers({
keys: ['Q'],
action: 'shortcut-filter-my-cards',
}, {
+ keys: ['F'],
+ action: 'shortcut-toggle-filterbar',
+ }, {
keys: ['X'],
action: 'shortcut-clear-filters',
}, {
@@ -58,5 +89,8 @@ Template.keyboardShortcuts.helpers({
}, {
keys: [':'],
action: 'shortcut-autocomplete-emojies',
+ }, {
+ keys: ['SPACE'],
+ action: 'shortcut-assign-self',
}],
});
diff --git a/client/lib/modal.js b/client/lib/modal.js
index 5b3392b2..e6301cb5 100644
--- a/client/lib/modal.js
+++ b/client/lib/modal.js
@@ -21,9 +21,9 @@ window.Modal = new class {
}
}
- open(modalName, options) {
+ open(modalName, { onCloseGoTo = ''} = {}) {
this._currentModal.set(modalName);
- this._onCloseGoTo = options && options.onCloseGoTo || '';
+ this._onCloseGoTo = onCloseGoTo;
}
};
diff --git a/client/lib/multiSelection.js b/client/lib/multiSelection.js
index c2bb2bbc..eeb2015d 100644
--- a/client/lib/multiSelection.js
+++ b/client/lib/multiSelection.js
@@ -119,12 +119,13 @@ MultiSelection = {
}
},
- toggle(cardIds, options) {
+ toggle(cardIds, options = {}) {
cardIds = _.isString(cardIds) ? [cardIds] : cardIds;
- options = _.extend({
+ options = {
add: true,
remove: true,
- }, options || {});
+ ...options,
+ };
if (!this.isActive()) {
this.reset();
@@ -133,7 +134,7 @@ MultiSelection = {
const selectedCards = this._selectedCards.get();
- _.each(cardIds, (cardId) => {
+ cardIds.forEach((cardId) => {
const indexOfCard = selectedCards.indexOf(cardId);
if (options.remove && indexOfCard > -1)
diff --git a/client/lib/pasteImage.js b/client/lib/pasteImage.js
new file mode 100644
index 00000000..264d77ac
--- /dev/null
+++ b/client/lib/pasteImage.js
@@ -0,0 +1,57 @@
+/* eslint-disable */
+
+// ------------------------------------------------------------------------
+// Created by STRd6
+// MIT License
+// https://github.com/distri/jquery-image_reader/blob/master/paste.coffee.md
+//
+// Raymond re-write it to javascript
+
+(function($) {
+ $.event.fix = (function(originalFix) {
+ return function(event) {
+ event = originalFix.apply(this, arguments);
+ if (event.type.indexOf('copy') === 0 || event.type.indexOf('paste') === 0) {
+ event.clipboardData = event.originalEvent.clipboardData;
+ }
+ return event;
+ };
+ })($.event.fix);
+
+ const defaults = {
+ callback: $.noop,
+ matchType: /image.*/,
+ };
+
+ return $.fn.pasteImageReader = function(options) {
+ if (typeof options === 'function') {
+ options = {
+ callback: options,
+ };
+ }
+ options = $.extend({}, defaults, options);
+ return this.each(function() {
+ const element = this;
+ return $(element).bind('paste', function(event) {
+ const types = event.clipboardData.types;
+ const items = event.clipboardData.items;
+ for(let i=0; i<types.length; i++) {
+ if(types[i].match(options.matchType) || items[i].type.match(options.matchType)) {
+ const f = items[i].getAsFile();
+ const reader = new FileReader();
+ reader.onload = function(evt) {
+ return options.callback.call(element, {
+ dataURL: evt.target.result,
+ event: evt,
+ file: f,
+ name: f.name,
+ });
+ };
+ reader.readAsDataURL(f);
+ return;
+ }
+ }
+ });
+ });
+ };
+})(jQuery);
diff --git a/client/lib/popup.js b/client/lib/popup.js
index 3c39af29..7418d938 100644
--- a/client/lib/popup.js
+++ b/client/lib/popup.js
@@ -91,7 +91,7 @@ window.Popup = new class {
if (!self.isOpen()) {
self.current = Blaze.renderWithData(self.template, () => {
self._dep.depend();
- return _.extend(self._getTopStack(), { stack: self._stack });
+ return { ...self._getTopStack(), stack: self._stack };
}, document.body);
} else {
@@ -191,7 +191,7 @@ window.Popup = new class {
// We close a potential opened popup on any left click on the document, or go
// one step back by pressing escape.
const escapeActions = ['back', 'close'];
-_.each(escapeActions, (actionName) => {
+escapeActions.forEach((actionName) => {
EscapeActions.register(`popup-${actionName}`,
() => Popup[actionName](),
() => Popup.isOpen(),
diff --git a/client/lib/textComplete.js b/client/lib/textComplete.js
new file mode 100644
index 00000000..3e69d07f
--- /dev/null
+++ b/client/lib/textComplete.js
@@ -0,0 +1,54 @@
+// We “inherit” the jquery-textcomplete plugin to integrate with our
+// EscapeActions system. You should always use `escapeableTextComplete` instead
+// of the vanilla `textcomplete`.
+let dropdownMenuIsOpened = false;
+
+$.fn.escapeableTextComplete = function(strategies, options, ...otherArgs) {
+ // When the autocomplete menu is shown we want both a press of both `Tab`
+ // or `Enter` to validation the auto-completion. We also need to stop the
+ // event propagation to prevent EscapeActions side effect, for instance the
+ // minicard submission (on `Enter`) or going on the next column (on `Tab`).
+ options = {
+ onKeydown(evt, commands) {
+ if (evt.keyCode === 9 || evt.keyCode === 13) {
+ evt.stopPropagation();
+ return commands.KEY_ENTER;
+ }
+ },
+ ...options,
+ };
+
+ // Proxy to the vanilla jQuery component
+ this.textcomplete(strategies, options, ...otherArgs);
+
+ // Since commit d474017 jquery-textComplete automatically closes a potential
+ // opened dropdown menu when the user press Escape. This behavior conflicts
+ // with our EscapeActions system, but it's too complicated and hacky to
+ // monkey-pach textComplete to disable it -- I tried. Instead we listen to
+ // 'open' and 'hide' events, and create a ghost escapeAction when the dropdown
+ // is opened (and rely on textComplete to execute the actual action).
+ this.on({
+ 'textComplete:show'() {
+ dropdownMenuIsOpened = true;
+ },
+ 'textComplete:hide'() {
+ Tracker.afterFlush(() => {
+ // XXX Hack. We unfortunately need to set a setTimeout here to make the
+ // `noClickEscapeOn` work bellow, otherwise clicking on a autocomplete
+ // item will close both the autocomplete menu (as expected) but also the
+ // next item in the stack (for example the minicard editor) which we
+ // don't want.
+ setTimeout(() => {
+ dropdownMenuIsOpened = false;
+ }, 100);
+ });
+ },
+ });
+};
+
+EscapeActions.register('textcomplete',
+ () => {},
+ () => dropdownMenuIsOpened, {
+ noClickEscapeOn: '.textcomplete-dropdown',
+ }
+);
diff --git a/client/lib/unsavedEdits.js b/client/lib/unsavedEdits.js
index dc267bfb..17bb29b5 100644
--- a/client/lib/unsavedEdits.js
+++ b/client/lib/unsavedEdits.js
@@ -65,7 +65,7 @@ UnsavedEdits = {
};
Blaze.registerHelper('getUnsavedValue', (fieldName, docId, defaultTo) => {
- // Workaround some blaze feature that ass a list of keywords arguments as the
+ // Workaround some blaze feature that pass a list of keywords arguments as the
// last parameter (even if the caller didn't specify any).
if (!_.isString(defaultTo)) {
defaultTo = '';
diff --git a/client/lib/utils.js b/client/lib/utils.js
index 0cd93419..6bdd5822 100644
--- a/client/lib/utils.js
+++ b/client/lib/utils.js
@@ -22,20 +22,6 @@ Utils = {
return string.charAt(0).toUpperCase() + string.slice(1);
},
- getLabelIndex(boardId, labelId) {
- const board = Boards.findOne(boardId);
- const labels = {};
- _.each(board.labels, (a, b) => {
- labels[a._id] = b;
- });
- return {
- index: labels[labelId],
- key(key) {
- return `labels.${labels[labelId]}.${key}`;
- },
- };
- },
-
// Determine the new sort index
calculateIndex(prevCardDomElement, nextCardDomElement, nCards = 1) {
let base, increment;