mirror of
https://github.com/wekan/wekan.git
synced 2025-04-23 13:37:09 -04: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
|
||||
|
||||
.list-composer,
|
||||
.open-minicard-composer
|
||||
.open-minicard-composer,
|
||||
.minicard-wrapper.is-checked
|
||||
display: none
|
||||
|
||||
.lists
|
||||
|
|
|
@ -21,24 +21,20 @@ template(name="headerBoard")
|
|||
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{/if}}"
|
||||
class="{{#if Filter.isActive}}emphasis{{/if}}")
|
||||
i.fa.fa-filter
|
||||
span {{#if Filter.isActive}}{{_ 'filter-on'}}{{else}}{{_ 'filter'}}{{/if}}
|
||||
if Filter.isActive
|
||||
span {{_ 'filter-on'}}
|
||||
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
|
||||
i.fa.fa-times-thin
|
||||
else
|
||||
span {{_ 'filter'}}
|
||||
|
||||
if currentUser.isBoardMember
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
title="{{#if MultiSelection.isActive}}{{_ 'filter-on-desc'}}{{/if}}"
|
||||
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
|
||||
i.fa.fa-check-square-o
|
||||
span Multi-Selection {{#if MultiSelection.isActive}}is on{{/if}}
|
||||
if MultiSelection.isActive
|
||||
span Multi-Selection is on
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
i.fa.fa-times-thin
|
||||
else
|
||||
span Multi-Selection
|
||||
|
||||
.separator
|
||||
a.board-header-btn.js-open-board-menu
|
||||
|
|
|
@ -19,6 +19,7 @@ setBoardColor(color)
|
|||
background-color: darken(color, 20%)
|
||||
|
||||
&.pop-over .pop-over-list li a:hover,
|
||||
.sidebar .sidebar-content .sidebar-btn:hover,
|
||||
.sidebar-list li a:hover
|
||||
background-color: lighten(color, 10%)
|
||||
|
||||
|
|
|
@ -27,10 +27,12 @@ BlazeComponent.extendComponent({
|
|||
var title = textarea.val();
|
||||
var position = Blaze.getData(evt.currentTarget).position;
|
||||
var sortIndex;
|
||||
var firstCard = this.find('.js-minicard:first');
|
||||
var lastCard = this.find('.js-minicard:last');
|
||||
if (position === 'top') {
|
||||
sortIndex = Utils.getSortIndex(null, this.find('.js-minicard:first'));
|
||||
sortIndex = Utils.calculateIndex(null, firstCard).base;
|
||||
} else if (position === 'bottom') {
|
||||
sortIndex = Utils.getSortIndex(this.find('.js-minicard:last'), null);
|
||||
sortIndex = Utils.calculateIndex(lastCard, null).base;
|
||||
}
|
||||
|
||||
if ($.trim(title)) {
|
||||
|
|
|
@ -56,22 +56,23 @@ BlazeComponent.extendComponent({
|
|||
stop: function(evt, ui) {
|
||||
// To attribute the new index number, we need to get the dom element
|
||||
// of the previous and the following card -- if any.
|
||||
var cardDomElement = ui.item.get(0);
|
||||
var prevCardDomElement = ui.item.prev('.js-minicard').get(0);
|
||||
var nextCardDomElement = ui.item.next('.js-minicard').get(0);
|
||||
var sort = Utils.getSortIndex(prevCardDomElement, nextCardDomElement);
|
||||
var prevCardDom = ui.item.prev('.js-minicard').get(0);
|
||||
var nextCardDom = ui.item.next('.js-minicard').get(0);
|
||||
var nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
|
||||
var sortIndex = Utils.calculateIndex(prevCardDom, nextCardDom, nCards);
|
||||
var listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
|
||||
|
||||
if (MultiSelection.isActive()) {
|
||||
Cards.find(MultiSelection.getMongoSelector()).forEach(function(c) {
|
||||
Cards.find(MultiSelection.getMongoSelector()).forEach(function(c, i) {
|
||||
Cards.update(c._id, {
|
||||
$set: {
|
||||
listId: listId,
|
||||
sort: sort
|
||||
sort: sortIndex.base + i * sortIndex.increment
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
var cardDomElement = ui.item.get(0);
|
||||
var cardId = Blaze.getData(cardDomElement)._id;
|
||||
Cards.update(cardId, {
|
||||
$set: {
|
||||
|
@ -79,7 +80,7 @@ BlazeComponent.extendComponent({
|
|||
// XXX Using the same sort index for multiple cards is
|
||||
// unacceptable. Keep that only until we figure out if we want to
|
||||
// refactor the whole sorting mecanism or do something more basic.
|
||||
sort: sort
|
||||
sort: sortIndex.base
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -51,6 +51,19 @@
|
|||
.fa.fa-check
|
||||
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
|
||||
width: 248px
|
||||
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
|
||||
and #each x in y constructors.
|
||||
and #each x in y constructors to fix this.
|
||||
|
||||
template(name="filterSidebar")
|
||||
ul.sidebar-list
|
||||
|
@ -16,22 +16,27 @@ template(name="filterSidebar")
|
|||
span.quiet {{_ "label-default" color}}
|
||||
if Filter.labelIds.isSelected _id
|
||||
i.fa.fa-check
|
||||
hr
|
||||
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-filter
|
||||
+userAvatar(userId=this._id)
|
||||
span.sidebar-list-item-description
|
||||
= profile.name
|
||||
| (<span class="username">{{ username }}</span>)
|
||||
if Filter.members.isSelected _id
|
||||
i.fa.fa-check
|
||||
if Filter.isActive
|
||||
hr
|
||||
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-filter
|
||||
+userAvatar(userId=this._id)
|
||||
span.sidebar-list-item-description
|
||||
= profile.name
|
||||
| (<span class="username">{{ username }}</span>)
|
||||
if Filter.members.isSelected _id
|
||||
i.fa.fa-check
|
||||
hr
|
||||
a.js-clear-all(class="{{#unless Filter.isActive}}disabled{{/unless}}")
|
||||
| {{_ 'filter-clear'}}
|
||||
a.sidebar-btn.js-clear-all
|
||||
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")
|
||||
ul.sidebar-list
|
||||
|
@ -48,10 +53,32 @@ template(name="multiselectionSidebar")
|
|||
i.fa.fa-check
|
||||
else if someSelectedElementHave 'label' _id
|
||||
i.fa.fa-ellipsis-h
|
||||
//-
|
||||
XXX We should be able to assign a member to the list of selected cards.
|
||||
hr
|
||||
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")
|
||||
p What do you want to do?
|
||||
button.wide.js-remove-label Remove 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() {
|
||||
return [{
|
||||
'click .js-toggle-label-filter': function(event) {
|
||||
'click .js-toggle-label-filter': function(evt) {
|
||||
evt.preventDefault();
|
||||
Filter.labelIds.toogle(this.currentData()._id);
|
||||
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.resetExceptions();
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-clear-all': function(event) {
|
||||
'click .js-clear-all': function(evt) {
|
||||
evt.preventDefault();
|
||||
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() {
|
||||
return [{
|
||||
'click .js-toggle-label-multiselection': function(evt, tpl) {
|
||||
'click .js-toggle-label-multiselection': function(evt) {
|
||||
var labelId = this.currentData()._id;
|
||||
var mappedSelection = this.mapSelection('label', labelId);
|
||||
var operation;
|
||||
|
@ -69,7 +76,7 @@ BlazeComponent.extendComponent({
|
|||
var popup = Popup.open('disambiguateMultiLabel');
|
||||
// XXX We need to have a better integration between the popup and the
|
||||
// UI components systems.
|
||||
return popup.call(this.currentData(), evt, tpl);
|
||||
return popup.call(this.currentData(), evt);
|
||||
}
|
||||
|
||||
var query = {};
|
||||
|
@ -77,6 +84,30 @@ BlazeComponent.extendComponent({
|
|||
labelIds: labelId
|
||||
};
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
if (evt.which === 1 &&
|
||||
$(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();
|
||||
},
|
||||
|
||||
count: function() {
|
||||
return Cards.find(this.getMongoSelector()).count();
|
||||
},
|
||||
|
||||
isEmpty: function() {
|
||||
return this._selectedCards.get().length === 0;
|
||||
return this.count() === 0;
|
||||
},
|
||||
|
||||
activate: function() {
|
||||
if (! this.isActive()) {
|
||||
EscapeActions.executeUpTo('detailsPane');
|
||||
this._isActive.set(true);
|
||||
Sidebar.setView(this.sidebarView);
|
||||
Tracker.flush();
|
||||
}
|
||||
Sidebar.setView(this.sidebarView);
|
||||
},
|
||||
|
||||
disable: function() {
|
||||
|
@ -152,5 +156,7 @@ Blaze.registerHelper('MultiSelection', MultiSelection);
|
|||
|
||||
EscapeActions.register('multiselection',
|
||||
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
|
||||
getSortIndex: function(prevCardDomElement, nextCardDomElement) {
|
||||
calculateIndex: function(prevCardDomElement, nextCardDomElement, nCards) {
|
||||
nCards = nCards || 1;
|
||||
|
||||
// If we drop the card to an empty column
|
||||
if (! prevCardDomElement && ! nextCardDomElement) {
|
||||
return 0;
|
||||
return {base: 0, increment: 1};
|
||||
// If we drop the card in the first position
|
||||
} 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
|
||||
} 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
|
||||
// sort indexes.
|
||||
else {
|
||||
var prevSortIndex = Blaze.getData(prevCardDomElement).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",
|
||||
"changePasswordPopup-title": "Change Password",
|
||||
"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