summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/components/activities/activities.jade8
-rw-r--r--client/components/activities/activities.styl9
-rw-r--r--client/components/boards/boardArchive.jade2
-rw-r--r--client/components/boards/boardBody.jade28
-rw-r--r--client/components/boards/boardBody.js238
-rw-r--r--client/components/boards/boardBody.styl35
-rw-r--r--client/components/boards/boardColors.styl14
-rw-r--r--client/components/boards/boardHeader.jade14
-rw-r--r--client/components/boards/boardHeader.js20
-rw-r--r--client/components/boards/boardsList.jade6
-rw-r--r--client/components/boards/boardsList.js16
-rw-r--r--client/components/boards/boardsList.styl22
-rw-r--r--client/components/cards/attachments.jade6
-rw-r--r--client/components/cards/attachments.js18
-rw-r--r--client/components/cards/attachments.styl21
-rw-r--r--client/components/cards/cardDate.js227
-rw-r--r--client/components/cards/cardDate.styl24
-rw-r--r--client/components/cards/cardDetails.jade103
-rw-r--r--client/components/cards/cardDetails.js221
-rw-r--r--client/components/cards/cardDetails.styl9
-rw-r--r--client/components/cards/cardTime.jade22
-rw-r--r--client/components/cards/cardTime.js81
-rw-r--r--client/components/cards/cardTime.styl17
-rw-r--r--client/components/cards/checklists.jade64
-rw-r--r--client/components/cards/checklists.js184
-rw-r--r--client/components/cards/checklists.styl128
-rw-r--r--client/components/cards/labels.jade3
-rw-r--r--client/components/cards/minicard.jade16
-rw-r--r--client/components/cards/minicard.styl3
-rw-r--r--client/components/forms/forms.styl53
-rw-r--r--client/components/import/import.jade1
-rw-r--r--client/components/lists/list.jade4
-rw-r--r--client/components/lists/list.js28
-rw-r--r--client/components/lists/list.styl88
-rw-r--r--client/components/lists/listBody.jade4
-rw-r--r--client/components/lists/listBody.js32
-rw-r--r--client/components/lists/listHeader.jade61
-rw-r--r--client/components/lists/listHeader.js89
-rw-r--r--client/components/main/editor.jade5
-rwxr-xr-xclient/components/main/editor.js21
-rw-r--r--client/components/main/header.jade44
-rw-r--r--client/components/main/header.js21
-rw-r--r--client/components/main/header.styl34
-rw-r--r--client/components/main/layouts.jade1
-rw-r--r--client/components/main/layouts.js16
-rw-r--r--client/components/main/layouts.styl5
-rw-r--r--client/components/settings/invitationCode.js2
-rw-r--r--client/components/settings/peopleBody.jade90
-rw-r--r--client/components/settings/peopleBody.js158
-rw-r--r--client/components/settings/peopleBody.styl15
-rw-r--r--client/components/settings/settingBody.jade33
-rw-r--r--client/components/settings/settingBody.js122
-rw-r--r--client/components/settings/settingBody.styl7
-rw-r--r--client/components/settings/settingHeader.jade9
-rw-r--r--client/components/settings/settingHeader.styl2
-rw-r--r--client/components/sidebar/sidebar.jade18
-rw-r--r--client/components/sidebar/sidebar.js19
-rw-r--r--client/components/sidebar/sidebarArchives.jade11
-rw-r--r--client/components/sidebar/sidebarArchives.js16
-rw-r--r--client/components/sidebar/sidebarFilters.jade2
-rw-r--r--client/components/sidebar/sidebarSearches.jade8
-rw-r--r--client/components/sidebar/sidebarSearches.js19
-rw-r--r--client/components/sidebar/sidebarSearches.styl2
-rw-r--r--client/components/swimlanes/swimlaneHeader.jade23
-rw-r--r--client/components/swimlanes/swimlaneHeader.js25
-rw-r--r--client/components/swimlanes/swimlanes.jade75
-rw-r--r--client/components/swimlanes/swimlanes.js235
-rw-r--r--client/components/swimlanes/swimlanes.styl54
-rw-r--r--client/components/users/userAvatar.jade12
-rw-r--r--client/components/users/userAvatar.js4
-rw-r--r--client/components/users/userHeader.jade16
-rw-r--r--client/components/users/userHeader.js37
-rw-r--r--client/config/blazeHelpers.js11
-rw-r--r--client/lib/emoji-values.js152
-rwxr-xr-xclient/lib/keyboard.js3
-rw-r--r--client/lib/modal.js2
-rw-r--r--client/lib/popup.js3
-rw-r--r--client/lib/unsavedEdits.js2
-rw-r--r--client/lib/utils.js31
79 files changed, 2580 insertions, 704 deletions
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade
index b52a2981..d3e3d5ba 100644
--- a/client/components/activities/activities.jade
+++ b/client/components/activities/activities.jade
@@ -31,7 +31,7 @@ template(name="boardActivities")
.activity-checklist(href="{{ card.absoluteUrl }}")
+viewer
= checklist.title
-
+
if($eq activityType 'addChecklistItem')
| {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}.
.activity-checklist(href="{{ card.absoluteUrl }}")
@@ -44,6 +44,9 @@ template(name="boardActivities")
if($eq activityType 'archivedList')
| {{_ 'activity-archived' list.title}}.
+ if($eq activityType 'archivedSwimlane')
+ | {{_ 'activity-archived' swimlane.title}}.
+
if($eq activityType 'createBoard')
| {{_ 'activity-created' boardLabel}}.
@@ -56,6 +59,9 @@ template(name="boardActivities")
if($eq activityType 'createList')
| {{_ 'activity-added' list.title boardLabel}}.
+ if($eq activityType 'createSwimlane')
+ | {{_ 'activity-added' swimlane.title boardLabel}}.
+
if($eq activityType 'removeList')
| {{_ 'activity-removed' title boardLabel}}.
diff --git a/client/components/activities/activities.styl b/client/components/activities/activities.styl
index 2285fc0a..380e7b40 100644
--- a/client/components/activities/activities.styl
+++ b/client/components/activities/activities.styl
@@ -1,5 +1,10 @@
@import 'nib'
+.activity-title
+ margin: 0 0.5em 0.8em
+ display: flex
+ justify-content:space-between
+
.activities
clear: both
@@ -12,10 +17,14 @@
height: @width
.activity-desc
+ word-wrap: break-word
+ overflow: hidden
flex: 1
align-self: center
margin: 0
margin-left: 3px
+ overflow: hidden;
+ word-break: break-word;
.activity-comment
display: block
diff --git a/client/components/boards/boardArchive.jade b/client/components/boards/boardArchive.jade
index 724e6569..6576f742 100644
--- a/client/components/boards/boardArchive.jade
+++ b/client/components/boards/boardArchive.jade
@@ -11,4 +11,4 @@ template(name="archivedBoards")
| {{_ 'restore-board'}}
= title
else
- li.no-items-message {{_ 'no-archived-board'}}
+ li.no-items-message {{_ 'no-archived-boards'}}
diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade
index fe10c921..29a613b9 100644
--- a/client/components/boards/boardBody.jade
+++ b/client/components/boards/boardBody.jade
@@ -14,30 +14,14 @@ template(name="board")
template(name="boardBody")
.board-wrapper(class=currentBoard.colorClass)
+sidebar
- .board-canvas(
+ .board-canvas.js-swimlanes.js-perfect-scrollbar(
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
if showOverlay.get
.board-overlay
- .lists.js-lists
- each currentBoard.lists
- +list(this)
- if currentCardIsInThisList
- +cardDetails(currentCard)
- if canSeeAddList
- +addListForm
-
-template(name="addListForm")
- .list.js-list.list-composer.js-list-composer
- .list-header
- +inlinedForm(autoclose=false)
- input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
- autocomplete="off" autofocus)
- .edit-controls.clearfix
- button.primary.confirm(type="submit") {{_ 'save'}}
- a.fa.fa-times-thin.js-close-inlined-form
- else
- a.open-list-composer.js-open-inlined-form
- i.fa.fa-plus
- | {{_ 'add-list'}}
+ if isViewSwimlanes
+ each currentBoard.swimlanes
+ +swimlane(this)
+ if isViewLists
+ +listsGroup
diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js
index b3880c61..456bf9b3 100644
--- a/client/components/boards/boardBody.js
+++ b/client/components/boards/boardBody.js
@@ -1,9 +1,8 @@
const subManager = new SubsManager();
+const { calculateIndex } = Utils;
BlazeComponent.extendComponent({
onCreated() {
- this.draggingActive = new ReactiveVar(false);
- this.showOverlay = new ReactiveVar(false);
this.isBoardReady = new ReactiveVar(false);
// The pattern we use to manually handle data loading is described here:
@@ -21,40 +20,90 @@ BlazeComponent.extendComponent({
});
});
});
+ },
- this._isDragging = false;
- this._lastDragPositionX = 0;
+ onlyShowCurrentCard() {
+ return Utils.isMiniScreen() && Session.get('currentCard');
+ },
+
+}).register('board');
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.showOverlay = new ReactiveVar(false);
+ this.draggingActive = new ReactiveVar(false);
+ this._isDragging = false;
// Used to set the overlay
this.mouseHasEnterCardDetails = false;
},
+ onRendered() {
+ const boardComponent = this;
+ const $swimlanesDom = boardComponent.$('.js-swimlanes');
+
+ $swimlanesDom.sortable({
+ tolerance: 'pointer',
+ appendTo: '.board-canvas',
+ helper: 'clone',
+ handle: '.js-swimlane-header',
+ items: '.js-swimlane:not(.placeholder)',
+ placeholder: 'swimlane placeholder',
+ distance: 7,
+ start(evt, ui) {
+ ui.placeholder.height(ui.helper.height());
+ EscapeActions.executeUpTo('popup-close');
+ boardComponent.setIsDragging(true);
+ },
+ stop(evt, ui) {
+ // To attribute the new index number, we need to get the DOM element
+ // of the previous and the following card -- if any.
+ const prevSwimlaneDom = ui.item.prev('.js-swimlane').get(0);
+ const nextSwimlaneDom = ui.item.next('.js-swimlane').get(0);
+ const sortIndex = calculateIndex(prevSwimlaneDom, nextSwimlaneDom, 1);
+
+ $swimlanesDom.sortable('cancel');
+ const swimlaneDomElement = ui.item.get(0);
+ const swimlane = Blaze.getData(swimlaneDomElement);
+
+ Swimlanes.update(swimlane._id, {
+ $set: {
+ sort: sortIndex.base,
+ },
+ });
- openNewListForm() {
- this.childComponents('addListForm')[0].open();
- },
+ boardComponent.setIsDragging(false);
+ },
+ });
- // XXX Flow components allow us to avoid creating these two setter methods by
- // exposing a public API to modify the component state. We need to investigate
- // best practices here.
- setIsDragging(bool) {
- this.draggingActive.set(bool);
+ function userIsMember() {
+ return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+ }
+
+ // If there is no data in the board (ie, no lists) we autofocus the list
+ // creation form by clicking on the corresponding element.
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ if (userIsMember() && currentBoard.lists().count() === 0) {
+ boardComponent.openNewListForm();
+ }
},
- scrollLeft(position = 0) {
- const lists = this.$('.js-lists');
- lists && lists.animate({
- scrollLeft: position,
- });
+ isViewSwimlanes() {
+ const currentUser = Meteor.user();
+ return (currentUser.profile.boardView === 'board-view-swimlanes');
},
- currentCardIsInThisList() {
- const currentCard = Cards.findOne(Session.get('currentCard'));
- const listId = this.currentData()._id;
- return currentCard && currentCard.listId === listId;
+ isViewLists() {
+ const currentUser = Meteor.user();
+ return (currentUser.profile.boardView === 'board-view-lists');
},
- onlyShowCurrentCard() {
- return Utils.isMiniScreen() && Session.get('currentCard');
+ openNewListForm() {
+ if (this.isViewSwimlanes()) {
+ this.childComponents('swimlane')[0]
+ .childComponents('addListAndSwimlaneForm')[0].open();
+ } else if (this.isViewLists()) {
+ this.childComponents('listsGroup')[0]
+ .childComponents('addListForm')[0].open();
+ }
},
events() {
@@ -66,147 +115,26 @@ BlazeComponent.extendComponent({
this.showOverlay.set(false);
}
},
-
- // Click-and-drag action
- 'mousedown .board-canvas'(evt) {
- // Translating the board canvas using the click-and-drag action can
- // conflict with the build-in browser mechanism to select text. We
- // define a list of elements in which we disable the dragging because
- // the user will legitimately expect to be able to select some text with
- // his mouse.
- const noDragInside = ['a', 'input', 'textarea', 'p', '.js-list-header'];
- if ($(evt.target).closest(noDragInside.join(',')).length === 0 && $('.lists').prop('clientHeight') > evt.offsetY) {
- this._isDragging = true;
- this._lastDragPositionX = evt.clientX;
- }
- },
'mouseup'() {
if (this._isDragging) {
this._isDragging = false;
}
},
- 'mousemove'(evt) {
- if (this._isDragging) {
- // Update the canvas position
- this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX;
- this._lastDragPositionX = evt.clientX;
- // Disable browser text selection while dragging
- evt.stopPropagation();
- evt.preventDefault();
- // Don't close opened card or inlined form at the end of the
- // click-and-drag.
- EscapeActions.executeUpTo('popup-close');
- EscapeActions.preventNextClick();
- }
- },
}];
},
-}).register('board');
-
-Template.boardBody.onRendered(function() {
- const self = BlazeComponent.getComponentForElement(this.firstNode);
-
- self.listsDom = this.find('.js-lists');
-
- if (!Session.get('currentCard')) {
- self.scrollLeft();
- }
-
- // We want to animate the card details window closing. We rely on CSS
- // transition for the actual animation.
- self.listsDom._uihooks = {
- removeElement(node) {
- const removeNode = _.once(() => {
- node.parentNode.removeChild(node);
- });
- if ($(node).hasClass('js-card-details')) {
- $(node).css({
- flexBasis: 0,
- padding: 0,
- });
- $(self.listsDom).one(CSSEvents.transitionend, removeNode);
- } else {
- removeNode();
- }
- },
- };
-
- $(self.listsDom).sortable({
- tolerance: 'pointer',
- helper: 'clone',
- handle: '.js-list-header',
- items: '.js-list:not(.js-list-composer)',
- placeholder: 'list placeholder',
- distance: 7,
- start(evt, ui) {
- ui.placeholder.height(ui.helper.height());
- Popup.close();
- },
- stop() {
- $(self.listsDom).find('.js-list:not(.js-list-composer)').each(
- (i, list) => {
- const data = Blaze.getData(list);
- Lists.update(data._id, {
- $set: {
- sort: i,
- },
- });
- }
- );
- },
- });
-
- function userIsMember() {
- return Meteor.user() && Meteor.user().isBoardMember();
- }
-
- // Disable drag-dropping while in multi-selection mode, or if the current user
- // is not a board member
- self.autorun(() => {
- const $listDom = $(self.listsDom);
- if ($listDom.data('sortable')) {
- $(self.listsDom).sortable('option', 'disabled',
- MultiSelection.isActive() || !userIsMember());
- }
- });
- // If there is no data in the board (ie, no lists) we autofocus the list
- // creation form by clicking on the corresponding element.
- const currentBoard = Boards.findOne(Session.get('currentBoard'));
- if (userIsMember() && currentBoard.lists().count() === 0) {
- self.openNewListForm();
- }
-});
-
-BlazeComponent.extendComponent({
- // Proxy
- open() {
- this.childComponents('inlinedForm')[0].open();
+ // XXX Flow components allow us to avoid creating these two setter methods by
+ // exposing a public API to modify the component state. We need to investigate
+ // best practices here.
+ setIsDragging(bool) {
+ this.draggingActive.set(bool);
},
- events() {
- return [{
- submit(evt) {
- evt.preventDefault();
- const titleInput = this.find('.list-name-input');
- const title = titleInput.value.trim();
- if (title) {
- Lists.insert({
- title,
- boardId: Session.get('currentBoard'),
- sort: $('.list').length,
- });
-
- titleInput.value = '';
- titleInput.focus();
- }
- },
- }];
+ scrollLeft(position = 0) {
+ const swimlanes = this.$('.js-swimlanes');
+ swimlanes && swimlanes.animate({
+ scrollLeft: position,
+ });
},
-}).register('addListForm');
-Template.boardBody.helpers({
- canSeeAddList() {
- return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
- },
-});
+}).register('boardBody');
diff --git a/client/components/boards/boardBody.styl b/client/components/boards/boardBody.styl
index df5696a2..a614c7ed 100644
--- a/client/components/boards/boardBody.styl
+++ b/client/components/boards/boardBody.styl
@@ -1,8 +1,11 @@
@import 'nib'
position()
- if arguments[0] == cover
- position: absolute
+ if arguments[0] == cover || arguments[0] == fixed-cover
+ if arguments[0] == cover
+ position: absolute
+ else
+ position: fixed
left: 0
right: 0
top: 0
@@ -12,26 +15,18 @@ position()
.board-wrapper
position: cover
+ overflow-y: hidden;
.board-canvas
position: cover
transition: margin .1s
+ overflow-y: auto;
&.is-sibling-sidebar-open
margin-right: 248px
- .lists
- align-items: flex-start
- display: flex
- flex-direction: row
- margin: 0 0 10px
- padding: 0 40px 5px 0
- overflow-x: auto
- overflow-y: hidden
- position: cover
-
.board-overlay
- position: cover
+ position: fixed-cover
top: -100px
right: -400px
background: black
@@ -43,3 +38,17 @@ position()
.open-minicard-composer,
.minicard-wrapper.is-checked
display: none
+
+@media screen and (max-width: 800px)
+ .board-wrapper
+
+ .board-canvas
+
+ .swimlane
+ border-bottom: 1px solid #CCC
+ display: flex
+ flex-direction: column
+ margin: 0
+ padding: 0 40px 0px 0
+ overflow-x: hidden
+ overflow-y: auto
diff --git a/client/components/boards/boardColors.styl b/client/components/boards/boardColors.styl
index 8e28fcfa..233659ca 100644
--- a/client/components/boards/boardColors.styl
+++ b/client/components/boards/boardColors.styl
@@ -51,11 +51,25 @@ setBoardColor(color)
&:not(.is-checked) + .minicard:hover:not(.minicard-composer)
background: lighten(color, 97%)
+ .toggle-label
+
+ &:after
+ background-color: darken(color, 20%)
+
+ .toggle-switch:checked ~ .toggle-label
+ background-color: lighten(color, 20%)
+
+ &:after
+ background-color: darken(color, 20%)
+
@media screen and (max-width: 800px)
&.pop-over .header
background: color
color: white
+ &#header ul li.current, &#header-quick-access ul li.current
+ border-bottom: 4px solid lighten(color, 20%)
+
.board-color-nephritis
setBoardColor(#27AE60)
diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade
index 67acdc9e..b4ccd3b3 100644
--- a/client/components/boards/boardHeader.jade
+++ b/client/components/boards/boardHeader.jade
@@ -2,7 +2,8 @@ template(name="boardHeaderBar")
h1.header-board-menu
with currentBoard
a(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}")
- = title
+ +viewer
+ = title
.board-header-btns.left
unless isMiniScreen
@@ -71,7 +72,7 @@ template(name="boardHeaderBar")
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
span {{_ 'log-in'}}
-
+
if isSandstorm
if currentUser
a.board-header-btn.js-open-archived-board
@@ -87,6 +88,15 @@ template(name="boardHeaderBar")
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
+ a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
+ i.fa.fa-search
+ span {{_ 'search'}}
+
+ a.board-header-btn.js-toggle-board-view(
+ title="{{_ 'board-view'}}")
+ i.fa.fa-th-large
+ span {{_ currentUser.profile.boardView}}
+
if canModifyBoard
a.board-header-btn.js-multiselection-activate(
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js
index 8983c722..e0b19246 100644
--- a/client/components/boards/boardHeader.js
+++ b/client/components/boards/boardHeader.js
@@ -62,10 +62,6 @@ BlazeComponent.extendComponent({
return user && user.hasStarred(boardId);
},
- isMiniScreen() {
- return Utils.isMiniScreen();
- },
-
// Only show the star counter if the number of star is greater than 2
showStarCounter() {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
@@ -84,6 +80,14 @@ BlazeComponent.extendComponent({
'click .js-open-archived-board'() {
Modal.open('archivedBoards');
},
+ 'click .js-toggle-board-view'() {
+ const currentUser = Meteor.user();
+ if (currentUser.profile.boardView === 'board-view-swimlanes') {
+ currentUser.setBoardView('board-view-lists');
+ } else if (currentUser.profile.boardView === 'board-view-lists') {
+ currentUser.setBoardView('board-view-swimlanes');
+ }
+ },
'click .js-open-filter-view'() {
Sidebar.setView('filter');
},
@@ -92,6 +96,9 @@ BlazeComponent.extendComponent({
Sidebar.setView();
Filter.reset();
},
+ 'click .js-open-search-view'() {
+ Sidebar.setView('search');
+ },
'click .js-multiselection-activate'() {
const currentCard = Session.get('currentCard');
MultiSelection.activate();
@@ -172,6 +179,11 @@ const CreateBoard = BlazeComponent.extendComponent({
permission: visibility,
}));
+ Swimlanes.insert({
+ title: 'Default',
+ boardId: this.boardId.get(),
+ });
+
Utils.goBoardId(this.boardId.get());
},
diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade
index ae82dfa9..95ce3678 100644
--- a/client/components/boards/boardsList.jade
+++ b/client/components/boards/boardsList.jade
@@ -20,6 +20,12 @@ template(name="boardList")
i.fa.js-star-board(
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
title="{{_ 'star-board-title'}}")
+
+ if hasSpentTimeCards
+ i.fa.js-has-spenttime-cards(
+ class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
+ title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
+
p.board-list-item-desc= description
li.js-add-board
a.board-list-item.label {{_ 'add-board'}}
diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js
index e4bb050e..1ed88146 100644
--- a/client/components/boards/boardsList.js
+++ b/client/components/boards/boardsList.js
@@ -1,4 +1,10 @@
+const subManager = new SubsManager();
+
BlazeComponent.extendComponent({
+ onCreated() {
+ Meteor.subscribe('setting');
+ },
+
boards() {
return Boards.find({
archived: false,
@@ -13,6 +19,16 @@ BlazeComponent.extendComponent({
return user && user.hasStarred(this.currentData()._id);
},
+ hasOvertimeCards() {
+ subManager.subscribe('board', this.currentData()._id);
+ return this.currentData().hasOvertimeCards();
+ },
+
+ hasSpentTimeCards() {
+ subManager.subscribe('board', this.currentData()._id);
+ return this.currentData().hasSpentTimeCards();
+ },
+
isInvited() {
const user = Meteor.user();
return user && user.isInvitedTo(this.currentData()._id);
diff --git a/client/components/boards/boardsList.styl b/client/components/boards/boardsList.styl
index 4b5245f9..80e47685 100644
--- a/client/components/boards/boardsList.styl
+++ b/client/components/boards/boardsList.styl
@@ -17,6 +17,7 @@ $spaceBetweenTiles = 16px
opacity: 1
.board-list-item
+ overflow: hidden;
background-color: #999
color: #f6f6f6
height: 90px
@@ -43,9 +44,9 @@ $spaceBetweenTiles = 16px
line-height: 22px
.board-list-item-desc
- color: rgba(255, 255, 255, .5)
+ color: #fff
display: block
- font-size: 10px
+ font-size: 14px
font-weight: 400
line-height: 18px
@@ -73,6 +74,23 @@ $spaceBetweenTiles = 16px
transition-duration: .15s
transition-property: color, font-size, background
+ .fa-circle
+ bottom: 0;
+ font-size: 10px;
+ height: 10px;
+ line-height: 10px;
+ padding: 9px 9px;
+ position: absolute;
+ right: 0;
+ transition-duration: .15s
+ transition-property: color, font-size, background
+
+ .has-overtime-card-active
+ color: #eb4646 !important
+
+ .no-overtime-card-active
+ color: #3cb500 !important
+
.is-star-active
color: white
diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade
index e35b364a..0f79323b 100644
--- a/client/components/cards/attachments.jade
+++ b/client/components/cards/attachments.jade
@@ -21,11 +21,11 @@ template(name="attachmentDeletePopup")
template(name="attachmentsGalery")
.attachments-galery
each attachments
- a.attachment-item.js-open-viewer(title="{{_ 'added'}} {{ moment uploadedAt }}")
- .attachment-thumbnail
+ .attachment-item
+ a.attachment-thumbnail.swipebox(href="{{url}}" title="{{name}}")
if isUploaded
if isImage
- img.attachment-thumbnail-img.js-preview-image(src="{{url}}")
+ img.attachment-thumbnail-img(src="{{url}}")
else
span.attachment-thumbnail-ext= extension
else
diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js
index 95cb9f55..bc7d3979 100644
--- a/client/components/cards/attachments.js
+++ b/client/components/cards/attachments.js
@@ -11,9 +11,6 @@ Template.attachmentsGalery.events({
'click .js-download'(event) {
event.stopPropagation();
},
- 'click .js-open-viewer'() {
- // XXX Not implemented!
- },
'click .js-add-cover'() {
Cards.findOne(this.cardId).setCover(this._id);
},
@@ -63,7 +60,13 @@ Template.cardAttachmentsPopup.events({
file.boardId = card.boardId;
file.cardId = card._id;
file.userId = Meteor.userId();
- Attachments.insert(file);
+
+ const attachment = Attachments.insert(file);
+
+ if (attachment && attachment._id && attachment.isImage()) {
+ card.setCover(attachment._id);
+ }
+
Popup.close();
});
},
@@ -110,7 +113,12 @@ Template.previewClipboardImagePopup.events({
file.boardId = card.boardId;
file.cardId = card._id;
file.userId = Meteor.userId();
- Attachments.insert(file);
+ const attachment = Attachments.insert(file);
+
+ if (attachment && attachment._id && attachment.isImage()) {
+ card.setCover(attachment._id);
+ }
+
pastedResults = null;
$(document.body).pasteImageReader(() => {});
Popup.close();
diff --git a/client/components/cards/attachments.styl b/client/components/cards/attachments.styl
index 9a5d0645..4a22fd8a 100644
--- a/client/components/cards/attachments.styl
+++ b/client/components/cards/attachments.styl
@@ -58,7 +58,28 @@
.preview-clipboard-image
width: 280px
+ max-width: 100%;
height: 200px
display: block
border: 1px solid black
box-shadow: 0 1px 2px rgba(0,0,0,.2)
+
+@media screen and (max-width: 800px)
+ .attachments-galery
+ flex-direction
+ row
+ .attachment-item
+ width: 50% - 2%
+
+ .attachment-thumbnail
+ height: 130px
+ .attachment-details
+ font-size: 1.1em
+
+@media screen and (max-width: 360px)
+ .attachments-galery
+ .attachment-item
+ width: 100%
+
+ .attachment-thumbnail
+ height: 200px
diff --git a/client/components/cards/cardDate.js b/client/components/cards/cardDate.js
index 09a6761b..f33e8c19 100644
--- a/client/components/cards/cardDate.js
+++ b/client/components/cards/cardDate.js
@@ -1,10 +1,114 @@
-// Edit start & due dates
+// Edit received, start, due & end dates
+const EditCardDate = BlazeComponent.extendComponent({
+ template() {
+ return 'editCardDate';
+ },
+
+ onCreated() {
+ this.error = new ReactiveVar('');
+ this.card = this.data();
+ this.date = new ReactiveVar(moment.invalid());
+ },
+
+ onRendered() {
+ const $picker = this.$('.js-datepicker').datepicker({
+ todayHighlight: true,
+ todayBtn: 'linked',
+ language: TAPi18n.getLanguage(),
+ }).on('changeDate', function(evt) {
+ this.find('#date').value = moment(evt.date).format('L');
+ this.error.set('');
+ this.find('#time').focus();
+ }.bind(this));
+
+ if (this.date.get().isValid()) {
+ $picker.datepicker('update', this.date.get().toDate());
+ }
+ },
+
+ showDate() {
+ if (this.date.get().isValid())
+ return this.date.get().format('L');
+ return '';
+ },
+ showTime() {
+ if (this.date.get().isValid())
+ return this.date.get().format('LT');
+ return '';
+ },
+ dateFormat() {
+ return moment.localeData().longDateFormat('L');
+ },
+ timeFormat() {
+ return moment.localeData().longDateFormat('LT');
+ },
+
+ events() {
+ return [{
+ 'keyup .js-date-field'() {
+ // parse for localized date format in strict mode
+ const dateMoment = moment(this.find('#date').value, 'L', true);
+ if (dateMoment.isValid()) {
+ this.error.set('');
+ this.$('.js-datepicker').datepicker('update', dateMoment.toDate());
+ }
+ },
+ 'keyup .js-time-field'() {
+ // parse for localized time format in strict mode
+ const dateMoment = moment(this.find('#time').value, 'LT', true);
+ if (dateMoment.isValid()) {
+ this.error.set('');
+ }
+ },
+ 'submit .edit-date'(evt) {
+ evt.preventDefault();
+
+ // if no time was given, init with 12:00
+ const time = evt.target.time.value || moment(new Date().setHours(12, 0, 0)).format('LT');
+
+ const dateString = `${evt.target.date.value} ${time}`;
+ const newDate = moment(dateString, 'L LT', true);
+ if (newDate.isValid()) {
+ this._storeDate(newDate.toDate());
+ Popup.close();
+ }
+ else {
+ this.error.set('invalid-date');
+ evt.target.date.focus();
+ }
+ },
+ 'click .js-delete-date'(evt) {
+ evt.preventDefault();
+ this._deleteDate();
+ Popup.close();
+ },
+ }];
+ },
+});
+
Template.dateBadge.helpers({
canModifyCard() {
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
},
});
+// editCardReceivedDatePopup
+(class extends EditCardDate {
+ onCreated() {
+ super.onCreated();
+ this.data().receivedAt && this.date.set(moment(this.data().receivedAt));
+ }
+
+ _storeDate(date) {
+ this.card.setReceived(date);
+ }
+
+ _deleteDate() {
+ this.card.unsetReceived();
+ }
+}).register('editCardReceivedDatePopup');
+
+
// editCardStartDatePopup
(class extends DatePicker {
onCreated() {
@@ -12,6 +116,13 @@ Template.dateBadge.helpers({
this.data().startAt && this.date.set(moment(this.data().startAt));
}
+ onRendered() {
+ super.onRendered();
+ if (moment.isDate(this.card.receivedAt)) {
+ this.$('.js-datepicker').datepicker('setStartDate', this.card.receivedAt);
+ }
+ }
+
_storeDate(date) {
this.card.setStart(date);
}
@@ -44,8 +155,31 @@ Template.dateBadge.helpers({
}
}).register('editCardDueDatePopup');
+// editCardEndDatePopup
+(class extends EditCardDate {
+ onCreated() {
+ super.onCreated();
+ this.data().endAt && this.date.set(moment(this.data().endAt));
+ }
+
+ onRendered() {
+ super.onRendered();
+ if (moment.isDate(this.card.startAt)) {
+ this.$('.js-datepicker').datepicker('setStartDate', this.card.startAt);
+ }
+ }
+
+ _storeDate(date) {
+ this.card.setEnd(date);
+ }
+
+ _deleteDate() {
+ this.card.unsetEnd();
+ }
+}).register('editCardEndDatePopup');
-// Display start & due dates
+
+// Display received, start, due & end dates
const CardDate = BlazeComponent.extendComponent({
template() {
return 'dateBadge';
@@ -74,6 +208,36 @@ const CardDate = BlazeComponent.extendComponent({
},
});
+class CardReceivedDate extends CardDate {
+ onCreated() {
+ super.onCreated();
+ const self = this;
+ self.autorun(() => {
+ self.date.set(moment(self.data().receivedAt));
+ });
+ }
+
+ classes() {
+ let classes = 'received-date' + ' ';
+ if (this.date.get().isBefore(this.now.get(), 'minute') &&
+ this.now.get().isBefore(this.data().dueAt)) {
+ classes += 'current';
+ }
+ return classes;
+ }
+
+ showTitle() {
+ return `${TAPi18n.__('card-received-on')} ${this.date.get().format('LLLL')}`;
+ }
+
+ events() {
+ return super.events().concat({
+ 'click .js-edit-date': Popup.open('editCardReceivedDate'),
+ });
+ }
+}
+CardReceivedDate.register('cardReceivedDate');
+
class CardStartDate extends CardDate {
onCreated() {
super.onCreated();
@@ -84,11 +248,12 @@ class CardStartDate extends CardDate {
}
classes() {
+ let classes = 'start-date' + ' ';
if (this.date.get().isBefore(this.now.get(), 'minute') &&
this.now.get().isBefore(this.data().dueAt)) {
- return 'current';
+ classes += 'current';
}
- return '';
+ return classes;
}
showTitle() {
@@ -113,13 +278,14 @@ class CardDueDate extends CardDate {
}
classes() {
+ let classes = 'due-date' + ' ';
if (this.now.get().diff(this.date.get(), 'days') >= 2)
- return 'long-overdue';
+ classes += 'long-overdue';
else if (this.now.get().diff(this.date.get(), 'minute') >= 0)
- return 'due';
+ classes += 'due';
else if (this.now.get().diff(this.date.get(), 'days') >= -1)
- return 'almost-due';
- return '';
+ classes += 'almost-due';
+ return classes;
}
showTitle() {
@@ -134,6 +300,44 @@ class CardDueDate extends CardDate {
}
CardDueDate.register('cardDueDate');
+class CardEndDate extends CardDate {
+ onCreated() {
+ super.onCreated();
+ const self = this;
+ self.autorun(() => {
+ self.date.set(moment(self.data().endAt));
+ });
+ }
+
+ classes() {
+ let classes = 'end-date' + ' ';
+ if (this.data.dueAt.diff(this.date.get(), 'days') >= 2)
+ classes += 'long-overdue';
+ else if (this.data.dueAt.diff(this.date.get(), 'days') >= 0)
+ classes += 'due';
+ else if (this.data.dueAt.diff(this.date.get(), 'days') >= -2)
+ classes += 'almost-due';
+ return classes;
+ }
+
+ showTitle() {
+ return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
+ }
+
+ events() {
+ return super.events().concat({
+ 'click .js-edit-date': Popup.open('editCardEndDate'),
+ });
+ }
+}
+CardEndDate.register('cardEndDate');
+
+(class extends CardReceivedDate {
+ showDate() {
+ return this.date.get().format('l');
+ }
+}).register('minicardReceivedDate');
+
(class extends CardStartDate {
showDate() {
return this.date.get().format('l');
@@ -145,3 +349,10 @@ CardDueDate.register('cardDueDate');
return this.date.get().format('l');
}
}).register('minicardDueDate');
+
+(class extends CardEndDate {
+ showDate() {
+ return this.date.get().format('l');
+ }
+}).register('minicardEndDate');
+
diff --git a/client/components/cards/cardDate.styl b/client/components/cards/cardDate.styl
index 87a3ed25..9775e82b 100644
--- a/client/components/cards/cardDate.styl
+++ b/client/components/cards/cardDate.styl
@@ -30,10 +30,30 @@
&:hover, &.is-active
background-color: darken(#fd5d47, 7)
+ &.end-date
+ time
+ &::before
+ content: "\f253" // symbol: fa-hourglass-end
+
+ &.due-date
+ time
+ &::before
+ content: "\f090" // symbol: fa-sign-in
+
+ &.start-date
+ time
+ &::before
+ content: "\f08b" // symbol: fa-sign-out
+
+ &.received-date
+ time
+ &::before
+ content: "\f251" // symbol: fa-hourglass-start
+
time
&::before
font: normal normal normal 14px/1 FontAwesome
font-size: inherit
-webkit-font-smoothing: antialiased
- content: "\f017" // clock symbol
- margin-right: 0.3em \ No newline at end of file
+ margin-right: 0.3em
+
diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade
index f72abe6d..b888210b 100644
--- a/client/components/cards/cardDetails.jade
+++ b/client/components/cards/cardDetails.jade
@@ -9,14 +9,44 @@ template(name="cardDetails")
a.fa.fa-navicon.card-details-menu.js-open-card-details-menu
h2.card-details-title.js-card-title(
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
- = title
- if isWatching
- i.fa.fa-eye.card-details-watch
+ +viewer
+ = title
+ if isWatching
+ i.fa.fa-eye.card-details-watch
if archived
p.warning {{_ 'card-archived'}}
.card-details-items
+ .card-details-item.card-details-item-received
+ h3.card-details-item-title {{_ 'card-received'}}
+ if receivedAt
+ +cardReceivedDate
+ else
+ a.js-received-date {{_ 'add'}}
+
+ .card-details-item.card-details-item-start
+ h3.card-details-item-title {{_ 'card-start'}}
+ if startAt
+ +cardStartDate
+ else
+ a.js-start-date {{_ 'add'}}
+
+ .card-details-item.card-details-item-due
+ h3.card-details-item-title {{_ 'card-due'}}
+ if dueAt
+ +cardDueDate
+ else
+ a.js-due-date {{_ 'add'}}
+
+ .card-details-item.card-details-item-end
+ h3.card-details-item-title {{_ 'card-end'}}
+ if endAt
+ +cardEndDate
+ else
+ a.js-end-date {{_ 'add'}}
+
+ .card-details-items
.card-details-item.card-details-item-members
h3.card-details-item-title {{_ 'members'}}
each members
@@ -51,6 +81,15 @@ template(name="cardDetails")
= definition.name
+cardCustomField
+ .card-details-items
+ if spentTime
+ .card-details-item.card-details-item-spent
+ if isOvertime
+ h3.card-details-item-title {{_ 'overtime-hours'}}
+ else
+ h3.card-details-item-title {{_ 'spent-time-hours'}}
+ +cardSpentTime
+
//- XXX We should use "editable" to avoid repetiting ourselves
if canModifyCard
h3.card-details-item-title {{_ 'description'}}
@@ -81,16 +120,24 @@ template(name="cardDetails")
hr
+checklists(cardId = _id)
- if attachments.count
- hr
- h2
- i.fa.fa-paperclip
- | {{_ 'attachments'}}
+ hr
+ h3
+ i.fa.fa-paperclip
+ | {{_ 'attachments'}}
- +attachmentsGalery
+ +attachmentsGalery
hr
- h2 {{ _ 'activity'}}
+ .activity-title
+ h3 {{ _ 'activity'}}
+ if currentUser.isBoardMember
+ .material-toggle-switch
+ span.toggle-switch-title {{_ 'hide-system-messages'}}
+ if hiddenSystemMessages
+ input.toggle-switch(type="checkbox" id="toggleButton" checked="checked")
+ else
+ input.toggle-switch(type="checkbox" id="toggleButton")
+ label.toggle-label(for="toggleButton")
if currentUser.isBoardMember
+commentForm
if isLoaded.get
@@ -112,9 +159,12 @@ template(name="cardDetailsActionsPopup")
li: a.js-members {{_ 'card-edit-members'}}
li: a.js-labels {{_ 'card-edit-labels'}}
li: a.js-attachments {{_ 'card-edit-attachments'}}
+ li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
li: a.js-custom-fields {{_ 'card-edit-custom-fields'}}
li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
+ li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
+ li: a.js-spent-time {{_ 'editCardSpentTimePopup-title'}}
hr
ul.pop-over-list
li: a.js-move-card-to-top {{_ 'moveCardToTop-title'}}
@@ -123,19 +173,48 @@ template(name="cardDetailsActionsPopup")
ul.pop-over-list
li: a.js-move-card {{_ 'moveCardPopup-title'}}
li: a.js-copy-card {{_ 'copyCardPopup-title'}}
+ li: a.js-copy-checklist-cards {{_ 'copyChecklistToManyCardsPopup-title'}}
unless archived
li: a.js-archive {{_ 'archive-card'}}
li: a.js-more {{_ 'cardMorePopup-title'}}
template(name="moveCardPopup")
- +boardLists
+ +boardsAndLists
template(name="copyCardPopup")
label(for='copy-card-title') {{_ 'title'}}:
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
= title
+ +boardsAndLists
+
+
+template(name="copyChecklistToManyCardsPopup")
+ label(for='copy-checklist-cards-title') {{_ 'copyChecklistToManyCardsPopup-instructions'}}:
+ textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
+ | {{_ 'copyChecklistToManyCardsPopup-format'}}
+ +boardsAndLists
+
+template(name="boardsAndLists")
+ label {{_ 'boards'}}:
+ select.js-select-boards
+ each boards
+ if $eq _id currentBoard._id
+ option(value="{{_id}}" selected) {{_ 'current'}}
+ else
+ option(value="{{_id}}") {{title}}
+
+ label {{_ 'swimlanes'}}:
+ select.js-select-swimlanes
+ each swimlanes
+ option(value="{{_id}}") {{title}}
+
label {{_ 'lists'}}:
- +boardLists
+ select.js-select-lists
+ each aBoardLists
+ option(value="{{_id}}") {{title}}
+
+ .edit-controls.clearfix
+ button.primary.confirm.js-done {{_ 'done'}}
template(name="cardMembersPopup")
ul.pop-over-list.js-card-member-list
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index 8d5c478d..26549fda 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -1,3 +1,6 @@
+const subManager = new SubsManager();
+const { calculateIndexData } = Utils;
+
BlazeComponent.extendComponent({
mixins() {
return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar];
@@ -18,9 +21,11 @@ BlazeComponent.extendComponent({
onCreated() {
this.isLoaded = new ReactiveVar(false);
- this.parentComponent().showOverlay.set(true);
- this.parentComponent().mouseHasEnterCardDetails = false;
+ this.parentComponent().parentComponent().showOverlay.set(true);
+ this.parentComponent().parentComponent().mouseHasEnterCardDetails = false;
this.calculateNextPeak();
+
+ Meteor.subscribe('unsaved-edits');
},
isWatching() {
@@ -28,16 +33,20 @@ BlazeComponent.extendComponent({
return card.findWatcher(Meteor.userId());
},
+ hiddenSystemMessages() {
+ return Meteor.user().hasHiddenSystemMessages();
+ },
+
canModifyCard() {
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
},
scrollParentContainer() {
const cardPanelWidth = 510;
- const bodyBoardComponent = this.parentComponent();
+ const bodyBoardComponent = this.parentComponent().parentComponent();
- const $cardContainer = bodyBoardComponent.$('.js-lists');
const $cardView = this.$(this.firstNode());
+ const $cardContainer = bodyBoardComponent.$('.js-swimlanes');
const cardContainerScroll = $cardContainer.scrollLeft();
const cardContainerWidth = $cardContainer.width();
@@ -58,10 +67,55 @@ BlazeComponent.extendComponent({
onRendered() {
if (!Utils.isMiniScreen()) this.scrollParentContainer();
+ const $checklistsDom = this.$('.card-checklist-items');
+
+ $checklistsDom.sortable({
+ tolerance: 'pointer',
+ helper: 'clone',
+ handle: '.checklist-title',
+ items: '.js-checklist',
+ placeholder: 'checklist placeholder',
+ distance: 7,
+ start(evt, ui) {
+ ui.placeholder.height(ui.helper.height());
+ EscapeActions.executeUpTo('popup-close');
+ },
+ stop(evt, ui) {
+ let prevChecklist = ui.item.prev('.js-checklist').get(0);
+ if (prevChecklist) {
+ prevChecklist = Blaze.getData(prevChecklist).checklist;
+ }
+ let nextChecklist = ui.item.next('.js-checklist').get(0);
+ if (nextChecklist) {
+ nextChecklist = Blaze.getData(nextChecklist).checklist;
+ }
+ const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1);
+
+ $checklistsDom.sortable('cancel');
+ const checklist = Blaze.getData(ui.item.get(0)).checklist;
+
+ Checklists.update(checklist._id, {
+ $set: {
+ sort: sortIndex.base,
+ },
+ });
+ },
+ });
+
+ function userIsMember() {
+ return Meteor.user() && Meteor.user().isBoardMember();
+ }
+
+ // Disable sorting if the current user is not a board member
+ this.autorun(() => {
+ if ($checklistsDom.data('sortable')) {
+ $checklistsDom.sortable('option', 'disabled', !userIsMember());
+ }
+ });
},
onDestroyed() {
- this.parentComponent().showOverlay.set(false);
+ this.parentComponent().parentComponent().showOverlay.set(false);
},
events() {
@@ -95,9 +149,16 @@ BlazeComponent.extendComponent({
'click .js-member': Popup.open('cardMember'),
'click .js-add-members': Popup.open('cardMembers'),
'click .js-add-labels': Popup.open('cardLabels'),
+ 'click .js-received-date': Popup.open('editCardReceivedDate'),
+ 'click .js-start-date': Popup.open('editCardStartDate'),
+ 'click .js-due-date': Popup.open('editCardDueDate'),
+ 'click .js-end-date': Popup.open('editCardEndDate'),
'mouseenter .js-card-details' () {
- this.parentComponent().showOverlay.set(true);
- this.parentComponent().mouseHasEnterCardDetails = true;
+ this.parentComponent().parentComponent().showOverlay.set(true);
+ this.parentComponent().parentComponent().mouseHasEnterCardDetails = true;
+ },
+ 'click #toggleButton'() {
+ Meteor.call('toggleSystemMessages');
},
}];
},
@@ -154,20 +215,24 @@ Template.cardDetailsActionsPopup.events({
'click .js-members': Popup.open('cardMembers'),
'click .js-labels': Popup.open('cardLabels'),
'click .js-attachments': Popup.open('cardAttachments'),
+ 'click .js-received-date': Popup.open('editCardReceivedDate'),
'click .js-custom-fields': Popup.open('cardCustomFields'),
'click .js-start-date': Popup.open('editCardStartDate'),
'click .js-due-date': Popup.open('editCardDueDate'),
+ 'click .js-end-date': Popup.open('editCardEndDate'),
+ 'click .js-spent-time': Popup.open('editCardSpentTime'),
'click .js-move-card': Popup.open('moveCard'),
'click .js-copy-card': Popup.open('copyCard'),
+ 'click .js-copy-checklist-cards': Popup.open('copyChecklistToManyCards'),
'click .js-move-card-to-top' (evt) {
evt.preventDefault();
- const minOrder = _.min(this.list().cards().map((c) => c.sort));
- this.move(this.listId, minOrder - 1);
+ const minOrder = _.min(this.list().cards(this.swimlaneId).map((c) => c.sort));
+ this.move(this.swimlaneId, this.listId, minOrder - 1);
},
'click .js-move-card-to-bottom' (evt) {
evt.preventDefault();
- const maxOrder = _.max(this.list().cards().map((c) => c.sort));
- this.move(this.listId, maxOrder + 1);
+ const maxOrder = _.max(this.list().cards(this.swimlaneId).map((c) => c.sort));
+ this.move(this.swimlaneId, this.listId, maxOrder + 1);
},
'click .js-archive' (evt) {
evt.preventDefault();
@@ -191,36 +256,82 @@ Template.editCardTitleForm.onRendered(function () {
Template.editCardTitleForm.events({
'keydown .js-edit-card-title' (evt) {
// If enter key was pressed, submit the data
- if (evt.keyCode === 13) {
+ // Unless the shift key is also being pressed
+ if (evt.keyCode === 13 && !evt.shiftKey) {
$('.js-submit-edit-card-title-form').click();
}
},
});
Template.moveCardPopup.events({
- 'click .js-select-list' () {
+ 'click .js-done' () {
// XXX We should *not* get the currentCard from the global state, but
// instead from a “component” state.
const card = Cards.findOne(Session.get('currentCard'));
- const newListId = this._id;
- card.move(newListId);
+ const lSelect = $('.js-select-lists')[0];
+ const newListId = lSelect.options[lSelect.selectedIndex].value;
+ const slSelect = $('.js-select-swimlanes')[0];
+ card.swimlaneId = slSelect.options[slSelect.selectedIndex].value;
+ card.move(card.swimlaneId, newListId, 0);
Popup.close();
},
});
+BlazeComponent.extendComponent({
+ onCreated() {
+ subManager.subscribe('board', Session.get('currentBoard'));
+ this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
+ },
+
+ boards() {
+ const boards = Boards.find({
+ archived: false,
+ 'members.userId': Meteor.userId(),
+ }, {
+ sort: ['title'],
+ });
+ return boards;
+ },
+
+ swimlanes() {
+ const board = Boards.findOne(this.selectedBoardId.get());
+ return board.swimlanes();
+ },
+
+ aBoardLists() {
+ const board = Boards.findOne(this.selectedBoardId.get());
+ return board.lists();
+ },
+
+ events() {
+ return [{
+ 'change .js-select-boards'(evt) {
+ this.selectedBoardId.set($(evt.currentTarget).val());
+ subManager.subscribe('board', this.selectedBoardId.get());
+ },
+ }];
+ },
+}).register('boardsAndLists');
+
Template.copyCardPopup.events({
- 'click .js-select-list' (evt) {
+ 'click .js-done'() {
const card = Cards.findOne(Session.get('currentCard'));
const oldId = card._id;
card._id = null;
- card.listId = this._id;
- const textarea = $(evt.currentTarget).parents('.content').find('textarea');
+ const lSelect = $('.js-select-lists')[0];
+ card.listId = lSelect.options[lSelect.selectedIndex].value;
+ const slSelect = $('.js-select-swimlanes')[0];
+ card.swimlaneId = slSelect.options[slSelect.selectedIndex].value;
+ const bSelect = $('.js-select-boards')[0];
+ card.boardId = bSelect.options[bSelect.selectedIndex].value;
+ const textarea = $('#copy-card-title');
const title = textarea.val().trim();
// insert new card to the bottom of new list
- card.sort = Lists.findOne(this._id).cards().count();
+ card.sort = Lists.findOne(card.listId).cards().count();
if (title) {
card.title = title;
+ card.coverId = '';
const _id = Cards.insert(card);
// In case the filter is active we need to add the newly inserted card in
// the list of exceptions -- cards that are not filtered. Otherwise the
@@ -233,9 +344,16 @@ Template.copyCardPopup.events({
cursor.forEach(function() {
'use strict';
const checklist = arguments[0];
+ const checklistId = checklist._id;
checklist.cardId = _id;
checklist._id = null;
- Checklists.insert(checklist);
+ const newChecklistId = Checklists.insert(checklist);
+ ChecklistItems.find({checklistId}).forEach(function(item) {
+ item._id = null;
+ item.checklistId = newChecklistId;
+ item.cardId = _id;
+ ChecklistItems.insert(item);
+ });
});
// copy card comments
@@ -252,6 +370,69 @@ Template.copyCardPopup.events({
},
});
+Template.copyChecklistToManyCardsPopup.events({
+ 'click .js-done' () {
+ const card = Cards.findOne(Session.get('currentCard'));
+ const oldId = card._id;
+ card._id = null;
+ const lSelect = $('.js-select-lists')[0];
+ card.listId = lSelect.options[lSelect.selectedIndex].value;
+ const slSelect = $('.js-select-swimlanes')[0];
+ card.swimlaneId = slSelect.options[slSelect.selectedIndex].value;
+ const bSelect = $('.js-select-boards')[0];
+ card.boardId = bSelect.options[bSelect.selectedIndex].value;
+ const textarea = $('#copy-card-title');
+ const titleEntry = textarea.val().trim();
+ // insert new card to the bottom of new list
+ card.sort = Lists.findOne(card.listId).cards().count();
+
+ if (titleEntry) {
+ const titleList = JSON.parse(titleEntry);
+ for (let i = 0; i < titleList.length; i++){
+ const obj = titleList[i];
+ card.title = obj.title;
+ card.description = obj.description;
+ card.coverId = '';
+ const _id = Cards.insert(card);
+ // In case the filter is active we need to add the newly inserted card in
+ // the list of exceptions -- cards that are not filtered. Otherwise the
+ // card will disappear instantly.
+ // See https://github.com/wekan/wekan/issues/80
+ Filter.addException(_id);
+
+ // copy checklists
+ let cursor = Checklists.find({cardId: oldId});
+ cursor.forEach(function() {
+ 'use strict';
+ const checklist = arguments[0];
+ const checklistId = checklist._id;
+ checklist.cardId = _id;
+ checklist._id = null;
+ const newChecklistId = Checklists.insert(checklist);
+ ChecklistItems.find({checklistId}).forEach(function(item) {
+ item._id = null;
+ item.checklistId = newChecklistId;
+ item.cardId = _id;
+ ChecklistItems.insert(item);
+ });
+ });
+
+ // copy card comments
+ cursor = CardComments.find({cardId: oldId});
+ cursor.forEach(function () {
+ 'use strict';
+ const comment = arguments[0];
+ comment.cardId = _id;
+ comment._id = null;
+ CardComments.insert(comment);
+ });
+ }
+ Popup.close();
+ }
+ },
+});
+
+
Template.cardMorePopup.events({
'click .js-copy-card-link-to-clipboard' () {
// Clipboard code from:
diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl
index c981e2a2..e18c07a1 100644
--- a/client/components/cards/cardDetails.styl
+++ b/client/components/cards/cardDetails.styl
@@ -2,7 +2,6 @@
.card-details
padding: 0 20px
- height: 100%
flex-shrink: 0
flex-basis: 470px
will-change: flex-basis
@@ -79,15 +78,19 @@
margin-right: 0
&.card-details-item-labels,
&.card-details-item-members,
+ &.card-details-item-received,
&.card-details-item-start,
&.card-details-item-due,
+ &.card-details-item-end
+ width: 50%
+ flex-shrink: 1
&.card-details-item-customfield
max-width: 50%
flex-grow: 1
.card-details-item-title
- font-size: 14px
- color: darken(white, 45%)
+ font-size: 16px
+ color: #000
.card-label
padding-top: 5px
diff --git a/client/components/cards/cardTime.jade b/client/components/cards/cardTime.jade
new file mode 100644
index 00000000..dcfc92f0
--- /dev/null
+++ b/client/components/cards/cardTime.jade
@@ -0,0 +1,22 @@
+template(name="editCardSpentTime")
+ .edit-card-time
+ form.edit-time
+ .fields
+ label(for="time") {{_ 'time'}}
+ input.js-time-field#time(type="number" step="0.01" name="time" value="{{card.spentTime}}" placeholder=timeFormat autofocus)
+ label(for="overtime") {{_ 'overtime'}}
+ a.js-toggle-overtime
+ .materialCheckBox#overtime(class="{{#if card.isOvertime}}is-checked{{/if}}" name="overtime")
+
+ if error.get
+ .warning {{_ error.get}}
+ button.primary.wide.left.js-submit-time(type="submit") {{_ 'save'}}
+ button.js-delete-time.negate.wide.right {{_ 'delete'}}
+
+template(name="timeBadge")
+ if canModifyCard
+ a.js-edit-time.card-time(title="{{showTitle}}" class="{{#if isOvertime}}card-label-red{{else}}card-label-green{{/if}}")
+ | {{showTime}}
+ else
+ a.card-time(title="{{showTitle}}" class="{{#if isOvertime}}card-label-red{{else}}card-label-green{{/if}}")
+ | {{showTime}}
diff --git a/client/components/cards/cardTime.js b/client/components/cards/cardTime.js
new file mode 100644
index 00000000..eadcc88e
--- /dev/null
+++ b/client/components/cards/cardTime.js
@@ -0,0 +1,81 @@
+BlazeComponent.extendComponent({
+ template() {
+ return 'editCardSpentTime';
+ },
+ onCreated() {
+ this.error = new ReactiveVar('');
+ this.card = this.data();
+ },
+ toggleOvertime() {
+ this.card.isOvertime = !this.card.isOvertime;
+ $('#overtime .materialCheckBox').toggleClass('is-checked');
+
+ $('#overtime').toggleClass('is-checked');
+ },
+ storeTime(spentTime, isOvertime) {
+ this.card.setSpentTime(spentTime);
+ this.card.setOvertime(isOvertime);
+ },
+ deleteTime() {
+ this.card.unsetSpentTime();
+ },
+ events() {
+ return [{
+ //TODO : need checking this portion
+ 'submit .edit-time'(evt) {
+ evt.preventDefault();
+
+ const spentTime = parseFloat(evt.target.time.value);
+ const isOvertime = this.card.isOvertime;
+
+ if (spentTime >= 0) {
+ this.storeTime(spentTime, isOvertime);
+ Popup.close();
+ } else {
+ this.error.set('invalid-time');
+ evt.target.time.focus();
+ }
+ },
+ 'click .js-delete-time'(evt) {
+ evt.preventDefault();
+ this.deleteTime();
+ Popup.close();
+ },
+ 'click a.js-toggle-overtime': this.toggleOvertime,
+ }];
+ },
+}).register('editCardSpentTimePopup');
+
+BlazeComponent.extendComponent({
+ template() {
+ return 'timeBadge';
+ },
+ onCreated() {
+ const self = this;
+ self.time = ReactiveVar();
+ },
+ showTitle() {
+ if (this.data().isOvertime) {
+ return `${TAPi18n.__('overtime')} ${this.data().spentTime} ${TAPi18n.__('hours')}`;
+ } else {
+ return `${TAPi18n.__('card-spent')} ${this.data().spentTime} ${TAPi18n.__('hours')}`;
+ }
+ },
+ showTime() {
+ return this.data().spentTime;
+ },
+ isOvertime() {
+ return this.data().isOvertime;
+ },
+ events() {
+ return [{
+ 'click .js-edit-time': Popup.open('editCardSpentTime'),
+ }];
+ },
+}).register('cardSpentTime');
+
+Template.timeBadge.helpers({
+ canModifyCard() {
+ return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+ },
+});
diff --git a/client/components/cards/cardTime.styl b/client/components/cards/cardTime.styl
new file mode 100644
index 00000000..3c4b43ae
--- /dev/null
+++ b/client/components/cards/cardTime.styl
@@ -0,0 +1,17 @@
+.card-time
+ display: block
+ border-radius: 4px
+ padding: 1px 3px
+ color: #fff
+
+ background-color: #dbdbdb
+ &:hover, &.is-active
+ background-color: #b3b3b3
+
+ time
+ &::before
+ font: normal normal normal 14px/1 FontAwesome
+ font-size: inherit
+ -webkit-font-smoothing: antialiased
+ content: "\f017" // clock symbol
+ margin-right: 0.3em
diff --git a/client/components/cards/checklists.jade b/client/components/cards/checklists.jade
index 7ecc5dd3..ae680bd5 100644
--- a/client/components/cards/checklists.jade
+++ b/client/components/cards/checklists.jade
@@ -1,8 +1,14 @@
template(name="checklists")
- h2 {{_ 'checklists'}}
+ h3 {{_ 'checklists'}}
+ if toggleDeleteDialog.get
+ .board-overlay#card-details-overlay
+ +checklistDeleteDialog(checklist = checklistToDelete)
+
+
.card-checklist-items
each checklist in currentCard.checklists
+checklistDetail(checklist = checklist)
+
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId)
+addChecklistItemForm
@@ -12,19 +18,37 @@ template(name="checklists")
| {{_ 'add-checklist'}}...
template(name="checklistDetail")
- +inlinedForm(classNames="js-edit-checklist-title" checklist = checklist)
- +editChecklistItemForm(checklist = checklist)
- else
- .checklist-title
- .checkbox.fa.fa-check-square-o
- if canModifyCard
- a.js-delete-checklist {{_ "delete"}}...
- span.checklist-stat(class="{{#if checklist.isFinished}}is-finished{{/if}}") {{checklist.finishedCount}}/{{checklist.itemCount}}
- if canModifyCard
- h2.title.js-open-inlined-form.is-editable {{checklist.title}}
- else
- h2.title {{checklist.title}}
- +checklistItems(checklist = checklist)
+ .js-checklist.checklist
+ +inlinedForm(classNames="js-edit-checklist-title" checklist = checklist)
+ +editChecklistItemForm(checklist = checklist)
+ else
+ .checklist-title
+ span
+ if canModifyCard
+ a.js-delete-checklist.toggle-delete-checklist-dialog {{_ "delete"}}...
+
+ span.checklist-stat(class="{{#if checklist.isFinished}}is-finished{{/if}}") {{checklist.finishedCount}}/{{checklist.itemCount}}
+ if canModifyCard
+ h2.title.js-open-inlined-form.is-editable
+ +viewer
+ = checklist.title
+ else
+ h2.title
+ +viewer
+ = checklist.title
+ +checklistItems(checklist = checklist)
+
+template(name="checklistDeleteDialog")
+ .js-confirm-checklist-delete
+ p
+ i(class="fa fa-exclamation-triangle" aria-hidden="true")
+ p
+ | {{_ 'confirm-checklist-delete-dialog'}}
+ span {{checklist.title}}
+ | ?
+ .js-checklist-delete-buttons
+ button.confirm-checklist-delete(type="button") {{_ 'delete'}}
+ button.toggle-delete-checklist-dialog(type="button") {{_ 'cancel'}}
template(name="addChecklistItemForm")
textarea.js-add-checklist-item(rows='1' autofocus)
@@ -47,7 +71,7 @@ template(name="editChecklistItemForm")
template(name="checklistItems")
.checklist-items.js-checklist-items
- each item in checklist.getItems
+ each item in checklist.items
+inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
+editChecklistItemForm(type = 'item' item = item checklist = checklist)
else
@@ -61,10 +85,14 @@ template(name="checklistItems")
| {{_ 'add-checklist-item'}}...
template(name='itemDetail')
- .item.js-checklist-item
+ .js-checklist-item.checklist-item
if canModifyCard
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
- .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}") {{item.title}}
+ .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+ +viewer
+ = item.title
else
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
- .item-title(class="{{#if item.isFinished }}is-checked{{/if}}") {{item.title}}
+ .item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
+ +viewer
+ = item.title
diff --git a/client/components/cards/checklists.js b/client/components/cards/checklists.js
index bd9d275a..1f05aded 100644
--- a/client/components/cards/checklists.js
+++ b/client/components/cards/checklists.js
@@ -1,11 +1,14 @@
+const { calculateIndexData } = Utils;
+
function initSorting(items) {
items.sortable({
tolerance: 'pointer',
helper: 'clone',
items: '.js-checklist-item:not(.placeholder)',
- axis: 'y',
+ connectWith: '.js-checklist-items',
+ appendTo: '.board-canvas',
distance: 7,
- placeholder: 'placeholder',
+ placeholder: 'checklist-item placeholder',
scroll: false,
start(evt, ui) {
ui.placeholder.height(ui.helper.height());
@@ -13,57 +16,57 @@ function initSorting(items) {
},
stop(evt, ui) {
const parent = ui.item.parents('.js-checklist-items');
- const orderedItems = [];
- parent.find('.js-checklist-item').each(function(i, item) {
- const checklistItem = Blaze.getData(item).item;
- orderedItems.push(checklistItem._id);
- });
- items.sortable('cancel');
- const formerParent = ui.item.parents('.js-checklist-items');
- let checklist = Blaze.getData(parent.get(0)).checklist;
- const oldChecklist = Blaze.getData(formerParent.get(0)).checklist;
- if (oldChecklist._id !== checklist._id) {
- const currentItem = Blaze.getData(ui.item.get(0)).item;
- for (let i = 0; i < orderedItems.length; i++) {
- let itemId = orderedItems[i];
- if (itemId !== currentItem._id) continue;
- checklist.addItem(currentItem.title);
- checklist = Checklists.findOne({_id: checklist._id});
- itemId = checklist._id + (checklist.newItemIndex - 1);
- if (currentItem.finished) {
- checklist.finishItem(itemId);
- }
- orderedItems[i] = itemId;
- oldChecklist.removeItem(currentItem._id);
- }
+ const checklistId = Blaze.getData(parent.get(0)).checklist._id;
+ let prevItem = ui.item.prev('.js-checklist-item').get(0);
+ if (prevItem) {
+ prevItem = Blaze.getData(prevItem).item;
}
- checklist.sortItems(orderedItems);
+ let nextItem = ui.item.next('.js-checklist-item').get(0);
+ if (nextItem) {
+ nextItem = Blaze.getData(nextItem).item;
+ }
+ const nItems = 1;
+ const sortIndex = calculateIndexData(prevItem, nextItem, nItems);
+ const checklistDomElement = ui.item.get(0);
+ const checklistData = Blaze.getData(checklistDomElement);
+ const checklistItem = checklistData.item;
+
+ items.sortable('cancel');
+
+ checklistItem.move(checklistId, sortIndex.base);
},
});
}
-Template.checklists.onRendered(function () {
- const self = BlazeComponent.getComponentForElement(this.firstNode);
- self.itemsDom = this.$('.card-checklist-items');
- initSorting(self.itemsDom);
- self.itemsDom.mousedown(function(evt) {
- evt.stopPropagation();
- });
+BlazeComponent.extendComponent({
+ onRendered() {
+ const self = this;
+ self.itemsDom = this.$('.js-checklist-items');
+ initSorting(self.itemsDom);
+ self.itemsDom.mousedown(function(evt) {
+ evt.stopPropagation();
+ });
+
+ function userIsMember() {
+ return Meteor.user() && Meteor.user().isBoardMember();
+ }
- function userIsMember() {
- return Meteor.user() && Meteor.user().isBoardMember();
- }
+ // Disable sorting if the current user is not a board member
+ self.autorun(() => {
+ const $itemsDom = $(self.itemsDom);
+ if ($itemsDom.data('sortable')) {
+ $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
+ }
+ });
+ },
- // Disable sorting if the current user is not a board member
- self.autorun(() => {
- const $itemsDom = $(self.itemsDom);
- if ($itemsDom.data('sortable')) {
- $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
- }
- });
-});
+ canModifyCard() {
+ return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+ },
+}).register('checklistDetail');
BlazeComponent.extendComponent({
+
addChecklist(event) {
event.preventDefault();
const textarea = this.find('textarea.js-add-checklist-item');
@@ -92,13 +95,38 @@ BlazeComponent.extendComponent({
const checklist = this.currentData().checklist;
if (title) {
- checklist.addItem(title);
+ ChecklistItems.insert({
+ title,
+ checklistId: checklist._id,
+ cardId: checklist.cardId,
+ sort: checklist.itemCount(),
+ });
}
// We keep the form opened, empty it.
textarea.value = '';
textarea.focus();
},
+ canModifyCard() {
+ return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+ },
+
+ deleteChecklist() {
+ const checklist = this.currentData().checklist;
+ if (checklist && checklist._id) {
+ Checklists.remove(checklist._id);
+ this.toggleDeleteDialog.set(false);
+ }
+ },
+
+ deleteItem() {
+ const checklist = this.currentData().checklist;
+ const item = this.currentData().item;
+ if (checklist && item && item._id) {
+ ChecklistItems.remove(item._id);
+ }
+ },
+
editChecklist(event) {
event.preventDefault();
const textarea = this.find('textarea.js-edit-checklist-item');
@@ -107,38 +135,24 @@ BlazeComponent.extendComponent({
checklist.setTitle(title);
},
- canModifyCard() {
- return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
- },
-
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);
- }
+ item.setTitle(title);
},
- deleteChecklist() {
- const checklist = this.currentData().checklist;
- if (checklist && checklist._id) {
- Checklists.remove(checklist._id);
- }
+ onCreated() {
+ this.toggleDeleteDialog = new ReactiveVar(false);
+ this.checklistToDelete = null; //Store data context to pass to checklistDeleteDialog template
},
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) {
+ //If user press enter key inside a form, submit it
+ //Unless the user is also holding down the 'shift' key
+ if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault();
const $form = $(event.currentTarget).closest('form');
$form.find('button[type=submit]').click();
@@ -146,18 +160,50 @@ BlazeComponent.extendComponent({
},
events() {
+ const events = {
+ 'click .toggle-delete-checklist-dialog'(event) {
+ if($(event.target).hasClass('js-delete-checklist')){
+ this.checklistToDelete = this.currentData().checklist; //Store data context
+ }
+ this.toggleDeleteDialog.set(!this.toggleDeleteDialog.get());
+ },
+ };
+
return [{
+ ...events,
'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,
+ 'click .confirm-checklist-delete': this.deleteChecklist,
keydown: this.pressKey,
}];
},
}).register('checklists');
+Template.checklistDeleteDialog.onCreated(() => {
+ const $cardDetails = this.$('.card-details');
+ this.scrollState = { position: $cardDetails.scrollTop(), //save current scroll position
+ top: false, //required for smooth scroll animation
+ };
+ //Callback's purpose is to only prevent scrolling after animation is complete
+ $cardDetails.animate({ scrollTop: 0 }, 500, () => { this.scrollState.top = true; });
+
+ //Prevent scrolling while dialog is open
+ $cardDetails.on('scroll', () => {
+ if(this.scrollState.top) { //If it's already in position, keep it there. Otherwise let animation scroll
+ $cardDetails.scrollTop(0);
+ }
+ });
+});
+
+Template.checklistDeleteDialog.onDestroyed(() => {
+ const $cardDetails = this.$('.card-details');
+ $cardDetails.off('scroll'); //Reactivate scrolling
+ $cardDetails.animate( { scrollTop: this.scrollState.position });
+});
+
Template.itemDetail.helpers({
canModifyCard() {
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
@@ -169,12 +215,12 @@ BlazeComponent.extendComponent({
const checklist = this.currentData().checklist;
const item = this.currentData().item;
if (checklist && item && item._id) {
- checklist.toggleItem(item._id);
+ item.toggleItem();
}
},
events() {
return [{
- 'click .item .check-box': this.toggleItem,
+ 'click .js-checklist-item .check-box': this.toggleItem,
}];
},
}).register('itemDetail');
diff --git a/client/components/cards/checklists.styl b/client/components/cards/checklists.styl
index 77668349..d48c1851 100644
--- a/client/components/cards/checklists.styl
+++ b/client/components/cards/checklists.styl
@@ -26,7 +26,7 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
.title
font-size: 18px
- line-height: 30px
+ line-height: 25px
.checklist-stat
margin: 0 0.5em
@@ -38,34 +38,102 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
.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
- &:hover
- background-color: darken(white, 8%)
-
- .check-box
- margin-top: 5px
- &.is-checked
- border-bottom: 2px solid #3cb500
- border-right: 2px solid #3cb500
-
- .item-title
- flex: 1
- padding-left: 10px;
- &.is-checked
- color: #8c8c8c
- font-style: italic
-
- .js-delete-checklist-item
- @extends .delete-text
- padding: 12px 0 0 0
+.js-confirm-checklist-delete
+ background-color: darken(white, 3%)
+ position: absolute
+ float: left;
+ width: 60%
+ margin-top: 0
+ margin-left: 13%
+ padding-bottom: 2%
+ padding-left: 3%
+ padding-right: 3%
+ z-index: 17
+ border-radius: 3px
+
+ p
+ position: relative
+ margin-top: 3%
+ width: 100%
+ text-align: center
+ span
+ font-weight: bold
+
+ i
+ font-size: 2em
+
+ .js-checklist-delete-buttons
+ position: relative
+ padding: left 2% right 2%
+ .confirm-checklist-delete
+ margin-left: 12%
+ float: left
+ .toggle-delete-checklist-dialog
+ margin-right: 12%
+ float: right
+
+#card-details-overlay
+ top: 0
+ bottom: -600px
+ right: 0
+
+.checklist
+ background: darken(white, 3%)
+
+ &.placeholder
+ background: darken(white, 20%)
+ border-radius: 2px
+
+ &.ui-sortable-helper
+ box-shadow: -2px 2px 8px rgba(0, 0, 0, .3),
+ 0 0 1px rgba(0, 0, 0, .5)
+ transform: rotate(4deg)
+ cursor: grabbing
+
+
+.checklist-item
+ margin: 0 0 0 0.1em
+ line-height: 18px
+ font-size: 1.1em
+ margin-top: 3px
+ display: flex
+ background: darken(white, 3%)
+
+ &.placeholder
+ background: darken(white, 20%)
+ border-radius: 2px
+
+ &.ui-sortable-helper
+ box-shadow: -2px 2px 8px rgba(0, 0, 0, .3),
+ 0 0 1px rgba(0, 0, 0, .5)
+ transform: rotate(4deg)
+ cursor: grabbing
+
+ &:hover
+ background-color: darken(white, 8%)
+
+ .check-box
+ margin: 0.1em 0 0 0;
+ &.is-checked
+ border-bottom: 2px solid #3cb500
+ border-right: 2px solid #3cb500
+
+ .item-title
+ flex: 1
+ padding-left: 10px;
+ &.is-checked
+ color: #8c8c8c
+ font-style: italic
+ & .viewer
+ p
+ margin-bottom: 2px
+
+.js-delete-checklist-item
+ margin: 0 0 0.5em 1.33em
+ @extends .delete-text
+ padding: 12px 0 0 0
- .add-checklist-item
- padding-top: 0.5em
- display: inline-block
+.add-checklist-item
+ margin: 0.2em 0 0.5em 1.33em
+ display: inline-block
diff --git a/client/components/cards/labels.jade b/client/components/cards/labels.jade
index 31bd4d06..6c6efb08 100644
--- a/client/components/cards/labels.jade
+++ b/client/components/cards/labels.jade
@@ -34,4 +34,5 @@ template(name="cardLabelsPopup")
= name
if(isLabelSelected ../_id)
i.card-label-selectable-icon.fa.fa-check
- a.quiet-button.full.js-add-label {{_ 'label-create'}}
+ if currentUser.isBoardAdmin
+ a.quiet-button.full.js-add-label {{_ 'label-create'}}
diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade
index 6fd83386..9fa4dd57 100644
--- a/client/components/cards/minicard.jade
+++ b/client/components/cards/minicard.jade
@@ -6,14 +6,20 @@ template(name="minicard")
.minicard-labels
each labels
.minicard-label(class="card-label-{{color}}" title="{{name}}")
- .minicard-title= title
+ .minicard-title
+ +viewer
+ = title
.dates
if startAt
- .date
- +minicardStartDate
+ .date
+ +minicardStartDate
if dueAt
- .date
- +minicardDueDate
+ .date
+ +minicardDueDate
+ if spentTime
+ .date
+ +cardSpentTime
+
if members
.minicard-members.js-minicard-members
each members
diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl
index a6aad896..d59f1f63 100644
--- a/client/components/cards/minicard.styl
+++ b/client/components/cards/minicard.styl
@@ -77,6 +77,9 @@
height: @width
border-radius: 2px
margin-left: 3px
+ .minicard-title
+ p:last-child
+ margin-bottom: 0
.dates
display: flex;
flex-direction: row;
diff --git a/client/components/forms/forms.styl b/client/components/forms/forms.styl
index 81780a6f..0a905943 100644
--- a/client/components/forms/forms.styl
+++ b/client/components/forms/forms.styl
@@ -633,6 +633,59 @@ button
a, .quiet
color: white
+// Material Design Toggle Switch
+.material-toggle-switch
+ display: flex
+
+.toggle-label
+ position: relative
+ display: block
+ height: 20px
+ width: 44px
+ background-color: #a6a6a6
+ border-radius: 100px
+ cursor: pointer
+ transition: all 0.3s ease
+
+ &:after
+ position: absolute
+ left: -2px
+ top: -3px
+ display: block
+ width: 26px
+ height: 26px
+ border-radius: 100px
+ background-color: #fff
+ box-shadow: 0px 3px 3px rgba(0,0,0,0.05)
+ content: ''
+ transition: all 0.3s ease
+
+ &:active
+ &:after
+ transform: scale(1.15, 0.85)
+
+.toggle-switch:checked ~ .toggle-label
+ background-color: #6fbeb5
+
+ &:after
+ left: 20px
+ background-color: #179588
+
+.toggle-switch:checked:disabled ~ .toggle-label
+ background-color: #d5d5d5
+ pointer-events: none
+
+ &:after
+ background-color: #bcbdbc
+
+.toggle-switch
+ display: none
+
+.toggle-switch-title
+ margin: 0 0.5em
+ display: flex
+
+
@media screen and (max-width: 800px)
.edit-controls,
.add-controls
diff --git a/client/components/import/import.jade b/client/components/import/import.jade
index 5e737cc6..a1fbd83b 100644
--- a/client/components/import/import.jade
+++ b/client/components/import/import.jade
@@ -16,6 +16,7 @@ template(name="importTextarea")
textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
| {{jsonText}}
if isSandstorm
+ h1.warning DANGER !!! THIS DESTROYS YOUR IMPORTED DATA, CAUSES BOARD NOT FOUND ERROR WHEN YOU OPEN THIS GRAIN AGAIN https://github.com/wekan/wekan/issues/1430
p.warning {{_ 'import-sandstorm-warning'}}
input.primary.wide(type="submit" value="{{_ 'import'}}")
diff --git a/client/components/lists/list.jade b/client/components/lists/list.jade
index c959b87f..c02e0dd6 100644
--- a/client/components/lists/list.jade
+++ b/client/components/lists/list.jade
@@ -2,3 +2,7 @@ template(name='list')
.list.js-list(id="js-list-{{_id}}")
+listHeader
+listBody
+
+template(name='miniList')
+ a.mini-list.js-select-list.js-list(id="js-list-{{_id}}")
+ +listHeader
diff --git a/client/components/lists/list.js b/client/components/lists/list.js
index 9c191348..38a87674 100644
--- a/client/components/lists/list.js
+++ b/client/components/lists/list.js
@@ -18,13 +18,18 @@ BlazeComponent.extendComponent({
// callback, we basically solve all issues related to reactive updates. A
// comment below provides further details.
onRendered() {
- const boardComponent = this.parentComponent();
+ const boardComponent = this.parentComponent().parentComponent();
+
+ function userIsMember() {
+ return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+ }
+
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
$cards.sortable({
- connectWith: '.js-minicards',
+ connectWith: '.js-minicards:not(.js-list-full)',
tolerance: 'pointer',
- appendTo: 'body',
+ appendTo: '.board-canvas',
helper(evt, item) {
const helper = item.clone();
if (MultiSelection.isActive()) {
@@ -40,7 +45,6 @@ BlazeComponent.extendComponent({
},
distance: 7,
items: itemsSelector,
- scroll: false,
placeholder: 'minicard-wrapper placeholder',
start(evt, ui) {
ui.placeholder.height(ui.helper.height());
@@ -55,6 +59,7 @@ BlazeComponent.extendComponent({
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
+ const swimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))._id;
// Normally the jquery-ui sortable library moves the dragged DOM element
// to its new position, which disrupts Blaze reactive updates mechanism
@@ -67,21 +72,17 @@ BlazeComponent.extendComponent({
if (MultiSelection.isActive()) {
Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
- card.move(listId, sortIndex.base + i * sortIndex.increment);
+ card.move(swimlaneId, listId, sortIndex.base + i * sortIndex.increment);
});
} else {
const cardDomElement = ui.item.get(0);
const card = Blaze.getData(cardDomElement);
- card.move(listId, sortIndex.base);
+ card.move(swimlaneId, listId, sortIndex.base);
}
boardComponent.setIsDragging(false);
},
});
- function userIsMember() {
- return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
- }
-
// Disable drag-dropping if the current user is not a board member or is comment only
this.autorun(() => {
$cards.sortable('option', 'disabled', !userIsMember());
@@ -114,3 +115,10 @@ BlazeComponent.extendComponent({
});
},
}).register('list');
+
+Template.miniList.events({
+ 'click .js-select-list'() {
+ const listId = this._id;
+ Session.set('currentList', listId);
+ },
+});
diff --git a/client/components/lists/list.styl b/client/components/lists/list.styl
index cf939a6e..fa32ff6d 100644
--- a/client/components/lists/list.styl
+++ b/client/components/lists/list.styl
@@ -9,8 +9,8 @@
// Even if this background color is the same as the body we can't leave it
// transparent, because that won't work during a list drag.
background: darken(white, 13%)
- height: 100%
border-left: 1px solid darken(white, 20%)
+ border-bottom: 1px solid #CCC
padding: 0
float: left
@@ -53,6 +53,9 @@
&.ui-sortable-handle
cursor: grab
+ .list-header-left-icon
+ display: none
+
.list-header-name
display: inline
font-size: 16px
@@ -69,21 +72,30 @@
padding-left: 10px
color: #a6a6a6
- .list-header-menu-icon
+ .list-header-menu
position: absolute
padding: 7px
margin-top: 1px
top: -@padding
right: -@padding
+ .list-header-plus-icon
+ color: #a6a6a6
+ margin-right: 10px
+
+ .highlight
+ color: #ce1414
+
.list-body
- flex: 1
+ flex: 1 1 auto
+ flex-direction: column
display: flex
overflow-y: auto
padding: 5px 11px
.minicards
- flex: 1
+ flex-grow: 1
+ flex-shrink: 0
form
margin-bottom: 9px
@@ -107,3 +119,71 @@
background: #fafafa
color: #222
box-shadow: 0 1px 2px rgba(0,0,0,.2)
+
+#js-wip-limit-edit
+ padding-top: 2%
+
+ p
+ margin-bottom: 0
+
+ input
+ display: inline-block
+
+ .wip-limit-value
+ width: 20%
+ margin-right: 5%
+
+ .wip-limit-error
+ display: none
+
+ .soft-wip-limit
+ margin-right: 8px
+
+ div
+ float: left
+
+@media screen and (max-width: 800px)
+ .mini-list
+ flex: 0 0 60px
+ height: 60px
+ width: 100%
+ border-left: 0px
+ border-bottom: 1px solid darken(white, 20%)
+
+ .list
+ display: block
+ width: 100%
+ border-left: 0px
+
+ &.ui-sortable-helper
+ flex: 0 0 60px
+ height: 60px
+ width: 100%
+ border-left: 0px
+ border-bottom: 1px solid darken(white, 20%)
+
+ .list-header.ui-sortable-handle
+ cursor: grabbing
+
+ &.placeholder
+ flex: 0 0 60px
+ height: 60px
+ width: 100%
+ border-left: 0px
+ border-bottom: 1px solid darken(white, 20%)
+
+ .list-header
+
+ .list-header-left-icon
+ display: inline
+ padding: 7px
+ padding-right: 27px
+ margin-top: 1px
+ top: -@padding
+ left: -@padding
+
+ .list-header-menu-icon
+ position: absolute
+ padding: 7px
+ top: -@padding
+ right: 17px
diff --git a/client/components/lists/listBody.jade b/client/components/lists/listBody.jade
index 01aa7179..32c6b278 100644
--- a/client/components/lists/listBody.jade
+++ b/client/components/lists/listBody.jade
@@ -1,10 +1,10 @@
template(name="listBody")
.list-body.js-perfect-scrollbar
- .minicards.clearfix.js-minicards
+ .minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
if cards.count
+inlinedForm(autoclose=false position="top")
+addCardForm(listId=_id position="top")
- each cards
+ each (cards (idOrNull ../../_id))
a.minicard-wrapper.js-minicard(href=absoluteUrl
class="{{#if cardIsSelected}}is-selected{{/if}}"
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js
index edac5b03..24e5cf5d 100644
--- a/client/components/lists/listBody.js
+++ b/client/components/lists/listBody.js
@@ -40,6 +40,14 @@ BlazeComponent.extendComponent({
console.log("labelIds", labelIds);
console.log("customFields", customFields);
+ const boardId = this.data().board()._id;
+ let swimlaneId = '';
+ const boardView = Meteor.user().profile.boardView;
+ if (boardView === 'board-view-swimlanes')
+ swimlaneId = this.parentComponent().parentComponent().data()._id;
+ else if (boardView === 'board-view-lists')
+ swimlaneId = Swimlanes.findOne({boardId})._id;
+
if (title) {
const _id = Cards.insert({
title,
@@ -49,6 +57,7 @@ BlazeComponent.extendComponent({
listId: this.data()._id,
boardId: this.data().board()._id,
sort: sortIndex,
+ swimlaneId,
});
// In case the filter is active we need to add the newly inserted card in
// the list of exceptions -- cards that are not filtered. Otherwise the
@@ -101,6 +110,22 @@ BlazeComponent.extendComponent({
MultiSelection.toggle(this.currentData()._id);
},
+ idOrNull(swimlaneId) {
+ const currentUser = Meteor.user();
+ if (currentUser.profile.boardView === 'board-view-swimlanes')
+ return swimlaneId;
+ return undefined;
+ },
+
+ canSeeAddCard() {
+ return !this.reachedWipLimit() && Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+ },
+
+ reachedWipLimit() {
+ const list = Template.currentData();
+ return !list.getWipLimit('soft') && list.getWipLimit('enabled') && list.getWipLimit('value') <= list.cards().count();
+ },
+
events() {
return [{
'click .js-minicard': this.clickOnMiniCard,
@@ -246,10 +271,3 @@ BlazeComponent.extendComponent({
});
},
}).register('addCardForm');
-
-
-Template.listBody.helpers({
- canSeeAddCard() {
- return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
- },
-});
diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade
index 11905586..61771449 100644
--- a/client/components/lists/listHeader.jade
+++ b/client/components/lists/listHeader.jade
@@ -3,17 +3,40 @@ template(name="listHeader")
+inlinedForm
+editListTitleForm
else
+ if isMiniScreen
+ if currentList
+ a.list-header-left-icon.fa.fa-angle-left.js-unselect-list
h2.list-header-name(
class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
= title
+ if wipLimit.enabled
+ |&nbsp;(
+ span(class="{{#if reachedWipLimit}}highlight{{/if}}") {{cards.count}}
+ |/#{wipLimit.value})
+
if showCardsCountForList cards.count
= cards.count
- span.lowercase
- | {{_ 'cards'}}
- if currentUser.isBoardMember
+ span
+ | {{_ 'cards-count'}}
+ if isMiniScreen
+ if currentList
+ if isWatching
+ i.list-header-watch-icon.fa.fa-eye
+ div.list-header-menu
+ unless currentUser.isCommentOnly
+ if canSeeAddCard
+ a.js-add-card.fa.fa-plus.list-header-plus-icon
+ a.fa.fa-navicon.js-open-list-menu
+ else
+ a.list-header-menu-icon.fa.fa-angle-right.js-select-list
+ else if currentUser.isBoardMember
if isWatching
i.list-header-watch-icon.fa.fa-eye
- a.list-header-menu-icon.fa.fa-navicon.js-open-list-menu
+ div.list-header-menu
+ unless currentUser.isCommentOnly
+ if canSeeAddCard
+ a.js-add-card.fa.fa-plus.list-header-plus-icon
+ a.fa.fa-navicon.js-open-list-menu
template(name="editListTitleForm")
.list-composer
@@ -28,10 +51,13 @@ template(name="listActionPopup")
unless currentUser.isCommentOnly
hr
ul.pop-over-list
- li: a.js-add-card {{_ 'add-card'}}
if cards.count
li: a.js-select-cards {{_ 'list-select-cards'}}
- hr
+ hr
+ if currentUser.isBoardAdmin
+ ul.pop-over-list
+ li: a.js-set-wip-limit {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
+ hr
ul.pop-over-list
li: a.js-close-list {{_ 'archive-list'}}
hr
@@ -63,3 +89,26 @@ template(name="listDeletePopup")
unless archived
p {{_ "list-delete-suggest-archive"}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+
+template(name="setWipLimitPopup")
+ #js-wip-limit-edit
+ label {{_ 'set-wip-limit-value'}}
+ ul.pop-over-list
+ li: a.js-enable-wip-limit {{_ 'enable-wip-limit'}}
+ if isWipLimitEnabled
+ i.fa.fa-check
+ if isWipLimitEnabled
+ p
+ input.wip-limit-value(type="number" value="{{ wipLimitValue }}" min="1" max="99")
+ input.wip-limit-apply(type="submit" value="{{_ 'apply'}}")
+ input.wip-limit-error
+ p
+ .soft-wip-limit
+ .materialCheckBox(class="{{#if isWipLimitSoft}}is-checked{{/if}}")
+ label {{_ 'soft-wip-limit'}}
+
+template(name="wipLimitErrorPopup")
+ .wip-limit-invalid
+ p {{_ 'wipLimitErrorPopup-dialog-pt1'}}
+ p {{_ 'wipLimitErrorPopup-dialog-pt2'}}
+ button.full.js-back-view(type="submit") {{_ 'cancel'}}
diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js
index 1ad9f9dd..4b6bf196 100644
--- a/client/components/lists/listHeader.js
+++ b/client/components/lists/listHeader.js
@@ -1,4 +1,9 @@
BlazeComponent.extendComponent({
+ canSeeAddCard() {
+ const list = Template.currentData();
+ return !list.getWipLimit('enabled') || list.getWipLimit('soft') || !this.reachedWipLimit();
+ },
+
editTitle(evt) {
evt.preventDefault();
const newTitle = this.childComponents('inlinedForm')[0].getValue().trim();
@@ -17,33 +22,45 @@ BlazeComponent.extendComponent({
return Meteor.user().getLimitToShowCardsCount();
},
+ reachedWipLimit() {
+ const list = Template.currentData();
+ return list.getWipLimit('enabled') && list.getWipLimit('value') <= list.cards().count();
+ },
+
showCardsCountForList(count) {
- return count > this.limitToShowCardsCount();
+ const limit = this.limitToShowCardsCount();
+ return limit > 0 && count > limit;
},
events() {
return [{
'click .js-open-list-menu': Popup.open('listAction'),
+ 'click .js-add-card' (evt) {
+ const listDom = $(evt.target).parents(`#js-list-${this.currentData()._id}`)[0];
+ const listComponent = BlazeComponent.getComponentForElement(listDom);
+ listComponent.openForm({
+ position: 'top',
+ });
+ },
+ 'click .js-unselect-list'() {
+ Session.set('currentList', null);
+ },
submit: this.editTitle,
}];
},
}).register('listHeader');
Template.listActionPopup.helpers({
+ isWipLimitEnabled() {
+ return Template.currentData().getWipLimit('enabled');
+ },
+
isWatching() {
return this.findWatcher(Meteor.userId());
},
});
Template.listActionPopup.events({
- 'click .js-add-card' () {
- const listDom = document.getElementById(`js-list-${this._id}`);
- const listComponent = BlazeComponent.getComponentForElement(listDom);
- listComponent.openForm({
- position: 'top',
- });
- Popup.close();
- },
'click .js-list-subscribe' () {},
'click .js-select-cards' () {
const cardIds = this.allCards().map((card) => card._id);
@@ -62,9 +79,63 @@ Template.listActionPopup.events({
this.archive();
Popup.close();
},
+ 'click .js-set-wip-limit': Popup.open('setWipLimit'),
'click .js-more': Popup.open('listMore'),
});
+BlazeComponent.extendComponent({
+ applyWipLimit() {
+ const list = Template.currentData();
+ const limit = parseInt(Template.instance().$('.wip-limit-value').val(), 10);
+
+ if(limit < list.cards().count() && !list.getWipLimit('soft')){
+ Template.instance().$('.wip-limit-error').click();
+ } else {
+ Meteor.call('applyWipLimit', list._id, limit);
+ Popup.back();
+ }
+ },
+
+ enableSoftLimit() {
+ const list = Template.currentData();
+
+ if(list.getWipLimit('soft') && list.getWipLimit('value') < list.cards().count()){
+ list.setWipLimit(list.cards().count());
+ }
+ Meteor.call('enableSoftLimit', Template.currentData()._id);
+ },
+
+ enableWipLimit() {
+ const list = Template.currentData();
+ // Prevent user from using previously stored wipLimit.value if it is less than the current number of cards in the list
+ if(!list.getWipLimit('enabled') && list.getWipLimit('value') < list.cards().count()){
+ list.setWipLimit(list.cards().count());
+ }
+ Meteor.call('enableWipLimit', list._id);
+ },
+
+ isWipLimitSoft() {
+ return Template.currentData().getWipLimit('soft');
+ },
+
+ isWipLimitEnabled() {
+ return Template.currentData().getWipLimit('enabled');
+ },
+
+ wipLimitValue(){
+ return Template.currentData().getWipLimit('value');
+ },
+
+ events() {
+ return [{
+ 'click .js-enable-wip-limit': this.enableWipLimit,
+ 'click .wip-limit-apply': this.applyWipLimit,
+ 'click .wip-limit-error': Popup.open('wipLimitError'),
+ 'click .materialCheckBox': this.enableSoftLimit,
+ }];
+ },
+}).register('setWipLimitPopup');
+
Template.listMorePopup.events({
'click .js-delete': Popup.afterConfirm('listDelete', function () {
Popup.close();
diff --git a/client/components/main/editor.jade b/client/components/main/editor.jade
index f10d64a1..31f533e6 100644
--- a/client/components/main/editor.jade
+++ b/client/components/main/editor.jade
@@ -9,6 +9,5 @@ template(name="editor")
template(name="viewer")
.viewer
+mentions
- +emoji
- +markdown
- {{> UI.contentBlock }}
+ +markdown
+ {{> UI.contentBlock }}
diff --git a/client/components/main/editor.js b/client/components/main/editor.js
index 5987b772..888fbe00 100755
--- a/client/components/main/editor.js
+++ b/client/components/main/editor.js
@@ -4,25 +4,6 @@ Template.editor.onRendered(() => {
autosize($textarea);
$textarea.escapeableTextComplete([
- // Emoji
- {
- match: /\B:([\-+\w]*)$/,
- search(term, callback) {
- callback(Emoji.values.map((emoji) => {
- return emoji.includes(term) ? emoji : null;
- }).filter(Boolean));
- },
- template(value) {
- const imgSrc = Emoji.baseImagePath + value;
- const image = `<img src="${imgSrc}.png" />`;
- return image + value;
- },
- replace(value) {
- return `:${value}:`;
- },
- index: 1,
- },
-
// User mentions
{
match: /\B@([\w.]*)$/,
@@ -47,7 +28,7 @@ Template.editor.onRendered(() => {
import sanitizeXss from 'xss';
// XXX I believe we should compute a HTML rendered field on the server that
-// would handle markdown, emoji and user mentions. We can simply have two
+// would handle markdown and user mentions. We can simply have two
// fields, one source, and one compiled version (in HTML) and send only the
// compiled version to most users -- who don't need to edit.
// In the meantime, all the transformation are done on the client using the
diff --git a/client/components/main/header.jade b/client/components/main/header.jade
index bd0af880..dd071b3e 100644
--- a/client/components/main/header.jade
+++ b/client/components/main/header.jade
@@ -7,19 +7,31 @@ template(name="header")
unless isSandstorm
if currentUser
#header-quick-access(class=currentBoard.colorClass)
- ul
- li
- a(href="{{pathFor 'home'}}")
- span.fa.fa-home
- | {{_ 'all-boards'}}
- each currentUser.starredBoards
- li.separator -
- li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
- a(href="{{pathFor 'board' id=_id slug=slug}}")
- = title
- else
- li.current {{_ 'quick-access-description'}}
+ if isMiniScreen
+ ul
+ li
+ a(href="{{pathFor 'home'}}")
+ span.fa.fa-home
+ if currentList
+ each currentBoard.lists
+ li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}")
+ a.js-select-list
+ = title
+ #header-new-board-icon
+ else
+ ul
+ li
+ a(href="{{pathFor 'home'}}")
+ span.fa.fa-home
+ | {{_ 'all-boards'}}
+ each currentUser.starredBoards
+ li.separator -
+ li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
+ a(href="{{pathFor 'board' id=_id slug=slug}}")
+ = title
+ else
+ li.current {{_ 'quick-access-description'}}
a#header-new-board-icon.js-create-board
i.fa.fa-plus(title="Create a new board")
@@ -49,6 +61,14 @@ template(name="header")
if appIsOffline
+offlineWarning
+ if currentUser.isBoardMember
+ if hasAnnouncement
+ .announcement
+ p
+ i.fa.fa-bullhorn
+ | #{announcement}
+ i.fa.fa-times-circle.js-close-announcement
+
template(name="offlineWarning")
.offline-warning
p
diff --git a/client/components/main/header.js b/client/components/main/header.js
index 49acbfef..7fbc5716 100644
--- a/client/components/main/header.js
+++ b/client/components/main/header.js
@@ -1,3 +1,6 @@
+Meteor.subscribe('user-admin');
+Meteor.subscribe('boards');
+
Template.header.helpers({
wrappedHeader() {
return !Session.get('currentBoard');
@@ -10,8 +13,26 @@ Template.header.helpers({
appIsOffline() {
return !Meteor.status().connected;
},
+
+ hasAnnouncement() {
+ const announcements = Announcements.findOne();
+ return announcements && announcements.enabled;
+ },
+
+ announcement() {
+ $('.announcement').show();
+ const announcements = Announcements.findOne();
+ return announcements && announcements.body;
+ },
});
Template.header.events({
'click .js-create-board': Popup.open('headerBarCreateBoard'),
+ 'click .js-close-announcement'() {
+ $('.announcement').hide();
+ },
+ 'click .js-select-list'() {
+ Session.set('currentList', this._id);
+ Session.set('currentCard', null);
+ },
});
diff --git a/client/components/main/header.styl b/client/components/main/header.styl
index 0e35d38a..f9455f8e 100644
--- a/client/components/main/header.styl
+++ b/client/components/main/header.styl
@@ -4,6 +4,7 @@
color: white
transition: background-color 0.4s
background: #2980B9
+ z-index: 17
#header-main-bar
height: 40px
@@ -28,13 +29,12 @@
font-size: 0.9em
margin-right: 10px
-
.wekan-logo
- display: block
- margin: 3px auto 0
+ margin: 3px auto auto
width: 97px
opacity: 0.6
transition: opacity 0.15s
+ float: right
&:hover
opacity: 0.9
@@ -99,6 +99,7 @@
height: 28px
font-size: 12px
display: flex
+ z-index: 17
#header-user-bar,
#header-new-board-icon,
@@ -191,12 +192,35 @@
bottom: 0px
ul
- width: calc(100% - 150px)
+ width: calc(100% - 60px)
overflow: ellipsis
+ padding: 10px
+ margin: -10px
li
- height: 28px
+ height: 100%
+ padding: 12px 0px
+ margin: -10px 0px
+
+ a
+ height: 100%
+ padding: 12px 10px
+ margin: -10px 0px
+
+ .fa-home
+ font-size: 26px
+ margin-top: -2px
+
+ #header-new-board-icon
+ display: none
+
+ #header-user-bar
+ position: absolute
+ right: 0px
+ padding: 10px
+ margin: -10px
+.announcement,
.offline-warning
width: 100%
text-align: center
diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade
index a51a35e0..4d76aabb 100644
--- a/client/components/main/layouts.jade
+++ b/client/components/main/layouts.jade
@@ -8,6 +8,7 @@ head
difficult to do that cleanly with Blaze -- at least without adding extra
packages.
link(rel="shortcut icon" href="/wekan-favicon.png")
+ link(rel="apple-touch-icon" href="/wekan-favicon.png")
link(rel="manifest" href="/wekan-manifest.json")
template(name="userFormsLayout")
diff --git a/client/components/main/layouts.js b/client/components/main/layouts.js
index 1936d7aa..f12718a7 100644
--- a/client/components/main/layouts.js
+++ b/client/components/main/layouts.js
@@ -1,7 +1,3 @@
-Meteor.subscribe('boards');
-Meteor.subscribe('setting');
-Meteor.subscribe('user-admin');
-
BlazeLayout.setRoot('body');
const i18nTagToT9n = (i18nTag) => {
@@ -21,10 +17,14 @@ Template.userFormsLayout.onRendered(() => {
Template.userFormsLayout.helpers({
languages() {
return _.map(TAPi18n.getLanguages(), (lang, code) => {
- return {
- tag: code,
- name: lang.name === 'br' ? 'Brezhoneg' : lang.name,
- };
+ const tag = code;
+ let name = lang.name;
+ if (lang.name === 'br') {
+ name = 'Brezhoneg';
+ } else if (lang.name === 'ig') {
+ name = 'Igbo';
+ }
+ return { tag, name };
}).sort(function(a, b) {
if (a.name === b.name) {
return 0;
diff --git a/client/components/main/layouts.styl b/client/components/main/layouts.styl
index 734a5e83..a79ff337 100644
--- a/client/components/main/layouts.styl
+++ b/client/components/main/layouts.styl
@@ -270,11 +270,6 @@ kbd
.grabbing
cursor: grabbing
-.emoji
- height: 18px
- width: 18px
- vertical-align: text-bottom
-
// Implement a thiner close icon as suggested in
// https://github.com/FortAwesome/Font-Awesome/issues/1540#issuecomment-68689950
.fa.fa-times-thin:before
diff --git a/client/components/settings/invitationCode.js b/client/components/settings/invitationCode.js
index a403d8ab..c02f860f 100644
--- a/client/components/settings/invitationCode.js
+++ b/client/components/settings/invitationCode.js
@@ -1,6 +1,6 @@
Template.invitationCode.onRendered(() => {
const setting = Settings.findOne();
- if (!setting || !setting.disableRegistration) {
+ if (setting || setting.disableRegistration) {
$('#invitationcode').hide();
}
});
diff --git a/client/components/settings/peopleBody.jade b/client/components/settings/peopleBody.jade
new file mode 100644
index 00000000..a3506a24
--- /dev/null
+++ b/client/components/settings/peopleBody.jade
@@ -0,0 +1,90 @@
+template(name="people")
+ .setting-content
+ unless currentUser.isAdmin
+ | {{_ 'error-notAuthorized'}}
+ else
+ .content-title
+ span {{_ 'people'}}
+ .content-body
+ .side-menu
+ ul
+ li.active
+ a.js-setting-menu(data-id="people-setting") {{_ 'people'}}
+ .main-body
+ if loading.get
+ +spinner
+ else if people.get
+ +peopleGeneral
+
+template(name="peopleGeneral")
+ table
+ tbody
+ tr
+ th {{_ 'username'}}
+ th {{_ 'fullname'}}
+ th {{_ 'admin'}}
+ th {{_ 'email'}}
+ th {{_ 'verified'}}
+ th {{_ 'createdAt'}}
+ th {{_ 'active'}}
+ th
+ each user in peopleList
+ +peopleRow(userId=user._id)
+
+template(name="peopleRow")
+ tr
+ td.username {{ userData.username }}
+ td {{ userData.profile.fullname }}
+ td
+ if userData.isAdmin
+ | {{_ 'yes'}}
+ else
+ | {{_ 'no'}}
+ td {{ userData.emails.[0].address }}
+ td
+ if userData.emails.[0].verified
+ | {{_ 'yes'}}
+ else
+ | {{_ 'no'}}
+ td {{ moment userData.createdAt 'LLL' }}
+ td
+ if userData.loginDisabled
+ | {{_ 'no'}}
+ else
+ | {{_ 'yes'}}
+ td
+ a.edit-user
+ | {{_ 'edit'}}
+
+template(name="editUserPopup")
+ form
+ label.hide.userId(type="text" value=user._id)
+ label
+ | {{_ 'fullname'}}
+ input.js-profile-fullname(type="text" value=user.profile.fullname autofocus)
+ label
+ | {{_ 'username'}}
+ span.error.hide.username-taken
+ | {{_ 'error-username-taken'}}
+ input.js-profile-username(type="text" value=user.username)
+ label
+ | {{_ 'email'}}
+ span.error.hide.email-taken
+ | {{_ 'error-email-taken'}}
+ input.js-profile-email(type="email" value="{{user.emails.[0].address}}")
+ label
+ | {{_ 'admin'}}
+ select.select-role.js-profile-isadmin
+ option(value="false") {{_ 'no'}}
+ option(value="true" selected="{{user.isAdmin}}") {{_ 'yes'}}
+ label
+ | {{_ 'active'}}
+ select.select-active.js-profile-isactive
+ option(value="false") {{_ 'yes'}}
+ option(value="true" selected="{{user.loginDisabled}}") {{_ 'no'}}
+ hr
+ label
+ | {{_ 'password'}}
+ input.js-profile-password(type="password")
+
+ input.primary.wide(type="submit" value="{{_ 'save'}}")
diff --git a/client/components/settings/peopleBody.js b/client/components/settings/peopleBody.js
new file mode 100644
index 00000000..7cc992f2
--- /dev/null
+++ b/client/components/settings/peopleBody.js
@@ -0,0 +1,158 @@
+const usersPerPage = 25;
+
+BlazeComponent.extendComponent({
+ mixins() {
+ return [Mixins.InfiniteScrolling];
+ },
+ onCreated() {
+ this.error = new ReactiveVar('');
+ this.loading = new ReactiveVar(false);
+ this.people = new ReactiveVar(true);
+
+ this.page = new ReactiveVar(1);
+ this.loadNextPageLocked = false;
+ this.callFirstWith(null, 'resetNextPeak');
+ this.autorun(() => {
+ const limit = this.page.get() * usersPerPage;
+
+ this.subscribe('people', limit, () => {
+ this.loadNextPageLocked = false;
+ const nextPeakBefore = this.callFirstWith(null, 'getNextPeak');
+ this.calculateNextPeak();
+ const nextPeakAfter = this.callFirstWith(null, 'getNextPeak');
+ if (nextPeakBefore === nextPeakAfter) {
+ this.callFirstWith(null, 'resetNextPeak');
+ }
+ });
+ });
+ },
+ loadNextPage() {
+ if (this.loadNextPageLocked === false) {
+ this.page.set(this.page.get() + 1);
+ this.loadNextPageLocked = true;
+ }
+ },
+ calculateNextPeak() {
+ const element = this.find('.main-body');
+ if (element) {
+ const altitude = element.scrollHeight;
+ this.callFirstWith(this, 'setNextPeak', altitude);
+ }
+ },
+ reachNextPeak() {
+ this.loadNextPage();
+ },
+ setError(error) {
+ this.error.set(error);
+ },
+ setLoading(w) {
+ this.loading.set(w);
+ },
+ peopleList() {
+ return Users.find({}, {
+ fields: {_id: true},
+ });
+ },
+}).register('people');
+
+Template.peopleRow.helpers({
+ userData() {
+ const userCollection = this.esSearch ? ESSearchResults : Users;
+ return userCollection.findOne(this.userId);
+ },
+});
+
+Template.editUserPopup.helpers({
+ user() {
+ return Users.findOne(this.userId);
+ },
+});
+
+BlazeComponent.extendComponent({
+ onCreated() {
+ },
+ user() {
+ return Users.findOne(this.userId);
+ },
+ events() {
+ return [{
+ 'click a.edit-user': Popup.open('editUser'),
+ }];
+ },
+}).register('peopleRow');
+
+Template.editUserPopup.events({
+ submit(evt, tpl) {
+ evt.preventDefault();
+ const user = Users.findOne(this.userId);
+ const fullname = tpl.find('.js-profile-fullname').value.trim();
+ const username = tpl.find('.js-profile-username').value.trim();
+ const password = tpl.find('.js-profile-password').value;
+ const isAdmin = tpl.find('.js-profile-isadmin').value.trim();
+ const isActive = tpl.find('.js-profile-isactive').value.trim();
+ const email = tpl.find('.js-profile-email').value.trim();
+
+ const isChangePassword = password.length > 0;
+ const isChangeUserName = username !== user.username;
+ const isChangeEmail = email.toLowerCase() !== user.emails[0].address.toLowerCase();
+
+ Users.update(this.userId, {
+ $set: {
+ 'profile.fullname': fullname,
+ 'isAdmin': isAdmin === 'true',
+ 'loginDisabled': isActive === 'true',
+ },
+ });
+
+ if(isChangePassword){
+ Meteor.call('setPassword', password, this.userId);
+ }
+
+ if (isChangeUserName && isChangeEmail) {
+ Meteor.call('setUsernameAndEmail', username, email.toLowerCase(), this.userId, function (error) {
+ const usernameMessageElement = tpl.$('.username-taken');
+ const emailMessageElement = tpl.$('.email-taken');
+ if (error) {
+ const errorElement = error.error;
+ if (errorElement === 'username-already-taken') {
+ usernameMessageElement.show();
+ emailMessageElement.hide();
+ } else if (errorElement === 'email-already-taken') {
+ usernameMessageElement.hide();
+ emailMessageElement.show();
+ }
+ } else {
+ usernameMessageElement.hide();
+ emailMessageElement.hide();
+ Popup.close();
+ }
+ });
+ } else if (isChangeUserName) {
+ Meteor.call('setUsername', username, this.userId, function (error) {
+ const usernameMessageElement = tpl.$('.username-taken');
+ if (error) {
+ const errorElement = error.error;
+ if (errorElement === 'username-already-taken') {
+ usernameMessageElement.show();
+ }
+ } else {
+ usernameMessageElement.hide();
+ Popup.close();
+ }
+ });
+ } else if (isChangeEmail) {
+ Meteor.call('setEmail', email.toLowerCase(), this.userId, function (error) {
+ const emailMessageElement = tpl.$('.email-taken');
+ if (error) {
+ const errorElement = error.error;
+ if (errorElement === 'email-already-taken') {
+ emailMessageElement.show();
+ }
+ } else {
+ emailMessageElement.hide();
+ Popup.close();
+ }
+ });
+ } else Popup.close();
+ },
+});
diff --git a/client/components/settings/peopleBody.styl b/client/components/settings/peopleBody.styl
new file mode 100644
index 00000000..84db44a7
--- /dev/null
+++ b/client/components/settings/peopleBody.styl
@@ -0,0 +1,15 @@
+.main-body
+ overflow: scroll;
+
+table
+ border-collapse: collapse;
+ width: 100%;
+ color: #000;
+
+ td, th
+ border: 1px solid #d2d0d0;
+ text-align: left;
+ padding: 8px;
+
+ tr:nth-child(even)
+ background-color: #dddddd;
diff --git a/client/components/settings/settingBody.jade b/client/components/settings/settingBody.jade
index 5864efd5..5bc7972d 100644
--- a/client/components/settings/settingBody.jade
+++ b/client/components/settings/settingBody.jade
@@ -14,6 +14,8 @@ template(name="setting")
a.js-setting-menu(data-id="email-setting") {{_ 'email'}}
li
a.js-setting-menu(data-id="account-setting") {{_ 'accounts'}}
+ li
+ a.js-setting-menu(data-id="announcement-setting") {{_ 'admin-announcement'}}
.main-body
if loading.get
+spinner
@@ -23,6 +25,8 @@ template(name="setting")
+email
else if accountSetting.get
+accountSettings
+ else if announcementSetting.get
+ +announcementSettings
template(name="general")
ul#registration-setting.setting-detail
@@ -85,9 +89,12 @@ template(name='email')
li
button.js-save.primary {{_ 'save'}}
+ li
+ button.js-send-smtp-test-email.primary {{_ 'send-smtp-test'}}
+
template(name='accountSettings')
ul#account-setting.setting-detail
- li.smtp-form
+ li.accounts-form
.title {{_ 'accounts-allowEmailChange'}}
.form-group.flex
input.form-control#accounts-allowEmailChange(type="radio" name="allowEmailChange" value="true" checked="{{#if allowEmailChange}}checked{{/if}}")
@@ -95,4 +102,28 @@ template(name='accountSettings')
input.form-control#accounts-allowEmailChange(type="radio" name="allowEmailChange" value="false" checked="{{#unless allowEmailChange}}checked{{/unless}}")
span {{_ 'no'}}
li
+ li.accounts-form
+ .title {{_ 'accounts-allowUserNameChange'}}
+ .form-group.flex
+ input.form-control#accounts-allowUserNameChange(type="radio" name="allowUserNameChange" value="true" checked="{{#if allowUserNameChange}}checked{{/if}}")
+ span {{_ 'yes'}}
+ input.form-control#accounts-allowUserNameChange(type="radio" name="allowUserNameChange" value="false" checked="{{#unless allowUserNameChange}}checked{{/unless}}")
+ span {{_ 'no'}}
+ li
button.js-accounts-save.primary {{_ 'save'}}
+
+template(name='announcementSettings')
+ ul#announcement-setting.setting-detail
+ li
+ a.flex.js-toggle-activemessage
+ .materialCheckBox(class="{{#if currentSetting.enabled}}is-checked{{/if}}")
+
+ span {{_ 'admin-announcement-active'}}
+ li
+ .admin-announcement(class="{{#if currentSetting.enabled}}{{else}}hide{{/if}}")
+ ul
+ li
+ .title {{_ 'admin-announcement-title'}}
+ textarea#admin-announcement.form-control= currentSetting.body
+ li
+ button.js-announcement-save.primary {{_ 'save'}}
diff --git a/client/components/settings/settingBody.js b/client/components/settings/settingBody.js
index a2993426..7230d893 100644
--- a/client/components/settings/settingBody.js
+++ b/client/components/settings/settingBody.js
@@ -1,7 +1,3 @@
-Meteor.subscribe('setting');
-Meteor.subscribe('mailServer');
-Meteor.subscribe('accountSettings');
-
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
@@ -9,6 +5,12 @@ BlazeComponent.extendComponent({
this.generalSetting = new ReactiveVar(true);
this.emailSetting = new ReactiveVar(false);
this.accountSetting = new ReactiveVar(false);
+ this.announcementSetting = new ReactiveVar(false);
+
+ Meteor.subscribe('setting');
+ Meteor.subscribe('mailServer');
+ Meteor.subscribe('accountSettings');
+ Meteor.subscribe('announcements');
},
setError(error) {
@@ -21,7 +23,7 @@ BlazeComponent.extendComponent({
checkField(selector) {
const value = $(selector).val();
- if(!value || value.trim() === ''){
+ if (!value || value.trim() === '') {
$(selector).parents('li.smtp-form').addClass('has-error');
throw Error('blank field');
} else {
@@ -29,7 +31,7 @@ BlazeComponent.extendComponent({
}
},
- currentSetting(){
+ currentSetting() {
return Settings.findOne();
},
@@ -42,35 +44,36 @@ BlazeComponent.extendComponent({
sort: ['title'],
});
},
- toggleRegistration(){
+ toggleRegistration() {
this.setLoading(true);
const registrationClosed = this.currentSetting().disableRegistration;
- Settings.update(Settings.findOne()._id, {$set:{disableRegistration: !registrationClosed}});
+ Settings.update(Settings.findOne()._id, {$set: {disableRegistration: !registrationClosed}});
this.setLoading(false);
- if(registrationClosed){
+ if (registrationClosed) {
$('.invite-people').slideUp();
- }else{
+ } else {
$('.invite-people').slideDown();
}
},
- toggleTLS(){
+ toggleTLS() {
$('#mail-server-tls').toggleClass('is-checked');
},
- switchMenu(event){
+ switchMenu(event) {
const target = $(event.target);
- if(!target.hasClass('active')){
+ if (!target.hasClass('active')) {
$('.side-menu li.active').removeClass('active');
target.parent().addClass('active');
const targetID = target.data('id');
this.generalSetting.set('registration-setting' === targetID);
this.emailSetting.set('email-setting' === targetID);
this.accountSetting.set('account-setting' === targetID);
+ this.announcementSetting.set('announcement-setting' === targetID);
}
},
- checkBoard(event){
+ checkBoard(event) {
let target = $(event.target);
- if(!target.hasClass('js-toggle-board-choose')){
+ if (!target.hasClass('js-toggle-board-choose')) {
target = target.parent();
}
const checkboxId = target.attr('id');
@@ -78,7 +81,7 @@ BlazeComponent.extendComponent({
$(`#${checkboxId}`).toggleClass('is-checked');
},
- inviteThroughEmail(){
+ inviteThroughEmail() {
const emails = $('#email-to-invite').val().trim().split('\n').join(',').split(',');
const boardsToInvite = [];
$('.js-toggle-board-choose .materialCheckBox.is-checked').each(function () {
@@ -101,19 +104,23 @@ BlazeComponent.extendComponent({
}
},
- saveMailServerInfo(){
+ saveMailServerInfo() {
this.setLoading(true);
$('li').removeClass('has-error');
- try{
+ try {
const host = this.checkField('#mail-server-host');
const port = this.checkField('#mail-server-port');
const username = $('#mail-server-username').val().trim();
const password = $('#mail-server-password').val().trim();
const from = this.checkField('#mail-server-from');
const tls = $('#mail-server-tls.is-checked').length > 0;
- Settings.update(Settings.findOne()._id, {$set:{'mailServer.host':host, 'mailServer.port': port, 'mailServer.username': username,
- 'mailServer.password': password, 'mailServer.enableTLS': tls, 'mailServer.from': from}});
+ Settings.update(Settings.findOne()._id, {
+ $set: {
+ 'mailServer.host': host, 'mailServer.port': port, 'mailServer.username': username,
+ 'mailServer.password': password, 'mailServer.enableTLS': tls, 'mailServer.from': from,
+ },
+ });
} catch (e) {
return;
} finally {
@@ -122,7 +129,23 @@ BlazeComponent.extendComponent({
},
- events(){
+ sendSMTPTestEmail() {
+ Meteor.call('sendSMTPTestEmail', (err, ret) => {
+ if (!err && ret) { /* eslint-disable no-console */
+ const message = `${TAPi18n.__(ret.message)}: ${ret.email}`;
+ console.log(message);
+ alert(message);
+ } else {
+ const reason = err.reason || '';
+ const message = `${TAPi18n.__(err.error)}\n${reason}`;
+ console.log(message, err);
+ alert(message);
+ }
+ /* eslint-enable no-console */
+ });
+ },
+
+ events() {
return [{
'click a.js-toggle-registration': this.toggleRegistration,
'click a.js-toggle-tls': this.toggleTLS,
@@ -130,25 +153,76 @@ BlazeComponent.extendComponent({
'click a.js-toggle-board-choose': this.checkBoard,
'click button.js-email-invite': this.inviteThroughEmail,
'click button.js-save': this.saveMailServerInfo,
+ 'click button.js-send-smtp-test-email': this.sendSMTPTestEmail,
}];
},
}).register('setting');
BlazeComponent.extendComponent({
- saveAllowEmailChange() {
+
+ saveAccountsChange() {
const allowEmailChange = ($('input[name=allowEmailChange]:checked').val() === 'true');
+ const allowUserNameChange = ($('input[name=allowUserNameChange]:checked').val() === 'true');
AccountSettings.update('accounts-allowEmailChange', {
- $set: { 'booleanValue': allowEmailChange },
+ $set: {'booleanValue': allowEmailChange},
+ });
+ AccountSettings.update('accounts-allowUserNameChange', {
+ $set: {'booleanValue': allowUserNameChange},
});
},
allowEmailChange() {
return AccountSettings.findOne('accounts-allowEmailChange').booleanValue;
},
+ allowUserNameChange() {
+ return AccountSettings.findOne('accounts-allowUserNameChange').booleanValue;
+ },
events() {
return [{
- 'click button.js-accounts-save': this.saveAllowEmailChange,
+ 'click button.js-accounts-save': this.saveAccountsChange,
}];
},
}).register('accountSettings');
+
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.loading = new ReactiveVar(false);
+ },
+
+ setLoading(w) {
+ this.loading.set(w);
+ },
+
+ currentSetting() {
+ return Announcements.findOne();
+ },
+
+ saveMessage() {
+ const message = $('#admin-announcement').val().trim();
+ Announcements.update(Announcements.findOne()._id, {
+ $set: {'body': message},
+ });
+ },
+
+ toggleActive() {
+ this.setLoading(true);
+ const isActive = this.currentSetting().enabled;
+ Announcements.update(Announcements.findOne()._id, {
+ $set: {'enabled': !isActive},
+ });
+ this.setLoading(false);
+ if (isActive) {
+ $('.admin-announcement').slideUp();
+ } else {
+ $('.admin-announcement').slideDown();
+ }
+ },
+
+ events() {
+ return [{
+ 'click a.js-toggle-activemessage': this.toggleActive,
+ 'click button.js-announcement-save': this.saveMessage,
+ }];
+ },
+}).register('announcementSettings');
diff --git a/client/components/settings/settingBody.styl b/client/components/settings/settingBody.styl
index 118d364c..fec64cee 100644
--- a/client/components/settings/settingBody.styl
+++ b/client/components/settings/settingBody.styl
@@ -61,10 +61,11 @@
.is-checked
border-bottom: 2px solid #2980b9;
border-right: 2px solid #2980b9;
-
- span
+
+ span
padding: 0 0.5rem
-
+
+ .admin-announcement,
.invite-people
padding-left 20px;
li
diff --git a/client/components/settings/settingHeader.jade b/client/components/settings/settingHeader.jade
index c22cf5c6..c2d4db3a 100644
--- a/client/components/settings/settingHeader.jade
+++ b/client/components/settings/settingHeader.jade
@@ -9,13 +9,14 @@ template(name="settingHeaderBar")
a.setting-header-btn.settings(href="{{pathFor 'setting'}}")
i.fa(class="fa-cog")
span {{_ 'settings'}}
+
+ a.setting-header-btn.people(href="{{pathFor 'people'}}")
+ i.fa(class="fa-users")
+ span {{_ 'people'}}
+
a.setting-header-btn.informations(href="{{pathFor 'information'}}")
i.fa(class="fa-info-circle")
span {{_ 'info'}}
-//TODO
-// a.setting-header-btn.people
-// i.fa(class="fa-users")
-// span {{_ 'people'}}
else
a.setting-header-btn.js-log-in(
diff --git a/client/components/settings/settingHeader.styl b/client/components/settings/settingHeader.styl
index 995ed26d..3699f180 100644
--- a/client/components/settings/settingHeader.styl
+++ b/client/components/settings/settingHeader.styl
@@ -22,4 +22,4 @@
+ span
display: inline-block
margin-top: 1px
- margin-right: 10px \ No newline at end of file
+ margin-right: 10px
diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade
index 6045b371..6085c2ad 100644
--- a/client/components/sidebar/sidebar.jade
+++ b/client/components/sidebar/sidebar.jade
@@ -82,23 +82,27 @@ template(name="memberPopup")
ul.pop-over-list
li
a.js-filter-member {{_ 'filter-cards'}}
- if currentUser.isBoardAdmin
- unless isSandstorm
+ unless isSandstorm
+ if currentUser.isBoardAdmin
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'}}
+ li
+ if $eq currentUser._id userId
+ a.js-leave-member {{_ 'leave-board'}}
+ else if currentUser.isBoardAdmin
+ a.js-remove-member {{_ 'remove-from-board'}}
template(name="removeMemberPopup")
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="leaveBoardPopup")
+ p {{_ 'leave-board-pop' boardTitle=board.title}}
+ button.js-confirm.negate.full(type="submit") {{_ 'leave-board'}}
+
template(name="addMemberPopup")
.js-search-member
+esInput(index="users")
diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js
index 59a2b42c..5a9de74b 100644
--- a/client/components/sidebar/sidebar.js
+++ b/client/components/sidebar/sidebar.js
@@ -4,6 +4,7 @@ const defaultView = 'home';
const viewTitles = {
filter: 'filter-cards',
+ search: 'search-cards',
multiselection: 'multi-selection',
customFields: 'custom-fields',
archives: 'archives',
@@ -154,15 +155,13 @@ Template.memberPopup.events({
Boards.findOne(boardId).removeMember(memberId);
Popup.close();
}),
- 'click .js-leave-member'() {
+ 'click .js-leave-member': Popup.afterConfirm('leaveBoard', () => {
const boardId = Session.get('currentBoard');
- Meteor.call('quitBoard', boardId, (err, ret) => {
- if (!ret && ret) {
- Popup.close();
- FlowRouter.go('home');
- }
+ Meteor.call('quitBoard', boardId, () => {
+ Popup.close();
+ FlowRouter.go('home');
});
- },
+ }),
});
Template.removeMemberPopup.helpers({
@@ -174,6 +173,12 @@ Template.removeMemberPopup.helpers({
},
});
+Template.leaveBoardPopup.helpers({
+ board() {
+ return Boards.findOne(Session.get('currentBoard'));
+ },
+});
+
Template.membersWidget.helpers({
isInvited() {
const user = Meteor.user();
diff --git a/client/components/sidebar/sidebarArchives.jade b/client/components/sidebar/sidebarArchives.jade
index aa14fc30..ee6cac01 100644
--- a/client/components/sidebar/sidebarArchives.jade
+++ b/client/components/sidebar/sidebarArchives.jade
@@ -25,3 +25,14 @@ template(name="archivesSidebar")
= title
else
li.no-items-message {{_ 'no-archived-lists'}}
+
+ +tabContent(slug="swimlanes")
+ ul.archived-lists
+ each archivedSwimlanes
+ li.archived-lists-item
+ if currentUser.isBoardMember
+ button.js-restore-swimlane
+ i.fa.fa-undo
+ = title
+ else
+ li.no-items-message {{_ 'no-archived-swimlanes'}}
diff --git a/client/components/sidebar/sidebarArchives.js b/client/components/sidebar/sidebarArchives.js
index c8196f23..6102bf11 100644
--- a/client/components/sidebar/sidebarArchives.js
+++ b/client/components/sidebar/sidebarArchives.js
@@ -3,6 +3,7 @@ BlazeComponent.extendComponent({
return [
{ name: TAPi18n.__('cards'), slug: 'cards' },
{ name: TAPi18n.__('lists'), slug: 'lists' },
+ { name: TAPi18n.__('swimlanes'), slug: 'swimlanes' },
];
},
@@ -20,6 +21,13 @@ BlazeComponent.extendComponent({
});
},
+ archivedSwimlanes() {
+ return Swimlanes.find({
+ archived: true,
+ boardId: Session.get('currentBoard'),
+ });
+ },
+
cardIsInArchivedList() {
return this.currentData().list().archived;
},
@@ -32,7 +40,9 @@ BlazeComponent.extendComponent({
return [{
'click .js-restore-card'() {
const card = this.currentData();
- card.restore();
+ if(card.canBeRestored()){
+ card.restore();
+ }
},
'click .js-delete-card': Popup.afterConfirm('cardDelete', function() {
const cardId = this._id;
@@ -43,6 +53,10 @@ BlazeComponent.extendComponent({
const list = this.currentData();
list.restore();
},
+ 'click .js-restore-swimlane'() {
+ const swimlane = this.currentData();
+ swimlane.restore();
+ },
}];
},
}).register('archivesSidebar');
diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade
index 9a9774bb..273df8c2 100644
--- a/client/components/sidebar/sidebarFilters.jade
+++ b/client/components/sidebar/sidebarFilters.jade
@@ -59,7 +59,7 @@ template(name="multiselectionSidebar")
if name
= name
else
- span.quiet {{_ "label-default" color}}
+ span.quiet {{_ "label-default" (_ (concat "color-" color))}}
if allSelectedElementHave 'label' _id
i.fa.fa-check
else if someSelectedElementHave 'label' _id
diff --git a/client/components/sidebar/sidebarSearches.jade b/client/components/sidebar/sidebarSearches.jade
new file mode 100644
index 00000000..2ad5b00f
--- /dev/null
+++ b/client/components/sidebar/sidebarSearches.jade
@@ -0,0 +1,8 @@
+template(name="searchSidebar")
+ form.js-search-term-form
+ input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus)
+ .list-body.js-perfect-scrollbar
+ .minicards.clearfix.js-minicards
+ each (results)
+ a.minicard-wrapper.js-minicard(href=absoluteUrl)
+ +minicard(this)
diff --git a/client/components/sidebar/sidebarSearches.js b/client/components/sidebar/sidebarSearches.js
new file mode 100644
index 00000000..111a86b8
--- /dev/null
+++ b/client/components/sidebar/sidebarSearches.js
@@ -0,0 +1,19 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.term = new ReactiveVar('');
+ },
+
+ results() {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ return currentBoard.searchCards(this.term.get());
+ },
+
+ events() {
+ return [{
+ 'submit .js-search-term-form'(evt) {
+ evt.preventDefault();
+ this.term.set(evt.target.searchTerm.value);
+ },
+ }];
+ },
+}).register('searchSidebar');
diff --git a/client/components/sidebar/sidebarSearches.styl b/client/components/sidebar/sidebarSearches.styl
new file mode 100644
index 00000000..6b8ad904
--- /dev/null
+++ b/client/components/sidebar/sidebarSearches.styl
@@ -0,0 +1,2 @@
+input
+ max-width: 100%
diff --git a/client/components/swimlanes/swimlaneHeader.jade b/client/components/swimlanes/swimlaneHeader.jade
new file mode 100644
index 00000000..483de06f
--- /dev/null
+++ b/client/components/swimlanes/swimlaneHeader.jade
@@ -0,0 +1,23 @@
+template(name="swimlaneHeader")
+ .swimlane-header-wrap.js-swimlane-header
+ +inlinedForm
+ +editSwimlaneTitleForm
+ else
+ .swimlane-header(
+ class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
+ = title
+ .swimlane-header-menu
+ unless currentUser.isCommentOnly
+ a.fa.fa-navicon.js-open-swimlane-menu
+
+template(name="editSwimlaneTitleForm")
+ .list-composer
+ input.list-name-input.full-line(type="text" value=title autofocus)
+ .edit-controls.clearfix
+ button.primary.confirm(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+
+template(name="swimlaneActionPopup")
+ unless currentUser.isCommentOnly
+ ul.pop-over-list
+ li: a.js-close-swimlane {{_ 'archive-swimlane'}}
diff --git a/client/components/swimlanes/swimlaneHeader.js b/client/components/swimlanes/swimlaneHeader.js
new file mode 100644
index 00000000..50635f86
--- /dev/null
+++ b/client/components/swimlanes/swimlaneHeader.js
@@ -0,0 +1,25 @@
+BlazeComponent.extendComponent({
+ editTitle(evt) {
+ evt.preventDefault();
+ const newTitle = this.childComponents('inlinedForm')[0].getValue().trim();
+ const swimlane = this.currentData();
+ if (newTitle) {
+ swimlane.rename(newTitle.trim());
+ }
+ },
+
+ events() {
+ return [{
+ 'click .js-open-swimlane-menu': Popup.open('swimlaneAction'),
+ submit: this.editTitle,
+ }];
+ },
+}).register('swimlaneHeader');
+
+Template.swimlaneActionPopup.events({
+ 'click .js-close-swimlane' (evt) {
+ evt.preventDefault();
+ this.archive();
+ Popup.close();
+ },
+});
diff --git a/client/components/swimlanes/swimlanes.jade b/client/components/swimlanes/swimlanes.jade
new file mode 100644
index 00000000..76f54c66
--- /dev/null
+++ b/client/components/swimlanes/swimlanes.jade
@@ -0,0 +1,75 @@
+template(name="swimlane")
+ .swimlane.js-lists.js-swimlane
+ +swimlaneHeader
+ if isMiniScreen
+ if currentList
+ +list(currentList)
+ else
+ each currentBoard.lists
+ +miniList(this)
+ if currentUser.isBoardMember
+ +addListForm
+ else
+ each currentBoard.lists
+ +list(this)
+ if currentCardIsInThisList _id ../_id
+ +cardDetails(currentCard)
+ if currentUser.isBoardMember
+ +addListAndSwimlaneForm
+
+template(name="listsGroup")
+ .swimlane.list-group.js-lists
+ if isMiniScreen
+ if currentList
+ +list(currentList)
+ else
+ each currentBoard.lists
+ +miniList(this)
+ if currentUser.isBoardMember
+ +addListForm
+ else
+ each currentBoard.lists
+ +list(this)
+ if currentCardIsInThisList _id null
+ +cardDetails(currentCard)
+ if currentUser.isBoardMember
+ +addListForm
+
+template(name="addListAndSwimlaneForm")
+ .list.list-composer.js-list-composer
+ .list-header
+ +inlinedForm(autoclose=false)
+ input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
+ autocomplete="off" autofocus)
+ .edit-controls.clearfix
+ button.primary.confirm(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+ else
+ a.open-list-composer.js-open-inlined-form
+ i.fa.fa-plus
+ | {{_ 'add-list'}}
+ .list-header
+ +inlinedForm(autoclose=false)
+ input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}"
+ autocomplete="off" autofocus)
+ .edit-controls.clearfix
+ button.primary.confirm(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+ else
+ a.open-list-composer.js-open-inlined-form
+ i.fa.fa-plus
+ | {{_ 'add-swimlane'}}
+
+template(name="addListForm")
+ .list.list-composer.js-list-composer
+ .list-header
+ +inlinedForm(autoclose=false)
+ input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
+ autocomplete="off" autofocus)
+ .edit-controls.clearfix
+ button.primary.confirm(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+ else
+ a.open-list-composer.js-open-inlined-form
+ i.fa.fa-plus
+ | {{_ 'add-list'}}
diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js
new file mode 100644
index 00000000..7965c2bc
--- /dev/null
+++ b/client/components/swimlanes/swimlanes.js
@@ -0,0 +1,235 @@
+const { calculateIndex } = Utils;
+
+function currentCardIsInThisList(listId, swimlaneId) {
+ const currentCard = Cards.findOne(Session.get('currentCard'));
+ const currentUser = Meteor.user();
+ if (currentUser.profile.boardView === 'board-view-lists')
+ return currentCard && currentCard.listId === listId;
+ else if (currentUser.profile.boardView === 'board-view-swimlanes')
+ return currentCard && currentCard.listId === listId && currentCard.swimlaneId === swimlaneId;
+ else
+ return false;
+}
+
+function initSortable(boardComponent, $listsDom) {
+ // We want to animate the card details window closing. We rely on CSS
+ // transition for the actual animation.
+ $listsDom._uihooks = {
+ removeElement(node) {
+ const removeNode = _.once(() => {
+ node.parentNode.removeChild(node);
+ });
+ if ($(node).hasClass('js-card-details')) {
+ $(node).css({
+ flexBasis: 0,
+ padding: 0,
+ });
+ $listsDom.one(CSSEvents.transitionend, removeNode);
+ } else {
+ removeNode();
+ }
+ },
+ };
+
+ $listsDom.sortable({
+ tolerance: 'pointer',
+ helper: 'clone',
+ handle: '.js-list-header',
+ items: '.js-list:not(.js-list-composer)',
+ placeholder: 'list placeholder',
+ distance: 7,
+ start(evt, ui) {
+ ui.placeholder.height(ui.helper.height());
+ EscapeActions.executeUpTo('popup-close');
+ boardComponent.setIsDragging(true);
+ },
+ stop(evt, ui) {
+ // To attribute the new index number, we need to get the DOM element
+ // of the previous and the following card -- if any.
+ const prevListDom = ui.item.prev('.js-list').get(0);
+ const nextListDom = ui.item.next('.js-list').get(0);
+ const sortIndex = calculateIndex(prevListDom, nextListDom, 1);
+
+ $listsDom.sortable('cancel');
+ const listDomElement = ui.item.get(0);
+ const list = Blaze.getData(listDomElement);
+
+ Lists.update(list._id, {
+ $set: {
+ sort: sortIndex.base,
+ },
+ });
+
+ boardComponent.setIsDragging(false);
+ },
+ });
+
+ function userIsMember() {
+ return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+ }
+
+ // Disable drag-dropping while in multi-selection mode, or if the current user
+ // is not a board member
+ boardComponent.autorun(() => {
+ const $listDom = $listsDom;
+ if ($listDom.data('sortable')) {
+ $listsDom.sortable('option', 'disabled',
+ MultiSelection.isActive() || !userIsMember());
+ }
+ });
+}
+
+BlazeComponent.extendComponent({
+ onRendered() {
+ const boardComponent = this.parentComponent();
+ const $listsDom = this.$('.js-lists');
+
+ if (!Session.get('currentCard')) {
+ boardComponent.scrollLeft();
+ }
+
+ initSortable(boardComponent, $listsDom);
+ },
+ onCreated() {
+ this.draggingActive = new ReactiveVar(false);
+
+ this._isDragging = false;
+ this._lastDragPositionX = 0;
+ },
+
+ id() {
+ return this._id;
+ },
+
+ currentCardIsInThisList(listId, swimlaneId) {
+ return currentCardIsInThisList(listId, swimlaneId);
+ },
+
+ events() {
+ return [{
+ // Click-and-drag action
+ 'mousedown .board-canvas'(evt) {
+ // Translating the board canvas using the click-and-drag action can
+ // conflict with the build-in browser mechanism to select text. We
+ // define a list of elements in which we disable the dragging because
+ // the user will legitimately expect to be able to select some text with
+ // his mouse.
+ const noDragInside = ['a', 'input', 'textarea', 'p', '.js-list-header'];
+ if ($(evt.target).closest(noDragInside.join(',')).length === 0 && this.$('.swimlane').prop('clientHeight') > evt.offsetY) {
+ this._isDragging = true;
+ this._lastDragPositionX = evt.clientX;
+ }
+ },
+ 'mouseup'() {
+ if (this._isDragging) {
+ this._isDragging = false;
+ }
+ },
+ 'mousemove'(evt) {
+ if (this._isDragging) {
+ // Update the canvas position
+ this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX;
+ this._lastDragPositionX = evt.clientX;
+ // Disable browser text selection while dragging
+ evt.stopPropagation();
+ evt.preventDefault();
+ // Don't close opened card or inlined form at the end of the
+ // click-and-drag.
+ EscapeActions.executeUpTo('popup-close');
+ EscapeActions.preventNextClick();
+ }
+ },
+ }];
+ },
+}).register('swimlane');
+
+BlazeComponent.extendComponent({
+ // Proxy
+ open() {
+ this.childComponents('inlinedForm')[0].open();
+ },
+
+ events() {
+ return [{
+ submit(evt) {
+ evt.preventDefault();
+ const titleInput = this.find('.list-name-input');
+ const title = titleInput.value.trim();
+ if (title) {
+ Lists.insert({
+ title,
+ boardId: Session.get('currentBoard'),
+ sort: $('.list').length,
+ });
+
+ titleInput.value = '';
+ titleInput.focus();
+ }
+ },
+ }];
+ },
+}).register('addListForm');
+
+BlazeComponent.extendComponent({
+ // Proxy
+ open() {
+ this.childComponents('inlinedForm')[0].open();
+ },
+
+ events() {
+ return [{
+ submit(evt) {
+ evt.preventDefault();
+ let titleInput = this.find('.list-name-input');
+ if (titleInput) {
+ const title = titleInput.value.trim();
+ if (title) {
+ Lists.insert({
+ title,
+ boardId: Session.get('currentBoard'),
+ sort: $('.list').length,
+ });
+
+ titleInput.value = '';
+ titleInput.focus();
+ }
+ } else {
+ titleInput = this.find('.swimlane-name-input');
+ const title = titleInput.value.trim();
+ if (title) {
+ Swimlanes.insert({
+ title,
+ boardId: Session.get('currentBoard'),
+ sort: $('.swimlane').length,
+ });
+
+ titleInput.value = '';
+ titleInput.focus();
+ }
+ }
+ },
+ }];
+ },
+}).register('addListAndSwimlaneForm');
+
+Template.swimlane.helpers({
+ canSeeAddList() {
+ return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+ },
+});
+
+BlazeComponent.extendComponent({
+ currentCardIsInThisList(listId, swimlaneId) {
+ return currentCardIsInThisList(listId, swimlaneId);
+ },
+ onRendered() {
+ const boardComponent = this.parentComponent();
+ const $listsDom = this.$('.js-lists');
+
+ if (!Session.get('currentCard')) {
+ boardComponent.scrollLeft();
+ }
+
+ initSortable(boardComponent, $listsDom);
+ },
+}).register('listsGroup');
diff --git a/client/components/swimlanes/swimlanes.styl b/client/components/swimlanes/swimlanes.styl
new file mode 100644
index 00000000..dce298b0
--- /dev/null
+++ b/client/components/swimlanes/swimlanes.styl
@@ -0,0 +1,54 @@
+@import 'nib'
+
+.swimlane
+ // Even if this background color is the same as the body we can't leave it
+ // transparent, because that won't work during a swimlane drag.
+ background: darken(white, 13%)
+ display: flex
+ flex-direction: row
+ overflow: 0;
+ max-height: 100%
+
+ &.placeholder
+ background-color: rgba(0, 0, 0, .2)
+ border-color: transparent
+ box-shadow: none
+ height: 100px
+
+ &.ui-sortable-helper
+ box-shadow: -2px 2px 8px rgba(0, 0, 0, .3),
+ 0 0 1px rgba(0, 0, 0, .5)
+ transform: rotate(2deg)
+ cursor: grabbing
+
+ .swimlane-header.ui-sortable-handle
+ cursor: grabbing
+
+ .swimlane-header-wrap
+ display: flex;
+ flex-direction: row;
+ flex: 0 0 50px;
+ padding-bottom: 30px;
+ border-bottom: 1px solid #CCC
+
+ .swimlane-header
+ writing-mode: vertical-rl;
+ transform: rotate(180deg);
+ font-size: 14px;
+ line-height: 50px;
+ margin-top: 50px;
+ font-weight: bold;
+ min-height: 9px;
+ width: 50px;
+ overflow: hidden;
+ -o-text-overflow: ellipsis;
+ text-overflow: ellipsis;
+ word-wrap: break-word;
+ text-align: center;
+
+ .swimlane-header-menu
+ position: absolute
+ padding: 20px 20px
+
+.list-group
+ height: 100%
diff --git a/client/components/users/userAvatar.jade b/client/components/users/userAvatar.jade
index c61f2365..83e2c8d0 100644
--- a/client/components/users/userAvatar.jade
+++ b/client/components/users/userAvatar.jade
@@ -71,10 +71,10 @@ template(name="cardMemberPopup")
.info
h3= user.profile.fullname
p.quiet @{{ user.username }}
- if currentUser.isBoardMember
- ul.pop-over-list
- li: a.js-remove-member {{_ 'remove-member-from-card'}}
+ ul.pop-over-list
+ if currentUser.isNotCommentOnly
+ li: a.js-remove-member {{_ 'remove-member-from-card'}}
- if $eq currentUser._id user._id
- with currentUser
- li: a.js-edit-profile {{_ 'edit-profile'}}
+ if $eq currentUser._id user._id
+ with currentUser
+ li: a.js-edit-profile {{_ 'edit-profile'}}
diff --git a/client/components/users/userAvatar.js b/client/components/users/userAvatar.js
index 1066c632..be7a85d2 100644
--- a/client/components/users/userAvatar.js
+++ b/client/components/users/userAvatar.js
@@ -1,5 +1,3 @@
-Meteor.subscribe('my-avatars');
-
Template.userAvatar.helpers({
userData() {
// We need to handle a special case for the search results provided by the
@@ -54,6 +52,8 @@ Template.userAvatarInitials.helpers({
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
+
+ Meteor.subscribe('my-avatars');
},
avatarUrlOptions() {
diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade
index f67f82ee..a8fdb143 100644
--- a/client/components/users/userHeader.jade
+++ b/client/components/users/userHeader.jade
@@ -3,10 +3,11 @@ template(name="headerUserBar")
a.header-user-bar-name.js-open-header-member-menu
.header-user-bar-avatar
+userAvatar(userId=currentUser._id)
- if currentUser.profile.fullname
- = currentUser.profile.fullname
- else
- = currentUser.username
+ unless isMiniScreen
+ if currentUser.profile.fullname
+ = currentUser.profile.fullname
+ else
+ = currentUser.username
template(name="memberMenuPopup")
ul.pop-over-list
@@ -32,7 +33,10 @@ template(name="editProfilePopup")
| {{_ 'username'}}
span.error.hide.username-taken
| {{_ 'error-username-taken'}}
- input.js-profile-username(type="text" value=username)
+ if allowUserNameChange
+ input.js-profile-username(type="text" value=username)
+ else
+ input.js-profile-username(type="text" value=username readonly)
label
| {{_ 'initials'}}
input.js-profile-initials(type="text" value=profile.initials)
@@ -85,5 +89,5 @@ template(name="changeSettingsPopup")
li
label.bold
| {{_ 'show-cards-minimum-count'}}
- input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="1" max="99" onkeydown="return false")
+ input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="0" max="99" onkeydown="return false")
input.js-apply-show-cards-at.left(type="submit" value="{{_ 'apply'}}")
diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js
index 90205ee1..d96a9b3d 100644
--- a/client/components/users/userHeader.js
+++ b/client/components/users/userHeader.js
@@ -24,6 +24,9 @@ Template.editProfilePopup.helpers({
allowEmailChange() {
return AccountSettings.findOne('accounts-allowEmailChange').booleanValue;
},
+ allowUserNameChange() {
+ return AccountSettings.findOne('accounts-allowUserNameChange').booleanValue;
+ },
});
Template.editProfilePopup.events({
@@ -35,14 +38,16 @@ Template.editProfilePopup.events({
const email = tpl.find('.js-profile-email').value.trim();
let isChangeUserName = false;
let isChangeEmail = false;
- Users.update(Meteor.userId(), {$set: {
- 'profile.fullname': fullname,
- 'profile.initials': initials,
- }});
+ Users.update(Meteor.userId(), {
+ $set: {
+ 'profile.fullname': fullname,
+ 'profile.initials': initials,
+ },
+ });
isChangeUserName = username !== Meteor.user().username;
isChangeEmail = email.toLowerCase() !== Meteor.user().emails[0].address.toLowerCase();
if (isChangeUserName && isChangeEmail) {
- Meteor.call('setUsernameAndEmail', username, email.toLowerCase(), function(error) {
+ Meteor.call('setUsernameAndEmail', username, email.toLowerCase(), Meteor.userId(), function (error) {
const usernameMessageElement = tpl.$('.username-taken');
const emailMessageElement = tpl.$('.email-taken');
if (error) {
@@ -61,7 +66,7 @@ Template.editProfilePopup.events({
}
});
} else if (isChangeUserName) {
- Meteor.call('setUsername', username, function(error) {
+ Meteor.call('setUsername', username, Meteor.userId(), function (error) {
const messageElement = tpl.$('.username-taken');
if (error) {
messageElement.show();
@@ -71,7 +76,7 @@ Template.editProfilePopup.events({
}
});
} else if (isChangeEmail) {
- Meteor.call('setEmail', email.toLowerCase(), function(error) {
+ Meteor.call('setEmail', email.toLowerCase(), Meteor.userId(), function (error) {
const messageElement = tpl.$('.email-taken');
if (error) {
messageElement.show();
@@ -105,18 +110,24 @@ Template.editNotificationPopup.events({
// XXX For some reason the useraccounts autofocus isnt working in this case.
// See https://github.com/meteor-useraccounts/core/issues/384
-Template.changePasswordPopup.onRendered(function() {
+Template.changePasswordPopup.onRendered(function () {
this.find('#at-field-current_password').focus();
});
Template.changeLanguagePopup.helpers({
languages() {
return _.map(TAPi18n.getLanguages(), (lang, code) => {
- return {
- tag: code,
- name: lang.name === 'br' ? 'Brezhoneg' : lang.name,
- };
- }).sort(function(a, b) {
+ // Same code in /client/components/main/layouts.js
+ // TODO : Make code reusable
+ const tag = code;
+ let name = lang.name;
+ if (lang.name === 'br') {
+ name = 'Brezhoneg';
+ } else if (lang.name === 'ig') {
+ name = 'Igbo';
+ }
+ return { tag, name };
+ }).sort(function (a, b) {
if (a.name === b.name) {
return 0;
} else {
diff --git a/client/config/blazeHelpers.js b/client/config/blazeHelpers.js
index 73ee24b6..e058d722 100644
--- a/client/config/blazeHelpers.js
+++ b/client/config/blazeHelpers.js
@@ -16,6 +16,17 @@ Blaze.registerHelper('currentCard', () => {
}
});
+Blaze.registerHelper('currentList', () => {
+ const listId = Session.get('currentList');
+ if (listId) {
+ return Lists.findOne(listId);
+ } else {
+ return null;
+ }
+});
+
Blaze.registerHelper('getUser', (userId) => Users.findOne(userId));
Blaze.registerHelper('concat', (...args) => args.slice(0, -1).join(''));
+
+Blaze.registerHelper('isMiniScreen', () => Utils.isMiniScreen());
diff --git a/client/lib/emoji-values.js b/client/lib/emoji-values.js
deleted file mode 100644
index 1f07ac62..00000000
--- a/client/lib/emoji-values.js
+++ /dev/null
@@ -1,152 +0,0 @@
-Emoji.values = ['+1', '-1', '100', '1234', '8ball', 'a', 'ab', 'abc', 'abcd',
-'accept', 'aerial_tramway', 'airplane', 'alarm_clock', 'alien', 'ambulance',
-'anchor', 'angel', 'anger', 'angry', 'anguished', 'ant', 'apple', 'aquarius',
-'aries', 'arrow_backward', 'arrow_double_down', 'arrow_double_up', 'arrow_down',
-'arrow_down_small', 'arrow_forward', 'arrow_heading_down', 'arrow_heading_up',
-'arrow_left', 'arrow_lower_left', 'arrow_lower_right', 'arrow_right',
-'arrow_right_hook', 'arrow_up', 'arrow_up_down', 'arrow_up_small',
-'arrow_upper_left', 'arrow_upper_right', 'arrows_clockwise',
-'arrows_counterclockwise', 'art', 'articulated_lorry', 'astonished', 'atm', 'b',
-'baby', 'baby_bottle', 'baby_chick', 'baby_symbol', 'baggage_claim', 'balloon',
-'ballot_box_with_check', 'bamboo', 'banana', 'bangbang', 'bank', 'bar_chart',
-'barber', 'baseball', 'basketball', 'bath', 'bathtub', 'battery', 'bear', 'bee',
-'beer', 'beers', 'beetle', 'beginner', 'bell', 'bento', 'bicyclist', 'bike',
-'bikini', 'bird', 'birthday', 'black_circle', 'black_joker', 'black_nib',
-'black_square', 'black_square_button', 'blossom', 'blowfish', 'blue_book',
-'blue_car', 'blue_heart', 'blush', 'boar', 'boat', 'bomb', 'book', 'bookmark',
-'bookmark_tabs', 'books', 'boom', 'boot', 'bouquet', 'bow', 'bowling', 'bowtie',
-'boy', 'bread', 'bride_with_veil', 'bridge_at_night', 'briefcase',
-'broken_heart', 'bug', 'bulb', 'bullettrain_front', 'bullettrain_side', 'bus',
-'busstop', 'bust_in_silhouette', 'busts_in_silhouette', 'cactus', 'cake',
-'calendar', 'calling', 'camel', 'camera', 'cancer', 'candy', 'capital_abcd',
-'capricorn', 'car', 'card_index', 'carousel_horse', 'cat', 'cat2', 'cd',
-'chart', 'chart_with_downwards_trend', 'chart_with_upwards_trend',
-'checkered_flag', 'cherries', 'cherry_blossom', 'chestnut', 'chicken',
-'children_crossing', 'chocolate_bar', 'christmas_tree', 'church', 'cinema',
-'circus_tent', 'city_sunrise', 'city_sunset', 'cl', 'clap', 'clapper',
-'clipboard', 'clock1', 'clock10', 'clock1030', 'clock11', 'clock1130',
-'clock12', 'clock1230', 'clock130', 'clock2', 'clock230', 'clock3', 'clock330',
-'clock4', 'clock430', 'clock5', 'clock530', 'clock6', 'clock630', 'clock7',
-'clock730', 'clock8', 'clock830', 'clock9', 'clock930', 'closed_book',
-'closed_lock_with_key', 'closed_umbrella', 'cloud', 'clubs', 'cn', 'cocktail',
-'coffee', 'cold_sweat', 'collision', 'computer', 'confetti_ball', 'confounded',
-'confused', 'congratulations', 'construction', 'construction_worker',
-'convenience_store', 'cookie', 'cool', 'cop', 'copyright', 'corn', 'couple',
-'couple_with_heart', 'couplekiss', 'cow', 'cow2', 'credit_card', 'crocodile',
-'crossed_flags', 'crown', 'cry', 'crying_cat_face', 'crystal_ball', 'cupid',
-'curly_loop', 'currency_exchange', 'curry', 'custard', 'customs', 'cyclone',
-'dancer', 'dancers', 'dango', 'dart', 'dash', 'date', 'de', 'deciduous_tree',
-'department_store', 'diamond_shape_with_a_dot_inside', 'diamonds',
-'disappointed', 'disappointed_relieved', 'dizzy', 'dizzy_face', 'do_not_litter',
-'dog', 'dog2', 'dollar', 'dolls', 'dolphin', 'donut', 'door', 'doughnut',
-'dragon', 'dragon_face', 'dress', 'dromedary_camel', 'droplet', 'dvd', 'e-mail',
-'ear', 'ear_of_rice', 'earth_africa', 'earth_americas', 'earth_asia', 'egg',
-'eggplant', 'eight', 'eight_pointed_black_star', 'eight_spoked_asterisk',
-'electric_plug', 'elephant', 'email', 'end', 'envelope', 'es', 'euro',
-'european_castle', 'european_post_office', 'evergreen_tree', 'exclamation',
-'expressionless', 'eyeglasses', 'eyes', 'facepunch', 'factory', 'fallen_leaf',
-'family', 'fast_forward', 'fax', 'fearful', 'feelsgood', 'feet', 'ferris_wheel',
-'file_folder', 'finnadie', 'fire', 'fire_engine', 'fireworks',
-'first_quarter_moon', 'first_quarter_moon_with_face', 'fish', 'fish_cake',
-'fishing_pole_and_fish', 'fist', 'five', 'flags', 'flashlight', 'floppy_disk',
-'flower_playing_cards', 'flushed', 'foggy', 'football', 'fork_and_knife',
-'fountain', 'four', 'four_leaf_clover', 'fr', 'free', 'fried_shrimp', 'fries',
-'frog', 'frowning', 'fu', 'fuelpump', 'full_moon', 'full_moon_with_face',
-'game_die', 'gb', 'gem', 'gemini', 'ghost', 'gift', 'gift_heart', 'girl',
-'globe_with_meridians', 'goat', 'goberserk', 'godmode', 'golf', 'grapes',
-'green_apple', 'green_book', 'green_heart', 'grey_exclamation', 'grey_question',
-'grimacing', 'grin', 'grinning', 'guardsman', 'guitar', 'gun', 'haircut',
-'hamburger', 'hammer', 'hamster', 'hand', 'handbag', 'hankey', 'hash',
-'hatched_chick', 'hatching_chick', 'headphones', 'hear_no_evil', 'heart',
-'heart_decoration', 'heart_eyes', 'heart_eyes_cat', 'heartbeat', 'heartpulse',
-'hearts', 'heavy_check_mark', 'heavy_division_sign', 'heavy_dollar_sign',
-'heavy_exclamation_mark', 'heavy_minus_sign', 'heavy_multiplication_x',
-'heavy_plus_sign', 'helicopter', 'herb', 'hibiscus', 'high_brightness',
-'high_heel', 'hocho', 'honey_pot', 'honeybee', 'horse', 'horse_racing',
-'hospital', 'hotel', 'hotsprings', 'hourglass', 'hourglass_flowing_sand',
-'house', 'house_with_garden', 'hurtrealbad', 'hushed', 'ice_cream', 'icecream',
-'id', 'ideograph_advantage', 'imp', 'inbox_tray', 'incoming_envelope',
-'information_desk_person', 'information_source', 'innocent', 'interrobang',
-'iphone', 'it', 'izakaya_lantern', 'jack_o_lantern', 'japan', 'japanese_castle',
-'japanese_goblin', 'japanese_ogre', 'jeans', 'joy', 'joy_cat', 'jp', 'key',
-'keycap_ten', 'kimono', 'kiss', 'kissing', 'kissing_cat', 'kissing_closed_eyes',
-'kissing_face', 'kissing_heart', 'kissing_smiling_eyes', 'koala', 'koko', 'kr',
-'large_blue_circle', 'large_blue_diamond', 'large_orange_diamond',
-'last_quarter_moon', 'last_quarter_moon_with_face', 'laughing', 'leaves',
-'ledger', 'left_luggage', 'left_right_arrow', 'leftwards_arrow_with_hook',
-'lemon', 'leo', 'leopard', 'libra', 'light_rail', 'link', 'lips', 'lipstick',
-'lock', 'lock_with_ink_pen', 'lollipop', 'loop', 'loudspeaker', 'love_hotel',
-'love_letter', 'low_brightness', 'm', 'mag', 'mag_right', 'mahjong', 'mailbox',
-'mailbox_closed', 'mailbox_with_mail', 'mailbox_with_no_mail', 'man',
-'man_with_gua_pi_mao', 'man_with_turban', 'mans_shoe', 'maple_leaf', 'mask',
-'massage', 'meat_on_bone', 'mega', 'melon', 'memo', 'mens', 'metal', 'metro',
-'microphone', 'microscope', 'milky_way', 'minibus', 'minidisc',
-'mobile_phone_off', 'money_with_wings', 'moneybag', 'monkey', 'monkey_face',
-'monorail', 'moon', 'mortar_board', 'mount_fuji', 'mountain_bicyclist',
-'mountain_cableway', 'mountain_railway', 'mouse', 'mouse2', 'movie_camera',
-'moyai', 'muscle', 'mushroom', 'musical_keyboard', 'musical_note',
-'musical_score', 'mute', 'nail_care', 'name_badge', 'neckbeard', 'necktie',
-'negative_squared_cross_mark', 'neutral_face', 'new', 'new_moon',
-'new_moon_with_face', 'newspaper', 'ng', 'nine', 'no_bell', 'no_bicycles',
-'no_entry', 'no_entry_sign', 'no_good', 'no_mobile_phones', 'no_mouth',
-'no_pedestrians', 'no_smoking', 'non-potable_water', 'nose', 'notebook',
-'notebook_with_decorative_cover', 'notes', 'nut_and_bolt', 'o', 'o2', 'ocean',
-'octocat', 'octopus', 'oden', 'office', 'ok', 'ok_hand', 'ok_woman',
-'older_man', 'older_woman', 'on', 'oncoming_automobile', 'oncoming_bus',
-'oncoming_police_car', 'oncoming_taxi', 'one', 'open_file_folder', 'open_hands',
-'open_mouth', 'ophiuchus', 'orange_book', 'outbox_tray', 'ox', 'page_facing_up',
-'page_with_curl', 'pager', 'palm_tree', 'panda_face', 'paperclip', 'parking',
-'part_alternation_mark', 'partly_sunny', 'passport_control', 'paw_prints',
-'peach', 'pear', 'pencil', 'pencil2', 'penguin', 'pensive', 'performing_arts',
-'persevere', 'person_frowning', 'person_with_blond_hair',
-'person_with_pouting_face', 'phone', 'pig', 'pig2', 'pig_nose', 'pill',
-'pineapple', 'pisces', 'pizza', 'plus1', 'point_down', 'point_left',
-'point_right', 'point_up', 'point_up_2', 'police_car', 'poodle', 'poop',
-'post_office', 'postal_horn', 'postbox', 'potable_water', 'pouch',
-'poultry_leg', 'pound', 'pouting_cat', 'pray', 'princess', 'punch',
-'purple_heart', 'purse', 'pushpin', 'put_litter_in_its_place', 'question',
-'rabbit', 'rabbit2', 'racehorse', 'radio', 'radio_button', 'rage', 'rage1',
-'rage2', 'rage3', 'rage4', 'railway_car', 'rainbow', 'raised_hand',
-'raised_hands', 'raising_hand', 'ram', 'ramen', 'rat', 'recycle', 'red_car',
-'red_circle', 'registered', 'relaxed', 'relieved', 'repeat', 'repeat_one',
-'restroom', 'revolving_hearts', 'rewind', 'ribbon', 'rice', 'rice_ball',
-'rice_cracker', 'rice_scene', 'ring', 'rocket', 'roller_coaster', 'rooster',
-'rose', 'rotating_light', 'round_pushpin', 'rowboat', 'ru', 'rugby_football',
-'runner', 'running', 'running_shirt_with_sash', 'sa', 'sagittarius', 'sailboat',
-'sake', 'sandal', 'santa', 'satellite', 'satisfied', 'saxophone', 'school',
-'school_satchel', 'scissors', 'scorpius', 'scream', 'scream_cat', 'scroll',
-'seat', 'secret', 'see_no_evil', 'seedling', 'seven', 'shaved_ice', 'sheep',
-'shell', 'ship', 'shipit', 'shirt', 'shit', 'shoe', 'shower', 'signal_strength',
-'six', 'six_pointed_star', 'ski', 'skull', 'sleeping', 'sleepy', 'slot_machine',
-'small_blue_diamond', 'small_orange_diamond', 'small_red_triangle',
-'small_red_triangle_down', 'smile', 'smile_cat', 'smiley', 'smiley_cat',
-'smiling_imp', 'smirk', 'smirk_cat', 'smoking', 'snail', 'snake', 'snowboarder',
-'snowflake', 'snowman', 'sob', 'soccer', 'soon', 'sos', 'sound',
-'space_invader', 'spades', 'spaghetti', 'sparkler', 'sparkles',
-'sparkling_heart', 'speak_no_evil', 'speaker', 'speech_balloon', 'speedboat',
-'squirrel', 'star', 'star2', 'stars', 'station', 'statue_of_liberty',
-'steam_locomotive', 'stew', 'straight_ruler', 'strawberry', 'stuck_out_tongue',
-'stuck_out_tongue_closed_eyes', 'stuck_out_tongue_winking_eye', 'sun_with_face',
-'sunflower', 'sunglasses', 'sunny', 'sunrise', 'sunrise_over_mountains',
-'surfer', 'sushi', 'suspect', 'suspension_railway', 'sweat', 'sweat_drops',
-'sweat_smile', 'sweet_potato', 'swimmer', 'symbols', 'syringe', 'tada',
-'tanabata_tree', 'tangerine', 'taurus', 'taxi', 'tea', 'telephone',
-'telephone_receiver', 'telescope', 'tennis', 'tent', 'thought_balloon', 'three',
-'thumbsdown', 'thumbsup', 'ticket', 'tiger', 'tiger2', 'tired_face', 'tm',
-'toilet', 'tokyo_tower', 'tomato', 'tongue', 'top', 'tophat', 'tractor',
-'traffic_light', 'train', 'train2', 'tram', 'triangular_flag_on_post',
-'triangular_ruler', 'trident', 'triumph', 'trolleybus', 'trollface', 'trophy',
-'tropical_drink', 'tropical_fish', 'truck', 'trumpet', 'tshirt', 'tulip',
-'turtle', 'tv', 'twisted_rightwards_arrows', 'two', 'two_hearts',
-'two_men_holding_hands', 'two_women_holding_hands', 'u5272', 'u5408', 'u55b6',
-'u6307', 'u6708', 'u6709', 'u6e80', 'u7121', 'u7533', 'u7981', 'u7a7a', 'uk',
-'umbrella', 'unamused', 'underage', 'unlock', 'up', 'us', 'v',
-'vertical_traffic_light', 'vhs', 'vibration_mode', 'video_camera', 'video_game',
-'violin', 'virgo', 'volcano', 'vs', 'walking', 'waning_crescent_moon',
-'waning_gibbous_moon', 'warning', 'watch', 'water_buffalo', 'watermelon',
-'wave', 'wavy_dash', 'waxing_crescent_moon', 'waxing_gibbous_moon', 'wc',
-'weary', 'wedding', 'whale', 'whale2', 'wheelchair', 'white_check_mark',
-'white_circle', 'white_flower', 'white_square', 'white_square_button',
-'wind_chime', 'wine_glass', 'wink', 'wolf', 'woman', 'womans_clothes',
-'womans_hat', 'womens', 'worried', 'wrench', 'x', 'yellow_heart', 'yen', 'yum',
-'zap', 'zero', 'zzz'];
diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js
index 24955929..7abfa88d 100755
--- a/client/lib/keyboard.js
+++ b/client/lib/keyboard.js
@@ -91,9 +91,6 @@ Template.keyboardShortcuts.helpers({
keys: ['@'],
action: 'shortcut-autocomplete-members',
}, {
- keys: [':'],
- action: 'shortcut-autocomplete-emoji',
- }, {
keys: ['SPACE'],
action: 'shortcut-assign-self',
}],
diff --git a/client/lib/modal.js b/client/lib/modal.js
index 1ca8804b..d5350264 100644
--- a/client/lib/modal.js
+++ b/client/lib/modal.js
@@ -31,7 +31,7 @@ window.Modal = new class {
this._currentModal.set({ header, modalName });
this._onCloseGoTo = onCloseGoTo;
}
-};
+}();
Blaze.registerHelper('Modal', Modal);
diff --git a/client/lib/popup.js b/client/lib/popup.js
index 3658d883..0a700f82 100644
--- a/client/lib/popup.js
+++ b/client/lib/popup.js
@@ -190,7 +190,7 @@ window.Popup = new class {
return title !== translationKey ? title : defaultTitle;
};
}
-};
+}();
// We close a potential opened popup on any left click on the document, or go
// one step back by pressing escape.
@@ -205,4 +205,3 @@ escapeActions.forEach((actionName) => {
}
);
});
-
diff --git a/client/lib/unsavedEdits.js b/client/lib/unsavedEdits.js
index 17bb29b5..9c02eb84 100644
--- a/client/lib/unsavedEdits.js
+++ b/client/lib/unsavedEdits.js
@@ -1,5 +1,3 @@
-Meteor.subscribe('unsaved-edits');
-
// `UnsavedEdits` is a global key-value store used to save drafts of user
// inputs. We used to have the notion of a `cachedValue` that was local to a
// component but the global store has multiple advantages:
diff --git a/client/lib/utils.js b/client/lib/utils.js
index 9a9ff654..1f44c60d 100644
--- a/client/lib/utils.js
+++ b/client/lib/utils.js
@@ -33,6 +33,37 @@ Utils = {
return $(window).width() <= 800;
},
+ calculateIndexData(prevData, nextData, nItems = 1) {
+ let base, increment;
+ // If we drop the card to an empty column
+ if (!prevData && !nextData) {
+ base = 0;
+ increment = 1;
+ // If we drop the card in the first position
+ } else if (!prevData) {
+ base = nextData.sort - 1;
+ increment = -1;
+ // If we drop the card in the last position
+ } else if (!nextData) {
+ base = prevData.sort + 1;
+ increment = 1;
+ }
+ // In the general case take the average of the previous and next element
+ // sort indexes.
+ else {
+ const prevSortIndex = prevData.sort;
+ const nextSortIndex = nextData.sort;
+ increment = (nextSortIndex - prevSortIndex) / (nItems + 1);
+ base = prevSortIndex + increment;
+ }
+ // XXX Return a generator that yield values instead of a base with a
+ // increment number.
+ return {
+ base,
+ increment,
+ };
+ },
+
// Determine the new sort index
calculateIndex(prevCardDomElement, nextCardDomElement, nCards = 1) {
let base, increment;