summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLauri Ojansivu <x@xet7.org>2020-04-19 16:30:27 +0300
committerLauri Ojansivu <x@xet7.org>2020-04-19 16:30:27 +0300
commit6cae14d6e9c58f394df14cf2ce48d3d53990c223 (patch)
tree1ce50b227c98de1c0bb1c34cdb412c4fd1372896
parente7002f417b1e9550b257dfee6b669ef0ffa69012 (diff)
parentb42d8346cda99258f4ab5689ebd02fdc7c2e85c3 (diff)
downloadwekan-6cae14d6e9c58f394df14cf2ce48d3d53990c223.tar.gz
wekan-6cae14d6e9c58f394df14cf2ce48d3d53990c223.tar.bz2
wekan-6cae14d6e9c58f394df14cf2ce48d3d53990c223.zip
Merge branch 'feature-sortable-boards' of https://github.com/boeserwolf/wekan into boeserwolf-feature-sortable-boards
-rw-r--r--client/components/boards/boardArchive.js2
-rw-r--r--client/components/boards/boardsList.jade4
-rw-r--r--client/components/boards/boardsList.js72
-rw-r--r--client/components/boards/boardsList.styl15
-rw-r--r--client/components/cards/cardDetails.js6
-rw-r--r--client/components/lists/listBody.js4
-rw-r--r--client/components/rules/actions/boardActions.js2
-rw-r--r--client/components/settings/settingBody.js2
-rw-r--r--client/components/sidebar/sidebar.js6
-rw-r--r--models/boards.js29
-rw-r--r--models/users.js30
-rw-r--r--server/migrations.js12
-rw-r--r--server/publications/boards.js5
13 files changed, 154 insertions, 35 deletions
diff --git a/client/components/boards/boardArchive.js b/client/components/boards/boardArchive.js
index d3e65bd8..9f4d60a1 100644
--- a/client/components/boards/boardArchive.js
+++ b/client/components/boards/boardArchive.js
@@ -7,7 +7,7 @@ BlazeComponent.extendComponent({
return Boards.find(
{ archived: true },
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ }
},
);
},
diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade
index 46086693..bbce1d6f 100644
--- a/client/components/boards/boardsList.jade
+++ b/client/components/boards/boardsList.jade
@@ -1,10 +1,10 @@
template(name="boardList")
.wrapper
- ul.board-list.clearfix
+ ul.board-list.clearfix.js-boards
li.js-add-board
a.board-list-item.label {{_ 'add-board'}}
each boards
- li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
+ li(class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
if isInvited
.board-list-item
span.details
diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js
index 65bed16a..c700084f 100644
--- a/client/components/boards/boardsList.js
+++ b/client/components/boards/boardsList.js
@@ -1,4 +1,5 @@
const subManager = new SubsManager();
+const { calculateIndex, enableClickOnTouch } = Utils;
Template.boardListHeaderBar.events({
'click .js-open-archived-board'() {
@@ -7,8 +8,8 @@ Template.boardListHeaderBar.events({
});
Template.boardListHeaderBar.helpers({
- title(){
- return FlowRouter.getRouteName() == 'home' ? 'my-boards' :'public';
+ title() {
+ return FlowRouter.getRouteName() == 'home' ? 'my-boards' : 'public';
},
templatesBoardId() {
return Meteor.user() && Meteor.user().getTemplatesBoardId();
@@ -23,20 +24,69 @@ BlazeComponent.extendComponent({
Meteor.subscribe('setting');
},
+ onRendered() {
+ const self = this;
+ function userIsAllowedToMove() {
+ return Meteor.user();
+ }
+
+ const itemsSelector = '.js-board:not(.placeholder)';
+
+ const $boards = this.$('.js-boards');
+ $boards.sortable({
+ connectWith: '.js-boards',
+ tolerance: 'pointer',
+ appendTo: '.board-list',
+ helper: 'clone',
+ distance: 7,
+ items: itemsSelector,
+ placeholder: 'board-wrapper placeholder',
+ start(evt, ui) {
+ ui.helper.css('z-index', 1000);
+ ui.placeholder.height(ui.helper.height());
+ EscapeActions.executeUpTo('popup-close');
+ },
+ 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 prevBoardDom = ui.item.prev('.js-board').get(0);
+ const nextBoardBom = ui.item.next('.js-board').get(0);
+ const sortIndex = calculateIndex(prevBoardDom, nextBoardBom, 1);
+
+ const boardDomElement = ui.item.get(0);
+ const board = Blaze.getData(boardDomElement);
+ // Normally the jquery-ui sortable library moves the dragged DOM element
+ // to its new position, which disrupts Blaze reactive updates mechanism
+ // (especially when we move the last card of a list, or when multiple
+ // users move some cards at the same time). To prevent these UX glitches
+ // we ask sortable to gracefully cancel the move, and to put back the
+ // DOM in its initial state. The card move is then handled reactively by
+ // Blaze with the below query.
+ $boards.sortable('cancel');
+
+ board.move(sortIndex.base);
+ },
+ });
+
+ // ugly touch event hotfix
+ enableClickOnTouch(itemsSelector);
+
+ // Disable drag-dropping if the current user is not a board member or is comment only
+ this.autorun(() => {
+ $boards.sortable('option', 'disabled', !userIsAllowedToMove());
+ });
+ },
+
boards() {
let query = {
archived: false,
type: 'board',
- }
+ };
if (FlowRouter.getRouteName() == 'home')
- query['members.userId'] = Meteor.userId()
- else
- query.permission = 'public'
-
- return Boards.find(
- query,
- { sort: ['title'] },
- );
+ query['members.userId'] = Meteor.userId();
+ else query.permission = 'public';
+
+ return Boards.find(query, { sort: { sort: 1 /* boards default sorting */ } });
},
isStarred() {
const user = Meteor.user();
diff --git a/client/components/boards/boardsList.styl b/client/components/boards/boardsList.styl
index ae366e83..d12a0337 100644
--- a/client/components/boards/boardsList.styl
+++ b/client/components/boards/boardsList.styl
@@ -11,6 +11,19 @@ $spaceBetweenTiles = 16px
box-sizing: border-box
position: relative
+ &.placeholder:after
+ content: '';
+ display: block;
+ background: darken(white, 20%)
+ border-radius: 3px;
+ height: 106px;
+ margin: 8px;
+
+ &.ui-sortable-helper
+ cursor: grabbing
+ transform: rotate(4deg)
+ display: block !important
+
&.starred
.fa-star,
.fa-star-o
@@ -183,7 +196,7 @@ $spaceBetweenTiles = 16px
overflow: scroll
li
- width: 50%
+ width: 50%
.board-list-item
overflow: hidden
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index 9d31fc60..ce504146 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -727,7 +727,7 @@ BlazeComponent.extendComponent({
_id: { $ne: Meteor.user().getTemplatesBoardId() },
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
return boards;
@@ -903,7 +903,7 @@ BlazeComponent.extendComponent({
},
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
return boards;
@@ -974,7 +974,7 @@ BlazeComponent.extendComponent({
}
}
},
- 'click .js-delete': Popup.afterConfirm('cardDelete', function () {
+ 'click .js-delete': Popup.afterConfirm('cardDelete', function() {
Popup.close();
Cards.remove(this._id);
Utils.goBoardId(this.boardId);
diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js
index 03f88f63..88f88db0 100644
--- a/client/components/lists/listBody.js
+++ b/client/components/lists/listBody.js
@@ -411,7 +411,7 @@ BlazeComponent.extendComponent({
type: 'board',
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
return boards;
@@ -597,7 +597,7 @@ BlazeComponent.extendComponent({
type: 'board',
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
return boards;
diff --git a/client/components/rules/actions/boardActions.js b/client/components/rules/actions/boardActions.js
index c2f2375a..02910cc1 100644
--- a/client/components/rules/actions/boardActions.js
+++ b/client/components/rules/actions/boardActions.js
@@ -11,7 +11,7 @@ BlazeComponent.extendComponent({
},
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
return boards;
diff --git a/client/components/settings/settingBody.js b/client/components/settings/settingBody.js
index 319c066b..62752084 100644
--- a/client/components/settings/settingBody.js
+++ b/client/components/settings/settingBody.js
@@ -48,7 +48,7 @@ BlazeComponent.extendComponent({
'members.isAdmin': true,
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
},
diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js
index 78b47a48..11471c2f 100644
--- a/client/components/sidebar/sidebar.js
+++ b/client/components/sidebar/sidebar.js
@@ -510,7 +510,7 @@ BlazeComponent.extendComponent({
'members.userId': Meteor.userId(),
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
},
@@ -589,7 +589,7 @@ BlazeComponent.extendComponent({
'subtext-with-parent',
'no-parent',
];
- options.forEach(function (element) {
+ options.forEach(function(element) {
if (element !== value) {
$(`#${element} ${MCB}`).toggleClass(CKCLS, false);
$(`#${element}`).toggleClass(CKCLS, false);
@@ -688,7 +688,7 @@ BlazeComponent.extendComponent({
'members.userId': Meteor.userId(),
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
},
diff --git a/models/boards.js b/models/boards.js
index 35ee1a36..170ebc5a 100644
--- a/models/boards.js
+++ b/models/boards.js
@@ -493,6 +493,14 @@ Boards.attachSchema(
type: String,
defaultValue: 'board',
},
+ sort: {
+ /**
+ * Sort value
+ */
+ type: Number,
+ decimal: true,
+ defaultValue: -1,
+ },
}),
);
@@ -1186,6 +1194,10 @@ Boards.mutations({
setPresentParentTask(presentParentTask) {
return { $set: { presentParentTask } };
},
+
+ move(sortIndex) {
+ return { $set: { sort: sortIndex } };
+ },
});
function boardRemover(userId, doc) {
@@ -1283,6 +1295,14 @@ if (Meteor.isServer) {
});
}
+// Insert new board at last position in sort order.
+Boards.before.insert((userId, doc) => {
+ const lastBoard = Boards.findOne({ sort: { $exists: true } }, { sort: { sort: -1 } });
+ if (lastBoard && typeof lastBoard.sort !== 'undefined') {
+ doc.sort = lastBoard.sort + 1;
+ }
+});
+
if (Meteor.isServer) {
// Let MongoDB ensure that a member is not included twice in the same board
Meteor.startup(() => {
@@ -1466,7 +1486,7 @@ if (Meteor.isServer) {
'members.userId': paramUserId,
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
).map(function(board) {
return {
@@ -1496,7 +1516,12 @@ if (Meteor.isServer) {
Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, {
code: 200,
- data: Boards.find({ permission: 'public' }).map(function(doc) {
+ data: Boards.find(
+ { permission: 'public' },
+ {
+ sort: { sort: 1 /* boards default sorting */ },
+ },
+ ).map(function(doc) {
return {
_id: doc._id,
title: doc.title,
diff --git a/models/users.js b/models/users.js
index a9eeb38b..f4b7329a 100644
--- a/models/users.js
+++ b/models/users.js
@@ -386,12 +386,20 @@ if (Meteor.isClient) {
Users.helpers({
boards() {
- return Boards.find({ 'members.userId': this._id });
+ return Boards.find(
+ { 'members.userId': this._id },
+ { sort: { sort: 1 /* boards default sorting */ } },
+ );
},
starredBoards() {
const { starredBoards = [] } = this.profile || {};
- return Boards.find({ archived: false, _id: { $in: starredBoards } });
+ return Boards.find(
+ { archived: false, _id: { $in: starredBoards } },
+ {
+ sort: { sort: 1 /* boards default sorting */ },
+ },
+ );
},
hasStarred(boardId) {
@@ -401,7 +409,12 @@ Users.helpers({
invitedBoards() {
const { invitedBoards = [] } = this.profile || {};
- return Boards.find({ archived: false, _id: { $in: invitedBoards } });
+ return Boards.find(
+ { archived: false, _id: { $in: invitedBoards } },
+ {
+ sort: { sort: 1 /* boards default sorting */ },
+ },
+ );
},
isInvitedTo(boardId) {
@@ -1292,10 +1305,13 @@ if (Meteor.isServer) {
let data = Meteor.users.findOne({ _id: id });
if (data !== undefined) {
if (action === 'takeOwnership') {
- data = Boards.find({
- 'members.userId': id,
- 'members.isAdmin': true,
- }).map(function(board) {
+ data = Boards.find(
+ {
+ 'members.userId': id,
+ 'members.isAdmin': true,
+ },
+ { sort: { sort: 1 /* boards default sorting */ } },
+ ).map(function(board) {
if (board.hasMember(req.userId)) {
board.removeMember(req.userId);
}
diff --git a/server/migrations.js b/server/migrations.js
index b4489987..21b54bda 100644
--- a/server/migrations.js
+++ b/server/migrations.js
@@ -1033,3 +1033,15 @@ Migrations.add('add-description-text-allowed', () => {
noValidateMulti,
);
});
+
+Migrations.add('add-sort-field-to-boards', () => {
+ Boards.find().forEach((board, index) => {
+ if (!board.hasOwnProperty('sort')) {
+ Boards.direct.update(
+ board._id,
+ { $set: { sort: index } },
+ noValidate
+ );
+ }
+ });
+});
diff --git a/server/publications/boards.js b/server/publications/boards.js
index 6fbd9860..b80a6b23 100644
--- a/server/publications/boards.js
+++ b/server/publications/boards.js
@@ -35,7 +35,9 @@ Meteor.publish('boards', function() {
members: 1,
permission: 1,
type: 1,
+ sort: 1,
},
+ sort: { sort: 1 /* boards default sorting */ },
},
);
});
@@ -61,6 +63,7 @@ Meteor.publish('archivedBoards', function() {
slug: 1,
title: 1,
},
+ sort: { sort: 1 /* boards default sorting */ },
},
);
});
@@ -90,7 +93,7 @@ Meteor.publishRelations('board', function(boardId, isArchived) {
$or,
// Sort required to ensure oplog usage
},
- { limit: 1, sort: { _id: 1 } },
+ { limit: 1, sort: { sort: 1 /* boards default sorting */ } },
),
function(boardId, board) {
this.cursor(Lists.find({ boardId, archived: isArchived }));