summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
authorThiago Fernando <thiagofernando@outlook.com>2019-05-10 14:54:25 -0300
committerGitHub <noreply@github.com>2019-05-10 14:54:25 -0300
commitce0473480bab3fc621d4baecfff0f413e21b5e2c (patch)
tree06d724b4f80885bdd13137f7977d7a914cca0138 /client
parentc43508cacbd64357409a3de114db9dab2ae59a9d (diff)
parent7ff4067e88ed59686c86d81447fa2ce550032034 (diff)
downloadwekan-ce0473480bab3fc621d4baecfff0f413e21b5e2c.tar.gz
wekan-ce0473480bab3fc621d4baecfff0f413e21b5e2c.tar.bz2
wekan-ce0473480bab3fc621d4baecfff0f413e21b5e2c.zip
Merge pull request #1 from wekan/devel
ldap changes
Diffstat (limited to 'client')
-rw-r--r--client/components/activities/activities.jade69
-rw-r--r--client/components/activities/activities.js63
-rw-r--r--client/components/activities/comments.js11
-rw-r--r--client/components/boards/boardArchive.jade16
-rw-r--r--client/components/boards/boardArchive.js17
-rw-r--r--client/components/boards/boardBody.jade20
-rw-r--r--client/components/boards/boardBody.js216
-rw-r--r--client/components/boards/boardBody.styl7
-rw-r--r--client/components/boards/boardHeader.jade203
-rw-r--r--client/components/boards/boardHeader.js101
-rw-r--r--client/components/boards/boardHeader.styl19
-rw-r--r--client/components/boards/boardsList.jade13
-rw-r--r--client/components/boards/boardsList.js36
-rw-r--r--client/components/boards/boardsList.styl14
-rw-r--r--client/components/boards/miniboard.jade8
-rw-r--r--client/components/cards/attachments.js9
-rw-r--r--client/components/cards/cardCustomFields.jade76
-rw-r--r--client/components/cards/cardCustomFields.js179
-rw-r--r--client/components/cards/cardDate.jade16
-rw-r--r--client/components/cards/cardDate.js99
-rw-r--r--client/components/cards/cardDate.styl23
-rw-r--r--client/components/cards/cardDetails.jade209
-rw-r--r--client/components/cards/cardDetails.js409
-rw-r--r--client/components/cards/cardDetails.styl114
-rw-r--r--client/components/cards/cardTime.jade8
-rw-r--r--client/components/cards/cardTime.js19
-rw-r--r--client/components/cards/checklists.jade7
-rw-r--r--client/components/cards/checklists.js13
-rw-r--r--client/components/cards/labels.styl54
-rw-r--r--client/components/cards/minicard.jade71
-rw-r--r--client/components/cards/minicard.js11
-rw-r--r--client/components/cards/minicard.styl124
-rw-r--r--client/components/cards/subtasks.jade97
-rw-r--r--client/components/cards/subtasks.js146
-rw-r--r--client/components/cards/subtasks.styl142
-rw-r--r--client/components/forms/datepicker.jade15
-rw-r--r--client/components/forms/datepicker.styl17
-rw-r--r--client/components/forms/forms.styl8
-rw-r--r--client/components/import/import.jade4
-rw-r--r--client/components/lists/list.js25
-rw-r--r--client/components/lists/list.styl137
-rw-r--r--client/components/lists/listBody.jade93
-rw-r--r--client/components/lists/listBody.js430
-rw-r--r--client/components/lists/listHeader.jade22
-rw-r--r--client/components/lists/listHeader.js47
-rw-r--r--client/components/lists/minilist.jade8
-rw-r--r--client/components/main/editor.jade3
-rwxr-xr-xclient/components/main/editor.js9
-rw-r--r--client/components/main/header.jade73
-rw-r--r--client/components/main/header.js5
-rw-r--r--client/components/main/header.styl4
-rw-r--r--client/components/main/layouts.jade59
-rw-r--r--client/components/main/layouts.js115
-rw-r--r--client/components/main/layouts.styl123
-rw-r--r--client/components/main/popup.styl3
-rw-r--r--client/components/mixins/perfectScrollbar.js18
-rw-r--r--client/components/rules/.DS_Storebin0 -> 6148 bytes
-rw-r--r--client/components/rules/actions/boardActions.jade72
-rw-r--r--client/components/rules/actions/boardActions.js168
-rw-r--r--client/components/rules/actions/cardActions.jade55
-rw-r--r--client/components/rules/actions/cardActions.js188
-rw-r--r--client/components/rules/actions/checklistActions.jade70
-rw-r--r--client/components/rules/actions/checklistActions.js151
-rw-r--r--client/components/rules/actions/mailActions.jade11
-rw-r--r--client/components/rules/actions/mailActions.js35
-rw-r--r--client/components/rules/ruleDetails.jade20
-rw-r--r--client/components/rules/ruleDetails.js38
-rw-r--r--client/components/rules/rules.styl190
-rw-r--r--client/components/rules/rulesActions.jade29
-rw-r--r--client/components/rules/rulesActions.js58
-rw-r--r--client/components/rules/rulesList.jade27
-rw-r--r--client/components/rules/rulesList.js15
-rw-r--r--client/components/rules/rulesMain.jade9
-rw-r--r--client/components/rules/rulesMain.js97
-rw-r--r--client/components/rules/rulesTriggers.jade25
-rw-r--r--client/components/rules/rulesTriggers.js53
-rw-r--r--client/components/rules/triggers/boardTriggers.jade116
-rw-r--r--client/components/rules/triggers/boardTriggers.js119
-rw-r--r--client/components/rules/triggers/cardTriggers.jade114
-rw-r--r--client/components/rules/triggers/cardTriggers.js131
-rw-r--r--client/components/rules/triggers/checklistTriggers.jade125
-rw-r--r--client/components/rules/triggers/checklistTriggers.js146
-rw-r--r--client/components/settings/connectionMethod.jade9
-rw-r--r--client/components/settings/connectionMethod.js37
-rw-r--r--client/components/settings/informationBody.jade2
-rw-r--r--client/components/settings/invitationCode.js17
-rw-r--r--client/components/settings/peopleBody.jade34
-rw-r--r--client/components/settings/peopleBody.js76
-rw-r--r--client/components/settings/peopleBody.styl33
-rw-r--r--client/components/settings/settingBody.jade47
-rw-r--r--client/components/settings/settingBody.js76
-rw-r--r--client/components/settings/settingBody.styl7
-rw-r--r--client/components/settings/settingHeader.jade31
-rw-r--r--client/components/sidebar/sidebar.jade171
-rw-r--r--client/components/sidebar/sidebar.js243
-rw-r--r--client/components/sidebar/sidebar.styl61
-rw-r--r--client/components/sidebar/sidebarArchives.jade28
-rw-r--r--client/components/sidebar/sidebarArchives.js48
-rw-r--r--client/components/sidebar/sidebarCustomFields.jade62
-rw-r--r--client/components/sidebar/sidebarCustomFields.js157
-rw-r--r--client/components/sidebar/sidebarFilters.jade19
-rw-r--r--client/components/sidebar/sidebarFilters.js10
-rw-r--r--client/components/sidebar/sidebarSearches.jade2
-rw-r--r--client/components/swimlanes/miniswimlane.jade8
-rw-r--r--client/components/swimlanes/swimlaneHeader.jade57
-rw-r--r--client/components/swimlanes/swimlaneHeader.js77
-rw-r--r--client/components/swimlanes/swimlanes.jade50
-rw-r--r--client/components/swimlanes/swimlanes.js81
-rw-r--r--client/components/swimlanes/swimlanes.styl104
-rw-r--r--client/components/users/userAvatar.jade2
-rw-r--r--client/components/users/userAvatar.js5
-rw-r--r--client/components/users/userHeader.jade43
-rw-r--r--client/components/users/userHeader.js32
-rw-r--r--client/lib/datepicker.js86
-rw-r--r--client/lib/escapeActions.js2
-rw-r--r--client/lib/filter.js425
-rw-r--r--client/lib/inlinedform.js12
-rw-r--r--client/lib/modal.js14
-rw-r--r--client/lib/popup.js12
-rw-r--r--client/lib/utils.js153
120 files changed, 7377 insertions, 875 deletions
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade
index 2054777a..54066da8 100644
--- a/client/components/activities/activities.jade
+++ b/client/components/activities/activities.jade
@@ -14,6 +14,9 @@ template(name="boardActivities")
p.activity-desc
+memberName(user=user)
+ if($eq activityType 'deleteAttachment')
+ | {{{_ 'activity-delete-attach' cardLink}}}.
+
if($eq activityType 'addAttachment')
| {{{_ 'activity-attached' attachmentLink cardLink}}}.
@@ -31,12 +34,28 @@ template(name="boardActivities")
.activity-checklist(href="{{ card.absoluteUrl }}")
+viewer
= checklist.title
+ if($eq activityType 'removeChecklist')
+ | {{{_ 'activity-checklist-removed' cardLink}}}.
+
+ if($eq activityType 'checkedItem')
+ | {{{_ 'activity-checked-item' checkItem checklist.title cardLink}}}.
+
+ if($eq activityType 'uncheckedItem')
+ | {{{_ 'activity-unchecked-item' checkItem checklist.title cardLink}}}.
+
+ if($eq activityType 'checklistCompleted')
+ | {{{_ 'activity-checklist-completed' checklist.title cardLink}}}.
+
+ if($eq activityType 'checklistUncompleted')
+ | {{{_ 'activity-checklist-uncompleted' checklist.title cardLink}}}.
if($eq activityType 'addChecklistItem')
| {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}.
.activity-checklist(href="{{ card.absoluteUrl }}")
+viewer
= checklistItem.title
+ if($eq activityType 'removedChecklistItem')
+ | {{{_ 'activity-checklist-item-removed' checklist.title cardLink}}}.
if($eq activityType 'archivedCard')
| {{{_ 'activity-archived' cardLink}}}.
@@ -53,6 +72,9 @@ template(name="boardActivities")
if($eq activityType 'createCard')
| {{{_ 'activity-added' cardLink boardLabel}}}.
+ if($eq activityType 'createCustomField')
+ | {{_ 'activity-customfield-created' customField}}.
+
if($eq activityType 'createList')
| {{_ 'activity-added' list.title boardLabel}}.
@@ -77,6 +99,9 @@ template(name="boardActivities")
else
| {{{_ 'activity-added' memberLink cardLink}}}.
+ if($eq activityType 'moveCardBoard')
+ | {{{_ 'activity-moved' cardLink oldBoardName boardName}}}.
+
if($eq activityType 'moveCard')
| {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
@@ -86,6 +111,18 @@ template(name="boardActivities")
if($eq activityType 'restoredCard')
| {{{_ 'activity-sent' cardLink boardLabel}}}.
+ if($eq activityType 'addedLabel')
+ | {{{_ 'activity-added-label' lastLabel cardLink}}}.
+
+ if($eq activityType 'removedLabel')
+ | {{{_ 'activity-removed-label' lastLabel cardLink}}}.
+
+ if($eq activityType 'setCustomField')
+ | {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}.
+
+ if($eq activityType 'unsetCustomField')
+ | {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}.
+
if($eq activityType 'unjoinMember')
if($eq user._id member._id)
| {{{_ 'activity-unjoined' cardLink}}}.
@@ -101,7 +138,7 @@ template(name="cardActivities")
p.activity-desc
+memberName(user=user)
if($eq activityType 'createCard')
- | {{_ 'activity-added' cardLabel list.title}}.
+ | {{_ 'activity-added' cardLabel listName}}.
if($eq activityType 'importCard')
| {{{_ 'activity-imported' cardLabel list.title sourceLink}}}.
if($eq activityType 'joinMember')
@@ -116,14 +153,44 @@ template(name="cardActivities")
| {{{_ 'activity-removed' cardLabel memberLink}}}.
if($eq activityType 'archivedCard')
| {{_ 'activity-archived' cardLabel}}.
+
+ if($eq activityType 'addedLabel')
+ | {{{_ 'activity-added-label-card' lastLabel }}}.
+
+ if($eq activityType 'removedLabel')
+ | {{{_ 'activity-removed-label-card' lastLabel }}}.
+
+ if($eq activityType 'removeChecklist')
+ | {{{_ 'activity-checklist-removed' cardLabel}}}.
+
+ if($eq activityType 'checkedItem')
+ | {{{_ 'activity-checked-item-card' checkItem checklist.title }}}.
+
+ if($eq activityType 'uncheckedItem')
+ | {{{_ 'activity-unchecked-item-card' checkItem checklist.title }}}.
+
+ if($eq activityType 'checklistCompleted')
+ | {{{_ 'activity-checklist-completed-card' checklist.title }}}.
+
+ if($eq activityType 'checklistUncompleted')
+ | {{{_ 'activity-checklist-uncompleted-card' checklist.title }}}.
+
if($eq activityType 'restoredCard')
| {{_ 'activity-sent' cardLabel boardLabel}}.
if($eq activityType 'moveCard')
| {{_ 'activity-moved' cardLabel oldList.title list.title}}.
+
+ if($eq activityType 'moveCardBoard')
+ | {{{_ 'activity-moved' cardLink oldBoardName boardName}}}.
+
if($eq activityType 'addAttachment')
| {{{_ 'activity-attached' attachmentLink cardLabel}}}.
if attachment.isImage
img.attachment-image-preview(src=attachment.url)
+ if($eq activityType 'deleteAttachment')
+ | {{{_ 'activity-delete-attach' cardLabel}}}.
+ if($eq activityType 'removedChecklist')
+ | {{{_ 'activity-checklist-removed' cardLabel}}}.
if($eq activityType 'addChecklist')
| {{{_ 'activity-checklist-added' cardLabel}}}.
.activity-checklist
diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js
index ccb064f3..0476897f 100644
--- a/client/components/activities/activities.js
+++ b/client/components/activities/activities.js
@@ -8,16 +8,24 @@ BlazeComponent.extendComponent({
const sidebar = this.parentComponent(); // XXX for some reason not working
sidebar.callFirstWith(null, 'resetNextPeak');
this.autorun(() => {
- const mode = this.data().mode;
+ let mode = this.data().mode;
const capitalizedMode = Utils.capitalize(mode);
- const id = Session.get(`current${capitalizedMode}`);
+ let thisId, searchId;
+ if (mode === 'linkedcard' || mode === 'linkedboard') {
+ thisId = Session.get('currentCard');
+ searchId = Cards.findOne({_id: thisId}).linkedId;
+ mode = mode.replace('linked', '');
+ } else {
+ thisId = Session.get(`current${capitalizedMode}`);
+ searchId = thisId;
+ }
const limit = this.page.get() * activitiesPerPage;
const user = Meteor.user();
const hideSystem = user ? user.hasHiddenSystemMessages() : false;
- if (id === null)
+ if (searchId === null)
return;
- this.subscribe('activities', mode, id, limit, hideSystem, () => {
+ this.subscribe('activities', mode, searchId, limit, hideSystem, () => {
this.loadNextPageLocked = false;
// If the sibear peak hasn't increased, that mean that there are no more
@@ -42,6 +50,12 @@ BlazeComponent.extendComponent({
}
},
+ checkItem(){
+ const checkItemId = this.currentData().checklistItemId;
+ const checkItem = ChecklistItems.findOne({_id:checkItemId});
+ return checkItem.title;
+ },
+
boardLabel() {
return TAPi18n.__('this-board');
},
@@ -58,6 +72,40 @@ BlazeComponent.extendComponent({
}, card.title));
},
+ lastLabel(){
+ const lastLabelId = this.currentData().labelId;
+ if (!lastLabelId)
+ return null;
+ const lastLabel = Boards.findOne(Session.get('currentBoard')).getLabelById(lastLabelId);
+ if(lastLabel.name === undefined || lastLabel.name === ''){
+ return lastLabel.color;
+ }else{
+ return lastLabel.name;
+ }
+ },
+
+ lastCustomField(){
+ const lastCustomField = CustomFields.findOne(this.currentData().customFieldId);
+ if (!lastCustomField)
+ return null;
+ return lastCustomField.name;
+ },
+
+ lastCustomFieldValue(){
+ const lastCustomField = CustomFields.findOne(this.currentData().customFieldId);
+ if (!lastCustomField)
+ return null;
+ const value = this.currentData().value;
+ if (lastCustomField.settings.dropdownItems && lastCustomField.settings.dropdownItems.length > 0) {
+ const dropDownValue = _.find(lastCustomField.settings.dropdownItems, (item) => {
+ return item._id === value;
+ });
+ if (dropDownValue)
+ return dropDownValue.name;
+ }
+ return value;
+ },
+
listLabel() {
return this.currentData().list().title;
},
@@ -91,6 +139,13 @@ BlazeComponent.extendComponent({
}, attachment.name()));
},
+ customField() {
+ const customField = this.currentData().customField();
+ if (!customField)
+ return null;
+ return customField.name;
+ },
+
events() {
return [{
// XXX We should use Popup.afterConfirmation here
diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js
index 9b6aedd6..34b6402c 100644
--- a/client/components/activities/comments.js
+++ b/client/components/activities/comments.js
@@ -21,11 +21,18 @@ BlazeComponent.extendComponent({
'submit .js-new-comment-form'(evt) {
const input = this.getInput();
const text = input.val().trim();
+ const card = this.currentData();
+ let boardId = card.boardId;
+ let cardId = card._id;
+ if (card.isLinkedCard()) {
+ boardId = Cards.findOne(card.linkedId).boardId;
+ cardId = card.linkedId;
+ }
if (text) {
CardComments.insert({
text,
- boardId: this.currentData().boardId,
- cardId: this.currentData()._id,
+ boardId,
+ cardId,
});
resetCommentInput(input);
Tracker.flush();
diff --git a/client/components/boards/boardArchive.jade b/client/components/boards/boardArchive.jade
index 6576f742..5d291f00 100644
--- a/client/components/boards/boardArchive.jade
+++ b/client/components/boards/boardArchive.jade
@@ -6,9 +6,17 @@ template(name="archivedBoards")
ul.archived-lists
each archivedBoards
li.archived-lists-item
- button.js-restore-board
- i.fa.fa-undo
- | {{_ 'restore-board'}}
- = title
+ div.board-header-btns
+ button.board-header-btn.js-delete-board
+ i.fa.fa-trash-o
+ | {{_ 'delete-board'}}
+ button.board-header-btn.js-restore-board
+ i.fa.fa-undo
+ | {{_ 'restore-board'}}
+ = title
else
li.no-items-message {{_ 'no-archived-boards'}}
+
+template(name="boardDeletePopup")
+ p {{_ 'delete-board-confirm-popup'}}
+ button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
diff --git a/client/components/boards/boardArchive.js b/client/components/boards/boardArchive.js
index acb53149..c8bbb341 100644
--- a/client/components/boards/boardArchive.js
+++ b/client/components/boards/boardArchive.js
@@ -1,9 +1,3 @@
-Template.boardListHeaderBar.events({
- 'click .js-open-archived-board'() {
- Modal.open('archivedBoards');
- },
-});
-
BlazeComponent.extendComponent({
onCreated() {
this.subscribe('archivedBoards');
@@ -29,6 +23,17 @@ BlazeComponent.extendComponent({
board.restore();
Utils.goBoardId(board._id);
},
+ 'click .js-delete-board': Popup.afterConfirm('boardDelete', function() {
+ Popup.close();
+ const isSandstorm = Meteor.settings && Meteor.settings.public &&
+ Meteor.settings.public.sandstorm;
+ if (isSandstorm && Session.get('currentBoard')) {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ Boards.remove(currentBoard._id);
+ }
+ Boards.remove(this._id);
+ FlowRouter.go('home');
+ }),
}];
},
}).register('archivedBoards');
diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade
index 29a613b9..017d0b0a 100644
--- a/client/components/boards/boardBody.jade
+++ b/client/components/boards/boardBody.jade
@@ -20,8 +20,22 @@ template(name="boardBody")
class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
if showOverlay.get
.board-overlay
- if isViewSwimlanes
+ if currentBoard.isTemplatesBoard
each currentBoard.swimlanes
+swimlane(this)
- if isViewLists
- +listsGroup
+ else if isViewSwimlanes
+ each currentBoard.swimlanes
+ +swimlane(this)
+ else if isViewLists
+ +listsGroup(currentBoard)
+ else if isViewCalendar
+ +calendarView
+ else
+ +listsGroup(currentBoard)
+
+template(name="calendarView")
+ if isViewCalendar
+ .calendar-view.swimlane
+ if currentCard
+ +cardDetails(currentCard)
+ +fullcalendar(calendarOptions)
diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js
index 456bf9b3..301c0742 100644
--- a/client/components/boards/boardBody.js
+++ b/client/components/boards/boardBody.js
@@ -1,5 +1,6 @@
const subManager = new SubsManager();
-const { calculateIndex } = Utils;
+const { calculateIndex, enableClickOnTouch } = Utils;
+const swimlaneWhileSortingHeight = 150;
BlazeComponent.extendComponent({
onCreated() {
@@ -35,6 +36,37 @@ BlazeComponent.extendComponent({
this._isDragging = false;
// Used to set the overlay
this.mouseHasEnterCardDetails = false;
+
+ // fix swimlanes sort field if there are null values
+ const currentBoardData = Boards.findOne(Session.get('currentBoard'));
+ const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
+ if (nullSortSwimlanes.count() > 0) {
+ const swimlanes = currentBoardData.swimlanes();
+ let count = 0;
+ swimlanes.forEach((s) => {
+ Swimlanes.update(s._id, {
+ $set: {
+ sort: count,
+ },
+ });
+ count += 1;
+ });
+ }
+
+ // fix lists sort field if there are null values
+ const nullSortLists = currentBoardData.nullSortLists();
+ if (nullSortLists.count() > 0) {
+ const lists = currentBoardData.lists();
+ let count = 0;
+ lists.forEach((l) => {
+ Lists.update(l._id, {
+ $set: {
+ sort: count,
+ },
+ });
+ count += 1;
+ });
+ }
},
onRendered() {
const boardComponent = this;
@@ -43,21 +75,64 @@ BlazeComponent.extendComponent({
$swimlanesDom.sortable({
tolerance: 'pointer',
appendTo: '.board-canvas',
- helper: 'clone',
+ helper(evt, item) {
+ const helper = $(`<div class="swimlane"
+ style="flex-direction: column;
+ height: ${swimlaneWhileSortingHeight}px;
+ width: $(boardComponent.width)px;
+ overflow: hidden;"/>`);
+ helper.append(item.clone());
+ // Also grab the list of lists of cards
+ const list = item.next();
+ helper.append(list.clone());
+ return helper;
+ },
handle: '.js-swimlane-header',
- items: '.js-swimlane:not(.placeholder)',
+ items: '.swimlane:not(.placeholder)',
placeholder: 'swimlane placeholder',
distance: 7,
start(evt, ui) {
+ const listDom = ui.placeholder.next('.js-swimlane');
+ const parentOffset = ui.item.parent().offset();
+
ui.placeholder.height(ui.helper.height());
EscapeActions.executeUpTo('popup-close');
+ listDom.addClass('moving-swimlane');
boardComponent.setIsDragging(true);
+
+ ui.placeholder.insertAfter(ui.placeholder.next());
+ boardComponent.origPlaceholderIndex = ui.placeholder.index();
+
+ // resize all swimlanes + headers to be a total of 150 px per row
+ // this could be achieved by setIsDragging(true) but we want immediate
+ // result
+ ui.item.siblings('.js-swimlane').css('height', `${swimlaneWhileSortingHeight - 26}px`);
+
+ // set the new scroll height after the resize and insertion of
+ // the placeholder. We want the element under the cursor to stay
+ // at the same place on the screen
+ ui.item.parent().get(0).scrollTop = ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;
+ },
+ beforeStop(evt, ui) {
+ const parentOffset = ui.item.parent().offset();
+ const siblings = ui.item.siblings('.js-swimlane');
+ siblings.css('height', '');
+
+ // compute the new scroll height after the resize and removal of
+ // the placeholder
+ const scrollTop = ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;
+
+ // then reset the original view of the swimlane
+ siblings.removeClass('moving-swimlane');
+
+ // and apply the computed scrollheight
+ ui.item.parent().get(0).scrollTop = scrollTop;
},
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 prevSwimlaneDom = ui.item.prevAll('.js-swimlane').get(0);
+ const nextSwimlaneDom = ui.item.nextAll('.js-swimlane').get(0);
const sortIndex = calculateIndex(prevSwimlaneDom, nextSwimlaneDom, 1);
$swimlanesDom.sortable('cancel');
@@ -72,8 +147,35 @@ BlazeComponent.extendComponent({
boardComponent.setIsDragging(false);
},
+ sort(evt, ui) {
+ // get the mouse position in the sortable
+ const parentOffset = ui.item.parent().offset();
+ const cursorY = evt.pageY - parentOffset.top + ui.item.parent().scrollTop();
+
+ // compute the intended index of the placeholder (we need to skip the
+ // slots between the headers and the list of cards)
+ const newplaceholderIndex = Math.floor(cursorY / swimlaneWhileSortingHeight);
+ let destPlaceholderIndex = (newplaceholderIndex + 1) * 2;
+
+ // if we are scrolling far away from the bottom of the list
+ if (destPlaceholderIndex >= ui.item.parent().get(0).childElementCount) {
+ destPlaceholderIndex = ui.item.parent().get(0).childElementCount - 1;
+ }
+
+ // update the placeholder position in the DOM tree
+ if (destPlaceholderIndex !== ui.placeholder.index()) {
+ if (destPlaceholderIndex < boardComponent.origPlaceholderIndex) {
+ ui.placeholder.insertBefore(ui.placeholder.siblings().slice(destPlaceholderIndex - 2, destPlaceholderIndex - 1));
+ } else {
+ ui.placeholder.insertAfter(ui.placeholder.siblings().slice(destPlaceholderIndex - 1, destPlaceholderIndex));
+ }
+ }
+ },
});
+ // ugly touch event hotfix
+ enableClickOnTouch('.js-swimlane:not(.placeholder)');
+
function userIsMember() {
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
}
@@ -88,12 +190,20 @@ BlazeComponent.extendComponent({
isViewSwimlanes() {
const currentUser = Meteor.user();
- return (currentUser.profile.boardView === 'board-view-swimlanes');
+ if (!currentUser) return false;
+ return ((currentUser.profile || {}).boardView === 'board-view-swimlanes');
},
isViewLists() {
const currentUser = Meteor.user();
- return (currentUser.profile.boardView === 'board-view-lists');
+ if (!currentUser) return true;
+ return ((currentUser.profile || {}).boardView === 'board-view-lists');
+ },
+
+ isViewCalendar() {
+ const currentUser = Meteor.user();
+ if (!currentUser) return false;
+ return ((currentUser.profile || {}).boardView === 'board-view-cal');
},
openNewListForm() {
@@ -105,7 +215,6 @@ BlazeComponent.extendComponent({
.childComponents('addListForm')[0].open();
}
},
-
events() {
return [{
// XXX The board-overlay div should probably be moved to the parent
@@ -137,4 +246,95 @@ BlazeComponent.extendComponent({
});
},
+ scrollTop(position = 0) {
+ const swimlanes = this.$('.js-swimlanes');
+ swimlanes && swimlanes.animate({
+ scrollTop: position,
+ });
+ },
+
}).register('boardBody');
+
+BlazeComponent.extendComponent({
+ onRendered() {
+ this.autorun(function(){
+ $('#calendar-view').fullCalendar('refetchEvents');
+ });
+ },
+ calendarOptions() {
+ return {
+ id: 'calendar-view',
+ defaultView: 'agendaDay',
+ editable: true,
+ timezone: 'local',
+ header: {
+ left: 'title today prev,next',
+ center: 'agendaDay,listDay,timelineDay agendaWeek,listWeek,timelineWeek month,timelineMonth timelineYear',
+ right: '',
+ },
+ // height: 'parent', nope, doesn't work as the parent might be small
+ height: 'auto',
+ /* TODO: lists as resources: https://fullcalendar.io/docs/vertical-resource-view */
+ navLinks: true,
+ nowIndicator: true,
+ businessHours: {
+ // days of week. an array of zero-based day of week integers (0=Sunday)
+ dow: [ 1, 2, 3, 4, 5 ], // Monday - Friday
+ start: '8:00',
+ end: '18:00',
+ },
+ locale: TAPi18n.getLanguage(),
+ events(start, end, timezone, callback) {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ const events = [];
+ currentBoard.cardsInInterval(start.toDate(), end.toDate()).forEach(function(card){
+ events.push({
+ id: card._id,
+ title: card.title,
+ start: card.startAt,
+ end: card.endAt,
+ allDay: Math.abs(card.endAt.getTime() - card.startAt.getTime()) / 1000 === 24*3600,
+ url: FlowRouter.url('card', {
+ boardId: currentBoard._id,
+ slug: currentBoard.slug,
+ cardId: card._id,
+ }),
+ });
+ });
+ callback(events);
+ },
+ eventResize(event, delta, revertFunc) {
+ let isOk = false;
+ const card = Cards.findOne(event.id);
+
+ if (card) {
+ card.setEnd(event.end.toDate());
+ isOk = true;
+ }
+ if (!isOk) {
+ revertFunc();
+ }
+ },
+ eventDrop(event, delta, revertFunc) {
+ let isOk = false;
+ const card = Cards.findOne(event.id);
+ if (card) {
+ // TODO: add a flag for allDay events
+ if (!event.allDay) {
+ card.setStart(event.start.toDate());
+ card.setEnd(event.end.toDate());
+ isOk = true;
+ }
+ }
+ if (!isOk) {
+ revertFunc();
+ }
+ },
+ };
+ },
+ isViewCalendar() {
+ const currentUser = Meteor.user();
+ if (!currentUser) return false;
+ return ((currentUser.profile || {}).boardView === 'board-view-cal');
+ },
+}).register('calendarView');
diff --git a/client/components/boards/boardBody.styl b/client/components/boards/boardBody.styl
index a614c7ed..dfaaa050 100644
--- a/client/components/boards/boardBody.styl
+++ b/client/components/boards/boardBody.styl
@@ -15,12 +15,13 @@ position()
.board-wrapper
position: cover
- overflow-y: hidden;
+ overflow-x: hidden
+ overflow-y: hidden
.board-canvas
position: cover
transition: margin .1s
- overflow-y: auto;
+ overflow-y: auto
&.is-sibling-sidebar-open
margin-right: 248px
@@ -49,6 +50,6 @@ position()
display: flex
flex-direction: column
margin: 0
- padding: 0 40px 0px 0
+ padding: 0 0px 0px 0
overflow-x: hidden
overflow-y: auto
diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade
index fe0771cb..8bc61975 100644
--- a/client/components/boards/boardHeader.jade
+++ b/client/components/boards/boardHeader.jade
@@ -7,71 +7,69 @@ template(name="boardHeaderBar")
.board-header-btns.left
unless isMiniScreen
- unless isSandstorm
- if currentBoard
- if currentUser
- a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
- title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
- i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
- if showStarCounter
- span
- = currentBoard.stars
-
- a.board-header-btn(
- class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
- title="{{_ currentBoard.permission}}")
- i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
- span {{_ currentBoard.permission}}
-
- a.board-header-btn.js-watch-board(
- title="{{_ watchLevel }}")
- if $eq watchLevel "watching"
- i.fa.fa-eye
- if $eq watchLevel "tracking"
- i.fa.fa-bell
- if $eq watchLevel "muted"
- i.fa.fa-bell-slash
- span {{_ watchLevel}}
+ if currentBoard
+ if currentUser
+ a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
+ title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
+ i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
+ if showStarCounter
+ span
+ = currentBoard.stars
+
+ a.board-header-btn(
+ class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
+ title="{{_ currentBoard.permission}}")
+ i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
+ span {{_ currentBoard.permission}}
+
+ a.board-header-btn.js-watch-board(
+ title="{{_ watchLevel }}")
+ if $eq watchLevel "watching"
+ i.fa.fa-eye
+ if $eq watchLevel "tracking"
+ i.fa.fa-bell
+ if $eq watchLevel "muted"
+ i.fa.fa-bell-slash
+ span {{_ watchLevel}}
- else
- a.board-header-btn.js-log-in(
- title="{{_ 'log-in'}}")
- i.fa.fa-sign-in
- span {{_ 'log-in'}}
+ else
+ a.board-header-btn.js-log-in(
+ title="{{_ 'log-in'}}")
+ i.fa.fa-sign-in
+ span {{_ 'log-in'}}
.board-header-btns.right
if currentBoard
if isMiniScreen
- unless isSandstorm
- if currentUser
- a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
- title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
- i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
- if showStarCounter
- span
- = currentBoard.stars
-
- a.board-header-btn(
- class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
- title="{{_ currentBoard.permission}}")
- i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
- span {{_ currentBoard.permission}}
-
- a.board-header-btn.js-watch-board(
- title="{{_ watchLevel }}")
- if $eq watchLevel "watching"
- i.fa.fa-eye
- if $eq watchLevel "tracking"
- i.fa.fa-bell
- if $eq watchLevel "muted"
- i.fa.fa-bell-slash
- span {{_ watchLevel}}
+ if currentUser
+ a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
+ title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
+ i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
+ if showStarCounter
+ span
+ = currentBoard.stars
+
+ a.board-header-btn(
+ class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
+ title="{{_ currentBoard.permission}}")
+ i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
+ span {{_ currentBoard.permission}}
+
+ a.board-header-btn.js-watch-board(
+ title="{{_ watchLevel }}")
+ if $eq watchLevel "watching"
+ i.fa.fa-eye
+ if $eq watchLevel "tracking"
+ i.fa.fa-bell
+ if $eq watchLevel "muted"
+ i.fa.fa-bell-slash
+ span {{_ watchLevel}}
- else
- a.board-header-btn.js-log-in(
- title="{{_ 'log-in'}}")
- i.fa.fa-sign-in
- span {{_ 'log-in'}}
+ else
+ a.board-header-btn.js-log-in(
+ title="{{_ 'log-in'}}")
+ i.fa.fa-sign-in
+ span {{_ 'log-in'}}
if isSandstorm
if currentUser
@@ -87,15 +85,20 @@ template(name="boardHeaderBar")
if Filter.isActive
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
+ if currentUser.isAdmin
+ a.board-header-btn.js-open-rules-view(title="{{_ 'rules'}}")
+ i.fa.fa-magic
+ span {{_ 'rules'}}
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}}
+ unless currentBoard.isTemplatesBoard
+ a.board-header-btn.js-toggle-board-view(
+ title="{{_ 'board-view'}}")
+ i.fa.fa-th-large
+ span {{#if currentUser.profile.boardView}}{{_ currentUser.profile.boardView}}{{else}}{{_ 'board-view-lists'}}{{/if}}
if canModifyBoard
a.board-header-btn.js-multiselection-activate(
@@ -108,32 +111,8 @@ template(name="boardHeaderBar")
i.fa.fa-times-thin
.separator
- a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}")
- i.board-header-btn-icon.fa.fa-navicon
-
-template(name="boardMenuPopup")
- ul.pop-over-list
- li: a.js-open-archives {{_ 'archived-items'}}
- if currentUser.isBoardAdmin
- li: a.js-change-board-color {{_ 'board-change-color'}}
- //-
- XXX Language should be handled by sandstorm, but for now display a
- language selection link in the board menu. This link is normally present
- in the header bar that is not displayed on sandstorm.
- if isSandstorm
- li: a.js-change-language {{_ 'language'}}
- unless isSandstorm
- if currentUser.isBoardAdmin
- hr
- ul.pop-over-list
- li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
- li: a.js-archive-board {{_ 'archive-board'}}
- li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
- if isSandstorm
- hr
- ul.pop-over-list
- li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
- li: a.js-import-board {{_ 'import-board-c'}}
+ a.board-header-btn.js-toggle-sidebar
+ i.fa.fa-navicon
template(name="boardVisibilityList")
ul.pop-over-list
@@ -184,14 +163,6 @@ template(name="boardChangeWatchPopup")
i.fa.fa-check
span.sub-name {{_ 'muted-info'}}
-template(name="boardChangeColorPopup")
- .board-backgrounds-list.clearfix
- each backgroundColors
- .board-background-select.js-select-background
- span.background-box(class="board-color-{{this}}")
- if isSelected
- i.fa.fa-check
-
template(name="createBoard")
form
label
@@ -213,45 +184,21 @@ template(name="createBoard")
input.primary.wide(type="submit" value="{{_ 'create'}}")
span.quiet
| {{_ 'or'}}
- a.js-import-board {{_ 'import-board'}}
-
-template(name="chooseBoardSource")
- ul.pop-over-list
- li
- a(href="{{pathFor '/import/trello'}}") {{_ 'from-trello'}}
- li
- a(href="{{pathFor '/import/wekan'}}") {{_ 'from-wekan'}}
+ a.js-import-board {{_ 'import'}}
+ span.quiet
+ | /
+ a.js-board-template {{_ 'template'}}
template(name="boardChangeTitlePopup")
form
label
| {{_ 'title'}}
- input.js-board-name(type="text" value=title autofocus)
+ input.js-board-name(type="text" value=title autofocus dir="auto")
label
| {{_ 'description'}}
- textarea.js-board-desc= description
+ textarea.js-board-desc(dir="auto")= description
input.primary.wide(type="submit" value="{{_ 'rename'}}")
-template(name="archiveBoardPopup")
+template(name="boardCreateRulePopup")
p {{_ 'close-board-pop'}}
button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
-
-template(name="outgoingWebhooksPopup")
- each integrations
- form.integration-form
- if title
- h4 {{title}}
- else
- h4 {{_ 'no-name'}}
- label
- | URL
- input.js-outgoing-webhooks-url(type="text" name="url" value=url)
- input(type="hidden" value=_id name="id")
- input.primary.wide(type="submit" value="{{_ 'save'}}")
- form.integration-form
- h4
- | {{_ 'new-outgoing-webhook'}}
- label
- | URL
- input.js-outgoing-webhooks-url(type="text" name="url" autofocus)
- input.primary.wide(type="submit" value="{{_ 'save'}}")
diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js
index 2b587831..f2b5c4f5 100644
--- a/client/components/boards/boardHeader.js
+++ b/client/components/boards/boardHeader.js
@@ -1,5 +1,9 @@
Template.boardMenuPopup.events({
'click .js-rename-board': Popup.open('boardChangeTitle'),
+ 'click .js-custom-fields'() {
+ Sidebar.setView('customFields');
+ Popup.close();
+ },
'click .js-open-archives'() {
Sidebar.setView('archives');
Popup.close();
@@ -13,8 +17,15 @@ Template.boardMenuPopup.events({
// confirm that the board was successfully archived.
FlowRouter.go('home');
}),
+ 'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ Popup.close();
+ Boards.remove(currentBoard._id);
+ FlowRouter.go('home');
+ }),
'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
'click .js-import-board': Popup.open('chooseBoardSource'),
+ 'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
});
Template.boardMenuPopup.helpers({
@@ -78,12 +89,19 @@ BlazeComponent.extendComponent({
},
'click .js-toggle-board-view'() {
const currentUser = Meteor.user();
- if (currentUser.profile.boardView === 'board-view-swimlanes') {
+ if ((currentUser.profile || {}).boardView === 'board-view-swimlanes') {
+ currentUser.setBoardView('board-view-cal');
+ } else if ((currentUser.profile || {}).boardView === 'board-view-lists') {
+ currentUser.setBoardView('board-view-swimlanes');
+ } else if ((currentUser.profile || {}).boardView === 'board-view-cal') {
currentUser.setBoardView('board-view-lists');
- } else if (currentUser.profile.boardView === 'board-view-lists') {
+ } else {
currentUser.setBoardView('board-view-swimlanes');
}
},
+ 'click .js-toggle-sidebar'() {
+ Sidebar.toggle();
+ },
'click .js-open-filter-view'() {
Sidebar.setView('filter');
},
@@ -95,6 +113,9 @@ BlazeComponent.extendComponent({
'click .js-open-search-view'() {
Sidebar.setView('search');
},
+ 'click .js-open-rules-view'() {
+ Modal.openWide('rulesMain');
+ },
'click .js-multiselection-activate'() {
const currentCard = Session.get('currentCard');
MultiSelection.activate();
@@ -119,28 +140,6 @@ Template.boardHeaderBar.helpers({
},
});
-BlazeComponent.extendComponent({
- backgroundColors() {
- return Boards.simpleSchema()._schema.color.allowedValues;
- },
-
- isSelected() {
- const currentBoard = Boards.findOne(Session.get('currentBoard'));
- return currentBoard.color === this.currentData().toString();
- },
-
- events() {
- return [{
- 'click .js-select-background'(evt) {
- const currentBoard = Boards.findOne(Session.get('currentBoard'));
- const newColor = this.currentData().toString();
- currentBoard.setColor(newColor);
- evt.preventDefault();
- },
- }];
- },
-}).register('boardChangeColorPopup');
-
const CreateBoard = BlazeComponent.extendComponent({
template() {
return 'createBoard';
@@ -192,16 +191,11 @@ const CreateBoard = BlazeComponent.extendComponent({
'click .js-import': Popup.open('boardImportBoard'),
submit: this.onSubmit,
'click .js-import-board': Popup.open('chooseBoardSource'),
+ 'click .js-board-template': Popup.open('searchElement'),
}];
},
}).register('createBoardPopup');
-BlazeComponent.extendComponent({
- template() {
- return 'chooseBoardSource';
- },
-}).register('chooseBoardSourcePopup');
-
(class HeaderBarCreateBoard extends CreateBoard {
onSubmit(evt) {
super.onSubmit(evt);
@@ -251,50 +245,3 @@ BlazeComponent.extendComponent({
}];
},
}).register('boardChangeWatchPopup');
-
-BlazeComponent.extendComponent({
- integrations() {
- const boardId = Session.get('currentBoard');
- return Integrations.find({ boardId: `${boardId}` }).fetch();
- },
-
- integration(id) {
- const boardId = Session.get('currentBoard');
- return Integrations.findOne({ _id: id, boardId: `${boardId}` });
- },
-
- events() {
- return [{
- 'submit'(evt) {
- evt.preventDefault();
- const url = evt.target.url.value;
- const boardId = Session.get('currentBoard');
- let id = null;
- let integration = null;
- if (evt.target.id) {
- id = evt.target.id.value;
- integration = this.integration(id);
- if (url) {
- Integrations.update(integration._id, {
- $set: {
- url: `${url}`,
- },
- });
- } else {
- Integrations.remove(integration._id);
- }
- } else if (url) {
- Integrations.insert({
- userId: Meteor.userId(),
- enabled: true,
- type: 'outgoing-webhooks',
- url: `${url}`,
- boardId: `${boardId}`,
- activities: ['all'],
- });
- }
- Popup.close();
- },
- }];
- },
-}).register('outgoingWebhooksPopup');
diff --git a/client/components/boards/boardHeader.styl b/client/components/boards/boardHeader.styl
index 0abdb5bd..402b4f1e 100644
--- a/client/components/boards/boardHeader.styl
+++ b/client/components/boards/boardHeader.styl
@@ -1,3 +1,22 @@
.integration-form
padding: 5px
border-bottom: 1px solid #ccc
+
+.flex
+ display: -webkit-box
+ display: -moz-box
+ display: -webkit-flex
+ display: -moz-flex
+ display: -ms-flexbox
+ display: flex
+
+.option
+ @extends .flex
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+ background: #fff;
+ text-decoration: none;
+ -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.2);
+ box-shadow: 0 1px 2px rgba(0,0,0,0.2);
+ margin-top: 5px;
+ padding: 5px;
diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade
index 95ce3678..70b29c49 100644
--- a/client/components/boards/boardsList.jade
+++ b/client/components/boards/boardsList.jade
@@ -1,6 +1,8 @@
template(name="boardList")
.wrapper
ul.board-list.clearfix
+ li.js-add-board
+ a.board-list-item.label {{_ 'add-board'}}
each boards
li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
if isInvited
@@ -20,15 +22,15 @@ template(name="boardList")
i.fa.js-star-board(
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
title="{{_ 'star-board-title'}}")
-
+ p.board-list-item-desc= description
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}}")
+ i.fa.js-clone-board(
+ class="fa-clone"
+ title="{{_ 'duplicate-board'}}")
- p.board-list-item-desc= description
- li.js-add-board
- a.board-list-item.label {{_ 'add-board'}}
template(name="boardListHeaderBar")
@@ -37,3 +39,6 @@ template(name="boardListHeaderBar")
a.board-header-btn.js-open-archived-board
i.fa.fa-archive
span {{_ 'archives'}}
+ a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
+ i.fa.fa-clone
+ span {{_ 'templates'}}
diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js
index 1ed88146..8c45fbe2 100644
--- a/client/components/boards/boardsList.js
+++ b/client/components/boards/boardsList.js
@@ -1,5 +1,20 @@
const subManager = new SubsManager();
+Template.boardListHeaderBar.events({
+ 'click .js-open-archived-board'() {
+ Modal.open('archivedBoards');
+ },
+});
+
+Template.boardListHeaderBar.helpers({
+ templatesBoardId() {
+ return Meteor.user().getTemplatesBoardId();
+ },
+ templatesBoardSlug() {
+ return Meteor.user().getTemplatesBoardSlug();
+ },
+});
+
BlazeComponent.extendComponent({
onCreated() {
Meteor.subscribe('setting');
@@ -9,11 +24,9 @@ BlazeComponent.extendComponent({
return Boards.find({
archived: false,
'members.userId': Meteor.userId(),
- }, {
- sort: ['title'],
- });
+ type: 'board',
+ }, { sort: ['title'] });
},
-
isStarred() {
const user = Meteor.user();
return user && user.hasStarred(this.currentData()._id);
@@ -42,6 +55,21 @@ BlazeComponent.extendComponent({
Meteor.user().toggleBoardStar(boardId);
evt.preventDefault();
},
+ 'click .js-clone-board'(evt) {
+ Meteor.call('cloneBoard',
+ this.currentData()._id,
+ Session.get('fromBoard'),
+ (err, res) => {
+ if (err) {
+ this.setError(err.error);
+ } else {
+ Session.set('fromBoard', null);
+ Utils.goBoardId(res);
+ }
+ }
+ );
+ evt.preventDefault();
+ },
'click .js-accept-invite'() {
const boardId = this.currentData()._id;
Meteor.user().removeInvite(boardId);
diff --git a/client/components/boards/boardsList.styl b/client/components/boards/boardsList.styl
index 80e47685..7e834411 100644
--- a/client/components/boards/boardsList.styl
+++ b/client/components/boards/boardsList.styl
@@ -94,13 +94,27 @@ $spaceBetweenTiles = 16px
.is-star-active
color: white
+ .fa-clone
+ position: absolute;
+ bottom: 0
+ font-size: 14px
+ height: 18px
+ line-height: 18px
+ opacity: 0
+ right: 0
+ padding: 9px 9px
+ transition-duration: .15s
+ transition-property: color, font-size, background
+
li:hover a
&:hover
.fa-star,
+ .fa-clone,
.fa-star-o
color: white
.fa-star,
+ .fa-clone,
.fa-star-o
color: white
opacity: .75
diff --git a/client/components/boards/miniboard.jade b/client/components/boards/miniboard.jade
new file mode 100644
index 00000000..d1fb0b07
--- /dev/null
+++ b/client/components/boards/miniboard.jade
@@ -0,0 +1,8 @@
+template(name="miniboard")
+ .minicard(
+ class="minicard-{{colorClass}}")
+ .minicard-title
+ .handle
+ .fa.fa-arrows
+ +viewer
+ = title
diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js
index bc7d3979..5cac930d 100644
--- a/client/components/cards/attachments.js
+++ b/client/components/cards/attachments.js
@@ -57,8 +57,13 @@ Template.cardAttachmentsPopup.events({
const card = this;
FS.Utility.eachFile(evt, (f) => {
const file = new FS.File(f);
- file.boardId = card.boardId;
- file.cardId = card._id;
+ if (card.isLinkedCard()) {
+ file.boardId = Cards.findOne(card.linkedId).boardId;
+ file.cardId = card.linkedId;
+ } else {
+ file.boardId = card.boardId;
+ file.cardId = card._id;
+ }
file.userId = Meteor.userId();
const attachment = Attachments.insert(file);
diff --git a/client/components/cards/cardCustomFields.jade b/client/components/cards/cardCustomFields.jade
new file mode 100644
index 00000000..65081e3b
--- /dev/null
+++ b/client/components/cards/cardCustomFields.jade
@@ -0,0 +1,76 @@
+template(name="cardCustomFieldsPopup")
+ ul.pop-over-list
+ each board.customFields
+ li.item(class="")
+ a.name.js-select-field(href="#")
+ span.full-name
+ = name
+ if hasCustomField
+ i.fa.fa-check
+ hr
+ a.quiet-button.full.js-settings
+ i.fa.fa-cog
+ span {{_ 'settings'}}
+
+template(name="cardCustomField")
+ +Template.dynamic(template=getTemplate)
+
+template(name="cardCustomField-text")
+ if canModifyCard
+ +inlinedForm(classNames="js-card-customfield-text")
+ +editor(autofocus=true)
+ = value
+ .edit-controls.clearfix
+ button.primary(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+ else
+ a.js-open-inlined-form
+ if value
+ +viewer
+ = value
+ else
+ | {{_ 'edit'}}
+
+template(name="cardCustomField-number")
+ if canModifyCard
+ +inlinedForm(classNames="js-card-customfield-number")
+ input(type="number" value=data.value)
+ .edit-controls.clearfix
+ button.primary(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+ else
+ a.js-open-inlined-form
+ if value
+ = value
+ else
+ | {{_ 'edit'}}
+
+template(name="cardCustomField-date")
+ if canModifyCard
+ a.js-edit-date(title="{{showTitle}}" class="{{classes}}")
+ if value
+ div.card-date
+ time(datetime="{{showISODate}}")
+ | {{showDate}}
+ else
+ | {{_ 'edit'}}
+
+template(name="cardCustomField-dropdown")
+ if canModifyCard
+ +inlinedForm(classNames="js-card-customfield-dropdown")
+ select.inline
+ each items
+ if($eq data.value this._id)
+ option(value=_id selected="selected") {{name}}
+ else
+ option(value=_id) {{name}}
+ .edit-controls.clearfix
+ button.primary(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+ else
+ a.js-open-inlined-form
+ if value
+ +viewer
+ = selectedItem
+ else
+ | {{_ 'edit'}} \ No newline at end of file
diff --git a/client/components/cards/cardCustomFields.js b/client/components/cards/cardCustomFields.js
new file mode 100644
index 00000000..aa50caec
--- /dev/null
+++ b/client/components/cards/cardCustomFields.js
@@ -0,0 +1,179 @@
+Template.cardCustomFieldsPopup.helpers({
+ hasCustomField() {
+ const card = Cards.findOne(Session.get('currentCard'));
+ const customFieldId = this._id;
+ return card.customFieldIndex(customFieldId) > -1;
+ },
+});
+
+Template.cardCustomFieldsPopup.events({
+ 'click .js-select-field'(evt) {
+ const card = Cards.findOne(Session.get('currentCard'));
+ const customFieldId = this._id;
+ card.toggleCustomField(customFieldId);
+ evt.preventDefault();
+ },
+ 'click .js-settings'(evt) {
+ EscapeActions.executeUpTo('detailsPane');
+ Sidebar.setView('customFields');
+ evt.preventDefault();
+ },
+});
+
+// cardCustomField
+const CardCustomField = BlazeComponent.extendComponent({
+
+ getTemplate() {
+ return `cardCustomField-${this.data().definition.type}`;
+ },
+
+ onCreated() {
+ const self = this;
+ self.card = Cards.findOne(Session.get('currentCard'));
+ self.customFieldId = this.data()._id;
+ },
+
+ canModifyCard() {
+ return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+ },
+});
+CardCustomField.register('cardCustomField');
+
+// cardCustomField-text
+(class extends CardCustomField {
+
+ onCreated() {
+ super.onCreated();
+ }
+
+ events() {
+ return [{
+ 'submit .js-card-customfield-text'(evt) {
+ evt.preventDefault();
+ const value = this.currentComponent().getValue();
+ this.card.setCustomField(this.customFieldId, value);
+ },
+ }];
+ }
+
+}).register('cardCustomField-text');
+
+// cardCustomField-number
+(class extends CardCustomField {
+
+ onCreated() {
+ super.onCreated();
+ }
+
+ events() {
+ return [{
+ 'submit .js-card-customfield-number'(evt) {
+ evt.preventDefault();
+ const value = parseInt(this.find('input').value, 10);
+ this.card.setCustomField(this.customFieldId, value);
+ },
+ }];
+ }
+
+}).register('cardCustomField-number');
+
+// cardCustomField-date
+(class extends CardCustomField {
+
+ onCreated() {
+ super.onCreated();
+ const self = this;
+ self.date = ReactiveVar();
+ self.now = ReactiveVar(moment());
+ window.setInterval(() => {
+ self.now.set(moment());
+ }, 60000);
+
+ self.autorun(() => {
+ self.date.set(moment(self.data().value));
+ });
+ }
+
+ showDate() {
+ // this will start working once mquandalle:moment
+ // is updated to at least moment.js 2.10.5
+ // until then, the date is displayed in the "L" format
+ return this.date.get().calendar(null, {
+ sameElse: 'llll',
+ });
+ }
+
+ showISODate() {
+ return this.date.get().toISOString();
+ }
+
+ classes() {
+ if (this.date.get().isBefore(this.now.get(), 'minute') &&
+ this.now.get().isBefore(this.data().value)) {
+ return 'current';
+ }
+ return '';
+ }
+
+ showTitle() {
+ return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
+ }
+
+ events() {
+ return [{
+ 'click .js-edit-date': Popup.open('cardCustomField-date'),
+ }];
+ }
+
+}).register('cardCustomField-date');
+
+// cardCustomField-datePopup
+(class extends DatePicker {
+ onCreated() {
+ super.onCreated();
+ const self = this;
+ self.card = Cards.findOne(Session.get('currentCard'));
+ self.customFieldId = this.data()._id;
+ this.data().value && this.date.set(moment(this.data().value));
+ }
+
+ _storeDate(date) {
+ this.card.setCustomField(this.customFieldId, date);
+ }
+
+ _deleteDate() {
+ this.card.setCustomField(this.customFieldId, '');
+ }
+}).register('cardCustomField-datePopup');
+
+// cardCustomField-dropdown
+(class extends CardCustomField {
+
+ onCreated() {
+ super.onCreated();
+ this._items = this.data().definition.settings.dropdownItems;
+ this.items = this._items.slice(0);
+ this.items.unshift({
+ _id: '',
+ name: TAPi18n.__('custom-field-dropdown-none'),
+ });
+ }
+
+ selectedItem() {
+ const selected = this._items.find((item) => {
+ return item._id === this.data().value;
+ });
+ return (selected) ? selected.name : TAPi18n.__('custom-field-dropdown-unknown');
+ }
+
+ events() {
+ return [{
+ 'submit .js-card-customfield-dropdown'(evt) {
+ evt.preventDefault();
+ const value = this.find('select').value;
+ this.card.setCustomField(this.customFieldId, value);
+ },
+ }];
+ }
+
+}).register('cardCustomField-dropdown');
diff --git a/client/components/cards/cardDate.jade b/client/components/cards/cardDate.jade
index 525f27ed..2e447506 100644
--- a/client/components/cards/cardDate.jade
+++ b/client/components/cards/cardDate.jade
@@ -1,19 +1,3 @@
-template(name="editCardDate")
- .edit-card-date
- form.edit-date
- .fields
- .left
- label(for="date") {{_ 'date'}}
- input.js-date-field#date(type="text" name="date" value=showDate placeholder=dateFormat autofocus)
- .right
- label(for="time") {{_ 'time'}}
- input.js-time-field#time(type="text" name="time" value=showTime placeholder=timeFormat)
- .js-datepicker
- if error.get
- .warning {{_ error.get}}
- button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
- button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
-
template(name="dateBadge")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}}" class="{{classes}}")
diff --git a/client/components/cards/cardDate.js b/client/components/cards/cardDate.js
index 7c0ad6ab..182705d5 100644
--- a/client/components/cards/cardDate.js
+++ b/client/components/cards/cardDate.js
@@ -1,5 +1,5 @@
// Edit received, start, due & end dates
-const EditCardDate = BlazeComponent.extendComponent({
+BlazeComponent.extendComponent({
template() {
return 'editCardDate';
},
@@ -93,10 +93,10 @@ Template.dateBadge.helpers({
});
// editCardReceivedDatePopup
-(class extends EditCardDate {
+(class extends DatePicker {
onCreated() {
super.onCreated();
- this.data().receivedAt && this.date.set(moment(this.data().receivedAt));
+ this.data().getReceived() && this.date.set(moment(this.data().getReceived()));
}
_storeDate(date) {
@@ -104,22 +104,22 @@ Template.dateBadge.helpers({
}
_deleteDate() {
- this.card.unsetReceived();
+ this.card.setReceived(null);
}
}).register('editCardReceivedDatePopup');
// editCardStartDatePopup
-(class extends EditCardDate {
+(class extends DatePicker {
onCreated() {
super.onCreated();
- this.data().startAt && this.date.set(moment(this.data().startAt));
+ this.data().getStart() && this.date.set(moment(this.data().getStart()));
}
onRendered() {
super.onRendered();
- if (moment.isDate(this.card.receivedAt)) {
- this.$('.js-datepicker').datepicker('setStartDate', this.card.receivedAt);
+ if (moment.isDate(this.card.getReceived())) {
+ this.$('.js-datepicker').datepicker('setStartDate', this.card.getReceived());
}
}
@@ -128,21 +128,21 @@ Template.dateBadge.helpers({
}
_deleteDate() {
- this.card.unsetStart();
+ this.card.setStart(null);
}
}).register('editCardStartDatePopup');
// editCardDueDatePopup
-(class extends EditCardDate {
+(class extends DatePicker {
onCreated() {
super.onCreated();
- this.data().dueAt && this.date.set(moment(this.data().dueAt));
+ this.data().getDue() && this.date.set(moment(this.data().getDue()));
}
onRendered() {
super.onRendered();
- if (moment.isDate(this.card.startAt)) {
- this.$('.js-datepicker').datepicker('setStartDate', this.card.startAt);
+ if (moment.isDate(this.card.getStart())) {
+ this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
}
}
@@ -151,21 +151,21 @@ Template.dateBadge.helpers({
}
_deleteDate() {
- this.card.unsetDue();
+ this.card.setDue(null);
}
}).register('editCardDueDatePopup');
// editCardEndDatePopup
-(class extends EditCardDate {
+(class extends DatePicker {
onCreated() {
super.onCreated();
- this.data().endAt && this.date.set(moment(this.data().endAt));
+ this.data().getEnd() && this.date.set(moment(this.data().getEnd()));
}
onRendered() {
super.onRendered();
- if (moment.isDate(this.card.startAt)) {
- this.$('.js-datepicker').datepicker('setStartDate', this.card.startAt);
+ if (moment.isDate(this.card.getStart())) {
+ this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
}
}
@@ -174,7 +174,7 @@ Template.dateBadge.helpers({
}
_deleteDate() {
- this.card.unsetEnd();
+ this.card.setEnd(null);
}
}).register('editCardEndDatePopup');
@@ -213,16 +213,23 @@ class CardReceivedDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
- self.date.set(moment(self.data().receivedAt));
+ self.date.set(moment(self.data().getReceived()));
});
}
classes() {
- let classes = 'received-date' + ' ';
- if (this.date.get().isBefore(this.now.get(), 'minute') &&
- this.now.get().isBefore(this.data().dueAt)) {
+ let classes = 'received-date ';
+ const dueAt = this.data().getDue();
+ const endAt = this.data().getEnd();
+ const startAt = this.data().getStart();
+ const theDate = this.date.get();
+ // if dueAt, endAt and startAt exist & are > receivedAt, receivedAt doesn't need to be flagged
+ if (((startAt) && (theDate.isAfter(dueAt))) ||
+ ((endAt) && (theDate.isAfter(endAt))) ||
+ ((dueAt) && (theDate.isAfter(dueAt))))
+ classes += 'long-overdue';
+ else
classes += 'current';
- }
return classes;
}
@@ -243,16 +250,24 @@ class CardStartDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
- self.date.set(moment(self.data().startAt));
+ self.date.set(moment(self.data().getStart()));
});
}
classes() {
let classes = 'start-date' + ' ';
- if (this.date.get().isBefore(this.now.get(), 'minute') &&
- this.now.get().isBefore(this.data().dueAt)) {
+ const dueAt = this.data().getDue();
+ const endAt = this.data().getEnd();
+ const theDate = this.date.get();
+ const now = this.now.get();
+ // if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged
+ if (((endAt) && (theDate.isAfter(endAt))) ||
+ ((dueAt) && (theDate.isAfter(dueAt))))
+ classes += 'long-overdue';
+ else if (theDate.isBefore(now, 'minute'))
+ classes += 'almost-due';
+ else
classes += 'current';
- }
return classes;
}
@@ -273,17 +288,26 @@ class CardDueDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
- self.date.set(moment(self.data().dueAt));
+ self.date.set(moment(self.data().getDue()));
});
}
classes() {
let classes = 'due-date' + ' ';
- if (this.now.get().diff(this.date.get(), 'days') >= 2)
+ const endAt = this.data().getEnd();
+ const theDate = this.date.get();
+ const now = this.now.get();
+ // if the due date is after the end date, green - done early
+ if ((endAt) && (theDate.isAfter(endAt)))
+ classes += 'current';
+ // if there is an end date, don't need to flag the due date
+ else if (endAt)
+ classes += '';
+ else if (now.diff(theDate, 'days') >= 2)
classes += 'long-overdue';
- else if (this.now.get().diff(this.date.get(), 'minute') >= 0)
+ else if (now.diff(theDate, 'minute') >= 0)
classes += 'due';
- else if (this.now.get().diff(this.date.get(), 'days') >= -1)
+ else if (now.diff(theDate, 'days') >= -1)
classes += 'almost-due';
return classes;
}
@@ -305,17 +329,19 @@ class CardEndDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
- self.date.set(moment(self.data().endAt));
+ self.date.set(moment(self.data().getEnd()));
});
}
classes() {
let classes = 'end-date' + ' ';
- if (this.data.dueAt.diff(this.date.get(), 'days') >= 2)
+ const dueAt = this.data().getDue();
+ const theDate = this.date.get();
+ if (theDate.diff(dueAt, 'days') >= 2)
classes += 'long-overdue';
- else if (this.data.dueAt.diff(this.date.get(), 'days') >= 0)
+ else if (theDate.diff(dueAt, 'days') >= 0)
classes += 'due';
- else if (this.data.dueAt.diff(this.date.get(), 'days') >= -2)
+ else if (theDate.diff(dueAt, 'days') >= -2)
classes += 'almost-due';
return classes;
}
@@ -355,4 +381,3 @@ CardEndDate.register('cardEndDate');
return this.date.get().format('l');
}
}).register('minicardEndDate');
-
diff --git a/client/components/cards/cardDate.styl b/client/components/cards/cardDate.styl
index 1ad3adb3..62cfdcd9 100644
--- a/client/components/cards/cardDate.styl
+++ b/client/components/cards/cardDate.styl
@@ -1,22 +1,3 @@
-.edit-card-date
- .fields
- .left
- width: 56%
- .right
- width: 38%
- .datepicker
- width: 100%
- table
- width: 100%
- border: none
- border-spacing: 0
- border-collapse: collapse
- thead
- background: none
- td, th
- box-sizing: border-box
-
-
.card-date
display: block
border-radius: 4px
@@ -62,12 +43,12 @@
&.start-date
time
&::before
- content: "\f08b" // symbol: fa-sign-out
+ content: "\f251" // symbol: fa-hourglass-start
&.received-date
time
&::before
- content: "\f251" // symbol: fa-hourglass-start
+ content: "\f08b" // symbol: fa-sign-out
time
&::before
diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade
index 047d7518..88f97adc 100644
--- a/client/components/cards/cardDetails.jade
+++ b/client/components/cards/cardDetails.jade
@@ -1,6 +1,6 @@
template(name="cardDetails")
section.card-details.js-card-details.js-perfect-scrollbar: .card-details-canvas
- .card-details-header
+ .card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
+inlinedForm(classNames="js-card-details-title")
+editCardTitleForm
else
@@ -10,46 +10,63 @@ template(name="cardDetails")
h2.card-details-title.js-card-title(
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
+viewer
- = title
+ = getTitle
if isWatching
i.fa.fa-eye.card-details-watch
+ .card-details-path
+ each parentList
+ | &nbsp; &gt; &nbsp;
+ a.js-parent-card(href=linkForCard) {{title}}
+ // else
+ {{_ 'top-level-card'}}
+ if isLinkedCard
+ h3.linked-card-location
+ +viewer
+ | {{getBoardTitle}} > {{getTitle}}
- if archived
- p.warning {{_ 'card-archived'}}
+ if getArchived
+ if isLinkedBoard
+ p.warning {{_ 'board-archived'}}
+ else
+ p.warning {{_ 'card-archived'}}
.card-details-items
.card-details-item.card-details-item-received
h3.card-details-item-title {{_ 'card-received'}}
- if receivedAt
+ if getReceived
+cardReceivedDate
else
- a.js-received-date {{_ 'add'}}
+ if canModifyCard
+ a.js-received-date {{_ 'add'}}
.card-details-item.card-details-item-start
h3.card-details-item-title {{_ 'card-start'}}
- if startAt
+ if getStart
+cardStartDate
else
- a.js-start-date {{_ 'add'}}
+ if canModifyCard
+ a.js-start-date {{_ 'add'}}
.card-details-item.card-details-item-due
h3.card-details-item-title {{_ 'card-due'}}
- if dueAt
+ if getDue
+cardDueDate
else
- a.js-due-date {{_ 'add'}}
+ if canModifyCard
+ a.js-due-date {{_ 'add'}}
.card-details-item.card-details-item-end
h3.card-details-item-title {{_ 'card-end'}}
- if endAt
+ if getEnd
+cardEndDate
else
- a.js-end-date {{_ 'add'}}
+ if canModifyCard
+ a.js-end-date {{_ 'add'}}
.card-details-items
.card-details-item.card-details-item-members
h3.card-details-item-title {{_ 'members'}}
- each members
+ each getMembers
+userAvatar(userId=this cardId=../_id)
| {{! XXX Hack to hide syntaxic coloration /// }}
if canModifyCard
@@ -66,9 +83,16 @@ template(name="cardDetails")
i.fa.fa-plus
.card-details-items
- if spentTime
+ each customFieldsWD
+ .card-details-item.card-details-item-customfield
+ h3.card-details-item-title
+ = definition.name
+ +cardCustomField
+
+ .card-details-items
+ if getSpentTime
.card-details-item.card-details-item-spent
- if isOvertime
+ if getIsOvertime
h3.card-details-item-title {{_ 'overtime-hours'}}
else
h3.card-details-item-title {{_ 'spent-time-hours'}}
@@ -79,15 +103,15 @@ template(name="cardDetails")
h3.card-details-item-title {{_ 'description'}}
+inlinedCardDescription(classNames="card-description js-card-description")
+editor(autofocus=true)
- | {{getUnsavedValue 'cardDescription' _id description}}
+ | {{getUnsavedValue 'cardDescription' _id getDescription}}
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
else
a.js-open-inlined-form
- if description
+ if getDescription
+viewer
- = description
+ = getDescription
else
| {{_ 'edit'}}
if (hasUnsavedValue 'cardDescription' _id)
@@ -96,14 +120,51 @@ template(name="cardDetails")
a.js-open-inlined-form {{_ 'view-it'}}
= ' - '
a.js-close-inlined-form {{_ 'discard'}}
- else if description
+ else if getDescription
h3.card-details-item-title {{_ 'description'}}
+viewer
- = description
+ = getDescription
+
+ .card-details-items
+ .card-details-item.card-details-item-name
+ h3.card-details-item-title {{_ 'requested-by'}}
+ if canModifyCard
+ +inlinedForm(classNames="js-card-details-requester")
+ +editCardRequesterForm
+ else
+ a.js-open-inlined-form
+ if getRequestedBy
+ +viewer
+ = getRequestedBy
+ else
+ | {{_ 'add'}}
+ else if getRequestedBy
+ +viewer
+ = getRequestedBy
+
+ .card-details-item.card-details-item-name
+ h3.card-details-item-title {{_ 'assigned-by'}}
+ if canModifyCard
+ +inlinedForm(classNames="js-card-details-assigner")
+ +editCardAssignerForm
+ else
+ a.js-open-inlined-form
+ if getAssignedBy
+ +viewer
+ = getAssignedBy
+ else
+ | {{_ 'add'}}
+ else if getRequestedBy
+ +viewer
+ = getAssignedBy
hr
+checklists(cardId = _id)
+ if currentBoard.allowsSubtasks
+ hr
+ +subtasks(cardId = _id)
+
hr
h3
i.fa.fa-paperclip
@@ -112,42 +173,64 @@ template(name="cardDetails")
+attachmentsGalery
hr
- .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")
+ unless currentUser.isNoComments
+ .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
- +activities(card=this mode="card")
+ unless currentUser.isNoComments
+ +commentForm
+ unless currentUser.isNoComments
+ if isLoaded.get
+ if isLinkedCard
+ +activities(card=this mode="linkedcard")
+ else if isLinkedBoard
+ +activities(card=this mode="linkedboard")
+ else
+ +activities(card=this mode="card")
template(name="editCardTitleForm")
- textarea.js-edit-card-title(rows='1' autofocus)
- = title
+ textarea.js-edit-card-title(rows='1' autofocus dir="auto")
+ = getTitle
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-title-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
+template(name="editCardRequesterForm")
+ input.js-edit-card-requester(type='text' autofocus value=getRequestedBy dir="auto")
+ .edit-controls.clearfix
+ button.primary.confirm.js-submit-edit-card-requester-form(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+
+template(name="editCardAssignerForm")
+ input.js-edit-card-assigner(type='text' autofocus value=getAssignedBy dir="auto")
+ .edit-controls.clearfix
+ button.primary.confirm.js-submit-edit-card-assigner-form(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+
template(name="cardDetailsActionsPopup")
ul.pop-over-list
li: a.js-toggle-watch-card {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
if canModifyCard
hr
ul.pop-over-list
- 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-start-date {{_ 'editCardStartDatePopup-title'}}
- li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
- li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
+ //li: a.js-members {{_ 'card-edit-members'}}
+ //li: a.js-labels {{_ 'card-edit-labels'}}
+ //li: a.js-attachments {{_ 'card-edit-attachments'}}
+ li: a.js-custom-fields {{_ 'card-edit-custom-fields'}}
+ //li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
+ //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'}}
+ li: a.js-set-card-color {{_ 'setCardColorPopup-title'}}
hr
ul.pop-over-list
li: a.js-move-card-to-top {{_ 'moveCardToTop-title'}}
@@ -167,10 +250,9 @@ template(name="moveCardPopup")
template(name="copyCardPopup")
label(for='copy-card-title') {{_ 'title'}}:
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
- = title
+ = getTitle
+boardsAndLists
-
template(name="copyChecklistToManyCardsPopup")
label(for='copy-checklist-cards-title') {{_ 'copyChecklistToManyCardsPopup-instructions'}}:
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
@@ -179,7 +261,7 @@ template(name="copyChecklistToManyCardsPopup")
template(name="boardsAndLists")
label {{_ 'boards'}}:
- select.js-select-boards
+ select.js-select-boards(autofocus)
each boards
if $eq _id currentBoard._id
option(value="{{_id}}" selected) {{_ 'current'}}
@@ -217,14 +299,49 @@ template(name="cardMorePopup")
span {{_ 'link-card'}}
= ' '
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
- input.inline-input(type="text" id="cardURL" readonly value="{{ absoluteUrl }}")
+ input.inline-input(type="text" id="cardURL" readonly value="{{ absoluteUrl }}" autofocus="autofocus")
button.js-copy-card-link-to-clipboard(class="btn") {{_ 'copy-card-link-to-clipboard'}}
span.clearfix
br
+ h2 {{_ 'change-card-parent'}}
+ label {{_ 'source-board'}}:
+ select.js-field-parent-board
+ if isTopLevel
+ option(value="none" selected) {{_ 'custom-field-dropdown-none'}}
+ else
+ option(value="none") {{_ 'custom-field-dropdown-none'}}
+ each boards
+ if isParentBoard
+ option(value="{{_id}}" selected) {{title}}
+ else
+ option(value="{{_id}}") {{title}}
+
+ label {{_ 'parent-card'}}:
+ select.js-field-parent-card
+ if isTopLevel
+ option(value="none" selected) {{_ 'custom-field-dropdown-none'}}
+ else
+ option(value="none") {{_ 'custom-field-dropdown-none'}}
+ each cards
+ if isParentCard
+ option(value="{{_id}}" selected) {{title}}
+ else
+ option(value="{{_id}}") {{title}}
+ br
| {{_ 'added'}}
span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
a.js-delete(title="{{_ 'card-delete-notice'}}") {{_ 'delete'}}
+template(name="setCardColorPopup")
+ form.edit-label
+ .palette-colors: each colors
+ unless $eq color 'white'
+ span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
+ if(isSelected color)
+ i.fa.fa-check
+ button.primary.confirm.js-submit {{_ 'save'}}
+ button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
+
template(name="cardDeletePopup")
p {{_ "card-delete-pop"}}
unless archived
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index cdd027e6..79e9e311 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -1,5 +1,10 @@
const subManager = new SubsManager();
-const { calculateIndexData } = Utils;
+const { calculateIndexData, enableClickOnTouch } = Utils;
+
+let cardColors;
+Meteor.startup(() => {
+ cardColors = Cards.simpleSchema()._schema.color.allowedValues;
+});
BlazeComponent.extendComponent({
mixins() {
@@ -20,9 +25,14 @@ BlazeComponent.extendComponent({
},
onCreated() {
+ this.currentBoard = Boards.findOne(Session.get('currentBoard'));
this.isLoaded = new ReactiveVar(false);
- this.parentComponent().parentComponent().showOverlay.set(true);
- this.parentComponent().parentComponent().mouseHasEnterCardDetails = false;
+ const boardBody = this.parentComponent().parentComponent();
+ //in Miniview parent is Board, not BoardBody.
+ if (boardBody !== null) {
+ boardBody.showOverlay.set(true);
+ boardBody.mouseHasEnterCardDetails = false;
+ }
this.calculateNextPeak();
Meteor.subscribe('unsaved-edits');
@@ -44,7 +54,8 @@ BlazeComponent.extendComponent({
scrollParentContainer() {
const cardPanelWidth = 510;
const bodyBoardComponent = this.parentComponent().parentComponent();
-
+ //On Mobile View Parent is Board, Not Board Body. I cant see how this funciton should work then.
+ if (bodyBoardComponent === null) return;
const $cardView = this.$(this.firstNode());
const $cardContainer = bodyBoardComponent.$('.js-swimlanes');
const cardContainerScroll = $cardContainer.scrollLeft();
@@ -63,10 +74,52 @@ BlazeComponent.extendComponent({
if (offset) {
bodyBoardComponent.scrollLeft(cardContainerScroll + offset);
}
+
+ //Scroll top
+ const cardViewStartTop = $cardView.offset().top;
+ const cardContainerScrollTop = $cardContainer.scrollTop();
+
+ let topOffset = false;
+ if(cardViewStartTop !== 100){
+ topOffset = cardViewStartTop - 100;
+ }
+ if(topOffset !== false) {
+ bodyBoardComponent.scrollTop(cardContainerScrollTop + topOffset);
+ }
+
+ },
+
+ presentParentTask() {
+ let result = this.currentBoard.presentParentTask;
+ if ((result === null) || (result === undefined)) {
+ result = 'no-parent';
+ }
+ return result;
+ },
+
+ linkForCard() {
+ const card = this.currentData();
+ let result = '#';
+ if (card) {
+ const board = Boards.findOne(card.boardId);
+ if (board) {
+ result = FlowRouter.url('card', {
+ boardId: card.boardId,
+ slug: board.slug,
+ cardId: card._id,
+ });
+ }
+ }
+ return result;
},
onRendered() {
- if (!Utils.isMiniScreen()) this.scrollParentContainer();
+ if (!Utils.isMiniScreen()) {
+ Meteor.setTimeout(() => {
+ $('.card-details').mCustomScrollbar({theme:'minimal-dark', setWidth: false, setLeft: 0, scrollbarPosition: 'outside', mouseWheel: true });
+ this.scrollParentContainer();
+ }, 500);
+ }
const $checklistsDom = this.$('.card-checklist-items');
$checklistsDom.sortable({
@@ -102,6 +155,47 @@ BlazeComponent.extendComponent({
},
});
+ // ugly touch event hotfix
+ enableClickOnTouch('.card-checklist-items .js-checklist');
+
+ const $subtasksDom = this.$('.card-subtasks-items');
+
+ $subtasksDom.sortable({
+ tolerance: 'pointer',
+ helper: 'clone',
+ handle: '.subtask-title',
+ items: '.js-subtasks',
+ placeholder: 'subtasks 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-subtasks').get(0);
+ if (prevChecklist) {
+ prevChecklist = Blaze.getData(prevChecklist).subtask;
+ }
+ let nextChecklist = ui.item.next('.js-subtasks').get(0);
+ if (nextChecklist) {
+ nextChecklist = Blaze.getData(nextChecklist).subtask;
+ }
+ const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1);
+
+ $subtasksDom.sortable('cancel');
+ const subtask = Blaze.getData(ui.item.get(0)).subtask;
+
+ Subtasks.update(subtask._id, {
+ $set: {
+ subtaskSort: sortIndex.base,
+ },
+ });
+ },
+ });
+
+ // ugly touch event hotfix
+ enableClickOnTouch('.card-subtasks-items .js-subtasks');
+
function userIsMember() {
return Meteor.user() && Meteor.user().isBoardMember();
}
@@ -111,11 +205,17 @@ BlazeComponent.extendComponent({
if ($checklistsDom.data('sortable')) {
$checklistsDom.sortable('option', 'disabled', !userIsMember());
}
+ if ($subtasksDom.data('sortable')) {
+ $subtasksDom.sortable('option', 'disabled', !userIsMember());
+ }
});
},
onDestroyed() {
- this.parentComponent().parentComponent().showOverlay.set(false);
+ const parentComponent = this.parentComponent().parentComponent();
+ //on mobile view parent is Board, not board body.
+ if (parentComponent === null) return;
+ parentComponent.showOverlay.set(false);
},
events() {
@@ -146,6 +246,20 @@ BlazeComponent.extendComponent({
this.data().setTitle(title);
}
},
+ 'submit .js-card-details-assigner'(evt) {
+ evt.preventDefault();
+ const assigner = this.currentComponent().getValue().trim();
+ if (assigner) {
+ this.data().setAssignedBy(assigner);
+ }
+ },
+ 'submit .js-card-details-requester'(evt) {
+ evt.preventDefault();
+ const requester = this.currentComponent().getValue().trim();
+ if (requester) {
+ this.data().setRequestedBy(requester);
+ }
+ },
'click .js-member': Popup.open('cardMember'),
'click .js-add-members': Popup.open('cardMembers'),
'click .js-add-labels': Popup.open('cardLabels'),
@@ -154,8 +268,11 @@ BlazeComponent.extendComponent({
'click .js-due-date': Popup.open('editCardDueDate'),
'click .js-end-date': Popup.open('editCardEndDate'),
'mouseenter .js-card-details' () {
- this.parentComponent().parentComponent().showOverlay.set(true);
- this.parentComponent().parentComponent().mouseHasEnterCardDetails = true;
+ const parentComponent = this.parentComponent().parentComponent();
+ //on mobile view parent is Board, not BoardBody.
+ if (parentComponent === null) return;
+ parentComponent.showOverlay.set(true);
+ parentComponent.mouseHasEnterCardDetails = true;
},
'click #toggleButton'() {
Meteor.call('toggleSystemMessages');
@@ -180,7 +297,7 @@ BlazeComponent.extendComponent({
close(isReset = false) {
if (this.isOpen.get() && !isReset) {
const draft = this.getValue().trim();
- if (draft !== Cards.findOne(Session.get('currentCard')).description) {
+ if (draft !== Cards.findOne(Session.get('currentCard')).getDescription()) {
UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue());
}
}
@@ -215,6 +332,7 @@ Template.cardDetailsActionsPopup.events({
'click .js-members': Popup.open('cardMembers'),
'click .js-labels': Popup.open('cardLabels'),
'click .js-attachments': Popup.open('cardAttachments'),
+ 'click .js-custom-fields': Popup.open('cardCustomFields'),
'click .js-received-date': Popup.open('editCardReceivedDate'),
'click .js-start-date': Popup.open('editCardStartDate'),
'click .js-due-date': Popup.open('editCardDueDate'),
@@ -223,6 +341,7 @@ Template.cardDetailsActionsPopup.events({
'click .js-move-card': Popup.open('moveCard'),
'click .js-copy-card': Popup.open('copyCard'),
'click .js-copy-checklist-cards': Popup.open('copyChecklistToManyCards'),
+ 'click .js-set-card-color': Popup.open('setCardColor'),
'click .js-move-card-to-top' (evt) {
evt.preventDefault();
const minOrder = _.min(this.list().cards(this.swimlaneId).map((c) => c.sort));
@@ -262,20 +381,47 @@ Template.editCardTitleForm.events({
},
});
+Template.editCardRequesterForm.onRendered(function() {
+ autosize(this.$('.js-edit-card-requester'));
+});
+
+Template.editCardRequesterForm.events({
+ 'keydown .js-edit-card-requester'(evt) {
+ // If enter key was pressed, submit the data
+ if (evt.keyCode === 13) {
+ $('.js-submit-edit-card-requester-form').click();
+ }
+ },
+});
+
+Template.editCardAssignerForm.onRendered(function() {
+ autosize(this.$('.js-edit-card-assigner'));
+});
+
+Template.editCardAssignerForm.events({
+ 'keydown .js-edit-card-assigner'(evt) {
+ // If enter key was pressed, submit the data
+ if (evt.keyCode === 13) {
+ $('.js-submit-edit-card-assigner-form').click();
+ }
+ },
+});
+
Template.moveCardPopup.events({
'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 bSelect = $('.js-select-boards')[0];
+ const boardId = bSelect.options[bSelect.selectedIndex].value;
const lSelect = $('.js-select-lists')[0];
- const newListId = lSelect.options[lSelect.selectedIndex].value;
+ const listId = lSelect.options[lSelect.selectedIndex].value;
const slSelect = $('.js-select-swimlanes')[0];
- card.swimlaneId = slSelect.options[slSelect.selectedIndex].value;
- card.move(card.swimlaneId, newListId, 0);
+ const swimlaneId = slSelect.options[slSelect.selectedIndex].value;
+ card.move(boardId, swimlaneId, listId, 0);
Popup.close();
},
});
-
BlazeComponent.extendComponent({
onCreated() {
subManager.subscribe('board', Session.get('currentBoard'));
@@ -286,6 +432,7 @@ BlazeComponent.extendComponent({
const boards = Boards.find({
archived: false,
'members.userId': Meteor.userId(),
+ _id: {$ne: Meteor.user().getTemplatesBoardId()},
}, {
sort: ['title'],
});
@@ -315,14 +462,12 @@ BlazeComponent.extendComponent({
Template.copyCardPopup.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;
+ listId = lSelect.options[lSelect.selectedIndex].value;
const slSelect = $('.js-select-swimlanes')[0];
- card.swimlaneId = slSelect.options[slSelect.selectedIndex].value;
+ const swimlaneId = slSelect.options[slSelect.selectedIndex].value;
const bSelect = $('.js-select-boards')[0];
- card.boardId = bSelect.options[bSelect.selectedIndex].value;
+ const 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
@@ -331,39 +476,13 @@ Template.copyCardPopup.events({
if (title) {
card.title = title;
card.coverId = '';
- const _id = Cards.insert(card);
+ const _id = card.copy(boardId, swimlaneId, listId);
// 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();
}
},
@@ -400,30 +519,23 @@ Template.copyChecklistToManyCardsPopup.events({
Filter.addException(_id);
// copy checklists
- let cursor = Checklists.find({cardId: oldId});
+ Checklists.find({cardId: oldId}).forEach((ch) => {
+ ch.copy(_id);
+ });
+
+ // copy subtasks
+ cursor = Cards.find({parentId: 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);
- });
+ const subtask = arguments[0];
+ subtask.parentId = _id;
+ subtask._id = null;
+ /* const newSubtaskId = */ Cards.insert(subtask);
});
// 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);
+ CardComments.find({cardId: oldId}).forEach((cmt) => {
+ cmt.copy(_id);
});
}
Popup.close();
@@ -431,36 +543,153 @@ Template.copyChecklistToManyCardsPopup.events({
},
});
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.currentCard = this.currentData();
+ this.currentColor = new ReactiveVar(this.currentCard.color);
+ },
+
+ colors() {
+ return cardColors.map((color) => ({ color, name: '' }));
+ },
-Template.cardMorePopup.events({
- 'click .js-copy-card-link-to-clipboard' () {
- // Clipboard code from:
- // https://stackoverflow.com/questions/6300213/copy-selected-text-to-the-clipboard-without-using-flash-must-be-cross-browser
- const StringToCopyElement = document.getElementById('cardURL');
- StringToCopyElement.select();
- if (document.execCommand('copy')) {
- StringToCopyElement.blur();
+ isSelected(color) {
+ if (this.currentColor.get() === null) {
+ return color === 'white';
+ }
+ return this.currentColor.get() === color;
+ },
+
+ events() {
+ return [{
+ 'click .js-palette-color'() {
+ this.currentColor.set(this.currentData().color);
+ },
+ 'click .js-submit' () {
+ this.currentCard.setColor(this.currentColor.get());
+ Popup.close();
+ },
+ 'click .js-remove-color'() {
+ this.currentCard.setColor(null);
+ Popup.close();
+ },
+ }];
+ },
+}).register('setCardColorPopup');
+
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.currentCard = this.currentData();
+ this.parentBoard = new ReactiveVar(null);
+ this.parentCard = this.currentCard.parentCard();
+ if (this.parentCard) {
+ const list = $('.js-field-parent-card');
+ list.val(this.parentCard._id);
+ this.parentBoard.set(this.parentCard.board()._id);
} else {
- document.getElementById('cardURL').selectionStart = 0;
- document.getElementById('cardURL').selectionEnd = 999;
- document.execCommand('copy');
- if (window.getSelection) {
- if (window.getSelection().empty) { // Chrome
- window.getSelection().empty();
- } else if (window.getSelection().removeAllRanges) { // Firefox
- window.getSelection().removeAllRanges();
- }
- } else if (document.selection) { // IE?
- document.selection.empty();
- }
+ this.parentBoard.set(null);
}
},
- 'click .js-delete': Popup.afterConfirm('cardDelete', function () {
- Popup.close();
- Cards.remove(this._id);
- Utils.goBoardId(this.boardId);
- }),
-});
+
+ boards() {
+ const boards = Boards.find({
+ archived: false,
+ 'members.userId': Meteor.userId(),
+ _id: {
+ $ne: Meteor.user().getTemplatesBoardId(),
+ },
+ }, {
+ sort: ['title'],
+ });
+ return boards;
+ },
+
+ cards() {
+ const currentId = Session.get('currentCard');
+ if (this.parentBoard.get()) {
+ return Cards.find({
+ boardId: this.parentBoard.get(),
+ _id: {$ne: currentId},
+ });
+ } else {
+ return [];
+ }
+ },
+
+ isParentBoard() {
+ const board = this.currentData();
+ if (this.parentBoard.get()) {
+ return board._id === this.parentBoard.get();
+ }
+ return false;
+ },
+
+ isParentCard() {
+ const card = this.currentData();
+ if (this.parentCard) {
+ return card._id === this.parentCard;
+ }
+ return false;
+ },
+
+ setParentCardId(cardId) {
+ if (cardId) {
+ this.parentCard = Cards.findOne(cardId);
+ } else {
+ this.parentCard = null;
+ }
+ this.currentCard.setParentId(cardId);
+ },
+
+ events() {
+ return [{
+ 'click .js-copy-card-link-to-clipboard' () {
+ // Clipboard code from:
+ // https://stackoverflow.com/questions/6300213/copy-selected-text-to-the-clipboard-without-using-flash-must-be-cross-browser
+ const StringToCopyElement = document.getElementById('cardURL');
+ StringToCopyElement.select();
+ if (document.execCommand('copy')) {
+ StringToCopyElement.blur();
+ } else {
+ document.getElementById('cardURL').selectionStart = 0;
+ document.getElementById('cardURL').selectionEnd = 999;
+ document.execCommand('copy');
+ if (window.getSelection) {
+ if (window.getSelection().empty) { // Chrome
+ window.getSelection().empty();
+ } else if (window.getSelection().removeAllRanges) { // Firefox
+ window.getSelection().removeAllRanges();
+ }
+ } else if (document.selection) { // IE?
+ document.selection.empty();
+ }
+ }
+ },
+ 'click .js-delete': Popup.afterConfirm('cardDelete', function () {
+ Popup.close();
+ Cards.remove(this._id);
+ Utils.goBoardId(this.boardId);
+ }),
+ 'change .js-field-parent-board'(evt) {
+ const selection = $(evt.currentTarget).val();
+ const list = $('.js-field-parent-card');
+ if (selection === 'none') {
+ this.parentBoard.set(null);
+ } else {
+ subManager.subscribe('board', $(evt.currentTarget).val());
+ this.parentBoard.set(selection);
+ list.prop('disabled', false);
+ }
+ this.setParentCardId(null);
+ },
+ 'change .js-field-parent-card'(evt) {
+ const selection = $(evt.currentTarget).val();
+ this.setParentCardId(selection);
+ },
+ }];
+ },
+}).register('cardMorePopup');
+
// Close the card details pane by pressing escape
EscapeActions.register('detailsPane',
diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl
index e5739a93..c1d6b7e1 100644
--- a/client/components/cards/cardDetails.styl
+++ b/client/components/cards/cardDetails.styl
@@ -1,11 +1,12 @@
@import 'nib'
.card-details
- padding: 0 20px
+ padding: 0
flex-shrink: 0
- flex-basis: 470px
+ flex-basis: 510px
will-change: flex-basis
- overflow: hidden
+ overflow-y: scroll
+ overflow-x: hidden
background: darken(white, 3%)
border-radius: bottom 3px
z-index: 20 !important
@@ -13,8 +14,16 @@
box-shadow: 0 0 7px 0 darken(white, 30%)
transition: flex-basis 0.1s
+ .mCustomScrollBox
+ padding-left: 0
+
+ .ps-scrollbar-y-rail
+ pointer-event: all
+ position: absolute;
+
.card-details-canvas
width: 470px
+ padding-left: 20px;
.card-details-header
margin: 0 -20px 5px
@@ -46,6 +55,12 @@
margin: 7px 0 0
padding: 0
+ .linked-card-location
+ font-style: italic
+ font-size: 1em
+ margin-bottom: 0
+ & p
+ margin-bottom: 0
form.inlined-form
margin-top: 5px
@@ -69,6 +84,7 @@
.card-details-items
display: flex
+ flex-wrap: wrap
margin: 15px 0
.card-details-item
@@ -80,9 +96,11 @@
&.card-details-item-received,
&.card-details-item-start,
&.card-details-item-due,
- &.card-details-item-end
- width: 50%
- flex-shrink: 1
+ &.card-details-item-end,
+ &.card-details-item-customfield,
+ &.card-details-item-name
+ max-width: 50%
+ flex-grow: 1
.card-details-item-title
font-size: 16px
@@ -115,10 +133,92 @@ input[type="submit"].attachment-add-link-submit
.card-details-canvas
width: 100%
-
+ padding-left: 0px;
+
.card-details-header
.close-card-details
margin-right: 0px
.card-details-menu
margin-right: 10px
+
+card-details-color(background, color...)
+ background: background !important
+ if color
+ color: color !important //overwrite text for better visibility
+
+.card-details-white
+ card-details-color(unset, #000) //Black text for better visibility
+ border: 1px solid #eee
+
+.card-details-green
+ card-details-color(#3cb500, #ffffff) //White text for better visibility
+
+.card-details-yellow
+ card-details-color(#fad900, #000) //Black text for better visibility
+
+.card-details-orange
+ card-details-color(#ff9f19, #000) //Black text for better visibility
+
+.card-details-red
+ card-details-color(#eb4646, #ffffff) //White text for better visibility
+
+.card-details-purple
+ card-details-color(#a632db, #ffffff) //White text for better visibility
+
+.card-details-blue
+ card-details-color(#0079bf, #ffffff) //White text for better visibility
+
+.card-details-pink
+ card-details-color(#ff78cb, #000) //Black text for better visibility
+
+.card-details-sky
+ card-details-color(#00c2e0, #ffffff) //White text for better visibility
+
+.card-details-black
+ card-details-color(#4d4d4d, #ffffff) //White text for better visibility
+
+.card-details-lime
+ card-details-color(#51e898, #000) //Black text for better visibility
+
+.card-details-silver
+ card-details-color(#c0c0c0, #000) //Black text for better visibility
+
+.card-details-peachpuff
+ card-details-color(#ffdab9, #000) //Black text for better visibility
+
+.card-details-crimson
+ card-details-color(#dc143c, #ffffff) //White text for better visibility
+
+.card-details-plum
+ card-details-color(#dda0dd, #000) //Black text for better visibility
+
+.card-details-darkgreen
+ card-details-color(#006400, #ffffff) //White text for better visibility
+
+.card-details-slateblue
+ card-details-color(#6a5acd, #ffffff) //White text for better visibility
+
+.card-details-magenta
+ card-details-color(#ff00ff, #ffffff) //White text for better visibility
+
+.card-details-gold
+ card-details-color(#ffd700, #000) //Black text for better visibility
+
+.card-details-navy
+ card-details-color(#000080, #ffffff) //White text for better visibility
+
+.card-details-gray
+ card-details-color(#808080, #ffffff) //White text for better visibility
+
+.card-details-saddlebrown
+ card-details-color(#8b4513, #ffffff) //White text for better visibility
+
+.card-details-paleturquoise
+ card-details-color(#afeeee, #000) //Black text for better visibility
+
+.card-details-mistyrose
+ card-details-color(#ffe4e1, #000) //Black text for better visibility
+
+.card-details-indigo
+ card-details-color(#4b0082, #ffffff) //White text for better visibility
diff --git a/client/components/cards/cardTime.jade b/client/components/cards/cardTime.jade
index dcfc92f0..8af8c414 100644
--- a/client/components/cards/cardTime.jade
+++ b/client/components/cards/cardTime.jade
@@ -3,10 +3,10 @@ template(name="editCardSpentTime")
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)
+ input.js-time-field#time(type="number" step="0.01" name="time" value="{{card.getSpentTime}}" placeholder=timeFormat autofocus)
label(for="overtime") {{_ 'overtime'}}
a.js-toggle-overtime
- .materialCheckBox#overtime(class="{{#if card.isOvertime}}is-checked{{/if}}" name="overtime")
+ .materialCheckBox#overtime(class="{{#if getIsOvertime}}is-checked{{/if}}" name="overtime")
if error.get
.warning {{_ error.get}}
@@ -15,8 +15,8 @@ template(name="editCardSpentTime")
template(name="timeBadge")
if canModifyCard
- a.js-edit-time.card-time(title="{{showTitle}}" class="{{#if isOvertime}}card-label-red{{else}}card-label-green{{/if}}")
+ a.js-edit-time.card-time(title="{{showTitle}}" class="{{#if getIsOvertime}}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}}")
+ a.card-time(title="{{showTitle}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
| {{showTime}}
diff --git a/client/components/cards/cardTime.js b/client/components/cards/cardTime.js
index eadcc88e..80b7fc84 100644
--- a/client/components/cards/cardTime.js
+++ b/client/components/cards/cardTime.js
@@ -7,17 +7,17 @@ BlazeComponent.extendComponent({
this.card = this.data();
},
toggleOvertime() {
- this.card.isOvertime = !this.card.isOvertime;
+ this.card.setIsOvertime(!this.card.getIsOvertime());
$('#overtime .materialCheckBox').toggleClass('is-checked');
$('#overtime').toggleClass('is-checked');
},
storeTime(spentTime, isOvertime) {
this.card.setSpentTime(spentTime);
- this.card.setOvertime(isOvertime);
+ this.card.setIsOvertime(isOvertime);
},
deleteTime() {
- this.card.unsetSpentTime();
+ this.card.setSpentTime(null);
},
events() {
return [{
@@ -26,7 +26,7 @@ BlazeComponent.extendComponent({
evt.preventDefault();
const spentTime = parseFloat(evt.target.time.value);
- const isOvertime = this.card.isOvertime;
+ const isOvertime = this.card.getIsOvertime();
if (spentTime >= 0) {
this.storeTime(spentTime, isOvertime);
@@ -55,17 +55,14 @@ BlazeComponent.extendComponent({
self.time = ReactiveVar();
},
showTitle() {
- if (this.data().isOvertime) {
- return `${TAPi18n.__('overtime')} ${this.data().spentTime} ${TAPi18n.__('hours')}`;
+ if (this.data().getIsOvertime()) {
+ return `${TAPi18n.__('overtime')} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
} else {
- return `${TAPi18n.__('card-spent')} ${this.data().spentTime} ${TAPi18n.__('hours')}`;
+ return `${TAPi18n.__('card-spent')} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
}
},
showTime() {
- return this.data().spentTime;
- },
- isOvertime() {
- return this.data().isOvertime;
+ return this.data().getSpentTime();
},
events() {
return [{
diff --git a/client/components/cards/checklists.jade b/client/components/cards/checklists.jade
index ae680bd5..279d3671 100644
--- a/client/components/cards/checklists.jade
+++ b/client/components/cards/checklists.jade
@@ -27,7 +27,6 @@ template(name="checklistDetail")
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
@@ -57,7 +56,7 @@ template(name="addChecklistItemForm")
a.fa.fa-times-thin.js-close-inlined-form
template(name="editChecklistItemForm")
- textarea.js-edit-checklist-item(rows='1' autofocus)
+ textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
if $eq type 'item'
= item.title
else
@@ -75,7 +74,7 @@ template(name="checklistItems")
+inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
+editChecklistItemForm(type = 'item' item = item checklist = checklist)
else
- +itemDetail(item = item checklist = checklist)
+ +checklistItemDetail(item = item checklist = checklist)
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist)
+addChecklistItemForm
@@ -84,7 +83,7 @@ template(name="checklistItems")
i.fa.fa-plus
| {{_ 'add-checklist-item'}}...
-template(name='itemDetail')
+template(name='checklistItemDetail')
.js-checklist-item.checklist-item
if canModifyCard
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
diff --git a/client/components/cards/checklists.js b/client/components/cards/checklists.js
index 1f05aded..5d789351 100644
--- a/client/components/cards/checklists.js
+++ b/client/components/cards/checklists.js
@@ -1,4 +1,4 @@
-const { calculateIndexData } = Utils;
+const { calculateIndexData, enableClickOnTouch } = Utils;
function initSorting(items) {
items.sortable({
@@ -36,6 +36,9 @@ function initSorting(items) {
checklistItem.move(checklistId, sortIndex.base);
},
});
+
+ // ugly touch event hotfix
+ enableClickOnTouch('.js-checklist-item:not(.placeholder)');
}
BlazeComponent.extendComponent({
@@ -71,8 +74,10 @@ BlazeComponent.extendComponent({
event.preventDefault();
const textarea = this.find('textarea.js-add-checklist-item');
const title = textarea.value.trim();
- const cardId = this.currentData().cardId;
+ let cardId = this.currentData().cardId;
const card = Cards.findOne(cardId);
+ if (card.isLinked())
+ cardId = card.linkedId;
if (title) {
Checklists.insert({
@@ -204,7 +209,7 @@ Template.checklistDeleteDialog.onDestroyed(() => {
$cardDetails.animate( { scrollTop: this.scrollState.position });
});
-Template.itemDetail.helpers({
+Template.checklistItemDetail.helpers({
canModifyCard() {
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
},
@@ -223,4 +228,4 @@ BlazeComponent.extendComponent({
'click .js-checklist-item .check-box': this.toggleItem,
}];
},
-}).register('itemDetail');
+}).register('checklistItemDetail');
diff --git a/client/components/cards/labels.styl b/client/components/cards/labels.styl
index 361a17ae..3b481d93 100644
--- a/client/components/cards/labels.styl
+++ b/client/components/cards/labels.styl
@@ -3,7 +3,7 @@
// XXX Use .board-widget-labels as a flexbox container
.card-label
border-radius: 4px
- color: white
+ color: white //Default white text, in select cases, changed to black to improve contrast between label colour and text
display: inline-block
font-weight: 700
font-size: 13px
@@ -48,9 +48,11 @@
.card-label-yellow
background-color: #fad900
+ color: #000000 //Black text for better visibility
.card-label-orange
background-color: #ff9f19
+ color: #000000 //Black text for better visibility
.card-label-red
background-color: #eb4646
@@ -63,6 +65,7 @@
.card-label-pink
background-color: #ff78cb
+ color: #000000 //Black text for better visibility
.card-label-sky
background-color: #00c2e0
@@ -72,6 +75,55 @@
.card-label-lime
background-color: #51e898
+ color: #000000 //Black text for better visibility
+
+.card-label-silver
+ background-color: #c0c0c0
+ color: #000000 //Black text for better visibility
+
+.card-label-peachpuff
+ background-color: #ffdab9
+ color: #000000 //Black text for better visibility
+
+.card-label-crimson
+ background-color: #dc143c
+
+.card-label-plum
+ background-color: #dda0dd
+ color: #000000 //Black text for better visibility
+
+.card-label-darkgreen
+ background-color: #006400
+
+.card-label-slateblue
+ background-color: #6a5acd
+
+.card-label-magenta
+ background-color: #ff00ff
+
+.card-label-gold
+ background-color: #ffd700
+ color: #000000 //Black text for better visibility
+
+.card-label-navy
+ background-color: #000080
+
+.card-label-gray
+ background-color: #808080
+
+.card-label-saddlebrown
+ background-color: #8b4513
+
+.card-label-paleturquoise
+ background-color: #afeeee
+ color: #000000 //Black text for better visibility
+
+.card-label-mistyrose
+ background-color: #ffe4e1
+ color: #000000 //Black text for better visibility
+
+.card-label-indigo
+ background-color: #4b0082
.edit-label,
.create-label
diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade
index 9fa4dd57..f47ae0c9 100644
--- a/client/components/cards/minicard.jade
+++ b/client/components/cards/minicard.jade
@@ -1,5 +1,8 @@
template(name="minicard")
- .minicard
+ .minicard(
+ class="{{#if isLinkedCard}}linked-card{{/if}}"
+ class="{{#if isLinkedBoard}}linked-board{{/if}}"
+ class="minicard-{{colorClass}}")
if cover
.minicard-cover(style="background-image: url('{{cover.url}}');")
if labels
@@ -7,30 +10,72 @@ template(name="minicard")
each labels
.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 ' > ' }}
+ if $eq 'prefix-with-parent' currentBoard.presentParentTask
+ .parent-prefix
+ | {{ parentCardName }}
+ if isLinkedBoard
+ a.js-linked-link
+ span.linked-icon.fa.fa-folder
+ else if isLinkedCard
+ a.js-linked-link
+ span.linked-icon.fa.fa-id-card
+ if getArchived
+ span.linked-icon.linked-archived.fa.fa-archive
+viewer
- = title
+ = getTitle
+ if $eq 'subtext-with-full-path' currentBoard.presentParentTask
+ .parent-subtext
+ | {{ parentString ' > ' }}
+ if $eq 'subtext-with-parent' currentBoard.presentParentTask
+ .parent-subtext
+ | {{ parentCardName }}
+
.dates
- if startAt
+ if getReceived
+ unless getStart
+ unless getDue
+ unless getEnd
+ .date
+ +minicardReceivedDate
+ if getStart
.date
+minicardStartDate
- if dueAt
+ if getDue
.date
+minicardDueDate
- if spentTime
+ if getSpentTime
.date
+cardSpentTime
- if members
+ .minicard-custom-fields
+ each customFieldsWD
+ if definition.showOnCard
+ .minicard-custom-field
+ if definition.showLabelOnMiniCard
+ .minicard-custom-field-item
+ = definition.name
+ .minicard-custom-field-item
+ +viewer
+ = trueValue
+
+ if getMembers
.minicard-members.js-minicard-members
- each members
+ each getMembers
+userAvatar(userId=this)
+
.badges
- if comments.count
- .badge(title="{{_ 'card-comments-title' comments.count }}")
- span.badge-icon.fa.fa-comment-o.badge-comment
- span.badge-text= comments.count
- if description
- .badge.badge-state-image-only(title=description)
+ unless currentUser.isNoComments
+ if comments.count
+ .badge(title="{{_ 'card-comments-title' comments.count }}")
+ span.badge-icon.fa.fa-comment-o.badge-comment
+ span.badge-text= comments.count
+ if getDescription
+ .badge.badge-state-image-only(title=getDescription)
span.badge-icon.fa.fa-align-left
if attachments.count
.badge
diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js
index a98b5730..da7f9e01 100644
--- a/client/components/cards/minicard.js
+++ b/client/components/cards/minicard.js
@@ -6,4 +6,15 @@ BlazeComponent.extendComponent({
template() {
return 'minicard';
},
+
+ events() {
+ return [{
+ 'click .js-linked-link' () {
+ if (this.data().isLinkedCard())
+ Utils.goCardId(this.data().linkedId);
+ else if (this.data().isLinkedBoard())
+ Utils.goBoardId(this.data().linkedId);
+ },
+ }];
+ },
}).register('minicard');
diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl
index d59f1f63..e3d1ff20 100644
--- a/client/components/cards/minicard.styl
+++ b/client/components/cards/minicard.styl
@@ -9,7 +9,7 @@
&.placeholder
background: darken(white, 20%)
- border-radius: 2px
+ border-radius: 9px
&.ui-sortable-helper
cursor: grabbing
@@ -44,6 +44,16 @@
transition: transform 0.2s,
border-radius 0.2s
+ &.linked-board
+ &.linked-card
+ .linked-icon
+ display: inline-block
+ margin-right: 11px
+ vertical-align: baseline
+ font-size: 0.9em
+ .linked-archived
+ color: #937760
+
.is-selected &
transform: translateX(11px)
border-bottom-right-radius: 0
@@ -77,9 +87,31 @@
height: @width
border-radius: 2px
margin-left: 3px
+ .minicard-custom-fields
+ display:block;
+ .minicard-custom-field
+ display:flex;
+ .minicard-custom-field-item
+ max-width:50%;
+ flex-grow:1;
+ .handle
+ width: 20px;
+ height: 20px;
+ position: absolute;
+ right: 5px;
+ top: 5px;
+ display:none;
+ @media only screen and (max-width: 1199px) {
+ display:block;
+ }
+ .fa-arrows
+ font-size:20px;
+ color: #ccc;
.minicard-title
p:last-child
margin-bottom: 0
+ .viewer
+ display: inline-block
.dates
display: flex;
flex-direction: row;
@@ -155,6 +187,13 @@
margin-bottom: 20px
overflow-y: auto
+.parent-prefix
+ color: darken(white, 30%)
+ font-size: 0.9em
+.parent-subtext
+ color: darken(white, 30%)
+ font-size: 0.9em
+
@media screen and (max-width: 800px)
.minicard
.is-selected &
@@ -163,3 +202,86 @@
border-top-right-radius: 0
z-index: 15
box-shadow: 0 1px 2px rgba(0,0,0,.15)
+
+minicard-color(background, color...)
+ background-color: background
+ if color
+ color: color //overwrite text for better visibility
+ &:hover:not(.minicard-composer),
+ .is-selected &,
+ .draggable-hover-card &
+ background: darken(background, 3%)
+ .draggable-hover-card &
+ background: darken(background, 7%)
+
+.minicard-green
+ minicard-color(#3cb500, #ffffff) //White text for better visibility
+
+.minicard-yellow
+ minicard-color(#fad900)
+
+.minicard-orange
+ minicard-color(#ff9f19)
+
+.minicard-red
+ minicard-color(#eb4646, #ffffff) //White text for better visibility
+
+.minicard-purple
+ minicard-color(#a632db, #ffffff) //White text for better visibility
+
+.minicard-blue
+ minicard-color(#0079bf, #ffffff) //White text for better visibility
+
+.minicard-pink
+ minicard-color(#ff78cb)
+
+.minicard-sky
+ minicard-color(#00c2e0, #ffffff) //White text for better visibility
+
+.minicard-black
+ minicard-color(#4d4d4d, #ffffff) //White text for better visibility
+
+.minicard-lime
+ minicard-color(#51e898)
+
+.minicard-silver
+ minicard-color(#c0c0c0)
+
+.minicard-peachpuff
+ minicard-color(#ffdab9)
+
+.minicard-crimson
+ minicard-color(#dc143c, #ffffff) //White text for better visibility
+
+.minicard-plum
+ minicard-color(#dda0dd)
+
+.minicard-darkgreen
+ minicard-color(#006400, #ffffff) //White text for better visibility
+
+.minicard-slateblue
+ minicard-color(#6a5acd, #ffffff) //White text for better visibility
+
+.minicard-magenta
+ minicard-color(#ff00ff, #ffffff) //White text for better visibility
+
+.minicard-gold
+ minicard-color(#ffd700)
+
+.minicard-navy
+ minicard-color(#000080, #ffffff) //White text for better visibility
+
+.minicard-gray
+ minicard-color(#808080, #ffffff) //White text for better visibility
+
+.minicard-saddlebrown
+ minicard-color(#8b4513, #ffffff) //White text for better visibility
+
+.minicard-paleturquoise
+ minicard-color(#afeeee)
+
+.minicard-mistyrose
+ minicard-color(#ffe4e1)
+
+.minicard-indigo
+ minicard-color(#4b0082, #ffffff) //White text for better visibility
diff --git a/client/components/cards/subtasks.jade b/client/components/cards/subtasks.jade
new file mode 100644
index 00000000..7e64e23f
--- /dev/null
+++ b/client/components/cards/subtasks.jade
@@ -0,0 +1,97 @@
+template(name="subtasks")
+ h3 {{_ 'subtasks'}}
+ if toggleDeleteDialog.get
+ .board-overlay#card-details-overlay
+ +subtaskDeleteDialog(subtask = subtaskToDelete)
+
+
+ .card-subtasks-items
+ each subtask in currentCard.subtasks
+ +subtaskDetail(subtask = subtask)
+
+ if canModifyCard
+ +inlinedForm(autoclose=false classNames="js-add-subtask" cardId = cardId)
+ +addSubtaskItemForm
+ else
+ a.js-open-inlined-form
+ i.fa.fa-plus
+ | {{_ 'add-subtask'}}...
+
+template(name="subtaskDetail")
+ .js-subtasks.subtask
+ +inlinedForm(classNames="js-edit-subtask-title" subtask = subtask)
+ +editSubtaskItemForm(subtask = subtask)
+ else
+ .subtask-title
+ span
+ a.js-view-subtask(title="{{ subtask.title }}") {{_ "view-it"}}
+ if canModifyCard
+ a.js-delete-subtask.toggle-delete-subtask-dialog {{_ "delete"}}...
+
+ if canModifyCard
+ h2.title.js-open-inlined-form.is-editable
+ +viewer
+ = subtask.title
+ else
+ h2.title
+ +viewer
+ = subtask.title
+
+template(name="subtaskDeleteDialog")
+ .js-confirm-subtask-delete
+ p
+ i(class="fa fa-exclamation-triangle" aria-hidden="true")
+ p
+ | {{_ 'confirm-subtask-delete-dialog'}}
+ span {{subtask.title}}
+ | ?
+ .js-subtask-delete-buttons
+ button.confirm-subtask-delete(type="button") {{_ 'delete'}}
+ button.toggle-delete-subtask-dialog(type="button") {{_ 'cancel'}}
+
+template(name="addSubtaskItemForm")
+ textarea.js-add-subtask-item(rows='1' autofocus dir="auto")
+ .edit-controls.clearfix
+ button.primary.confirm.js-submit-add-subtask-item-form(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+
+template(name="editSubtaskItemForm")
+ textarea.js-edit-subtask-item(rows='1' autofocus dir="auto")
+ if $eq type 'item'
+ = item.title
+ else
+ = subtask.title
+ .edit-controls.clearfix
+ button.primary.confirm.js-submit-edit-subtask-item-form(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+ span(title=createdAt) {{ moment createdAt }}
+ if canModifyCard
+ a.js-delete-subtask-item {{_ "delete"}}...
+
+template(name="subtasksItems")
+ .subtasks-items.js-subtasks-items
+ each item in subtasks.items
+ +inlinedForm(classNames="js-edit-subtask-item" item = item subtasks = subtasks)
+ +editSubtaskItemForm(type = 'item' item = item subtasks = subtasks)
+ else
+ +subtaskItemDetail(item = item subtasks = subtasks)
+ if canModifyCard
+ +inlinedForm(autoclose=false classNames="js-add-subtask-item" subtasks = subtasks dir="auto")
+ +addSubtaskItemForm
+ else
+ a.add-subtask-item.js-open-inlined-form
+ i.fa.fa-plus
+ | {{_ 'add-subtask-item'}}...
+
+template(name='subtaskItemDetail')
+ .js-subtasks-item.subtasks-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}}")
+ +viewer
+ = item.title
+ else
+ .materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
+ .item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
+ +viewer
+ = item.title
diff --git a/client/components/cards/subtasks.js b/client/components/cards/subtasks.js
new file mode 100644
index 00000000..1651d449
--- /dev/null
+++ b/client/components/cards/subtasks.js
@@ -0,0 +1,146 @@
+BlazeComponent.extendComponent({
+ canModifyCard() {
+ return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+ },
+}).register('subtaskDetail');
+
+BlazeComponent.extendComponent({
+
+ addSubtask(event) {
+ event.preventDefault();
+ const textarea = this.find('textarea.js-add-subtask-item');
+ const title = textarea.value.trim();
+ const cardId = this.currentData().cardId;
+ const card = Cards.findOne(cardId);
+ const sortIndex = -1;
+ const crtBoard = Boards.findOne(card.boardId);
+ const targetBoard = crtBoard.getDefaultSubtasksBoard();
+ const listId = targetBoard.getDefaultSubtasksListId();
+ const swimlaneId = targetBoard.getDefaultSwimline()._id;
+
+ if (title) {
+ const _id = Cards.insert({
+ title,
+ parentId: cardId,
+ members: [],
+ labelIds: [],
+ customFields: [],
+ listId,
+ boardId: targetBoard._id,
+ sort: sortIndex,
+ swimlaneId,
+ type: 'cardType-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);
+
+
+ setTimeout(() => {
+ this.$('.add-subtask-item').last().click();
+ }, 100);
+ }
+ textarea.value = '';
+ textarea.focus();
+ },
+
+ canModifyCard() {
+ return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+ },
+
+ deleteSubtask() {
+ const subtask = this.currentData().subtask;
+ if (subtask && subtask._id) {
+ subtask.archive();
+ this.toggleDeleteDialog.set(false);
+ }
+ },
+
+ editSubtask(event) {
+ event.preventDefault();
+ const textarea = this.find('textarea.js-edit-subtask-item');
+ const title = textarea.value.trim();
+ const subtask = this.currentData().subtask;
+ subtask.setTitle(title);
+ },
+
+ onCreated() {
+ this.toggleDeleteDialog = new ReactiveVar(false);
+ this.subtaskToDelete = null; //Store data context to pass to subtaskDeleteDialog template
+ },
+
+ pressKey(event) {
+ //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();
+ }
+ },
+
+ events() {
+ const events = {
+ 'click .toggle-delete-subtask-dialog'(event) {
+ if($(event.target).hasClass('js-delete-subtask')){
+ this.subtaskToDelete = this.currentData().subtask; //Store data context
+ }
+ this.toggleDeleteDialog.set(!this.toggleDeleteDialog.get());
+ },
+ 'click .js-view-subtask'(event) {
+ if($(event.target).hasClass('js-view-subtask')){
+ const subtask = this.currentData().subtask;
+ const board = subtask.board();
+ FlowRouter.go('card', {
+ boardId: board._id,
+ slug: board.slug,
+ cardId: subtask._id,
+ });
+ }
+ },
+ };
+
+ return [{
+ ...events,
+ 'submit .js-add-subtask': this.addSubtask,
+ 'submit .js-edit-subtask-title': this.editSubtask,
+ 'click .confirm-subtask-delete': this.deleteSubtask,
+ keydown: this.pressKey,
+ }];
+ },
+}).register('subtasks');
+
+Template.subtaskDeleteDialog.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.subtaskDeleteDialog.onDestroyed(() => {
+ const $cardDetails = this.$('.card-details');
+ $cardDetails.off('scroll'); //Reactivate scrolling
+ $cardDetails.animate( { scrollTop: this.scrollState.position });
+});
+
+Template.subtaskItemDetail.helpers({
+ canModifyCard() {
+ return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+ },
+});
+
+BlazeComponent.extendComponent({
+ // ...
+}).register('subtaskItemDetail');
diff --git a/client/components/cards/subtasks.styl b/client/components/cards/subtasks.styl
new file mode 100644
index 00000000..c2f09aa1
--- /dev/null
+++ b/client/components/cards/subtasks.styl
@@ -0,0 +1,142 @@
+.js-add-subtask
+ color: #8c8c8c
+
+textarea.js-add-subtask-item, textarea.js-edit-subtask-item
+ overflow: hidden
+ word-wrap: break-word
+ resize: none
+ height: 34px
+
+.delete-text
+ color: #8c8c8c
+ text-decoration: underline
+ word-wrap: break-word
+ float: right
+ padding-top: 6px
+ &:hover
+ color: inherit
+
+.subtask-title
+ .checkbox
+ float: left
+ width: 30px
+ height 30px
+ font-size: 18px
+ line-height: 30px
+
+ .title
+ font-size: 18px
+ line-height: 25px
+
+ .subtasks-stat
+ margin: 0 0.5em
+ float: right
+ padding-top: 6px
+ &.is-finished
+ color: #3cb500
+
+ .js-delete-subtask
+ @extends .delete-text
+ margin: 0 0.5em
+
+ .js-view-subtask
+ @extends .delete-text
+
+.js-confirm-subtask-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-subtask-delete-buttons
+ position: relative
+ padding: left 2% right 2%
+ .confirm-subtask-delete
+ margin-left: 12%
+ float: left
+ .toggle-delete-subtask-dialog
+ margin-right: 12%
+ float: right
+
+#card-details-overlay
+ top: 0
+ bottom: -600px
+ right: 0
+
+.subtasks
+ 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
+
+
+.subtasks-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-subtask-item
+ margin: 0 0 0.5em 1.33em
+ @extends .delete-text
+ padding: 12px 0 0 0
+
+.add-subtask-item
+ margin: 0.2em 0 0.5em 1.33em
+ display: inline-block
diff --git a/client/components/forms/datepicker.jade b/client/components/forms/datepicker.jade
new file mode 100644
index 00000000..96f63bc4
--- /dev/null
+++ b/client/components/forms/datepicker.jade
@@ -0,0 +1,15 @@
+template(name="datepicker")
+ .datepicker-container
+ form.edit-date
+ .fields
+ .left
+ label(for="date") {{_ 'date'}}
+ input.js-date-field#date(type="text" name="date" value=showDate placeholder=dateFormat autofocus)
+ .right
+ label(for="time") {{_ 'time'}}
+ input.js-time-field#time(type="text" name="time" value=showTime placeholder=timeFormat)
+ .js-datepicker
+ if error.get
+ .warning {{_ error.get}}
+ button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
+ button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}} \ No newline at end of file
diff --git a/client/components/forms/datepicker.styl b/client/components/forms/datepicker.styl
new file mode 100644
index 00000000..a2558094
--- /dev/null
+++ b/client/components/forms/datepicker.styl
@@ -0,0 +1,17 @@
+.datepicker-container
+ .fields
+ .left
+ width: 56%
+ .right
+ width: 38%
+ .datepicker
+ width: 100%
+ table
+ width: 100%
+ border: none
+ border-spacing: 0
+ border-collapse: collapse
+ thead
+ background: none
+ td, th
+ box-sizing: border-box \ No newline at end of file
diff --git a/client/components/forms/forms.styl b/client/components/forms/forms.styl
index 1947c11d..bfae2e7c 100644
--- a/client/components/forms/forms.styl
+++ b/client/components/forms/forms.styl
@@ -1,10 +1,10 @@
@import 'nib'
+select,
textarea,
input:not([type=file]),
button
box-sizing: border-box
- -webkit-appearance: none
background-color: #ebebeb
border: 1px solid #ccc
border-radius: 3px
@@ -85,6 +85,9 @@ select
width: 256px
margin-bottom: 8px
+ &.inline
+ width: 100%
+
option[disabled]
color: #8c8c8c
@@ -222,9 +225,12 @@ textarea
.edit-controls,
.add-controls
+ display: flex
+ align-items: baseline
margin-top: 0
button[type=submit]
+ input[type=button]
float: left
height: 32px
margin-top: -2px
diff --git a/client/components/import/import.jade b/client/components/import/import.jade
index a1fbd83b..5b52f417 100644
--- a/client/components/import/import.jade
+++ b/client/components/import/import.jade
@@ -12,11 +12,11 @@ template(name="import")
template(name="importTextarea")
form
- p: label(for='import-textarea') {{_ instruction}}
+ p: label(for='import-textarea') {{_ instruction}} {{_ 'import-board-instruction-about-errors'}}
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
+ h1.warning {{_ 'import-sandstorm-backup-warning'}}
p.warning {{_ 'import-sandstorm-warning'}}
input.primary.wide(type="submit" value="{{_ 'import'}}")
diff --git a/client/components/lists/list.js b/client/components/lists/list.js
index 38a87674..ea0068eb 100644
--- a/client/components/lists/list.js
+++ b/client/components/lists/list.js
@@ -1,4 +1,4 @@
-const { calculateIndex } = Utils;
+const { calculateIndex, enableClickOnTouch } = Utils;
BlazeComponent.extendComponent({
// Proxy
@@ -26,6 +26,13 @@ BlazeComponent.extendComponent({
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
+
+ if(window.matchMedia('(max-width: 1199px)').matches) {
+ $( '.js-minicards' ).sortable({
+ handle: '.handle',
+ });
+ }
+
$cards.sortable({
connectWith: '.js-minicards:not(.js-list-full)',
tolerance: 'pointer',
@@ -47,6 +54,7 @@ BlazeComponent.extendComponent({
items: itemsSelector,
placeholder: 'minicard-wrapper placeholder',
start(evt, ui) {
+ ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
@@ -59,7 +67,13 @@ 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;
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ let swimlaneId = '';
+ const boardView = (Meteor.user().profile || {}).boardView;
+ if (boardView === 'board-view-swimlanes' || currentBoard.isTemplatesBoard())
+ swimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))._id;
+ else if ((boardView === 'board-view-lists') || (boardView === 'board-view-cal') || !boardView)
+ swimlaneId = currentBoard.getDefaultSwimline()._id;
// Normally the jquery-ui sortable library moves the dragged DOM element
// to its new position, which disrupts Blaze reactive updates mechanism
@@ -72,17 +86,20 @@ BlazeComponent.extendComponent({
if (MultiSelection.isActive()) {
Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
- card.move(swimlaneId, listId, sortIndex.base + i * sortIndex.increment);
+ card.move(currentBoard._id, swimlaneId, listId, sortIndex.base + i * sortIndex.increment);
});
} else {
const cardDomElement = ui.item.get(0);
const card = Blaze.getData(cardDomElement);
- card.move(swimlaneId, listId, sortIndex.base);
+ card.move(currentBoard._id, swimlaneId, listId, sortIndex.base);
}
boardComponent.setIsDragging(false);
},
});
+ // ugly touch event hotfix
+ enableClickOnTouch(itemsSelector);
+
// Disable drag-dropping if the current user is not a board member or is comment only
this.autorun(() => {
$cards.sortable('option', 'disabled', !userIsMember());
diff --git a/client/components/lists/list.styl b/client/components/lists/list.styl
index fa32ff6d..dcbeb93f 100644
--- a/client/components/lists/list.styl
+++ b/client/components/lists/list.styl
@@ -10,7 +10,6 @@
// transparent, because that won't work during a list drag.
background: darken(white, 13%)
border-left: 1px solid darken(white, 20%)
- border-bottom: 1px solid #CCC
padding: 0
float: left
@@ -44,11 +43,23 @@
background: white
margin: -3px 0 8px
+.list-header-card-count
+ height: 35px
+
+.list-header-add
+ flex: 0 0 auto
+ padding: 20px 12px 4px
+ position: relative
+ min-height: 20px
+
.list-header
flex: 0 0 auto
- margin: 20px 12px 4px
+ padding: 20px 12px 4px
position: relative
min-height: 20px
+ background-color: #e4e4e4;
+ border-bottom: 6px solid #e4e4e4;
+
&.ui-sortable-handle
cursor: grab
@@ -68,16 +79,18 @@
text-overflow: ellipsis
word-wrap: break-word
+
.list-header-watch-icon
padding-left: 10px
color: #a6a6a6
+
.list-header-menu
position: absolute
- padding: 7px
+ padding: 27px 19px
margin-top: 1px
- top: -@padding
- right: -@padding
+ top: -7px
+ right: -7px
.list-header-plus-icon
color: #a6a6a6
@@ -143,9 +156,12 @@
float: left
@media screen and (max-width: 800px)
+ .list-header-menu
+ margin-right: 30px
+
.mini-list
flex: 0 0 60px
- height: 60px
+ height: auto
width: 100%
border-left: 0px
border-bottom: 1px solid darken(white, 20%)
@@ -154,6 +170,8 @@
display: block
width: 100%
border-left: 0px
+ &:first-child
+ margin-left: 0px
&.ui-sortable-helper
flex: 0 0 60px
@@ -172,8 +190,16 @@
border-left: 0px
border-bottom: 1px solid darken(white, 20%)
- .list-header
+ .list-body
+ padding: 15px 19px;
+ .list-header
+ padding: 0 12px 0px
+ border-bottom: 0px solid #e4e4e4
+ height: 60px
+ margin-top: 10px
+ display: flex
+ align-items: center
.list-header-left-icon
display: inline
padding: 7px
@@ -185,5 +211,100 @@
.list-header-menu-icon
position: absolute
padding: 7px
- top: -@padding
+ top: 50%
+ transform: translateY(-50%)
right: 17px
+ font-size: 20px
+
+.link-board-wrapper
+ display: flex
+ align-items: baseline
+
+ .js-link-board
+ margin-left: 15px
+
+.search-card-results
+ max-height: 250px
+ overflow: hidden
+
+.sk-spinner-list
+ margin-top: unset !important
+
+list-header-color(background, color...)
+ border-bottom: 6px solid background
+
+.list-header-white
+ list-header-color(#ffffff, #4d4d4d) //Black text for better visibility
+ border: 1px solid #eee
+
+.list-header-green
+ list-header-color(#3cb500, #ffffff) //White text for better visibility
+
+.list-header-yellow
+ list-header-color(#fad900, #4d4d4d) //Black text for better visibility
+
+.list-header-orange
+ list-header-color(#ff9f19, #4d4d4d) //Black text for better visibility
+
+.list-header-red
+ list-header-color(#eb4646, #ffffff) //White text for better visibility
+
+.list-header-purple
+ list-header-color(#a632db, #ffffff) //White text for better visibility
+
+.list-header-blue
+ list-header-color(#0079bf, #ffffff) //White text for better visibility
+
+.list-header-pink
+ list-header-color(#ff78cb, #4d4d4d) //Black text for better visibility
+
+.list-header-sky
+ list-header-color(#00c2e0, #ffffff) //White text for better visibility
+
+.list-header-black
+ list-header-color(#4d4d4d, #ffffff) //White text for better visibility
+
+.list-header-lime
+ list-header-color(#51e898, #4d4d4d) //Black text for better visibility
+
+.list-header-silver
+ list-header-color(unset, #4d4d4d) //Black text for better visibility
+
+.list-header-peachpuff
+ list-header-color(#ffdab9, #4d4d4d) //Black text for better visibility
+
+.list-header-crimson
+ list-header-color(#dc143c, #ffffff) //White text for better visibility
+
+.list-header-plum
+ list-header-color(#dda0dd, #4d4d4d) //Black text for better visibility
+
+.list-header-darkgreen
+ list-header-color(#006400, #ffffff) //White text for better visibility
+
+.list-header-slateblue
+ list-header-color(#6a5acd, #ffffff) //White text for better visibility
+
+.list-header-magenta
+ list-header-color(#ff00ff, #ffffff) //White text for better visibility
+
+.list-header-gold
+ list-header-color(#ffd700, #4d4d4d) //Black text for better visibility
+
+.list-header-navy
+ list-header-color(#000080, #ffffff) //White text for better visibility
+
+.list-header-gray
+ list-header-color(#808080, #ffffff) //White text for better visibility
+
+.list-header-saddlebrown
+ list-header-color(#8b4513, #ffffff) //White text for better visibility
+
+.list-header-paleturquoise
+ list-header-color(#afeeee, #4d4d4d) //Black text for better visibility
+
+.list-header-mistyrose
+ list-header-color(#ffe4e1, #4d4d4d) //Black text for better visibility
+
+.list-header-indigo
+ list-header-color(#4b0082, #ffffff) //White text for better visibility
diff --git a/client/components/lists/listBody.jade b/client/components/lists/listBody.jade
index 32c6b278..71ad7f90 100644
--- a/client/components/lists/listBody.jade
+++ b/client/components/lists/listBody.jade
@@ -4,7 +4,7 @@ template(name="listBody")
if cards.count
+inlinedForm(autoclose=false position="top")
+addCardForm(listId=_id position="top")
- each (cards (idOrNull ../../_id))
+ each (cardsWithLimit (idOrNull ../../_id))
a.minicard-wrapper.js-minicard(href=absoluteUrl
class="{{#if cardIsSelected}}is-selected{{/if}}"
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
@@ -12,6 +12,9 @@ template(name="listBody")
.materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
+minicard(this)
+ if (showSpinner (idOrNull ../../_id))
+ +spinnerList
+
if canSeeAddCard
+inlinedForm(autoclose=false position="bottom")
+addCardForm(listId=_id position="bottom")
@@ -20,6 +23,16 @@ template(name="listBody")
i.fa.fa-plus
| {{_ 'add-card'}}
+template(name="spinnerList")
+ .sk-spinner.sk-spinner-wave.sk-spinner-list(
+ class=currentBoard.colorClass
+ id="showMoreResults")
+ .sk-rect1
+ .sk-rect2
+ .sk-rect3
+ .sk-rect4
+ .sk-rect5
+
template(name="addCardForm")
.minicard.minicard-composer.js-composer
if getLabels
@@ -34,8 +47,84 @@ template(name="addCardForm")
.add-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
- a.fa.fa-times-thin.js-close-inlined-form
+ unless currentBoard.isTemplatesBoard
+ unless currentBoard.isTemplateBoard
+ span.quiet
+ | {{_ 'or'}}
+ a.js-link {{_ 'link'}}
+ span.quiet
+ | &nbsp;
+ | /
+ a.js-search {{_ 'search'}}
+ span.quiet
+ | &nbsp;
+ | /
+ a.js-card-template {{_ 'template'}}
template(name="autocompleteLabelLine")
.minicard-label(class="card-label-{{colorName}}" title=labelName)
span(class="{{#if hasNoName}}quiet{{/if}}")= labelName
+
+template(name="linkCardPopup")
+ label {{_ 'boards'}}:
+ .link-board-wrapper
+ select.js-select-boards
+ option(value="")
+ each boards
+ option(value="{{_id}}") {{title}}
+ input.primary.confirm.js-link-board(type="button" value="{{_ 'link'}}")
+
+ label {{_ 'swimlanes'}}:
+ select.js-select-swimlanes
+ each swimlanes
+ option(value="{{_id}}") {{title}}
+
+ label {{_ 'lists'}}:
+ select.js-select-lists
+ each lists
+ option(value="{{_id}}") {{title}}
+
+ label {{_ 'cards'}}:
+ select.js-select-cards
+ each cards
+ option(value="{{getId}}") {{getTitle}}
+
+ .edit-controls.clearfix
+ input.primary.confirm.js-done(type="button" value="{{_ 'link'}}")
+
+template(name="searchElementPopup")
+ form
+ label
+ | {{_ 'title'}}
+ input.js-element-title(type="text" placeholder="{{_ 'title'}}" autofocus required)
+ unless isTemplateSearch
+ label {{_ 'boards'}}:
+ .link-board-wrapper
+ select.js-select-boards
+ option(value="")
+ each boards
+ option(value="{{_id}}") {{title}}
+ form.js-search-term-form
+ input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
+ .list-body.js-perfect-scrollbar.search-card-results
+ .minicards.clearfix.js-minicards
+ if isBoardTemplateSearch
+ each results
+ a.minicard-wrapper.js-minicard
+ +miniboard(this)
+ if isListTemplateSearch
+ each results
+ a.minicard-wrapper.js-minicard
+ +minilist(this)
+ if isSwimlaneTemplateSearch
+ each results
+ a.minicard-wrapper.js-minicard
+ +miniswimlane(this)
+ if isCardTemplateSearch
+ each results
+ a.minicard-wrapper.js-minicard
+ +minicard(this)
+ unless isTemplateSearch
+ each results
+ a.minicard-wrapper.js-minicard
+ +minicard(this)
diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js
index 52f34fab..a5ccba3f 100644
--- a/client/components/lists/listBody.js
+++ b/client/components/lists/listBody.js
@@ -1,4 +1,12 @@
+const subManager = new SubsManager();
+const InfiniteScrollIter = 10;
+
BlazeComponent.extendComponent({
+ onCreated() {
+ // for infinite scrolling
+ this.cardlimit = new ReactiveVar(InfiniteScrollIter);
+ },
+
mixins() {
return [Mixins.PerfectScrollbar];
},
@@ -35,25 +43,59 @@ BlazeComponent.extendComponent({
const members = formComponent.members.get();
const labelIds = formComponent.labels.get();
+ const customFields = formComponent.customFields.get();
- const boardId = this.data().board()._id;
+ const board = this.data().board();
+ let linkedId = '';
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;
-
+ const boardView = (Meteor.user().profile || {}).boardView;
+ let cardType = 'cardType-card';
if (title) {
+ if (board.isTemplatesBoard()) {
+ swimlaneId = this.parentComponent().parentComponent().data()._id; // Always swimlanes view
+ const swimlane = Swimlanes.findOne(swimlaneId);
+ // If this is the card templates swimlane, insert a card template
+ if (swimlane.isCardTemplatesSwimlane())
+ cardType = 'template-card';
+ // If this is the board templates swimlane, insert a board template and a linked card
+ else if (swimlane.isBoardTemplatesSwimlane()) {
+ linkedId = Boards.insert({
+ title,
+ permission: 'private',
+ type: 'template-board',
+ });
+ Swimlanes.insert({
+ title: TAPi18n.__('default'),
+ boardId: linkedId,
+ });
+ cardType = 'cardType-linkedBoard';
+ }
+ } else if (boardView === 'board-view-swimlanes')
+ swimlaneId = this.parentComponent().parentComponent().data()._id;
+ else if ((boardView === 'board-view-lists') || (boardView === 'board-view-cal') || !boardView)
+ swimlaneId = board.getDefaultSwimline()._id;
+
const _id = Cards.insert({
title,
members,
labelIds,
+ customFields,
listId: this.data()._id,
- boardId: this.data().board()._id,
+ boardId: board._id,
sort: sortIndex,
swimlaneId,
+ type: cardType,
+ linkedId,
});
+
+ // if the displayed card count is less than the total cards in the list,
+ // we need to increment the displayed card count to prevent the spinner
+ // to appear
+ const cardCount = this.data().cards(this.idOrNull(swimlaneId)).count();
+ if (this.cardlimit.get() < cardCount) {
+ this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
+ }
+
// 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.
@@ -85,9 +127,9 @@ BlazeComponent.extendComponent({
const methodName = evt.shiftKey ? 'toggleRange' : 'toggle';
MultiSelection[methodName](this.currentData()._id);
- // If the card is already selected, we want to de-select it.
- // XXX We should probably modify the minicard href attribute instead of
- // overwriting the event in case the card is already selected.
+ // If the card is already selected, we want to de-select it.
+ // XXX We should probably modify the minicard href attribute instead of
+ // overwriting the event in case the card is already selected.
} else if (Session.equals('currentCard', this.currentData()._id)) {
evt.stopImmediatePropagation();
evt.preventDefault();
@@ -107,11 +149,31 @@ BlazeComponent.extendComponent({
idOrNull(swimlaneId) {
const currentUser = Meteor.user();
- if (currentUser.profile.boardView === 'board-view-swimlanes')
+ if ((currentUser.profile || {}).boardView === 'board-view-swimlanes' ||
+ this.data().board().isTemplatesBoard())
return swimlaneId;
return undefined;
},
+ cardsWithLimit(swimlaneId) {
+ const limit = this.cardlimit.get();
+ const selector = {
+ listId: this.currentData()._id,
+ archived: false,
+ };
+ if (swimlaneId)
+ selector.swimlaneId = swimlaneId;
+ return Cards.find(Filter.mongoSelector(selector), {
+ sort: ['sort'],
+ limit,
+ });
+ },
+
+ showSpinner(swimlaneId) {
+ const list = Template.currentData();
+ return list.cards(swimlaneId).count() > this.cardlimit.get();
+ },
+
canSeeAddCard() {
return !this.reachedWipLimit() && Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
},
@@ -146,11 +208,21 @@ BlazeComponent.extendComponent({
onCreated() {
this.labels = new ReactiveVar([]);
this.members = new ReactiveVar([]);
+ this.customFields = new ReactiveVar([]);
+
+ const currentBoardId = Session.get('currentBoard');
+ arr = [];
+ _.forEach(Boards.findOne(currentBoardId).customFields().fetch(), function(field){
+ if(field.automaticallyOnCard)
+ arr.push({_id: field._id, value: null});
+ });
+ this.customFields.set(arr);
},
reset() {
this.labels.set([]);
this.members.set([]);
+ this.customFields.set([]);
},
getLabels() {
@@ -162,7 +234,7 @@ BlazeComponent.extendComponent({
pressKey(evt) {
// Pressing Enter should submit the card
- if (evt.keyCode === 13) {
+ if (evt.keyCode === 13 && !evt.shiftKey) {
evt.preventDefault();
const $form = $(evt.currentTarget).closest('form');
// XXX For some reason $form.submit() does not work (it's probably a bug
@@ -171,8 +243,8 @@ BlazeComponent.extendComponent({
// work.
$form.find('button[type=submit]').click();
- // Pressing Tab should open the form of the next column, and Maj+Tab go
- // in the reverse order
+ // Pressing Tab should open the form of the next column, and Maj+Tab go
+ // in the reverse order
} else if (evt.keyCode === 9) {
evt.preventDefault();
const isReverse = evt.shiftKey;
@@ -193,6 +265,9 @@ BlazeComponent.extendComponent({
events() {
return [{
keydown: this.pressKey,
+ 'click .js-link': Popup.open('linkCard'),
+ 'click .js-search': Popup.open('searchElement'),
+ 'click .js-card-template': Popup.open('searchElement'),
}];
},
@@ -230,7 +305,7 @@ BlazeComponent.extendComponent({
const currentBoard = Boards.findOne(Session.get('currentBoard'));
callback($.map(currentBoard.labels, (label) => {
if (label.name.indexOf(term) > -1 ||
- label.color.indexOf(term) > -1) {
+ label.color.indexOf(term) > -1) {
return label;
}
return null;
@@ -264,3 +339,326 @@ BlazeComponent.extendComponent({
});
},
}).register('addCardForm');
+
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.selectedBoardId = new ReactiveVar('');
+ this.selectedSwimlaneId = new ReactiveVar('');
+ this.selectedListId = new ReactiveVar('');
+
+ this.boardId = Session.get('currentBoard');
+ // In order to get current board info
+ subManager.subscribe('board', this.boardId);
+ this.board = Boards.findOne(this.boardId);
+ // List where to insert card
+ const list = $(Popup._getTopStack().openerElement).closest('.js-list');
+ this.listId = Blaze.getData(list[0])._id;
+ // Swimlane where to insert card
+ const swimlane = $(Popup._getTopStack().openerElement).closest('.js-swimlane');
+ this.swimlaneId = '';
+ const boardView = (Meteor.user().profile || {}).boardView;
+ if (boardView === 'board-view-swimlanes')
+ this.swimlaneId = Blaze.getData(swimlane[0])._id;
+ else if (boardView === 'board-view-lists' || !boardView)
+ this.swimlaneId = Swimlanes.findOne({boardId: this.boardId})._id;
+ },
+
+ boards() {
+ const boards = Boards.find({
+ archived: false,
+ 'members.userId': Meteor.userId(),
+ _id: {$ne: Session.get('currentBoard')},
+ type: 'board',
+ }, {
+ sort: ['title'],
+ });
+ return boards;
+ },
+
+ swimlanes() {
+ if (!this.selectedBoardId.get()) {
+ return [];
+ }
+ const swimlanes = Swimlanes.find({boardId: this.selectedBoardId.get()});
+ if (swimlanes.count())
+ this.selectedSwimlaneId.set(swimlanes.fetch()[0]._id);
+ return swimlanes;
+ },
+
+ lists() {
+ if (!this.selectedBoardId.get()) {
+ return [];
+ }
+ const lists = Lists.find({boardId: this.selectedBoardId.get()});
+ if (lists.count())
+ this.selectedListId.set(lists.fetch()[0]._id);
+ return lists;
+ },
+
+ cards() {
+ if (!this.board) {
+ return [];
+ }
+ const ownCardsIds = this.board.cards().map((card) => { return card.linkedId || card._id; });
+ return Cards.find({
+ boardId: this.selectedBoardId.get(),
+ swimlaneId: this.selectedSwimlaneId.get(),
+ listId: this.selectedListId.get(),
+ archived: false,
+ linkedId: {$nin: ownCardsIds},
+ _id: {$nin: ownCardsIds},
+ type: {$nin: ['template-card']},
+ });
+ },
+
+ events() {
+ return [{
+ 'change .js-select-boards'(evt) {
+ subManager.subscribe('board', $(evt.currentTarget).val());
+ this.selectedBoardId.set($(evt.currentTarget).val());
+ },
+ 'change .js-select-swimlanes'(evt) {
+ this.selectedSwimlaneId.set($(evt.currentTarget).val());
+ },
+ 'change .js-select-lists'(evt) {
+ this.selectedListId.set($(evt.currentTarget).val());
+ },
+ 'click .js-done' (evt) {
+ // LINK CARD
+ evt.stopPropagation();
+ evt.preventDefault();
+ const linkedId = $('.js-select-cards option:selected').val();
+ if (!linkedId) {
+ Popup.close();
+ return;
+ }
+ const _id = Cards.insert({
+ title: $('.js-select-cards option:selected').text(), //dummy
+ listId: this.listId,
+ swimlaneId: this.swimlaneId,
+ boardId: this.boardId,
+ sort: Lists.findOne(this.listId).cards().count(),
+ type: 'cardType-linkedCard',
+ linkedId,
+ });
+ Filter.addException(_id);
+ Popup.close();
+ },
+ 'click .js-link-board' (evt) {
+ //LINK BOARD
+ evt.stopPropagation();
+ evt.preventDefault();
+ const impBoardId = $('.js-select-boards option:selected').val();
+ if (!impBoardId || Cards.findOne({linkedId: impBoardId, archived: false})) {
+ Popup.close();
+ return;
+ }
+ const _id = Cards.insert({
+ title: $('.js-select-boards option:selected').text(), //dummy
+ listId: this.listId,
+ swimlaneId: this.swimlaneId,
+ boardId: this.boardId,
+ sort: Lists.findOne(this.listId).cards().count(),
+ type: 'cardType-linkedBoard',
+ linkedId: impBoardId,
+ });
+ Filter.addException(_id);
+ Popup.close();
+ },
+ }];
+ },
+}).register('linkCardPopup');
+
+BlazeComponent.extendComponent({
+ mixins() {
+ return [Mixins.PerfectScrollbar];
+ },
+
+ onCreated() {
+ this.isCardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-card-template');
+ this.isListTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-list-template');
+ this.isSwimlaneTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-open-add-swimlane-menu');
+ this.isBoardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-add-board');
+ this.isTemplateSearch = this.isCardTemplateSearch ||
+ this.isListTemplateSearch ||
+ this.isSwimlaneTemplateSearch ||
+ this.isBoardTemplateSearch;
+ let board = {};
+ if (this.isTemplateSearch) {
+ board = Boards.findOne((Meteor.user().profile || {}).templatesBoardId);
+ } else {
+ // Prefetch first non-current board id
+ board = Boards.findOne({
+ archived: false,
+ 'members.userId': Meteor.userId(),
+ _id: {$nin: [Session.get('currentBoard'), (Meteor.user().profile || {}).templatesBoardId]},
+ });
+ }
+ if (!board) {
+ Popup.close();
+ return;
+ }
+ const boardId = board._id;
+ // Subscribe to this board
+ subManager.subscribe('board', boardId);
+ this.selectedBoardId = new ReactiveVar(boardId);
+
+ if (!this.isBoardTemplateSearch) {
+ this.boardId = Session.get('currentBoard');
+ // In order to get current board info
+ subManager.subscribe('board', this.boardId);
+ this.swimlaneId = '';
+ // Swimlane where to insert card
+ const swimlane = $(Popup._getTopStack().openerElement).parents('.js-swimlane');
+ if ((Meteor.user().profile || {}).boardView === 'board-view-swimlanes')
+ this.swimlaneId = Blaze.getData(swimlane[0])._id;
+ else
+ this.swimlaneId = Swimlanes.findOne({boardId: this.boardId})._id;
+ // List where to insert card
+ const list = $(Popup._getTopStack().openerElement).closest('.js-list');
+ this.listId = Blaze.getData(list[0])._id;
+ }
+ this.term = new ReactiveVar('');
+ },
+
+ boards() {
+ const boards = Boards.find({
+ archived: false,
+ 'members.userId': Meteor.userId(),
+ _id: {$ne: Session.get('currentBoard')},
+ type: 'board',
+ }, {
+ sort: ['title'],
+ });
+ return boards;
+ },
+
+ results() {
+ if (!this.selectedBoardId) {
+ return [];
+ }
+ const board = Boards.findOne(this.selectedBoardId.get());
+ if (!this.isTemplateSearch || this.isCardTemplateSearch) {
+ return board.searchCards(this.term.get(), false);
+ } else if (this.isListTemplateSearch) {
+ return board.searchLists(this.term.get());
+ } else if (this.isSwimlaneTemplateSearch) {
+ return board.searchSwimlanes(this.term.get());
+ } else if (this.isBoardTemplateSearch) {
+ const boards = board.searchBoards(this.term.get());
+ boards.forEach((board) => {
+ subManager.subscribe('board', board.linkedId);
+ });
+ return boards;
+ } else {
+ return [];
+ }
+ },
+
+ events() {
+ return [{
+ 'change .js-select-boards'(evt) {
+ subManager.subscribe('board', $(evt.currentTarget).val());
+ this.selectedBoardId.set($(evt.currentTarget).val());
+ },
+ 'submit .js-search-term-form'(evt) {
+ evt.preventDefault();
+ this.term.set(evt.target.searchTerm.value);
+ },
+ 'click .js-minicard'(evt) {
+ // 0. Common
+ const title = $('.js-element-title').val().trim();
+ if (!title)
+ return;
+ const element = Blaze.getData(evt.currentTarget);
+ element.title = title;
+ let _id = '';
+ if (!this.isTemplateSearch || this.isCardTemplateSearch) {
+ // Card insertion
+ // 1. Common
+ element.sort = Lists.findOne(this.listId).cards().count();
+ // 1.A From template
+ if (this.isTemplateSearch) {
+ element.type = 'cardType-card';
+ element.linkedId = '';
+ _id = element.copy(this.boardId, this.swimlaneId, this.listId);
+ // 1.B Linked card
+ } else {
+ delete element._id;
+ element.type = 'cardType-linkedCard';
+ element.linkedId = element.linkedId || element._id;
+ _id = Cards.insert(element);
+ }
+ Filter.addException(_id);
+ // List insertion
+ } else if (this.isListTemplateSearch) {
+ element.sort = Swimlanes.findOne(this.swimlaneId).lists().count();
+ element.type = 'list';
+ _id = element.copy(this.boardId, this.swimlaneId);
+ } else if (this.isSwimlaneTemplateSearch) {
+ element.sort = Boards.findOne(this.boardId).swimlanes().count();
+ element.type = 'swimlalne';
+ _id = element.copy(this.boardId);
+ } else if (this.isBoardTemplateSearch) {
+ board = Boards.findOne(element.linkedId);
+ board.sort = Boards.find({archived: false}).count();
+ board.type = 'board';
+ board.title = element.title;
+ delete board.slug;
+ _id = board.copy();
+ }
+ Popup.close();
+ },
+ }];
+ },
+}).register('searchElementPopup');
+
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.cardlimit = this.parentComponent().cardlimit;
+
+ this.listId = this.parentComponent().data()._id;
+ this.swimlaneId = '';
+
+ const boardView = (Meteor.user().profile || {}).boardView;
+ if (boardView === 'board-view-swimlanes')
+ this.swimlaneId = this.parentComponent().parentComponent().parentComponent().data()._id;
+ },
+
+ onRendered() {
+ this.spinner = this.find('.sk-spinner-list');
+ this.container = this.$(this.spinner).parents('.js-perfect-scrollbar')[0];
+
+ $(this.container).on(`scroll.spinner_${this.swimlaneId}_${this.listId}`, () => this.updateList());
+ $(window).on(`resize.spinner_${this.swimlaneId}_${this.listId}`, () => this.updateList());
+
+ this.updateList();
+ },
+
+ onDestroyed() {
+ $(this.container).off(`scroll.spinner_${this.swimlaneId}_${this.listId}`);
+ $(window).off(`resize.spinner_${this.swimlaneId}_${this.listId}`);
+ },
+
+ updateList() {
+ if (this.spinnerInView()) {
+ this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
+ window.requestIdleCallback(() => this.updateList());
+ }
+ },
+
+ spinnerInView() {
+ const parentViewHeight = this.container.clientHeight;
+ const bottomViewPosition = this.container.scrollTop + parentViewHeight;
+
+ const threshold = this.spinner.offsetTop;
+
+ // spinner deleted
+ if (!this.spinner.offsetTop) {
+ return false;
+ }
+
+ return bottomViewPosition > threshold;
+ },
+
+}).register('spinnerList');
diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade
index 61771449..aa6d3786 100644
--- a/client/components/lists/listHeader.jade
+++ b/client/components/lists/listHeader.jade
@@ -1,5 +1,7 @@
template(name="listHeader")
- .list-header.js-list-header
+ .list-header.js-list-header(
+ class="{{#if limitToShowCardsCount}}list-header-card-count{{/if}}"
+ class="{{#if colorClass}}list-header-{{colorClass}}{{/if}}")
+inlinedForm
+editListTitleForm
else
@@ -15,9 +17,8 @@ template(name="listHeader")
|/#{wipLimit.value})
if showCardsCountForList cards.count
- = cards.count
- span
- | {{_ 'cards-count'}}
+ |&nbsp;
+ p.quiet.small {{cardsCount}} {{_ 'cards-count'}}
if isMiniScreen
if currentList
if isWatching
@@ -51,6 +52,9 @@ template(name="listActionPopup")
unless currentUser.isCommentOnly
hr
ul.pop-over-list
+ li: a.js-set-color-list {{_ 'set-color-list'}}
+ hr
+ ul.pop-over-list
if cards.count
li: a.js-select-cards {{_ 'list-select-cards'}}
hr
@@ -112,3 +116,13 @@ template(name="wipLimitErrorPopup")
p {{_ 'wipLimitErrorPopup-dialog-pt1'}}
p {{_ 'wipLimitErrorPopup-dialog-pt2'}}
button.full.js-back-view(type="submit") {{_ 'cancel'}}
+
+template(name="setListColorPopup")
+ form.edit-label
+ .palette-colors: each colors
+ // note: we use the swimlane palette to have more than just the border
+ span.card-label.palette-color.js-palette-color(class="swimlane-{{color}}")
+ if(isSelected color)
+ i.fa.fa-check
+ button.primary.confirm.js-submit {{_ 'save'}}
+ button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js
index 4b6bf196..923d6063 100644
--- a/client/components/lists/listHeader.js
+++ b/client/components/lists/listHeader.js
@@ -1,3 +1,8 @@
+let listsColors;
+Meteor.startup(() => {
+ listsColors = Lists.simpleSchema()._schema.color.allowedValues;
+});
+
BlazeComponent.extendComponent({
canSeeAddCard() {
const list = Template.currentData();
@@ -22,6 +27,16 @@ BlazeComponent.extendComponent({
return Meteor.user().getLimitToShowCardsCount();
},
+ cardsCount() {
+ const list = Template.currentData();
+ let swimlaneId = '';
+ const boardView = (Meteor.user().profile || {}).boardView;
+ if (boardView === 'board-view-swimlanes')
+ swimlaneId = this.parentComponent().parentComponent().data()._id;
+
+ return list.cards(swimlaneId).count();
+ },
+
reachedWipLimit() {
const list = Template.currentData();
return list.getWipLimit('enabled') && list.getWipLimit('value') <= list.cards().count();
@@ -62,6 +77,7 @@ Template.listActionPopup.helpers({
Template.listActionPopup.events({
'click .js-list-subscribe' () {},
+ 'click .js-set-color-list': Popup.open('setListColor'),
'click .js-select-cards' () {
const cardIds = this.allCards().map((card) => card._id);
MultiSelection.add(cardIds);
@@ -144,3 +160,34 @@ Template.listMorePopup.events({
Utils.goBoardId(this.boardId);
}),
});
+
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.currentList = this.currentData();
+ this.currentColor = new ReactiveVar(this.currentList.color);
+ },
+
+ colors() {
+ return listsColors.map((color) => ({ color, name: '' }));
+ },
+
+ isSelected(color) {
+ return this.currentColor.get() === color;
+ },
+
+ events() {
+ return [{
+ 'click .js-palette-color'() {
+ this.currentColor.set(this.currentData().color);
+ },
+ 'click .js-submit' () {
+ this.currentList.setColor(this.currentColor.get());
+ Popup.close();
+ },
+ 'click .js-remove-color'() {
+ this.currentList.setColor(null);
+ Popup.close();
+ },
+ }];
+ },
+}).register('setListColorPopup');
diff --git a/client/components/lists/minilist.jade b/client/components/lists/minilist.jade
new file mode 100644
index 00000000..e34214c4
--- /dev/null
+++ b/client/components/lists/minilist.jade
@@ -0,0 +1,8 @@
+template(name="minilist")
+ .minicard(
+ class="minicard-{{colorClass}}")
+ .minicard-title
+ .handle
+ .fa.fa-arrows
+ +viewer
+ = title
diff --git a/client/components/main/editor.jade b/client/components/main/editor.jade
index 31f533e6..dbd61715 100644
--- a/client/components/main/editor.jade
+++ b/client/components/main/editor.jade
@@ -1,5 +1,6 @@
template(name="editor")
textarea.editor(
+ dir="auto"
class="{{class}}"
id=id
autofocus=autofocus
@@ -7,7 +8,7 @@ template(name="editor")
+Template.contentBlock
template(name="viewer")
- .viewer
+ .viewer(dir="auto")
+mentions
+markdown
{{> UI.contentBlock }}
diff --git a/client/components/main/editor.js b/client/components/main/editor.js
index 888fbe00..88d8abf0 100755
--- a/client/components/main/editor.js
+++ b/client/components/main/editor.js
@@ -36,13 +36,18 @@ import sanitizeXss from 'xss';
const at = HTML.CharRef({html: '&commat;', str: '@'});
Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
const view = this;
+ let content = Blaze.toHTML(view.templateContentBlock);
const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ if (!currentBoard)
+ return HTML.Raw(sanitizeXss(content));
const knowedUsers = currentBoard.members.map((member) => {
- member.username = Users.findOne(member.userId).username;
+ const u = Users.findOne(member.userId);
+ if(u){
+ member.username = u.username;
+ }
return member;
});
const mentionRegex = /\B@([\w.]*)/gi;
- let content = Blaze.toHTML(view.templateContentBlock);
let currentMention;
while ((currentMention = mentionRegex.exec(content)) !== null) {
diff --git a/client/components/main/header.jade b/client/components/main/header.jade
index dd071b3e..75e84c0c 100644
--- a/client/components/main/header.jade
+++ b/client/components/main/header.jade
@@ -4,39 +4,38 @@ template(name="header")
list all starred boards with a link to go there. This is inspired by the
Reddit "subreddit" bar.
The first link goes to the boards page.
- unless isSandstorm
- if currentUser
- #header-quick-access(class=currentBoard.colorClass)
- if isMiniScreen
- ul
- li
- a(href="{{pathFor 'home'}}")
- span.fa.fa-home
+ if currentUser
+ #header-quick-access(class=currentBoard.colorClass)
+ 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}}")
+ if currentList
+ each currentBoard.lists
+ li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}")
+ a.js-select-list
= title
- else
- li.current {{_ 'quick-access-description'}}
+ #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")
+ a#header-new-board-icon.js-create-board
+ i.fa.fa-plus(title="Create a new board")
- +headerUserBar
+ +headerUserBar
#header(class=currentBoard.colorClass)
//-
@@ -46,17 +45,16 @@ template(name="header")
#header-main-bar(class="{{#if wrappedHeader}}wrapper{{/if}}")
+Template.dynamic(template=headerBar)
- unless hideLogo
+ //unless hideLogo
+
//-
On sandstorm, the logo shouldn't be clickable, because we only have one
page/document on it, and we don't want to see the home page containing
the list of all boards.
- if isSandstorm
- .wekan-logo
- img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan")
- else
- a.wekan-logo(href="{{pathFor 'home'}}" title="{{_ 'header-logo-title'}}")
- img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan")
+
+ // unless currentSetting.hideLogo
+ // a.wekan-logo(href="{{pathFor 'home'}}" title="{{_ 'header-logo-title'}}")
+ // img(src="{{pathFor '/logo-header.png'}}" alt="")
if appIsOffline
+offlineWarning
@@ -66,7 +64,8 @@ template(name="header")
.announcement
p
i.fa.fa-bullhorn
- | #{announcement}
+ +viewer
+ | #{announcement}
i.fa.fa-times-circle.js-close-announcement
template(name="offlineWarning")
diff --git a/client/components/main/header.js b/client/components/main/header.js
index 7fbc5716..c05b1c3c 100644
--- a/client/components/main/header.js
+++ b/client/components/main/header.js
@@ -1,11 +1,16 @@
Meteor.subscribe('user-admin');
Meteor.subscribe('boards');
+Meteor.subscribe('setting');
Template.header.helpers({
wrappedHeader() {
return !Session.get('currentBoard');
},
+ currentSetting() {
+ return Settings.findOne();
+ },
+
hideLogo() {
return Utils.isMiniScreen() && Session.get('currentBoard');
},
diff --git a/client/components/main/header.styl b/client/components/main/header.styl
index f9455f8e..e3c7618d 100644
--- a/client/components/main/header.styl
+++ b/client/components/main/header.styl
@@ -188,8 +188,6 @@
width: 100%
padding: 10px 0px
z-index: 30
- position: absolute
- bottom: 0px
ul
width: calc(100% - 60px)
@@ -218,7 +216,7 @@
position: absolute
right: 0px
padding: 10px
- margin: -10px
+ margin: -10px 0 -10px -10px
.announcement,
.offline-warning
diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade
index 4d76aabb..9543c5c5 100644
--- a/client/components/main/layouts.jade
+++ b/client/components/main/layouts.jade
@@ -1,7 +1,6 @@
head
- title Wekan
- meta(name="viewport"
- content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0")
+ title
+ meta(name="viewport" content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0")
meta(http-equiv="X-UA-Compatible" content="IE=edge")
//- XXX We should use pathFor in the following `href` to support the case
where the application is deployed with a path prefix, but it seems to be
@@ -9,34 +8,47 @@ head
packages.
link(rel="shortcut icon" href="/wekan-favicon.png")
link(rel="apple-touch-icon" href="/wekan-favicon.png")
+ link(rel="mask-icon" href="/wekan-logo-150.svg")
link(rel="manifest" href="/wekan-manifest.json")
template(name="userFormsLayout")
section.auth-layout
- h1.at-form-landing-logo
- img(src="{{pathFor '/wekan-logo.png'}}" alt="Wekan")
section.auth-dialog
- +Template.dynamic(template=content)
- div.at-form-lang
- select.select-lang.js-userform-set-language
- each languages
- if isCurrentLanguage
- option(value="{{tag}}" selected="selected") {{name}}
- else
- option(value="{{tag}}") {{name}}
+ if isLoading
+ +loader
+ else
+ +Template.dynamic(template=content)
+ if currentSetting.displayAuthenticationMethod
+ +connectionMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod)
+ div.at-form-lang
+ select.select-lang.js-userform-set-language
+ each languages
+ if isCurrentLanguage
+ option(value="{{tag}}" selected="selected") {{name}}
+ else
+ option(value="{{tag}}") {{name}}
template(name="defaultLayout")
+header
#content
+ | {{{afterBodyStart}}}
+Template.dynamic(template=content)
+ | {{{beforeBodyEnd}}}
if (Modal.isOpen)
#modal
.overlay
- .modal-content
- a.modal-close-btn.js-close-modal
- i.fa.fa-times-thin
- +Template.dynamic(template=Modal.getHeaderName)
- +Template.dynamic(template=Modal.getTemplateName)
+ if (Modal.isWide)
+ .modal-content-wide.modal-container
+ a.modal-close-btn.js-close-modal
+ i.fa.fa-times-thin
+ +Template.dynamic(template=Modal.getHeaderName)
+ +Template.dynamic(template=Modal.getTemplateName)
+ else
+ .modal-content.modal-container
+ a.modal-close-btn.js-close-modal
+ i.fa.fa-times-thin
+ +Template.dynamic(template=Modal.getHeaderName)
+ +Template.dynamic(template=Modal.getTemplateName)
template(name="notFound")
+message(label='page-not-found')
@@ -47,3 +59,14 @@ template(name="message")
unless currentUser
with(pathFor route='atSignIn')
p {{{_ 'page-maybe-private' this}}}
+
+template(name="loader")
+ h1.loadingText {{_ 'loading'}}
+ .lds-roller
+ div
+ div
+ div
+ div
+ div
+ div
+ div
diff --git a/client/components/main/layouts.js b/client/components/main/layouts.js
index f12718a7..d5113a25 100644
--- a/client/components/main/layouts.js
+++ b/client/components/main/layouts.js
@@ -6,7 +6,36 @@ const i18nTagToT9n = (i18nTag) => {
return i18nTag;
};
+const validator = {
+ set(obj, prop, value) {
+ if (prop === 'state' && value !== 'signIn') {
+ $('.at-form-authentication').hide();
+ } else if (prop === 'state' && value === 'signIn') {
+ $('.at-form-authentication').show();
+ }
+ // The default behavior to store the value
+ obj[prop] = value;
+ // Indicate success
+ return true;
+ },
+};
+
+Template.userFormsLayout.onCreated(function() {
+ const instance = this;
+ instance.currentSetting = new ReactiveVar();
+ instance.isLoading = new ReactiveVar(false);
+
+ Meteor.subscribe('setting', {
+ onReady() {
+ instance.currentSetting.set(Settings.findOne());
+ return this.stop();
+ },
+ });
+});
+
Template.userFormsLayout.onRendered(() => {
+ AccountsTemplates.state.form.keys = new Proxy(AccountsTemplates.state.form.keys, validator);
+
const i18nTag = navigator.language;
if (i18nTag) {
T9n.setLanguage(i18nTagToT9n(i18nTag));
@@ -15,6 +44,22 @@ Template.userFormsLayout.onRendered(() => {
});
Template.userFormsLayout.helpers({
+ currentSetting() {
+ return Template.instance().currentSetting.get();
+ },
+
+ isLoading() {
+ return Template.instance().isLoading.get();
+ },
+
+ afterBodyStart() {
+ return currentSetting.customHTMLafterBodyStart;
+ },
+
+ beforeBodyEnd() {
+ return currentSetting.customHTMLbeforeBodyEnd;
+ },
+
languages() {
return _.map(TAPi18n.getLanguages(), (lang, code) => {
const tag = code;
@@ -47,6 +92,15 @@ Template.userFormsLayout.events({
T9n.setLanguage(i18nTagToT9n(i18nTag));
evt.preventDefault();
},
+ 'click #at-btn'(event, instance) {
+ if (FlowRouter.getRouteName() === 'atSignIn') {
+ instance.isLoading.set(true);
+ authentication(event, instance)
+ .then(() => {
+ instance.isLoading.set(false);
+ });
+ }
+ },
});
Template.defaultLayout.events({
@@ -54,3 +108,64 @@ Template.defaultLayout.events({
Modal.close();
},
});
+
+async function authentication(event, instance) {
+ const match = $('#at-field-username_and_email').val();
+ const password = $('#at-field-password').val();
+
+ if (!match || !password) return undefined;
+
+ const result = await getAuthenticationMethod(instance.currentSetting.get(), match);
+
+ if (result === 'password') return undefined;
+
+ // Stop submit #at-pwd-form
+ event.preventDefault();
+ event.stopImmediatePropagation();
+
+ switch (result) {
+ case 'ldap':
+ return new Promise((resolve) => {
+ Meteor.loginWithLDAP(match, password, function() {
+ resolve(FlowRouter.go('/'));
+ });
+ });
+
+ case 'cas':
+ return new Promise((resolve) => {
+ Meteor.loginWithCas(match, password, function() {
+ resolve(FlowRouter.go('/'));
+ });
+ });
+
+ default:
+ return undefined;
+ }
+}
+
+function getAuthenticationMethod({displayAuthenticationMethod, defaultAuthenticationMethod}, match) {
+ if (displayAuthenticationMethod) {
+ return $('.select-authentication').val();
+ }
+ return getUserAuthenticationMethod(defaultAuthenticationMethod, match);
+}
+
+function getUserAuthenticationMethod(defaultAuthenticationMethod, match) {
+ return new Promise((resolve) => {
+ try {
+ Meteor.subscribe('user-authenticationMethod', match, {
+ onReady() {
+ const user = Users.findOne();
+
+ const authenticationMethod = user
+ ? user.authenticationMethod
+ : defaultAuthenticationMethod;
+
+ resolve(authenticationMethod);
+ },
+ });
+ } catch(error) {
+ resolve(defaultAuthenticationMethod);
+ }
+ });
+}
diff --git a/client/components/main/layouts.styl b/client/components/main/layouts.styl
index a79ff337..46ee720c 100644
--- a/client/components/main/layouts.styl
+++ b/client/components/main/layouts.styl
@@ -62,6 +62,23 @@ body
float: right
font-size: 24px
+ .modal-content-wide
+ width: 800px
+ min-height: 0px
+ margin: 42px auto
+ padding: 12px
+ border-radius: 4px
+ background: darken(white, 13%)
+ z-index: 110
+
+ h2
+ margin-bottom: 25px
+
+ .modal-close-btn
+ display: block
+ float: right
+ font-size: 24px
+
h1
font-size: 22px
line-height: 1.2em
@@ -273,7 +290,7 @@ kbd
// Implement a thiner close icon as suggested in
// https://github.com/FortAwesome/Font-Awesome/issues/1540#issuecomment-68689950
.fa.fa-times-thin:before
- content: '\00d7';
+ content: '\00d7'
.fa.fa-globe.colorful, .fa.fa-bell.colorful
color: #4caf50
@@ -368,8 +385,8 @@ a
@media screen and (max-width: 800px)
#content
- margin: 1px 0px 49px 0px
- height: calc(100% - 96px)
+ margin: 1px 0px 0px 0px
+ height: calc(100% - 0px)
> .wrapper
margin-top: 0px
@@ -382,3 +399,103 @@ a
height: 37px
margin: 8px 10px 0 0
width: 50px
+
+.select-authentication
+ width: 100%
+
+.auth-layout
+ display: flex
+ flex-direction: column
+ align-items: center
+ justify-content: center
+ height: 100%
+
+ .auth-dialog
+ margin: 0 !important
+
+.loadingText
+ text-align: center
+
+.lds-roller
+ display: block
+ margin: auto
+ position: relative
+ width: 64px
+ height: 64px
+
+ div
+ animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite
+ transform-origin: 32px 32px
+
+ div:after
+ content: " "
+ display: block
+ position: absolute
+ width: 6px
+ height: 6px
+ border-radius: 50%
+ background: #dedede
+ margin: -3px 0 0 -3px
+
+ div:nth-child(1)
+ animation-delay: -0.036s
+
+ div:nth-child(1):after
+ top: 50px
+ left: 50px
+
+ div:nth-child(2)
+ animation-delay: -0.072s
+
+ div:nth-child(2):after
+ top: 54px
+ left: 45px
+
+ div:nth-child(3)
+ animation-delay: -0.108s
+
+ div:nth-child(3):after
+ top: 57px
+ left: 39px
+
+ div:nth-child(4)
+ animation-delay: -0.144s
+
+ div:nth-child(4):after
+ top: 58px
+ left: 32px
+
+ div:nth-child(5)
+ animation-delay: -0.18s
+
+ div:nth-child(5):after
+ top: 57px
+ left: 25px
+
+ div:nth-child(6)
+ animation-delay: -0.216s
+
+ div:nth-child(6):after
+ top: 54px
+ left: 19px
+
+ div:nth-child(7)
+ animation-delay: -0.252s
+
+ div:nth-child(7):after
+ top: 50px
+ left: 14px
+
+ div:nth-child(8)
+ animation-delay: -0.288s
+
+ div:nth-child(8):after
+ top: 45px
+ left: 10px
+
+@keyframes lds-roller
+ 0%
+ transform: rotate(0deg)
+
+ 100%
+ transform: rotate(360deg)
diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl
index b7c9e264..ff00eef3 100644
--- a/client/components/main/popup.styl
+++ b/client/components/main/popup.styl
@@ -33,6 +33,9 @@ $popupWidth = 300px
textarea
height: 72px
+ form a span
+ padding: 0 0.5rem
+
.header
height: 36px
position: relative
diff --git a/client/components/mixins/perfectScrollbar.js b/client/components/mixins/perfectScrollbar.js
index f652f043..12f8a892 100644
--- a/client/components/mixins/perfectScrollbar.js
+++ b/client/components/mixins/perfectScrollbar.js
@@ -1,12 +1,16 @@
+const { isTouchDevice } = Utils;
+
Mixins.PerfectScrollbar = BlazeComponent.extendComponent({
onRendered() {
- const component = this.mixinParent();
- const domElement = component.find('.js-perfect-scrollbar');
- Ps.initialize(domElement);
+ if (!isTouchDevice()) {
+ const component = this.mixinParent();
+ const domElement = component.find('.js-perfect-scrollbar');
+ Ps.initialize(domElement);
- // XXX We should create an event map to be consistent with other components
- // but since BlazeComponent doesn't merge Mixins events transparently I
- // prefered to use a jQuery event (which is what an event map ends up doing)
- component.$(domElement).on('mouseenter', () => Ps.update(domElement));
+ // XXX We should create an event map to be consistent with other components
+ // but since BlazeComponent doesn't merge Mixins events transparently I
+ // prefered to use a jQuery event (which is what an event map ends up doing)
+ component.$(domElement).on('mouseenter', () => Ps.update(domElement));
+ }
},
});
diff --git a/client/components/rules/.DS_Store b/client/components/rules/.DS_Store
new file mode 100644
index 00000000..5008ddfc
--- /dev/null
+++ b/client/components/rules/.DS_Store
Binary files differ
diff --git a/client/components/rules/actions/boardActions.jade b/client/components/rules/actions/boardActions.jade
new file mode 100644
index 00000000..6034184c
--- /dev/null
+++ b/client/components/rules/actions/boardActions.jade
@@ -0,0 +1,72 @@
+template(name="boardActions")
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-move-card-to'}}
+ div.trigger-dropdown
+ select(id="move-gen-action")
+ option(value="top") {{_'r-top-of'}}
+ option(value="bottom") {{_'r-bottom-of'}}
+ div.trigger-text
+ | {{_'r-its-list'}}
+ div.trigger-button.js-add-gen-move-action.js-goto-rules
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-move-card-to'}}
+ div.trigger-dropdown
+ select(id="move-spec-action")
+ option(value="top") {{_'r-top-of'}}
+ option(value="bottom") {{_'r-bottom-of'}}
+ div.trigger-text
+ | {{_'r-list'}}
+ div.trigger-dropdown
+ input(id="listName",type=text,placeholder="{{_'r-name'}}")
+ div.trigger-button.js-add-spec-move-action.js-goto-rules
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-dropdown
+ select(id="arch-action")
+ option(value="archive") {{_'r-archive'}}
+ option(value="unarchive") {{_'r-unarchive'}}
+ div.trigger-text
+ | {{_'r-card'}}
+ div.trigger-button.js-add-arch-action.js-goto-rules
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-add-swimlane'}}
+ div.trigger-dropdown
+ input(id="swimlane-name",type=text,placeholder="{{_'r-name'}}")
+ div.trigger-button.js-add-swimlane-action.js-goto-rules
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-create-card'}}
+ div.trigger-dropdown
+ input(id="card-name",type=text,placeholder="{{_'r-name'}}")
+ div.trigger-text
+ | {{_'r-in-list'}}
+ div.trigger-dropdown
+ input(id="list-name",type=text,placeholder="{{_'r-name'}}")
+ div.trigger-text
+ | {{_'r-in-swimlane'}}
+ div.trigger-dropdown
+ input(id="swimlane-name2",type=text,placeholder="{{_'r-name'}}")
+ div.trigger-button.js-create-card-action.js-goto-rules
+ i.fa.fa-plus
+
+
+
+
+
+
+
diff --git a/client/components/rules/actions/boardActions.js b/client/components/rules/actions/boardActions.js
new file mode 100644
index 00000000..e0b8edc9
--- /dev/null
+++ b/client/components/rules/actions/boardActions.js
@@ -0,0 +1,168 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+
+ },
+
+ events() {
+ return [{
+ 'click .js-create-card-action' (event) {
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const cardName = this.find('#card-name').value;
+ const listName = this.find('#list-name').value;
+ const swimlaneName = this.find('#swimlane-name2').value;
+ const boardId = Session.get('currentBoard');
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'createCard',
+ swimlaneName,
+ cardName,
+ listName,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+
+ },
+ 'click .js-add-swimlane-action' (event) {
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const swimlaneName = this.find('#swimlane-name').value;
+ const boardId = Session.get('currentBoard');
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'addSwimlane',
+ swimlaneName,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+
+ },
+ 'click .js-add-spec-move-action' (event) {
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const actionSelected = this.find('#move-spec-action').value;
+ const listTitle = this.find('#listName').value;
+ const boardId = Session.get('currentBoard');
+ const desc = Utils.getTriggerActionDesc(event, this);
+ if (actionSelected === 'top') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'moveCardToTop',
+ listTitle,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ if (actionSelected === 'bottom') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'moveCardToBottom',
+ listTitle,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ },
+ 'click .js-add-gen-move-action' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const boardId = Session.get('currentBoard');
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const actionSelected = this.find('#move-gen-action').value;
+ if (actionSelected === 'top') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'moveCardToTop',
+ 'listTitle': '*',
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ if (actionSelected === 'bottom') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'moveCardToBottom',
+ 'listTitle': '*',
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ },
+ 'click .js-add-arch-action' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const boardId = Session.get('currentBoard');
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const actionSelected = this.find('#arch-action').value;
+ if (actionSelected === 'archive') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'archive',
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ if (actionSelected === 'unarchive') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'unarchive',
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ },
+ }];
+ },
+
+}).register('boardActions');
+/* eslint-no-undef */
diff --git a/client/components/rules/actions/cardActions.jade b/client/components/rules/actions/cardActions.jade
new file mode 100644
index 00000000..8c6defc6
--- /dev/null
+++ b/client/components/rules/actions/cardActions.jade
@@ -0,0 +1,55 @@
+template(name="cardActions")
+ div.trigger-item
+ div.trigger-content
+ div.trigger-dropdown
+ select(id="label-action")
+ option(value="add") {{{_'r-add'}}}
+ option(value="remove") {{{_'r-remove'}}}
+ div.trigger-text
+ | {{{_'r-label'}}}
+ div.trigger-dropdown
+ select(id="label-id")
+ each labels
+ option(value="#{_id}")
+ = name
+ div.trigger-button.js-add-label-action.js-goto-rules
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-dropdown
+ select(id="member-action")
+ option(value="add") {{{_'r-add'}}}
+ option(value="remove") {{{_'r-remove'}}}
+ div.trigger-text
+ | {{{_'r-member'}}}
+ div.trigger-dropdown
+ input(id="member-name",type=text,placeholder="{{{_'r-name'}}}")
+ div.trigger-button.js-add-member-action.js-goto-rules
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{{_'r-remove-all'}}}
+ div.trigger-button.js-add-removeall-action.js-goto-rules
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{{_'r-set-color'}}}
+ button.trigger-button.trigger-button-color.js-show-color-palette(
+ id="color-action"
+ class="card-details-{{cardColorButton}}")
+ | {{{_ cardColorButtonText }}}
+ div.trigger-button.js-set-color-action.js-goto-rules
+ i.fa.fa-plus
+
+template(name="setCardActionsColorPopup")
+ form.edit-label
+ .palette-colors: each colors
+ span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
+ if(isSelected color)
+ i.fa.fa-check
+ button.primary.confirm.js-submit {{_ 'save'}}
diff --git a/client/components/rules/actions/cardActions.js b/client/components/rules/actions/cardActions.js
new file mode 100644
index 00000000..a1e43c38
--- /dev/null
+++ b/client/components/rules/actions/cardActions.js
@@ -0,0 +1,188 @@
+let cardColors;
+Meteor.startup(() => {
+ cardColors = Cards.simpleSchema()._schema.color.allowedValues;
+});
+
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.subscribe('allRules');
+ this.cardColorButtonValue = new ReactiveVar('green');
+ },
+
+ cardColorButton() {
+ return this.cardColorButtonValue.get();
+ },
+
+ cardColorButtonText() {
+ return `color-${ this.cardColorButtonValue.get() }`;
+ },
+
+ labels() {
+ const labels = Boards.findOne(Session.get('currentBoard')).labels;
+ for (let i = 0; i < labels.length; i++) {
+ if (labels[i].name === '' || labels[i].name === undefined) {
+ labels[i].name = labels[i].color.toUpperCase();
+ }
+ }
+ return labels;
+ },
+
+ events() {
+ return [{
+ 'click .js-add-label-action' (event) {
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const actionSelected = this.find('#label-action').value;
+ const labelId = this.find('#label-id').value;
+ const boardId = Session.get('currentBoard');
+ const desc = Utils.getTriggerActionDesc(event, this);
+ if (actionSelected === 'add') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'addLabel',
+ labelId,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ if (actionSelected === 'remove') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'removeLabel',
+ labelId,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+
+ },
+ 'click .js-add-member-action' (event) {
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const actionSelected = this.find('#member-action').value;
+ const username = this.find('#member-name').value;
+ const boardId = Session.get('currentBoard');
+ const desc = Utils.getTriggerActionDesc(event, this);
+ if (actionSelected === 'add') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'addMember',
+ username,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ desc,
+ });
+ }
+ if (actionSelected === 'remove') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'removeMember',
+ username,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ },
+ 'click .js-add-removeall-action' (event) {
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const triggerId = Triggers.insert(trigger);
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const boardId = Session.get('currentBoard');
+ const actionId = Actions.insert({
+ actionType: 'removeMember',
+ 'username': '*',
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ },
+ 'click .js-show-color-palette'(event){
+ const funct = Popup.open('setCardActionsColor');
+ const colorButton = this.find('#color-action');
+ if (colorButton.value === '') {
+ colorButton.value = 'green';
+ }
+ funct.call(this, event);
+ },
+ 'click .js-set-color-action' (event) {
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const selectedColor = this.cardColorButtonValue.get();
+ const boardId = Session.get('currentBoard');
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'setColor',
+ selectedColor,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ },
+ }];
+ },
+
+}).register('cardActions');
+
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.currentAction = this.currentData();
+ this.colorButtonValue = Popup.getOpenerComponent().cardColorButtonValue;
+ this.currentColor = new ReactiveVar(this.colorButtonValue.get());
+ },
+
+ colors() {
+ return cardColors.map((color) => ({ color, name: '' }));
+ },
+
+ isSelected(color) {
+ return this.currentColor.get() === color;
+ },
+
+ events() {
+ return [{
+ 'click .js-palette-color'() {
+ this.currentColor.set(this.currentData().color);
+ },
+ 'click .js-submit' () {
+ this.colorButtonValue.set(this.currentColor.get());
+ Popup.close();
+ },
+ }];
+ },
+}).register('setCardActionsColorPopup');
diff --git a/client/components/rules/actions/checklistActions.jade b/client/components/rules/actions/checklistActions.jade
new file mode 100644
index 00000000..94c63557
--- /dev/null
+++ b/client/components/rules/actions/checklistActions.jade
@@ -0,0 +1,70 @@
+template(name="checklistActions")
+ div.trigger-item
+ div.trigger-content
+ div.trigger-dropdown
+ select(id="check-action")
+ option(value="add") {{{_'r-add'}}}
+ option(value="remove") {{{_'r-remove'}}}
+ div.trigger-text
+ | {{{_'r-checklist'}}}
+ div.trigger-dropdown
+ input(id="checklist-name",type=text,placeholder="{{{_'r-name'}}}")
+ div.trigger-button.js-add-checklist-action.js-goto-rules
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-dropdown
+ select(id="checkall-action")
+ option(value="check") {{{_'r-check-all'}}}
+ option(value="uncheck") {{{_'r-uncheck-all'}}}
+ div.trigger-text
+ | {{{_'r-items-check'}}}
+ div.trigger-dropdown
+ input(id="checklist-name2",type=text,placeholder="{{{_'r-name'}}}")
+ div.trigger-button.js-add-checkall-action.js-goto-rules
+ i.fa.fa-plus
+
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-dropdown
+ select(id="check-item-action")
+ option(value="check") {{{_'r-check'}}}
+ option(value="uncheck") {{{_'r-uncheck'}}}
+ div.trigger-text
+ | {{{_'r-item'}}}
+ div.trigger-dropdown
+ input(id="checkitem-name",type=text,placeholder="{{{_'r-name'}}}")
+ div.trigger-text
+ | {{{_'r-of-checklist'}}}
+ div.trigger-dropdown
+ input(id="checklist-name3",type=text,placeholder="{{{_'r-name'}}}")
+ div.trigger-button.js-add-check-item-action.js-goto-rules
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{{_'r-add-checklist'}}}
+ div.trigger-dropdown
+ input(id="checklist-name-3",type=text,placeholder="{{{_'r-name'}}}")
+ div.trigger-text
+ | {{{_'r-with-items'}}}
+ div.trigger-dropdown
+ input(id="checklist-items",type=text,placeholder="{{{_'r-items-list'}}}")
+ div.trigger-button.js-add-checklist-items-action.js-goto-rules
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{{_'r-checklist-note'}}}
+
+
+
+
+
+
+
+
diff --git a/client/components/rules/actions/checklistActions.js b/client/components/rules/actions/checklistActions.js
new file mode 100644
index 00000000..3e79b075
--- /dev/null
+++ b/client/components/rules/actions/checklistActions.js
@@ -0,0 +1,151 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.subscribe('allRules');
+ },
+ events() {
+ return [{
+ 'click .js-add-checklist-items-action' (event) {
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const checklistName = this.find('#checklist-name-3').value;
+ const checklistItems = this.find('#checklist-items').value;
+ const boardId = Session.get('currentBoard');
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'addChecklistWithItems',
+ checklistName,
+ checklistItems,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+
+ },
+ 'click .js-add-checklist-action' (event) {
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const actionSelected = this.find('#check-action').value;
+ const checklistName = this.find('#checklist-name').value;
+ const boardId = Session.get('currentBoard');
+ const desc = Utils.getTriggerActionDesc(event, this);
+ if (actionSelected === 'add') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'addChecklist',
+ checklistName,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ if (actionSelected === 'remove') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'removeChecklist',
+ checklistName,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+
+ },
+ 'click .js-add-checkall-action' (event) {
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const actionSelected = this.find('#checkall-action').value;
+ const checklistName = this.find('#checklist-name2').value;
+ const boardId = Session.get('currentBoard');
+ const desc = Utils.getTriggerActionDesc(event, this);
+ if (actionSelected === 'check') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'checkAll',
+ checklistName,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ if (actionSelected === 'uncheck') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'uncheckAll',
+ checklistName,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ },
+ 'click .js-add-check-item-action' (event) {
+ const ruleName = this.data().ruleName.get();
+ const trigger = this.data().triggerVar.get();
+ const checkItemName = this.find('#checkitem-name');
+ const checklistName = this.find('#checklist-name3');
+ const actionSelected = this.find('#check-item-action').value;
+ const boardId = Session.get('currentBoard');
+ const desc = Utils.getTriggerActionDesc(event, this);
+ if (actionSelected === 'check') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'checkItem',
+ checklistName,
+ checkItemName,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ if (actionSelected === 'uncheck') {
+ const triggerId = Triggers.insert(trigger);
+ const actionId = Actions.insert({
+ actionType: 'uncheckItem',
+ checklistName,
+ checkItemName,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ }
+ },
+ }];
+ },
+
+}).register('checklistActions');
diff --git a/client/components/rules/actions/mailActions.jade b/client/components/rules/actions/mailActions.jade
new file mode 100644
index 00000000..7be78c75
--- /dev/null
+++ b/client/components/rules/actions/mailActions.jade
@@ -0,0 +1,11 @@
+template(name="mailActions")
+ div.trigger-item.trigger-item-mail
+ div.trigger-content.trigger-content-mail
+ div.trigger-text.trigger-text-email
+ | {{_'r-send-email'}}
+ div.trigger-dropdown-mail
+ input(id="email-to",type=text,placeholder="{{_'r-to'}}")
+ input(id="email-subject",type=text,placeholder="{{_'r-subject'}}")
+ textarea(id="email-msg")
+ div.trigger-button.trigger-button-email.js-mail-action.js-goto-rules
+ i.fa.fa-plus
diff --git a/client/components/rules/actions/mailActions.js b/client/components/rules/actions/mailActions.js
new file mode 100644
index 00000000..40cbc280
--- /dev/null
+++ b/client/components/rules/actions/mailActions.js
@@ -0,0 +1,35 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+
+ },
+
+ events() {
+ return [{
+ 'click .js-mail-action' (event) {
+ const emailTo = this.find('#email-to').value;
+ const emailSubject = this.find('#email-subject').value;
+ const emailMsg = this.find('#email-msg').value;
+ const trigger = this.data().triggerVar.get();
+ const ruleName = this.data().ruleName.get();
+ const triggerId = Triggers.insert(trigger);
+ const boardId = Session.get('currentBoard');
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const actionId = Actions.insert({
+ actionType: 'sendEmail',
+ emailTo,
+ emailSubject,
+ emailMsg,
+ boardId,
+ desc,
+ });
+ Rules.insert({
+ title: ruleName,
+ triggerId,
+ actionId,
+ boardId,
+ });
+ },
+ }];
+ },
+
+}).register('mailActions');
diff --git a/client/components/rules/ruleDetails.jade b/client/components/rules/ruleDetails.jade
new file mode 100644
index 00000000..7183cf96
--- /dev/null
+++ b/client/components/rules/ruleDetails.jade
@@ -0,0 +1,20 @@
+template(name="ruleDetails")
+ .rules
+ h2
+ i.fa.fa-magic
+ | {{{_ 'r-rule-details' }}}
+ .triggers-content
+ .triggers-body
+ .triggers-main-body
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ = trigger
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ = action
+ div.rules-back
+ button.js-goback
+ i.fa.fa-chevron-left
+ | {{{_ 'back'}}}
diff --git a/client/components/rules/ruleDetails.js b/client/components/rules/ruleDetails.js
new file mode 100644
index 00000000..17c86dc3
--- /dev/null
+++ b/client/components/rules/ruleDetails.js
@@ -0,0 +1,38 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.subscribe('allRules');
+ this.subscribe('allTriggers');
+ this.subscribe('allActions');
+
+ },
+
+ trigger() {
+ const ruleId = this.data().ruleId;
+ const rule = Rules.findOne({
+ _id: ruleId.get(),
+ });
+ const trigger = Triggers.findOne({
+ _id: rule.triggerId,
+ });
+ const desc = trigger.description();
+ const upperdesc = desc.charAt(0).toUpperCase() + desc.substr(1);
+ return upperdesc;
+ },
+ action() {
+ const ruleId = this.data().ruleId;
+ const rule = Rules.findOne({
+ _id: ruleId.get(),
+ });
+ const action = Actions.findOne({
+ _id: rule.actionId,
+ });
+ const desc = action.description();
+ const upperdesc = desc.charAt(0).toUpperCase() + desc.substr(1);
+ return upperdesc;
+ },
+
+ events() {
+ return [{}];
+ },
+
+}).register('ruleDetails');
diff --git a/client/components/rules/rules.styl b/client/components/rules/rules.styl
new file mode 100644
index 00000000..05302f7f
--- /dev/null
+++ b/client/components/rules/rules.styl
@@ -0,0 +1,190 @@
+.rules-list
+ overflow:hidden
+ overflow-y:scroll
+ max-height: 400px
+.rules-lists-item
+ display: block
+ position: relative
+ overflow: auto
+ p
+ display: inline-block
+ float: left
+ margin: revert
+.hide-element
+ display:none !important
+.user-details
+ display:inline-block
+.rules-btns-group
+ position: absolute
+ right: 0
+ top: 50%
+ transform: translateY(-50%)
+ button
+ margin: auto
+.rules-add
+ display: block
+ overflow: auto
+ margin-top: 15px
+ margin-bottom: 5px
+ input
+ display: inline-block
+ float: right
+ margin: auto
+ margin-right: 10px
+ button
+ display: inline-block
+ float: right
+ margin: auto
+.rules-back
+ display: block
+ overflow: auto
+ margin-top: 15px
+ margin-bottom: 5px
+ button
+ display: inline-block
+ float: right
+ margin: auto
+ margin-right:14px
+
+.flex
+ display: -webkit-box
+ display: -moz-box
+ display: -webkit-flex
+ display: -moz-flex
+ display: -ms-flexbox
+ display: flex
+
+
+
+.triggers-content
+ color: #727479
+ background: #dedede
+ .triggers-body
+ display flex
+ padding-top 15px
+ height 100%
+
+ .triggers-side-menu
+ background-color: #f7f7f7
+ border: 1px solid #f0f0f0
+ border-radius: 4px
+ height: intrinsic
+ box-shadow: inset -1px -1px 3px rgba(0,0,0,.05)
+
+ ul
+
+ li
+ margin: 0.1rem 0.2rem;
+ width:50px
+ height:50px
+ text-align:center
+ font-size: 25px
+ position: relative
+
+ i
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ box-shadow: none
+ transform: translate(-50%,-50%);
+
+
+ &.active
+ background #fff
+ box-shadow 0 1px 2px rgba(0,0,0,0.15);
+
+ &:hover
+ background #fff
+ box-shadow 0 1px 2px rgba(0,0,0,0.15);
+ a
+ @extends .flex
+ padding: 1rem 0 1rem 1rem
+ width: 100% - 5rem
+
+
+ span
+ font-size: 13px
+ .triggers-main-body
+ padding: 0.1em 1em
+ width:100%
+ .trigger-item
+ overflow:auto
+ padding:10px
+ height:40px
+ margin-bottom:5px
+ border-radius: 3px
+ position: relative
+ background-color: white
+ .trigger-content
+ position:absolute
+ top:50%
+ transform: translateY(-50%)
+ left:10px
+ .trigger-text
+ font-size: 16px
+ display:inline-block
+ .trigger-inline-button
+ font-size: 16px
+ display: inline;
+ padding: 6px;
+ border: 1px solid #eee
+ border-radius: 4px
+ box-shadow: inset -1px -1px 3px rgba(0,0,0,.05)
+ &:hover, &.is-active
+ box-shadow: 0 0 0 2px darken(white, 60%) inset
+ .trigger-text.trigger-text-email
+ margin-left: 5px;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ .trigger-dropdown
+ display:inline-block
+ select
+ width:auto
+ height:30px
+ margin:0px
+ margin-left:5px
+ input
+ display: inline-block
+ width: 80px;
+ margin: 0;
+ .trigger-content-mail
+ left:20px
+ right:100px
+ .trigger-button
+ position:absolute
+ top:50%
+ transform: translateY(-50%)
+ width:30px
+ height:30px
+ border: 1px solid #eee
+ border-radius: 4px
+ box-shadow: inset -1px -1px 3px rgba(0,0,0,.05)
+ text-align:center
+ font-size: 20px
+ right:10px
+ i
+ position: absolute
+ top: 50%
+ left: 50%
+ box-shadow: none
+ transform: translate(-50%,-50%)
+ &:hover, &.is-active
+ box-shadow: 0 0 0 2px darken(white, 60%) inset
+ .trigger-button.trigger-button-email
+ top:30px
+ .trigger-button.trigger-button-person
+ right:-40px
+ .trigger-button.trigger-button-color
+ top: unset
+ position: unset
+ transform: unset
+ font-size: 16px
+ width:auto
+ padding-left: 10px
+ padding-right: 10px
+ height:40px
+ .trigger-item.trigger-item-mail
+ height:300px
+
+
+
diff --git a/client/components/rules/rulesActions.jade b/client/components/rules/rulesActions.jade
new file mode 100644
index 00000000..3ac04e1c
--- /dev/null
+++ b/client/components/rules/rulesActions.jade
@@ -0,0 +1,29 @@
+template(name="rulesActions")
+ h2
+ i.fa.fa-magic
+ | {{{_ 'r-rule' }}} "#{data.ruleName.get}" - {{{_ 'r-add-action'}}}
+ .triggers-content
+ .triggers-body
+ .triggers-side-menu
+ ul
+ li.active.js-set-board-actions
+ i.fa.fa-columns
+ li.js-set-card-actions
+ i.fa.fa-sticky-note
+ li.js-set-checklist-actions
+ i.fa.fa-check
+ li.js-set-mail-actions
+ i.fa.fa-at
+ .triggers-main-body
+ if ($eq currentActions.get 'board')
+ +boardActions(ruleName=data.ruleName triggerVar=data.triggerVar)
+ else if ($eq currentActions.get 'card')
+ +cardActions(ruleName=data.ruleName triggerVar=data.triggerVar)
+ else if ($eq currentActions.get 'checklist')
+ +checklistActions(ruleName=data.ruleName triggerVar=data.triggerVar)
+ else if ($eq currentActions.get 'mail')
+ +mailActions(ruleName=data.ruleName triggerVar=data.triggerVar)
+ div.rules-back
+ button.js-goback
+ i.fa.fa-chevron-left
+ | {{{_ 'back'}}}
diff --git a/client/components/rules/rulesActions.js b/client/components/rules/rulesActions.js
new file mode 100644
index 00000000..64a5c70e
--- /dev/null
+++ b/client/components/rules/rulesActions.js
@@ -0,0 +1,58 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.currentActions = new ReactiveVar('board');
+ },
+
+ setBoardActions() {
+ this.currentActions.set('board');
+ $('.js-set-card-actions').removeClass('active');
+ $('.js-set-board-actions').addClass('active');
+ $('.js-set-checklist-actions').removeClass('active');
+ $('.js-set-mail-actions').removeClass('active');
+ },
+ setCardActions() {
+ this.currentActions.set('card');
+ $('.js-set-card-actions').addClass('active');
+ $('.js-set-board-actions').removeClass('active');
+ $('.js-set-checklist-actions').removeClass('active');
+ $('.js-set-mail-actions').removeClass('active');
+ },
+ setChecklistActions() {
+ this.currentActions.set('checklist');
+ $('.js-set-card-actions').removeClass('active');
+ $('.js-set-board-actions').removeClass('active');
+ $('.js-set-checklist-actions').addClass('active');
+ $('.js-set-mail-actions').removeClass('active');
+ },
+ setMailActions() {
+ this.currentActions.set('mail');
+ $('.js-set-card-actions').removeClass('active');
+ $('.js-set-board-actions').removeClass('active');
+ $('.js-set-checklist-actions').removeClass('active');
+ $('.js-set-mail-actions').addClass('active');
+ },
+
+ rules() {
+ return Rules.find({});
+ },
+
+ name() {
+ // console.log(this.data());
+ },
+ events() {
+ return [{
+ 'click .js-set-board-actions'(){
+ this.setBoardActions();
+ },
+ 'click .js-set-card-actions'() {
+ this.setCardActions();
+ },
+ 'click .js-set-mail-actions'() {
+ this.setMailActions();
+ },
+ 'click .js-set-checklist-actions'() {
+ this.setChecklistActions();
+ },
+ }];
+ },
+}).register('rulesActions');
diff --git a/client/components/rules/rulesList.jade b/client/components/rules/rulesList.jade
new file mode 100644
index 00000000..c2676aa7
--- /dev/null
+++ b/client/components/rules/rulesList.jade
@@ -0,0 +1,27 @@
+template(name="rulesList")
+ .rules
+ h2
+ i.fa.fa-magic
+ | {{{_ 'r-board-rules' }}}
+
+ ul.rules-list
+ each rules
+ li.rules-lists-item
+ p
+ = title
+ div.rules-btns-group
+ button.js-goto-details
+ i.fa.fa-eye
+ | {{{_ 'r-view-rule'}}}
+ if currentUser.isAdmin
+ button.js-delete-rule
+ i.fa.fa-trash-o
+ | {{{_ 'r-delete-rule'}}}
+ else
+ li.no-items-message {{{_ 'r-no-rules' }}}
+ if currentUser.isAdmin
+ div.rules-add
+ button.js-goto-trigger
+ i.fa.fa-plus
+ | {{{_ 'r-add-rule'}}}
+ input(type=text,placeholder="{{{_ 'r-new-rule-name' }}}",id="ruleTitle") \ No newline at end of file
diff --git a/client/components/rules/rulesList.js b/client/components/rules/rulesList.js
new file mode 100644
index 00000000..d3923bf9
--- /dev/null
+++ b/client/components/rules/rulesList.js
@@ -0,0 +1,15 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.subscribe('allRules');
+ },
+
+ rules() {
+ const boardId = Session.get('currentBoard');
+ return Rules.find({
+ boardId,
+ });
+ },
+ events() {
+ return [{}];
+ },
+}).register('rulesList');
diff --git a/client/components/rules/rulesMain.jade b/client/components/rules/rulesMain.jade
new file mode 100644
index 00000000..dc33ee4e
--- /dev/null
+++ b/client/components/rules/rulesMain.jade
@@ -0,0 +1,9 @@
+template(name="rulesMain")
+ if($eq rulesCurrentTab.get 'rulesList')
+ +rulesList
+ if($eq rulesCurrentTab.get 'trigger')
+ +rulesTriggers(ruleName=ruleName triggerVar=triggerVar)
+ if($eq rulesCurrentTab.get 'action')
+ +rulesActions(ruleName=ruleName triggerVar=triggerVar)
+ if($eq rulesCurrentTab.get 'ruleDetails')
+ +ruleDetails(ruleId=ruleId) \ No newline at end of file
diff --git a/client/components/rules/rulesMain.js b/client/components/rules/rulesMain.js
new file mode 100644
index 00000000..d4af38f4
--- /dev/null
+++ b/client/components/rules/rulesMain.js
@@ -0,0 +1,97 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.rulesCurrentTab = new ReactiveVar('rulesList');
+ this.ruleName = new ReactiveVar('');
+ this.triggerVar = new ReactiveVar();
+ this.ruleId = new ReactiveVar();
+ },
+
+ setTrigger() {
+ this.rulesCurrentTab.set('trigger');
+ },
+ sanitizeObject(obj){
+ Object.keys(obj).forEach((key) => {
+ if(obj[key] === '' || obj[key] === undefined){
+ obj[key] = '*';
+ }}
+ );
+ },
+ setRulesList() {
+ this.rulesCurrentTab.set('rulesList');
+ },
+
+ setAction() {
+ this.rulesCurrentTab.set('action');
+ },
+
+ setRuleDetails() {
+ this.rulesCurrentTab.set('ruleDetails');
+ },
+
+ events() {
+ return [{
+ 'click .js-delete-rule' () {
+ const rule = this.currentData();
+ Rules.remove(rule._id);
+ Actions.remove(rule.actionId);
+ Triggers.remove(rule.triggerId);
+
+ },
+ 'click .js-goto-trigger' (event) {
+ event.preventDefault();
+ const ruleTitle = this.find('#ruleTitle').value;
+ if(ruleTitle !== undefined && ruleTitle !== ''){
+ this.find('#ruleTitle').value = '';
+ this.ruleName.set(ruleTitle);
+ this.setTrigger();
+ }
+ },
+ 'click .js-goto-action' (event) {
+ event.preventDefault();
+ // Add user to the trigger
+ const username = $(event.currentTarget.offsetParent).find('.user-name').val();
+ let trigger = this.triggerVar.get();
+ trigger.userId = '*';
+ if(username !== undefined ){
+ const userFound = Users.findOne({username});
+ if(userFound !== undefined){
+ trigger.userId = userFound._id;
+ this.triggerVar.set(trigger);
+ }
+ }
+ // Sanitize trigger
+ trigger = this.triggerVar.get();
+ this.sanitizeObject(trigger);
+ this.triggerVar.set(trigger);
+ this.setAction();
+ },
+ 'click .js-show-user-field' (event) {
+ event.preventDefault();
+ $(event.currentTarget.offsetParent).find('.user-details').removeClass('hide-element');
+ },
+ 'click .js-goto-rules' (event) {
+ event.preventDefault();
+ this.setRulesList();
+ },
+ 'click .js-goback' (event) {
+ event.preventDefault();
+ if(this.rulesCurrentTab.get() === 'trigger' || this.rulesCurrentTab.get() === 'ruleDetails' ){
+ this.setRulesList();
+ }
+ if(this.rulesCurrentTab.get() === 'action'){
+ this.setTrigger();
+ }
+ },
+ 'click .js-goto-details' (event) {
+ event.preventDefault();
+ const rule = this.currentData();
+ this.ruleId.set(rule._id);
+ this.setRuleDetails();
+ },
+
+ }];
+ },
+
+}).register('rulesMain');
+
+
diff --git a/client/components/rules/rulesTriggers.jade b/client/components/rules/rulesTriggers.jade
new file mode 100644
index 00000000..79d9d98e
--- /dev/null
+++ b/client/components/rules/rulesTriggers.jade
@@ -0,0 +1,25 @@
+template(name="rulesTriggers")
+ h2
+ i.fa.fa-magic
+ | {{{_ 'r-rule' }}} "#{data.ruleName.get}" - {{{_ 'r-add-trigger'}}}
+ .triggers-content
+ .triggers-body
+ .triggers-side-menu
+ ul
+ li.active.js-set-board-triggers
+ i.fa.fa-columns
+ li.js-set-card-triggers
+ i.fa.fa-sticky-note
+ li.js-set-checklist-triggers
+ i.fa.fa-check
+ .triggers-main-body
+ if showBoardTrigger.get
+ +boardTriggers
+ else if showCardTrigger.get
+ +cardTriggers
+ else if showChecklistTrigger.get
+ +checklistTriggers
+ div.rules-back
+ button.js-goback
+ i.fa.fa-chevron-left
+ | {{{_ 'back'}}}
diff --git a/client/components/rules/rulesTriggers.js b/client/components/rules/rulesTriggers.js
new file mode 100644
index 00000000..e3c16221
--- /dev/null
+++ b/client/components/rules/rulesTriggers.js
@@ -0,0 +1,53 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.showBoardTrigger = new ReactiveVar(true);
+ this.showCardTrigger = new ReactiveVar(false);
+ this.showChecklistTrigger = new ReactiveVar(false);
+ },
+
+ setBoardTriggers() {
+ this.showBoardTrigger.set(true);
+ this.showCardTrigger.set(false);
+ this.showChecklistTrigger.set(false);
+ $('.js-set-card-triggers').removeClass('active');
+ $('.js-set-board-triggers').addClass('active');
+ $('.js-set-checklist-triggers').removeClass('active');
+ },
+ setCardTriggers() {
+ this.showBoardTrigger.set(false);
+ this.showCardTrigger.set(true);
+ this.showChecklistTrigger.set(false);
+ $('.js-set-card-triggers').addClass('active');
+ $('.js-set-board-triggers').removeClass('active');
+ $('.js-set-checklist-triggers').removeClass('active');
+ },
+ setChecklistTriggers() {
+ this.showBoardTrigger.set(false);
+ this.showCardTrigger.set(false);
+ this.showChecklistTrigger.set(true);
+ $('.js-set-card-triggers').removeClass('active');
+ $('.js-set-board-triggers').removeClass('active');
+ $('.js-set-checklist-triggers').addClass('active');
+ },
+
+ rules() {
+ return Rules.find({});
+ },
+
+ name() {
+ // console.log(this.data());
+ },
+ events() {
+ return [{
+ 'click .js-set-board-triggers' () {
+ this.setBoardTriggers();
+ },
+ 'click .js-set-card-triggers' () {
+ this.setCardTriggers();
+ },
+ 'click .js-set-checklist-triggers' () {
+ this.setChecklistTriggers();
+ },
+ }];
+ },
+}).register('rulesTriggers');
diff --git a/client/components/rules/triggers/boardTriggers.jade b/client/components/rules/triggers/boardTriggers.jade
new file mode 100644
index 00000000..ff1406f6
--- /dev/null
+++ b/client/components/rules/triggers/boardTriggers.jade
@@ -0,0 +1,116 @@
+template(name="boardTriggers")
+ div.trigger-item#trigger-two
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-a-card'}}
+ div.trigger-inline-button.js-open-card-title-popup
+ i.fa.fa-filter
+ div.trigger-text
+ | {{_'r-is'}}
+ div.trigger-text
+ | {{_'r-added-to'}}
+ div.trigger-text
+ | {{_'r-list'}}
+ div.trigger-dropdown
+ input(id="create-list-name",type=text,placeholder="{{_'r-list-name'}}")
+ div.trigger-text
+ | {{_'r-in-swimlane'}}
+ div.trigger-dropdown
+ input(id="create-swimlane-name",type=text,placeholder="{{_'r-swimlane-name'}}")
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-create-trigger.js-goto-action
+ i.fa.fa-plus
+
+ div.trigger-item#trigger-three
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-a-card'}}
+ div.trigger-inline-button.js-open-card-title-popup
+ i.fa.fa-filter
+ div.trigger-text
+ | {{_'r-is-moved'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-gen-moved-trigger.js-goto-action
+ i.fa.fa-plus
+
+ div.trigger-item#trigger-four
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-a-card'}}
+ div.trigger-inline-button.js-open-card-title-popup
+ i.fa.fa-filter
+ div.trigger-text
+ | {{_'r-is'}}
+ div.trigger-dropdown
+ select(id="move-action")
+ option(value="moved-to") {{_'r-moved-to'}}
+ option(value="moved-from") {{_'r-moved-from'}}
+ div.trigger-text
+ | {{_'r-list'}}
+ div.trigger-dropdown
+ input(id="move-list-name",type=text,placeholder="{{_'r-list-name'}}")
+ div.trigger-text
+ | {{_'r-in-swimlane'}}
+ div.trigger-dropdown
+ input(id="create-swimlane-name-2",type=text,placeholder="{{_'r-swimlane-name'}}")
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-moved-trigger.js-goto-action
+ i.fa.fa-plus
+
+ div.trigger-item#trigger-five
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-a-card'}}
+ div.trigger-inline-button.js-open-card-title-popup
+ i.fa.fa-filter
+ div.trigger-text
+ | {{_'r-is'}}
+ div.trigger-dropdown
+ select(id="arch-action")
+ option(value="archived") {{_'r-archived'}}
+ option(value="unarchived") {{_'r-unarchived'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-arch-trigger.js-goto-action
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{{_'r-board-note'}}}
+
+template(name="boardCardTitlePopup")
+ form
+ label
+ | Card Title Filter
+ input.js-card-filter-name(type="text" value=title autofocus)
+ input.js-card-filter-button.primary.wide(type="submit" value="{{_ 'set-filter'}}")
+
+
+
+
+
+
diff --git a/client/components/rules/triggers/boardTriggers.js b/client/components/rules/triggers/boardTriggers.js
new file mode 100644
index 00000000..1dc5c437
--- /dev/null
+++ b/client/components/rules/triggers/boardTriggers.js
@@ -0,0 +1,119 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.provaVar = new ReactiveVar('');
+ this.currentPopupTriggerId = 'def';
+ this.cardTitleFilters = {};
+ },
+ setNameFilter(name){
+ this.cardTitleFilters[this.currentPopupTriggerId] = name;
+ },
+
+ events() {
+ return [{
+ 'click .js-open-card-title-popup'(event){
+ const funct = Popup.open('boardCardTitle');
+ const divId = $(event.currentTarget.parentNode.parentNode).attr('id');
+ //console.log('current popup');
+ //console.log(this.currentPopupTriggerId);
+ this.currentPopupTriggerId = divId;
+ funct.call(this, event);
+ },
+ 'click .js-add-create-trigger' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const datas = this.data();
+ const listName = this.find('#create-list-name').value;
+ const swimlaneName = this.find('#create-swimlane-name').value;
+ const boardId = Session.get('currentBoard');
+ const divId = $(event.currentTarget.parentNode).attr('id');
+ const cardTitle = this.cardTitleFilters[divId];
+ // move to generic funciont
+ datas.triggerVar.set({
+ activityType: 'createCard',
+ boardId,
+ cardTitle,
+ swimlaneName,
+ listName,
+ desc,
+ });
+ },
+ 'click .js-add-moved-trigger' (event) {
+ const datas = this.data();
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const swimlaneName = this.find('#create-swimlane-name-2').value;
+ const actionSelected = this.find('#move-action').value;
+ const listName = this.find('#move-list-name').value;
+ const boardId = Session.get('currentBoard');
+ const divId = $(event.currentTarget.parentNode).attr('id');
+ const cardTitle = this.cardTitleFilters[divId];
+ if (actionSelected === 'moved-to') {
+ datas.triggerVar.set({
+ activityType: 'moveCard',
+ boardId,
+ listName,
+ cardTitle,
+ swimlaneName,
+ 'oldListName': '*',
+ desc,
+ });
+ }
+ if (actionSelected === 'moved-from') {
+ datas.triggerVar.set({
+ activityType: 'moveCard',
+ boardId,
+ cardTitle,
+ swimlaneName,
+ 'listName': '*',
+ 'oldListName': listName,
+ desc,
+ });
+ }
+ },
+ 'click .js-add-gen-moved-trigger' (event){
+ const datas = this.data();
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const boardId = Session.get('currentBoard');
+
+ datas.triggerVar.set({
+ 'activityType': 'moveCard',
+ boardId,
+ 'swimlaneName': '*',
+ 'listName':'*',
+ 'oldListName': '*',
+ desc,
+ });
+ },
+ 'click .js-add-arc-trigger' (event) {
+ const datas = this.data();
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const actionSelected = this.find('#arch-action').value;
+ const boardId = Session.get('currentBoard');
+ if (actionSelected === 'archived') {
+ datas.triggerVar.set({
+ activityType: 'archivedCard',
+ boardId,
+ desc,
+ });
+ }
+ if (actionSelected === 'unarchived') {
+ datas.triggerVar.set({
+ activityType: 'restoredCard',
+ boardId,
+ desc,
+ });
+ }
+ },
+
+ }];
+ },
+
+}).register('boardTriggers');
+
+
+Template.boardCardTitlePopup.events({
+ submit(evt, tpl) {
+ const title = tpl.$('.js-card-filter-name').val().trim();
+ Popup.getOpenerComponent().setNameFilter(title);
+ evt.preventDefault();
+ Popup.close();
+ },
+});
diff --git a/client/components/rules/triggers/cardTriggers.jade b/client/components/rules/triggers/cardTriggers.jade
new file mode 100644
index 00000000..72c4b8db
--- /dev/null
+++ b/client/components/rules/triggers/cardTriggers.jade
@@ -0,0 +1,114 @@
+template(name="cardTriggers")
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-a-label-is'}}
+ div.trigger-dropdown
+ select(id="label-action")
+ option(value="added") {{_'r-added-to'}}
+ option(value="removed") {{_'r-removed-from'}}
+ div.trigger-text
+ | {{_'r-a-card'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-gen-label-trigger.js-goto-action
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-the-label-is'}}
+ div.trigger-dropdown
+ select(id="spec-label")
+ each labels
+ option(value="#{_id}" style="background-color: #{name}")
+ = translatedname
+ div.trigger-text
+ | {{_'r-is'}}
+ div.trigger-dropdown
+ select(id="spec-label-action")
+ option(value="added") {{_'r-added-to'}}
+ option(value="removed") {{_'r-removed-from'}}
+ div.trigger-text
+ | {{_'r-a-card'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-spec-label-trigger.js-goto-action
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-a-member'}}
+ div.trigger-dropdown
+ select(id="gen-member-action")
+ option(value="added") {{_'r-added-to'}}
+ option(value="removed") {{_'r-removed-from'}}
+ div.trigger-text
+ | {{_'r-a-card'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-gen-member-trigger.js-goto-action
+ i.fa.fa-plus
+
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-the-member'}}
+ div.trigger-dropdown
+ input(id="spec-member",type=text,placeholder="{{_'r-name'}}")
+ div.trigger-text
+ | {{_'r-is'}}
+ div.trigger-dropdown
+ select(id="spec-member-action")
+ option(value="added") {{_'r-added-to'}}
+ option(value="removed") {{_'r-removed-from'}}
+ div.trigger-text
+ | {{_'r-a-card'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-spec-member-trigger.js-goto-action
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-a-attach'}}
+ div.trigger-text
+ | {{_'r-is'}}
+ div.trigger-dropdown
+ select(id="attach-action")
+ option(value="added") {{_'r-added-to'}}
+ option(value="removed") {{_'r-removed-from'}}
+ div.trigger-text
+ | {{_'r-a-card'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-attachment-trigger.js-goto-action
+ i.fa.fa-plus
diff --git a/client/components/rules/triggers/cardTriggers.js b/client/components/rules/triggers/cardTriggers.js
new file mode 100644
index 00000000..82b21d61
--- /dev/null
+++ b/client/components/rules/triggers/cardTriggers.js
@@ -0,0 +1,131 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.subscribe('allRules');
+ },
+ labels() {
+ const labels = Boards.findOne(Session.get('currentBoard')).labels;
+ for (let i = 0; i < labels.length; i++) {
+ if (labels[i].name === '' || labels[i].name === undefined) {
+ labels[i].name = labels[i].color;
+ labels[i].translatedname = `${TAPi18n.__(`color-${ labels[i].color}`)}`;
+ } else {
+ labels[i].translatedname = labels[i].name;
+ }
+ }
+ return labels;
+ },
+ events() {
+ return [{
+ 'click .js-add-gen-label-trigger' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const datas = this.data();
+ const actionSelected = this.find('#label-action').value;
+ const boardId = Session.get('currentBoard');
+ if (actionSelected === 'added') {
+ datas.triggerVar.set({
+ activityType: 'addedLabel',
+ boardId,
+ 'labelId': '*',
+ desc,
+ });
+ }
+ if (actionSelected === 'removed') {
+ datas.triggerVar.set({
+ activityType: 'removedLabel',
+ boardId,
+ 'labelId': '*',
+ desc,
+ });
+ }
+ },
+ 'click .js-add-spec-label-trigger' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const datas = this.data();
+ const actionSelected = this.find('#spec-label-action').value;
+ const labelId = this.find('#spec-label').value;
+ const boardId = Session.get('currentBoard');
+ if (actionSelected === 'added') {
+ datas.triggerVar.set({
+ activityType: 'addedLabel',
+ boardId,
+ labelId,
+ desc,
+ });
+ }
+ if (actionSelected === 'removed') {
+ datas.triggerVar.set({
+ activityType: 'removedLabel',
+ boardId,
+ labelId,
+ desc,
+ });
+ }
+ },
+ 'click .js-add-gen-member-trigger' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const datas = this.data();
+ const actionSelected = this.find('#gen-member-action').value;
+ const boardId = Session.get('currentBoard');
+ if (actionSelected === 'added') {
+ datas.triggerVar.set({
+ activityType: 'joinMember',
+ boardId,
+ 'username': '*',
+ desc,
+ });
+ }
+ if (actionSelected === 'removed') {
+ datas.triggerVar.set({
+ activityType: 'unjoinMember',
+ boardId,
+ 'username': '*',
+ desc,
+ });
+ }
+ },
+ 'click .js-add-spec-member-trigger' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const datas = this.data();
+ const actionSelected = this.find('#spec-member-action').value;
+ const username = this.find('#spec-member').value;
+ const boardId = Session.get('currentBoard');
+ if (actionSelected === 'added') {
+ datas.triggerVar.set({
+ activityType: 'joinMember',
+ boardId,
+ username,
+ desc,
+ });
+ }
+ if (actionSelected === 'removed') {
+ datas.triggerVar.set({
+ activityType: 'unjoinMember',
+ boardId,
+ username,
+ desc,
+ });
+ }
+ },
+ 'click .js-add-attachment-trigger' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const datas = this.data();
+ const actionSelected = this.find('#attach-action').value;
+ const boardId = Session.get('currentBoard');
+ if (actionSelected === 'added') {
+ datas.triggerVar.set({
+ activityType: 'addAttachment',
+ boardId,
+ desc,
+ });
+ }
+ if (actionSelected === 'removed') {
+ datas.triggerVar.set({
+ activityType: 'deleteAttachment',
+ boardId,
+ desc,
+ });
+ }
+ },
+ }];
+ },
+}).register('cardTriggers');
diff --git a/client/components/rules/triggers/checklistTriggers.jade b/client/components/rules/triggers/checklistTriggers.jade
new file mode 100644
index 00000000..841ec6f7
--- /dev/null
+++ b/client/components/rules/triggers/checklistTriggers.jade
@@ -0,0 +1,125 @@
+template(name="checklistTriggers")
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-a-checklist'}}
+ div.trigger-dropdown
+ select(id="gen-check-action")
+ option(value="created") {{_'r-added-to'}}
+ option(value="removed") {{_'r-removed-from'}}
+ div.trigger-text
+ | {{_'r-a-card'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-gen-check-trigger.js-goto-action
+ i.fa.fa-plus
+
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-the-checklist'}}
+ div.trigger-dropdown
+ input(id="check-name",type=text,placeholder="{{_'r-name'}}")
+ div.trigger-text
+ | {{_'r-is'}}
+ div.trigger-dropdown
+ select(id="spec-check-action")
+ option(value="created") {{_'r-added-to'}}
+ option(value="removed") {{_'r-removed-from'}}
+ div.trigger-text
+ | {{_'r-a-card'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-spec-check-trigger.js-goto-action
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-a-checklist'}}
+ div.trigger-dropdown
+ select(id="gen-comp-check-action")
+ option(value="completed") {{_'r-completed'}}
+ option(value="uncompleted") {{_'r-made-incomplete'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-gen-comp-trigger.js-goto-action
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-the-checklist'}}
+ div.trigger-dropdown
+ input(id="spec-comp-check-name",type=text,placeholder="{{_'r-name'}}")
+ div.trigger-text
+ | {{_'r-is'}}
+ div.trigger-dropdown
+ select(id="spec-comp-check-action")
+ option(value="completed") {{_'r-completed'}}
+ option(value="uncompleted") {{_'r-made-incomplete'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-spec-comp-trigger.js-goto-action
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-a-item'}}
+ div.trigger-dropdown
+ select(id="check-item-gen-action")
+ option(value="checked") {{_'r-checked'}}
+ option(value="unchecked") {{_'r-unchecked'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-gen-check-item-trigger.js-goto-action
+ i.fa.fa-plus
+
+ div.trigger-item
+ div.trigger-content
+ div.trigger-text
+ | {{_'r-when-the-item'}}
+ div.trigger-dropdown
+ input(id="check-item-name",type=text,placeholder="{{_'r-name'}}")
+ div.trigger-text
+ | {{_'r-is'}}
+ div.trigger-dropdown
+ select(id="check-item-spec-action")
+ option(value="checked") {{_'r-checked'}}
+ option(value="unchecked") {{_'r-unchecked'}}
+ div.trigger-button.trigger-button-person.js-show-user-field
+ i.fa.fa-user
+ div.user-details.hide-element
+ div.trigger-text
+ | {{_'r-by'}}
+ div.trigger-dropdown
+ input(class="user-name",type=text,placeholder="{{_'username'}}")
+ div.trigger-button.js-add-spec-check-item-trigger.js-goto-action
+ i.fa.fa-plus
diff --git a/client/components/rules/triggers/checklistTriggers.js b/client/components/rules/triggers/checklistTriggers.js
new file mode 100644
index 00000000..2272be29
--- /dev/null
+++ b/client/components/rules/triggers/checklistTriggers.js
@@ -0,0 +1,146 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.subscribe('allRules');
+ },
+ events() {
+ return [{
+ 'click .js-add-gen-check-trigger' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const datas = this.data();
+ const actionSelected = this.find('#gen-check-action').value;
+ const boardId = Session.get('currentBoard');
+ if (actionSelected === 'created') {
+ datas.triggerVar.set({
+ activityType: 'addChecklist',
+ boardId,
+ 'checklistName': '*',
+ desc,
+ });
+ }
+ if (actionSelected === 'removed') {
+ datas.triggerVar.set({
+ activityType: 'removeChecklist',
+ boardId,
+ 'checklistName': '*',
+ desc,
+ });
+ }
+ },
+ 'click .js-add-spec-check-trigger' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const datas = this.data();
+ const actionSelected = this.find('#spec-check-action').value;
+ const checklistId = this.find('#check-name').value;
+ const boardId = Session.get('currentBoard');
+ if (actionSelected === 'created') {
+ datas.triggerVar.set({
+ activityType: 'addChecklist',
+ boardId,
+ 'checklistName': checklistId,
+ desc,
+ });
+ }
+ if (actionSelected === 'removed') {
+ datas.triggerVar.set({
+ activityType: 'removeChecklist',
+ boardId,
+ 'checklistName': checklistId,
+ desc,
+ });
+ }
+ },
+ 'click .js-add-gen-comp-trigger' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+
+ const datas = this.data();
+ const actionSelected = this.find('#gen-comp-check-action').value;
+ const boardId = Session.get('currentBoard');
+ if (actionSelected === 'completed') {
+ datas.triggerVar.set({
+ activityType: 'completeChecklist',
+ boardId,
+ 'checklistName': '*',
+ desc,
+ });
+ }
+ if (actionSelected === 'uncompleted') {
+ datas.triggerVar.set({
+ activityType: 'uncompleteChecklist',
+ boardId,
+ 'checklistName': '*',
+ desc,
+ });
+ }
+ },
+ 'click .js-add-spec-comp-trigger' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const datas = this.data();
+ const actionSelected = this.find('#spec-comp-check-action').value;
+ const checklistId = this.find('#spec-comp-check-name').value;
+ const boardId = Session.get('currentBoard');
+ if (actionSelected === 'completed') {
+ datas.triggerVar.set({
+ activityType: 'completeChecklist',
+ boardId,
+ 'checklistName': checklistId,
+ desc,
+ });
+ }
+ if (actionSelected === 'uncompleted') {
+ datas.triggerVar.set({
+ activityType: 'uncompleteChecklist',
+ boardId,
+ 'checklistName': checklistId,
+ desc,
+ });
+ }
+ },
+ 'click .js-add-gen-check-item-trigger' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const datas = this.data();
+ const actionSelected = this.find('#check-item-gen-action').value;
+ const boardId = Session.get('currentBoard');
+ if (actionSelected === 'checked') {
+ datas.triggerVar.set({
+ activityType: 'checkedItem',
+ boardId,
+ 'checklistItemName': '*',
+ desc,
+ });
+ }
+ if (actionSelected === 'unchecked') {
+ datas.triggerVar.set({
+ activityType: 'uncheckedItem',
+ boardId,
+ 'checklistItemName': '*',
+ desc,
+ });
+ }
+ },
+ 'click .js-add-spec-check-item-trigger' (event) {
+ const desc = Utils.getTriggerActionDesc(event, this);
+ const datas = this.data();
+ const actionSelected = this.find('#check-item-spec-action').value;
+ const checklistItemId = this.find('#check-item-name').value;
+ const boardId = Session.get('currentBoard');
+ if (actionSelected === 'checked') {
+ datas.triggerVar.set({
+ activityType: 'checkedItem',
+ boardId,
+ 'checklistItemName': checklistItemId,
+ desc,
+ });
+ }
+ if (actionSelected === 'unchecked') {
+ datas.triggerVar.set({
+ activityType: 'uncheckedItem',
+ boardId,
+ 'checklistItemName': checklistItemId,
+ desc,
+ });
+ }
+ },
+ }];
+ },
+
+}).register('checklistTriggers');
diff --git a/client/components/settings/connectionMethod.jade b/client/components/settings/connectionMethod.jade
new file mode 100644
index 00000000..d191929f
--- /dev/null
+++ b/client/components/settings/connectionMethod.jade
@@ -0,0 +1,9 @@
+template(name='connectionMethod')
+ div.at-form-authentication
+ label {{_ 'authentication-method'}}
+ select.select-authentication
+ each authentications
+ if isSelected value
+ option(value="{{value}}" selected) {{_ value}}
+ else
+ option(value="{{value}}") {{_ value}} \ No newline at end of file
diff --git a/client/components/settings/connectionMethod.js b/client/components/settings/connectionMethod.js
new file mode 100644
index 00000000..db9da25f
--- /dev/null
+++ b/client/components/settings/connectionMethod.js
@@ -0,0 +1,37 @@
+Template.connectionMethod.onCreated(function() {
+ this.authenticationMethods = new ReactiveVar([]);
+
+ Meteor.call('getAuthenticationsEnabled', (_, result) => {
+ if (result) {
+ // TODO : add a management of different languages
+ // (ex {value: ldap, text: TAPi18n.__('ldap', {}, T9n.getLanguage() || 'en')})
+ this.authenticationMethods.set([
+ {value: 'password'},
+ // Gets only the authentication methods availables
+ ...Object.entries(result).filter((e) => e[1]).map((e) => ({value: e[0]})),
+ ]);
+ }
+
+ // If only the default authentication available, hides the select boxe
+ const content = $('.at-form-authentication');
+ if (!(this.authenticationMethods.get().length > 1)) {
+ content.hide();
+ } else {
+ content.show();
+ }
+ });
+});
+
+Template.connectionMethod.onRendered(() => {
+ // Moves the select boxe in the first place of the at-pwd-form div
+ $('.at-form-authentication').detach().prependTo('.at-pwd-form');
+});
+
+Template.connectionMethod.helpers({
+ authentications() {
+ return Template.instance().authenticationMethods.get();
+ },
+ isSelected(match) {
+ return Template.instance().data.authenticationMethod === match;
+ },
+});
diff --git a/client/components/settings/informationBody.jade b/client/components/settings/informationBody.jade
index 53907513..feb7c0dc 100644
--- a/client/components/settings/informationBody.jade
+++ b/client/components/settings/informationBody.jade
@@ -17,7 +17,7 @@ template(name='statistics')
table
tbody
tr
- th {{_ 'Wekan_version'}}
+ th Wekan {{_ 'info'}}
td {{statistics.version}}
tr
th {{_ 'Node_version'}}
diff --git a/client/components/settings/invitationCode.js b/client/components/settings/invitationCode.js
index c02f860f..fa355179 100644
--- a/client/components/settings/invitationCode.js
+++ b/client/components/settings/invitationCode.js
@@ -1,6 +1,13 @@
-Template.invitationCode.onRendered(() => {
- const setting = Settings.findOne();
- if (setting || setting.disableRegistration) {
- $('#invitationcode').hide();
- }
+Template.invitationCode.onRendered(function() {
+ Meteor.subscribe('setting', {
+ onReady() {
+ const setting = Settings.findOne();
+
+ if (!setting || !setting.disableRegistration) {
+ $('#invitationcode').hide();
+ }
+
+ return this.stop();
+ },
+ });
});
diff --git a/client/components/settings/peopleBody.jade b/client/components/settings/peopleBody.jade
index a3506a24..15deb005 100644
--- a/client/components/settings/peopleBody.jade
+++ b/client/components/settings/peopleBody.jade
@@ -3,8 +3,13 @@ template(name="people")
unless currentUser.isAdmin
| {{_ 'error-notAuthorized'}}
else
- .content-title
- span {{_ 'people'}}
+ .content-title.ext-box
+ .ext-box-left
+ span {{_ 'people'}}
+ input#searchInput(placeholder="{{_ 'search'}}")
+ button#searchButton {{_ 'search'}}
+ .ext-box-right
+ span {{_ 'people-number'}} #{peopleNumber}
.content-body
.side-menu
ul
@@ -27,6 +32,7 @@ template(name="peopleGeneral")
th {{_ 'verified'}}
th {{_ 'createdAt'}}
th {{_ 'active'}}
+ th {{_ 'authentication-method'}}
th
each user in peopleList
+peopleRow(userId=user._id)
@@ -52,6 +58,7 @@ template(name="peopleRow")
| {{_ 'no'}}
else
| {{_ 'yes'}}
+ td {{_ userData.authenticationMethod }}
td
a.edit-user
| {{_ 'edit'}}
@@ -66,12 +73,18 @@ template(name="editUserPopup")
| {{_ 'username'}}
span.error.hide.username-taken
| {{_ 'error-username-taken'}}
- input.js-profile-username(type="text" value=user.username)
+ if isLdap
+ input.js-profile-username(type="text" value=user.username readonly)
+ else
+ 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}}")
+ if isLdap
+ input.js-profile-email(type="email" value="{{user.emails.[0].address}}" readonly)
+ else
+ input.js-profile-email(type="email" value="{{user.emails.[0].address}}")
label
| {{_ 'admin'}}
select.select-role.js-profile-isadmin
@@ -82,9 +95,20 @@ template(name="editUserPopup")
select.select-active.js-profile-isactive
option(value="false") {{_ 'yes'}}
option(value="true" selected="{{user.loginDisabled}}") {{_ 'no'}}
+ label
+ | {{_ 'authentication-type'}}
+ select.select-authenticationMethod.js-authenticationMethod
+ each authentications
+ if isSelected value
+ option(value="{{value}}" selected) {{_ value}}
+ else
+ option(value="{{value}}") {{_ value}}
hr
label
| {{_ 'password'}}
input.js-profile-password(type="password")
+ div.buttonsContainer
+ input.primary.wide(type="submit" value="{{_ 'save'}}")
+ div
+ input#deleteButton.primary.wide(type="button" value="{{_ 'delete'}}")
- input.primary.wide(type="submit" value="{{_ 'save'}}")
diff --git a/client/components/settings/peopleBody.js b/client/components/settings/peopleBody.js
index 7cc992f2..83cf14fa 100644
--- a/client/components/settings/peopleBody.js
+++ b/client/components/settings/peopleBody.js
@@ -8,6 +8,8 @@ BlazeComponent.extendComponent({
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
this.people = new ReactiveVar(true);
+ this.findUsersOptions = new ReactiveVar({});
+ this.number = new ReactiveVar(0);
this.page = new ReactiveVar(1);
this.loadNextPageLocked = false;
@@ -26,6 +28,33 @@ BlazeComponent.extendComponent({
});
});
},
+ events() {
+ return [{
+ 'click #searchButton'() {
+ this.filterPeople();
+ },
+ 'keydown #searchInput'(event) {
+ if (event.keyCode === 13 && !event.shiftKey) {
+ this.filterPeople();
+ }
+ },
+ }];
+ },
+ filterPeople() {
+ const value = $('#searchInput').first().val();
+ if (value === '') {
+ this.findUsersOptions.set({});
+ } else {
+ const regex = new RegExp(value, 'i');
+ this.findUsersOptions.set({
+ $or: [
+ { username: regex },
+ { 'profile.fullname': regex },
+ { 'emails.address': regex },
+ ],
+ });
+ }
+ },
loadNextPage() {
if (this.loadNextPageLocked === false) {
this.page.set(this.page.get() + 1);
@@ -49,9 +78,14 @@ BlazeComponent.extendComponent({
this.loading.set(w);
},
peopleList() {
- return Users.find({}, {
+ const users = Users.find(this.findUsersOptions.get(), {
fields: {_id: true},
});
+ this.number.set(users.count());
+ return users;
+ },
+ peopleNumber() {
+ return this.number.get();
},
}).register('people');
@@ -62,10 +96,43 @@ Template.peopleRow.helpers({
},
});
+Template.editUserPopup.onCreated(function() {
+ this.authenticationMethods = new ReactiveVar([]);
+ this.errorMessage = new ReactiveVar('');
+
+ Meteor.call('getAuthenticationsEnabled', (_, result) => {
+ if (result) {
+ // TODO : add a management of different languages
+ // (ex {value: ldap, text: TAPi18n.__('ldap', {}, T9n.getLanguage() || 'en')})
+ this.authenticationMethods.set([
+ {value: 'password'},
+ // Gets only the authentication methods availables
+ ...Object.entries(result).filter((e) => e[1]).map((e) => ({value: e[0]})),
+ ]);
+ }
+ });
+});
+
Template.editUserPopup.helpers({
user() {
return Users.findOne(this.userId);
},
+ authentications() {
+ return Template.instance().authenticationMethods.get();
+ },
+ isSelected(match) {
+ const userId = Template.instance().data.userId;
+ const selected = Users.findOne(userId).authenticationMethod;
+ return selected === match;
+ },
+ isLdap() {
+ const userId = Template.instance().data.userId;
+ const selected = Users.findOne(userId).authenticationMethod;
+ return selected === 'ldap';
+ },
+ errorMessage() {
+ return Template.instance().errorMessage.get();
+ },
});
BlazeComponent.extendComponent({
@@ -91,6 +158,7 @@ Template.editUserPopup.events({
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 authentication = tpl.find('.js-authenticationMethod').value.trim();
const isChangePassword = password.length > 0;
const isChangeUserName = username !== user.username;
@@ -101,6 +169,7 @@ Template.editUserPopup.events({
'profile.fullname': fullname,
'isAdmin': isAdmin === 'true',
'loginDisabled': isActive === 'true',
+ 'authenticationMethod': authentication,
},
});
@@ -155,4 +224,9 @@ Template.editUserPopup.events({
});
} else Popup.close();
},
+
+ 'click #deleteButton'() {
+ Users.remove(this.userId);
+ Popup.close();
+ },
});
diff --git a/client/components/settings/peopleBody.styl b/client/components/settings/peopleBody.styl
index 84db44a7..80387611 100644
--- a/client/components/settings/peopleBody.styl
+++ b/client/components/settings/peopleBody.styl
@@ -13,3 +13,36 @@ table
tr:nth-child(even)
background-color: #dddddd;
+
+.ext-box
+ display: flex;
+ flex-direction: row;
+ height: 34px;
+
+ .ext-box-left
+ display: flex;
+ width: 40%
+
+ span
+ vertical-align: center;
+ line-height: 34px;
+ margin-right: 10px;
+
+ input, button
+ margin: 0 10px 0 0;
+ padding: 0;
+
+ button
+ min-width: 60px;
+
+.content-wrapper
+ margin-top: 10px
+
+.buttonsContainer
+ display: flex
+
+ input
+ margin: 0
+
+ div
+ margin: auto
diff --git a/client/components/settings/settingBody.jade b/client/components/settings/settingBody.jade
index 5bc7972d..89911e09 100644
--- a/client/components/settings/settingBody.jade
+++ b/client/components/settings/settingBody.jade
@@ -16,6 +16,8 @@ template(name="setting")
a.js-setting-menu(data-id="account-setting") {{_ 'accounts'}}
li
a.js-setting-menu(data-id="announcement-setting") {{_ 'admin-announcement'}}
+ li
+ a.js-setting-menu(data-id="layout-setting") {{_ 'layout'}}
.main-body
if loading.get
+spinner
@@ -27,6 +29,8 @@ template(name="setting")
+accountSettings
else if announcementSetting.get
+announcementSettings
+ else if layoutSetting.get
+ +layoutSettings
template(name="general")
ul#registration-setting.setting-detail
@@ -72,7 +76,7 @@ template(name='email')
li.smtp-form
.title {{_ 'smtp-password'}}
.form-group
- input.form-control#mail-server-password(type="text", placeholder="{{_ 'password'}}" value="{{currentSetting.mailServer.password}}")
+ input.form-control#mail-server-password(type="password", placeholder="{{_ 'password'}}" value="{{currentSetting.mailServer.password}}")
li.smtp-form
.title {{_ 'smtp-tls'}}
.form-group
@@ -127,3 +131,44 @@ template(name='announcementSettings')
textarea#admin-announcement.form-control= currentSetting.body
li
button.js-announcement-save.primary {{_ 'save'}}
+
+template(name='layoutSettings')
+ ul#layout-setting.setting-detail
+ //li.layout-form
+ .title {{_ 'hide-logo'}}
+ .form-group.flex
+ input.form-control#hide-logo(type="radio" name="hideLogo" value="true" checked="{{#if currentSetting.hideLogo}}checked{{/if}}")
+ span {{_ 'yes'}}
+ input.form-control#hide-logo(type="radio" name="hideLogo" value="false" checked="{{#unless currentSetting.hideLogo}}checked{{/unless}}")
+ span {{_ 'no'}}
+ li.layout-form
+ .title {{_ 'display-authentication-method'}}
+ .form-group.flex
+ input.form-control#display-authentication-method(type="radio" name="displayAuthenticationMethod" value="true" checked="{{#if currentSetting.displayAuthenticationMethod}}checked{{/if}}")
+ span {{_ 'yes'}}
+ input.form-control#display-authentication-method(type="radio" name="displayAuthenticationMethod" value="false" checked="{{#unless currentSetting.displayAuthenticationMethod}}checked{{/unless}}")
+ span {{_ 'no'}}
+ li.layout-form
+ .title {{_ 'default-authentication-method'}}
+ +selectAuthenticationMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod)
+ li.layout-form
+ .title {{_ 'custom-product-name'}}
+ .form-group
+ input.form-control#product-name(type="text", placeholder="" value="{{currentSetting.productName}}")
+ li.layout-form
+ .title {{_ 'add-custom-html-after-body-start'}}
+ textarea#customHTMLafterBodyStart.form-control= currentSetting.customHTMLafterBodyStart
+ li.layout-form
+ .title {{_ 'add-custom-html-before-body-end'}}
+ textarea#customHTMLbeforeBodyEnd.form-control= currentSetting.customHTMLbeforeBodyEnd
+ li
+ button.js-save-layout.primary {{_ 'save'}}
+
+
+template(name='selectAuthenticationMethod')
+ select#defaultAuthenticationMethod
+ each authentications
+ if isSelected value
+ option(value="{{value}}" selected) {{_ value}}
+ else
+ option(value="{{value}}") {{_ value}}
diff --git a/client/components/settings/settingBody.js b/client/components/settings/settingBody.js
index 7230d893..8279a092 100644
--- a/client/components/settings/settingBody.js
+++ b/client/components/settings/settingBody.js
@@ -6,6 +6,7 @@ BlazeComponent.extendComponent({
this.emailSetting = new ReactiveVar(false);
this.accountSetting = new ReactiveVar(false);
this.announcementSetting = new ReactiveVar(false);
+ this.layoutSetting = new ReactiveVar(false);
Meteor.subscribe('setting');
Meteor.subscribe('mailServer');
@@ -58,6 +59,12 @@ BlazeComponent.extendComponent({
toggleTLS() {
$('#mail-server-tls').toggleClass('is-checked');
},
+ toggleHideLogo() {
+ $('#hide-logo').toggleClass('is-checked');
+ },
+ toggleDisplayAuthenticationMethod() {
+ $('#display-authentication-method').toggleClass('is-checked');
+ },
switchMenu(event) {
const target = $(event.target);
if (!target.hasClass('active')) {
@@ -68,6 +75,7 @@ BlazeComponent.extendComponent({
this.emailSetting.set('email-setting' === targetID);
this.accountSetting.set('account-setting' === targetID);
this.announcementSetting.set('announcement-setting' === targetID);
+ this.layoutSetting.set('layout-setting' === targetID);
}
},
@@ -82,7 +90,7 @@ BlazeComponent.extendComponent({
},
inviteThroughEmail() {
- const emails = $('#email-to-invite').val().trim().split('\n').join(',').split(',');
+ const emails = $('#email-to-invite').val().toLowerCase().trim().split('\n').join(',').split(',');
const boardsToInvite = [];
$('.js-toggle-board-choose .materialCheckBox.is-checked').each(function () {
boardsToInvite.push($(this).data('id'));
@@ -129,19 +137,48 @@ BlazeComponent.extendComponent({
},
+ saveLayout() {
+ this.setLoading(true);
+ $('li').removeClass('has-error');
+
+ const productName = $('#product-name').val().trim();
+ const hideLogoChange = ($('input[name=hideLogo]:checked').val() === 'true');
+ const displayAuthenticationMethod = ($('input[name=displayAuthenticationMethod]:checked').val() === 'true');
+ const defaultAuthenticationMethod = $('#defaultAuthenticationMethod').val();
+ const customHTMLafterBodyStart = $('#customHTMLafterBodyStart').val().trim();
+ const customHTMLbeforeBodyEnd = $('#customHTMLbeforeBodyEnd').val().trim();
+
+ try {
+ Settings.update(Settings.findOne()._id, {
+ $set: {
+ productName,
+ hideLogo: hideLogoChange,
+ customHTMLafterBodyStart,
+ customHTMLbeforeBodyEnd,
+ displayAuthenticationMethod,
+ defaultAuthenticationMethod,
+ },
+ });
+ } catch (e) {
+ return;
+ } finally {
+ this.setLoading(false);
+ }
+
+ DocHead.setTitle(productName);
+
+ },
+
sendSMTPTestEmail() {
Meteor.call('sendSMTPTestEmail', (err, ret) => {
- if (!err && ret) { /* eslint-disable no-console */
+ if (!err && ret) {
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 */
});
},
@@ -154,6 +191,9 @@ BlazeComponent.extendComponent({
'click button.js-email-invite': this.inviteThroughEmail,
'click button.js-save': this.saveMailServerInfo,
'click button.js-send-smtp-test-email': this.sendSMTPTestEmail,
+ 'click a.js-toggle-hide-logo': this.toggleHideLogo,
+ 'click button.js-save-layout': this.saveLayout,
+ 'click a.js-toggle-display-authentication-method': this.toggleDisplayAuthenticationMethod,
}];
},
}).register('setting');
@@ -226,3 +266,29 @@ BlazeComponent.extendComponent({
}];
},
}).register('announcementSettings');
+
+
+Template.selectAuthenticationMethod.onCreated(function() {
+ this.authenticationMethods = new ReactiveVar([]);
+
+ Meteor.call('getAuthenticationsEnabled', (_, result) => {
+ if (result) {
+ // TODO : add a management of different languages
+ // (ex {value: ldap, text: TAPi18n.__('ldap', {}, T9n.getLanguage() || 'en')})
+ this.authenticationMethods.set([
+ {value: 'password'},
+ // Gets only the authentication methods availables
+ ...Object.entries(result).filter((e) => e[1]).map((e) => ({value: e[0]})),
+ ]);
+ }
+ });
+});
+
+Template.selectAuthenticationMethod.helpers({
+ authentications() {
+ return Template.instance().authenticationMethods.get();
+ },
+ isSelected(match) {
+ return Template.instance().data.authenticationMethod === match;
+ },
+});
diff --git a/client/components/settings/settingBody.styl b/client/components/settings/settingBody.styl
index fec64cee..dbf91a6c 100644
--- a/client/components/settings/settingBody.styl
+++ b/client/components/settings/settingBody.styl
@@ -52,6 +52,10 @@
.main-body
padding: 0.1em 1em
+ -webkit-user-select: auto // Safari 3.1+
+ -moz-user-select: auto // Firefox 2+
+ -ms-user-select: auto // IE 10+
+ user-select: auto // Standard syntax
ul
li
@@ -66,7 +70,8 @@
padding: 0 0.5rem
.admin-announcement,
- .invite-people
+ .invite-people,
+ .layout
padding-left 20px;
li
min-width: 500px;
diff --git a/client/components/settings/settingHeader.jade b/client/components/settings/settingHeader.jade
index c2d4db3a..221c1b79 100644
--- a/client/components/settings/settingHeader.jade
+++ b/client/components/settings/settingHeader.jade
@@ -4,22 +4,21 @@ template(name="settingHeaderBar")
.setting-header-btns.left
unless isMiniScreen
- unless isSandstorm
- if currentUser
- a.setting-header-btn.settings(href="{{pathFor 'setting'}}")
- i.fa(class="fa-cog")
- span {{_ 'settings'}}
+ if currentUser
+ 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.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'}}
+ a.setting-header-btn.informations(href="{{pathFor 'information'}}")
+ i.fa(class="fa-info-circle")
+ span {{_ 'info'}}
- else
- a.setting-header-btn.js-log-in(
- title="{{_ 'log-in'}}")
- i.fa.fa-sign-in
- span {{_ 'log-in'}}
+ else
+ a.setting-header-btn.js-log-in(
+ title="{{_ 'log-in'}}")
+ i.fa.fa-sign-in
+ span {{_ 'log-in'}}
diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade
index 6085c2ad..4e4d355c 100644
--- a/client/components/sidebar/sidebar.jade
+++ b/client/components/sidebar/sidebar.jade
@@ -1,9 +1,9 @@
template(name="sidebar")
.board-sidebar.sidebar(class="{{#if isOpen}}is-open{{/if}}")
- a.sidebar-tongue.js-toggle-sidebar(
- class="{{#if isTongueHidden}}is-hidden{{/if}}",
- title="{{showTongueTitle}}")
- i.fa.fa-angle-left
+ //a.sidebar-tongue.js-toggle-sidebar(
+ // class="{{#if isTongueHidden}}is-hidden{{/if}}",
+ // title="{{showTongueTitle}}")
+ // i.fa.fa-navicon
.sidebar-shadow
.sidebar-content.sidebar-shortcuts
a.board-header-btn.js-shortcuts
@@ -11,7 +11,7 @@ template(name="sidebar")
span {{_ 'keyboard-shortcuts' }}
.sidebar-content.js-board-sidebar-content.js-perfect-scrollbar
a.hide-btn.js-hide-sidebar
- i.fa.fa-angle-right
+ i.fa.fa-navicon
unless isDefaultView
h2
a.fa.fa-chevron-left.js-back-home
@@ -23,16 +23,20 @@ template(name='homeSidebar')
hr
+labelsWidget
hr
- h3
- i.fa.fa-comments-o
- | {{_ 'activities'}}
- +activities(mode="board")
+ unless currentUser.isNoComments
+ h3
+ i.fa.fa-comments-o
+ | {{_ 'activities'}}
+ +activities(mode="board")
template(name="membersWidget")
.board-widget.board-widget-members
h3
i.fa.fa-user
| {{_ 'members'}}
+ a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}").right
+ i.board-header-btn-icon.fa.fa-cog
+
.board-widget-content
each currentBoard.activeMembers
+userAvatar(userId=this.userId showStatus=true)
@@ -52,6 +56,130 @@ template(name="membersWidget")
button.js-member-invite-accept.primary {{_ 'accept'}}
button.js-member-invite-decline {{_ 'decline'}}
+template(name="boardChangeColorPopup")
+ .board-backgrounds-list.clearfix
+ each backgroundColors
+ .board-background-select.js-select-background
+ span.background-box(class="board-color-{{this}}")
+ if isSelected
+ i.fa.fa-check
+
+template(name="boardSubtaskSettingsPopup")
+ form.board-subtask-settings
+ h3 {{_ 'show-parent-in-minicard'}}
+ a#prefix-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
+ .materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
+ span {{_ 'prefix-with-full-path'}}
+ a#prefix-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
+ .materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
+ span {{_ 'prefix-with-parent'}}
+ a#subtext-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
+ .materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
+ span {{_ 'subtext-with-full-path'}}
+ a#subtext-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
+ .materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
+ span {{_ 'subtext-with-parent'}}
+ a#no-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
+ .materialCheckBox(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
+ span {{_ 'no-parent'}}
+ div
+ hr
+
+ div.check-div
+ a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}")
+ .materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}")
+ span {{_ 'show-subtasks-field'}}
+
+ label
+ | {{_ 'deposit-subtasks-board'}}
+ select.js-field-deposit-board(disabled="{{#unless allowsSubtasks}}disabled{{/unless}}")
+ each boards
+ if isBoardSelected
+ option(value=_id selected="selected") {{title}}
+ else
+ option(value=_id) {{title}}
+ if isNullBoardSelected
+ option(value='null' selected="selected") {{_ 'custom-field-dropdown-none'}}
+ else
+ option(value='null') {{_ 'custom-field-dropdown-none'}}
+ div
+ hr
+
+ label
+ | {{_ 'deposit-subtasks-list'}}
+ select.js-field-deposit-list(disabled="{{#unless hasLists}}disabled{{/unless}}")
+ each lists
+ if isListSelected
+ option(value=_id selected="selected") {{title}}
+ else
+ option(value=_id) {{title}}
+
+template(name="chooseBoardSource")
+ ul.pop-over-list
+ li
+ a(href="{{pathFor '/import/trello'}}") {{_ 'from-trello'}}
+ li
+ a(href="{{pathFor '/import/wekan'}}") {{_ 'from-wekan'}}
+
+template(name="archiveBoardPopup")
+ p {{_ 'close-board-pop'}}
+ button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
+
+template(name="outgoingWebhooksPopup")
+ each integrations
+ form.integration-form
+ if title
+ h4 {{title}}
+ else
+ h4 {{_ 'no-name'}}
+ label
+ | URL
+ input.js-outgoing-webhooks-url(type="text" name="url" value=url)
+ input(type="hidden" value=_id name="id")
+ input.primary.wide(type="submit" value="{{_ 'save'}}")
+ form.integration-form
+ h4
+ | {{_ 'new-outgoing-webhook'}}
+ label
+ | URL
+ input.js-outgoing-webhooks-url(type="text" name="url" autofocus)
+ input.primary.wide(type="submit" value="{{_ 'save'}}")
+
+template(name="boardMenuPopup")
+ ul.pop-over-list
+ li: a.js-custom-fields {{_ 'custom-fields'}}
+ li: a.js-open-archives {{_ 'archived-items'}}
+ if currentUser.isBoardAdmin
+ li: a.js-change-board-color {{_ 'board-change-color'}}
+ //-
+ XXX Language should be handled by sandstorm, but for now display a
+ language selection link in the board menu. This link is normally present
+ in the header bar that is not displayed on sandstorm.
+ if isSandstorm
+ li: a.js-change-language {{_ 'language'}}
+ unless isSandstorm
+ if currentUser.isBoardAdmin
+ hr
+ ul.pop-over-list
+ li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
+ unless currentBoard.isTemplatesBoard
+ li: a.js-archive-board {{_ 'archive-board'}}
+ li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
+ hr
+ ul.pop-over-list
+ li: a.js-subtask-settings {{_ 'subtask-settings'}}
+
+ if isSandstorm
+ hr
+ ul.pop-over-list
+ li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
+ li: a.js-import-board {{_ 'import-board-c'}}
+ li: a.js-archive-board {{_ 'archive-board'}}
+ li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
+ hr
+ ul.pop-over-list
+ li: a.js-subtask-settings {{_ 'subtask-settings'}}
+
template(name="labelsWidget")
.board-widget.board-widget-labels
h3
@@ -82,17 +210,16 @@ template(name="memberPopup")
ul.pop-over-list
li
a.js-filter-member {{_ 'filter-cards'}}
- unless isSandstorm
- if currentUser.isBoardAdmin
- li
- a.js-change-role
- | {{_ 'change-permissions'}}
- span.quiet (#{memberType})
+ if currentUser.isBoardAdmin
li
- if $eq currentUser._id userId
- a.js-leave-member {{_ 'leave-board'}}
- else if currentUser.isBoardAdmin
- a.js-remove-member {{_ 'remove-from-board'}}
+ a.js-change-role
+ | {{_ 'change-permissions'}}
+ span.quiet (#{memberType})
+ 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")
@@ -146,6 +273,12 @@ template(name="changePermissionsPopup")
i.fa.fa-check
span.sub-name {{_ 'normal-desc'}}
li
+ a(class="{{#if isLastAdmin}}disabled{{else}}js-set-no-comments{{/if}}")
+ | {{_ 'no-comments'}}
+ if isNoComments
+ i.fa.fa-check
+ span.sub-name {{_ 'no-comments-desc'}}
+ li
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-comment-only{{/if}}")
| {{_ 'comment-only'}}
if isCommentOnly
diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js
index bff96dcb..e8de3c96 100644
--- a/client/components/sidebar/sidebar.js
+++ b/client/components/sidebar/sidebar.js
@@ -6,6 +6,7 @@ const viewTitles = {
filter: 'filter-cards',
search: 'search-cards',
multiselection: 'multi-selection',
+ customFields: 'custom-fields',
archives: 'archives',
};
@@ -15,8 +16,7 @@ BlazeComponent.extendComponent({
},
onCreated() {
- const initOpen = Utils.isMiniScreen() ? false : (!Session.get('currentCard'));
- this._isOpen = new ReactiveVar(initOpen);
+ this._isOpen = new ReactiveVar(false);
this._view = new ReactiveVar(defaultView);
Sidebar = this;
},
@@ -125,8 +125,11 @@ Template.memberPopup.helpers({
if(type === 'normal'){
const currentBoard = Boards.findOne(Session.get('currentBoard'));
const commentOnly = currentBoard.hasCommentOnly(this.userId);
+ const noComments = currentBoard.hasNoComments(this.userId);
if(commentOnly){
return TAPi18n.__('comment-only').toLowerCase();
+ } else if(noComments) {
+ return TAPi18n.__('no-comments').toLowerCase();
} else {
return TAPi18n.__(type).toLowerCase();
}
@@ -139,6 +142,52 @@ Template.memberPopup.helpers({
},
});
+Template.boardMenuPopup.events({
+ 'click .js-rename-board': Popup.open('boardChangeTitle'),
+ 'click .js-custom-fields'() {
+ Sidebar.setView('customFields');
+ Popup.close();
+ },
+ 'click .js-open-archives'() {
+ Sidebar.setView('archives');
+ Popup.close();
+ },
+ 'click .js-change-board-color': Popup.open('boardChangeColor'),
+ 'click .js-change-language': Popup.open('changeLanguage'),
+ 'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ currentBoard.archive();
+ // XXX We should have some kind of notification on top of the page to
+ // confirm that the board was successfully archived.
+ FlowRouter.go('home');
+ }),
+ 'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ Popup.close();
+ Boards.remove(currentBoard._id);
+ FlowRouter.go('home');
+ }),
+ 'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
+ 'click .js-import-board': Popup.open('chooseBoardSource'),
+ 'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
+});
+
+Template.boardMenuPopup.helpers({
+ exportUrl() {
+ const params = {
+ boardId: Session.get('currentBoard'),
+ };
+ const queryParams = {
+ authToken: Accounts._storedLoginToken(),
+ };
+ return FlowRouter.path('/api/boards/:boardId/export', params, queryParams);
+ },
+ exportFilename() {
+ const boardId = Session.get('currentBoard');
+ return `wekan-export-board-${boardId}.json`;
+ },
+});
+
Template.memberPopup.events({
'click .js-filter-member'() {
Filter.members.toggle(this.userId);
@@ -187,7 +236,14 @@ Template.membersWidget.helpers({
Template.membersWidget.events({
'click .js-member': Popup.open('member'),
+ 'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-manage-board-members': Popup.open('addMember'),
+ 'click .js-import': Popup.open('boardImportBoard'),
+ submit: this.onSubmit,
+ 'click .js-import-board': Popup.open('chooseBoardSource'),
+ 'click .js-open-archived-board'() {
+ Modal.open('archivedBoards');
+ },
'click .sandstorm-powerbox-request-identity'() {
window.sandstormRequestIdentity();
},
@@ -206,6 +262,59 @@ Template.membersWidget.events({
},
});
+BlazeComponent.extendComponent({
+ integrations() {
+ const boardId = Session.get('currentBoard');
+ return Integrations.find({ boardId: `${boardId}` }).fetch();
+ },
+
+ integration(id) {
+ const boardId = Session.get('currentBoard');
+ return Integrations.findOne({ _id: id, boardId: `${boardId}` });
+ },
+
+ events() {
+ return [{
+ 'submit'(evt) {
+ evt.preventDefault();
+ const url = evt.target.url.value;
+ const boardId = Session.get('currentBoard');
+ let id = null;
+ let integration = null;
+ if (evt.target.id) {
+ id = evt.target.id.value;
+ integration = this.integration(id);
+ if (url) {
+ Integrations.update(integration._id, {
+ $set: {
+ url: `${url}`,
+ },
+ });
+ } else {
+ Integrations.remove(integration._id);
+ }
+ } else if (url) {
+ Integrations.insert({
+ userId: Meteor.userId(),
+ enabled: true,
+ type: 'outgoing-webhooks',
+ url: `${url}`,
+ boardId: `${boardId}`,
+ activities: ['all'],
+ });
+ }
+ Popup.close();
+ },
+ }];
+ },
+}).register('outgoingWebhooksPopup');
+
+BlazeComponent.extendComponent({
+ template() {
+ return 'chooseBoardSource';
+ },
+}).register('chooseBoardSourcePopup');
+
Template.labelsWidget.events({
'click .js-label': Popup.open('editLabel'),
'click .js-add-label': Popup.open('createLabel'),
@@ -256,6 +365,124 @@ Template.membersWidget.onRendered(draggableMembersLabelsWidgets);
Template.labelsWidget.onRendered(draggableMembersLabelsWidgets);
BlazeComponent.extendComponent({
+ backgroundColors() {
+ return Boards.simpleSchema()._schema.color.allowedValues;
+ },
+
+ isSelected() {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ return currentBoard.color === this.currentData().toString();
+ },
+
+ events() {
+ return [{
+ 'click .js-select-background'(evt) {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ const newColor = this.currentData().toString();
+ currentBoard.setColor(newColor);
+ evt.preventDefault();
+ },
+ }];
+ },
+}).register('boardChangeColorPopup');
+
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.currentBoard = Boards.findOne(Session.get('currentBoard'));
+ },
+
+ allowsSubtasks() {
+ return this.currentBoard.allowsSubtasks;
+ },
+
+ isBoardSelected() {
+ return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
+ },
+
+ isNullBoardSelected() {
+ return (this.currentBoard.subtasksDefaultBoardId === null) || (this.currentBoard.subtasksDefaultBoardId === undefined);
+ },
+
+ boards() {
+ return Boards.find({
+ archived: false,
+ 'members.userId': Meteor.userId(),
+ }, {
+ sort: ['title'],
+ });
+ },
+
+ lists() {
+ return Lists.find({
+ boardId: this.currentBoard._id,
+ archived: false,
+ }, {
+ sort: ['title'],
+ });
+ },
+
+ hasLists() {
+ return this.lists().count() > 0;
+ },
+
+ isListSelected() {
+ return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
+ },
+
+ presentParentTask() {
+ let result = this.currentBoard.presentParentTask;
+ if ((result === null) || (result === undefined)) {
+ result = 'no-parent';
+ }
+ return result;
+ },
+
+ events() {
+ return [{
+ 'click .js-field-has-subtasks'(evt) {
+ evt.preventDefault();
+ this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks;
+ this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks);
+ $('.js-field-has-subtasks .materialCheckBox').toggleClass('is-checked', this.currentBoard.allowsSubtasks);
+ $('.js-field-has-subtasks').toggleClass('is-checked', this.currentBoard.allowsSubtasks);
+ $('.js-field-deposit-board').prop('disabled', !this.currentBoard.allowsSubtasks);
+ },
+ 'change .js-field-deposit-board'(evt) {
+ let value = evt.target.value;
+ if (value === 'null') {
+ value = null;
+ }
+ this.currentBoard.setSubtasksDefaultBoardId(value);
+ evt.preventDefault();
+ },
+ 'change .js-field-deposit-list'(evt) {
+ this.currentBoard.setSubtasksDefaultListId(evt.target.value);
+ evt.preventDefault();
+ },
+ 'click .js-field-show-parent-in-minicard'(evt) {
+ const value = evt.target.id || $(evt.target).parent()[0].id || $(evt.target).parent()[0].parent()[0].id;
+ const options = [
+ 'prefix-with-full-path',
+ 'prefix-with-parent',
+ 'subtext-with-full-path',
+ 'subtext-with-parent',
+ 'no-parent'];
+ options.forEach(function(element) {
+ if (element !== value) {
+ $(`#${element} .materialCheckBox`).toggleClass('is-checked', false);
+ $(`#${element}`).toggleClass('is-checked', false);
+ }
+ });
+ $(`#${value} .materialCheckBox`).toggleClass('is-checked', true);
+ $(`#${value}`).toggleClass('is-checked', true);
+ this.currentBoard.setPresentParentTask(value);
+ evt.preventDefault();
+ },
+ }];
+ },
+}).register('boardSubtaskSettingsPopup');
+
+BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
@@ -323,12 +550,13 @@ BlazeComponent.extendComponent({
}).register('addMemberPopup');
Template.changePermissionsPopup.events({
- 'click .js-set-admin, click .js-set-normal, click .js-set-comment-only'(event) {
+ 'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only'(event) {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
const memberId = this.userId;
const isAdmin = $(event.currentTarget).hasClass('js-set-admin');
const isCommentOnly = $(event.currentTarget).hasClass('js-set-comment-only');
- currentBoard.setMemberPermission(memberId, isAdmin, isCommentOnly);
+ const isNoComments = $(event.currentTarget).hasClass('js-set-no-comments');
+ currentBoard.setMemberPermission(memberId, isAdmin, isNoComments, isCommentOnly);
Popup.back(1);
},
});
@@ -341,7 +569,12 @@ Template.changePermissionsPopup.helpers({
isNormal() {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
- return !currentBoard.hasAdmin(this.userId) && !currentBoard.hasCommentOnly(this.userId);
+ return !currentBoard.hasAdmin(this.userId) && !currentBoard.hasNoComments(this.userId) && !currentBoard.hasCommentOnly(this.userId);
+ },
+
+ isNoComments() {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ return !currentBoard.hasAdmin(this.userId) && currentBoard.hasNoComments(this.userId);
},
isCommentOnly() {
diff --git a/client/components/sidebar/sidebar.styl b/client/components/sidebar/sidebar.styl
index 8f2f493e..740186b5 100644
--- a/client/components/sidebar/sidebar.styl
+++ b/client/components/sidebar/sidebar.styl
@@ -45,28 +45,45 @@
display: flex
flex-direction: column
- li > a
- display: flex
- height: 30px
- margin: 0
- padding: 4px
- border-radius: 3px
- align-items: center
-
- &:hover
- &, i, .quiet
- color white
-
- .member, .card-label
- margin-right: 7px
- margin-top: 5px
-
- .sidebar-list-item-description
- flex: 1
- overflow: ellipsis
-
- .fa.fa-check
- margin: 0 4px
+ li
+ & > a
+ display: flex
+ height: 30px
+ margin: 0
+ padding: 4px
+ border-radius: 3px
+ align-items: center
+
+ &:hover
+ &, i, .quiet
+ color white
+
+ .member, .card-label
+ margin-right: 7px
+ margin-top: 5px
+
+ .minicard-edit-button
+ float: right
+ padding: 8px
+ border-radius: 3px
+
+ .sidebar-list-item-description
+ flex: 1
+ overflow: ellipsis
+
+ .fa.fa-check
+ margin: 0 4px
+
+ .minicard
+ padding: 6px 8px 4px
+
+ .minicard-edit-button
+ float: right
+ padding: 4px
+ border-radius: 3px
+
+ &:hover
+ background: #dbdbdb
.sidebar-btn
display: block
diff --git a/client/components/sidebar/sidebarArchives.jade b/client/components/sidebar/sidebarArchives.jade
index ee6cac01..e2f3e395 100644
--- a/client/components/sidebar/sidebarArchives.jade
+++ b/client/components/sidebar/sidebarArchives.jade
@@ -2,6 +2,10 @@ template(name="archivesSidebar")
+basicTabs(tabs=tabs)
+tabContent(slug="cards")
+ p.quiet
+ a.js-restore-all-cards {{_ 'restore-all'}}
+ | -
+ a.js-delete-all-cards {{_ 'delete-all'}}
each archivedCards
.minicard-wrapper.js-minicard
+minicard(this)
@@ -16,23 +20,35 @@ template(name="archivesSidebar")
p.no-items-message {{_ 'no-archived-cards'}}
+tabContent(slug="lists")
+ p.quiet
+ a.js-restore-all-lists {{_ 'restore-all'}}
+ | -
+ a.js-delete-all-lists {{_ 'delete-all'}}
ul.archived-lists
each archivedLists
li.archived-lists-item
- if currentUser.isBoardMember
- button.js-restore-list
- i.fa.fa-undo
= title
+ if currentUser.isBoardMember
+ p.quiet
+ a.js-restore-list {{_ 'restore'}}
+ | -
+ a.js-delete-list {{_ 'delete'}}
else
li.no-items-message {{_ 'no-archived-lists'}}
+tabContent(slug="swimlanes")
+ p.quiet
+ a.js-restore-all-swimlanes {{_ 'restore-all'}}
+ | -
+ a.js-delete-all-swimlanes {{_ 'delete-all'}}
ul.archived-lists
each archivedSwimlanes
li.archived-lists-item
- if currentUser.isBoardMember
- button.js-restore-swimlane
- i.fa.fa-undo
= title
+ if currentUser.isBoardMember
+ p.quiet
+ a.js-restore-swimlane {{_ 'restore'}}
+ | -
+ a.js-delete-swimlane {{_ 'delete'}}
else
li.no-items-message {{_ 'no-archived-swimlanes'}}
diff --git a/client/components/sidebar/sidebarArchives.js b/client/components/sidebar/sidebarArchives.js
index 6102bf11..b50043fd 100644
--- a/client/components/sidebar/sidebarArchives.js
+++ b/client/components/sidebar/sidebarArchives.js
@@ -44,19 +44,67 @@ BlazeComponent.extendComponent({
card.restore();
}
},
+ 'click .js-restore-all-cards'() {
+ this.archivedCards().forEach((card) => {
+ if(card.canBeRestored()){
+ card.restore();
+ }
+ });
+ },
+
'click .js-delete-card': Popup.afterConfirm('cardDelete', function() {
const cardId = this._id;
Cards.remove(cardId);
Popup.close();
}),
+ 'click .js-delete-all-cards': Popup.afterConfirm('cardDelete', () => {
+ this.archivedCards().forEach((card) => {
+ Cards.remove(card._id);
+ });
+ Popup.close();
+ }),
+
'click .js-restore-list'() {
const list = this.currentData();
list.restore();
},
+ 'click .js-restore-all-lists'() {
+ this.archivedLists().forEach((list) => {
+ list.restore();
+ });
+ },
+
+ 'click .js-delete-list': Popup.afterConfirm('listDelete', function() {
+ this.remove();
+ Popup.close();
+ }),
+ 'click .js-delete-all-lists': Popup.afterConfirm('listDelete', () => {
+ this.archivedLists().forEach((list) => {
+ list.remove();
+ });
+ Popup.close();
+ }),
+
'click .js-restore-swimlane'() {
const swimlane = this.currentData();
swimlane.restore();
},
+ 'click .js-restore-all-swimlanes'() {
+ this.archivedSwimlanes().forEach((swimlane) => {
+ swimlane.restore();
+ });
+ },
+
+ 'click .js-delete-swimlane': Popup.afterConfirm('swimlaneDelete', function() {
+ this.remove();
+ Popup.close();
+ }),
+ 'click .js-delete-all-swimlanes': Popup.afterConfirm('swimlaneDelete', () => {
+ this.archivedSwimlanes().forEach((swimlane) => {
+ swimlane.remove();
+ });
+ Popup.close();
+ }),
}];
},
}).register('archivesSidebar');
diff --git a/client/components/sidebar/sidebarCustomFields.jade b/client/components/sidebar/sidebarCustomFields.jade
new file mode 100644
index 00000000..f0a17773
--- /dev/null
+++ b/client/components/sidebar/sidebarCustomFields.jade
@@ -0,0 +1,62 @@
+template(name="customFieldsSidebar")
+ ul.sidebar-list
+ each customFields
+ li
+ div.minicard-wrapper.js-minicard
+ div.minicard
+ a.fa.fa-pencil.js-edit-custom-field.minicard-edit-button
+ div.minicard-title
+ | {{ name }} ({{ type }})
+
+ if currentUser.isBoardMember
+ hr
+ a.sidebar-btn.js-open-create-custom-field
+ i.fa.fa-plus
+ span {{_ 'createCustomField'}}
+
+template(name="createCustomFieldPopup")
+ form
+ label
+ | {{_ 'name'}}
+ unless _id
+ input.js-field-name(type="text" autofocus)
+ else
+ input.js-field-name(type="text" value=name)
+
+ label
+ | {{_ 'type'}}
+ select.js-field-type(disabled="{{#if _id}}disabled{{/if}}")
+ each types
+ if selected
+ option(value=value selected="selected") {{name}}
+ else
+ option(value=value) {{name}}
+ div.js-field-settings.js-field-settings-dropdown(class="{{#if isTypeNotSelected 'dropdown'}}hide{{/if}}")
+ label
+ | {{_ 'custom-field-dropdown-options'}}
+ each dropdownItems.get
+ input.js-dropdown-item(type="text" value=name placeholder="")
+ input.js-dropdown-item.last(type="text" value="" placeholder="{{_ 'custom-field-dropdown-options-placeholder'}}")
+ a.flex.js-field-show-on-card(class="{{#if showOnCard}}is-checked{{/if}}")
+ .materialCheckBox(class="{{#if showOnCard}}is-checked{{/if}}")
+
+ span {{_ 'show-field-on-card'}}
+ a.flex.js-field-automatically-on-card(class="{{#if automaticallyOnCard}}is-checked{{/if}}")
+ .materialCheckBox(class="{{#if automaticallyOnCard}}is-checked{{/if}}")
+
+ span {{_ 'automatically-field-on-card'}}
+
+ a.flex.js-field-showLabel-on-card(class="{{#if showLabelOnMiniCard}}is-checked{{/if}}")
+ .materialCheckBox(class="{{#if showLabelOnMiniCard}}is-checked{{/if}}")
+
+ span {{_ 'showLabel-field-on-card'}}
+
+ button.primary.wide.left(type="button")
+ | {{_ 'save'}}
+ if _id
+ button.negate.wide.right.js-delete-custom-field(type="button")
+ | {{_ 'delete'}}
+
+template(name="deleteCustomFieldPopup")
+ p {{_ "custom-field-delete-pop"}}
+ button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
diff --git a/client/components/sidebar/sidebarCustomFields.js b/client/components/sidebar/sidebarCustomFields.js
new file mode 100644
index 00000000..28af973b
--- /dev/null
+++ b/client/components/sidebar/sidebarCustomFields.js
@@ -0,0 +1,157 @@
+BlazeComponent.extendComponent({
+
+ customFields() {
+ return CustomFields.find({
+ boardIds: {$in: [Session.get('currentBoard')]},
+ });
+ },
+
+ events() {
+ return [{
+ 'click .js-open-create-custom-field': Popup.open('createCustomField'),
+ 'click .js-edit-custom-field': Popup.open('editCustomField'),
+ }];
+ },
+
+}).register('customFieldsSidebar');
+
+const CreateCustomFieldPopup = BlazeComponent.extendComponent({
+
+ _types: ['text', 'number', 'date', 'dropdown'],
+
+ onCreated() {
+ this.type = new ReactiveVar((this.data().type) ? this.data().type : this._types[0]);
+ this.dropdownItems = new ReactiveVar((this.data().settings && this.data().settings.dropdownItems) ? this.data().settings.dropdownItems : []);
+ },
+
+ types() {
+ const currentType = this.data().type;
+ return this._types.
+ map((type) => {return {
+ value: type,
+ name: TAPi18n.__(`custom-field-${type}`),
+ selected: type === currentType,
+ };});
+ },
+
+ isTypeNotSelected(type) {
+ return this.type.get() !== type;
+ },
+
+ getDropdownItems() {
+ const items = this.dropdownItems.get();
+ Array.from(this.findAll('.js-field-settings-dropdown input')).forEach((el, index) => {
+ //console.log('each item!', index, el.value);
+ if (!items[index]) items[index] = {
+ _id: Random.id(6),
+ };
+ items[index].name = el.value.trim();
+ });
+ return items;
+ },
+
+ getSettings() {
+ const settings = {};
+ switch (this.type.get()) {
+ case 'dropdown': {
+ const dropdownItems = this.getDropdownItems().filter((item) => !!item.name.trim());
+ settings.dropdownItems = dropdownItems;
+ break;
+ }
+ }
+ return settings;
+ },
+
+ events() {
+ return [{
+ 'change .js-field-type'(evt) {
+ const value = evt.target.value;
+ this.type.set(value);
+ },
+ 'keydown .js-dropdown-item.last'(evt) {
+ if (evt.target.value.trim() && evt.keyCode === 13) {
+ const items = this.getDropdownItems();
+ this.dropdownItems.set(items);
+ evt.target.value = '';
+ }
+ },
+ 'click .js-field-show-on-card'(evt) {
+ let $target = $(evt.target);
+ if(!$target.hasClass('js-field-show-on-card')){
+ $target = $target.parent();
+ }
+ $target.find('.materialCheckBox').toggleClass('is-checked');
+ $target.toggleClass('is-checked');
+ },
+ 'click .js-field-automatically-on-card'(evt) {
+ let $target = $(evt.target);
+ if(!$target.hasClass('js-field-automatically-on-card')){
+ $target = $target.parent();
+ }
+ $target.find('.materialCheckBox').toggleClass('is-checked');
+ $target.toggleClass('is-checked');
+ },
+ 'click .js-field-showLabel-on-card'(evt) {
+ let $target = $(evt.target);
+ if(!$target.hasClass('js-field-showLabel-on-card')){
+ $target = $target.parent();
+ }
+ $target.find('.materialCheckBox').toggleClass('is-checked');
+ $target.toggleClass('is-checked');
+ },
+ 'click .primary'(evt) {
+ evt.preventDefault();
+
+ const data = {
+ name: this.find('.js-field-name').value.trim(),
+ type: this.type.get(),
+ settings: this.getSettings(),
+ showOnCard: this.find('.js-field-show-on-card.is-checked') !== null,
+ showLabelOnMiniCard: this.find('.js-field-showLabel-on-card.is-checked') !== null,
+ automaticallyOnCard: this.find('.js-field-automatically-on-card.is-checked') !== null,
+ };
+
+ // insert or update
+ if (!this.data()._id) {
+ data.boardIds = [Session.get('currentBoard')];
+ CustomFields.insert(data);
+ } else {
+ CustomFields.update(this.data()._id, {$set: data});
+ }
+
+ Popup.back();
+ },
+ 'click .js-delete-custom-field': Popup.afterConfirm('deleteCustomField', function() {
+ const customField = CustomFields.findOne(this._id);
+ if (customField.boardIds.length > 1) {
+ CustomFields.update(customField._id, {
+ $pull: {
+ boardIds: Session.get('currentBoard'),
+ },
+ });
+ } else {
+ CustomFields.remove(customField._id);
+ }
+ Popup.close();
+ }),
+ }];
+ },
+
+});
+CreateCustomFieldPopup.register('createCustomFieldPopup');
+
+(class extends CreateCustomFieldPopup {
+
+ template() {
+ return 'createCustomFieldPopup';
+ }
+
+}).register('editCustomFieldPopup');
+
+/*Template.deleteCustomFieldPopup.events({
+ 'submit'(evt) {
+ const customFieldId = this._id;
+ CustomFields.remove(customFieldId);
+ Popup.close();
+ }
+});*/
diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade
index 273df8c2..f11528b1 100644
--- a/client/components/sidebar/sidebarFilters.jade
+++ b/client/components/sidebar/sidebarFilters.jade
@@ -40,6 +40,25 @@ template(name="filterSidebar")
| (<span class="username">{{ username }}</span>)
if Filter.members.isSelected _id
i.fa.fa-check
+ hr
+ ul.sidebar-list
+ li(class="{{#if Filter.customFields.isSelected undefined}}active{{/if}}")
+ a.name.js-toggle-custom-fields-filter
+ span.sidebar-list-item-description
+ | {{_ 'filter-no-custom-fields'}}
+ if Filter.customFields.isSelected undefined
+ i.fa.fa-check
+ each currentBoard.customFields
+ li(class="{{#if Filter.customFields.isSelected _id}}active{{/if}}")
+ a.name.js-toggle-custom-fields-filter
+ span.sidebar-list-item-description
+ | {{ name }}
+ if Filter.customFields.isSelected _id
+ i.fa.fa-check
+ hr
+ span {{_ 'advanced-filter-label'}}
+ input.js-field-advanced-filter(type="text")
+ span {{_ 'advanced-filter-description'}}
if Filter.isActive
hr
a.sidebar-btn.js-clear-all
diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js
index f02d3a4a..fd8229e4 100644
--- a/client/components/sidebar/sidebarFilters.js
+++ b/client/components/sidebar/sidebarFilters.js
@@ -11,6 +11,16 @@ BlazeComponent.extendComponent({
Filter.members.toggle(this.currentData()._id);
Filter.resetExceptions();
},
+ 'click .js-toggle-custom-fields-filter'(evt) {
+ evt.preventDefault();
+ Filter.customFields.toggle(this.currentData()._id);
+ Filter.resetExceptions();
+ },
+ 'change .js-field-advanced-filter'(evt) {
+ evt.preventDefault();
+ Filter.advanced.set(this.find('.js-field-advanced-filter').value.trim());
+ Filter.resetExceptions();
+ },
'click .js-clear-all'(evt) {
evt.preventDefault();
Filter.reset();
diff --git a/client/components/sidebar/sidebarSearches.jade b/client/components/sidebar/sidebarSearches.jade
index 2ad5b00f..96877c50 100644
--- a/client/components/sidebar/sidebarSearches.jade
+++ b/client/components/sidebar/sidebarSearches.jade
@@ -1,6 +1,6 @@
template(name="searchSidebar")
form.js-search-term-form
- input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus)
+ input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
.list-body.js-perfect-scrollbar
.minicards.clearfix.js-minicards
each (results)
diff --git a/client/components/swimlanes/miniswimlane.jade b/client/components/swimlanes/miniswimlane.jade
new file mode 100644
index 00000000..d4be8599
--- /dev/null
+++ b/client/components/swimlanes/miniswimlane.jade
@@ -0,0 +1,8 @@
+template(name="miniswimlane")
+ .minicard(
+ class="minicard-{{colorClass}}")
+ .minicard-title
+ .handle
+ .fa.fa-arrows
+ +viewer
+ = title
diff --git a/client/components/swimlanes/swimlaneHeader.jade b/client/components/swimlanes/swimlaneHeader.jade
index 483de06f..8c6aa5a3 100644
--- a/client/components/swimlanes/swimlaneHeader.jade
+++ b/client/components/swimlanes/swimlaneHeader.jade
@@ -1,14 +1,21 @@
template(name="swimlaneHeader")
- .swimlane-header-wrap.js-swimlane-header
- +inlinedForm
- +editSwimlaneTitleForm
+ .swimlane-header-wrap.js-swimlane-header(class='{{#if colorClass}}swimlane-{{colorClass}}{{/if}}')
+ if this.isTemplateContainer
+ +swimlaneFixedHeader(this)
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
+ +inlinedForm
+ +editSwimlaneTitleForm
+ else
+ +swimlaneFixedHeader(this)
+
+template(name="swimlaneFixedHeader")
+ .swimlane-header(
+ class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
+ = title
+ .swimlane-header-menu
+ unless currentUser.isCommentOnly
+ a.fa.fa-plus.js-open-add-swimlane-menu.swimlane-header-plus-icon
+ a.fa.fa-navicon.js-open-swimlane-menu
template(name="editSwimlaneTitleForm")
.list-composer
@@ -20,4 +27,34 @@ template(name="editSwimlaneTitleForm")
template(name="swimlaneActionPopup")
unless currentUser.isCommentOnly
ul.pop-over-list
- li: a.js-close-swimlane {{_ 'archive-swimlane'}}
+ li: a.js-set-swimlane-color {{_ 'select-color'}}
+ unless this.isTemplateContainer
+ hr
+ ul.pop-over-list
+ li: a.js-close-swimlane {{_ 'archive-swimlane'}}
+
+template(name="swimlaneAddPopup")
+ unless currentUser.isCommentOnly
+ form
+ input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}"
+ autocomplete="off" autofocus)
+ .edit-controls.clearfix
+ button.primary.confirm(type="submit") {{_ 'add'}}
+ unless currentBoard.isTemplatesBoard
+ unless currentBoard.isTemplateBoard
+ span.quiet
+ | {{_ 'or'}}
+ a.js-swimlane-template {{_ 'template'}}
+
+template(name="setSwimlaneColorPopup")
+ form.edit-label
+ .palette-colors: each colors
+ span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
+ if(isSelected color)
+ i.fa.fa-check
+ button.primary.confirm.js-submit {{_ 'save'}}
+ button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
+
+template(name="swimlaneDeletePopup")
+ p {{_ "swimlane-delete-pop"}}
+ button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
diff --git a/client/components/swimlanes/swimlaneHeader.js b/client/components/swimlanes/swimlaneHeader.js
index 50635f86..e7f3cc76 100644
--- a/client/components/swimlanes/swimlaneHeader.js
+++ b/client/components/swimlanes/swimlaneHeader.js
@@ -1,3 +1,10 @@
+const { calculateIndexData } = Utils;
+
+let swimlaneColors;
+Meteor.startup(() => {
+ swimlaneColors = Swimlanes.simpleSchema()._schema.color.allowedValues;
+});
+
BlazeComponent.extendComponent({
editTitle(evt) {
evt.preventDefault();
@@ -11,15 +18,85 @@ BlazeComponent.extendComponent({
events() {
return [{
'click .js-open-swimlane-menu': Popup.open('swimlaneAction'),
+ 'click .js-open-add-swimlane-menu': Popup.open('swimlaneAdd'),
submit: this.editTitle,
}];
},
}).register('swimlaneHeader');
Template.swimlaneActionPopup.events({
+ 'click .js-set-swimlane-color': Popup.open('setSwimlaneColor'),
'click .js-close-swimlane' (evt) {
evt.preventDefault();
this.archive();
Popup.close();
},
});
+
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.currentSwimlane = this.currentData();
+ },
+
+ events() {
+ return [{
+ submit(evt) {
+ evt.preventDefault();
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ const nextSwimlane = currentBoard.nextSwimlane(this.currentSwimlane);
+ const titleInput = this.find('.swimlane-name-input');
+ const title = titleInput.value.trim();
+ const sortValue = calculateIndexData(this.currentSwimlane, nextSwimlane, 1);
+ const swimlaneType = (currentBoard.isTemplatesBoard())?'template-swimlane':'swimlane';
+
+ if (title) {
+ Swimlanes.insert({
+ title,
+ boardId: Session.get('currentBoard'),
+ sort: sortValue.base,
+ type: swimlaneType,
+ });
+
+ titleInput.value = '';
+ titleInput.focus();
+ }
+ // XXX ideally, we should move the popup to the newly
+ // created swimlane so a user can add more than one swimlane
+ // with a minimum of interactions
+ Popup.close();
+ },
+ 'click .js-swimlane-template': Popup.open('searchElement'),
+ }];
+ },
+}).register('swimlaneAddPopup');
+
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.currentSwimlane = this.currentData();
+ this.currentColor = new ReactiveVar(this.currentSwimlane.color);
+ },
+
+ colors() {
+ return swimlaneColors.map((color) => ({ color, name: '' }));
+ },
+
+ isSelected(color) {
+ return this.currentColor.get() === color;
+ },
+
+ events() {
+ return [{
+ 'click .js-palette-color'() {
+ this.currentColor.set(this.currentData().color);
+ },
+ 'click .js-submit' () {
+ this.currentSwimlane.setColor(this.currentColor.get());
+ Popup.close();
+ },
+ 'click .js-remove-color'() {
+ this.currentSwimlane.setColor(null);
+ Popup.close();
+ },
+ }];
+ },
+}).register('setSwimlaneColorPopup');
diff --git a/client/components/swimlanes/swimlanes.jade b/client/components/swimlanes/swimlanes.jade
index 76f54c66..c56834df 100644
--- a/client/components/swimlanes/swimlanes.jade
+++ b/client/components/swimlanes/swimlanes.jade
@@ -1,21 +1,22 @@
template(name="swimlane")
- .swimlane.js-lists.js-swimlane
+ .swimlane
+swimlaneHeader
+ .swimlane.js-lists.js-swimlane
if isMiniScreen
- if currentList
+ if currentListIsInThisSwimlane _id
+list(currentList)
- else
- each currentBoard.lists
+ unless currentList
+ each lists
+miniList(this)
if currentUser.isBoardMember
+addListForm
else
- each currentBoard.lists
+ each lists
+list(this)
if currentCardIsInThisList _id ../_id
+cardDetails(currentCard)
if currentUser.isBoardMember
- +addListAndSwimlaneForm
+ +addListForm
template(name="listsGroup")
.swimlane.list-group.js-lists
@@ -23,52 +24,31 @@ template(name="listsGroup")
if currentList
+list(currentList)
else
- each currentBoard.lists
+ each lists
+miniList(this)
if currentUser.isBoardMember
+addListForm
else
- each currentBoard.lists
+ each 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
+ .list-header-add
+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
+ unless currentBoard.isTemplatesBoard
+ unless currentBoard.isTemplateBoard
+ span.quiet
+ | {{_ 'or'}}
+ a.js-list-template {{_ 'template'}}
else
a.open-list-composer.js-open-inlined-form
i.fa.fa-plus
diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js
index 7965c2bc..d0ec3f4a 100644
--- a/client/components/swimlanes/swimlanes.js
+++ b/client/components/swimlanes/swimlanes.js
@@ -1,14 +1,25 @@
-const { calculateIndex } = Utils;
+const { calculateIndex, enableClickOnTouch } = Utils;
+
+function currentListIsInThisSwimlane(swimlaneId) {
+ const currentList = Lists.findOne(Session.get('currentList'));
+ return currentList && (currentList.swimlaneId === swimlaneId || currentList.swimlaneId === '');
+}
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')
+ if (currentUser && currentUser.profile && currentUser.profile.boardView === 'board-view-swimlanes')
return currentCard && currentCard.listId === listId && currentCard.swimlaneId === swimlaneId;
- else
- return false;
+ else // Default view: board-view-lists
+ return currentCard && currentCard.listId === listId;
+ // https://github.com/wekan/wekan/issues/1623
+ // https://github.com/ChronikEwok/wekan/commit/cad9b20451bb6149bfb527a99b5001873b06c3de
+ // TODO: In public board, if you would like to switch between List/Swimlane view, you could
+ // 1) If there is no view cookie, save to cookie board-view-lists
+ // board-view-lists / board-view-swimlanes / board-view-cal
+ // 2) If public user changes clicks board-view-lists then change view and
+ // then change view and save cookie with view value
+ // without using currentuser above, because currentuser is null.
}
function initSortable(boardComponent, $listsDom) {
@@ -64,6 +75,9 @@ function initSortable(boardComponent, $listsDom) {
},
});
+ // ugly touch event hotfix
+ enableClickOnTouch('.js-list:not(.js-list-composer)');
+
function userIsMember() {
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
}
@@ -105,6 +119,10 @@ BlazeComponent.extendComponent({
return currentCardIsInThisList(listId, swimlaneId);
},
+ currentListIsInThisSwimlane(swimlaneId) {
+ return currentListIsInThisSwimlane(swimlaneId);
+ },
+
events() {
return [{
// Click-and-drag action
@@ -144,6 +162,12 @@ BlazeComponent.extendComponent({
}).register('swimlane');
BlazeComponent.extendComponent({
+ onCreated() {
+ this.currentBoard = Boards.findOne(Session.get('currentBoard'));
+ this.isListTemplatesSwimlane = this.currentBoard.isTemplatesBoard() && this.currentData().isListTemplatesSwimlane();
+ this.currentSwimlane = this.currentData();
+ },
+
// Proxy
open() {
this.childComponents('inlinedForm')[0].open();
@@ -160,58 +184,19 @@ BlazeComponent.extendComponent({
title,
boardId: Session.get('currentBoard'),
sort: $('.list').length,
+ type: (this.isListTemplatesSwimlane)?'template-list':'list',
+ swimlaneId: (this.currentBoard.isTemplatesBoard())?this.currentSwimlane._id:'',
});
titleInput.value = '';
titleInput.focus();
}
},
+ 'click .js-list-template': Popup.open('searchElement'),
}];
},
}).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();
diff --git a/client/components/swimlanes/swimlanes.styl b/client/components/swimlanes/swimlanes.styl
index dce298b0..1056e1e3 100644
--- a/client/components/swimlanes/swimlanes.styl
+++ b/client/components/swimlanes/swimlanes.styl
@@ -7,7 +7,7 @@
display: flex
flex-direction: row
overflow: 0;
- max-height: 100%
+ max-height: calc(100% - 26px)
&.placeholder
background-color: rgba(0, 0, 0, .2)
@@ -27,19 +27,15 @@
.swimlane-header-wrap
display: flex;
flex-direction: row;
- flex: 0 0 50px;
- padding-bottom: 30px;
- border-bottom: 1px solid #CCC
+ flex: 1 0 100%;
+ background-color: #ccc;
.swimlane-header
- writing-mode: vertical-rl;
- transform: rotate(180deg);
font-size: 14px;
- line-height: 50px;
- margin-top: 50px;
+ padding: 5px 5px
font-weight: bold;
min-height: 9px;
- width: 50px;
+ width: 100%;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
@@ -48,7 +44,95 @@
.swimlane-header-menu
position: absolute
- padding: 20px 20px
+ padding: 5px 5px
+
+ .swimlane-header-plus-icon
+ margin-left: 5px
+ margin-right: 10px
.list-group
height: 100%
+
+.moving-swimlane
+ display: none
+
+swimlane-color(background, color...)
+ background: background !important
+ if color
+ color: color !important //overwrite text for better visibility
+
+.swimlane-white
+ swimlane-color(#ffffff, #4d4d4d) //Black text for better visibility
+ border: 1px solid #eee
+
+.swimlane-green
+ swimlane-color(#3cb500, #ffffff) //White text for better visibility
+
+.swimlane-yellow
+ swimlane-color(#fad900, #4d4d4d) //Black text for better visibility
+
+.swimlane-orange
+ swimlane-color(#ff9f19, #4d4d4d) //Black text for better visibility
+
+.swimlane-red
+ swimlane-color(#eb4646, #ffffff) //White text for better visibility
+
+.swimlane-purple
+ swimlane-color(#a632db, #ffffff) //White text for better visibility
+
+.swimlane-blue
+ swimlane-color(#0079bf, #ffffff) //White text for better visibility
+
+.swimlane-pink
+ swimlane-color(#ff78cb, #4d4d4d) //Black text for better visibility
+
+.swimlane-sky
+ swimlane-color(#00c2e0, #ffffff) //White text for better visibility
+
+.swimlane-black
+ swimlane-color(#4d4d4d, #ffffff) //White text for better visibility
+
+.swimlane-lime
+ swimlane-color(#51e898, #4d4d4d) //Black text for better visibility
+
+.swimlane-silver
+ swimlane-color(unset, #4d4d4d) //Black text for better visibility
+
+.swimlane-peachpuff
+ swimlane-color(#ffdab9, #4d4d4d) //Black text for better visibility
+
+.swimlane-crimson
+ swimlane-color(#dc143c, #ffffff) //White text for better visibility
+
+.swimlane-plum
+ swimlane-color(#dda0dd, #4d4d4d) //Black text for better visibility
+
+.swimlane-darkgreen
+ swimlane-color(#006400, #ffffff) //White text for better visibility
+
+.swimlane-slateblue
+ swimlane-color(#6a5acd, #ffffff) //White text for better visibility
+
+.swimlane-magenta
+ swimlane-color(#ff00ff, #ffffff) //White text for better visibility
+
+.swimlane-gold
+ swimlane-color(#ffd700, #4d4d4d) //Black text for better visibility
+
+.swimlane-navy
+ swimlane-color(#000080, #ffffff) //White text for better visibility
+
+.swimlane-gray
+ swimlane-color(#808080, #ffffff) //White text for better visibility
+
+.swimlane-saddlebrown
+ swimlane-color(#8b4513, #ffffff) //White text for better visibility
+
+.swimlane-paleturquoise
+ swimlane-color(#afeeee, #4d4d4d) //Black text for better visibility
+
+.swimlane-mistyrose
+ swimlane-color(#ffe4e1, #4d4d4d) //Black text for better visibility
+
+.swimlane-indigo
+ swimlane-color(#4b0082, #ffffff) //White text for better visibility
diff --git a/client/components/users/userAvatar.jade b/client/components/users/userAvatar.jade
index 83e2c8d0..ebfa48ba 100644
--- a/client/components/users/userAvatar.jade
+++ b/client/components/users/userAvatar.jade
@@ -1,7 +1,7 @@
template(name="userAvatar")
a.member.js-member(title="{{userData.profile.fullname}} ({{userData.username}})")
if userData.profile.avatarUrl
- img.avatar.avatar-image(src=userData.profile.avatarUrl)
+ img.avatar.avatar-image(src="{{userData.profile.avatarUrl}}")
else
+userAvatarInitials(userId=userData._id)
diff --git a/client/components/users/userAvatar.js b/client/components/users/userAvatar.js
index be7a85d2..91cad237 100644
--- a/client/components/users/userAvatar.js
+++ b/client/components/users/userAvatar.js
@@ -134,8 +134,9 @@ BlazeComponent.extendComponent({
Template.cardMembersPopup.helpers({
isCardMember() {
- const cardId = Template.parentData()._id;
- const cardMembers = Cards.findOne(cardId).members || [];
+ const card = Template.parentData();
+ const cardMembers = card.getMembers();
+
return _.contains(cardMembers, this.userId);
},
diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade
index a8fdb143..2a3d04cc 100644
--- a/client/components/users/userHeader.jade
+++ b/client/components/users/userHeader.jade
@@ -4,10 +4,11 @@ template(name="headerUserBar")
.header-user-bar-avatar
+userAvatar(userId=currentUser._id)
unless isMiniScreen
- if currentUser.profile.fullname
- = currentUser.profile.fullname
- else
- = currentUser.username
+ unless isSandstorm
+ if currentUser.profile.fullname
+ = currentUser.profile.fullname
+ else
+ = currentUser.username
template(name="memberMenuPopup")
ul.pop-over-list
@@ -15,14 +16,18 @@ template(name="memberMenuPopup")
li: a.js-edit-profile {{_ 'edit-profile'}}
li: a.js-change-settings {{_ 'change-settings'}}
li: a.js-change-avatar {{_ 'edit-avatar'}}
- li: a.js-change-password {{_ 'changePasswordPopup-title'}}
- li: a.js-change-language {{_ 'changeLanguagePopup-title'}}
- li: a.js-edit-notification {{_ 'editNotificationPopup-title'}}
+ unless isSandstorm
+ li: a.js-change-password {{_ 'changePasswordPopup-title'}}
+ li: a.js-change-language {{_ 'changeLanguagePopup-title'}}
if currentUser.isAdmin
li: a.js-go-setting(href="{{pathFor 'setting'}}") {{_ 'admin-panel'}}
hr
ul.pop-over-list
- li: a.js-logout {{_ 'log-out'}}
+ li: a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") {{_ 'templates'}}
+ unless isSandstorm
+ hr
+ ul.pop-over-list
+ li: a.js-logout {{_ 'log-out'}}
template(name="editProfilePopup")
form
@@ -48,24 +53,10 @@ template(name="editProfilePopup")
input.js-profile-email(type="email" value="{{emails.[0].address}}")
else
input.js-profile-email(type="email" value="{{emails.[0].address}}" readonly)
- input.primary.wide(type="submit" value="{{_ 'save'}}")
-
-template(name="editNotificationPopup")
- ul.pop-over-list
- li
- a.js-toggle-tag-notify-watch
- i.fa.fa-eye.colorful
- | {{_ 'watching'}}
- if hasTag "notify-watch"
- i.fa.fa-check
- span.sub-name {{_ 'notify-watch'}}
- li
- a.js-toggle-tag-notify-participate
- i.fa.fa-bell.colorful
- | {{_ 'tracking'}}
- if hasTag "notify-participate"
- i.fa.fa-check
- span.sub-name {{_ 'notify-participate'}}
+ div.buttonsContainer
+ input.primary.wide(type="submit" value="{{_ 'save'}}")
+ div
+ input#deleteButton.primary.wide(type="button" value="{{_ 'delete'}}")
template(name="changePasswordPopup")
+atForm(state='changePwd')
diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js
index d96a9b3d..869c9d15 100644
--- a/client/components/users/userHeader.js
+++ b/client/components/users/userHeader.js
@@ -3,13 +3,21 @@ Template.headerUserBar.events({
'click .js-change-avatar': Popup.open('changeAvatar'),
});
+Template.memberMenuPopup.helpers({
+ templatesBoardId() {
+ return Meteor.user().getTemplatesBoardId();
+ },
+ templatesBoardSlug() {
+ return Meteor.user().getTemplatesBoardSlug();
+ },
+});
+
Template.memberMenuPopup.events({
'click .js-edit-profile': Popup.open('editProfile'),
'click .js-change-settings': Popup.open('changeSettings'),
'click .js-change-avatar': Popup.open('changeAvatar'),
'click .js-change-password': Popup.open('changePassword'),
'click .js-change-language': Popup.open('changeLanguage'),
- 'click .js-edit-notification': Popup.open('editNotification'),
'click .js-logout'(evt) {
evt.preventDefault();
@@ -87,24 +95,10 @@ Template.editProfilePopup.events({
});
} else Popup.back();
},
-});
-
-Template.editNotificationPopup.helpers({
- hasTag(tag) {
- const user = Meteor.user();
- return user && user.hasTag(tag);
- },
-});
-
-// we defined github like rules, see: https://github.com/settings/notifications
-Template.editNotificationPopup.events({
- 'click .js-toggle-tag-notify-participate'() {
- const user = Meteor.user();
- if (user) user.toggleTag('notify-participate');
- },
- 'click .js-toggle-tag-notify-watch'() {
- const user = Meteor.user();
- if (user) user.toggleTag('notify-watch');
+ 'click #deleteButton'() {
+ Users.remove(Meteor.userId());
+ Popup.close();
+ AccountsTemplates.logout();
},
});
diff --git a/client/lib/datepicker.js b/client/lib/datepicker.js
new file mode 100644
index 00000000..ab2da0bd
--- /dev/null
+++ b/client/lib/datepicker.js
@@ -0,0 +1,86 @@
+DatePicker = BlazeComponent.extendComponent({
+ template() {
+ return 'datepicker';
+ },
+
+ 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();
+ },
+ }];
+ },
+});
diff --git a/client/lib/escapeActions.js b/client/lib/escapeActions.js
index 666e33e0..0757ae46 100644
--- a/client/lib/escapeActions.js
+++ b/client/lib/escapeActions.js
@@ -135,6 +135,6 @@ $(document).on('click', (evt) => {
}
});
-$(document).on('click', 'a[href=#]', (evt) => {
+$(document).on('click', 'a[href=\\#]', (evt) => {
evt.preventDefault();
});
diff --git a/client/lib/filter.js b/client/lib/filter.js
index 8129776b..c3c1b070 100644
--- a/client/lib/filter.js
+++ b/client/lib/filter.js
@@ -3,17 +3,19 @@
// RangeFilter, dateFilter, etc.). We then define a global `Filter` object whose
// goal is to filter complete documents by using the local filters for each
// fields.
-
function showFilterSidebar() {
Sidebar.setView('filter');
}
// Use a "set" filter for a field that is a set of documents uniquely
// identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`.
+// use "subField" for searching inside object Fields.
+// For instance '{ 'customFields._id': ['field1','field2']} (subField would be: _id)
class SetFilter {
- constructor() {
+ constructor(subField = '') {
this._dep = new Tracker.Dependency();
this._selectedElements = [];
+ this.subField = subField;
}
isSelected(val) {
@@ -61,7 +63,9 @@ class SetFilter {
_getMongoSelector() {
this._dep.depend();
- return { $in: this._selectedElements };
+ return {
+ $in: this._selectedElements,
+ };
}
_getEmptySelector() {
@@ -72,10 +76,385 @@ class SetFilter {
includeEmpty = true;
}
});
- return includeEmpty ? { $eq: [] } : null;
+ return includeEmpty ? {
+ $eq: [],
+ } : null;
}
}
+
+// Advanced filter forms a MongoSelector from a users String.
+// Build by: Ignatz 19.05.2018 (github feuerball11)
+class AdvancedFilter {
+ constructor() {
+ this._dep = new Tracker.Dependency();
+ this._filter = '';
+ this._lastValide = {};
+ }
+
+ set(str) {
+ this._filter = str;
+ this._dep.changed();
+ }
+
+ reset() {
+ this._filter = '';
+ this._lastValide = {};
+ this._dep.changed();
+ }
+
+ _isActive() {
+ this._dep.depend();
+ return this._filter !== '';
+ }
+
+ _filterToCommands() {
+ const commands = [];
+ let current = '';
+ let string = false;
+ let regex = false;
+ let wasString = false;
+ let ignore = false;
+ for (let i = 0; i < this._filter.length; i++) {
+ const char = this._filter.charAt(i);
+ if (ignore) {
+ ignore = false;
+ current += char;
+ continue;
+ }
+ if (char === '/') {
+ string = !string;
+ if (string) regex = true;
+ current += char;
+ continue;
+ }
+ if (char === '\'') {
+ string = !string;
+ if (string) wasString = true;
+ continue;
+ }
+ if (char === '\\' && !string) {
+ ignore = true;
+ continue;
+ }
+ if (char === ' ' && !string) {
+ commands.push({
+ 'cmd': current,
+ 'string': wasString,
+ regex,
+ });
+ wasString = false;
+ current = '';
+ continue;
+ }
+ current += char;
+ }
+ if (current !== '') {
+ commands.push({
+ 'cmd': current,
+ 'string': wasString,
+ regex,
+ });
+ }
+ return commands;
+ }
+
+ _fieldNameToId(field) {
+ const found = CustomFields.findOne({
+ 'name': field,
+ });
+ return found._id;
+ }
+
+ _fieldValueToId(field, value) {
+ const found = CustomFields.findOne({
+ 'name': field,
+ });
+ if (found.settings.dropdownItems && found.settings.dropdownItems.length > 0) {
+ for (let i = 0; i < found.settings.dropdownItems.length; i++) {
+ if (found.settings.dropdownItems[i].name === value) {
+ return found.settings.dropdownItems[i]._id;
+ }
+ }
+ }
+ return value;
+ }
+
+ _arrayToSelector(commands) {
+ try {
+ //let changed = false;
+ this._processSubCommands(commands);
+ } catch (e) {
+ return this._lastValide;
+ }
+ this._lastValide = {
+ $or: commands,
+ };
+ return {
+ $or: commands,
+ };
+ }
+
+ _processSubCommands(commands) {
+ const subcommands = [];
+ let level = 0;
+ let start = -1;
+ for (let i = 0; i < commands.length; i++) {
+ if (commands[i].cmd) {
+ switch (commands[i].cmd) {
+ case '(':
+ {
+ level++;
+ if (start === -1) start = i;
+ continue;
+ }
+ case ')':
+ {
+ level--;
+ commands.splice(i, 1);
+ i--;
+ continue;
+ }
+ default:
+ {
+ if (level > 0) {
+ subcommands.push(commands[i]);
+ commands.splice(i, 1);
+ i--;
+ continue;
+ }
+ }
+ }
+ }
+ }
+ if (start !== -1) {
+ this._processSubCommands(subcommands);
+ if (subcommands.length === 1)
+ commands.splice(start, 0, subcommands[0]);
+ else
+ commands.splice(start, 0, subcommands);
+ }
+ this._processConditions(commands);
+ this._processLogicalOperators(commands);
+ }
+
+ _processConditions(commands) {
+ for (let i = 0; i < commands.length; i++) {
+ if (!commands[i].string && commands[i].cmd) {
+ switch (commands[i].cmd) {
+ case '=':
+ case '==':
+ case '===':
+ {
+ const field = commands[i - 1].cmd;
+ const str = commands[i + 1].cmd;
+ if (commands[i + 1].regex) {
+ const match = str.match(new RegExp('^/(.*?)/([gimy]*)$'));
+ let regex = null;
+ if (match.length > 2)
+ regex = new RegExp(match[1], match[2]);
+ else
+ regex = new RegExp(match[1]);
+ commands[i] = {
+ 'customFields._id': this._fieldNameToId(field),
+ 'customFields.value': regex,
+ };
+ } else {
+ commands[i] = {
+ 'customFields._id': this._fieldNameToId(field),
+ 'customFields.value': {
+ $in: [this._fieldValueToId(field, str), parseInt(str, 10)],
+ },
+ };
+ }
+ commands.splice(i - 1, 1);
+ commands.splice(i, 1);
+ //changed = true;
+ i--;
+ break;
+ }
+ case '!=':
+ case '!==':
+ {
+ const field = commands[i - 1].cmd;
+ const str = commands[i + 1].cmd;
+ if (commands[i + 1].regex) {
+ const match = str.match(new RegExp('^/(.*?)/([gimy]*)$'));
+ let regex = null;
+ if (match.length > 2)
+ regex = new RegExp(match[1], match[2]);
+ else
+ regex = new RegExp(match[1]);
+ commands[i] = {
+ 'customFields._id': this._fieldNameToId(field),
+ 'customFields.value': {
+ $not: regex,
+ },
+ };
+ } else {
+ commands[i] = {
+ 'customFields._id': this._fieldNameToId(field),
+ 'customFields.value': {
+ $not: {
+ $in: [this._fieldValueToId(field, str), parseInt(str, 10)],
+ },
+ },
+ };
+ }
+ commands.splice(i - 1, 1);
+ commands.splice(i, 1);
+ //changed = true;
+ i--;
+ break;
+ }
+ case '>':
+ case 'gt':
+ case 'Gt':
+ case 'GT':
+ {
+ const field = commands[i - 1].cmd;
+ const str = commands[i + 1].cmd;
+ commands[i] = {
+ 'customFields._id': this._fieldNameToId(field),
+ 'customFields.value': {
+ $gt: parseInt(str, 10),
+ },
+ };
+ commands.splice(i - 1, 1);
+ commands.splice(i, 1);
+ //changed = true;
+ i--;
+ break;
+ }
+ case '>=':
+ case '>==':
+ case 'gte':
+ case 'Gte':
+ case 'GTE':
+ {
+ const field = commands[i - 1].cmd;
+ const str = commands[i + 1].cmd;
+ commands[i] = {
+ 'customFields._id': this._fieldNameToId(field),
+ 'customFields.value': {
+ $gte: parseInt(str, 10),
+ },
+ };
+ commands.splice(i - 1, 1);
+ commands.splice(i, 1);
+ //changed = true;
+ i--;
+ break;
+ }
+ case '<':
+ case 'lt':
+ case 'Lt':
+ case 'LT':
+ {
+ const field = commands[i - 1].cmd;
+ const str = commands[i + 1].cmd;
+ commands[i] = {
+ 'customFields._id': this._fieldNameToId(field),
+ 'customFields.value': {
+ $lt: parseInt(str, 10),
+ },
+ };
+ commands.splice(i - 1, 1);
+ commands.splice(i, 1);
+ //changed = true;
+ i--;
+ break;
+ }
+ case '<=':
+ case '<==':
+ case 'lte':
+ case 'Lte':
+ case 'LTE':
+ {
+ const field = commands[i - 1].cmd;
+ const str = commands[i + 1].cmd;
+ commands[i] = {
+ 'customFields._id': this._fieldNameToId(field),
+ 'customFields.value': {
+ $lte: parseInt(str, 10),
+ },
+ };
+ commands.splice(i - 1, 1);
+ commands.splice(i, 1);
+ //changed = true;
+ i--;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ _processLogicalOperators(commands) {
+ for (let i = 0; i < commands.length; i++) {
+ if (!commands[i].string && commands[i].cmd) {
+ switch (commands[i].cmd) {
+ case 'or':
+ case 'Or':
+ case 'OR':
+ case '|':
+ case '||':
+ {
+ const op1 = commands[i - 1];
+ const op2 = commands[i + 1];
+ commands[i] = {
+ $or: [op1, op2],
+ };
+ commands.splice(i - 1, 1);
+ commands.splice(i, 1);
+ //changed = true;
+ i--;
+ break;
+ }
+ case 'and':
+ case 'And':
+ case 'AND':
+ case '&':
+ case '&&':
+ {
+ const op1 = commands[i - 1];
+ const op2 = commands[i + 1];
+ commands[i] = {
+ $and: [op1, op2],
+ };
+ commands.splice(i - 1, 1);
+ commands.splice(i, 1);
+ //changed = true;
+ i--;
+ break;
+ }
+ case 'not':
+ case 'Not':
+ case 'NOT':
+ case '!':
+ {
+ const op1 = commands[i + 1];
+ commands[i] = {
+ $not: op1,
+ };
+ commands.splice(i + 1, 1);
+ //changed = true;
+ i--;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ _getMongoSelector() {
+ this._dep.depend();
+ const commands = this._filterToCommands();
+ return this._arrayToSelector(commands);
+ }
+
+}
+
// The global Filter object.
// XXX It would be possible to re-write this object more elegantly, and removing
// the need to provide a list of `_fields`. We also should move methods into the
@@ -86,8 +465,10 @@ Filter = {
// before changing the schema.
labelIds: new SetFilter(),
members: new SetFilter(),
+ customFields: new SetFilter('_id'),
+ advanced: new AdvancedFilter(),
- _fields: ['labelIds', 'members'],
+ _fields: ['labelIds', 'members', 'customFields'],
// We don't filter cards that have been added after the last filter change. To
// implement this we keep the id of these cards in this `_exceptions` fields
@@ -98,7 +479,7 @@ Filter = {
isActive() {
return _.any(this._fields, (fieldName) => {
return this[fieldName]._isActive();
- });
+ }) || this.advanced._isActive();
},
_getMongoSelector() {
@@ -111,7 +492,11 @@ Filter = {
this._fields.forEach((fieldName) => {
const filter = this[fieldName];
if (filter._isActive()) {
- filterSelector[fieldName] = filter._getMongoSelector();
+ if (filter.subField !== '') {
+ filterSelector[`${fieldName}.${filter.subField}`] = filter._getMongoSelector();
+ } else {
+ filterSelector[fieldName] = filter._getMongoSelector();
+ }
emptySelector[fieldName] = filter._getEmptySelector();
if (emptySelector[fieldName] !== null) {
includeEmptySelectors = true;
@@ -119,13 +504,24 @@ Filter = {
}
});
- const exceptionsSelector = {_id: {$in: this._exceptions}};
+ const exceptionsSelector = {
+ _id: {
+ $in: this._exceptions,
+ },
+ };
this._exceptionsDep.depend();
- if (includeEmptySelectors)
- return {$or: [filterSelector, exceptionsSelector, emptySelector]};
- else
- return {$or: [filterSelector, exceptionsSelector]};
+ const selectors = [exceptionsSelector];
+
+ if (_.any(this._fields, (fieldName) => {
+ return this[fieldName]._isActive();
+ })) selectors.push(filterSelector);
+ if (includeEmptySelectors) selectors.push(emptySelector);
+ if (this.advanced._isActive()) selectors.push(this.advanced._getMongoSelector());
+
+ return {
+ $or: selectors,
+ };
},
mongoSelector(additionalSelector) {
@@ -133,7 +529,9 @@ Filter = {
if (_.isUndefined(additionalSelector))
return filterSelector;
else
- return {$and: [filterSelector, additionalSelector]};
+ return {
+ $and: [filterSelector, additionalSelector],
+ };
},
reset() {
@@ -141,6 +539,7 @@ Filter = {
const filter = this[fieldName];
filter.reset();
});
+ this.advanced.reset();
this.resetExceptions();
},
diff --git a/client/lib/inlinedform.js b/client/lib/inlinedform.js
index 56768a13..e5e4d4ed 100644
--- a/client/lib/inlinedform.js
+++ b/client/lib/inlinedform.js
@@ -75,6 +75,16 @@ InlinedForm = BlazeComponent.extendComponent({
EscapeActions.register('inlinedForm',
() => { currentlyOpenedForm.get().close(); },
() => { return currentlyOpenedForm.get() !== null; }, {
- noClickEscapeOn: '.js-inlined-form',
+ enabledOnClick: false,
}
);
+
+// submit on click outside
+document.addEventListener('click', function(evt) {
+ const openedForm = currentlyOpenedForm.get();
+ const isClickOutside = $(evt.target).closest('.js-inlined-form').length === 0;
+ if (openedForm && isClickOutside) {
+ $('.js-inlined-form button[type=submit]').click();
+ openedForm.close();
+ }
+}, true);
diff --git a/client/lib/modal.js b/client/lib/modal.js
index d5350264..3c27a179 100644
--- a/client/lib/modal.js
+++ b/client/lib/modal.js
@@ -4,6 +4,7 @@ window.Modal = new class {
constructor() {
this._currentModal = new ReactiveVar(closedValue);
this._onCloseGoTo = '';
+ this._isWideModal = false;
}
getHeaderName() {
@@ -20,6 +21,10 @@ window.Modal = new class {
return this.getTemplateName() !== closedValue;
}
+ isWide(){
+ return this._isWideModal;
+ }
+
close() {
this._currentModal.set(closedValue);
if (this._onCloseGoTo) {
@@ -27,9 +32,16 @@ window.Modal = new class {
}
}
+ openWide(modalName, { header = '', onCloseGoTo = ''} = {}) {
+ this._currentModal.set({ header, modalName });
+ this._onCloseGoTo = onCloseGoTo;
+ this._isWideModal = true;
+ }
+
open(modalName, { header = '', onCloseGoTo = ''} = {}) {
this._currentModal.set({ header, modalName });
this._onCloseGoTo = onCloseGoTo;
+
}
}();
@@ -38,5 +50,5 @@ Blaze.registerHelper('Modal', Modal);
EscapeActions.register('modalWindow',
() => Modal.close(),
() => Modal.isOpen(),
- { noClickEscapeOn: '.modal-content' }
+ { noClickEscapeOn: '.modal-container' }
);
diff --git a/client/lib/popup.js b/client/lib/popup.js
index 0a700f82..9abe48aa 100644
--- a/client/lib/popup.js
+++ b/client/lib/popup.js
@@ -4,9 +4,9 @@ window.Popup = new class {
this.template = Template.popup;
// We only want to display one popup at a time and we keep the view object
- // in this `Popup._current` variable. If there is no popup currently opened
+ // in this `Popup.current` variable. If there is no popup currently opened
// the value is `null`.
- this._current = null;
+ this.current = null;
// It's possible to open a sub-popup B from a popup A. In that case we keep
// the data of popup A so we can return back to it. Every time we open a new
@@ -27,11 +27,9 @@ window.Popup = new class {
open(name) {
const self = this;
const popupName = `${name}Popup`;
-
function clickFromPopup(evt) {
return $(evt.target).closest('.js-pop-over').length !== 0;
}
-
return function(evt) {
// If a popup is already opened, clicking again on the opener element
// should close it -- and interrupt the current `open` function.
@@ -57,7 +55,6 @@ window.Popup = new class {
self._stack = [];
openerElement = evt.currentTarget;
}
-
$(openerElement).addClass('is-active');
evt.preventDefault();
@@ -139,6 +136,7 @@ window.Popup = new class {
const openerElement = this._getTopStack().openerElement;
$(openerElement).removeClass('is-active');
+
this._stack = [];
}
}
@@ -186,7 +184,7 @@ window.Popup = new class {
// positives.
const title = TAPi18n.__(translationKey);
// when popup showed as full of small screen, we need a default header to clearly see [X] button
- const defaultTitle = Utils.isMiniScreen() ? 'Wekan' : false;
+ const defaultTitle = Utils.isMiniScreen() ? '' : false;
return title !== translationKey ? title : defaultTitle;
};
}
@@ -200,7 +198,7 @@ escapeActions.forEach((actionName) => {
() => Popup[actionName](),
() => Popup.isOpen(),
{
- noClickEscapeOn: '.js-pop-over',
+ noClickEscapeOn: '.js-pop-over,.js-open-card-title-popup',
enabledOnClick: actionName === 'close',
}
);
diff --git a/client/lib/utils.js b/client/lib/utils.js
index 1f44c60d..e2339763 100644
--- a/client/lib/utils.js
+++ b/client/lib/utils.js
@@ -39,11 +39,11 @@ Utils = {
if (!prevData && !nextData) {
base = 0;
increment = 1;
- // If we drop the card in the first position
+ // 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
+ // If we drop the card in the last position
} else if (!nextData) {
base = prevData.sort + 1;
increment = 1;
@@ -71,11 +71,11 @@ Utils = {
if (!prevCardDomElement && !nextCardDomElement) {
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) {
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) {
base = Blaze.getData(prevCardDomElement).sort + 1;
increment = 1;
@@ -95,6 +95,151 @@ Utils = {
increment,
};
},
+
+ // Detect touch device
+ isTouchDevice() {
+ const isTouchable = (() => {
+ const prefixes = ' -webkit- -moz- -o- -ms- '.split(' ');
+ const mq = function(query) {
+ return window.matchMedia(query).matches;
+ };
+
+ if (('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch) {
+ return true;
+ }
+
+ // include the 'heartz' as a way to have a non matching MQ to help terminate the join
+ // https://git.io/vznFH
+ const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('');
+ return mq(query);
+ })();
+ Utils.isTouchDevice = () => isTouchable;
+ return isTouchable;
+ },
+
+ calculateTouchDistance(touchA, touchB) {
+ return Math.sqrt(
+ Math.pow(touchA.screenX - touchB.screenX, 2) +
+ Math.pow(touchA.screenY - touchB.screenY, 2)
+ );
+ },
+
+ enableClickOnTouch(selector) {
+ let touchStart = null;
+ let lastTouch = null;
+
+ $(document).on('touchstart', selector, function(e) {
+ touchStart = e.originalEvent.touches[0];
+ });
+ $(document).on('touchmove', selector, function(e) {
+ const touches = e.originalEvent.touches;
+ lastTouch = touches[touches.length - 1];
+ });
+ $(document).on('touchend', selector, function(e) {
+ if (touchStart && lastTouch && Utils.calculateTouchDistance(touchStart, lastTouch) <= 20) {
+ e.preventDefault();
+ const clickEvent = document.createEvent('MouseEvents');
+ clickEvent.initEvent('click', true, true);
+ e.target.dispatchEvent(clickEvent);
+ }
+ });
+ },
+
+ manageCustomUI(){
+ Meteor.call('getCustomUI', (err, data) => {
+ if (err && err.error[0] === 'var-not-exist'){
+ Session.set('customUI', false); // siteId || address server not defined
+ }
+ if (!err){
+ Utils.setCustomUI(data);
+ }
+ });
+ },
+
+ setCustomUI(data){
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ if (currentBoard) {
+ DocHead.setTitle(`${currentBoard.title } - ${ data.productName}`);
+ } else {
+ DocHead.setTitle(`${data.productName}`);
+ }
+ },
+
+ setMatomo(data){
+ window._paq = window._paq || [];
+ window._paq.push(['setDoNotTrack', data.doNotTrack]);
+ if (data.withUserName){
+ window._paq.push(['setUserId', Meteor.user().username]);
+ }
+ window._paq.push(['trackPageView']);
+ window._paq.push(['enableLinkTracking']);
+
+ (function() {
+ window._paq.push(['setTrackerUrl', `${data.address}piwik.php`]);
+ window._paq.push(['setSiteId', data.siteId]);
+
+ const script = document.createElement('script');
+ Object.assign(script, {
+ id: 'scriptMatomo',
+ type: 'text/javascript',
+ async: 'true',
+ defer: 'true',
+ src: `${data.address}piwik.js`,
+ });
+
+ const s = document.getElementsByTagName('script')[0];
+ s.parentNode.insertBefore(script, s);
+ })();
+
+ Session.set('matomo', true);
+ },
+
+ manageMatomo() {
+ const matomo = Session.get('matomo');
+ if (matomo === undefined){
+ Meteor.call('getMatomoConf', (err, data) => {
+ if (err && err.error[0] === 'var-not-exist'){
+ Session.set('matomo', false); // siteId || address server not defined
+ }
+ if (!err){
+ Utils.setMatomo(data);
+ }
+ });
+ } else if (matomo) {
+ window._paq.push(['trackPageView']);
+ }
+ },
+
+ getTriggerActionDesc(event, tempInstance) {
+ const jqueryEl = tempInstance.$(event.currentTarget.parentNode);
+ const triggerEls = jqueryEl.find('.trigger-content').children();
+ let finalString = '';
+ for (let i = 0; i < triggerEls.length; i++) {
+ const element = tempInstance.$(triggerEls[i]);
+ if (element.hasClass('trigger-text')) {
+ finalString += element.text().toLowerCase();
+ } else if (element.hasClass('user-details')) {
+ let username = element.find('input').val();
+ if(username === undefined || username === ''){
+ username = '*';
+ }
+ finalString += `${element.find('.trigger-text').text().toLowerCase() } ${ username}`;
+ } else if (element.find('select').length > 0) {
+ finalString += element.find('select option:selected').text().toLowerCase();
+ } else if (element.find('input').length > 0) {
+ let inputvalue = element.find('input').val();
+ if(inputvalue === undefined || inputvalue === ''){
+ inputvalue = '*';
+ }
+ finalString += inputvalue;
+ }
+ // Add space
+ if (i !== length - 1) {
+ finalString += ' ';
+ }
+ }
+ return finalString;
+ },
};
// A simple tracker dependency that we invalidate every time the window is