mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 23:40:13 +01:00
Improve the multi-selection experience
New features: - select all filtered cards - assign or unassign a member to selected cards - archive selected cards This commit also fix the card sort indexes calculation when a multi- selection is drag-dropped.
This commit is contained in:
parent
a41e07b37e
commit
5478fc93db
12 changed files with 146 additions and 53 deletions
|
|
@ -32,7 +32,8 @@ position()
|
||||||
&.is-dragging-active
|
&.is-dragging-active
|
||||||
|
|
||||||
.list-composer,
|
.list-composer,
|
||||||
.open-minicard-composer
|
.open-minicard-composer,
|
||||||
|
.minicard-wrapper.is-checked
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
.lists
|
.lists
|
||||||
|
|
|
||||||
|
|
@ -21,24 +21,20 @@ template(name="headerBoard")
|
||||||
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{/if}}"
|
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{/if}}"
|
||||||
class="{{#if Filter.isActive}}emphasis{{/if}}")
|
class="{{#if Filter.isActive}}emphasis{{/if}}")
|
||||||
i.fa.fa-filter
|
i.fa.fa-filter
|
||||||
|
span {{#if Filter.isActive}}{{_ 'filter-on'}}{{else}}{{_ 'filter'}}{{/if}}
|
||||||
if Filter.isActive
|
if Filter.isActive
|
||||||
span {{_ 'filter-on'}}
|
|
||||||
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
|
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
|
||||||
i.fa.fa-times-thin
|
i.fa.fa-times-thin
|
||||||
else
|
|
||||||
span {{_ 'filter'}}
|
|
||||||
|
|
||||||
if currentUser.isBoardMember
|
if currentUser.isBoardMember
|
||||||
a.board-header-btn.js-multiselection-activate(
|
a.board-header-btn.js-multiselection-activate(
|
||||||
title="{{#if MultiSelection.isActive}}{{_ 'filter-on-desc'}}{{/if}}"
|
title="{{#if MultiSelection.isActive}}{{_ 'filter-on-desc'}}{{/if}}"
|
||||||
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
|
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
|
||||||
i.fa.fa-check-square-o
|
i.fa.fa-check-square-o
|
||||||
|
span Multi-Selection {{#if MultiSelection.isActive}}is on{{/if}}
|
||||||
if MultiSelection.isActive
|
if MultiSelection.isActive
|
||||||
span Multi-Selection is on
|
|
||||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||||
i.fa.fa-times-thin
|
i.fa.fa-times-thin
|
||||||
else
|
|
||||||
span Multi-Selection
|
|
||||||
|
|
||||||
.separator
|
.separator
|
||||||
a.board-header-btn.js-open-board-menu
|
a.board-header-btn.js-open-board-menu
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ setBoardColor(color)
|
||||||
background-color: darken(color, 20%)
|
background-color: darken(color, 20%)
|
||||||
|
|
||||||
&.pop-over .pop-over-list li a:hover,
|
&.pop-over .pop-over-list li a:hover,
|
||||||
|
.sidebar .sidebar-content .sidebar-btn:hover,
|
||||||
.sidebar-list li a:hover
|
.sidebar-list li a:hover
|
||||||
background-color: lighten(color, 10%)
|
background-color: lighten(color, 10%)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,12 @@ BlazeComponent.extendComponent({
|
||||||
var title = textarea.val();
|
var title = textarea.val();
|
||||||
var position = Blaze.getData(evt.currentTarget).position;
|
var position = Blaze.getData(evt.currentTarget).position;
|
||||||
var sortIndex;
|
var sortIndex;
|
||||||
|
var firstCard = this.find('.js-minicard:first');
|
||||||
|
var lastCard = this.find('.js-minicard:last');
|
||||||
if (position === 'top') {
|
if (position === 'top') {
|
||||||
sortIndex = Utils.getSortIndex(null, this.find('.js-minicard:first'));
|
sortIndex = Utils.calculateIndex(null, firstCard).base;
|
||||||
} else if (position === 'bottom') {
|
} else if (position === 'bottom') {
|
||||||
sortIndex = Utils.getSortIndex(this.find('.js-minicard:last'), null);
|
sortIndex = Utils.calculateIndex(lastCard, null).base;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($.trim(title)) {
|
if ($.trim(title)) {
|
||||||
|
|
|
||||||
|
|
@ -56,22 +56,23 @@ BlazeComponent.extendComponent({
|
||||||
stop: function(evt, ui) {
|
stop: function(evt, ui) {
|
||||||
// To attribute the new index number, we need to get the dom element
|
// To attribute the new index number, we need to get the dom element
|
||||||
// of the previous and the following card -- if any.
|
// of the previous and the following card -- if any.
|
||||||
var cardDomElement = ui.item.get(0);
|
var prevCardDom = ui.item.prev('.js-minicard').get(0);
|
||||||
var prevCardDomElement = ui.item.prev('.js-minicard').get(0);
|
var nextCardDom = ui.item.next('.js-minicard').get(0);
|
||||||
var nextCardDomElement = ui.item.next('.js-minicard').get(0);
|
var nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
|
||||||
var sort = Utils.getSortIndex(prevCardDomElement, nextCardDomElement);
|
var sortIndex = Utils.calculateIndex(prevCardDom, nextCardDom, nCards);
|
||||||
var listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
|
var listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
|
||||||
|
|
||||||
if (MultiSelection.isActive()) {
|
if (MultiSelection.isActive()) {
|
||||||
Cards.find(MultiSelection.getMongoSelector()).forEach(function(c) {
|
Cards.find(MultiSelection.getMongoSelector()).forEach(function(c, i) {
|
||||||
Cards.update(c._id, {
|
Cards.update(c._id, {
|
||||||
$set: {
|
$set: {
|
||||||
listId: listId,
|
listId: listId,
|
||||||
sort: sort
|
sort: sortIndex.base + i * sortIndex.increment
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
var cardDomElement = ui.item.get(0);
|
||||||
var cardId = Blaze.getData(cardDomElement)._id;
|
var cardId = Blaze.getData(cardDomElement)._id;
|
||||||
Cards.update(cardId, {
|
Cards.update(cardId, {
|
||||||
$set: {
|
$set: {
|
||||||
|
|
@ -79,7 +80,7 @@ BlazeComponent.extendComponent({
|
||||||
// XXX Using the same sort index for multiple cards is
|
// XXX Using the same sort index for multiple cards is
|
||||||
// unacceptable. Keep that only until we figure out if we want to
|
// unacceptable. Keep that only until we figure out if we want to
|
||||||
// refactor the whole sorting mecanism or do something more basic.
|
// refactor the whole sorting mecanism or do something more basic.
|
||||||
sort: sort
|
sort: sortIndex.base
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,19 @@
|
||||||
.fa.fa-check
|
.fa.fa-check
|
||||||
margin: 0 4px
|
margin: 0 4px
|
||||||
|
|
||||||
|
.sidebar-btn
|
||||||
|
display: block
|
||||||
|
margin: 5px 0
|
||||||
|
padding: 10px
|
||||||
|
border-radius: 3px
|
||||||
|
background: darken(white, 10%)
|
||||||
|
|
||||||
|
&:hover *
|
||||||
|
color: white
|
||||||
|
|
||||||
|
i.fa
|
||||||
|
margin-right: 10px
|
||||||
|
|
||||||
.board-sidebar
|
.board-sidebar
|
||||||
width: 248px
|
width: 248px
|
||||||
right: -@width
|
right: -@width
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//-
|
//-
|
||||||
XXX There is a *lot* of code duplication in the above templates and in the
|
XXX There is a *lot* of code duplication in the below templates and in the
|
||||||
corresponding JavaScript components. We will probably need the upcoming #let
|
corresponding JavaScript components. We will probably need the upcoming #let
|
||||||
and #each x in y constructors.
|
and #each x in y constructors to fix this.
|
||||||
|
|
||||||
template(name="filterSidebar")
|
template(name="filterSidebar")
|
||||||
ul.sidebar-list
|
ul.sidebar-list
|
||||||
|
|
@ -29,9 +29,14 @@ template(name="filterSidebar")
|
||||||
| (<span class="username">{{ username }}</span>)
|
| (<span class="username">{{ username }}</span>)
|
||||||
if Filter.members.isSelected _id
|
if Filter.members.isSelected _id
|
||||||
i.fa.fa-check
|
i.fa.fa-check
|
||||||
|
if Filter.isActive
|
||||||
hr
|
hr
|
||||||
a.js-clear-all(class="{{#unless Filter.isActive}}disabled{{/unless}}")
|
a.sidebar-btn.js-clear-all
|
||||||
| {{_ 'filter-clear'}}
|
i.fa.fa-filter
|
||||||
|
span {{_ 'filter-clear'}}
|
||||||
|
a.sidebar-btn.js-filter-to-selection
|
||||||
|
i.fa.fa-check-square-o
|
||||||
|
span Filter to selection
|
||||||
|
|
||||||
template(name="multiselectionSidebar")
|
template(name="multiselectionSidebar")
|
||||||
ul.sidebar-list
|
ul.sidebar-list
|
||||||
|
|
@ -48,10 +53,32 @@ template(name="multiselectionSidebar")
|
||||||
i.fa.fa-check
|
i.fa.fa-check
|
||||||
else if someSelectedElementHave 'label' _id
|
else if someSelectedElementHave 'label' _id
|
||||||
i.fa.fa-ellipsis-h
|
i.fa.fa-ellipsis-h
|
||||||
//-
|
hr
|
||||||
XXX We should be able to assign a member to the list of selected cards.
|
ul.sidebar-list
|
||||||
|
each currentBoard.members
|
||||||
|
if isActive
|
||||||
|
with getUser userId
|
||||||
|
li(class="{{#if Filter.members.isSelected _id}}active{{/if}}")
|
||||||
|
a.name.js-toogle-member-multiselection
|
||||||
|
+userAvatar(userId=this._id)
|
||||||
|
span.sidebar-list-item-description
|
||||||
|
= profile.name
|
||||||
|
| (<span class="username">{{ username }}</span>)
|
||||||
|
if allSelectedElementHave 'member' _id
|
||||||
|
i.fa.fa-check
|
||||||
|
else if someSelectedElementHave 'member' _id
|
||||||
|
i.fa.fa-ellipsis-h
|
||||||
|
hr
|
||||||
|
a.sidebar-btn.js-archive-selection
|
||||||
|
i.fa.fa-archive
|
||||||
|
span Archive selection
|
||||||
|
|
||||||
template(name="disambiguateMultiLabelPopup")
|
template(name="disambiguateMultiLabelPopup")
|
||||||
p What do you want to do?
|
p What do you want to do?
|
||||||
button.wide.js-remove-label Remove the label
|
button.wide.js-remove-label Remove the label
|
||||||
button.wide.js-add-label Add the label
|
button.wide.js-add-label Add the label
|
||||||
|
|
||||||
|
template(name="disambiguateMultiMemberPopup")
|
||||||
|
p What do you want to do?
|
||||||
|
button.wide.js-unassign-member Unassign member
|
||||||
|
button.wide.js-assign-member Assign member
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,26 @@ BlazeComponent.extendComponent({
|
||||||
|
|
||||||
events: function() {
|
events: function() {
|
||||||
return [{
|
return [{
|
||||||
'click .js-toggle-label-filter': function(event) {
|
'click .js-toggle-label-filter': function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
Filter.labelIds.toogle(this.currentData()._id);
|
Filter.labelIds.toogle(this.currentData()._id);
|
||||||
Filter.resetExceptions();
|
Filter.resetExceptions();
|
||||||
event.preventDefault();
|
|
||||||
},
|
},
|
||||||
'click .js-toogle-member-filter': function(event) {
|
'click .js-toogle-member-filter': function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
Filter.members.toogle(this.currentData()._id);
|
Filter.members.toogle(this.currentData()._id);
|
||||||
Filter.resetExceptions();
|
Filter.resetExceptions();
|
||||||
event.preventDefault();
|
|
||||||
},
|
},
|
||||||
'click .js-clear-all': function(event) {
|
'click .js-clear-all': function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
Filter.reset();
|
Filter.reset();
|
||||||
event.preventDefault();
|
},
|
||||||
|
'click .js-filter-to-selection': function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
var selectedCards = Cards.find(Filter.mongoSelector()).map(function(c) {
|
||||||
|
return c._id;
|
||||||
|
});
|
||||||
|
MultiSelection.add(selectedCards);
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +64,7 @@ BlazeComponent.extendComponent({
|
||||||
|
|
||||||
events: function() {
|
events: function() {
|
||||||
return [{
|
return [{
|
||||||
'click .js-toggle-label-multiselection': function(evt, tpl) {
|
'click .js-toggle-label-multiselection': function(evt) {
|
||||||
var labelId = this.currentData()._id;
|
var labelId = this.currentData()._id;
|
||||||
var mappedSelection = this.mapSelection('label', labelId);
|
var mappedSelection = this.mapSelection('label', labelId);
|
||||||
var operation;
|
var operation;
|
||||||
|
|
@ -69,7 +76,7 @@ BlazeComponent.extendComponent({
|
||||||
var popup = Popup.open('disambiguateMultiLabel');
|
var popup = Popup.open('disambiguateMultiLabel');
|
||||||
// XXX We need to have a better integration between the popup and the
|
// XXX We need to have a better integration between the popup and the
|
||||||
// UI components systems.
|
// UI components systems.
|
||||||
return popup.call(this.currentData(), evt, tpl);
|
return popup.call(this.currentData(), evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
var query = {};
|
var query = {};
|
||||||
|
|
@ -77,6 +84,30 @@ BlazeComponent.extendComponent({
|
||||||
labelIds: labelId
|
labelIds: labelId
|
||||||
};
|
};
|
||||||
updateSelectedCards(query);
|
updateSelectedCards(query);
|
||||||
|
},
|
||||||
|
'click .js-toogle-member-multiselection': function(evt) {
|
||||||
|
var memberId = this.currentData()._id;
|
||||||
|
var mappedSelection = this.mapSelection('member', memberId);
|
||||||
|
var operation;
|
||||||
|
if (_.every(mappedSelection))
|
||||||
|
operation = '$pull';
|
||||||
|
else if (_.every(mappedSelection, function(bool) { return ! bool; }))
|
||||||
|
operation = '$addToSet';
|
||||||
|
else {
|
||||||
|
var popup = Popup.open('disambiguateMultiMember');
|
||||||
|
// XXX We need to have a better integration between the popup and the
|
||||||
|
// UI components systems.
|
||||||
|
return popup.call(this.currentData(), evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = {};
|
||||||
|
query[operation] = {
|
||||||
|
members: memberId
|
||||||
|
};
|
||||||
|
updateSelectedCards(query);
|
||||||
|
},
|
||||||
|
'click .js-archive-selection': function() {
|
||||||
|
updateSelectedCards({$set: {archived: true}});
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
@ -92,3 +123,14 @@ Template.disambiguateMultiLabelPopup.events({
|
||||||
Popup.close();
|
Popup.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Template.disambiguateMultiMemberPopup.events({
|
||||||
|
'click .js-unassign-member': function() {
|
||||||
|
updateSelectedCards({$pull: {members: this._id}});
|
||||||
|
Popup.close();
|
||||||
|
},
|
||||||
|
'click .js-assign-member': function() {
|
||||||
|
updateSelectedCards({$addToSet: {members: this._id}});
|
||||||
|
Popup.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,6 @@ Mousetrap.bindGlobal('esc', function() {
|
||||||
$(document).on('click', function(evt) {
|
$(document).on('click', function(evt) {
|
||||||
if (evt.which === 1 &&
|
if (evt.which === 1 &&
|
||||||
$(evt.target).closest('a,button,.is-editable').length === 0) {
|
$(evt.target).closest('a,button,.is-editable').length === 0) {
|
||||||
EscapeActions.clickExecute(evt, 'detailsPane');
|
EscapeActions.clickExecute(evt, 'multiselection');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -72,17 +72,21 @@ MultiSelection = {
|
||||||
return this._isActive.get();
|
return this._isActive.get();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
count: function() {
|
||||||
|
return Cards.find(this.getMongoSelector()).count();
|
||||||
|
},
|
||||||
|
|
||||||
isEmpty: function() {
|
isEmpty: function() {
|
||||||
return this._selectedCards.get().length === 0;
|
return this.count() === 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
activate: function() {
|
activate: function() {
|
||||||
if (! this.isActive()) {
|
if (! this.isActive()) {
|
||||||
EscapeActions.executeUpTo('detailsPane');
|
EscapeActions.executeUpTo('detailsPane');
|
||||||
this._isActive.set(true);
|
this._isActive.set(true);
|
||||||
Sidebar.setView(this.sidebarView);
|
|
||||||
Tracker.flush();
|
Tracker.flush();
|
||||||
}
|
}
|
||||||
|
Sidebar.setView(this.sidebarView);
|
||||||
},
|
},
|
||||||
|
|
||||||
disable: function() {
|
disable: function() {
|
||||||
|
|
@ -152,5 +156,7 @@ Blaze.registerHelper('MultiSelection', MultiSelection);
|
||||||
|
|
||||||
EscapeActions.register('multiselection',
|
EscapeActions.register('multiselection',
|
||||||
function() { MultiSelection.disable(); },
|
function() { MultiSelection.disable(); },
|
||||||
function() { return MultiSelection.isActive(); }
|
function() { return MultiSelection.isActive(); }, {
|
||||||
|
noClickEscapeOn: '.js-minicard,.js-board-sidebar-content'
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -37,23 +37,26 @@ Utils = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Determine the new sort index
|
// Determine the new sort index
|
||||||
getSortIndex: function(prevCardDomElement, nextCardDomElement) {
|
calculateIndex: function(prevCardDomElement, nextCardDomElement, nCards) {
|
||||||
|
nCards = nCards || 1;
|
||||||
|
|
||||||
// If we drop the card to an empty column
|
// If we drop the card to an empty column
|
||||||
if (! prevCardDomElement && ! nextCardDomElement) {
|
if (! prevCardDomElement && ! nextCardDomElement) {
|
||||||
return 0;
|
return {base: 0, increment: 1};
|
||||||
// If we drop the card in the first position
|
// If we drop the card in the first position
|
||||||
} else if (! prevCardDomElement) {
|
} else if (! prevCardDomElement) {
|
||||||
return Blaze.getData(nextCardDomElement).sort - 1;
|
return {base: Blaze.getData(nextCardDomElement).sort - 1, increment: -1};
|
||||||
// If we drop the card in the last position
|
// If we drop the card in the last position
|
||||||
} else if (! nextCardDomElement) {
|
} else if (! nextCardDomElement) {
|
||||||
return Blaze.getData(prevCardDomElement).sort + 1;
|
return {base: Blaze.getData(prevCardDomElement).sort + 1, increment: 1};
|
||||||
}
|
}
|
||||||
// In the general case take the average of the previous and next element
|
// In the general case take the average of the previous and next element
|
||||||
// sort indexes.
|
// sort indexes.
|
||||||
else {
|
else {
|
||||||
var prevSortIndex = Blaze.getData(prevCardDomElement).sort;
|
var prevSortIndex = Blaze.getData(prevCardDomElement).sort;
|
||||||
var nextSortIndex = Blaze.getData(nextCardDomElement).sort;
|
var nextSortIndex = Blaze.getData(nextCardDomElement).sort;
|
||||||
return (prevSortIndex + nextSortIndex) / 2;
|
var increment = (nextSortIndex - prevSortIndex) / (nCards + 1);
|
||||||
|
return {base: prevSortIndex + increment, increment: increment};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -190,5 +190,6 @@
|
||||||
"changeAvatarPopup-title": "Change Avatar",
|
"changeAvatarPopup-title": "Change Avatar",
|
||||||
"changePasswordPopup-title": "Change Password",
|
"changePasswordPopup-title": "Change Password",
|
||||||
"cardDetailsActionsPopup-title": "Card Actions",
|
"cardDetailsActionsPopup-title": "Card Actions",
|
||||||
"disambiguateMultiLabelPopup-title": "Disambiguate Label Action"
|
"disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
|
||||||
|
"disambiguateMultiMemberPopup-title": "Disambiguate Member Action"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue