summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/components/boards/boardBody.js2
-rw-r--r--client/components/boards/boardHeader.jade18
-rw-r--r--client/components/boards/boardHeader.js102
-rw-r--r--client/components/cards/minicard.jade11
-rw-r--r--client/components/cards/minicard.js3
-rw-r--r--client/components/cards/minicard.styl5
-rw-r--r--client/components/lists/list.js14
-rw-r--r--client/components/lists/list.styl26
-rw-r--r--client/components/lists/listHeader.jade7
-rw-r--r--client/components/lists/listHeader.js24
-rw-r--r--client/components/sidebar/sidebarFilters.jade4
-rw-r--r--client/components/sidebar/sidebarFilters.js4
-rw-r--r--client/components/sidebar/sidebarSearches.jade4
-rw-r--r--client/components/sidebar/sidebarSearches.js5
-rw-r--r--client/components/swimlanes/swimlaneHeader.jade2
-rw-r--r--client/components/swimlanes/swimlaneHeader.js6
-rw-r--r--client/components/swimlanes/swimlanes.jade18
-rw-r--r--client/components/swimlanes/swimlanes.js35
-rw-r--r--client/components/swimlanes/swimlanes.styl8
-rw-r--r--client/components/users/userHeader.jade5
-rw-r--r--client/components/users/userHeader.js6
-rw-r--r--client/lib/filter.js10
-rw-r--r--i18n/en.i18n.json14
-rw-r--r--models/boards.js17
-rw-r--r--models/cards.js28
-rw-r--r--models/export.js15
-rw-r--r--models/lists.js26
-rw-r--r--models/swimlanes.js15
-rw-r--r--models/users.js82
29 files changed, 475 insertions, 41 deletions
diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js
index d64636f4..47042ae7 100644
--- a/client/components/boards/boardBody.js
+++ b/client/components/boards/boardBody.js
@@ -89,7 +89,7 @@ BlazeComponent.extendComponent({
helper.append(list.clone());
return helper;
},
- handle: '.js-swimlane-header',
+ handle: '.js-swimlane-header-handle',
items: '.swimlane:not(.placeholder)',
placeholder: 'swimlane placeholder',
distance: 7,
diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade
index fe533f95..175cc2c2 100644
--- a/client/components/boards/boardHeader.jade
+++ b/client/components/boards/boardHeader.jade
@@ -77,6 +77,10 @@ template(name="boardHeaderBar")
i.fa.fa-archive
span {{_ 'archives'}}
+ if showSort
+ a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
+ i.fa(class="{{directionClass}}")
+ span {{_ 'sort'}}{{_ listSortShortDesc}}
a.board-header-btn.js-open-filter-view(
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
class="{{#if Filter.isActive}}emphasis{{/if}}")
@@ -194,6 +198,20 @@ template(name="createBoard")
| /
a.js-board-template {{_ 'template'}}
+template(name="listsortPopup")
+ h2
+ | {{_ 'list-sort-by'}}
+ hr
+ ul.pop-over-list
+ each value in allowedSortValues
+ li
+ a.js-sort-by(name="{{value.name}}")
+ if $eq sortby value.name
+ i(class="fa {{Direction}}")
+ | {{_ value.label }}{{_ value.shortLabel}}
+ if $eq sortby value.name
+ i(class="fa fa-check")
+
template(name="boardChangeTitlePopup")
form
label
diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js
index cb84c233..e14b1444 100644
--- a/client/components/boards/boardHeader.js
+++ b/client/components/boards/boardHeader.js
@@ -1,3 +1,5 @@
+const DOWNCLS = 'fa-sort-down';
+const UPCLS = 'fa-sort-up';
Template.boardMenuPopup.events({
'click .js-rename-board': Popup.open('boardChangeTitle'),
'click .js-custom-fields'() {
@@ -80,7 +82,25 @@ BlazeComponent.extendComponent({
const currentBoard = Boards.findOne(Session.get('currentBoard'));
return currentBoard && currentBoard.stars >= 2;
},
-
+ showSort() {
+ return Meteor.user().hasSortBy();
+ },
+ directionClass() {
+ return this.currentDirection() === -1 ? DOWNCLS : UPCLS;
+ },
+ changeDirection() {
+ const direction = 0 - this.currentDirection() === -1 ? '-' : '';
+ Meteor.call('setListSortBy', direction + this.currentListSortBy());
+ },
+ currentDirection() {
+ return Meteor.user().getListSortByDirection();
+ },
+ currentListSortBy() {
+ return Meteor.user().getListSortBy();
+ },
+ listSortShortDesc() {
+ return `list-label-short-${this.currentListSortBy()}`;
+ },
events() {
return [
{
@@ -118,6 +138,16 @@ BlazeComponent.extendComponent({
'click .js-open-filter-view'() {
Sidebar.setView('filter');
},
+ 'click .js-open-sort-view'(evt) {
+ const target = evt.target;
+ if (target.tagName === 'I') {
+ // click on the text, popup choices
+ this.changeDirection();
+ } else {
+ // change the sort order
+ Popup.open('listsort')(evt);
+ }
+ },
'click .js-filter-reset'(event) {
event.stopPropagation();
Sidebar.setView();
@@ -277,3 +307,73 @@ BlazeComponent.extendComponent({
];
},
}).register('boardChangeWatchPopup');
+
+BlazeComponent.extendComponent({
+ onCreated() {
+ //this.sortBy = new ReactiveVar();
+ ////this.sortDirection = new ReactiveVar();
+ //this.setSortBy();
+ this.downClass = DOWNCLS;
+ this.upClass = UPCLS;
+ },
+ allowedSortValues() {
+ const types = [];
+ const pushed = {};
+ Meteor.user()
+ .getListSortTypes()
+ .forEach(type => {
+ const key = type.replace(/^-/, '');
+ if (pushed[key] === undefined) {
+ types.push({
+ name: key,
+ label: `list-label-${key}`,
+ shortLabel: `list-label-short-${key}`,
+ });
+ pushed[key] = 1;
+ }
+ });
+ return types;
+ },
+ Direction() {
+ return Meteor.user().getListSortByDirection() === -1
+ ? this.downClass
+ : this.upClass;
+ },
+ sortby() {
+ return Meteor.user().getListSortBy();
+ },
+
+ setSortBy(type = null) {
+ const user = Meteor.user();
+ if (type === null) {
+ type = user._getListSortBy();
+ } else {
+ let value = '';
+ if (type.map) {
+ // is an array
+ value = (type[1] === -1 ? '-' : '') + type[0];
+ }
+ Meteor.call('setListSortBy', value);
+ }
+ //this.sortBy.set(type[0]);
+ //this.sortDirection.set(type[1]);
+ },
+
+ events() {
+ return [
+ {
+ 'click .js-sort-by'(evt) {
+ evt.preventDefault();
+ const target = evt.target;
+ const sortby = target.getAttribute('name');
+ const down = !!target.querySelector(`.${this.upClass}`);
+ const direction = down ? -1 : 1;
+ this.setSortBy([sortby, direction]);
+ if (Utils.isMiniScreen) {
+ Popup.close();
+ }
+ },
+ },
+ ];
+ },
+}).register('listsortPopup');
diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade
index 3806ce41..ba0c5707 100644
--- a/client/components/cards/minicard.jade
+++ b/client/components/cards/minicard.jade
@@ -3,6 +3,13 @@ template(name="minicard")
class="{{#if isLinkedCard}}linked-card{{/if}}"
class="{{#if isLinkedBoard}}linked-board{{/if}}"
class="minicard-{{colorClass}}")
+ if isMiniScreen
+ .handle
+ .fa.fa-arrows
+ unless isMiniScreen
+ if showDesktopDragHandles
+ .handle
+ .fa.fa-arrows
if cover
.minicard-cover(style="background-image: url('{{cover.url}}');")
if labels
@@ -15,8 +22,6 @@ template(name="minicard")
if hiddenMinicardLabelText
.minicard-label(class="card-label-{{color}}" title="{{name}}")
.minicard-title
- .handle
- .fa.fa-arrows
if $eq 'prefix-with-full-path' currentBoard.presentParentTask
.parent-prefix
| {{ parentString ' > ' }}
@@ -53,6 +58,8 @@ template(name="minicard")
if getDue
.date
+minicardDueDate
+ if getEnd
+ +minicardEndDate
if getSpentTime
.date
+cardSpentTime
diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js
index 4c25c11d..4c76db46 100644
--- a/client/components/cards/minicard.js
+++ b/client/components/cards/minicard.js
@@ -26,6 +26,9 @@ BlazeComponent.extendComponent({
}).register('minicard');
Template.minicard.helpers({
+ showDesktopDragHandles() {
+ return Meteor.user().hasShowDesktopDragHandles();
+ },
hiddenMinicardLabelText() {
return Meteor.user().hasHiddenMinicardLabelText();
},
diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl
index 7c27cba1..9997fd5f 100644
--- a/client/components/cards/minicard.styl
+++ b/client/components/cards/minicard.styl
@@ -105,8 +105,7 @@
right: 5px;
top: 5px;
display:none;
- // @media only screen and (max-width: 1199px) {
- @media only screen and (max-width: 800px) {
+ @media only screen {
display:block;
}
.fa-arrows
@@ -128,7 +127,7 @@
.badges
float: left
margin-top: 7px
- color: darken(white, 80%)
+ color: darken(white, 50%)
&:empty
display: none
diff --git a/client/components/lists/list.js b/client/components/lists/list.js
index bde43520..b7b8b2e0 100644
--- a/client/components/lists/list.js
+++ b/client/components/lists/list.js
@@ -31,7 +31,13 @@ BlazeComponent.extendComponent({
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
- if (Utils.isMiniScreen()) {
+ if (Utils.isMiniScreen) {
+ $('.js-minicards').sortable({
+ handle: '.handle',
+ });
+ }
+
+ if (!Utils.isMiniScreen && showDesktopDragHandles) {
$('.js-minicards').sortable({
handle: '.handle',
});
@@ -155,6 +161,12 @@ BlazeComponent.extendComponent({
},
}).register('list');
+Template.list.helpers({
+ showDesktopDragHandles() {
+ return Meteor.user().hasShowDesktopDragHandles();
+ },
+});
+
Template.miniList.events({
'click .js-select-list'() {
const listId = this._id;
diff --git a/client/components/lists/list.styl b/client/components/lists/list.styl
index 81938c1a..0d3ccfce 100644
--- a/client/components/lists/list.styl
+++ b/client/components/lists/list.styl
@@ -84,17 +84,16 @@
padding-left: 10px
color: #a6a6a6
-
.list-header-menu
position: absolute
padding: 27px 19px
margin-top: 1px
top: -7px
- right: -7px
+ right: 3px
.list-header-plus-icon
color: #a6a6a6
- margin-right: 10px
+ margin-right: 15px
.highlight
color: #ce1414
@@ -165,7 +164,16 @@
@media screen and (max-width: 800px)
.list-header-menu
- margin-right: 30px
+ position: absolute
+ padding: 27px 19px
+ margin-top: 1px
+ top: -7px
+ margin-right: 50px
+ right: -3px
+
+ .list-header
+ .list-header-name
+ margin-left: 1.4rem
.mini-list
flex: 0 0 60px
@@ -221,9 +229,17 @@
padding: 7px
top: 50%
transform: translateY(-50%)
- right: 17px
+ margin-right: 27px
font-size: 20px
+ .list-header-menu-handle
+ position: absolute
+ padding: 7px
+ top: 50%
+ transform: translateY(-50%)
+ right: 10px
+ font-size: 24px
+
.link-board-wrapper
display: flex
align-items: baseline
diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade
index f930e57a..3b3a0242 100644
--- a/client/components/lists/listHeader.jade
+++ b/client/components/lists/listHeader.jade
@@ -9,6 +9,7 @@ template(name="listHeader")
if currentList
a.list-header-left-icon.fa.fa-angle-left.js-unselect-list
h2.list-header-name(
+ title="{{ moment modifiedAt 'LLL' }}"
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}js-open-inlined-form is-editable{{/unless}}{{/if}}")
+viewer
= title
@@ -29,16 +30,22 @@ template(name="listHeader")
if canSeeAddCard
a.js-add-card.fa.fa-plus.list-header-plus-icon
a.fa.fa-navicon.js-open-list-menu
+ a.list-header-menu-handle.handle.fa.fa-arrows.js-list-handle
else
a.list-header-menu-icon.fa.fa-angle-right.js-select-list
+ a.list-header-menu-handle.handle.fa.fa-arrows.js-list-handle
else if currentUser.isBoardMember
if isWatching
i.list-header-watch-icon.fa.fa-eye
div.list-header-menu
unless currentUser.isCommentOnly
+ if isBoardAdmin
+ a.fa.js-list-star.list-header-plus-icon(class="fa-star{{#unless starred}}-o{{/unless}}")
if canSeeAddCard
a.js-add-card.fa.fa-plus.list-header-plus-icon
a.fa.fa-navicon.js-open-list-menu
+ if showDesktopDragHandles
+ a.list-header-menu-handle.handle.fa.fa-arrows.js-list-handle
template(name="editListTitleForm")
.list-composer
diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js
index e8a82499..b524d4e0 100644
--- a/client/components/lists/listHeader.js
+++ b/client/components/lists/listHeader.js
@@ -13,6 +13,20 @@ BlazeComponent.extendComponent({
);
},
+ isBoardAdmin() {
+ return Meteor.user().isBoardAdmin();
+ },
+ starred(check = undefined) {
+ const list = Template.currentData();
+ const status = list.isStarred();
+ if (check === undefined) {
+ // just check
+ return status;
+ } else {
+ list.star(!status);
+ return !status;
+ }
+ },
editTitle(event) {
event.preventDefault();
const newTitle = this.childComponents('inlinedForm')[0]
@@ -61,6 +75,10 @@ BlazeComponent.extendComponent({
events() {
return [
{
+ 'click .js-list-star'(event) {
+ event.preventDefault();
+ this.starred(!this.starred());
+ },
'click .js-open-list-menu': Popup.open('listAction'),
'click .js-add-card'(event) {
const listDom = $(event.target).parents(
@@ -80,6 +98,12 @@ BlazeComponent.extendComponent({
},
}).register('listHeader');
+Template.listHeader.helpers({
+ showDesktopDragHandles() {
+ return Meteor.user().hasShowDesktopDragHandles();
+ },
+});
+
Template.listActionPopup.helpers({
isWipLimitEnabled() {
return Template.currentData().getWipLimit('enabled');
diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade
index 55ab213a..5f929cb9 100644
--- a/client/components/sidebar/sidebarFilters.jade
+++ b/client/components/sidebar/sidebarFilters.jade
@@ -5,6 +5,10 @@
template(name="filterSidebar")
ul.sidebar-list
+ span {{_ 'list-filter-label'}}
+ form.js-list-filter
+ input(type="text")
+ ul.sidebar-list
li(class="{{#if Filter.labelIds.isSelected undefined}}active{{/if}}")
a.name.js-toggle-label-filter
span.sidebar-list-item-description
diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js
index 3483d00c..ee0176b9 100644
--- a/client/components/sidebar/sidebarFilters.js
+++ b/client/components/sidebar/sidebarFilters.js
@@ -4,6 +4,10 @@ BlazeComponent.extendComponent({
events() {
return [
{
+ 'submit .js-list-filter'(evt) {
+ evt.preventDefault();
+ Filter.lists.set(this.find('.js-list-filter input').value.trim());
+ },
'click .js-toggle-label-filter'(evt) {
evt.preventDefault();
Filter.labelIds.toggle(this.currentData()._id);
diff --git a/client/components/sidebar/sidebarSearches.jade b/client/components/sidebar/sidebarSearches.jade
index 96877c50..4ee7fc9c 100644
--- a/client/components/sidebar/sidebarSearches.jade
+++ b/client/components/sidebar/sidebarSearches.jade
@@ -2,6 +2,10 @@ template(name="searchSidebar")
form.js-search-term-form
input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
.list-body.js-perfect-scrollbar
+ .minilists.clearfix.js-minilists
+ each (lists)
+ a.minilist-wrapper.js-minilist(href=absoluteUrl)
+ +minilist(this)
.minicards.clearfix.js-minicards
each (results)
a.minicard-wrapper.js-minicard(href=absoluteUrl)
diff --git a/client/components/sidebar/sidebarSearches.js b/client/components/sidebar/sidebarSearches.js
index 8944c04e..02677260 100644
--- a/client/components/sidebar/sidebarSearches.js
+++ b/client/components/sidebar/sidebarSearches.js
@@ -8,6 +8,11 @@ BlazeComponent.extendComponent({
return currentBoard.searchCards(this.term.get());
},
+ lists() {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ return currentBoard.searchLists(this.term.get());
+ },
+
events() {
return [
{
diff --git a/client/components/swimlanes/swimlaneHeader.jade b/client/components/swimlanes/swimlaneHeader.jade
index 8c6aa5a3..fb6ef21d 100644
--- a/client/components/swimlanes/swimlaneHeader.jade
+++ b/client/components/swimlanes/swimlaneHeader.jade
@@ -16,6 +16,8 @@ template(name="swimlaneFixedHeader")
unless currentUser.isCommentOnly
a.fa.fa-plus.js-open-add-swimlane-menu.swimlane-header-plus-icon
a.fa.fa-navicon.js-open-swimlane-menu
+ if showDesktopDragHandles
+ a.swimlane-header-menu-handle.handle.fa.fa-arrows.js-swimlane-header-handle
template(name="editSwimlaneTitleForm")
.list-composer
diff --git a/client/components/swimlanes/swimlaneHeader.js b/client/components/swimlanes/swimlaneHeader.js
index ee21d100..6f8029fd 100644
--- a/client/components/swimlanes/swimlaneHeader.js
+++ b/client/components/swimlanes/swimlaneHeader.js
@@ -28,6 +28,12 @@ BlazeComponent.extendComponent({
},
}).register('swimlaneHeader');
+Template.swimlaneHeader.helpers({
+ showDesktopDragHandles() {
+ return Meteor.user().hasShowDesktopDragHandles();
+ },
+});
+
Template.swimlaneActionPopup.events({
'click .js-set-swimlane-color': Popup.open('setSwimlaneColor'),
'click .js-close-swimlane'(event) {
diff --git a/client/components/swimlanes/swimlanes.jade b/client/components/swimlanes/swimlanes.jade
index 3ad43777..98af6d54 100644
--- a/client/components/swimlanes/swimlanes.jade
+++ b/client/components/swimlanes/swimlanes.jade
@@ -12,13 +12,13 @@ template(name="swimlane")
unless currentUser.isCommentOnly
+addListForm
else
+ if currentUser.isBoardMember
+ unless currentUser.isCommentOnly
+ +addListForm
each lists
+list(this)
if currentCardIsInThisList _id ../_id
+cardDetails(currentCard)
- if currentUser.isBoardMember
- unless currentUser.isCommentOnly
- +addListForm
template(name="listsGroup")
.swimlane.list-group.js-lists
@@ -26,23 +26,23 @@ template(name="listsGroup")
if currentList
+list(currentList)
else
- each lists
- +miniList(this)
if currentUser.isBoardMember
unless currentUser.isCommentOnly
+addListForm
+ each lists
+ +miniList(this)
else
+ if currentUser.isBoardMember
+ unless currentUser.isCommentOnly
+ +addListForm
each lists
if visible this
+list(this)
if currentCardIsInThisList _id null
+cardDetails(currentCard)
- if currentUser.isBoardMember
- unless currentUser.isCommentOnly
- +addListForm
template(name="addListForm")
- .list.list-composer.js-list-composer
+ .list.list-composer.js-list-composer(class="{{#if isMiniScreen}}mini-list{{/if}}")
.list-header-add
+inlinedForm(autoclose=false)
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js
index e0857003..8faad870 100644
--- a/client/components/swimlanes/swimlanes.js
+++ b/client/components/swimlanes/swimlanes.js
@@ -53,10 +53,21 @@ function initSortable(boardComponent, $listsDom) {
},
};
+ if (Utils.isMiniScreen) {
+ $listsDom.sortable({
+ handle: '.js-list-handle',
+ });
+ }
+
+ if (!Utils.isMiniScreen && showDesktopDragHandles) {
+ $listsDom.sortable({
+ handle: '.js-list-header',
+ });
+ }
+
$listsDom.sortable({
tolerance: 'pointer',
helper: 'clone',
- handle: '.js-list-header',
items: '.js-list:not(.js-list-composer)',
placeholder: 'list placeholder',
distance: 7,
@@ -151,13 +162,13 @@ BlazeComponent.extendComponent({
// 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',
- ];
+
+ const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
+ Util.isMiniScreen || (!Util.isMiniScreen && showDesktopDragHandles)
+ ? ['.js-list-handle', '.js-swimlane-header-handle']
+ : ['.js-list-header'],
+ );
+
if (
$(evt.target).closest(noDragInside.join(',')).length === 0 &&
this.$('.swimlane').prop('clientHeight') > evt.offsetY
@@ -233,6 +244,9 @@ BlazeComponent.extendComponent({
}).register('addListForm');
Template.swimlane.helpers({
+ showDesktopDragHandles() {
+ return Meteor.user().hasShowDesktopDragHandles();
+ },
canSeeAddList() {
return (
Meteor.user() &&
@@ -253,6 +267,11 @@ BlazeComponent.extendComponent({
return false;
}
}
+ if (Filter.lists._isActive()) {
+ if (!list.title.match(Filter.lists.getRegexSelector())) {
+ return false;
+ }
+ }
if (Filter.hideEmpty.isSelected()) {
const swimlaneId = this.parentComponent()
.parentComponent()
diff --git a/client/components/swimlanes/swimlanes.styl b/client/components/swimlanes/swimlanes.styl
index 1056e1e3..503091ee 100644
--- a/client/components/swimlanes/swimlanes.styl
+++ b/client/components/swimlanes/swimlanes.styl
@@ -50,6 +50,14 @@
margin-left: 5px
margin-right: 10px
+ .swimlane-header-menu-handle
+ position: absolute
+ padding: 7px
+ top: 50%
+ transform: translateY(-50%)
+ left: 300px
+ font-size: 18px
+
.list-group
height: 100%
diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade
index 946bdab1..50a80396 100644
--- a/client/components/users/userHeader.jade
+++ b/client/components/users/userHeader.jade
@@ -79,6 +79,11 @@ template(name="changeSettingsPopup")
if hiddenSystemMessages
i.fa.fa-check
li
+ a.js-toggle-desktop-drag-handles
+ | {{_ 'show-desktop-drag-handles'}}
+ if showDesktopDragHandles
+ i.fa.fa-check
+ li
label.bold
| {{_ 'show-cards-minimum-count'}}
input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="0" max="99" onkeydown="return false")
diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js
index 36fb2020..194f990f 100644
--- a/client/components/users/userHeader.js
+++ b/client/components/users/userHeader.js
@@ -161,6 +161,9 @@ Template.changeLanguagePopup.events({
});
Template.changeSettingsPopup.helpers({
+ showDesktopDragHandles() {
+ return Meteor.user().hasShowDesktopDragHandles();
+ },
hiddenSystemMessages() {
return Meteor.user().hasHiddenSystemMessages();
},
@@ -170,6 +173,9 @@ Template.changeSettingsPopup.helpers({
});
Template.changeSettingsPopup.events({
+ 'click .js-toggle-desktop-drag-handles'() {
+ Meteor.call('toggleDesktopDragHandles');
+ },
'click .js-toggle-system-messages'() {
Meteor.call('toggleSystemMessages');
},
diff --git a/client/lib/filter.js b/client/lib/filter.js
index 1ca3a280..9ddea65c 100644
--- a/client/lib/filter.js
+++ b/client/lib/filter.js
@@ -439,6 +439,14 @@ class AdvancedFilter {
const commands = this._filterToCommands();
return this._arrayToSelector(commands);
}
+ getRegexSelector() {
+ // generate a regex for filter list
+ this._dep.depend();
+ return new RegExp(
+ `^.*${this._filter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*$`,
+ 'i',
+ );
+ }
}
// The global Filter object.
@@ -455,6 +463,7 @@ Filter = {
hideEmpty: new SetFilter(),
customFields: new SetFilter('_id'),
advanced: new AdvancedFilter(),
+ lists: new AdvancedFilter(), // we need the ability to filter list by name as well
_fields: ['labelIds', 'members', 'archive', 'hideEmpty', 'customFields'],
@@ -533,6 +542,7 @@ Filter = {
const filter = this[fieldName];
filter.reset();
});
+ this.lists.reset();
this.advanced.reset();
this.resetExceptions();
},
diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json
index 58fda954..dd8b7130 100644
--- a/i18n/en.i18n.json
+++ b/i18n/en.i18n.json
@@ -300,8 +300,18 @@
"error-username-taken": "This username is already taken",
"error-email-taken": "Email has already been taken",
"export-board": "Export board",
+ "sort": "Sort",
+ "sort-desc": "Click to Sort List",
+ "list-sort-by": "Sort the List By:",
+ "list-label-modifiedAt": "Last Access Time",
+ "list-label-title": "Name of the List",
+ "list-label-sort": "Your Manual Order",
+ "list-label-short-modifiedAt": "(L)",
+ "list-label-short-title": "(N)",
+ "list-label-short-sort": "(M)",
"filter": "Filter",
- "filter-cards": "Filter Cards",
+ "filter-cards": "Filter Cards or Lists",
+ "list-filter-label": "Filter List by Title",
"filter-clear": "Clear filter",
"filter-no-label": "No label",
"filter-no-member": "No member",
@@ -426,7 +436,7 @@
"save": "Save",
"search": "Search",
"rules": "Rules",
- "search-cards": "Search from card titles and descriptions on this board",
+ "search-cards": "Search from card/list titles and descriptions on this board",
"search-example": "Text to search for?",
"select-color": "Select Color",
"set-wip-limit-value": "Set a limit for the maximum number of tasks in this list",
diff --git a/models/boards.js b/models/boards.js
index a9348478..85a7558c 100644
--- a/models/boards.js
+++ b/models/boards.js
@@ -409,6 +409,23 @@ Boards.helpers({
},
lists() {
+ const enabled = Meteor.user().hasSortBy();
+ return enabled ? this.newestLists() : this.draggableLists();
+ },
+
+ newestLists() {
+ // sorted lists from newest to the oldest, by its creation date or its cards' last modification date
+ const value = Meteor.user()._getListSortBy();
+ const sortKey = { starred: -1, [value[0]]: value[1] }; // [["starred",-1],value];
+ return Lists.find(
+ {
+ boardId: this._id,
+ archived: false,
+ },
+ { sort: sortKey },
+ );
+ },
+ draggableLists() {
return Lists.find({ boardId: this._id }, { sort: { sort: 1 } });
},
diff --git a/models/cards.js b/models/cards.js
index 635a4e72..27dda0ee 100644
--- a/models/cards.js
+++ b/models/cards.js
@@ -1695,6 +1695,25 @@ if (Meteor.isServer) {
const oldvalue = doc[action] || '';
const activityType = `a-${action}`;
const card = Cards.findOne(doc._id);
+ const list = card.list();
+ if (list && action === 'endAt') {
+ // change list modifiedAt
+ const modifiedAt = new Date(
+ new Date(value).getTime() - 365 * 24 * 3600 * 1e3,
+ ); // set it as 1 year before
+ const boardId = list.boardId;
+ Lists.direct.update(
+ {
+ _id: list._id,
+ },
+ {
+ $set: {
+ modifiedAt,
+ boardId,
+ },
+ },
+ );
+ }
const username = Users.findOne(userId).username;
const activity = {
userId,
@@ -1852,15 +1871,8 @@ if (Meteor.isServer) {
const check = Users.findOne({
_id: req.body.authorId,
});
+ const members = req.body.members || [req.body.authorId];
if (typeof check !== 'undefined') {
- let members = req.body.members || [];
- if (_.isString(members)) {
- if (members === '') {
- members = [];
- } else {
- members = [members];
- }
- }
const id = Cards.direct.insert({
title: req.body.title,
boardId: paramBoardId,
diff --git a/models/export.js b/models/export.js
index a69be970..056eefdc 100644
--- a/models/export.js
+++ b/models/export.js
@@ -50,12 +50,18 @@ if (Meteor.isServer) {
});
}
+// exporter maybe is broken since Gridfs introduced, add fs and path
+
export class Exporter {
constructor(boardId) {
this._boardId = boardId;
}
build() {
+ const fs = Npm.require('fs');
+ const os = Npm.require('os');
+ const path = Npm.require('path');
+
const byBoard = { boardId: this._boardId };
const byBoardNoLinked = {
boardId: this._boardId,
@@ -134,6 +140,11 @@ export class Exporter {
const getBase64Data = function(doc, callback) {
let buffer = new Buffer(0);
// callback has the form function (err, res) {}
+ const tmpFile = path.join(
+ os.tmpdir(),
+ `tmpexport${process.pid}${Math.random()}`,
+ );
+ const tmpWriteable = fs.createWriteStream(tmpFile);
const readStream = doc.createReadStream();
readStream.on('data', function(chunk) {
buffer = Buffer.concat([buffer, chunk]);
@@ -143,8 +154,12 @@ export class Exporter {
});
readStream.on('end', function() {
// done
+ fs.unlink(tmpFile, () => {
+ //ignored
+ });
callback(null, buffer.toString('base64'));
});
+ readStream.pipe(tmpWriteable);
};
const getBase64DataSync = Meteor.wrapAsync(getBase64Data);
result.attachments = Attachments.find(byBoard)
diff --git a/models/lists.js b/models/lists.js
index 9136c337..f06b15b1 100644
--- a/models/lists.js
+++ b/models/lists.js
@@ -11,6 +11,15 @@ Lists.attachSchema(
*/
type: String,
},
+ starred: {
+ /**
+ * if a list is stared
+ * then we put it on the top
+ */
+ type: Boolean,
+ optional: true,
+ defaultValue: false,
+ },
archived: {
/**
* is the list archived
@@ -81,10 +90,14 @@ Lists.attachSchema(
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
- if (this.isInsert || this.isUpsert || this.isUpdate) {
+ // this is redundant with updatedAt
+ /*if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
+ }*/
+ if (!this.isSet) {
+ return new Date();
}
},
},
@@ -252,6 +265,14 @@ Lists.helpers({
return this.type === 'template-list';
},
+ isStarred() {
+ return this.starred === true;
+ },
+
+ absoluteUrl() {
+ const card = Cards.findOne({ listId: this._id });
+ return card && card.absoluteUrl();
+ },
remove() {
Lists.remove({ _id: this._id });
},
@@ -261,6 +282,9 @@ Lists.mutations({
rename(title) {
return { $set: { title } };
},
+ star(enable = true) {
+ return { $set: { starred: !!enable } };
+ },
archive() {
if (this.isTemplateList()) {
diff --git a/models/swimlanes.js b/models/swimlanes.js
index 46e410da..831f1eff 100644
--- a/models/swimlanes.js
+++ b/models/swimlanes.js
@@ -174,6 +174,21 @@ Swimlanes.helpers({
},
lists() {
+ const enabled = Meteor.user().hasSortBy();
+ return enabled ? this.newestLists() : this.draggableLists();
+ },
+ newestLists() {
+ // sorted lists from newest to the oldest, by its creation date or its cards' last modification date
+ return Lists.find(
+ {
+ boardId: this.boardId,
+ swimlaneId: { $in: [this._id, ''] },
+ archived: false,
+ },
+ { sort: { modifiedAt: -1 } },
+ );
+ },
+ draggableLists() {
return Lists.find(
{
boardId: this.boardId,
diff --git a/models/users.js b/models/users.js
index 9147322c..83a224ba 100644
--- a/models/users.js
+++ b/models/users.js
@@ -4,6 +4,16 @@ const isSandstorm =
Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm;
Users = Meteor.users;
+const allowedSortValues = [
+ '-modifiedAt',
+ 'modifiedAt',
+ '-title',
+ 'title',
+ '-sort',
+ 'sort',
+];
+const defaultSortBy = allowedSortValues[0];
+
/**
* A User in wekan
*/
@@ -109,6 +119,13 @@ Users.attachSchema(
type: String,
optional: true,
},
+ 'profile.showDesktopDragHandles': {
+ /**
+ * does the user want to hide system messages?
+ */
+ type: Boolean,
+ optional: true,
+ },
'profile.hiddenSystemMessages': {
/**
* does the user want to hide system messages?
@@ -184,6 +201,15 @@ Users.attachSchema(
'board-view-cal',
],
},
+ 'profile.listSortBy': {
+ /**
+ * default sort list for user
+ */
+ type: String,
+ optional: true,
+ defaultValue: defaultSortBy,
+ allowedValues: allowedSortValues,
+ },
'profile.templatesBoardId': {
/**
* Reference to the templates board
@@ -358,6 +384,31 @@ Users.helpers({
return _.contains(invitedBoards, boardId);
},
+ _getListSortBy() {
+ const profile = this.profile || {};
+ const sortBy = profile.listSortBy || defaultSortBy;
+ const keyPattern = /^(-{0,1})(.*$)/;
+ const ret = [];
+ if (keyPattern.exec(sortBy)) {
+ ret[0] = RegExp.$2;
+ ret[1] = RegExp.$1 ? -1 : 1;
+ }
+ return ret;
+ },
+ hasSortBy() {
+ // if use doesn't have dragHandle, then we can let user to choose sort list by different order
+ return !this.hasShowDesktopDragHandles();
+ },
+ getListSortBy() {
+ return this._getListSortBy()[0];
+ },
+ getListSortTypes() {
+ return allowedSortValues;
+ },
+ getListSortByDirection() {
+ return this._getListSortBy()[1];
+ },
+
hasTag(tag) {
const { tags = [] } = this.profile || {};
return _.contains(tags, tag);
@@ -368,6 +419,11 @@ Users.helpers({
return _.contains(notifications, activityId);
},
+ hasShowDesktopDragHandles() {
+ const profile = this.profile || {};
+ return profile.showDesktopDragHandles || false;
+ },
+
hasHiddenSystemMessages() {
const profile = this.profile || {};
return profile.hiddenSystemMessages || false;
@@ -473,6 +529,21 @@ Users.mutations({
else this.addTag(tag);
},
+ setListSortBy(value) {
+ return {
+ $set: {
+ 'profile.listSortBy': value,
+ },
+ };
+ },
+ toggleDesktopHandles(value = false) {
+ return {
+ $set: {
+ 'profile.showDesktopDragHandles': !value,
+ },
+ };
+ },
+
toggleSystem(value = false) {
return {
$set: {
@@ -549,6 +620,14 @@ Meteor.methods({
Users.update(userId, { $set: { username } });
}
},
+ setListSortBy(value) {
+ check(value, String);
+ Meteor.user().setListSortBy(value);
+ },
+ toggleDesktopDragHandles() {
+ const user = Meteor.user();
+ user.toggleDesktopHandles(user.hasShowDesktopDragHandles());
+ },
toggleSystemMessages() {
const user = Meteor.user();
user.toggleSystem(user.hasHiddenSystemMessages());
@@ -776,6 +855,9 @@ if (Meteor.isServer) {
if (Meteor.isServer) {
// Let mongoDB ensure username unicity
Meteor.startup(() => {
+ allowedSortValues.forEach(value => {
+ Lists._collection._ensureIndex(value);
+ });
Users._collection._ensureIndex({ modifiedAt: -1 });
Users._collection._ensureIndex(
{