diff options
author | 蔡仲明 (Romulus Urakagi Tsai) <urakagi@gmail.com> | 2019-11-21 11:25:56 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-11-21 11:25:56 +0800 |
commit | 3e0bedd8c7a6dec97352212adb1cbde1ade44190 (patch) | |
tree | 651ff30d25ddb0416444370368d699e597c142d7 /client | |
parent | 9bbeb73db1cd0ce1caaaca8dfb14ea92131bbf9d (diff) | |
parent | 4f5de87cc4c2281bd576548693de7c94e6a988c6 (diff) | |
download | wekan-3e0bedd8c7a6dec97352212adb1cbde1ade44190.tar.gz wekan-3e0bedd8c7a6dec97352212adb1cbde1ade44190.tar.bz2 wekan-3e0bedd8c7a6dec97352212adb1cbde1ade44190.zip |
Merge pull request #1 from wekan/master
Update master
Diffstat (limited to 'client')
41 files changed, 1535 insertions, 291 deletions
diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index 05149826..b082273a 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -85,7 +85,7 @@ BlazeComponent.extendComponent({ const lastLabel = Boards.findOne(Session.get('currentBoard')).getLabelById( lastLabelId, ); - if (lastLabel.name === undefined || lastLabel.name === '') { + if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) { return lastLabel.color; } else { return lastLabel.name; diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 6cff5ab1..41b6f4ef 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -89,7 +89,6 @@ BlazeComponent.extendComponent({ helper.append(list.clone()); return helper; }, - handle: '.js-swimlane-header', items: '.swimlane:not(.placeholder)', placeholder: 'swimlane placeholder', distance: 7, @@ -193,11 +192,42 @@ BlazeComponent.extendComponent({ // ugly touch event hotfix enableClickOnTouch('.js-swimlane:not(.placeholder)'); + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + + this.autorun(() => { + let showDesktopDragHandles = false; + currentUser = Meteor.user(); + if (currentUser) { + showDesktopDragHandles = (currentUser.profile || {}) + .showDesktopDragHandles; + } else if (cookies.has('showDesktopDragHandles')) { + showDesktopDragHandles = true; + } else { + showDesktopDragHandles = false; + } + if ( + Utils.isMiniScreen() + || (!Utils.isMiniScreen() && showDesktopDragHandles) + ) { + $swimlanesDom.sortable({ + handle: '.js-swimlane-header-handle', + }); + } else { + $swimlanesDom.sortable({ + handle: '.swimlane-header', + }); + } + + // Disable drag-dropping if the current user is not a board member or is comment only + $swimlanesDom.sortable('option', 'disabled', !userIsMember()); + }); + function userIsMember() { return ( - Meteor.user() && - Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + Meteor.user() + && Meteor.user().isBoardMember() + && !Meteor.user().isCommentOnly() ); } @@ -210,21 +240,36 @@ BlazeComponent.extendComponent({ }, isViewSwimlanes() { - const currentUser = Meteor.user(); - if (!currentUser) return false; - return (currentUser.profile || {}).boardView === 'board-view-swimlanes'; + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).boardView === 'board-view-swimlanes'; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + return cookies.get('boardView') === 'board-view-swimlanes'; + } }, isViewLists() { - const currentUser = Meteor.user(); - if (!currentUser) return true; - return (currentUser.profile || {}).boardView === 'board-view-lists'; + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).boardView === 'board-view-lists'; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + return cookies.get('boardView') === 'board-view-lists'; + } }, isViewCalendar() { - const currentUser = Meteor.user(); - if (!currentUser) return false; - return (currentUser.profile || {}).boardView === 'board-view-cal'; + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).boardView === 'board-view-cal'; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + return cookies.get('boardView') === 'board-view-cal'; + } }, openNewListForm() { @@ -261,16 +306,16 @@ BlazeComponent.extendComponent({ scrollLeft(position = 0) { const swimlanes = this.$('.js-swimlanes'); - swimlanes && - swimlanes.animate({ + swimlanes + && swimlanes.animate({ scrollLeft: position, }); }, scrollTop(position = 0) { const swimlanes = this.$('.js-swimlanes'); - swimlanes && - swimlanes.animate({ + swimlanes + && swimlanes.animate({ scrollTop: position, }); }, @@ -309,25 +354,46 @@ BlazeComponent.extendComponent({ events(start, end, timezone, callback) { const currentBoard = Boards.findOne(Session.get('currentBoard')); const events = []; + const pushEvent = function(card, title, start, end, extraCls) { + start = start || card.startAt; + end = end || card.endAt; + title = title || card.title; + const className = + (extraCls ? `${extraCls} ` : '') + + (card.color ? `calendar-event-${card.color}` : ''); + events.push({ + id: card._id, + title, + start, + end: end || card.endAt, + allDay: + Math.abs(end.getTime() - start.getTime()) / 1000 === 24 * 3600, + url: FlowRouter.url('card', { + boardId: currentBoard._id, + slug: currentBoard.slug, + cardId: card._id, + }), + className, + }); + }; 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, - }), - }); + pushEvent(card); + }); + currentBoard + .cardsDueInBetween(start.toDate(), end.toDate()) + .forEach(function(card) { + pushEvent( + card, + `${card.title} ${TAPi18n.__('card-due')}`, + card.dueAt, + new Date(card.dueAt.getTime() + 36e5), + ); }); + events.sort(function(first, second) { + return first.id > second.id ? 1 : -1; + }); callback(events); }, eventResize(event, delta, revertFunc) { @@ -360,8 +426,13 @@ BlazeComponent.extendComponent({ }; }, isViewCalendar() { - const currentUser = Meteor.user(); - if (!currentUser) return false; - return (currentUser.profile || {}).boardView === 'board-view-cal'; + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).boardView === 'board-view-cal'; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + return cookies.get('boardView') === 'board-view-cal'; + } }, }).register('calendarView'); diff --git a/client/components/boards/boardBody.styl b/client/components/boards/boardBody.styl index dfaaa050..32207d82 100644 --- a/client/components/boards/boardBody.styl +++ b/client/components/boards/boardBody.styl @@ -53,3 +53,81 @@ position() padding: 0 0px 0px 0 overflow-x: hidden overflow-y: auto + +calendar-event-color(background, borderColor, color...) + background: background !important + border-color: borderColor + if color + color: color !important //overwrite text for better visibility + +.calendar-event-green + calendar-event-color(#3cb500, #2a8000, #ffffff) //White text for better visibility + +.calendar-event-yellow + calendar-event-color(#fad900, #c7ac00, #000) //Black text for better visibility + +.calendar-event-orange + calendar-event-color(#ff9f19, #cc7c14, #000) //Black text for better visibility + +.calendar-event-red + calendar-event-color(#eb4646, #b83737, #ffffff) //White text for better visibility + +.calendar-event-purple + calendar-event-color(#a632db, #7d26a6, #ffffff) //White text for better visibility + +.calendar-event-blue + calendar-event-color(#0079bf, #005a8a, #ffffff) //White text for better visibility + +.calendar-event-pink + calendar-event-color(#ff78cb, #cc62a3, #000) //Black text for better visibility + +.calendar-event-sky + calendar-event-color(#00c2e0, #0094ab, #ffffff) //White text for better visibility + +.calendar-event-black + calendar-event-color(#4d4d4d, #1a1a1a, #ffffff) //White text for better visibility + +.calendar-event-lime + calendar-event-color(#51e898, #3eb375, #000) //Black text for better visibility + +.calendar-event-silver + calendar-event-color(#c0c0c0, #8c8c8c, #000) //Black text for better visibility + +.calendar-event-peachpuff + calendar-event-color(#ffdab9, #ccaf95, #000) //Black text for better visibility + +.calendar-event-crimson + calendar-event-color(#dc143c, #a8112f, #ffffff) //White text for better visibility + +.calendar-event-plum + calendar-event-color(#dda0dd, #a87ba8, #000) //Black text for better visibility + +.calendar-event-darkgreen + calendar-event-color(#006400, #003000, #ffffff) //White text for better visibility + +.calendar-event-slateblue + calendar-event-color(#6a5acd, #4f4399, #ffffff) //White text for better visibility + +.calendar-event-magenta + calendar-event-color(#ff00ff, #cc00cc, #ffffff) //White text for better visibility + +.calendar-event-gold + calendar-event-color(#ffd700, #ccaa00, #000) //Black text for better visibility + +.calendar-event-navy + calendar-event-color(#000080, #000033, #ffffff) //White text for better visibility + +.calendar-event-gray + calendar-event-color(#808080, #333333, #ffffff) //White text for better visibility + +.calendar-event-saddlebrown + calendar-event-color(#8b4513, #572b0c, #ffffff) //White text for better visibility + +.calendar-event-paleturquoise + calendar-event-color(#afeeee, #8ababa, #000) //Black text for better visibility + +.calendar-event-mistyrose + calendar-event-color(#ffe4e1, #ccb8b6, #000) //Black text for better visibility + +.calendar-event-indigo + calendar-event-color(#4b0082, #2b004d, #ffffff) //White text for better visibility diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index fe533f95..39221778 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -77,6 +77,11 @@ template(name="boardHeaderBar") i.fa.fa-archive span {{_ 'archives'}} + //if showSort + // a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}") + // i.fa(class="{{directionClass}}") + // span {{_ 'sort'}}{{_ listSortShortDesc}} + a.board-header-btn.js-open-filter-view( title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}" class="{{#if Filter.isActive}}emphasis{{/if}}") @@ -85,15 +90,6 @@ 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'}} - else if currentUser.isBoardAdmin - 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 @@ -102,8 +98,19 @@ template(name="boardHeaderBar") 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}} + i.fa.fa-caret-down + if $eq boardView 'board-view-lists' + i.fa.fa-trello + if $eq boardView 'board-view-swimlanes' + i.fa.fa-th-large + // unless collapseSwimlane + // i.fa.fa-th-large + // if collapseSwimlane + // i.fa.fa-play + if $eq boardView 'board-view-cal' + i.fa.fa-calendar + span {{#if boardView}}{{_ boardView}}{{else}}{{_ 'board-view-lists'}}{{/if}} + //span {{#if collapseSwimlane}}{{_ 'board-view-collapse'}}{{else}}{{#if boardView}}{{_ boardView}}{{else}}{{_ 'board-view-lists'}}{{/if}}{{/if}} if canModifyBoard a.board-header-btn.js-multiselection-activate( @@ -168,6 +175,51 @@ template(name="boardChangeWatchPopup") i.fa.fa-check span.sub-name {{_ 'muted-info'}} +template(name="boardChangeViewPopup") + ul.pop-over-list + li + with "board-view-lists" + a.js-open-lists-view + i.fa.fa-trello.colorful + | {{_ 'board-view-lists'}} + if $eq Utils.boardView "board-view-lists" + i.fa.fa-check + li + with "board-view-swimlanes" + a.js-open-swimlanes-view + i.fa.fa-th-large.colorful + | {{_ 'board-view-swimlanes'}} + if $eq Utils.boardView "board-view-swimlanes" + i.fa.fa-check + //li + // with "board-view-collapse" + // a.js-open-collapse-view + // i.fa.fa-play.colorful + // | {{_ 'board-view-collapse'}} + // if $eq Utils.boardView "board-view-collapse" + // i.fa.fa-check + li + with "board-view-cal" + a.js-open-cal-view + i.fa.fa-calendar.colorful + | {{_ 'board-view-cal'}} + if $eq Utils.boardView "board-view-cal" + i.fa.fa-check + if currentUser.isAdmin + hr + li + with "board-view-rules" + a.js-open-rules-view(title="{{_ 'rules'}}") + i.fa.fa-magic + | {{_ 'rules'}} + else if currentUser.isBoardAdmin + hr + li + with "board-view-rules" + a.js-open-rules-view(title="{{_ 'rules'}}") + i.fa.fa-magic + | {{_ 'rules'}} + template(name="createBoard") form label @@ -194,6 +246,20 @@ template(name="createBoard") | / a.js-board-template {{_ 'template'}} +//template(name="listsortPopup") +// h2 +// | {{_ 'list-sort-by'}} +// hr +// ul.pop-over-list +// each value in allowedSortValues +// li +// a.js-sort-by(name="{{value.name}}") +// if $eq sortby value.name +// i(class="fa {{Direction}}") +// | {{_ value.label }}{{_ value.shortLabel}} +// if $eq sortby value.name +// i(class="fa fa-check") + template(name="boardChangeTitlePopup") form label diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index cb84c233..ffbb9b72 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -1,3 +1,7 @@ +/* +const DOWNCLS = 'fa-sort-down'; +const UPCLS = 'fa-sort-up'; +*/ Template.boardMenuPopup.events({ 'click .js-rename-board': Popup.open('boardChangeTitle'), 'click .js-custom-fields'() { @@ -80,7 +84,27 @@ BlazeComponent.extendComponent({ const currentBoard = Boards.findOne(Session.get('currentBoard')); return currentBoard && currentBoard.stars >= 2; }, - + /* + showSort() { + return Meteor.user().hasSortBy(); + }, + directionClass() { + return this.currentDirection() === -1 ? DOWNCLS : UPCLS; + }, + changeDirection() { + const direction = 0 - this.currentDirection() === -1 ? '-' : ''; + Meteor.call('setListSortBy', direction + this.currentListSortBy()); + }, + currentDirection() { + return Meteor.user().getListSortByDirection(); + }, + currentListSortBy() { + return Meteor.user().getListSortBy(); + }, + listSortShortDesc() { + return `list-label-short-${this.currentListSortBy()}`; + }, + */ events() { return [ { @@ -94,30 +118,25 @@ BlazeComponent.extendComponent({ 'click .js-open-archived-board'() { Modal.open('archivedBoards'); }, - 'click .js-toggle-board-view'() { - const currentUser = Meteor.user(); - 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 { - currentUser.setBoardView('board-view-swimlanes'); - } - }, + 'click .js-toggle-board-view': Popup.open('boardChangeView'), 'click .js-toggle-sidebar'() { Sidebar.toggle(); }, 'click .js-open-filter-view'() { Sidebar.setView('filter'); }, + /* + 'click .js-open-sort-view'(evt) { + const target = evt.target; + if (target.tagName === 'I') { + // click on the text, popup choices + this.changeDirection(); + } else { + // change the sort order + Popup.open('listsort')(evt); + } + }, + */ 'click .js-filter-reset'(event) { event.stopPropagation(); Sidebar.setView(); @@ -126,9 +145,6 @@ 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(); @@ -156,6 +172,40 @@ Template.boardHeaderBar.helpers({ !Meteor.user().isCommentOnly() ); }, + boardView() { + return Utils.boardView(); + }, + //collapseSwimlane() { + // import { Cookies } from 'meteor/ostrio:cookies'; + // const cookies = new Cookies(); + // if (cookies.has('collapseSwimlane')) { + // return true; + // } else { + // return false; + // } + //}, +}); + +Template.boardChangeViewPopup.events({ + 'click .js-open-lists-view'() { + Utils.setBoardView('board-view-lists'); + Popup.close(); + }, + 'click .js-open-swimlanes-view'() { + Utils.setBoardView('board-view-swimlanes'); + Popup.close(); + }, + //'click .js-open-collapse-view'() { + // Utils.setBoardView('board-view-collapse'); + //Popup.close(); + 'click .js-open-cal-view'() { + Utils.setBoardView('board-view-cal'); + Popup.close(); + }, + 'click .js-open-rules-view'() { + Modal.openWide('rulesMain'); + Popup.close(); + }, }); const CreateBoard = BlazeComponent.extendComponent({ @@ -277,3 +327,75 @@ BlazeComponent.extendComponent({ ]; }, }).register('boardChangeWatchPopup'); + +/* +BlazeComponent.extendComponent({ + onCreated() { + //this.sortBy = new ReactiveVar(); + ////this.sortDirection = new ReactiveVar(); + //this.setSortBy(); + this.downClass = DOWNCLS; + this.upClass = UPCLS; + }, + allowedSortValues() { + const types = []; + const pushed = {}; + Meteor.user() + .getListSortTypes() + .forEach(type => { + const key = type.replace(/^-/, ''); + if (pushed[key] === undefined) { + types.push({ + name: key, + label: `list-label-${key}`, + shortLabel: `list-label-short-${key}`, + }); + pushed[key] = 1; + } + }); + return types; + }, + Direction() { + return Meteor.user().getListSortByDirection() === -1 + ? this.downClass + : this.upClass; + }, + sortby() { + return Meteor.user().getListSortBy(); + }, + + setSortBy(type = null) { + const user = Meteor.user(); + if (type === null) { + type = user._getListSortBy(); + } else { + let value = ''; + if (type.map) { + // is an array + value = (type[1] === -1 ? '-' : '') + type[0]; + } + Meteor.call('setListSortBy', value); + } + //this.sortBy.set(type[0]); + //this.sortDirection.set(type[1]); + }, + + events() { + return [ + { + 'click .js-sort-by'(evt) { + evt.preventDefault(); + const target = evt.target; + const sortby = target.getAttribute('name'); + const down = !!target.querySelector(`.${this.upClass}`); + const direction = down ? -1 : 1; + this.setSortBy([sortby, direction]); + if (Utils.isMiniScreen) { + Popup.close(); + } + }, + }, + ]; + }, +}).register('listsortPopup'); +*/ diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index 843f1eb7..e4439155 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -131,6 +131,8 @@ Template.previewClipboardImagePopup.onRendered(() => { direct(results); }, }); + } else { + direct(results); } } }; diff --git a/client/components/cards/cardDate.js b/client/components/cards/cardDate.js index 91205f1c..cb54b033 100644 --- a/client/components/cards/cardDate.js +++ b/client/components/cards/cardDate.js @@ -105,7 +105,7 @@ Template.dateBadge.helpers({ // editCardReceivedDatePopup (class extends DatePicker { onCreated() { - super.onCreated(); + super.onCreated(moment().format('YYYY-MM-DD HH:mm')); this.data().getReceived() && this.date.set(moment(this.data().getReceived())); } @@ -122,7 +122,7 @@ Template.dateBadge.helpers({ // editCardStartDatePopup (class extends DatePicker { onCreated() { - super.onCreated(); + super.onCreated(moment().format('YYYY-MM-DD HH:mm')); this.data().getStart() && this.date.set(moment(this.data().getStart())); } @@ -148,7 +148,7 @@ Template.dateBadge.helpers({ // editCardDueDatePopup (class extends DatePicker { onCreated() { - super.onCreated(); + super.onCreated('1970-01-01 17:00:00'); this.data().getDue() && this.date.set(moment(this.data().getDue())); } @@ -171,7 +171,7 @@ Template.dateBadge.helpers({ // editCardEndDatePopup (class extends DatePicker { onCreated() { - super.onCreated(); + super.onCreated(moment().format('YYYY-MM-DD HH:mm')); this.data().getEnd() && this.date.set(moment(this.data().getEnd())); } @@ -237,7 +237,7 @@ class CardReceivedDate extends CardDate { 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)) || + (startAt && theDate.isAfter(startAt)) || (endAt && theDate.isAfter(endAt)) || (dueAt && theDate.isAfter(dueAt)) ) @@ -344,9 +344,9 @@ class CardEndDate extends CardDate { let classes = 'end-date' + ' '; const dueAt = this.data().getDue(); const theDate = this.date.get(); - if (theDate.diff(dueAt, 'days') >= 2) classes += 'long-overdue'; - else if (theDate.diff(dueAt, 'days') >= 0) classes += 'due'; - else if (theDate.diff(dueAt, 'days') >= -2) classes += 'almost-due'; + if (!dueAt) classes += ''; + else if (theDate.isBefore(dueAt)) classes += 'current'; + else if (theDate.isAfter(dueAt)) classes += 'due'; return classes; } diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index 13b6bd13..2b4f44b9 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -4,9 +4,14 @@ template(name="cardDetails") +inlinedForm(classNames="js-card-details-title") +editCardTitleForm else - a.fa.fa-times-thin.close-card-details.js-close-card-details - if currentUser.isBoardMember - a.fa.fa-navicon.card-details-menu.js-open-card-details-menu + unless isMiniScreen + a.fa.fa-times-thin.close-card-details.js-close-card-details + if currentUser.isBoardMember + a.fa.fa-navicon.card-details-menu.js-open-card-details-menu + if isMiniScreen + a.fa.fa-times-thin.close-card-details-mobile-web.js-close-card-details + if currentUser.isBoardMember + a.fa.fa-navicon.card-details-menu-mobile-web.js-open-card-details-menu h2.card-details-title.js-card-title( class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}") +viewer @@ -73,6 +78,16 @@ template(name="cardDetails") a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}") i.fa.fa-plus + .card-details-item.card-details-item-assignees + h3.card-details-item-title {{_ 'assignee'}} + each getAssignees + +userAvatarAssignee(userId=this cardId=../_id) + | {{! XXX Hack to hide syntaxic coloration /// }} + if canModifyCard + unless assigneeSelected + a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}") + i.fa.fa-plus + .card-details-item.card-details-item-labels h3.card-details-item-title {{_ 'labels'}} a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}") @@ -296,6 +311,54 @@ template(name="cardMembersPopup") if isCardMember i.fa.fa-check +template(name="cardAssigneesPopup") + ul.pop-over-list.js-card-assignee-list + each board.activeMembers + li.item(class="{{#if isCardAssignee}}active{{/if}}") + a.name.js-select-assignee(href="#") + +userAvatar(userId=user._id) + span.full-name + = user.profile.fullname + | (<span class="username">{{ user.username }}</span>) + if isCardAssignee + i.fa.fa-check + +template(name="userAvatarAssignee") + a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})") + if userData.profile.avatarUrl + img.avatar.avatar-image(src="{{userData.profile.avatarUrl}}") + else + +userAvatarAssigneeInitials(userId=userData._id) + + if showStatus + span.assignee-presence-status(class=presenceStatusClassName) + span.member-type(class=memberType) + + unless isSandstorm + if showEdit + if $eq currentUser._id userData._id + a.edit-avatar.js-change-avatar + i.fa.fa-pencil + +template(name="cardAssigneePopup") + .board-assignee-menu + .mini-profile-info + +userAvatar(userId=user._id showEdit=true) + .info + h3= user.profile.fullname + p.quiet @{{ user.username }} + ul.pop-over-list + if currentUser.isNotCommentOnly + li: a.js-remove-assignee {{_ 'remove-member-from-card'}} + + if $eq currentUser._id user._id + with currentUser + li: a.js-edit-profile {{_ 'edit-profile'}} + +template(name="userAvatarAssigneeInitials") + svg.avatar.avatar-assignee-initials(viewBox="0 0 {{viewPortWidth}} 15") + text(x="50%" y="13" text-anchor="middle")= initials + template(name="cardMorePopup") p.quiet span.clearfix diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index cd8813f5..7bb54223 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -121,11 +121,6 @@ BlazeComponent.extendComponent({ // Send Webhook but not create Activities records --- const card = this.currentData(); const userId = Meteor.userId(); - //console.log(`userId: ${userId}`); - //console.log(`cardId: ${card._id}`); - //console.log(`boardId: ${card.boardId}`); - //console.log(`listId: ${card.listId}`); - //console.log(`swimlaneId: ${card.swimlaneId}`); const params = { userId, cardId: card._id, @@ -134,16 +129,25 @@ BlazeComponent.extendComponent({ user: Meteor.user().username, url: '', }; - //console.log('looking for integrations...'); + const integrations = Integrations.find({ - boardId: card.boardId, - type: 'outgoing-webhooks', + boardId: { $in: [card.boardId, Integrations.Const.GLOBAL_WEBHOOK_ID] }, enabled: true, activities: { $in: ['CardDetailsRendered', 'all'] }, }).fetch(); - //console.log(`Investigation length: ${integrations.length}`); + if (integrations.length > 0) { - Meteor.call('outgoingWebhooks', integrations, 'CardSelected', params); + integrations.forEach(integration => { + Meteor.call( + 'outgoingWebhooks', + integration, + 'CardSelected', + params, + () => { + return; + }, + ); + }); } //------------- } @@ -309,6 +313,8 @@ BlazeComponent.extendComponent({ }, 'click .js-member': Popup.open('cardMember'), 'click .js-add-members': Popup.open('cardMembers'), + 'click .js-assignee': Popup.open('cardAssignee'), + 'click .js-add-assignees': Popup.open('cardAssignees'), 'click .js-add-labels': Popup.open('cardLabels'), 'click .js-received-date': Popup.open('editCardReceivedDate'), 'click .js-start-date': Popup.open('editCardStartDate'), @@ -321,6 +327,19 @@ BlazeComponent.extendComponent({ parentComponent.showOverlay.set(true); parentComponent.mouseHasEnterCardDetails = true; }, + 'mousedown .js-card-details'() { + Session.set('cardDetailsIsDragging', false); + Session.set('cardDetailsIsMouseDown', true); + }, + 'mousemove .js-card-details'() { + if (Session.get('cardDetailsIsMouseDown')) { + Session.set('cardDetailsIsDragging', true); + } + }, + 'mouseup .js-card-details'() { + Session.set('cardDetailsIsDragging', false); + Session.set('cardDetailsIsMouseDown', false); + }, 'click #toggleButton'() { Meteor.call('toggleSystemMessages'); }, @@ -329,6 +348,58 @@ BlazeComponent.extendComponent({ }, }).register('cardDetails'); +Template.cardDetails.helpers({ + userData() { + // We need to handle a special case for the search results provided by the + // `matteodem:easy-search` package. Since these results gets published in a + // separate collection, and not in the standard Meteor.Users collection as + // expected, we use a component parameter ("property") to distinguish the + // two cases. + const userCollection = this.esSearch ? ESSearchResults : Users; + return userCollection.findOne(this.userId, { + fields: { + profile: 1, + username: 1, + }, + }); + }, + + assigneeSelected() { + if (this.getAssignees().length === 0) { + return false; + } else { + return true; + } + }, + + memberType() { + const user = Users.findOne(this.userId); + return user && user.isBoardAdmin() ? 'admin' : 'normal'; + }, + + presenceStatusClassName() { + const user = Users.findOne(this.userId); + const userPresence = presences.findOne({ userId: this.userId }); + if (user && user.isInvitedTo(Session.get('currentBoard'))) return 'pending'; + else if (!userPresence) return 'disconnected'; + else if (Session.equals('currentBoard', userPresence.state.currentBoardId)) + return 'active'; + else return 'idle'; + }, +}); + +Template.userAvatarAssigneeInitials.helpers({ + initials() { + const user = Users.findOne(this.userId); + return user && user.getInitials(); + }, + + viewPortWidth() { + const user = Users.findOne(this.userId); + return ((user && user.getInitials().length) || 1) * 12; + }, +}); + // We extends the normal InlinedForm component to support UnsavedEdits draft // feature. (class extends InlinedForm { @@ -386,6 +457,7 @@ Template.cardDetailsActionsPopup.helpers({ Template.cardDetailsActionsPopup.events({ 'click .js-members': Popup.open('cardMembers'), + 'click .js-assignees': Popup.open('cardAssignees'), 'click .js-labels': Popup.open('cardLabels'), 'click .js-attachments': Popup.open('cardAttachments'), 'click .js-custom-fields': Popup.open('cardCustomFields'), @@ -777,7 +849,14 @@ BlazeComponent.extendComponent({ EscapeActions.register( 'detailsPane', () => { - Utils.goBoardId(Session.get('currentBoard')); + if (Session.get('cardDetailsIsDragging')) { + // Reset dragging status as the mouse landed outside the cardDetails template area and this will prevent a mousedown event from firing + Session.set('cardDetailsIsDragging', false); + Session.set('cardDetailsIsMouseDown', false); + } else { + // Prevent close card when the user is selecting text and moves the mouse cursor outside the card detail area + Utils.goBoardId(Session.get('currentBoard')); + } }, () => { return !Session.equals('currentCard', null); @@ -786,3 +865,76 @@ EscapeActions.register( noClickEscapeOn: '.js-card-details,.board-sidebar,#header', }, ); + +Template.cardAssigneesPopup.events({ + 'click .js-select-assignee'(event) { + const card = Cards.findOne(Session.get('currentCard')); + const assigneeId = this.userId; + card.toggleAssignee(assigneeId); + event.preventDefault(); + }, +}); + +Template.cardAssigneesPopup.helpers({ + isCardAssignee() { + const card = Template.parentData(); + const cardAssignees = card.getAssignees(); + + return _.contains(cardAssignees, this.userId); + }, + + user() { + return Users.findOne(this.userId); + }, +}); + +Template.cardAssigneePopup.helpers({ + userData() { + // We need to handle a special case for the search results provided by the + // `matteodem:easy-search` package. Since these results gets published in a + // separate collection, and not in the standard Meteor.Users collection as + // expected, we use a component parameter ("property") to distinguish the + // two cases. + const userCollection = this.esSearch ? ESSearchResults : Users; + return userCollection.findOne(this.userId, { + fields: { + profile: 1, + username: 1, + }, + }); + }, + + memberType() { + const user = Users.findOne(this.userId); + return user && user.isBoardAdmin() ? 'admin' : 'normal'; + }, + + presenceStatusClassName() { + const user = Users.findOne(this.userId); + const userPresence = presences.findOne({ userId: this.userId }); + if (user && user.isInvitedTo(Session.get('currentBoard'))) return 'pending'; + else if (!userPresence) return 'disconnected'; + else if (Session.equals('currentBoard', userPresence.state.currentBoardId)) + return 'active'; + else return 'idle'; + }, + + isCardAssignee() { + const card = Template.parentData(); + const cardAssignees = card.getAssignees(); + + return _.contains(cardAssignees, this.userId); + }, + + user() { + return Users.findOne(this.userId); + }, +}); + +Template.cardAssigneePopup.events({ + 'click .js-remove-assignee'() { + Cards.findOne(this.cardId).unassignAssignee(this.userId); + Popup.close(); + }, + 'click .js-edit-profile': Popup.open('editProfile'), +}); diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl index cd475072..3fc4d047 100644 --- a/client/components/cards/cardDetails.styl +++ b/client/components/cards/cardDetails.styl @@ -1,5 +1,80 @@ @import 'nib' +// Assignee, code copied from wekan/client/users/userAvatar.styl + +avatar-radius = 50% + +.assignee + border-radius: 3px + display: block + position: relative + float: left + height: 30px + width: @height + margin: 0 4px 4px 0 + cursor: pointer + user-select: none + z-index: 1 + text-decoration: none + border-radius: avatar-radius + + .avatar + overflow: hidden + border-radius: avatar-radius + + &.avatar-assignee-initials + height: 70% + width: @height + padding: 15% + background-color: #dbdbdb + color: #444444 + position: absolute + + &.avatar-image + height: 100% + width: @height + + .assignee-presence-status + background-color: #b3b3b3 + border: 1px solid #fff + border-radius: 50% + height: 7px + width: @height + position: absolute + right: -1px + bottom: -1px + border: 1px solid white + z-index: 15 + + &.active + background: #64c464 + border-color: #daf1da + + &.idle + background: #e4e467 + border-color: #f7f7d4 + + &.disconnected + background: #bdbdbd + border-color: #ededed + + &.pending + background: #e44242 + border-color: #f1dada + + + + &.add-assignee + display: flex + align-items: center + justify-content: center + box-shadow: 0 0 0 2px darken(white, 25%) inset + + &:hover, &.is-active + box-shadow: 0 0 0 2px darken(white, 60%) inset + +// Other card details + .card-details padding: 0 flex-shrink: 0 @@ -32,7 +107,9 @@ border-bottom: 1px solid darken(white, 14%) .close-card-details, - .card-details-menu + .card-details-menu, + .close-card-details-mobile-web, + .card-details-menu-mobile-web float: right .close-card-details @@ -40,10 +117,20 @@ padding: 5px margin-right: -8px + .close-card-details-mobile-web + font-size: 24px + padding: 5px + margin-right: 40px + .card-details-menu font-size: 17px padding: 10px + .card-details-menu-mobile-web + font-size: 17px + padding: 10px + margin-right: 30px + .card-details-watch font-size: 17px padding-left: 7px @@ -93,6 +180,7 @@ margin-right: 0 &.card-details-item-labels, &.card-details-item-members, + &.card-details-item-assignees, &.card-details-item-received, &.card-details-item-start, &.card-details-item-due, diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index 3806ce41..79672f8c 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -3,6 +3,13 @@ template(name="minicard") class="{{#if isLinkedCard}}linked-card{{/if}}" class="{{#if isLinkedBoard}}linked-board{{/if}}" class="minicard-{{colorClass}}") + if isMiniScreen + .handle + .fa.fa-arrows + unless isMiniScreen + if showDesktopDragHandles + .handle + .fa.fa-arrows if cover .minicard-cover(style="background-image: url('{{cover.url}}');") if labels @@ -15,8 +22,6 @@ template(name="minicard") if hiddenMinicardLabelText .minicard-label(class="card-label-{{color}}" title="{{name}}") .minicard-title - .handle - .fa.fa-arrows if $eq 'prefix-with-full-path' currentBoard.presentParentTask .parent-prefix | {{ parentString ' > ' }} @@ -53,6 +58,8 @@ template(name="minicard") if getDue .date +minicardDueDate + if getEnd + +minicardEndDate if getSpentTime .date +cardSpentTime @@ -69,6 +76,12 @@ template(name="minicard") +viewer = trueValue + if getAssignees + .minicard-assignees.js-minicard-assignees + each getAssignees + +userAvatar(userId=this) + hr + if getMembers .minicard-members.js-minicard-members each getMembers diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index 4c25c11d..a9f92dec 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -18,7 +18,13 @@ BlazeComponent.extendComponent({ }, { 'click .js-toggle-minicard-label-text'() { - Meteor.call('toggleMinicardLabelText'); + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('hiddenMinicardLabelText')) { + cookies.remove('hiddenMinicardLabelText'); //true + } else { + cookies.set('hiddenMinicardLabelText', 'true'); //true + } }, }, ]; @@ -26,7 +32,32 @@ BlazeComponent.extendComponent({ }).register('minicard'); Template.minicard.helpers({ + showDesktopDragHandles() { + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).showDesktopDragHandles; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('showDesktopDragHandles')) { + return true; + } else { + return false; + } + } + }, hiddenMinicardLabelText() { - return Meteor.user().hasHiddenMinicardLabelText(); + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).hiddenMinicardLabelText; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('hiddenMinicardLabelText')) { + return true; + } else { + return false; + } + } }, }); diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl index c4172572..8607e118 100644 --- a/client/components/cards/minicard.styl +++ b/client/components/cards/minicard.styl @@ -105,7 +105,7 @@ right: 5px; top: 5px; display:none; - @media only screen and (max-width: 1199px) { + @media only screen { display:block; } .fa-arrows @@ -160,9 +160,10 @@ padding-left: 0px line-height: 12px - .minicard-members + .minicard-members, + .minicard-assignees float: right - margin: 2px -8px -2px 0 + margin: 2px -8px 12px 0 .member float: right @@ -170,10 +171,17 @@ height: 28px width: @height + .assignee + float: right + border-radius: 50% + height: 28px + width: @height + + .badges margin-top: 10px - .minicard-members:empty + .minicard-members:empty, + .minicard-assignees:empty display: none &.minicard-composer diff --git a/client/components/lists/list.js b/client/components/lists/list.js index c2b39be9..e58ea430 100644 --- a/client/components/lists/list.js +++ b/client/components/lists/list.js @@ -22,21 +22,15 @@ BlazeComponent.extendComponent({ function userIsMember() { return ( - Meteor.user() && - Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + Meteor.user() + && Meteor.user().isBoardMember() + && !Meteor.user().isCommentOnly() ); } 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', @@ -79,16 +73,15 @@ BlazeComponent.extendComponent({ const listId = Blaze.getData(ui.item.parents('.list').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() + Utils.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 + Utils.boardView() === 'board-view-lists' + || Utils.boardView() === 'board-view-cal' + || !Utils.boardView ) swimlaneId = currentBoard.getDefaultSwimline()._id; @@ -122,8 +115,32 @@ BlazeComponent.extendComponent({ // ugly touch event hotfix enableClickOnTouch(itemsSelector); - // Disable drag-dropping if the current user is not a board member or is comment only + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + this.autorun(() => { + let showDesktopDragHandles = false; + currentUser = Meteor.user(); + if (currentUser) { + showDesktopDragHandles = (currentUser.profile || {}) + .showDesktopDragHandles; + } else if (cookies.has('showDesktopDragHandles')) { + showDesktopDragHandles = true; + } else { + showDesktopDragHandles = false; + } + + if (!Utils.isMiniScreen() && showDesktopDragHandles) { + $cards.sortable({ + handle: '.handle', + }); + } else { + $cards.sortable({ + handle: '.minicard', + }); + } + + // Disable drag-dropping if the current user is not a board member or is comment only $cards.sortable('option', 'disabled', !userIsMember()); }); @@ -155,6 +172,23 @@ BlazeComponent.extendComponent({ }, }).register('list'); +Template.list.helpers({ + showDesktopDragHandles() { + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).showDesktopDragHandles; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('showDesktopDragHandles')) { + return true; + } else { + return false; + } + } + }, +}); + Template.miniList.events({ 'click .js-select-list'() { const listId = this._id; diff --git a/client/components/lists/list.styl b/client/components/lists/list.styl index 81938c1a..27cf678c 100644 --- a/client/components/lists/list.styl +++ b/client/components/lists/list.styl @@ -84,17 +84,16 @@ padding-left: 10px color: #a6a6a6 - .list-header-menu position: absolute padding: 27px 19px margin-top: 1px top: -7px - right: -7px + right: 3px .list-header-plus-icon color: #a6a6a6 - margin-right: 10px + margin-right: 15px .highlight color: #ce1414 @@ -165,7 +164,16 @@ @media screen and (max-width: 800px) .list-header-menu - margin-right: 30px + position: absolute + padding: 27px 19px + margin-top: 1px + top: -7px + margin-right: 7px + right: -3px + + .list-header + .list-header-name + margin-left: 1.4rem .mini-list flex: 0 0 60px @@ -221,9 +229,17 @@ padding: 7px top: 50% transform: translateY(-50%) - right: 17px + right: 47px font-size: 20px + .list-header-handle + position: absolute + padding: 7px + top: 50% + transform: translateY(-50%) + right: 10px + font-size: 24px + .link-board-wrapper display: flex align-items: baseline diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js index a1a4c11a..b0974705 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -48,7 +48,6 @@ BlazeComponent.extendComponent({ const board = this.data().board(); let linkedId = ''; let swimlaneId = ''; - const boardView = (Meteor.user().profile || {}).boardView; let cardType = 'cardType-card'; if (title) { if (board.isTemplatesBoard()) { @@ -71,14 +70,14 @@ BlazeComponent.extendComponent({ }); cardType = 'cardType-linkedBoard'; } - } else if (boardView === 'board-view-swimlanes') + } else if (Utils.boardView() === 'board-view-swimlanes') swimlaneId = this.parentComponent() .parentComponent() .data()._id; else if ( - boardView === 'board-view-lists' || - boardView === 'board-view-cal' || - !boardView + Utils.boardView() === 'board-view-lists' || + Utils.boardView() === 'board-view-cal' || + !Utils.boardView ) swimlaneId = board.getDefaultSwimline()._id; @@ -157,9 +156,8 @@ BlazeComponent.extendComponent({ }, idOrNull(swimlaneId) { - const currentUser = Meteor.user(); if ( - (currentUser.profile || {}).boardView === 'board-view-swimlanes' || + Utils.boardView() === 'board-view-swimlanes' || this.data() .board() .isTemplatesBoard() @@ -397,10 +395,9 @@ BlazeComponent.extendComponent({ '.js-swimlane', ); this.swimlaneId = ''; - const boardView = (Meteor.user().profile || {}).boardView; - if (boardView === 'board-view-swimlanes') + if (Utils.boardView() === 'board-view-swimlanes') this.swimlaneId = Blaze.getData(swimlane[0])._id; - else if (boardView === 'board-view-lists' || !boardView) + else if (Utils.boardView() === 'board-view-lists' || !Utils.boardView) this.swimlaneId = Swimlanes.findOne({ boardId: this.boardId })._id; }, @@ -580,7 +577,7 @@ BlazeComponent.extendComponent({ const swimlane = $(Popup._getTopStack().openerElement).parents( '.js-swimlane', ); - if ((Meteor.user().profile || {}).boardView === 'board-view-swimlanes') + if (Utils.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 @@ -701,15 +698,26 @@ BlazeComponent.extendComponent({ this.listId = this.parentComponent().data()._id; this.swimlaneId = ''; - let user = Meteor.user(); - if (user) { - const boardView = (Meteor.user().profile || {}).boardView; - if (boardView === 'board-view-swimlanes') { - this.swimlaneId = this.parentComponent() - .parentComponent() - .parentComponent() - .data()._id; + const isSandstorm = + Meteor.settings && + Meteor.settings.public && + Meteor.settings.public.sandstorm; + + if (isSandstorm) { + const user = Meteor.user(); + if (user) { + if (Utils.boardView() === 'board-view-swimlanes') { + this.swimlaneId = this.parentComponent() + .parentComponent() + .parentComponent() + .data()._id; + } } + } else if (Utils.boardView() === 'board-view-swimlanes') { + this.swimlaneId = this.parentComponent() + .parentComponent() + .parentComponent() + .data()._id; } }, diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade index f930e57a..631f68a0 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -9,6 +9,7 @@ template(name="listHeader") if currentList a.list-header-left-icon.fa.fa-angle-left.js-unselect-list h2.list-header-name( + title="{{ moment modifiedAt 'LLL' }}" class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}js-open-inlined-form is-editable{{/unless}}{{/if}}") +viewer = title @@ -29,16 +30,22 @@ template(name="listHeader") if canSeeAddCard a.js-add-card.fa.fa-plus.list-header-plus-icon a.fa.fa-navicon.js-open-list-menu + a.list-header-handle.handle.fa.fa-arrows.js-list-handle else a.list-header-menu-icon.fa.fa-angle-right.js-select-list + a.list-header-handle.handle.fa.fa-arrows.js-list-handle else if currentUser.isBoardMember if isWatching i.list-header-watch-icon.fa.fa-eye div.list-header-menu unless currentUser.isCommentOnly + //if isBoardAdmin + // a.fa.js-list-star.list-header-plus-icon(class="fa-star{{#unless starred}}-o{{/unless}}") if canSeeAddCard a.js-add-card.fa.fa-plus.list-header-plus-icon a.fa.fa-navicon.js-open-list-menu + if showDesktopDragHandles + a.list-header-handle.handle.fa.fa-arrows.js-list-handle template(name="editListTitleForm") .list-composer diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index e8a82499..34322fa9 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -7,12 +7,26 @@ BlazeComponent.extendComponent({ canSeeAddCard() { const list = Template.currentData(); return ( - !list.getWipLimit('enabled') || - list.getWipLimit('soft') || - !this.reachedWipLimit() + !list.getWipLimit('enabled') + || list.getWipLimit('soft') + || !this.reachedWipLimit() ); }, + isBoardAdmin() { + return Meteor.user().isBoardAdmin(); + }, + starred(check = undefined) { + const list = Template.currentData(); + const status = list.isStarred(); + if (check === undefined) { + // just check + return status; + } else { + list.star(!status); + return !status; + } + }, editTitle(event) { event.preventDefault(); const newTitle = this.childComponents('inlinedForm')[0] @@ -30,14 +44,18 @@ BlazeComponent.extendComponent({ }, limitToShowCardsCount() { - return Meteor.user().getLimitToShowCardsCount(); + const currentUser = Meteor.user(); + if (currentUser) { + return Meteor.user().getLimitToShowCardsCount(); + } else { + return false; + } }, cardsCount() { const list = Template.currentData(); let swimlaneId = ''; - const boardView = (Meteor.user().profile || {}).boardView; - if (boardView === 'board-view-swimlanes') + if (Utils.boardView() === 'board-view-swimlanes') swimlaneId = this.parentComponent() .parentComponent() .data()._id; @@ -48,8 +66,8 @@ BlazeComponent.extendComponent({ reachedWipLimit() { const list = Template.currentData(); return ( - list.getWipLimit('enabled') && - list.getWipLimit('value') <= list.cards().count() + list.getWipLimit('enabled') + && list.getWipLimit('value') <= list.cards().count() ); }, @@ -61,6 +79,10 @@ BlazeComponent.extendComponent({ events() { return [ { + 'click .js-list-star'(event) { + event.preventDefault(); + this.starred(!this.starred()); + }, 'click .js-open-list-menu': Popup.open('listAction'), 'click .js-add-card'(event) { const listDom = $(event.target).parents( @@ -80,6 +102,23 @@ BlazeComponent.extendComponent({ }, }).register('listHeader'); +Template.listHeader.helpers({ + showDesktopDragHandles() { + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).showDesktopDragHandles; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('showDesktopDragHandles')) { + return true; + } else { + return false; + } + } + }, +}); + Template.listActionPopup.helpers({ isWipLimitEnabled() { return Template.currentData().getWipLimit('enabled'); @@ -138,8 +177,8 @@ BlazeComponent.extendComponent({ const list = Template.currentData(); if ( - list.getWipLimit('soft') && - list.getWipLimit('value') < list.cards().count() + list.getWipLimit('soft') + && list.getWipLimit('value') < list.cards().count() ) { list.setWipLimit(list.cards().count()); } @@ -150,8 +189,8 @@ BlazeComponent.extendComponent({ const list = Template.currentData(); // Prevent user from using previously stored wipLimit.value if it is less than the current number of cards in the list if ( - !list.getWipLimit('enabled') && - list.getWipLimit('value') < list.cards().count() + !list.getWipLimit('enabled') + && list.getWipLimit('value') < list.cards().count() ) { list.setWipLimit(list.cards().count()); } diff --git a/client/components/main/editor.js b/client/components/main/editor.js index 91403086..39c03aa9 100755 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -94,7 +94,13 @@ Template.editor.onRendered(() => { currentBoard .activeMembers() .map(member => { - const username = Users.findOne(member.userId).username; + const user = Users.findOne(member.userId); + if (user._id === Meteor.userId()) { + return null; + } + const value = user.username; + const username = + value && value.match(/\s+/) ? `"${value}"` : value; return username.includes(term) ? username : null; }) .filter(Boolean), @@ -120,9 +126,10 @@ Template.editor.onRendered(() => { ? [ ['view', ['fullscreen']], ['table', ['table']], - ['font', ['bold', 'underline']], - //['fontsize', ['fontsize']], + ['font', ['bold']], ['color', ['color']], + ['insert', ['video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled + //['fontsize', ['fontsize']], ] : [ ['style', ['style']], @@ -156,25 +163,45 @@ Template.editor.onRendered(() => { } return undefined; }; + let popupShown = false; inputs.each(function(idx, input) { mSummernotes[idx] = $(input).summernote({ placeholder, callbacks: { + onKeydown(e) { + if (popupShown) { + e.preventDefault(); + } + }, + onKeyup(e) { + if (popupShown) { + e.preventDefault(); + } + }, onInit(object) { const originalInput = this; + const setAutocomplete = function(jEditor) { + if (jEditor !== undefined) { + jEditor.escapeableTextComplete(mentions).on({ + 'textComplete:show'() { + popupShown = true; + }, + 'textComplete:hide'() { + popupShown = false; + }, + }); + } + }; $(originalInput).on('submitted', function() { // resetCommentInput has been called if (!this.value) { const sn = getSummernote(this); - sn && sn.summernote('reset'); - object && object.editingArea.find('.note-placeholder').show(); + sn && sn.summernote('code', ''); } }); const jEditor = object && object.editable; const toolbar = object && object.toolbar; - if (jEditor !== undefined) { - jEditor.escapeableTextComplete(mentions); - } + setAutocomplete(jEditor); if (toolbar !== undefined) { const fBtn = toolbar.find('.btn-fullscreen'); fBtn.on('click', function() { @@ -264,7 +291,7 @@ Template.editor.onRendered(() => { const someNote = getSummernote(object); const original = someNote.summernote('code'); const cleaned = cleanPastedHTML(original); //this is where to call whatever clean function you want. I have mine in a different file, called CleanPastedHTML. - someNote.summernote('reset'); //clear original + someNote.summernote('code', ''); //clear original someNote.summernote('pasteHTML', cleaned); //this sets the displayed content editor to the cleaned pasted code. }; setTimeout(function() { @@ -325,11 +352,12 @@ Blaze.Template.registerHelper( } return member; }); - const mentionRegex = /\B@([\w.]*)/gi; + const mentionRegex = /\B@(?:(?:"([\w.\s]*)")|([\w.]+))/gi; // including space in username let currentMention; while ((currentMention = mentionRegex.exec(content)) !== null) { - const [fullMention, username] = currentMention; + const [fullMention, quoteduser, simple] = currentMention; + const username = quoteduser || simple; const knowedUser = _.findWhere(knowedUsers, { username }); if (!knowedUser) { continue; diff --git a/client/components/main/layouts.styl b/client/components/main/layouts.styl index 56c35284..01ce2f16 100644 --- a/client/components/main/layouts.styl +++ b/client/components/main/layouts.styl @@ -381,6 +381,10 @@ a display: block word-wrap: break-word + table + word-wrap: normal + word-break: normal + ol list-style-type: decimal padding-left: 20px diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl index ff00eef3..023cba3d 100644 --- a/client/components/main/popup.styl +++ b/client/components/main/popup.styl @@ -130,7 +130,8 @@ $popupWidth = 300px .popup-container-depth-{depth} transform: translateX(- depth * $popupWidth) -.select-members-list +.select-members-list, +.select-avatars-list margin-bottom: 8px .pop-over-list @@ -230,7 +231,8 @@ $popupWidth = 300px min-height: 56px position: relative - .member + .member, + .avatar position: absolute top: 2px left: 2px diff --git a/client/components/settings/peopleBody.js b/client/components/settings/peopleBody.js index a9f2247c..8610034e 100644 --- a/client/components/settings/peopleBody.js +++ b/client/components/settings/peopleBody.js @@ -17,7 +17,7 @@ BlazeComponent.extendComponent({ this.autorun(() => { const limit = this.page.get() * usersPerPage; - this.subscribe('people', limit, () => { + this.subscribe('people', this.findUsersOptions.get(), limit, () => { this.loadNextPageLocked = false; const nextPeakBefore = this.callFirstWith(null, 'getNextPeak'); this.calculateNextPeak(); @@ -85,7 +85,7 @@ BlazeComponent.extendComponent({ const users = Users.find(this.findUsersOptions.get(), { fields: { _id: true }, }); - this.number.set(users.count()); + this.number.set(users.count(false)); return users; }, peopleNumber() { diff --git a/client/components/settings/settingBody.jade b/client/components/settings/settingBody.jade index 8eb584dc..04b635e8 100644 --- a/client/components/settings/settingBody.jade +++ b/client/components/settings/settingBody.jade @@ -18,6 +18,8 @@ template(name="setting") a.js-setting-menu(data-id="announcement-setting") {{_ 'admin-announcement'}} li a.js-setting-menu(data-id="layout-setting") {{_ 'layout'}} + li + a.js-setting-menu(data-id="webhook-setting") {{_ 'global-webhook'}} .main-body if loading.get +spinner @@ -31,6 +33,12 @@ template(name="setting") +announcementSettings else if layoutSetting.get +layoutSettings + else if webhookSetting.get + +webhookSettings + +template(name="webhookSettings") + span + +outgoingWebhooksPopup template(name="general") ul#registration-setting.setting-detail diff --git a/client/components/settings/settingBody.js b/client/components/settings/settingBody.js index f9b5c08d..4ff5aedd 100644 --- a/client/components/settings/settingBody.js +++ b/client/components/settings/settingBody.js @@ -7,11 +7,13 @@ BlazeComponent.extendComponent({ this.accountSetting = new ReactiveVar(false); this.announcementSetting = new ReactiveVar(false); this.layoutSetting = new ReactiveVar(false); + this.webhookSetting = new ReactiveVar(false); Meteor.subscribe('setting'); Meteor.subscribe('mailServer'); Meteor.subscribe('accountSettings'); Meteor.subscribe('announcements'); + Meteor.subscribe('globalwebhooks'); }, setError(error) { @@ -83,6 +85,7 @@ BlazeComponent.extendComponent({ this.accountSetting.set('account-setting' === targetID); this.announcementSetting.set('announcement-setting' === targetID); this.layoutSetting.set('layout-setting' === targetID); + this.webhookSetting.set('webhook-setting' === targetID); } }, diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index 2dfe41b3..ccfadc0c 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -135,22 +135,30 @@ template(name="archiveBoardPopup") 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") + a.flex + span {{_ 'disable-webhook'}} + b + .materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}") + input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title) + input.js-outgoing-webhooks-url(type="text" name="url" value=url autofocus) + input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token") + select.js-outgoing-webhooks-type(name="type") + each _type in types + if($eq _type this.type) + option(value=_type selected="selected") {{_ _type}} + else + option(value=_type) {{_ _type}} + input(type="hidden" value=this.type name="_type") + 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'}}") + input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" autofocus) + input.js-outgoing-webhooks-url(placeholder="{{_ 'URL' }}" type="text" name="url") + input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" name="token") + select.js-outgoing-webhooks-type(name="type") + each _type in types + option(value=_type) {{_ _type}} + input.primary.wide(type="submit" value="{{_ 'create'}}") template(name="boardMenuPopup") ul.pop-over-list diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index f7efb1e8..6bb22f39 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -1,6 +1,8 @@ Sidebar = null; const defaultView = 'home'; +const MCB = '.materialCheckBox'; +const CKCLS = 'is-checked'; const viewTitles = { filter: 'filter-cards', @@ -105,7 +107,18 @@ BlazeComponent.extendComponent({ 'click .js-toggle-sidebar': this.toggle, 'click .js-back-home': this.setView, 'click .js-toggle-minicard-label-text'() { - Meteor.call('toggleMinicardLabelText'); + currentUser = Meteor.user(); + if (currentUser) { + Meteor.call('toggleMinicardLabelText'); + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('hiddenMinicardLabelText')) { + cookies.remove('hiddenMinicardLabelText'); + } else { + cookies.set('hiddenMinicardLabelText', 'true'); + } + } }, 'click .js-shortcuts'() { FlowRouter.go('shortcuts'); @@ -119,7 +132,18 @@ Blaze.registerHelper('Sidebar', () => Sidebar); Template.homeSidebar.helpers({ hiddenMinicardLabelText() { - return Meteor.user().hasHiddenMinicardLabelText(); + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).hiddenMinicardLabelText; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('hiddenMinicardLabelText')) { + return true; + } else { + return false; + } + } }, }); @@ -280,44 +304,71 @@ Template.membersWidget.events({ }); BlazeComponent.extendComponent({ + boardId() { + return Session.get('currentBoard') || Integrations.Const.GLOBAL_WEBHOOK_ID; + }, integrations() { - const boardId = Session.get('currentBoard'); + const boardId = this.boardId(); return Integrations.find({ boardId: `${boardId}` }).fetch(); }, - - integration(id) { - const boardId = Session.get('currentBoard'); - return Integrations.findOne({ _id: id, boardId: `${boardId}` }); + types() { + return Integrations.Const.WEBHOOK_TYPES; + }, + integration(cond) { + const boardId = this.boardId(); + const condition = { boardId, ...cond }; + for (const k in condition) { + if (!condition[k]) delete condition[k]; + } + return Integrations.findOne(condition); + }, + onCreated() { + this.disabled = new ReactiveVar(false); }, - events() { return [ { + 'click a.flex'(evt) { + this.disabled.set(!this.disabled.get()); + $(evt.target).toggleClass(CKCLS, this.disabled.get()); + }, submit(evt) { evt.preventDefault(); const url = evt.target.url.value; - const boardId = Session.get('currentBoard'); + const boardId = this.boardId(); let id = null; let integration = null; + const title = evt.target.title.value; + const token = evt.target.token.value; + const type = evt.target.type.value; + const enabled = !this.disabled.get(); + let remove = false; + const values = { + url, + type, + token, + title, + enabled, + }; 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); - } + integration = this.integration({ _id: id }); + remove = !url; + } else if (url) { + integration = this.integration({ url, token }); + } + if (remove) { + Integrations.remove(integration._id); + } else if (integration && integration._id) { + Integrations.update(integration._id, { + $set: values, + }); } else if (url) { Integrations.insert({ + ...values, userId: Meteor.userId(), enabled: true, - type: 'outgoing-webhooks', - url: `${url}`, - boardId: `${boardId}`, + boardId, activities: ['all'], }); } @@ -474,12 +525,12 @@ BlazeComponent.extendComponent({ evt.preventDefault(); this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks; this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks); - $('.js-field-has-subtasks .materialCheckBox').toggleClass( - 'is-checked', + $(`.js-field-has-subtasks ${MCB}`).toggleClass( + CKCLS, this.currentBoard.allowsSubtasks, ); $('.js-field-has-subtasks').toggleClass( - 'is-checked', + CKCLS, this.currentBoard.allowsSubtasks, ); $('.js-field-deposit-board').prop( @@ -515,15 +566,12 @@ BlazeComponent.extendComponent({ ]; options.forEach(function(element) { if (element !== value) { - $(`#${element} .materialCheckBox`).toggleClass( - 'is-checked', - false, - ); - $(`#${element}`).toggleClass('is-checked', false); + $(`#${element} ${MCB}`).toggleClass(CKCLS, false); + $(`#${element}`).toggleClass(CKCLS, false); } }); - $(`#${value} .materialCheckBox`).toggleClass('is-checked', true); - $(`#${value}`).toggleClass('is-checked', true); + $(`#${value} ${MCB}`).toggleClass(CKCLS, true); + $(`#${value}`).toggleClass(CKCLS, true); this.currentBoard.setPresentParentTask(value); evt.preventDefault(); }, diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade index 55ab213a..5f929cb9 100644 --- a/client/components/sidebar/sidebarFilters.jade +++ b/client/components/sidebar/sidebarFilters.jade @@ -5,6 +5,10 @@ template(name="filterSidebar") ul.sidebar-list + span {{_ 'list-filter-label'}} + form.js-list-filter + input(type="text") + ul.sidebar-list li(class="{{#if Filter.labelIds.isSelected undefined}}active{{/if}}") a.name.js-toggle-label-filter span.sidebar-list-item-description diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js index 3483d00c..ee0176b9 100644 --- a/client/components/sidebar/sidebarFilters.js +++ b/client/components/sidebar/sidebarFilters.js @@ -4,6 +4,10 @@ BlazeComponent.extendComponent({ events() { return [ { + 'submit .js-list-filter'(evt) { + evt.preventDefault(); + Filter.lists.set(this.find('.js-list-filter input').value.trim()); + }, 'click .js-toggle-label-filter'(evt) { evt.preventDefault(); Filter.labelIds.toggle(this.currentData()._id); diff --git a/client/components/sidebar/sidebarSearches.jade b/client/components/sidebar/sidebarSearches.jade index 96877c50..4ee7fc9c 100644 --- a/client/components/sidebar/sidebarSearches.jade +++ b/client/components/sidebar/sidebarSearches.jade @@ -2,6 +2,10 @@ template(name="searchSidebar") form.js-search-term-form input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto") .list-body.js-perfect-scrollbar + .minilists.clearfix.js-minilists + each (lists) + a.minilist-wrapper.js-minilist(href=absoluteUrl) + +minilist(this) .minicards.clearfix.js-minicards each (results) a.minicard-wrapper.js-minicard(href=absoluteUrl) diff --git a/client/components/sidebar/sidebarSearches.js b/client/components/sidebar/sidebarSearches.js index 8944c04e..02677260 100644 --- a/client/components/sidebar/sidebarSearches.js +++ b/client/components/sidebar/sidebarSearches.js @@ -8,6 +8,11 @@ BlazeComponent.extendComponent({ return currentBoard.searchCards(this.term.get()); }, + lists() { + const currentBoard = Boards.findOne(Session.get('currentBoard')); + return currentBoard.searchLists(this.term.get()); + }, + events() { return [ { diff --git a/client/components/swimlanes/swimlaneHeader.jade b/client/components/swimlanes/swimlaneHeader.jade index 8c6aa5a3..72a7f054 100644 --- a/client/components/swimlanes/swimlaneHeader.jade +++ b/client/components/swimlanes/swimlaneHeader.jade @@ -16,6 +16,11 @@ template(name="swimlaneFixedHeader") unless currentUser.isCommentOnly a.fa.fa-plus.js-open-add-swimlane-menu.swimlane-header-plus-icon a.fa.fa-navicon.js-open-swimlane-menu + unless isMiniScreen + if showDesktopDragHandles + a.swimlane-header-handle.handle.fa.fa-arrows.js-swimlane-header-handle + if isMiniScreen + a.swimlane-header-miniscreen-handle.handle.fa.fa-arrows.js-swimlane-header-handle template(name="editSwimlaneTitleForm") .list-composer diff --git a/client/components/swimlanes/swimlaneHeader.js b/client/components/swimlanes/swimlaneHeader.js index ee21d100..69971b05 100644 --- a/client/components/swimlanes/swimlaneHeader.js +++ b/client/components/swimlanes/swimlaneHeader.js @@ -28,6 +28,23 @@ BlazeComponent.extendComponent({ }, }).register('swimlaneHeader'); +Template.swimlaneHeader.helpers({ + showDesktopDragHandles() { + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).showDesktopDragHandles; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('showDesktopDragHandles')) { + return true; + } else { + return false; + } + } + }, +}); + Template.swimlaneActionPopup.events({ 'click .js-set-swimlane-color': Popup.open('setSwimlaneColor'), 'click .js-close-swimlane'(event) { diff --git a/client/components/swimlanes/swimlanes.jade b/client/components/swimlanes/swimlanes.jade index 3ad43777..b2e03afe 100644 --- a/client/components/swimlanes/swimlanes.jade +++ b/client/components/swimlanes/swimlanes.jade @@ -1,24 +1,47 @@ template(name="swimlane") .swimlane +swimlaneHeader - .swimlane.js-lists.js-swimlane - if isMiniScreen - if currentListIsInThisSwimlane _id - +list(currentList) - unless currentList + unless collapseSwimlane + .swimlane.js-lists.js-swimlane + if isMiniScreen + if currentListIsInThisSwimlane _id + +list(currentList) + unless currentList + each lists + +miniList(this) + if currentUser.isBoardMember + unless currentUser.isCommentOnly + +addListForm + else each lists - +miniList(this) + +list(this) + if currentCardIsInThisList _id ../_id + +cardDetails(currentCard) if currentUser.isBoardMember unless currentUser.isCommentOnly +addListForm - else - each lists - +list(this) - if currentCardIsInThisList _id ../_id - +cardDetails(currentCard) - if currentUser.isBoardMember - unless currentUser.isCommentOnly - +addListForm + //if collapseSwimlane + // // Minimize swimlanes next 2 lines below https://www.w3schools.com/howto/howto_js_accordion.asp + // button(class="accordion") + // div(class="panel") + // .swimlane.js-lists.js-swimlane + // if isMiniScreen + // if currentListIsInThisSwimlane _id + // +list(currentList) + // unless currentList + // each lists + // +miniList(this) + // if currentUser.isBoardMember + // unless currentUser.isCommentOnly + // +addListForm + // else + // each lists + // +list(this) + // if currentCardIsInThisList _id ../_id + // +cardDetails(currentCard) + // if currentUser.isBoardMember + // unless currentUser.isCommentOnly + // +addListForm template(name="listsGroup") .swimlane.list-group.js-lists @@ -42,7 +65,7 @@ template(name="listsGroup") +addListForm template(name="addListForm") - .list.list-composer.js-list-composer + .list.list-composer.js-list-composer(class="{{#if isMiniScreen}}mini-list{{/if}}") .list-header-add +inlinedForm(autoclose=false) input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}" diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index e0857003..9bc093be 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -3,8 +3,8 @@ const { calculateIndex, enableClickOnTouch } = Utils; function currentListIsInThisSwimlane(swimlaneId) { const currentList = Lists.findOne(Session.get('currentList')); return ( - currentList && - (currentList.swimlaneId === swimlaneId || currentList.swimlaneId === '') + currentList + && (currentList.swimlaneId === swimlaneId || currentList.swimlaneId === '') ); } @@ -12,14 +12,14 @@ function currentCardIsInThisList(listId, swimlaneId) { const currentCard = Cards.findOne(Session.get('currentCard')); const currentUser = Meteor.user(); if ( - currentUser && - currentUser.profile && - currentUser.profile.boardView === 'board-view-swimlanes' + currentUser + && currentUser.profile + && Utils.boardView() === 'board-view-swimlanes' ) return ( - currentCard && - currentCard.listId === listId && - currentCard.swimlaneId === swimlaneId + currentCard + && currentCard.listId === listId + && currentCard.swimlaneId === swimlaneId ); // Default view: board-view-lists else return currentCard && currentCard.listId === listId; @@ -56,7 +56,6 @@ function initSortable(boardComponent, $listsDom) { $listsDom.sortable({ tolerance: 'pointer', helper: 'clone', - handle: '.js-list-header', items: '.js-list:not(.js-list-composer)', placeholder: 'list placeholder', distance: 7, @@ -91,21 +90,47 @@ function initSortable(boardComponent, $listsDom) { function userIsMember() { return ( - Meteor.user() && - Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + Meteor.user() + && Meteor.user().isBoardMember() + && !Meteor.user().isCommentOnly() ); } - // Disable drag-dropping while in multi-selection mode, or if the current user - // is not a board member boardComponent.autorun(() => { + let showDesktopDragHandles = false; + currentUser = Meteor.user(); + if (currentUser) { + showDesktopDragHandles = (currentUser.profile || {}) + .showDesktopDragHandles; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('showDesktopDragHandles')) { + showDesktopDragHandles = true; + } else { + showDesktopDragHandles = false; + } + } + + if (!Utils.isMiniScreen() && showDesktopDragHandles) { + $listsDom.sortable({ + handle: '.js-list-handle', + }); + } else { + $listsDom.sortable({ + handle: '.js-list-header', + }); + } + const $listDom = $listsDom; if ($listDom.data('sortable')) { $listsDom.sortable( 'option', 'disabled', - MultiSelection.isActive() || !userIsMember(), + // Disable drag-dropping when user is not member + !userIsMember(), + // Not disable drag-dropping while in multi-selection mode + // MultiSelection.isActive() || !userIsMember(), ); } }); @@ -121,6 +146,26 @@ BlazeComponent.extendComponent({ } initSortable(boardComponent, $listsDom); + + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('collapseSwimlane')) { + // Minimize swimlanes start https://www.w3schools.com/howto/howto_js_accordion.asp + const acc = document.getElementsByClassName('accordion'); + let i; + for (i = 0; i < acc.length; i++) { + acc[i].addEventListener('click', function() { + this.classList.toggle('active'); + const panel = this.nextElementSibling; + if (panel.style.maxHeight) { + panel.style.maxHeight = null; + } else { + panel.style.maxHeight = `${panel.scrollHeight}px`; + } + }); + } + // Minimize swimlanes end https://www.w3schools.com/howto/howto_js_accordion.asp + } }, onCreated() { this.draggingActive = new ReactiveVar(false); @@ -151,16 +196,32 @@ BlazeComponent.extendComponent({ // define a list of elements in which we disable the dragging because // the user will legitimately expect to be able to select some text with // his mouse. - const noDragInside = [ - 'a', - 'input', - 'textarea', - 'p', - '.js-list-header', - ]; + + let showDesktopDragHandles = false; + currentUser = Meteor.user(); + if (currentUser) { + showDesktopDragHandles = (currentUser.profile || {}) + .showDesktopDragHandles; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('showDesktopDragHandles')) { + showDesktopDragHandles = true; + } else { + showDesktopDragHandles = false; + } + } + + const noDragInside = ['a', 'input', 'textarea', 'p'].concat( + Utils.isMiniScreen() + || (!Utils.isMiniScreen() && showDesktopDragHandles) + ? ['.js-list-handle', '.js-swimlane-header-handle'] + : ['.js-list-header'], + ); + if ( - $(evt.target).closest(noDragInside.join(',')).length === 0 && - this.$('.swimlane').prop('clientHeight') > evt.offsetY + $(evt.target).closest(noDragInside.join(',')).length === 0 + && this.$('.swimlane').prop('clientHeight') > evt.offsetY ) { this._isDragging = true; this._lastDragPositionX = evt.clientX; @@ -194,8 +255,8 @@ BlazeComponent.extendComponent({ onCreated() { this.currentBoard = Boards.findOne(Session.get('currentBoard')); this.isListTemplatesSwimlane = - this.currentBoard.isTemplatesBoard() && - this.currentData().isListTemplatesSwimlane(); + this.currentBoard.isTemplatesBoard() + && this.currentData().isListTemplatesSwimlane(); this.currentSwimlane = this.currentData(); }, @@ -233,11 +294,25 @@ BlazeComponent.extendComponent({ }).register('addListForm'); Template.swimlane.helpers({ + showDesktopDragHandles() { + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).showDesktopDragHandles; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('showDesktopDragHandles')) { + return true; + } else { + return false; + } + } + }, canSeeAddList() { return ( - Meteor.user() && - Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + Meteor.user() + && Meteor.user().isBoardMember() + && !Meteor.user().isCommentOnly() ); }, }); @@ -253,6 +328,11 @@ BlazeComponent.extendComponent({ return false; } } + if (Filter.lists._isActive()) { + if (!list.title.match(Filter.lists.getRegexSelector())) { + return false; + } + } if (Filter.hideEmpty.isSelected()) { const swimlaneId = this.parentComponent() .parentComponent() diff --git a/client/components/swimlanes/swimlanes.styl b/client/components/swimlanes/swimlanes.styl index 1056e1e3..ca5611cc 100644 --- a/client/components/swimlanes/swimlanes.styl +++ b/client/components/swimlanes/swimlanes.styl @@ -1,5 +1,41 @@ @import 'nib' +/* +// Minimize swimlanes start https://www.w3schools.com/howto/howto_js_accordion.asp + +.accordion + cursor: pointer + width: 30px + height: 20px + border: none + outline: none + font-size: 18px + transition: 0.4s + padding-top: 0px + margin-top: 0px + +.accordion:after + // Unicode triagle right: + content: '\25B6' + color: #777 + font-weight: bold + float: left + +.active:after + // Unicode triangle down: + content: '\25BC' + +.panel + width: 100% + max-height: 0 + overflow: hidden + transition: max-height 0.2s ease-out + margin: 0px + padding: 0px + +// Minimize swimlanes end https://www.w3schools.com/howto/howto_js_accordion.asp +*/ + .swimlane // Even if this background color is the same as the body we can't leave it // transparent, because that won't work during a swimlane drag. @@ -25,22 +61,22 @@ cursor: grabbing .swimlane-header-wrap - display: flex; - flex-direction: row; - flex: 1 0 100%; - background-color: #ccc; + display: flex + flex-direction: row + flex: 1 0 100% + background-color: #ccc .swimlane-header - font-size: 14px; + font-size: 14px padding: 5px 5px - font-weight: bold; - min-height: 9px; - width: 100%; - overflow: hidden; - -o-text-overflow: ellipsis; - text-overflow: ellipsis; - word-wrap: break-word; - text-align: center; + font-weight: bold + min-height: 9px + width: 100% + overflow: hidden + -o-text-overflow: ellipsis + text-overflow: ellipsis + word-wrap: break-word + text-align: center .swimlane-header-menu position: absolute @@ -50,6 +86,22 @@ margin-left: 5px margin-right: 10px + .swimlane-header-handle + position: absolute + padding: 7px + top: 50% + transform: translateY(-50%) + left: 230px + font-size: 18px + + .swimlane-header-miniscreen-handle + position: absolute + padding: 7px + top: 50% + transform: translateY(-50%) + left: 87vw + font-size: 24px + .list-group height: 100% diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade index 946bdab1..50a80396 100644 --- a/client/components/users/userHeader.jade +++ b/client/components/users/userHeader.jade @@ -79,6 +79,11 @@ template(name="changeSettingsPopup") if hiddenSystemMessages i.fa.fa-check li + a.js-toggle-desktop-drag-handles + | {{_ 'show-desktop-drag-handles'}} + if showDesktopDragHandles + i.fa.fa-check + li label.bold | {{_ 'show-cards-minimum-count'}} input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="0" max="99" onkeydown="return false") diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js index 36fb2020..1f0e3ef0 100644 --- a/client/components/users/userHeader.js +++ b/client/components/users/userHeader.js @@ -5,10 +5,22 @@ Template.headerUserBar.events({ Template.memberMenuPopup.helpers({ templatesBoardId() { - return Meteor.user().getTemplatesBoardId(); + currentUser = Meteor.user(); + if (currentUser) { + return Meteor.user().getTemplatesBoardId(); + } else { + // No need to getTemplatesBoardId on public board + return false; + } }, templatesBoardSlug() { - return Meteor.user().getTemplatesBoardSlug(); + currentUser = Meteor.user(); + if (currentUser) { + return Meteor.user().getTemplatesBoardSlug(); + } else { + // No need to getTemplatesBoardSlug() on public board + return false; + } }, }); @@ -161,17 +173,74 @@ Template.changeLanguagePopup.events({ }); Template.changeSettingsPopup.helpers({ + showDesktopDragHandles() { + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).showDesktopDragHandles; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('showDesktopDragHandles')) { + return true; + } else { + return false; + } + } + }, hiddenSystemMessages() { - return Meteor.user().hasHiddenSystemMessages(); + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).hasHiddenSystemMessages; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('hasHiddenSystemMessages')) { + return true; + } else { + return false; + } + } }, showCardsCountAt() { - return Meteor.user().getLimitToShowCardsCount(); + currentUser = Meteor.user(); + if (currentUser) { + return Meteor.user().getLimitToShowCardsCount(); + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + return cookies.get('limitToShowCardsCount'); + } }, }); Template.changeSettingsPopup.events({ + 'click .js-toggle-desktop-drag-handles'() { + currentUser = Meteor.user(); + if (currentUser) { + Meteor.call('toggleDesktopDragHandles'); + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('showDesktopDragHandles')) { + cookies.remove('showDesktopDragHandles'); + } else { + cookies.set('showDesktopDragHandles', 'true'); + } + } + }, 'click .js-toggle-system-messages'() { - Meteor.call('toggleSystemMessages'); + currentUser = Meteor.user(); + if (currentUser) { + Meteor.call('toggleSystemMessages'); + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.has('hasHiddenSystemMessages')) { + cookies.remove('hasHiddenSystemMessages'); + } else { + cookies.set('hasHiddenSystemMessages', 'true'); + } + } }, 'click .js-apply-show-cards-at'(event, templateInstance) { event.preventDefault(); @@ -180,7 +249,14 @@ Template.changeSettingsPopup.events({ 10, ); if (!isNaN(minLimit)) { - Meteor.call('changeLimitToShowCardsCount', minLimit); + currentUser = Meteor.user(); + if (currentUser) { + Meteor.call('changeLimitToShowCardsCount', minLimit); + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + cookies.set('limitToShowCardsCount', minLimit); + } Popup.back(); } }, diff --git a/client/lib/datepicker.js b/client/lib/datepicker.js index eb5b60b8..8ad66c5f 100644 --- a/client/lib/datepicker.js +++ b/client/lib/datepicker.js @@ -3,10 +3,11 @@ DatePicker = BlazeComponent.extendComponent({ return 'datepicker'; }, - onCreated() { + onCreated(defaultTime = '1970-01-01 08:00:00') { this.error = new ReactiveVar(''); this.card = this.data(); this.date = new ReactiveVar(moment.invalid()); + this.defaultTime = defaultTime; }, onRendered() { @@ -21,7 +22,15 @@ DatePicker = BlazeComponent.extendComponent({ function(evt) { this.find('#date').value = moment(evt.date).format('L'); this.error.set(''); - this.find('#time').focus(); + const timeInput = this.find('#time'); + timeInput.focus(); + if (!timeInput.value) { + const currentHour = evt.date.getHours(); + const defaultMoment = moment( + currentHour > 0 ? evt.date : this.defaultTime, + ); // default to 8:00 am local time + timeInput.value = defaultMoment.format('LT'); + } }.bind(this), ); diff --git a/client/lib/filter.js b/client/lib/filter.js index 1ca3a280..592eb4ab 100644 --- a/client/lib/filter.js +++ b/client/lib/filter.js @@ -439,6 +439,14 @@ class AdvancedFilter { const commands = this._filterToCommands(); return this._arrayToSelector(commands); } + getRegexSelector() { + // generate a regex for filter list + this._dep.depend(); + return new RegExp( + `^.*${this._filter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*$`, + 'i', + ); + } } // The global Filter object. @@ -455,6 +463,7 @@ Filter = { hideEmpty: new SetFilter(), customFields: new SetFilter('_id'), advanced: new AdvancedFilter(), + lists: new AdvancedFilter(), // we need the ability to filter list by name as well _fields: ['labelIds', 'members', 'archive', 'hideEmpty', 'customFields'], @@ -468,7 +477,9 @@ Filter = { return ( _.any(this._fields, fieldName => { return this[fieldName]._isActive(); - }) || this.advanced._isActive() + }) || + this.advanced._isActive() || + this.lists._isActive() ); }, @@ -533,6 +544,7 @@ Filter = { const filter = this[fieldName]; filter.reset(); }); + this.lists.reset(); this.advanced.reset(); this.resetExceptions(); }, diff --git a/client/lib/textComplete.js b/client/lib/textComplete.js index 0261d7f6..8b6dc1f7 100644 --- a/client/lib/textComplete.js +++ b/client/lib/textComplete.js @@ -45,6 +45,7 @@ $.fn.escapeableTextComplete = function(strategies, options, ...otherArgs) { }); }, }); + return this; }; EscapeActions.register('textcomplete', () => {}, () => dropdownMenuIsOpened, { diff --git a/client/lib/utils.js b/client/lib/utils.js index 81835929..c90dd749 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -1,10 +1,59 @@ Utils = { + setBoardView(view) { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + currentUser = Meteor.user(); + if (currentUser) { + Meteor.user().setBoardView(view); + } else if (view === 'board-view-lists') { + cookies.set('boardView', 'board-view-lists'); //true + } else if (view === 'board-view-swimlanes') { + cookies.set('boardView', 'board-view-swimlanes'); //true + //} else if (view === 'board-view-collapse') { + // cookies.set('boardView', 'board-view-swimlane'); //true + // cookies.set('collapseSwimlane', 'true'); //true + } else if (view === 'board-view-cal') { + cookies.set('boardView', 'board-view-cal'); //true + } + }, + + unsetBoardView() { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + cookies.remove('boardView'); + cookies.remove('collapseSwimlane'); + }, + + boardView() { + currentUser = Meteor.user(); + if (currentUser) { + return (currentUser.profile || {}).boardView; + } else { + import { Cookies } from 'meteor/ostrio:cookies'; + const cookies = new Cookies(); + if (cookies.get('boardView') === 'board-view-lists') { + return 'board-view-lists'; + } else if ( + cookies.get('boardView') === 'board-view-swimlanes' + //&& !cookies.has('collapseSwimlane') + ) { + return 'board-view-swimlanes'; + //} else if (cookies.has('collapseSwimlane')) { + // return 'board-view-swimlanes'; + } else if (cookies.get('boardView') === 'board-view-cal') { + return 'board-view-cal'; + } else { + return false; + } + } + }, + // XXX We should remove these two methods goBoardId(_id) { const board = Boards.findOne(_id); return ( - board && - FlowRouter.go('board', { + board + && FlowRouter.go('board', { id: board._id, slug: board.slug, }) @@ -15,15 +64,14 @@ Utils = { const card = Cards.findOne(_id); const board = Boards.findOne(card.boardId); return ( - board && - FlowRouter.go('card', { + board + && FlowRouter.go('card', { cardId: card._id, boardId: board._id, slug: board.slug, }) ); }, - MAX_IMAGE_PIXEL: Meteor.settings.public.MAX_IMAGE_PIXEL, COMPRESS_RATIO: Meteor.settings.public.IMAGE_COMPRESS_RATIO, processUploadedAttachment(card, fileObj, callback) { @@ -188,8 +236,8 @@ Utils = { }; if ( - 'ontouchstart' in window || - (window.DocumentTouch && document instanceof window.DocumentTouch) + 'ontouchstart' in window + || (window.DocumentTouch && document instanceof window.DocumentTouch) ) { return true; } @@ -210,8 +258,8 @@ Utils = { calculateTouchDistance(touchA, touchB) { return Math.sqrt( - Math.pow(touchA.screenX - touchB.screenX, 2) + - Math.pow(touchA.screenY - touchB.screenY, 2), + Math.pow(touchA.screenX - touchB.screenX, 2) + + Math.pow(touchA.screenY - touchB.screenY, 2), ); }, @@ -228,9 +276,9 @@ Utils = { }); $(document).on('touchend', selector, function(e) { if ( - touchStart && - lastTouch && - Utils.calculateTouchDistance(touchStart, lastTouch) <= 20 + touchStart + && lastTouch + && Utils.calculateTouchDistance(touchStart, lastTouch) <= 20 ) { e.preventDefault(); const clickEvent = document.createEvent('MouseEvents'); |