diff options
Diffstat (limited to 'client')
75 files changed, 2732 insertions, 933 deletions
diff --git a/client/00-startup.js b/client/00-startup.js new file mode 100644 index 00000000..4a717b67 --- /dev/null +++ b/client/00-startup.js @@ -0,0 +1,6 @@ +// PWA +if ('serviceWorker' in navigator) { + window.addEventListener('load', function() { + navigator.serviceWorker.register('/pwa-service-worker.js'); + }); +} diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade index deb73072..c86936a0 100644 --- a/client/components/activities/activities.jade +++ b/client/components/activities/activities.jade @@ -8,234 +8,201 @@ template(name="activities") +cardActivities template(name="boardActivities") - each currentBoard.activities - .activity - +userAvatar(userId=user._id) - p.activity-desc - +memberName(user=user) + each activityData in currentBoard.activities + +activity(activity=activityData card=card mode=mode) - if($eq activityType 'deleteAttachment') - | {{{_ 'activity-delete-attach' cardLink}}}. +template(name="cardActivities") + each activityData in currentCard.activities + +activity(activity=activityData card=card mode=mode) + +template(name="activity") + .activity + +userAvatar(userId=activity.user._id) + p.activity-desc + +memberName(user=activity.user) + + //- attachment activity ------------------------------------------------- + if($eq activity.activityType 'deleteAttachment') + | {{{_ 'activity-delete-attach' cardLink}}}. + + if($eq activity.activityType 'addAttachment') + | {{{_ 'activity-attached' attachmentLink cardLink}}}. + if($neq mode 'board') + if activity.attachment.isImage + img.attachment-image-preview(src=activity.attachment.url) + + //- board activity ------------------------------------------------------ + if($eq mode 'board') + if($eq activity.activityType 'createBoard') + | {{_ 'activity-created' boardLabel}}. - if($eq activityType 'addAttachment') - | {{{_ 'activity-attached' attachmentLink cardLink}}}. + if($eq activity.activityType 'importBoard') + | {{{_ 'activity-imported-board' boardLabel sourceLink}}}. - if($eq activityType 'addBoardMember') + if($eq activity.activityType 'addBoardMember') | {{{_ 'activity-added' memberLink boardLabel}}}. - if($eq activityType 'addComment') - | {{{_ 'activity-on' cardLink}}} - a.activity-comment(href="{{ card.absoluteUrl }}") - +viewer - = comment.text - - if($eq activityType 'addChecklist') - | {{{_ 'activity-checklist-added' cardLink}}}. - .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 activity.activityType 'removeBoardMember') + | {{{_ 'activity-excluded' memberLink boardLabel}}}. - if($eq activityType 'checklistUncompleted') - | {{{_ 'activity-checklist-uncompleted' checklist.title cardLink}}}. + //- card activity ------------------------------------------------------- + if($eq activity.activityType 'createCard') + if($eq mode 'card') + | {{{_ 'activity-added' cardLabel activity.listName}}}. + else + | {{{_ 'activity-added' cardLabel boardLabel}}}. - 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 activity.activityType 'importCard') + | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}. - if($eq activityType 'archivedCard') - | {{{_ 'activity-archived' cardLink}}}. + if($eq activity.activityType 'moveCard') + | {{{_ 'activity-moved' cardLabel activity.oldList.title activity.list.title}}}. - if($eq activityType 'archivedList') - | {{_ 'activity-archived' list.title}}. + if($eq activity.activityType 'moveCardBoard') + | {{{_ 'activity-moved' cardLink activity.oldBoardName activity.boardName}}}. - if($eq activityType 'archivedSwimlane') - | {{_ 'activity-archived' swimlane.title}}. + if($eq activity.activityType 'archivedCard') + | {{{_ 'activity-archived' cardLink}}}. - if($eq activityType 'createBoard') - | {{_ 'activity-created' boardLabel}}. + if($eq activity.activityType 'restoredCard') + | {{{_ 'activity-sent' cardLink boardLabel}}}. - if($eq activityType 'createCard') - | {{{_ 'activity-added' cardLink boardLabel}}}. + //- checklist activity -------------------------------------------------- + if($eq activity.activityType 'addChecklist') + | {{{_ 'activity-checklist-added' cardLink}}}. + if($eq mode 'card') + .activity-checklist + +viewer + = activity.checklist.title + else + a.activity-checklist(href="{{ activity.card.absoluteUrl }}") + +viewer + = activity.checklist.title - if($eq activityType 'createCustomField') - | {{_ 'activity-customfield-created' customField}}. + if($eq activity.activityType 'removedChecklist') + | {{{_ 'activity-checklist-removed' cardLink}}}. - if($eq activityType 'createList') - | {{_ 'activity-added' list.title boardLabel}}. + if($eq activity.activityType 'completeChecklist') + | {{{_ 'activity-checklist-completed' activity.checklist.title cardLink}}}. - if($eq activityType 'createSwimlane') - | {{_ 'activity-added' swimlane.title boardLabel}}. + if($eq activity.activityType 'uncompleteChecklist') + | {{{_ 'activity-checklist-uncompleted' activity.checklist.title cardLink}}}. - if($eq activityType 'removeList') - | {{_ 'activity-removed' title boardLabel}}. + if($eq activity.activityType 'checkedItem') + | {{{_ 'activity-checked-item' checkItem activity.checklist.title cardLink}}}. - if($eq activityType 'importBoard') - | {{{_ 'activity-imported-board' boardLabel sourceLink}}}. + if($eq activity.activityType 'uncheckedItem') + | {{{_ 'activity-unchecked-item' checkItem activity.checklist.title cardLink}}}. - if($eq activityType 'importCard') - | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}. + if($eq activity.activityType 'addChecklistItem') + | {{{_ 'activity-checklist-item-added' activity.checklist.title cardLink}}}. + .activity-checklist(href="{{ activity.card.absoluteUrl }}") + +viewer + = activity.checklistItem.title - if($eq activityType 'importList') - | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}. + if($eq activity.activityType 'removedChecklistItem') + | {{{_ 'activity-checklist-item-removed' activity.checklist.title cardLink}}}. - if($eq activityType 'joinMember') - if($eq user._id member._id) - | {{{_ 'activity-joined' cardLink}}}. + //- comment activity ---------------------------------------------------- + if($eq mode 'card') + //- if we are in card mode we display the comment in a way that it + //- can be edited by the owner + if($eq activity.activityType 'addComment') + +inlinedForm(classNames='js-edit-comment') + +editor(autofocus=true) + = activity.comment.text + .edit-controls + button.primary(type="submit") {{_ 'edit'}} 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}}}. - - if($eq activityType 'removeBoardMember') - | {{{_ 'activity-excluded' memberLink boardLabel}}}. + .activity-comment + +viewer + = activity.comment.text + span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }} + if ($eq currentUser._id activity.comment.userId) + = ' - ' + a.js-open-inlined-form {{_ "edit"}} + = ' - ' + a.js-delete-comment {{_ "delete"}} - if($eq activityType 'restoredCard') - | {{{_ 'activity-sent' cardLink boardLabel}}}. + if($eq activity.activityType 'deleteComment') + | {{{_ 'activity-deleteComment' currentData.commentId}}}. - if($eq activityType 'addedLabel') - | {{{_ 'activity-added-label' lastLabel cardLink}}}. + if($eq activity.activityType 'editComment') + | {{{_ 'activity-editComment' currentData.commentId}}}. + else + //- if we are not in card mode we only display a summary of the comment + if($eq activity.activityType 'addComment') + | {{{_ 'activity-on' cardLink}}} + a.activity-comment(href="{{ activity.card.absoluteUrl }}") + +viewer + = activity.comment.text - if($eq activityType 'removedLabel') - | {{{_ 'activity-removed-label' lastLabel cardLink}}}. + //- customField activity ------------------------------------------------ + if($eq mode 'board') + if($eq activity.activityType 'createCustomField') + | {{_ 'activity-customfield-created' customField}}. - if($eq activityType 'setCustomField') + if($eq activity.activityType 'setCustomField') | {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}. - if($eq activityType 'unsetCustomField') + if($eq activity.activityType 'unsetCustomField') | {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}. - if($eq activityType 'unjoinMember') - if($eq user._id member._id) - | {{{_ 'activity-unjoined' cardLink}}}. - else - | {{{_ 'activity-removed' memberLink cardLink}}}. + //- label activity ------------------------------------------------------ + if($eq activity.activityType 'addedLabel') + | {{{_ 'activity-added-label' lastLabel cardLink}}}. - span(title=createdAt).activity-meta {{ moment createdAt }} + if($eq activity.activityType 'removedLabel') + | {{{_ 'activity-removed-label' lastLabel cardLink}}}. -template(name="cardActivities") - each currentCard.activities - .activity - +userAvatar(userId=user._id) - p.activity-desc - +memberName(user=user) - if($eq activityType 'createCard') - | {{_ 'activity-added' cardLabel listName}}. - if($eq activityType 'importCard') - | {{{_ 'activity-imported' cardLabel list.title sourceLink}}}. - if($eq activityType 'joinMember') - if($eq user._id member._id) - | {{_ 'activity-joined' cardLabel}}. - else - | {{{_ 'activity-added' memberLink cardLabel}}}. - if($eq activityType 'unjoinMember') - if($eq user._id member._id) - | {{_ 'activity-unjoined' cardLabel}}. - else - | {{{_ 'activity-removed' cardLabel memberLink}}}. - if($eq activityType 'archivedCard') - | {{_ 'activity-archived' cardLabel}}. + //- list activity ------------------------------------------------------- + if($neq mode 'card') + if($eq activity.activityType 'createList') + | {{{_ 'activity-added' listLabel boardLabel}}}. - if($eq activityType 'addedLabel') - | {{{_ 'activity-added-label-card' lastLabel }}}. - - if($eq activityType 'removedLabel') - | {{{_ 'activity-removed-label-card' lastLabel }}}. + if($eq activity.activityType 'importList') + | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}. - if($eq activityType 'removeChecklist') - | {{{_ 'activity-checklist-removed' cardLabel}}}. + if($eq activity.activityType 'removeList') + | {{{_ 'activity-removed' activity.title boardLabel}}}. - if($eq activityType 'checkedItem') - | {{{_ 'activity-checked-item-card' checkItem checklist.title }}}. + if($eq activity.activityType 'archivedList') + | {{_ 'activity-archived' listLabel}}. - if($eq activityType 'uncheckedItem') - | {{{_ 'activity-unchecked-item-card' checkItem checklist.title }}}. + //- member activity ---------------------------------------------------- + if($eq activity.activityType 'joinMember') + if($eq user._id activity.member._id) + | {{{_ 'activity-joined' cardLink}}}. + else + | {{{_ 'activity-added' memberLink cardLink}}}. - if($eq activityType 'checklistCompleted') - | {{{_ 'activity-checklist-completed-card' checklist.title }}}. + if($eq activity.activityType 'unjoinMember') + if($eq user._id activity.member._id) + | {{{_ 'activity-unjoined' cardLink}}}. + else + | {{{_ 'activity-removed' memberLink cardLink}}}. - if($eq activityType 'checklistUncompleted') - | {{{_ 'activity-checklist-uncompleted-card' checklist.title }}}. + //- swimlane activity -------------------------------------------------- + if($neq mode 'card') + if($eq activity.activityType 'createSwimlane') + | {{{_ 'activity-added' activity.swimlane.title boardLabel}}}. - if($eq activityType 'restoredCard') - | {{_ 'activity-sent' cardLabel boardLabel}}. - if($eq activityType 'moveCard') - | {{_ 'activity-moved' cardLabel oldList.title list.title}}. + if($eq activity.activityType 'archivedSwimlane') + | {{_ 'activity-archived' activity.swimlane.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 - +viewer - = checklist.title - if($eq activityType 'addChecklistItem') - | {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}. - .activity-checklist(href="{{ card.absoluteUrl }}") - +viewer - = checklistItem.title - - if(currentData.timeKey) - | {{{_ activityType }}} + //- I don't understand this part ---------------------------------------- + if(currentData.timeKey) + | {{{_ activity.activityType }}} + = ' ' + i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }} + if (currentData.timeOldValue) = ' ' - i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }} - if (currentData.timeOldValue) - = ' ' - | {{{_ "previous_as" }}} - = ' ' - i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }} - = ' @' - else if(currentData.timeValue) - | {{{_ activityType currentData.timeValue}}} - - - if($eq activityType 'deleteComment') - | {{{_ 'activity-deleteComment' currentData.commentId}}}. - if($eq activityType 'editComment') - | {{{_ 'activity-editComment' currentData.commentId}}}. - if($eq activityType 'addComment') - +inlinedForm(classNames='js-edit-comment') - +editor(autofocus=true) - = comment.text - .edit-controls - button.primary(type="submit") {{_ 'edit'}} - else - .activity-comment - +viewer - = comment.text - span(title=createdAt).activity-meta {{ moment createdAt }} - if ($eq currentUser._id comment.userId) - = ' - ' - a.js-open-inlined-form {{_ "edit"}} - = ' - ' - a.js-delete-comment {{_ "delete"}} - - else - span(title=createdAt).activity-meta {{ moment createdAt }} + | {{{_ "previous_as" }}} + = ' ' + i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }} + = ' @' + else if(currentData.timeValue) + | {{{_ activity.activityType currentData.timeValue}}} + + span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }} diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index b082273a..5d356f6e 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -41,7 +41,9 @@ BlazeComponent.extendComponent({ }); }); }, +}).register('activities'); +BlazeComponent.extendComponent({ loadNextPage() { if (this.loadNextPageLocked === false) { this.page.set(this.page.get() + 1); @@ -50,41 +52,37 @@ BlazeComponent.extendComponent({ }, checkItem() { - const checkItemId = this.currentData().checklistItemId; + const checkItemId = this.currentData().activity.checklistItemId; const checkItem = ChecklistItems.findOne({ _id: checkItemId }); - return checkItem.title; + return checkItem && checkItem.title; }, boardLabel() { + const data = this.currentData(); + if (data.mode !== 'board') { + return createBoardLink(data.activity.board(), data.activity.listName); + } return TAPi18n.__('this-board'); }, cardLabel() { + const data = this.currentData(); + if (data.mode !== 'card') { + return createCardLink(this.currentData().activity.card()); + } return TAPi18n.__('this-card'); }, cardLink() { - const card = this.currentData().card(); - return ( - card && - Blaze.toHTML( - HTML.A( - { - href: card.absoluteUrl(), - class: 'action-card', - }, - card.title, - ), - ) - ); + return createCardLink(this.currentData().activity.card()); }, lastLabel() { - const lastLabelId = this.currentData().labelId; + const lastLabelId = this.currentData().activity.labelId; if (!lastLabelId) return null; - const lastLabel = Boards.findOne(Session.get('currentBoard')).getLabelById( - lastLabelId, - ); + const lastLabel = Boards.findOne( + this.currentData().activity.boardId, + ).getLabelById(lastLabelId); if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) { return lastLabel.color; } else { @@ -94,7 +92,7 @@ BlazeComponent.extendComponent({ lastCustomField() { const lastCustomField = CustomFields.findOne( - this.currentData().customFieldId, + this.currentData().activity.customFieldId, ); if (!lastCustomField) return null; return lastCustomField.name; @@ -102,10 +100,10 @@ BlazeComponent.extendComponent({ lastCustomFieldValue() { const lastCustomField = CustomFields.findOne( - this.currentData().customFieldId, + this.currentData().activity.customFieldId, ); if (!lastCustomField) return null; - const value = this.currentData().value; + const value = this.currentData().activity.value; if ( lastCustomField.settings.dropdownItems && lastCustomField.settings.dropdownItems.length > 0 @@ -122,11 +120,13 @@ BlazeComponent.extendComponent({ }, listLabel() { - return this.currentData().list().title; + const activity = this.currentData().activity; + const list = activity.list(); + return (list && list.title) || activity.title; }, sourceLink() { - const source = this.currentData().source; + const source = this.currentData().activity.source; if (source) { if (source.url) { return Blaze.toHTML( @@ -146,30 +146,31 @@ BlazeComponent.extendComponent({ memberLink() { return Blaze.toHTMLWithData(Template.memberName, { - user: this.currentData().member(), + user: this.currentData().activity.member(), }); }, attachmentLink() { - const attachment = this.currentData().attachment(); + const attachment = this.currentData().activity.attachment(); // trying to display url before file is stored generates js errors return ( - attachment && - attachment.url({ download: true }) && - Blaze.toHTML( - HTML.A( - { - href: attachment.url({ download: true }), - target: '_blank', - }, - attachment.name(), - ), - ) + (attachment && + attachment.url({ download: true }) && + Blaze.toHTML( + HTML.A( + { + href: attachment.url({ download: true }), + target: '_blank', + }, + attachment.name(), + ), + )) || + this.currentData().activity.attachmentName ); }, customField() { - const customField = this.currentData().customField(); + const customField = this.currentData().activity.customField(); if (!customField) return null; return customField.name; }, @@ -179,7 +180,7 @@ BlazeComponent.extendComponent({ { // XXX We should use Popup.afterConfirmation here 'click .js-delete-comment'() { - const commentId = this.currentData().commentId; + const commentId = this.currentData().activity.commentId; CardComments.remove(commentId); }, 'submit .js-edit-comment'(evt) { @@ -187,7 +188,7 @@ BlazeComponent.extendComponent({ const commentText = this.currentComponent() .getValue() .trim(); - const commentId = Template.parentData().commentId; + const commentId = Template.parentData().activity.commentId; if (commentText) { CardComments.update(commentId, { $set: { @@ -199,4 +200,36 @@ BlazeComponent.extendComponent({ }, ]; }, -}).register('activities'); +}).register('activity'); + +function createCardLink(card) { + return ( + card && + Blaze.toHTML( + HTML.A( + { + href: card.absoluteUrl(), + class: 'action-card', + }, + card.title, + ), + ) + ); +} + +function createBoardLink(board, list) { + let text = board.title; + if (list) text += `: ${list}`; + return ( + board && + Blaze.toHTML( + HTML.A( + { + href: board.absoluteUrl(), + class: 'action-board', + }, + text, + ), + ) + ); +} diff --git a/client/components/activities/activities.styl b/client/components/activities/activities.styl index 380e7b40..f3b1acdd 100644 --- a/client/components/activities/activities.styl +++ b/client/components/activities/activities.styl @@ -9,7 +9,7 @@ clear: both .activity - margin: 10px 0 + margin: 0.5px 0 display: flex .member diff --git a/client/components/activities/comments.styl b/client/components/activities/comments.styl index 22f9c482..ccf24b72 100644 --- a/client/components/activities/comments.styl +++ b/client/components/activities/comments.styl @@ -46,3 +46,23 @@ &:is-open cursor: auto + +.comment-item + background-color: #fff + border: 0 + box-shadow: 0 1px 2px rgba(0, 0, 0, .23) + color: #8c8c8c + height: 36px + margin: 4px 4px 6px 0 + width: 92% + + &:hover + background: darken(white, 12%) + + &.add-comment + display: flex + margin: 5px + + a + display: block + margin: auto diff --git a/client/components/boards/boardArchive.js b/client/components/boards/boardArchive.js index d3e65bd8..5a5cf772 100644 --- a/client/components/boards/boardArchive.js +++ b/client/components/boards/boardArchive.js @@ -7,7 +7,7 @@ BlazeComponent.extendComponent({ return Boards.find( { archived: true }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); }, diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 55b8c0a1..4e473f18 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -1,7 +1,7 @@ import { Cookies } from 'meteor/ostrio:cookies'; const cookies = new Cookies(); const subManager = new SubsManager(); -const { calculateIndex, enableClickOnTouch } = Utils; +const { calculateIndex } = Utils; const swimlaneWhileSortingHeight = 150; BlazeComponent.extendComponent({ @@ -191,9 +191,6 @@ BlazeComponent.extendComponent({ }, }); - // ugly touch event hotfix - enableClickOnTouch('.js-swimlane:not(.placeholder)'); - this.autorun(() => { let showDesktopDragHandles = false; currentUser = Meteor.user(); @@ -205,20 +202,17 @@ BlazeComponent.extendComponent({ } else { showDesktopDragHandles = false; } - if ( - Utils.isMiniScreen() || - (!Utils.isMiniScreen() && showDesktopDragHandles) - ) { + if (Utils.isMiniScreen() || showDesktopDragHandles) { $swimlanesDom.sortable({ handle: '.js-swimlane-header-handle', }); - } else { + } else if (!Utils.isMiniScreen() && !showDesktopDragHandles) { $swimlanesDom.sortable({ handle: '.swimlane-header', }); } - // Disable drag-dropping if the current user is not a board member or is comment only + // Disable drag-dropping if the current user is not a board member $swimlanesDom.sortable('option', 'disabled', !userIsMember()); }); diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index 53a74f76..4c0edac4 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -193,20 +193,6 @@ template(name="boardChangeViewPopup") | {{_ '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 diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index eea43bd3..be0146ec 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -30,22 +30,7 @@ Template.boardMenuPopup.events({ '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`; - }, + 'click .js-card-settings': Popup.open('boardCardSettings'), }); Template.boardChangeTitlePopup.events({ @@ -190,10 +175,6 @@ Template.boardChangeViewPopup.events({ Utils.setBoardView('board-view-cal'); Popup.close(); }, - 'click .js-open-rules-view'() { - Modal.openWide('rulesMain'); - Popup.close(); - }, }); const CreateBoard = BlazeComponent.extendComponent({ diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade index 79bae502..bbce1d6f 100644 --- a/client/components/boards/boardsList.jade +++ b/client/components/boards/boardsList.jade @@ -1,10 +1,10 @@ template(name="boardList") .wrapper - ul.board-list.clearfix + ul.board-list.clearfix.js-boards li.js-add-board a.board-list-item.label {{_ 'add-board'}} each boards - li(class="{{#if isStarred}}starred{{/if}}" class=colorClass) + li(class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board if isInvited .board-list-item span.details @@ -39,7 +39,7 @@ template(name="boardList") i.fa.js-archive-board( class="fa-archive" title="{{_ 'archive-board'}}") - else if currentUser.isBoardAdmin + else if isAdministrable i.fa.js-clone-board( class="fa-clone" title="{{_ 'duplicate-board'}}") @@ -55,7 +55,7 @@ template(name="boardList") title="{{_ 'archive-board'}}") template(name="boardListHeaderBar") - h1 {{_ 'my-boards'}} + h1 {{_ title }} .board-header-btns.right a.board-header-btn.js-open-archived-board i.fa.fa-archive diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js index 3918af82..9208fdb2 100644 --- a/client/components/boards/boardsList.js +++ b/client/components/boards/boardsList.js @@ -1,4 +1,5 @@ const subManager = new SubsManager(); +const { calculateIndex, enableClickOnTouch } = Utils; Template.boardListHeaderBar.events({ 'click .js-open-archived-board'() { @@ -7,6 +8,9 @@ Template.boardListHeaderBar.events({ }); Template.boardListHeaderBar.helpers({ + title() { + return FlowRouter.getRouteName() === 'home' ? 'my-boards' : 'public'; + }, templatesBoardId() { return Meteor.user() && Meteor.user().getTemplatesBoardId(); }, @@ -20,20 +24,80 @@ BlazeComponent.extendComponent({ Meteor.subscribe('setting'); }, - boards() { - return Boards.find( - { - archived: false, - 'members.userId': Meteor.userId(), - type: 'board', + onRendered() { + const self = this; + function userIsAllowedToMove() { + return Meteor.user(); + } + + const itemsSelector = '.js-board:not(.placeholder)'; + + const $boards = this.$('.js-boards'); + $boards.sortable({ + connectWith: '.js-boards', + tolerance: 'pointer', + appendTo: '.board-list', + helper: 'clone', + distance: 7, + items: itemsSelector, + placeholder: 'board-wrapper placeholder', + start(evt, ui) { + ui.helper.css('z-index', 1000); + ui.placeholder.height(ui.helper.height()); + EscapeActions.executeUpTo('popup-close'); + }, + stop(evt, ui) { + // To attribute the new index number, we need to get the DOM element + // of the previous and the following card -- if any. + const prevBoardDom = ui.item.prev('.js-board').get(0); + const nextBoardBom = ui.item.next('.js-board').get(0); + const sortIndex = calculateIndex(prevBoardDom, nextBoardBom, 1); + + const boardDomElement = ui.item.get(0); + const board = Blaze.getData(boardDomElement); + // Normally the jquery-ui sortable library moves the dragged DOM element + // to its new position, which disrupts Blaze reactive updates mechanism + // (especially when we move the last card of a list, or when multiple + // users move some cards at the same time). To prevent these UX glitches + // we ask sortable to gracefully cancel the move, and to put back the + // DOM in its initial state. The card move is then handled reactively by + // Blaze with the below query. + $boards.sortable('cancel'); + + board.move(sortIndex.base); }, - { sort: ['title'] }, - ); + }); + + // ugly touch event hotfix + enableClickOnTouch(itemsSelector); + + // Disable drag-dropping if the current user is not a board member or is comment only + this.autorun(() => { + $boards.sortable('option', 'disabled', !userIsAllowedToMove()); + }); + }, + + boards() { + let query = { + archived: false, + type: 'board', + }; + if (FlowRouter.getRouteName() === 'home') + query['members.userId'] = Meteor.userId(); + else query.permission = 'public'; + + return Boards.find(query, { + sort: { sort: 1 /* boards default sorting */ }, + }); }, isStarred() { const user = Meteor.user(); return user && user.hasStarred(this.currentData()._id); }, + isAdministrable() { + const user = Meteor.user(); + return user && user.isBoardAdmin(this.currentData()._id); + }, hasOvertimeCards() { subManager.subscribe('board', this.currentData()._id, false); diff --git a/client/components/boards/boardsList.styl b/client/components/boards/boardsList.styl index ae366e83..97d4f195 100644 --- a/client/components/boards/boardsList.styl +++ b/client/components/boards/boardsList.styl @@ -11,6 +11,19 @@ $spaceBetweenTiles = 16px box-sizing: border-box position: relative + &.placeholder:after + content: ''; + display: block; + background: darken(white, 20%) + border-radius: 3px; + height: 106px; + margin: 8px; + + &.ui-sortable-helper + cursor: grabbing + transform: rotate(4deg) + display: block !important + &.starred .fa-star, .fa-star-o @@ -20,7 +33,7 @@ $spaceBetweenTiles = 16px overflow: hidden; background-color: #999 color: #f6f6f6 - height: 90px + height: auto font-size: 16px line-height: 22px border-radius: 3px @@ -31,6 +44,7 @@ $spaceBetweenTiles = 16px margin: ($spaceBetweenTiles/2) position: relative text-decoration: none + word-wrap: break-word &.tile background-size: auto @@ -55,7 +69,7 @@ $spaceBetweenTiles = 16px .label font-weight: normal - line-height:90px + line-height: 56px :hover background-color:#939393 @@ -183,7 +197,7 @@ $spaceBetweenTiles = 16px overflow: scroll li - width: 50% + width: 50% .board-list-item overflow: hidden diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade index 2a96f4f4..61454fa7 100644 --- a/client/components/cards/attachments.jade +++ b/client/components/cards/attachments.jade @@ -38,18 +38,22 @@ template(name="attachmentsGalery") | {{_ 'download'}} if currentUser.isBoardMember unless currentUser.isCommentOnly - if isImage - a(class="{{#if $eq ../coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}") - i.fa.fa-thumb-tack - if($eq ../coverId _id) - | {{_ 'remove-cover'}} - else - | {{_ 'add-cover'}} - a.js-confirm-delete - i.fa.fa-close - | {{_ 'delete'}} + unless currentUser.isWorker + if isImage + a(class="{{#if $eq ../coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}") + i.fa.fa-thumb-tack + if($eq ../coverId _id) + | {{_ 'remove-cover'}} + else + | {{_ 'add-cover'}} + a.js-confirm-delete + i.fa.fa-close + | {{_ 'delete'}} if currentUser.isBoardMember unless currentUser.isCommentOnly - li.attachment-item.add-attachment - a.js-add-attachment {{_ 'add-attachment' }} + unless currentUser.isWorker + //li.attachment-item.add-attachment + a.js-add-attachment + i.fa.fa-plus + | {{_ 'add-attachment' }} diff --git a/client/components/cards/cardDate.js b/client/components/cards/cardDate.js index cb54b033..c4b5c6d8 100644 --- a/client/components/cards/cardDate.js +++ b/client/components/cards/cardDate.js @@ -97,7 +97,8 @@ Template.dateBadge.helpers({ return ( Meteor.user() && Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + !Meteor.user().isCommentOnly() && + !Meteor.user().isWorker() ); }, }); diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index 2b4f44b9..ae97e0e9 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -8,16 +8,23 @@ template(name="cardDetails") 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 + input.inline-input(type="text" id="cardURL_copy" value="{{ absoluteUrl }}") + a.fa.fa-link.card-copy-button.js-copy-link( + class="fa-link" + title="{{_ 'copy-card-link-to-clipboard'}}" + value="{{ absoluteUrl }}" + ) 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 + a.fa.fa-link.card-copy-mobile-button h2.card-details-title.js-card-title( class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}") +viewer = getTitle - if isWatching - i.fa.fa-eye.card-details-watch + if isWatching + i.card-details-watch.fa.fa-eye .card-details-path each parentList | > @@ -25,7 +32,7 @@ template(name="cardDetails") // else {{_ 'top-level-card'}} if isLinkedCard - h3.linked-card-location + a.linked-card-location.js-go-to-linked-card +viewer | {{getBoardTitle}} > {{getTitle}} @@ -36,70 +43,105 @@ template(name="cardDetails") p.warning {{_ 'card-archived'}} .card-details-items - .card-details-item.card-details-item-received - h3.card-details-item-title {{_ 'card-received'}} - if getReceived - +cardReceivedDate - else - if canModifyCard - a.js-received-date {{_ 'add'}} - - .card-details-item.card-details-item-start - h3.card-details-item-title {{_ 'card-start'}} - if getStart - +cardStartDate - else - if canModifyCard - a.js-start-date {{_ 'add'}} - - .card-details-item.card-details-item-due - h3.card-details-item-title {{_ 'card-due'}} - if getDue - +cardDueDate - else + if currentBoard.allowsReceivedDate + .card-details-item.card-details-item-received + h3 + i.fa.fa-sign-out + card-details-item-title {{_ 'card-received'}} + if getReceived + +cardReceivedDate + else + if canModifyCard + unless currentUser.isWorker + a.card-label.add-label.js-received-date + i.fa.fa-plus + + if currentBoard.allowsStartDate + .card-details-item.card-details-item-start + h3 + i.fa.fa-hourglass-start + card-details-item-title {{_ 'card-start'}} + if getStart + +cardStartDate + else + if canModifyCard + unless currentUser.isWorker + a.card-label.add-label.js-start-date + i.fa.fa-plus + + if currentBoard.allowsDueDate + .card-details-item.card-details-item-due + h3 + i.fa.fa-sign-in + card-details-item-title {{_ 'card-due'}} + if getDue + +cardDueDate + else + if canModifyCard + unless currentUser.isWorker + a.card-label.add-label.js-due-date + i.fa.fa-plus + + if currentBoard.allowsEndDate + .card-details-item.card-details-item-end + h3 + i.fa.fa-hourglass-end + card-details-item-title {{_ 'card-end'}} + if getEnd + +cardEndDate + else + if canModifyCard + unless currentUser.isWorker + a.card-label.add-label.js-end-date + i.fa.fa-plus + + //.card-details-items + if currentBoard.allowsMembers + .card-details-item.card-details-item-members + h3 + i.fa.fa-users + card-details-item-title {{_ 'members'}} + each getMembers + +userAvatar(userId=this cardId=../_id) + | {{! XXX Hack to hide syntaxic coloration /// }} if canModifyCard - a.js-due-date {{_ 'add'}} - - .card-details-item.card-details-item-end - h3.card-details-item-title {{_ 'card-end'}} - if getEnd - +cardEndDate - else + unless currentUser.isWorker + a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}") + i.fa.fa-plus + + //if assigneeSelected + if currentBoard.allowsAssignee + .card-details-item.card-details-item-assignees + h3 + i.fa.fa-user + card-details-item-title {{_ 'assignee'}} + each getAssignees + +userAvatarAssignee(userId=this cardId=../_id) + | {{! XXX Hack to hide syntaxic coloration /// }} if canModifyCard - a.js-end-date {{_ 'add'}} - - .card-details-items - .card-details-item.card-details-item-members - h3.card-details-item-title {{_ 'members'}} - each getMembers - +userAvatar(userId=this cardId=../_id) - | {{! XXX Hack to hide syntaxic coloration /// }} - if canModifyCard - 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 + if currentUser.isWorker + unless assigneeSelected + a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}") + i.fa.fa-plus + + if currentBoard.allowsLabels + .card-details-item.card-details-item-labels + h3 + i.fa.fa-tags + card-details-item-title {{_ 'labels'}} + a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}") + each labels + span.card-label(class="card-label-{{color}}" title=name) + +viewer + = name + if canModifyCard + unless currentUser.isWorker + a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}") + 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'}}") - each labels - span.card-label(class="card-label-{{color}}" title=name) - +viewer - = name - if canModifyCard - a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}") - i.fa.fa-plus - - .card-details-items + //.card-details-items each customFieldsWD .card-details-item.card-details-item-customfield h3.card-details-item-title @@ -107,7 +149,7 @@ template(name="cardDetails") = definition.name +cardCustomField - .card-details-items + //.card-details-items if getSpentTime .card-details-item.card-details-item-spent if getIsOvertime @@ -116,84 +158,124 @@ template(name="cardDetails") h3.card-details-item-title {{_ 'spent-time-hours'}} +cardSpentTime - //- XXX We should use "editable" to avoid repetiting ourselves - if canModifyCard - h3.card-details-item-title {{_ 'description'}} - +inlinedCardDescription(classNames="card-description js-card-description") - +editor(autofocus=true) - | {{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 getDescription + //.card-details-items + if currentBoard.allowsRequestedBy + .card-details-item.card-details-item-name + h3 + i.fa.fa-shopping-cart + card-details-item-title {{_ 'requested-by'}} + if canModifyCard + unless currentUser.isWorker + +inlinedForm(classNames="js-card-details-requester") + +editCardRequesterForm + else + a.js-open-inlined-form + if getRequestedBy + +viewer + = getRequestedBy + else + | {{_ 'add'}} + else if getRequestedBy +viewer - = getDescription - else - | {{_ 'edit'}} - if (hasUnsavedValue 'cardDescription' _id) - p.quiet - | {{_ 'unsaved-description'}} - a.js-open-inlined-form {{_ 'view-it'}} - = ' - ' - a.js-close-inlined-form {{_ 'discard'}} - else if getDescription - h3.card-details-item-title {{_ 'description'}} - +viewer - = getDescription + = getRequestedBy - .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 + if currentBoard.allowsAssignedBy + .card-details-item.card-details-item-name + h3 + i.fa.fa-user-plus + card-details-item-title {{_ 'assigned-by'}} + if canModifyCard + unless currentUser.isWorker + +inlinedForm(classNames="js-card-details-assigner") + +editCardAssignerForm else - | {{_ 'add'}} - else if getRequestedBy - +viewer - = getAssignedBy - - hr - +checklists(cardId = _id) + a.js-open-inlined-form + if getAssignedBy + +viewer + = getAssignedBy + else + | {{_ 'add'}} + else if getRequestedBy + +viewer + = getAssignedBy - if currentBoard.allowsSubtasks + if getVoteQuestion hr - +subtasks(cardId = _id) - - hr - h3 - i.fa.fa-paperclip - | {{_ 'attachments'}} + .vote-title + h3 + i.fa.fa-thumbs-up + card-details-item-title {{_ 'vote-question'}} + .vote-result + if votePublic + a.card-label.card-label-green.js-show-positive-votes {{ voteCountPositive }} + a.card-label.card-label-red.js-show-negative-votes {{ voteCountNegative }} + else + .card-label.card-label-green {{ voteCountPositive }} + .card-label.card-label-red {{ voteCountNegative }} + +viewer + = getVoteQuestion + button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}") {{_ 'vote-for-it'}} + button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'vote-against'}} - +attachmentsGalery + //- XXX We should use "editable" to avoid repetiting ourselves + if canModifyCard + unless currentUser.isWorker + if currentBoard.allowsDescriptionTitle + hr + h3 + i.fa.fa-align-left + card-details-item-title {{_ 'description'}} + if currentBoard.allowsDescriptionText + +inlinedCardDescription(classNames="card-description js-card-description") + +editor(autofocus=true) + | {{getUnsavedValue 'cardDescription' _id getDescription}} + .edit-controls.clearfix + button.primary(type="submit") {{_ 'save'}} + a.fa.fa-times-thin.js-close-inlined-form + else + if currentBoard.allowsDescriptionText + a.js-open-inlined-form + if getDescription + +viewer + = getDescription + else + | {{_ 'edit'}} + if (hasUnsavedValue 'cardDescription' _id) + p.quiet + | {{_ 'unsaved-description'}} + a.js-open-inlined-form {{_ 'view-it'}} + = ' - ' + a.js-close-inlined-form {{_ 'discard'}} + else if getDescription + if currentBoard.allowsDescriptionTitle + hr + h3.card-details-item-title {{_ 'description'}} + if currentBoard.allowsDescriptionText + +viewer + = getDescription + + .card-checklist-attachmentGalerys + .card-checklist-attachmentGalery.card-checklists + if currentBoard.allowsChecklists + hr + +checklists(cardId = _id) + if currentBoard.allowsSubtasks + hr + +subtasks(cardId = _id) + if currentBoard.allowsAttachments + hr + h3 + i.fa.fa-paperclip + | {{_ 'attachments'}} + .card-checklist-attachmentGalery.card-attachmentGalery + +attachmentsGalery hr unless currentUser.isNoComments .activity-title - h3 {{ _ 'activity'}} + h3 + i.fa.fa-history + | {{ _ 'activity'}} if currentUser.isBoardMember .material-toggle-switch span.toggle-switch-title {{_ 'hide-system-messages'}} @@ -202,9 +284,10 @@ template(name="cardDetails") else input.toggle-switch(type="checkbox" id="toggleButton") label.toggle-label(for="toggleButton") - if currentUser.isBoardMember - unless currentUser.isNoComments - +commentForm + if currentBoard.allowsComments + if currentUser.isBoardMember + unless currentUser.isNoComments + +commentForm unless currentUser.isNoComments if isLoaded.get if isLinkedCard @@ -235,32 +318,89 @@ template(name="editCardAssignerForm") template(name="cardDetailsActionsPopup") ul.pop-over-list - li: a.js-toggle-watch-card {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}} + li + a.js-toggle-watch-card + if isWatching + i.fa.fa-eye + | {{_ 'unwatch'}} + else + i.fa.fa-eye-slash + | {{_ 'watch'}} 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-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'}} - li: a.js-move-card-to-bottom {{_ 'moveCardToBottom-title'}} - hr + unless currentUser.isWorker + 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'}} + if getVoteQuestion + li + a.js-cancel-voting + i.fa.fa-thumbs-up + | {{_ 'card-cancel-voting'}} + else + li + a.js-start-voting + i.fa.fa-thumbs-up + | {{_ 'card-start-voting'}} + li + a.js-custom-fields + i.fa.fa-list-alt + | {{_ '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 + i.fa.fa-clock-o + | {{_ 'editCardSpentTimePopup-title'}} + li + a.js-set-card-color + i.fa.fa-paint-brush + | {{_ 'setCardColorPopup-title'}} + hr ul.pop-over-list - li: a.js-move-card {{_ 'moveCardPopup-title'}} - li: a.js-copy-card {{_ 'copyCardPopup-title'}} - li: a.js-copy-checklist-cards {{_ 'copyChecklistToManyCardsPopup-title'}} + li + a.js-move-card-to-top + i.fa.fa-arrow-up + | {{_ 'moveCardToTop-title'}} + li + a.js-move-card-to-bottom + i.fa.fa-arrow-down + | {{_ 'moveCardToBottom-title'}} + unless currentUser.isWorker + hr + ul.pop-over-list + li + a.js-move-card + i.fa.fa-arrow-right + | {{_ 'moveCardPopup-title'}} + li + a.js-copy-card + i.fa.fa-copy + | {{_ 'copyCardPopup-title'}} + hr + ul.pop-over-list + li + a.js-copy-checklist-cards + i.fa.fa-list + i.fa.fa-copy + | {{_ 'copyChecklistToManyCardsPopup-title'}} unless archived - li: a.js-archive {{_ 'archive-card'}} - li: a.js-more {{_ 'cardMorePopup-title'}} + hr + ul.pop-over-list + li + a.js-archive + i.fa.fa-arrow-right + i.fa.fa-archive + | {{_ 'archive-card'}} + hr + ul.pop-over-list + li + a.js-more + i.fa.fa-link + | {{_ 'cardMorePopup-title'}} template(name="moveCardPopup") +boardsAndLists @@ -312,16 +452,27 @@ template(name="cardMembersPopup") 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 + unless currentUser.isWorker + 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 + if currentUser.isWorker + ul.pop-over-list.js-card-assignee-list + li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}") + a.name.js-select-assignee(href="#") + +userAvatar(userId=currentUser._id) + span.full-name + = currentUser.profile.fullname + | (<span class="username">{{ currentUser.username }}</span>) + if currentUser.isCardAssignee + i.fa.fa-check template(name="userAvatarAssignee") a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})") @@ -349,11 +500,13 @@ template(name="cardAssigneePopup") p.quiet @{{ user.username }} ul.pop-over-list if currentUser.isNotCommentOnly + unless currentUser.isWorker li: a.js-remove-assignee {{_ 'remove-member-from-card'}} - if $eq currentUser._id user._id - with currentUser - li: a.js-edit-profile {{_ 'edit-profile'}} + unless currentUser.isWorker + 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") @@ -413,3 +566,35 @@ template(name="cardDeletePopup") unless archived p {{_ "card-delete-suggest-archive"}} button.js-confirm.negate.full(type="submit") {{_ 'delete'}} + +template(name="cardStartVotingPopup") + form.edit-vote-question + .fields + label(for="vote") {{_ 'vote-question'}} + input.js-vote-field#vote(type="text" name="vote" value="{{card.getVoteQuestion}}" autofocus) + label(for="vote-public") {{_ 'vote-public'}} + a.js-toggle-vote-public + .materialCheckBox#vote-public(name="vote-public") + + button.primary.confirm.js-submit {{_ 'save'}} + //- button.js-remove-color.negate.wide.right {{_ 'delete'}} + +template(name="positiveVoteMembersPopup") + ul.pop-over-list.js-card-member-list + each m in voteMemberPositive + li.item + a.name + +userAvatar(userId=m._id) + span.full-name + = m.profile.fullname + | (<span class="username">{{ m.username }}</span>) + +template(name="negativeVoteMembersPopup") + ul.pop-over-list.js-card-member-list + each m in voteMemberNegative + li.item + a.name + +userAvatar(userId=m._id) + span.full-name + = m.profile.fullname + | (<span class="username">{{ m.username }}</span>) diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 67120043..271fbe2f 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -1,5 +1,5 @@ const subManager = new SubsManager(); -const { calculateIndexData, enableClickOnTouch } = Utils; +const { calculateIndexData } = Utils; let cardColors; Meteor.startup(() => { @@ -38,6 +38,37 @@ BlazeComponent.extendComponent({ Meteor.subscribe('unsaved-edits'); }, + voteState() { + const card = this.currentData(); + const userId = Meteor.userId(); + let state; + if (card.vote) { + if (card.vote.positive) { + state = _.contains(card.vote.positive, userId); + if (state === true) return true; + } + if (card.vote.negative) { + state = _.contains(card.vote.negative, userId); + if (state === true) return false; + } + } + return null; + }, + votePublic() { + const card = this.currentData(); + if (card.vote) return card.vote.public; + return null; + }, + voteCountPositive() { + const card = this.currentData(); + if (card.vote && card.vote.positive) return card.vote.positive.length; + return null; + }, + voteCountNegative() { + const card = this.currentData(); + if (card.vote && card.vote.negative) return card.vote.negative.length; + return null; + }, isWatching() { const card = this.currentData(); return card.findWatcher(Meteor.userId()); @@ -51,7 +82,8 @@ BlazeComponent.extendComponent({ return ( Meteor.user() && Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + !Meteor.user().isCommentOnly() && + !Meteor.user().isWorker() ); }, @@ -199,9 +231,6 @@ BlazeComponent.extendComponent({ }, }); - // ugly touch event hotfix - enableClickOnTouch('.card-checklist-items .js-checklist'); - const $subtasksDom = this.$('.card-subtasks-items'); $subtasksDom.sortable({ @@ -237,20 +266,21 @@ BlazeComponent.extendComponent({ }, }); - // ugly touch event hotfix - enableClickOnTouch('.card-subtasks-items .js-subtasks'); - function userIsMember() { return Meteor.user() && Meteor.user().isBoardMember(); } // Disable sorting if the current user is not a board member this.autorun(() => { - if ($checklistsDom.data('sortable')) { - $checklistsDom.sortable('option', 'disabled', !userIsMember()); + const disabled = !userIsMember() || Utils.isMiniScreen(); + if ( + $checklistsDom.data('uiSortable') || + $checklistsDom.data('sortable') + ) { + $checklistsDom.sortable('option', 'disabled', disabled); } - if ($subtasksDom.data('sortable')) { - $subtasksDom.sortable('option', 'disabled', !userIsMember()); + if ($subtasksDom.data('uiSortable') || $subtasksDom.data('sortable')) { + $subtasksDom.sortable('option', 'disabled', disabled); } }); }, @@ -278,6 +308,29 @@ BlazeComponent.extendComponent({ 'click .js-close-card-details'() { Utils.goBoardId(this.data().boardId); }, + 'click .js-copy-link'() { + StringToCopyElement = document.getElementById('cardURL_copy'); + StringToCopyElement.select(); + if (document.execCommand('copy')) { + StringToCopyElement.blur(); + } else { + document.getElementById('cardURL_copy').selectionStart = 0; + document.getElementById('cardURL_copy').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-open-card-details-menu': Popup.open('cardDetailsActions'), 'submit .js-card-description'(event) { event.preventDefault(); @@ -317,6 +370,9 @@ BlazeComponent.extendComponent({ this.data().setRequestedBy(''); } }, + 'click .js-go-to-linked-card'() { + Utils.goCardId(this.data().linkedId); + }, 'click .js-member': Popup.open('cardMember'), 'click .js-add-members': Popup.open('cardMembers'), 'click .js-assignee': Popup.open('cardAssignee'), @@ -326,6 +382,8 @@ BlazeComponent.extendComponent({ 'click .js-start-date': Popup.open('editCardStartDate'), 'click .js-due-date': Popup.open('editCardDueDate'), 'click .js-end-date': Popup.open('editCardEndDate'), + 'click .js-show-positive-votes': Popup.open('positiveVoteMembers'), + 'click .js-show-negative-votes': Popup.open('negativeVoteMembers'), 'mouseenter .js-card-details'() { const parentComponent = this.parentComponent().parentComponent(); //on mobile view parent is Board, not BoardBody. @@ -349,6 +407,18 @@ BlazeComponent.extendComponent({ 'click #toggleButton'() { Meteor.call('toggleSystemMessages'); }, + 'click .js-vote'(e) { + const forIt = $(e.target).hasClass('js-vote-positive'); + let newState = null; + if ( + this.voteState() === null || + (this.voteState() === false && forIt) || + (this.voteState() === true && !forIt) + ) { + newState = forIt; + } + this.data().setVote(Meteor.userId(), newState); + }, }, ]; }, @@ -370,6 +440,54 @@ Template.cardDetails.helpers({ }); }, + receivedSelected() { + if (this.getReceived().length === 0) { + return false; + } else { + return true; + } + }, + + startSelected() { + if (this.getStart().length === 0) { + return false; + } else { + return true; + } + }, + + endSelected() { + if (this.getEnd().length === 0) { + return false; + } else { + return true; + } + }, + + dueSelected() { + if (this.getDue().length === 0) { + return false; + } else { + return true; + } + }, + + memberSelected() { + if (this.getMembers().length === 0) { + return false; + } else { + return true; + } + }, + + labelSelected() { + if (this.getLabels().length === 0) { + return false; + } else { + return true; + } + }, + assigneeSelected() { if (this.getAssignees().length === 0) { return false; @@ -378,6 +496,22 @@ Template.cardDetails.helpers({ } }, + requestBySelected() { + if (this.getRequestBy().length === 0) { + return false; + } else { + return true; + } + }, + + assigneeBySelected() { + if (this.getAssigneeBy().length === 0) { + return false; + } else { + return true; + } + }, + memberType() { const user = Users.findOne(this.userId); return user && user.isBoardAdmin() ? 'admin' : 'normal'; @@ -466,6 +600,7 @@ Template.cardDetailsActionsPopup.events({ 'click .js-assignees': Popup.open('cardAssignees'), 'click .js-labels': Popup.open('cardLabels'), 'click .js-attachments': Popup.open('cardAttachments'), + 'click .js-start-voting': Popup.open('cardStartVoting'), 'click .js-custom-fields': Popup.open('cardCustomFields'), 'click .js-received-date': Popup.open('editCardReceivedDate'), 'click .js-start-date': Popup.open('editCardStartDate'), @@ -476,6 +611,11 @@ Template.cardDetailsActionsPopup.events({ '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-cancel-voting'(event) { + event.preventDefault(); + this.unsetVote(); + Popup.close(); + }, 'click .js-move-card-to-top'(event) { event.preventDefault(); const minOrder = _.min( @@ -578,7 +718,7 @@ BlazeComponent.extendComponent({ _id: { $ne: Meteor.user().getTemplatesBoardId() }, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; @@ -754,7 +894,7 @@ BlazeComponent.extendComponent({ }, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; @@ -851,6 +991,31 @@ BlazeComponent.extendComponent({ }, }).register('cardMorePopup'); +BlazeComponent.extendComponent({ + onCreated() { + this.currentCard = this.currentData(); + this.voteQuestion = new ReactiveVar(this.currentCard.voteQuestion); + }, + + events() { + return [ + { + 'submit .edit-vote-question'(evt) { + evt.preventDefault(); + const voteQuestion = evt.target.vote.value; + const publicVote = $('#vote-public').hasClass('is-checked'); + this.currentCard.setVoteQuestion(voteQuestion, publicVote); + Popup.close(); + }, + 'click a.js-toggle-vote-public'(event) { + event.preventDefault(); + $('#vote-public').toggleClass('is-checked'); + }, + }, + ]; + }, +}).register('cardStartVotingPopup'); + // 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 3fc4d047..3e2beadd 100644 --- a/client/components/cards/cardDetails.styl +++ b/client/components/cards/cardDetails.styl @@ -4,6 +4,12 @@ avatar-radius = 50% +#cardURL_copy + // Have clipboard text not visible by moving it to far left + position: absolute + left: -2000px + top: 0px + .assignee border-radius: 3px display: block @@ -88,17 +94,18 @@ avatar-radius = 50% animation: flexGrowIn 0.1s box-shadow: 0 0 7px 0 darken(white, 30%) transition: flex-basis 0.1s + box-sizing: border-box .mCustomScrollBox padding-left: 0 .ps-scrollbar-y-rail pointer-event: all - position: absolute; + position: absolute .card-details-canvas width: 470px - padding-left: 20px; + padding-left: 20px .card-details-header margin: 0 -20px 5px @@ -108,6 +115,8 @@ avatar-radius = 50% .close-card-details, .card-details-menu, + .card-copy-button, + .card-copy-mobile-button, .close-card-details-mobile-web, .card-details-menu-mobile-web float: right @@ -122,6 +131,16 @@ avatar-radius = 50% padding: 5px margin-right: 40px + .card-copy-button + font-size: 17px + padding: 10px + margin-right: 10px + + .card-copy-mobile-button + font-size: 17px + padding: 10px + margin-right: 10px + .card-details-menu font-size: 17px padding: 10px @@ -223,7 +242,7 @@ input[type="submit"].attachment-add-link-submit .card-details-canvas width: 100% - padding-left: 0px; + padding-left: 0px .card-details-header .close-card-details @@ -312,3 +331,13 @@ card-details-color(background, color...) .card-details-indigo card-details-color(#4b0082, #ffffff) //White text for better visibility + +.voted + opacity: .7 +.vote-title + display: flex + justify-content: space-between +.vote-result + display: flex +.js-show-positive-votes + cursor: pointer diff --git a/client/components/cards/checklists.jade b/client/components/cards/checklists.jade index 279d3671..1b1e088a 100644 --- a/client/components/cards/checklists.jade +++ b/client/components/cards/checklists.jade @@ -1,5 +1,7 @@ template(name="checklists") - h3 {{_ 'checklists'}} + h3 + i.fa.fa-check + | {{_ 'checklists'}} if toggleDeleteDialog.get .board-overlay#card-details-overlay +checklistDeleteDialog(checklist = checklistToDelete) @@ -86,7 +88,8 @@ template(name="checklistItems") template(name='checklistItemDetail') .js-checklist-item.checklist-item if canModifyCard - .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}") + .check-box-container + .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 diff --git a/client/components/cards/checklists.js b/client/components/cards/checklists.js index 57939eb8..29573d2b 100644 --- a/client/components/cards/checklists.js +++ b/client/components/cards/checklists.js @@ -1,4 +1,4 @@ -const { calculateIndexData, enableClickOnTouch } = Utils; +const { calculateIndexData, capitalize } = Utils; function initSorting(items) { items.sortable({ @@ -36,9 +36,6 @@ function initSorting(items) { checklistItem.move(checklistId, sortIndex.base); }, }); - - // ugly touch event hotfix - enableClickOnTouch('.js-checklist-item:not(.placeholder)'); } BlazeComponent.extendComponent({ @@ -54,11 +51,15 @@ BlazeComponent.extendComponent({ return Meteor.user() && Meteor.user().isBoardMember(); } - // Disable sorting if the current user is not a board member + // Disable sorting if the current user is not a board member or is a miniscreen self.autorun(() => { const $itemsDom = $(self.itemsDom); - if ($itemsDom.data('sortable')) { - $(self.itemsDom).sortable('option', 'disabled', !userIsMember()); + if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) { + $(self.itemsDom).sortable( + 'option', + 'disabled', + !userIsMember() || Utils.isMiniScreen(), + ); } }); }, @@ -67,7 +68,8 @@ BlazeComponent.extendComponent({ return ( Meteor.user() && Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + !Meteor.user().isCommentOnly() && + !Meteor.user().isWorker() ); }, }).register('checklistDetail'); @@ -120,7 +122,8 @@ BlazeComponent.extendComponent({ return ( Meteor.user() && Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + !Meteor.user().isCommentOnly() && + !Meteor.user().isWorker() ); }, @@ -172,6 +175,16 @@ BlazeComponent.extendComponent({ } }, + focusChecklistItem(event) { + // If a new checklist is created, pre-fill the title and select it. + const checklist = this.currentData().checklist; + if (!checklist) { + const textarea = event.target; + textarea.value = capitalize(TAPi18n.__('r-checklist')); + textarea.select(); + } + }, + events() { const events = { 'click .toggle-delete-checklist-dialog'(event) { @@ -191,6 +204,7 @@ BlazeComponent.extendComponent({ 'submit .js-edit-checklist-item': this.editChecklistItem, 'click .js-delete-checklist-item': this.deleteItem, 'click .confirm-checklist-delete': this.deleteChecklist, + 'focus .js-add-checklist-item': this.focusChecklistItem, keydown: this.pressKey, }, ]; @@ -228,7 +242,8 @@ Template.checklistItemDetail.helpers({ return ( Meteor.user() && Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + !Meteor.user().isCommentOnly() && + !Meteor.user().isWorker() ); }, }); @@ -244,7 +259,7 @@ BlazeComponent.extendComponent({ events() { return [ { - 'click .js-checklist-item .check-box': this.toggleItem, + 'click .js-checklist-item .check-box-container': this.toggleItem, }, ]; }, diff --git a/client/components/cards/checklists.styl b/client/components/cards/checklists.styl index 8ac37a15..0a6d688b 100644 --- a/client/components/cards/checklists.styl +++ b/client/components/cards/checklists.styl @@ -113,6 +113,9 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item &:hover background-color: darken(white, 8%) + .check-box-container + padding-right: 1px; + .check-box margin: 0.1em 0 0 0; &.is-checked @@ -121,7 +124,7 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item .item-title flex: 1 - padding-left: 10px; + margin-left: 10px; &.is-checked color: #8c8c8c font-style: italic diff --git a/client/components/cards/labels.styl b/client/components/cards/labels.styl index 9d7c7553..ee946656 100644 --- a/client/components/cards/labels.styl +++ b/client/components/cards/labels.styl @@ -158,6 +158,8 @@ .edit-labels-pop-over margin-bottom: 8px + .card-label .viewer p + margin: 0 .edit-labels-pop-over .shortcut display: inline-block diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index 7dd220ee..b6ccd4d7 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -100,6 +100,10 @@ template(name="minicard") if getDescription .badge.badge-state-image-only(title=getDescription) span.badge-icon.fa.fa-align-left + if getVoteQuestion + .badge.badge-state-image-only(title=getVoteQuestion) + span.badge-icon.fa.fa-thumbs-up + span.badge-icon.fa.fa-thumbs-down if attachments.count .badge span.badge-icon.fa.fa-paperclip diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index 1ea608f5..da36b87f 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -36,24 +36,20 @@ Template.minicard.helpers({ currentUser = Meteor.user(); if (currentUser) { return (currentUser.profile || {}).showDesktopDragHandles; + } else if (cookies.has('showDesktopDragHandles')) { + return true; } else { - if (cookies.has('showDesktopDragHandles')) { - return true; - } else { - return false; - } + return false; } }, hiddenMinicardLabelText() { currentUser = Meteor.user(); if (currentUser) { return (currentUser.profile || {}).hiddenMinicardLabelText; + } else if (cookies.has('hiddenMinicardLabelText')) { + return true; } else { - if (cookies.has('hiddenMinicardLabelText')) { - return true; - } else { - return false; - } + return false; } }, }); diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl index 8607e118..7d72a588 100644 --- a/client/components/cards/minicard.styl +++ b/client/components/cards/minicard.styl @@ -79,7 +79,7 @@ border-radius: top 2px .minicard-labels - float: right + float: none display: flex flex-wrap: wrap diff --git a/client/components/cards/subtasks.jade b/client/components/cards/subtasks.jade index 7e64e23f..df35bed3 100644 --- a/client/components/cards/subtasks.jade +++ b/client/components/cards/subtasks.jade @@ -1,5 +1,7 @@ template(name="subtasks") - h3 {{_ 'subtasks'}} + h3 + i.fa.fa-sitemap + | {{_ 'subtasks'}} if toggleDeleteDialog.get .board-overlay#card-details-overlay +subtaskDeleteDialog(subtask = subtaskToDelete) diff --git a/client/components/cards/subtasks.js b/client/components/cards/subtasks.js index fab860bb..4cd15c11 100644 --- a/client/components/cards/subtasks.js +++ b/client/components/cards/subtasks.js @@ -3,7 +3,8 @@ BlazeComponent.extendComponent({ return ( Meteor.user() && Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + !Meteor.user().isCommentOnly() && + !Meteor.user().isWorker() ); }, }).register('subtaskDetail'); @@ -19,7 +20,22 @@ BlazeComponent.extendComponent({ const crtBoard = Boards.findOne(card.boardId); const targetBoard = crtBoard.getDefaultSubtasksBoard(); const listId = targetBoard.getDefaultSubtasksListId(); - const swimlaneId = targetBoard.getDefaultSwimline()._id; + + //Get the full swimlane data for the parent task. + const parentSwimlane = Swimlanes.findOne({ + boardId: crtBoard._id, + _id: card.swimlaneId, + }); + //find the swimlane of the same name in the target board. + const targetSwimlane = Swimlanes.findOne({ + boardId: targetBoard._id, + title: parentSwimlane.title, + }); + //If no swimlane with a matching title exists in the target board, fall back to the default swimlane. + const swimlaneId = + targetSwimlane === undefined + ? targetBoard.getDefaultSwimline()._id + : targetSwimlane._id; if (title) { const _id = Cards.insert({ @@ -55,7 +71,8 @@ BlazeComponent.extendComponent({ return ( Meteor.user() && Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + !Meteor.user().isCommentOnly() && + !Meteor.user().isWorker() ); }, @@ -154,7 +171,8 @@ Template.subtaskItemDetail.helpers({ return ( Meteor.user() && Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + !Meteor.user().isCommentOnly() && + !Meteor.user().isWorker() ); }, }); diff --git a/client/components/import/import.jade b/client/components/import/import.jade index 5b52f417..1551a7dd 100644 --- a/client/components/import/import.jade +++ b/client/components/import/import.jade @@ -15,9 +15,6 @@ template(name="importTextarea") 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 {{_ 'import-sandstorm-backup-warning'}} - p.warning {{_ 'import-sandstorm-warning'}} input.primary.wide(type="submit" value="{{_ 'import'}}") template(name="importMapMembers") diff --git a/client/components/lists/list.js b/client/components/lists/list.js index 89d51e85..839304f8 100644 --- a/client/components/lists/list.js +++ b/client/components/lists/list.js @@ -1,6 +1,6 @@ import { Cookies } from 'meteor/ostrio:cookies'; const cookies = new Cookies(); -const { calculateIndex, enableClickOnTouch } = Utils; +const { calculateIndex } = Utils; BlazeComponent.extendComponent({ // Proxy @@ -114,9 +114,6 @@ BlazeComponent.extendComponent({ }, }); - // ugly touch event hotfix - enableClickOnTouch(itemsSelector); - this.autorun(() => { let showDesktopDragHandles = false; currentUser = Meteor.user(); @@ -129,18 +126,26 @@ BlazeComponent.extendComponent({ showDesktopDragHandles = false; } - if (!Utils.isMiniScreen() && showDesktopDragHandles) { + if (Utils.isMiniScreen() || showDesktopDragHandles) { $cards.sortable({ handle: '.handle', }); - } else { + } else if (!Utils.isMiniScreen() && !showDesktopDragHandles) { $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()); + if ($cards.data('uiSortable') || $cards.data('sortable')) { + $cards.sortable( + 'option', + 'disabled', + // Disable drag-dropping when user is not member + !userIsMember(), + // Not disable drag-dropping while in multi-selection mode + // MultiSelection.isActive() || !userIsMember(), + ); + } }); // We want to re-run this function any time a card is added. @@ -176,12 +181,10 @@ Template.list.helpers({ currentUser = Meteor.user(); if (currentUser) { return (currentUser.profile || {}).showDesktopDragHandles; + } else if (cookies.has('showDesktopDragHandles')) { + return true; } else { - if (cookies.has('showDesktopDragHandles')) { - return true; - } else { - return false; - } + return false; } }, }); diff --git a/client/components/lists/list.styl b/client/components/lists/list.styl index 27cf678c..bc7f763f 100644 --- a/client/components/lists/list.styl +++ b/client/components/lists/list.styl @@ -43,9 +43,6 @@ background: white margin: -3px 0 8px -.list-header-card-count - height: 35px - .list-header-add flex: 0 0 auto padding: 20px 12px 4px @@ -60,6 +57,9 @@ background-color: #e4e4e4; border-bottom: 6px solid #e4e4e4; + &.list-header-card-count + min-height: 35px + height: auto &.ui-sortable-handle cursor: grab diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js index b0974705..88f88db0 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -189,7 +189,8 @@ BlazeComponent.extendComponent({ !this.reachedWipLimit() && Meteor.user() && Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + !Meteor.user().isCommentOnly() && + !Meteor.user().isWorker() ); }, @@ -410,7 +411,7 @@ BlazeComponent.extendComponent({ type: 'board', }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; @@ -596,7 +597,7 @@ BlazeComponent.extendComponent({ type: 'board', }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; @@ -742,9 +743,25 @@ BlazeComponent.extendComponent({ }, updateList() { + // Use fallback when requestIdleCallback is not available on iOS and Safari + // https://www.afasterweb.com/2017/11/20/utilizing-idle-moments/ + checkIdleTime = + window.requestIdleCallback || + function(handler) { + const startTime = Date.now(); + return setTimeout(function() { + handler({ + didTimeout: false, + timeRemaining() { + return Math.max(0, 50.0 - (Date.now() - startTime)); + }, + }); + }, 1); + }; + if (this.spinnerInView()) { this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter); - window.requestIdleCallback(() => this.updateList()); + checkIdleTime(() => this.updateList()); } }, diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade index 631f68a0..fa1faf34 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -10,7 +10,7 @@ template(name="listHeader") 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}}") + class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}") +viewer = title if wipLimit.enabled @@ -30,7 +30,6 @@ 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 @@ -56,25 +55,47 @@ template(name="editListTitleForm") template(name="listActionPopup") ul.pop-over-list - li: a.js-toggle-watch-list {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}} + li + a.js-toggle-watch-list + if isWatching + i.fa.fa-eye + | {{_ 'unwatch'}} + else + i.fa.fa-eye-slash + | {{_ 'watch'}} unless currentUser.isCommentOnly - hr - ul.pop-over-list - li: a.js-set-color-list {{_ 'set-color-list'}} - hr + unless currentUser.isWorker + ul.pop-over-list + li + a.js-set-color-list + i.fa.fa-paint-brush + | {{_ 'set-color-list'}} ul.pop-over-list if cards.count - li: a.js-select-cards {{_ 'list-select-cards'}} - hr + li + a.js-select-cards + i.fa.fa-check-square + | {{_ 'list-select-cards'}} if currentUser.isBoardAdmin ul.pop-over-list - li: a.js-set-wip-limit {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}} + li + a.js-set-wip-limit + i.fa.fa-ban + | {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}} + unless currentUser.isWorker hr - ul.pop-over-list - li: a.js-close-list {{_ 'archive-list'}} + ul.pop-over-list + li + a.js-close-list + i.fa.fa-arrow-right + i.fa.fa-archive + | {{_ 'archive-list'}} hr ul.pop-over-list - li: a.js-more {{_ 'listMorePopup-title'}} + li + a.js-more + i.fa.fa-link + | {{_ 'listMorePopup-title'}} template(name="boardLists") ul.pop-over-list @@ -94,7 +115,8 @@ template(name="listMorePopup") input.inline-input(type="text" readonly value="{{ rootUrl }}") | {{_ 'added'}} span.date(title=list.createdAt) {{ moment createdAt 'LLL' }} - a.js-delete {{_ 'delete'}} + unless currentUser.isWorker + a.js-delete {{_ 'delete'}} template(name="listDeletePopup") p {{_ "list-delete-pop"}} diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index 570cc30f..46dbd748 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -9,9 +9,10 @@ BlazeComponent.extendComponent({ canSeeAddCard() { const list = Template.currentData(); return ( - !list.getWipLimit('enabled') || - list.getWipLimit('soft') || - !this.reachedWipLimit() + (!list.getWipLimit('enabled') || + list.getWipLimit('soft') || + !this.reachedWipLimit()) && + !Meteor.user().isWorker() ); }, @@ -109,12 +110,10 @@ Template.listHeader.helpers({ currentUser = Meteor.user(); if (currentUser) { return (currentUser.profile || {}).showDesktopDragHandles; + } else if (cookies.has('showDesktopDragHandles')) { + return true; } else { - if (cookies.has('showDesktopDragHandles')) { - return true; - } else { - return false; - } + return false; } }, }); diff --git a/client/components/main/editor.js b/client/components/main/editor.js index 39c03aa9..081c6521 100755 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -1,87 +1,3 @@ -import _sanitizeXss from 'xss'; -const ASIS = 'asis'; -const sanitizeXss = (input, options) => { - const defaultAllowedIframeSrc = /^(https:){0,1}\/\/.*?(youtube|vimeo|dailymotion|youku)/i; - const allowedIframeSrcRegex = (function() { - let reg = defaultAllowedIframeSrc; - const SAFE_IFRAME_SRC_PATTERN = - Meteor.settings.public.SAFE_IFRAME_SRC_PATTERN; - try { - if (SAFE_IFRAME_SRC_PATTERN !== undefined) { - reg = new RegExp(SAFE_IFRAME_SRC_PATTERN, 'i'); - } - } catch (e) { - /*eslint no-console: ["error", { allow: ["warn", "error"] }] */ - - console.error('Wrong pattern specified', SAFE_IFRAM_SRC_PATTERN, e); - } - return reg; - })(); - const targetWindow = '_blank'; - const getHtmlDOM = html => { - const i = document.createElement('i'); - i.innerHTML = html; - return i.firstChild; - }; - options = { - onTag(tag, html, options) { - const htmlDOM = getHtmlDOM(html); - const getAttr = attr => { - return htmlDOM && attr && htmlDOM.getAttribute(attr); - }; - if (tag === 'iframe') { - const clipCls = 'note-vide-clip'; - if (!options.isClosing) { - const iframeCls = getAttr('class'); - let safe = iframeCls.indexOf(clipCls) > -1; - const src = getAttr('src'); - if (allowedIframeSrcRegex.exec(src)) { - safe = true; - } - if (safe) - return `<iframe src='${src}' class="${clipCls}" width=100% height=auto allowfullscreen></iframe>`; - } else { - // remove </iframe> tag - return ''; - } - } else if (tag === 'a') { - if (!options.isClosing) { - if (getAttr(ASIS) === 'true') { - // if has a ASIS attribute, don't do anything, it's a member id - return html; - } else { - const href = getAttr('href'); - if (href.match(/^((http(s){0,1}:){0,1}\/\/|\/)/)) { - // a valid url - return `<a href=${href} target=${targetWindow}>`; - } - } - } - } else if (tag === 'img') { - if (!options.isClosing) { - const src = getAttr('src'); - if (src) { - return `<a href='${src}' class='swipebox'><img src='${src}' class="attachment-image-preview mCS_img_loaded"></a>`; - } - } - } - return undefined; - }, - onTagAttr(tag, name, value) { - if (tag === 'img' && name === 'src') { - if (value && value.substr(0, 5) === 'data:') { - // allow image with dataURI src - return `${name}='${value}'`; - } - } else if (tag === 'a' && name === 'target') { - return `${name}='${targetWindow}'`; // always change a href target to a new window - } - return undefined; - }, - ...options, - }; - return _sanitizeXss(input, options); -}; Template.editor.onRendered(() => { const textareaSelector = 'textarea'; const mentions = [ @@ -94,13 +10,7 @@ Template.editor.onRendered(() => { currentBoard .activeMembers() .map(member => { - 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; + const username = Users.findOne(member.userId).username; return username.includes(term) ? username : null; }) .filter(Boolean), @@ -126,10 +36,9 @@ Template.editor.onRendered(() => { ? [ ['view', ['fullscreen']], ['table', ['table']], - ['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 + ['font', ['bold', 'underline']], //['fontsize', ['fontsize']], + ['color', ['color']], ] : [ ['style', ['style']], @@ -139,11 +48,47 @@ Template.editor.onRendered(() => { ['color', ['color']], ['para', ['ul', 'ol', 'paragraph']], ['table', ['table']], - ['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled + //['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled //['insert', ['link', 'picture']], // modal popup has issue somehow :( ['view', ['fullscreen', 'help']], ]; - const cleanPastedHTML = sanitizeXss; + const cleanPastedHTML = function(input) { + const badTags = [ + 'style', + 'script', + 'applet', + 'embed', + 'noframes', + 'noscript', + 'meta', + 'link', + 'button', + 'form', + ].join('|'); + const badPatterns = new RegExp( + `(?:${[ + `<(${badTags})s*[^>][\\s\\S]*?<\\/\\1>`, + `<(${badTags})[^>]*?\\/>`, + ].join('|')})`, + 'gi', + ); + let output = input; + // remove bad Tags + output = output.replace(badPatterns, ''); + // remove attributes ' style="..."' + const badAttributes = new RegExp( + `(?:${[ + 'on\\S+=([\'"]?).*?\\1', + 'href=([\'"]?)javascript:.*?\\2', + 'style=([\'"]?).*?\\3', + 'target=\\S+', + ].join('|')})`, + 'gi', + ); + output = output.replace(badAttributes, ''); + output = output.replace(/(<a )/gi, '$1target=_ '); // always to new target + return output; + }; const editor = '.editor'; const selectors = [ `.js-new-comment-form ${editor}`, @@ -163,37 +108,14 @@ 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 + // when comment is submitted, the original textarea will be set to '', so shall we if (!this.value) { const sn = getSummernote(this); sn && sn.summernote('code', ''); @@ -201,7 +123,9 @@ Template.editor.onRendered(() => { }); const jEditor = object && object.editable; const toolbar = object && object.toolbar; - setAutocomplete(jEditor); + if (jEditor !== undefined) { + jEditor.escapeableTextComplete(mentions); + } if (toolbar !== undefined) { const fBtn = toolbar.find('.btn-fullscreen'); fBtn.on('click', function() { @@ -211,6 +135,7 @@ Template.editor.onRendered(() => { }); } }, + onImageUpload(files) { const $summernote = getSummernote(this); if (files && files.length > 0) { @@ -289,6 +214,12 @@ Template.editor.onRendered(() => { const thisNote = this; const updatePastedText = function(object) { const someNote = getSummernote(object); + // Fix Pasting text into a card is adding a line before and after + // (and multiplies by pasting more) by changing paste "p" to "br". + // Fixes https://github.com/wekan/wekan/2890 . + // == Fix Start == + someNote.execCommand('defaultParagraphSeparator', false, 'br'); + // == Fix End == 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('code', ''); //clear original @@ -331,6 +262,8 @@ Template.editor.onRendered(() => { } }); +import sanitizeXss from 'xss'; + // XXX I believe we should compute a HTML rendered field on the server that // would handle markdown and user mentions. We can simply have two // fields, one source, and one compiled version (in HTML) and send only the @@ -352,7 +285,7 @@ Blaze.Template.registerHelper( } return member; }); - const mentionRegex = /\B@(?:(?:"([\w.\s]*)")|([\w.]+))/gi; // including space in username + const mentionRegex = /\B@([\w.]*)/gi; let currentMention; while ((currentMention = mentionRegex.exec(content)) !== null) { @@ -368,7 +301,12 @@ Blaze.Template.registerHelper( if (knowedUser.userId === Meteor.userId()) { linkClass += ' me'; } - const link = HTML.A( + // This @user mention link generation did open same Wekan + // window in new tab, so now A is changed to U so it's + // underlined and there is no link popup. This way also + // text can be selected more easily. + //const link = HTML.A( + const link = HTML.U( { class: linkClass, // XXX Hack. Since we stringify this render function result below with @@ -376,16 +314,17 @@ Blaze.Template.registerHelper( // `userId` to the popup as usual, and we need to store it in the DOM // using a data attribute. 'data-userId': knowedUser.userId, - [ASIS]: 'true', }, linkValue, ); content = content.replace(fullMention, Blaze.toHTML(link)); } + return HTML.Raw(sanitizeXss(content)); }), ); + Template.viewer.events({ // Viewer sometimes have click-able wrapper around them (for instance to edit // the corresponding text). Clicking a link shouldn't fire these actions, stop @@ -397,10 +336,7 @@ Template.viewer.events({ Popup.open('member').call({ userId }, event, templateInstance); } else { const href = event.currentTarget.href; - const child = event.currentTarget.firstElementChild; - if (child && child.tagName === 'IMG') { - prevent = false; - } else if (href) { + if (href) { window.open(href, '_blank'); } } diff --git a/client/components/main/header.jade b/client/components/main/header.jade index 75e84c0c..de7ead93 100644 --- a/client/components/main/header.jade +++ b/client/components/main/header.jade @@ -24,6 +24,11 @@ template(name="header") a(href="{{pathFor 'home'}}") span.fa.fa-home | {{_ 'all-boards'}} + li.separator - + li + a(href="{{pathFor 'public'}}") + span.fa.fa-globe + | {{_ 'public'}} each currentUser.starredBoards li.separator - li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}") @@ -35,6 +40,8 @@ template(name="header") a#header-new-board-icon.js-create-board i.fa.fa-plus(title="Create a new board") + +notifications + +headerUserBar #header(class=currentBoard.colorClass) diff --git a/client/components/main/header.styl b/client/components/main/header.styl index e3c7618d..d8093861 100644 --- a/client/components/main/header.styl +++ b/client/components/main/header.styl @@ -99,7 +99,7 @@ height: 28px font-size: 12px display: flex - z-index: 17 + z-index: 21 #header-user-bar, #header-new-board-icon, @@ -127,7 +127,7 @@ &.current color: darken(white, 5%) - &:first-child .fa-home + &:first-child .fa-home,&:nth-child(3) .fa-globe margin-right: 5px a.js-create-board @@ -175,7 +175,7 @@ .board-header-btn height: 32px line-height: @height - font-size: 16px + font-size: 15px i.fa line-height: 32px @@ -218,6 +218,9 @@ padding: 10px margin: -10px 0 -10px -10px +.announcement .viewer + display: inline-block + .announcement, .offline-warning width: 100% diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade index 9543c5c5..08dfc58c 100644 --- a/client/components/main/layouts.jade +++ b/client/components/main/layouts.jade @@ -6,10 +6,16 @@ head where the application is deployed with a path prefix, but it seems to be difficult to do that cleanly with Blaze -- at least without adding extra 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") + link(rel="shortcut icon" type="image/x-icon" href="/favicon.ico") + link(rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png") + link(rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png") + link(rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png") + link(rel="manifest" href="/site.webmanifest") + link(rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5") + meta(name="apple-mobile-web-app-title" content="Wekan") + meta(name="application-name" content="Wekan") + meta(name="msapplication-TileColor" content="#00aba9") + meta(name="theme-color" content="#ffffff") template(name="userFormsLayout") section.auth-layout diff --git a/client/components/main/layouts.js b/client/components/main/layouts.js index ec4a35cc..83678e73 100644 --- a/client/components/main/layouts.js +++ b/client/components/main/layouts.js @@ -31,6 +31,11 @@ Template.userFormsLayout.onCreated(function() { return this.stop(); }, }); + Meteor.call('isPasswordLoginDisabled', (_, result) => { + if (result) { + $('.at-pwd-form').hide(); + } + }); }); Template.userFormsLayout.onRendered(() => { @@ -73,6 +78,8 @@ Template.userFormsLayout.helpers({ name = 'Igbo'; } else if (lang.name === 'oc') { name = 'Occitan'; + } else if (lang.name === '繁体中文(台湾)') { + name = '繁體中文(台灣)'; } return { tag, name }; }).sort(function(a, b) { diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl index 023cba3d..b4815ca6 100644 --- a/client/components/main/popup.styl +++ b/client/components/main/popup.styl @@ -135,6 +135,10 @@ $popupWidth = 300px margin-bottom: 8px .pop-over-list + li + display: block + clear: both + li > a clear: both cursor: pointer @@ -316,6 +320,7 @@ $popupWidth = 300px input[type="file"] margin: 4px 0 12px width: 100% + box-sizing: border-box .pop-over-list li > a diff --git a/client/components/notifications/notification.jade b/client/components/notifications/notification.jade new file mode 100644 index 00000000..c98bbdba --- /dev/null +++ b/client/components/notifications/notification.jade @@ -0,0 +1,10 @@ +template(name='notification') + li.notification(class="{{#if read}}read{{/if}}") + .read-status + .materialCheckBox(class="{{#if read}}is-checked{{/if}}") + +notificationIcon(activityData) + .details + +activity(activity=activityData mode='none') + if read + .remove + a.fa.fa-trash diff --git a/client/components/notifications/notification.js b/client/components/notifications/notification.js new file mode 100644 index 00000000..89277520 --- /dev/null +++ b/client/components/notifications/notification.js @@ -0,0 +1,28 @@ +Template.notification.events({ + 'click .read-status .materialCheckBox'() { + const update = {}; + update[`profile.notifications.${this.index}.read`] = this.read + ? null + : Date.now(); + Users.update(Meteor.userId(), { $set: update }); + }, + 'click .remove a'() { + Meteor.user().removeNotification(this.activityData._id); + }, +}); + +Template.notification.helpers({ + mode: 'board', + isOfActivityType(activityId, type) { + const activity = Activities.findOne(activityId); + return activity && activity.activityType === type; + }, + activityType(activityId) { + const activity = Activities.findOne(activityId); + return activity ? activity.activityType : ''; + }, + activityUser(activityId) { + const activity = Activities.findOne(activityId); + return activity && activity.userId; + }, +}); diff --git a/client/components/notifications/notification.styl b/client/components/notifications/notification.styl new file mode 100644 index 00000000..0cf0cfd5 --- /dev/null +++ b/client/components/notifications/notification.styl @@ -0,0 +1,57 @@ +#notifications-drawer + &.show-read .notification.read + display: flex + + .notification + display: flex + float: none + padding: 12px 8px 8px + color: black + border-bottom: 1px solid #dbdbdb + + &.read + display: none + + .read-status + width: 30px + + input + width: 24px + height: 24px + + .activity-type + margin: 16px 0 0 + width: 17px + height: 17px + font-size: 17px + display: block + color: #bbb + + .details + width: calc(100% - 30px) + + .activity + display: flex + + .activity-desc + width: 100%; + + .activity-comment + display: block + width: 100% + border-radius: 3px + background: #fff + text-decoration: none + box-shadow: 0 1px 2px rgba(0,0,0,0.2) + margin-top: 5px + padding: 5px + + .activity-meta + display: block + font-size: 0.8em + color: #999 + font-style: italic + + .remove + a:hover + color #eb4646 !important diff --git a/client/components/notifications/notificationIcon.jade b/client/components/notifications/notificationIcon.jade new file mode 100644 index 00000000..04377606 --- /dev/null +++ b/client/components/notifications/notificationIcon.jade @@ -0,0 +1,53 @@ +template(name='notificationIcon') + if($in activityType 'deleteAttachment' 'addAttachment') + i.fa.fa-paperclip.activity-type(title="attachment") + else if($in activityType 'createBoard' 'importBoard') + i.fa.fa-chalkboard.activity-type(title="board") + + else if($in activityType 'createCard' 'importCard' 'moveCard') + +cardNotificationIcon + else if($in activityType 'moveCardBoard' 'archivedCard' 'restoredCard') + +cardNotificationIcon + //- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it + //- DRY and consistant + + else if($in activityType 'addChecklist' 'removedChecklist' 'completeChecklist') + +checklistNotificationIcon + else if($in activityType 'uncompleteChecklist') + +checklistNotificationIcon + //- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it + //- DRY and consistant + + else if($in activityType 'checkedItem' 'uncheckedItem' 'addChecklistItem' 'removedChecklistItem') + i.fa.fa-check-square.activity-type(title="checklist item") + else if($in activityType 'addComment') + i.fa.fa-comment-o.activity-type(title="comment") + else if($in activityType 'createCustomField' 'setCustomField' 'unsetCustomField') + i.fa.fa-code.activity-type(title="custom field") + else if($in activityType 'addedLabel' 'removedLabel') + i.fa.fa-tag.activity-type(title="label") + + else if($in activityType 'createList' 'removeList' 'archivedList') + +listNotificationIcon + else if($in activityType 'importList') + +listNotificationIcon + //- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it + //- DRY and consistant + + //- elswhere in the app we use fa-trello to indicate lists... + //- i personally like fa-columns a bit better + else if($in activityType 'unjoinMember' 'addBoardMember' 'joinMember' 'removeBoardMember') + i.fa.fa-user.activity-type(title="member") + else if($in activityType 'createSwimlane' 'archivedSwimlane') + i.fa.fa-th-large.activity-type(title="swimlane") + else + i.fa.fa-bug.activity-type(title="can't find icon for #{activityType}") + +template(name='cardNotificationIcon') + i.fa.fa-clone.activity-type(title="card") + +template(name='checklistNotificationIcon') + i.fa.fa-list.activity-type(title="checklist") + +template(name='listNotificationIcon') + i.fa.fa-columns.activity-type(title="list") diff --git a/client/components/notifications/notifications.jade b/client/components/notifications/notifications.jade new file mode 100644 index 00000000..bf8acbbf --- /dev/null +++ b/client/components/notifications/notifications.jade @@ -0,0 +1,5 @@ +template(name='notifications') + #notifications.board-header-btns.right + a.notifications-drawer-toggle.fa.fa-bell(class="{{#if $gt unreadNotifications 0}}alert{{/if}}") + if $.Session.get 'showNotificationsDrawer' + +notificationsDrawer(unreadNotifications=unreadNotifications) diff --git a/client/components/notifications/notifications.js b/client/components/notifications/notifications.js new file mode 100644 index 00000000..c0aa6cb5 --- /dev/null +++ b/client/components/notifications/notifications.js @@ -0,0 +1,32 @@ +// this hides the notifications drawer if anyone clicks off of the panel +Template.body.events({ + click(event) { + if ( + !$(event.target).is('#notifications *') && + Session.get('showNotificationsDrawer') + ) { + toggleNotificationsDrawer(); + } + }, +}); + +Template.notifications.helpers({ + unreadNotifications() { + const notifications = Users.findOne(Meteor.userId()).notifications(); + const unreadNotifications = _.filter(notifications, v => !v.read); + return unreadNotifications.length; + }, +}); + +Template.notifications.events({ + 'click .notifications-drawer-toggle'() { + toggleNotificationsDrawer(); + }, +}); + +export function toggleNotificationsDrawer() { + Session.set( + 'showNotificationsDrawer', + !Session.get('showNotificationsDrawer'), + ); +} diff --git a/client/components/notifications/notifications.styl b/client/components/notifications/notifications.styl new file mode 100644 index 00000000..710cd3f9 --- /dev/null +++ b/client/components/notifications/notifications.styl @@ -0,0 +1,17 @@ +#notifications + position: relative + + .notifications-drawer-toggle + display: block + line-height: 28px + color: #f2f2f2 + margin: 0 10px + width: 28px + height: 28px + text-align: center + border: 0 + padding: 0 + + &.alert + background-color: #eb4646; + diff --git a/client/components/notifications/notificationsDrawer.jade b/client/components/notifications/notificationsDrawer.jade new file mode 100644 index 00000000..fee6aef6 --- /dev/null +++ b/client/components/notifications/notificationsDrawer.jade @@ -0,0 +1,20 @@ +template(name='notificationsDrawer') + section#notifications-drawer(class="{{#if $.Session.get 'showReadNotifications'}}show-read{{/if}}") + .header + if $.Session.get 'showReadNotifications' + a.toggle-read {{_ 'filter-by-unread'}} + else + a.toggle-read {{_ 'view-all'}} + h5 {{_ 'notifications'}} + if($gt unreadNotifications 0) + |(#{unreadNotifications}) + a.fa.fa-times-thin.close + ul.notifications + each transformedProfile.notifications + +notification(activityData=activity index=dbIndex read=read) + if($gt unreadNotifications 0) + a.all-read {{_ 'mark-all-as-read'}} + if ($and ($.Session.get 'showReadNotifications') ($gt readNotifications 0)) + a.remove-read + i.fa.fa-trash + | {{_ 'remove-all-read'}} diff --git a/client/components/notifications/notificationsDrawer.js b/client/components/notifications/notificationsDrawer.js new file mode 100644 index 00000000..76abeea7 --- /dev/null +++ b/client/components/notifications/notificationsDrawer.js @@ -0,0 +1,53 @@ +import { toggleNotificationsDrawer } from './notifications.js'; + +Template.notificationsDrawer.onCreated(function() { + Meteor.subscribe('notificationActivities'); + Meteor.subscribe('notificationCards'); + Meteor.subscribe('notificationUsers'); + Meteor.subscribe('notificationsAttachments'); + Meteor.subscribe('notificationChecklistItems'); + Meteor.subscribe('notificationChecklists'); + Meteor.subscribe('notificationComments'); + Meteor.subscribe('notificationLists'); + Meteor.subscribe('notificationSwimlanes'); +}); + +Template.notificationsDrawer.helpers({ + transformedProfile() { + return Users.findOne(Meteor.userId()); + }, + readNotifications() { + const readNotifications = _.filter( + Meteor.user().profile.notifications, + v => !!v.read, + ); + return readNotifications.length; + }, +}); + +Template.notificationsDrawer.events({ + 'click .all-read'() { + const notifications = Meteor.user().profile.notifications; + for (const index in notifications) { + if (notifications.hasOwnProperty(index) && !notifications[index].read) { + const update = {}; + update[`profile.notifications.${index}.read`] = Date.now(); + Users.update(Meteor.userId(), { $set: update }); + } + } + }, + 'click .close'() { + toggleNotificationsDrawer(); + }, + 'click .toggle-read'() { + Session.set('showReadNotifications', !Session.get('showReadNotifications')); + }, + 'click .remove-read'() { + const user = Meteor.user(); + for (const notification of user.profile.notifications) { + if (notification.read) { + user.removeNotification(notification.activity); + } + } + }, +}); diff --git a/client/components/notifications/notificationsDrawer.styl b/client/components/notifications/notificationsDrawer.styl new file mode 100644 index 00000000..f99e1299 --- /dev/null +++ b/client/components/notifications/notificationsDrawer.styl @@ -0,0 +1,69 @@ +belize = #2980b9 + +section#notifications-drawer + position: fixed + top: 28px + right: 0 + width: 400px + background-color: #fafafa + box-shadow: 0 1px 2px rgba(0,0,0,0.15) + border-radius: 2px + max-height: calc(100vh - 28px - 36px) + color: black + padding-top 36px + + a:hover + color: belize !important + + .header + position: fixed + top 28px + right 0 + width calc(400px - 32px) + padding: 8px 16px + background: #ededed + border-bottom: 1px solid #dbdbdb + z-index 2 + + .toggle-read + position absolute + left 16px + top calc(50% - 8px) + color belize + + h5 + text-align: center + margin: 0 + + .close + position: absolute + top: calc(50% - 12px) + right: 12px + font-size: 24px + height: 24px + line-height: 24px + opacity 1 + + .all-read, + .remove-read + color belize + background-color: #fafafa + margin 8px 16px 12px + display inline-block + + .remove-read + float right + + &:hover + color #eb4646 !important + + i.fa + color inherit + + + ul.notifications + display: block + padding: 0px 16px + margin: 0 + height: calc(100vh - 102px) + overflow-y: scroll diff --git a/client/components/rules/actions/boardActions.jade b/client/components/rules/actions/boardActions.jade index 6034184c..fda15062 100644 --- a/client/components/rules/actions/boardActions.jade +++ b/client/components/rules/actions/boardActions.jade @@ -1,29 +1,42 @@ template(name="boardActions") div.trigger-item div.trigger-content - div.trigger-text + 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 + 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 + 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-text + | {{_'r-the-board'}} + div.trigger-dropdown + select(id="board-id") + each boards + if $eq _id currentBoard._id + option(value="{{_id}}" selected) {{_ 'current'}} + else + option(value="{{_id}}") {{title}} + div.trigger-text + | {{_'r-in-list'}} div.trigger-dropdown input(id="listName",type=text,placeholder="{{_'r-name'}}") + div.trigger-text + | {{_'r-in-swimlane'}} + div.trigger-dropdown + input(id="swimlaneName",type=text,placeholder="{{_'r-name'}}") div.trigger-button.js-add-spec-move-action.js-goto-rules i.fa.fa-plus @@ -33,14 +46,14 @@ template(name="boardActions") select(id="arch-action") option(value="archive") {{_'r-archive'}} option(value="unarchive") {{_'r-unarchive'}} - div.trigger-text + 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 + div.trigger-text | {{_'r-add-swimlane'}} div.trigger-dropdown input(id="swimlane-name",type=text,placeholder="{{_'r-name'}}") @@ -49,15 +62,15 @@ template(name="boardActions") div.trigger-item div.trigger-content - div.trigger-text + div.trigger-text | {{_'r-create-card'}} div.trigger-dropdown input(id="card-name",type=text,placeholder="{{_'r-name'}}") - div.trigger-text + div.trigger-text | {{_'r-in-list'}} div.trigger-dropdown input(id="list-name",type=text,placeholder="{{_'r-name'}}") - div.trigger-text + div.trigger-text | {{_'r-in-swimlane'}} div.trigger-dropdown input(id="swimlane-name2",type=text,placeholder="{{_'r-name'}}") @@ -65,8 +78,8 @@ template(name="boardActions") i.fa.fa-plus - - + + diff --git a/client/components/rules/actions/boardActions.js b/client/components/rules/actions/boardActions.js index 8568d2bf..02910cc1 100644 --- a/client/components/rules/actions/boardActions.js +++ b/client/components/rules/actions/boardActions.js @@ -1,6 +1,22 @@ BlazeComponent.extendComponent({ onCreated() {}, + boards() { + const boards = Boards.find( + { + archived: false, + 'members.userId': Meteor.userId(), + _id: { + $ne: Meteor.user().getTemplatesBoardId(), + }, + }, + { + sort: { sort: 1 /* boards default sorting */ }, + }, + ); + return boards; + }, + events() { return [ { @@ -52,15 +68,18 @@ BlazeComponent.extendComponent({ 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 swimlaneName = this.find('#swimlaneName').value; + const listName = this.find('#listName').value; const boardId = Session.get('currentBoard'); + const destBoardId = this.find('#board-id').value; const desc = Utils.getTriggerActionDesc(event, this); if (actionSelected === 'top') { const triggerId = Triggers.insert(trigger); const actionId = Actions.insert({ actionType: 'moveCardToTop', - listTitle, - boardId, + listName, + swimlaneName, + boardId: destBoardId, desc, }); Rules.insert({ @@ -74,8 +93,9 @@ BlazeComponent.extendComponent({ const triggerId = Triggers.insert(trigger); const actionId = Actions.insert({ actionType: 'moveCardToBottom', - listTitle, - boardId, + listName, + swimlaneName, + boardId: destBoardId, desc, }); Rules.insert({ diff --git a/client/components/settings/informationBody.jade b/client/components/settings/informationBody.jade index 2c615ffd..0f85dd9c 100644 --- a/client/components/settings/informationBody.jade +++ b/client/components/settings/informationBody.jade @@ -4,12 +4,16 @@ template(name='information') | {{_ 'error-notAuthorized'}} else .content-title - span {{_ 'info'}} + span + i.fa.fa-info-circle + | {{_ 'info'}} .content-body .side-menu ul li.active - a.js-setting-menu(data-id="information-display") {{_ 'info'}} + a.js-setting-menu(data-id="information-display") + i.fa.fa-info-circle + | {{_ 'info'}} .main-body +statistics diff --git a/client/components/settings/peopleBody.jade b/client/components/settings/peopleBody.jade index d8f672b0..fef1067e 100644 --- a/client/components/settings/peopleBody.jade +++ b/client/components/settings/peopleBody.jade @@ -5,16 +5,22 @@ template(name="people") else .content-title.ext-box .ext-box-left - span {{_ 'people'}} + span + i.fa.fa-users + | {{_ 'people'}} input#searchInput(placeholder="{{_ 'search'}}") - button#searchButton {{_ 'search'}} + button#searchButton + i.fa.fa-search + | {{_ 'search'}} .ext-box-right span {{_ 'people-number'}} #{peopleNumber} .content-body .side-menu ul li.active - a.js-setting-menu(data-id="people-setting") {{_ 'people'}} + a.js-setting-menu(data-id="people-setting") + i.fa.fa-users + | {{_ 'people'}} .main-body if loading.get +spinner @@ -34,9 +40,15 @@ template(name="peopleGeneral") th {{_ 'active'}} th {{_ 'authentication-method'}} th + +newUserRow each user in peopleList +peopleRow(userId=user._id) +template(name="newUserRow") + a.new-user + i.fa.fa-edit + | {{_ 'new'}} + template(name="peopleRow") tr if userData.loginDisabled @@ -90,6 +102,7 @@ template(name="peopleRow") td {{_ userData.authenticationMethod }} td a.edit-user + i.fa.fa-edit | {{_ 'edit'}} template(name="editUserPopup") @@ -97,7 +110,7 @@ template(name="editUserPopup") label.hide.userId(type="text" value=user._id) label | {{_ 'fullname'}} - input.js-profile-fullname(type="text" value=user.profile.fullname autofocus) + input.js-profile-fullname(type="text" value=user.profile.fullname) label | {{_ 'username'}} span.error.hide.username-taken @@ -141,3 +154,49 @@ template(name="editUserPopup") // div // input#deleteButton.primary.wide(type="button" value="{{_ 'delete'}}") +template(name="newUserPopup") + form + //label.hide.userId(type="text" value=user._id) + label + | {{_ 'fullname'}} + input.js-profile-fullname(type="text" value="") + label + | {{_ 'username'}} + span.error.hide.username-taken + | {{_ 'error-username-taken'}} + //if isLdap + // input.js-profile-username(type="text" value=user.username readonly) + //else + input.js-profile-username(type="text" value="") + label + | {{_ 'email'}} + span.error.hide.email-taken + | {{_ 'error-email-taken'}} + //if isLdap + // input.js-profile-email(type="email" value="{{user.emails.[0].address}}" readonly) + //else + input.js-profile-email(type="email" value="") + label + | {{_ 'admin'}} + select.select-role.js-profile-isadmin + option(value="false" selected="selected") {{_ 'no'}} + option(value="true") {{_ 'yes'}} + label + | {{_ 'active'}} + select.select-active.js-profile-isactive + option(value="false" selected="selected") {{_ 'yes'}} + option(value="true") {{_ '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'}}") diff --git a/client/components/settings/peopleBody.js b/client/components/settings/peopleBody.js index 8610034e..186afd58 100644 --- a/client/components/settings/peopleBody.js +++ b/client/components/settings/peopleBody.js @@ -39,6 +39,9 @@ BlazeComponent.extendComponent({ this.filterPeople(); } }, + 'click #newUserButton'() { + Popup.open('newUser'); + }, }, ]; }, @@ -141,6 +144,47 @@ Template.editUserPopup.helpers({ }, }); +Template.newUserPopup.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.newUserPopup.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({ onCreated() {}, user() { @@ -155,6 +199,16 @@ BlazeComponent.extendComponent({ }, }).register('peopleRow'); +BlazeComponent.extendComponent({ + events() { + return [ + { + 'click a.new-user': Popup.open('newUser'), + }, + ]; + }, +}).register('newUserRow'); + Template.editUserPopup.events({ submit(event, templateInstance) { event.preventDefault(); @@ -248,3 +302,44 @@ Template.editUserPopup.events({ Popup.close(); }), }); + +Template.newUserPopup.events({ + submit(event, templateInstance) { + event.preventDefault(); + const fullname = templateInstance.find('.js-profile-fullname').value.trim(); + const username = templateInstance.find('.js-profile-username').value.trim(); + const password = templateInstance.find('.js-profile-password').value; + const isAdmin = templateInstance.find('.js-profile-isadmin').value.trim(); + const isActive = templateInstance.find('.js-profile-isactive').value.trim(); + const email = templateInstance.find('.js-profile-email').value.trim(); + + Meteor.call( + 'setCreateUser', + fullname, + username, + password, + isAdmin, + isActive, + email.toLowerCase(), + function(error) { + const usernameMessageElement = templateInstance.$('.username-taken'); + const emailMessageElement = templateInstance.$('.email-taken'); + if (error) { + const errorElement = error.error; + if (errorElement === 'username-already-taken') { + usernameMessageElement.show(); + emailMessageElement.hide(); + } else if (errorElement === 'email-already-taken') { + usernameMessageElement.hide(); + emailMessageElement.show(); + } + } else { + usernameMessageElement.hide(); + emailMessageElement.hide(); + Popup.close(); + } + }, + ); + Popup.close(); + }, +}); diff --git a/client/components/settings/peopleBody.styl b/client/components/settings/peopleBody.styl index 80387611..c223e181 100644 --- a/client/components/settings/peopleBody.styl +++ b/client/components/settings/peopleBody.styl @@ -33,7 +33,7 @@ table padding: 0; button - min-width: 60px; + min-width: 90px; .content-wrapper margin-top: 10px diff --git a/client/components/settings/settingBody.jade b/client/components/settings/settingBody.jade index 04b635e8..835a3b81 100644 --- a/client/components/settings/settingBody.jade +++ b/client/components/settings/settingBody.jade @@ -4,22 +4,35 @@ template(name="setting") | {{_ 'error-notAuthorized'}} else .content-title + i.fa.fa-cog span {{_ 'settings'}} .content-body .side-menu ul li.active - a.js-setting-menu(data-id="registration-setting") {{_ 'registration'}} + a.js-setting-menu(data-id="registration-setting") + i.fa.fa-sign-in + | {{_ 'registration'}} li - a.js-setting-menu(data-id="email-setting") {{_ 'email'}} + a.js-setting-menu(data-id="email-setting") + i.fa.fa-envelope + | {{_ 'email'}} li - a.js-setting-menu(data-id="account-setting") {{_ 'accounts'}} + a.js-setting-menu(data-id="account-setting") + i.fa.fa-users + | {{_ 'accounts'}} li - a.js-setting-menu(data-id="announcement-setting") {{_ 'admin-announcement'}} + a.js-setting-menu(data-id="announcement-setting") + i.fa.fa-bullhorn + | {{_ 'admin-announcement'}} li - a.js-setting-menu(data-id="layout-setting") {{_ 'layout'}} + a.js-setting-menu(data-id="layout-setting") + i.fa.fa-object-group + | {{_ 'layout'}} li - a.js-setting-menu(data-id="webhook-setting") {{_ 'global-webhook'}} + a.js-setting-menu(data-id="webhook-setting") + i.fa.fa-globe + | {{_ 'global-webhook'}} .main-body if loading.get +spinner @@ -171,12 +184,6 @@ template(name='layoutSettings') .title {{_ 'custom-product-name'}} .form-group input.wekan-form-control#product-name(type="text", placeholder="" value="{{currentSetting.productName}}") - li.layout-form - .title {{_ 'add-custom-html-after-body-start'}} - textarea#customHTMLafterBodyStart.wekan-form-control= currentSetting.customHTMLafterBodyStart - li.layout-form - .title {{_ 'add-custom-html-before-body-end'}} - textarea#customHTMLbeforeBodyEnd.wekan-form-control= currentSetting.customHTMLbeforeBodyEnd li button.js-save-layout.primary {{_ 'save'}} diff --git a/client/components/settings/settingBody.js b/client/components/settings/settingBody.js index 4ff5aedd..62752084 100644 --- a/client/components/settings/settingBody.js +++ b/client/components/settings/settingBody.js @@ -48,7 +48,7 @@ BlazeComponent.extendComponent({ 'members.isAdmin': true, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); }, @@ -171,20 +171,12 @@ BlazeComponent.extendComponent({ 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, }, diff --git a/client/components/settings/settingBody.styl b/client/components/settings/settingBody.styl index bcbd2ea1..d6ac32b2 100644 --- a/client/components/settings/settingBody.styl +++ b/client/components/settings/settingBody.styl @@ -41,15 +41,18 @@ &: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 + i + margin-right: 20px + .main-body padding: 0.1em 1em -webkit-user-select: text // Safari 3.1+ diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index ccfadc0c..6bfedc9c 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -37,11 +37,12 @@ template(name='homeSidebar') template(name="membersWidget") .board-widget.board-widget-members h3 - i.fa.fa-user + i.fa.fa-users | {{_ 'members'}} unless currentUser.isCommentOnly - a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}").right - i.board-header-btn-icon.fa.fa-cog + unless currentUser.isWorker + 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 @@ -71,6 +72,108 @@ template(name="boardChangeColorPopup") if isSelected i.fa.fa-check +template(name="boardCardSettingsPopup") + form.board-card-settings + h3 {{_ 'show-on-card'}} + div.check-div + a.flex.js-field-has-receiveddate(class="{{#if allowsReceivedDate}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsReceivedDate}}is-checked{{/if}}") + span + i.fa.fa-sign-out + | {{_ 'card-received'}} + div.check-div + a.flex.js-field-has-startdate(class="{{#if allowsStartDate}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsStartDate}}is-checked{{/if}}") + span + i.fa.fa-hourglass-start + | {{_ 'card-start'}} + div.check-div + a.flex.js-field-has-duedate(class="{{#if allowsDueDate}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsDueDate}}is-checked{{/if}}") + span + i.fa.fa-sign-in + | {{_ 'card-due'}} + div.check-div + a.flex.js-field-has-enddate(class="{{#if allowsEndDate}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsEndDate}}is-checked{{/if}}") + span + i.fa.fa-hourglass-end + | {{_ 'card-end'}} + div.check-div + a.flex.js-field-has-members(class="{{#if allowsMembers}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsMembers}}is-checked{{/if}}") + span + i.fa.fa-users + | {{_ 'members'}} + div.check-div + a.flex.js-field-has-assignee(class="{{#if allowsAssignee}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsAssignee}}is-checked{{/if}}") + span + i.fa.fa-user + | {{_ 'assignee'}} + div.check-div + a.flex.js-field-has-assigned-by(class="{{#if allowsAssignedBy}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsAssignedBy}}is-checked{{/if}}") + span + i.fa.fa-shopping-cart + | {{_ 'assigned-by'}} + div.check-div + a.flex.js-field-has-requested-by(class="{{#if allowsRequestedBy}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsRequestedBy}}is-checked{{/if}}") + span + i.fa.fa-user-plus + | {{_ 'requested-by'}} + div.check-div + a.flex.js-field-has-labels(class="{{#if allowsLabels}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsLabels}}is-checked{{/if}}") + span + i.fa.fa-tags + | {{_ 'labels'}} + div.check-div + a.flex.js-field-has-description-title(class="{{#if allowsDescriptionTitle}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsDescriptionTitle}}is-checked{{/if}}") + span + i.fa.fa-align-left + | {{_ 'description'}} + | {{_ 'title'}} + div.check-div + a.flex.js-field-has-description-text(class="{{#if allowsDescriptionText}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsDescriptionText}}is-checked{{/if}}") + span + i.fa.fa-align-left + | {{_ 'description'}} + | {{_ 'custom-field-text'}} + div.check-div + a.flex.js-field-has-checklists(class="{{#if allowsChecklists}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsChecklists}}is-checked{{/if}}") + span + i.fa.fa-check + | {{_ 'checklists'}} + div.check-div + a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}") + span + i.fa.fa-sitemap + | {{_ 'subtasks'}} + div.check-div + a.flex.js-field-has-attachments(class="{{#if allowsAttachments}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsAttachments}}is-checked{{/if}}") + span + i.fa.fa-paperclip + | {{_ 'attachments'}} + //div.check-div + // a.flex.js-field-has-comments(class="{{#if allowsComments}}is-checked{{/if}}") + // .materialCheckBox(class="{{#if allowsComments}}is-checked{{/if}}") + // span + // i.fa.fa-comment-o + // | {{_ 'comment'}} + //div.check-div + // a.flex.js-field-has-activities(class="{{#if allowsActivities}}is-checked{{/if}}") + // .materialCheckBox(class="{{#if allowsActivities}}is-checked{{/if}}") + // span + // i.fa.fa-history + // | {{_ 'activities'}} + template(name="boardSubtaskSettingsPopup") form.board-subtask-settings h3 {{_ 'show-parent-in-minicard'}} @@ -130,7 +233,9 @@ template(name="chooseBoardSource") template(name="archiveBoardPopup") p {{_ 'close-board-pop'}} - button.js-confirm.negate.full(type="submit") {{_ 'archive'}} + button.js-confirm.negate.full(type="submit") + i.fa.fa-archive + | {{_ 'archive'}} template(name="outgoingWebhooksPopup") each integrations @@ -140,7 +245,7 @@ template(name="outgoingWebhooksPopup") 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-url(type="text" name="url" value=url) 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 @@ -152,7 +257,7 @@ template(name="outgoingWebhooksPopup") input(type="hidden" value=_id name="id") input.primary.wide(type="submit" value="{{_ 'save'}}") form.integration-form - input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" autofocus) + input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title") 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") @@ -162,38 +267,98 @@ template(name="outgoingWebhooksPopup") template(name="boardMenuPopup") ul.pop-over-list - li: a.js-custom-fields {{_ 'custom-fields'}} - li: a.js-open-archives {{_ 'archived-items'}} + li + a.js-open-rules-view(title="{{_ 'rules'}}") + i.fa.fa-magic + | {{_ 'rules'}} + li + a.js-custom-fields + i.fa.fa-list-alt + | {{_ 'custom-fields'}} + li + a.js-open-archives + i.fa.fa-archive + | {{_ 'archived-items'}} if currentUser.isBoardAdmin - li: a.js-change-board-color {{_ 'board-change-color'}} + li + a.js-change-board-color + i.fa.fa-paint-brush + | {{_ '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'}} + li + a.js-change-language + i.fa.fa-flag + | {{_ '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 withApi + li + a(href="{{exportUrl}}", download="{{exportFilename}}") + i.fa.fa-share-alt + | {{_ 'export-board'}} + li + a.js-outgoing-webhooks + i.fa.fa-globe + | {{_ 'outgoing-webhooks'}} + li + a.js-card-settings + i.fa.fa-id-card-o + | {{_ 'card-settings'}} + li + a.js-subtask-settings + i.fa.fa-sitemap + | {{_ 'subtask-settings'}} + unless currentBoard.isTemplatesBoard + hr + ul.pop-over-list + li + a.js-archive-board + i.fa.fa-arrow-right + i.fa.fa-archive + | {{_ 'archive-board'}} 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'}} + if withApi + li + a(href="{{exportUrl}}", download="{{exportFilename}}") + i.fa.fa-share-alt + i.fa.fa-sign-out + | {{_ 'export-board'}} + li + a.js-import-board + i.fa.fa-share-alt + i.fa.fa-sign-in + | {{_ 'import-board-c'}} + li + a.js-archive-board + i.fa.fa-arrow-right + i.fa.fa-archive + | {{_ 'archive-board'}} + li + a.js-outgoing-webhooks + i.fa.fa-globe + | {{_ 'outgoing-webhooks'}} hr ul.pop-over-list - li: a.js-subtask-settings {{_ 'subtask-settings'}} + li + a.js-card-settings + i.fa.fa-id-card-o + | {{_ 'card-settings'}} + hr + ul.pop-over-list + li + a.js-subtask-settings + i.fa.fa-sitemap + | {{_ 'subtask-settings'}} template(name="labelsWidget") .board-widget.board-widget-labels @@ -203,7 +368,7 @@ template(name="labelsWidget") .board-widget-content each currentBoard.labels a.card-label(class="card-label-{{color}}" - class="{{#if currentUser.isNotCommentOnly}}js-label{{/if}}") + class="{{#if currentUser.isNotCommentOnly}}{{#if currentUser.isNotWorker}}js-label{{/if}}{{/if}}") span.card-label-name +viewer = name @@ -232,12 +397,12 @@ template(name="memberPopup") 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'}} - + unless currentUser.isWorker + 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") p {{_ 'remove-member-pop' name=user.profile.fullname username=user.username boardTitle=board.title}} @@ -301,6 +466,12 @@ template(name="changePermissionsPopup") if isCommentOnly i.fa.fa-check span.sub-name {{_ 'comment-only-desc'}} + li + a(class="{{#if isLastAdmin}}disabled{{else}}js-set-worker{{/if}}") + | {{_ 'worker'}} + if isWorker + i.fa.fa-check + span.sub-name {{_ 'worker-desc'}} if isLastAdmin hr p.quiet.bottom {{_ 'last-admin-desc'}} diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index caf36020..cbe00797 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -112,12 +112,10 @@ BlazeComponent.extendComponent({ currentUser = Meteor.user(); if (currentUser) { Meteor.call('toggleMinicardLabelText'); + } else if (cookies.has('hiddenMinicardLabelText')) { + cookies.remove('hiddenMinicardLabelText'); } else { - if (cookies.has('hiddenMinicardLabelText')) { - cookies.remove('hiddenMinicardLabelText'); - } else { - cookies.set('hiddenMinicardLabelText', 'true'); - } + cookies.set('hiddenMinicardLabelText', 'true'); } }, 'click .js-shortcuts'() { @@ -135,12 +133,10 @@ Template.homeSidebar.helpers({ currentUser = Meteor.user(); if (currentUser) { return (currentUser.profile || {}).hiddenMinicardLabelText; + } else if (cookies.has('hiddenMinicardLabelText')) { + return true; } else { - if (cookies.has('hiddenMinicardLabelText')) { - return true; - } else { - return false; - } + return false; } }, }); @@ -165,10 +161,13 @@ Template.memberPopup.helpers({ const currentBoard = Boards.findOne(Session.get('currentBoard')); const commentOnly = currentBoard.hasCommentOnly(this.userId); const noComments = currentBoard.hasNoComments(this.userId); + const worker = currentBoard.hasWorker(this.userId); if (commentOnly) { return TAPi18n.__('comment-only').toLowerCase(); } else if (noComments) { return TAPi18n.__('no-comments').toLowerCase(); + } else if (worker) { + return TAPi18n.__('worker').toLowerCase(); } else { return TAPi18n.__(type).toLowerCase(); } @@ -183,6 +182,10 @@ Template.memberPopup.helpers({ Template.boardMenuPopup.events({ 'click .js-rename-board': Popup.open('boardChangeTitle'), + 'click .js-open-rules-view'() { + Modal.openWide('rulesMain'); + Popup.close(); + }, 'click .js-custom-fields'() { Sidebar.setView('customFields'); Popup.close(); @@ -209,9 +212,20 @@ Template.boardMenuPopup.events({ 'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'), 'click .js-import-board': Popup.open('chooseBoardSource'), 'click .js-subtask-settings': Popup.open('boardSubtaskSettings'), + 'click .js-card-settings': Popup.open('boardCardSettings'), +}); + +Template.boardMenuPopup.onCreated(function() { + this.apiEnabled = new ReactiveVar(false); + Meteor.call('_isApiEnabled', (e, result) => { + this.apiEnabled.set(result); + }); }); Template.boardMenuPopup.helpers({ + withApi() { + return Template.instance().apiEnabled.get(); + }, exportUrl() { const params = { boardId: Session.get('currentBoard'), @@ -271,6 +285,14 @@ Template.membersWidget.helpers({ const user = Meteor.user(); return user && user.isInvitedTo(Session.get('currentBoard')); }, + isWorker() { + const user = Meteor.user(); + if (user) { + return Meteor.call(Boards.hasWorker(user.memberId)); + } else { + return false; + } + }, }); Template.membersWidget.events({ @@ -465,6 +487,10 @@ BlazeComponent.extendComponent({ return this.currentBoard.allowsSubtasks; }, + allowsReceivedDate() { + return this.currentBoard.allowsReceivedDate; + }, + isBoardSelected() { return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id; }, @@ -483,7 +509,7 @@ BlazeComponent.extendComponent({ 'members.userId': Meteor.userId(), }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); }, @@ -580,6 +606,359 @@ BlazeComponent.extendComponent({ BlazeComponent.extendComponent({ onCreated() { + this.currentBoard = Boards.findOne(Session.get('currentBoard')); + }, + + allowsReceivedDate() { + return this.currentBoard.allowsReceivedDate; + }, + + allowsStartDate() { + return this.currentBoard.allowsStartDate; + }, + + allowsDueDate() { + return this.currentBoard.allowsDueDate; + }, + + allowsEndDate() { + return this.currentBoard.allowsEndDate; + }, + + allowsSubtasks() { + return this.currentBoard.allowsSubtasks; + }, + + allowsMembers() { + return this.currentBoard.allowsMembers; + }, + + allowsAssignee() { + return this.currentBoard.allowsAssignee; + }, + + allowsAssignedBy() { + return this.currentBoard.allowsAssignedBy; + }, + + allowsRequestedBy() { + return this.currentBoard.allowsRequestedBy; + }, + + allowsLabels() { + return this.currentBoard.allowsLabels; + }, + + allowsChecklists() { + return this.currentBoard.allowsChecklists; + }, + + allowsAttachments() { + return this.currentBoard.allowsAttachments; + }, + + allowsComments() { + return this.currentBoard.allowsComments; + }, + + allowsDescriptionTitle() { + return this.currentBoard.allowsDescriptionTitle; + }, + + allowsDescriptionText() { + return this.currentBoard.allowsDescriptionText; + }, + + isBoardSelected() { + return this.currentBoard.dateSettingsDefaultBoardID; + }, + + isNullBoardSelected() { + return ( + this.currentBoard.dateSettingsDefaultBoardId === null || + this.currentBoard.dateSettingsDefaultBoardId === undefined + ); + }, + + boards() { + return Boards.find( + { + archived: false, + 'members.userId': Meteor.userId(), + }, + { + sort: { sort: 1 /* boards default sorting */ }, + }, + ); + }, + + lists() { + return Lists.find( + { + boardId: this.currentBoard._id, + archived: false, + }, + { + sort: ['title'], + }, + ); + }, + + hasLists() { + return this.lists().count() > 0; + }, + + isListSelected() { + return ( + this.currentBoard.dateSettingsDefaultBoardId === this.currentData()._id + ); + }, + + events() { + return [ + { + 'click .js-field-has-receiveddate'(evt) { + evt.preventDefault(); + this.currentBoard.allowsReceivedDate = !this.currentBoard + .allowsReceivedDate; + this.currentBoard.setAllowsReceivedDate( + this.currentBoard.allowsReceivedDate, + ); + $(`.js-field-has-receiveddate ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsReceivedDate, + ); + $('.js-field-has-receiveddate').toggleClass( + CKCLS, + this.currentBoard.allowsReceivedDate, + ); + }, + 'click .js-field-has-startdate'(evt) { + evt.preventDefault(); + this.currentBoard.allowsStartDate = !this.currentBoard + .allowsStartDate; + this.currentBoard.setAllowsStartDate( + this.currentBoard.allowsStartDate, + ); + $(`.js-field-has-startdate ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsStartDate, + ); + $('.js-field-has-startdate').toggleClass( + CKCLS, + this.currentBoard.allowsStartDate, + ); + }, + 'click .js-field-has-enddate'(evt) { + evt.preventDefault(); + this.currentBoard.allowsEndDate = !this.currentBoard.allowsEndDate; + this.currentBoard.setAllowsEndDate(this.currentBoard.allowsEndDate); + $(`.js-field-has-enddate ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsEndDate, + ); + $('.js-field-has-enddate').toggleClass( + CKCLS, + this.currentBoard.allowsEndDate, + ); + }, + 'click .js-field-has-duedate'(evt) { + evt.preventDefault(); + this.currentBoard.allowsDueDate = !this.currentBoard.allowsDueDate; + this.currentBoard.setAllowsDueDate(this.currentBoard.allowsDueDate); + $(`.js-field-has-duedate ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsDueDate, + ); + $('.js-field-has-duedate').toggleClass( + CKCLS, + this.currentBoard.allowsDueDate, + ); + }, + 'click .js-field-has-subtasks'(evt) { + evt.preventDefault(); + this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks; + this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks); + $(`.js-field-has-subtasks ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsSubtasks, + ); + $('.js-field-has-subtasks').toggleClass( + CKCLS, + this.currentBoard.allowsSubtasks, + ); + }, + 'click .js-field-has-members'(evt) { + evt.preventDefault(); + this.currentBoard.allowsMembers = !this.currentBoard.allowsMembers; + this.currentBoard.setAllowsMembers(this.currentBoard.allowsMembers); + $(`.js-field-has-members ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsMembers, + ); + $('.js-field-has-members').toggleClass( + CKCLS, + this.currentBoard.allowsMembers, + ); + }, + 'click .js-field-has-assignee'(evt) { + evt.preventDefault(); + this.currentBoard.allowsAssignee = !this.currentBoard.allowsAssignee; + this.currentBoard.setAllowsAssignee(this.currentBoard.allowsAssignee); + $(`.js-field-has-assignee ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsAssignee, + ); + $('.js-field-has-assignee').toggleClass( + CKCLS, + this.currentBoard.allowsAssignee, + ); + }, + 'click .js-field-has-assigned-by'(evt) { + evt.preventDefault(); + this.currentBoard.allowsAssignedBy = !this.currentBoard + .allowsAssignedBy; + this.currentBoard.setAllowsAssignedBy( + this.currentBoard.allowsAssignedBy, + ); + $(`.js-field-has-assigned-by ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsAssignedBy, + ); + $('.js-field-has-assigned-by').toggleClass( + CKCLS, + this.currentBoard.allowsAssignedBy, + ); + }, + 'click .js-field-has-requested-by'(evt) { + evt.preventDefault(); + this.currentBoard.allowsRequestedBy = !this.currentBoard + .allowsRequestedBy; + this.currentBoard.setAllowsRequestedBy( + this.currentBoard.allowsRequestedBy, + ); + $(`.js-field-has-requested-by ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsRequestedBy, + ); + $('.js-field-has-requested-by').toggleClass( + CKCLS, + this.currentBoard.allowsRequestedBy, + ); + }, + 'click .js-field-has-labels'(evt) { + evt.preventDefault(); + this.currentBoard.allowsLabels = !this.currentBoard.allowsLabels; + this.currentBoard.setAllowsLabels(this.currentBoard.allowsLabels); + $(`.js-field-has-labels ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsAssignee, + ); + $('.js-field-has-labels').toggleClass( + CKCLS, + this.currentBoard.allowsLabels, + ); + }, + 'click .js-field-has-description-title'(evt) { + evt.preventDefault(); + this.currentBoard.allowsDescriptionTitle = !this.currentBoard + .allowsDescriptionTitle; + this.currentBoard.setAllowsDescriptionTitle( + this.currentBoard.allowsDescriptionTitle, + ); + $(`.js-field-has-description-title ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsDescriptionTitle, + ); + $('.js-field-has-description-title').toggleClass( + CKCLS, + this.currentBoard.allowsDescriptionTitle, + ); + }, + 'click .js-field-has-description-text'(evt) { + evt.preventDefault(); + this.currentBoard.allowsDescriptionText = !this.currentBoard + .allowsDescriptionText; + this.currentBoard.setAllowsDescriptionText( + this.currentBoard.allowsDescriptionText, + ); + $(`.js-field-has-description-text ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsDescriptionText, + ); + $('.js-field-has-description-text').toggleClass( + CKCLS, + this.currentBoard.allowsDescriptionText, + ); + }, + 'click .js-field-has-checklists'(evt) { + evt.preventDefault(); + this.currentBoard.allowsChecklists = !this.currentBoard + .allowsChecklists; + this.currentBoard.setAllowsChecklists( + this.currentBoard.allowsChecklists, + ); + $(`.js-field-has-checklists ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsChecklists, + ); + $('.js-field-has-checklists').toggleClass( + CKCLS, + this.currentBoard.allowsChecklists, + ); + }, + 'click .js-field-has-attachments'(evt) { + evt.preventDefault(); + this.currentBoard.allowsAttachments = !this.currentBoard + .allowsAttachments; + this.currentBoard.setAllowsAttachments( + this.currentBoard.allowsAttachments, + ); + $(`.js-field-has-attachments ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsAttachments, + ); + $('.js-field-has-attachments').toggleClass( + CKCLS, + this.currentBoard.allowsAttachments, + ); + }, + 'click .js-field-has-comments'(evt) { + evt.preventDefault(); + this.currentBoard.allowsComments = !this.currentBoard.allowsComments; + this.currentBoard.setAllowsComments(this.currentBoard.allowsComments); + $(`.js-field-has-comments ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsComments, + ); + $('.js-field-has-comments').toggleClass( + CKCLS, + this.currentBoard.allowsComments, + ); + }, + 'click .js-field-has-activities'(evt) { + evt.preventDefault(); + this.currentBoard.allowsActivities = !this.currentBoard + .allowsActivities; + this.currentBoard.setAllowsActivities( + this.currentBoard.allowsActivities, + ); + $(`.js-field-has-activities ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsActivities, + ); + $('.js-field-has-activities').toggleClass( + CKCLS, + this.currentBoard.allowsActivities, + ); + }, + }, + ]; + }, +}).register('boardCardSettingsPopup'); + +BlazeComponent.extendComponent({ + onCreated() { this.error = new ReactiveVar(''); this.loading = new ReactiveVar(false); }, @@ -648,7 +1027,7 @@ BlazeComponent.extendComponent({ }).register('addMemberPopup'); Template.changePermissionsPopup.events({ - 'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only'( + 'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only, click .js-set-worker'( event, ) { const currentBoard = Boards.findOne(Session.get('currentBoard')); @@ -658,11 +1037,13 @@ Template.changePermissionsPopup.events({ 'js-set-comment-only', ); const isNoComments = $(event.currentTarget).hasClass('js-set-no-comments'); + const isWorker = $(event.currentTarget).hasClass('js-set-worker'); currentBoard.setMemberPermission( memberId, isAdmin, isNoComments, isCommentOnly, + isWorker, ); Popup.back(1); }, @@ -679,7 +1060,8 @@ Template.changePermissionsPopup.helpers({ return ( !currentBoard.hasAdmin(this.userId) && !currentBoard.hasNoComments(this.userId) && - !currentBoard.hasCommentOnly(this.userId) + !currentBoard.hasCommentOnly(this.userId) && + !currentBoard.hasWorker(this.userId) ); }, @@ -699,6 +1081,13 @@ Template.changePermissionsPopup.helpers({ ); }, + isWorker() { + const currentBoard = Boards.findOne(Session.get('currentBoard')); + return ( + !currentBoard.hasAdmin(this.userId) && currentBoard.hasWorker(this.userId) + ); + }, + isLastAdmin() { const currentBoard = Boards.findOne(Session.get('currentBoard')); return ( diff --git a/client/components/sidebar/sidebar.styl b/client/components/sidebar/sidebar.styl index 740186b5..c1047277 100644 --- a/client/components/sidebar/sidebar.styl +++ b/client/components/sidebar/sidebar.styl @@ -109,7 +109,7 @@ color: darken(white, 40%) .board-sidebar - width: 248px + width: 548px right: -@width transition: top .1s, right .1s, width .1s diff --git a/client/components/sidebar/sidebarArchives.jade b/client/components/sidebar/sidebarArchives.jade index 466d2cb0..56423ad7 100644 --- a/client/components/sidebar/sidebarArchives.jade +++ b/client/components/sidebar/sidebarArchives.jade @@ -2,54 +2,60 @@ template(name="archivesSidebar") if isArchiveReady.get +basicTabs(tabs=tabs) +tabContent(slug="cards") - p.quiet - a.js-restore-all-cards {{_ 'restore-all'}} - | - - a.js-delete-all-cards {{_ 'delete-all'}} + unless isWorker + p.quiet + a.js-restore-all-cards {{_ 'restore-all'}} + | - + a.js-delete-all-cards {{_ 'delete-all'}} each archivedCards .minicard-wrapper.js-minicard +minicard(this) if currentUser.isBoardMember - p.quiet - a.js-restore-card {{_ 'restore'}} - | - - a.js-delete-card {{_ 'delete'}} + unless isWorker + p.quiet + a.js-restore-card {{_ 'restore'}} + | - + a.js-delete-card {{_ 'delete'}} if cardIsInArchivedList p.quiet.small ({{_ 'warn-list-archived'}}) else 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'}} + unless isWorker + 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 = title if currentUser.isBoardMember - p.quiet - a.js-restore-list {{_ 'restore'}} - | - - a.js-delete-list {{_ 'delete'}} + unless isWorker + 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'}} + unless isWorker + 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 = title if currentUser.isBoardMember - p.quiet - a.js-restore-swimlane {{_ 'restore'}} - | - - a.js-delete-swimlane {{_ 'delete'}} + unless isWorker + p.quiet + a.js-restore-swimlane {{_ 'restore'}} + | - + a.js-delete-swimlane {{_ 'delete'}} else li.no-items-message {{_ 'no-archived-swimlanes'}} else diff --git a/client/components/sidebar/sidebarArchives.js b/client/components/sidebar/sidebarArchives.js index a4846561..75b694e9 100644 --- a/client/components/sidebar/sidebarArchives.js +++ b/client/components/sidebar/sidebarArchives.js @@ -139,3 +139,12 @@ BlazeComponent.extendComponent({ ]; }, }).register('archivesSidebar'); + +Template.archivesSidebar.helpers({ + isWorker() { + const currentBoard = Boards.findOne(Session.get('currentBoard')); + return ( + !currentBoard.hasAdmin(this.userId) && currentBoard.hasWorker(this.userId) + ); + }, +}); diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade index 5f929cb9..6d899b70 100644 --- a/client/components/sidebar/sidebarFilters.jade +++ b/client/components/sidebar/sidebarFilters.jade @@ -46,6 +46,24 @@ template(name="filterSidebar") i.fa.fa-check hr ul.sidebar-list + li(class="{{#if Filter.assignees.isSelected undefined}}active{{/if}}") + a.name.js-toggle-assignee-filter + span.sidebar-list-item-description + | {{_ 'filter-no-assignee'}} + if Filter.assignees.isSelected undefined + i.fa.fa-check + each currentBoard.activeMembers + with getUser userId + li(class="{{#if Filter.assignees.isSelected _id}}active{{/if}}") + a.name.js-toggle-assignee-filter + +userAvatar(userId=this._id) + span.sidebar-list-item-description + = profile.fullname + | (<span class="username">{{ username }}</span>) + if Filter.assignees.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 @@ -117,13 +135,14 @@ template(name="multiselectionSidebar") i.fa.fa-check else if someSelectedElementHave 'member' _id i.fa.fa-ellipsis-h - hr - a.sidebar-btn.js-move-selection - i.fa.fa-share - span {{_ 'move-selection'}} - a.sidebar-btn.js-archive-selection - i.fa.fa-archive - span {{_ 'archive-selection'}} + unless currentUser.isWorker + hr + a.sidebar-btn.js-move-selection + i.fa.fa-share + span {{_ 'move-selection'}} + a.sidebar-btn.js-archive-selection + i.fa.fa-archive + span {{_ 'archive-selection'}} template(name="disambiguateMultiLabelPopup") p {{_ 'what-to-do'}} diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js index ee0176b9..0d402ab5 100644 --- a/client/components/sidebar/sidebarFilters.js +++ b/client/components/sidebar/sidebarFilters.js @@ -18,6 +18,11 @@ BlazeComponent.extendComponent({ Filter.members.toggle(this.currentData()._id); Filter.resetExceptions(); }, + 'click .js-toggle-assignee-filter'(evt) { + evt.preventDefault(); + Filter.assignees.toggle(this.currentData()._id); + Filter.resetExceptions(); + }, 'click .js-toggle-archive-filter'(evt) { evt.preventDefault(); Filter.archive.toggle(this.currentData()._id); diff --git a/client/components/swimlanes/swimlaneHeader.js b/client/components/swimlanes/swimlaneHeader.js index fbc45351..3032966d 100644 --- a/client/components/swimlanes/swimlaneHeader.js +++ b/client/components/swimlanes/swimlaneHeader.js @@ -35,12 +35,10 @@ Template.swimlaneHeader.helpers({ currentUser = Meteor.user(); if (currentUser) { return (currentUser.profile || {}).showDesktopDragHandles; + } else if (cookies.has('showDesktopDragHandles')) { + return true; } else { - if (cookies.has('showDesktopDragHandles')) { - return true; - } else { - return false; - } + return false; } }, }); diff --git a/client/components/swimlanes/swimlanes.jade b/client/components/swimlanes/swimlanes.jade index 1dc23c59..9b00d9e8 100644 --- a/client/components/swimlanes/swimlanes.jade +++ b/client/components/swimlanes/swimlanes.jade @@ -43,19 +43,20 @@ template(name="listsGroup") +addListForm template(name="addListForm") - .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'}}" - autocomplete="off" autofocus) - .edit-controls.clearfix - button.primary.confirm(type="submit") {{_ 'save'}} - 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 - | {{_ 'add-list'}} + unless currentUser.isWorker + .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'}}" + autocomplete="off" autofocus) + .edit-controls.clearfix + button.primary.confirm(type="submit") {{_ 'save'}} + 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 + | {{_ 'add-list'}} diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index d072a2a2..753fa88b 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -1,6 +1,6 @@ import { Cookies } from 'meteor/ostrio:cookies'; const cookies = new Cookies(); -const { calculateIndex, enableClickOnTouch } = Utils; +const { calculateIndex } = Utils; function currentListIsInThisSwimlane(swimlaneId) { const currentList = Lists.findOne(Session.get('currentList')); @@ -87,14 +87,12 @@ 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() + !Meteor.user().isCommentOnly() && + !Meteor.user().isWorker() ); } @@ -104,31 +102,29 @@ function initSortable(boardComponent, $listsDom) { if (currentUser) { showDesktopDragHandles = (currentUser.profile || {}) .showDesktopDragHandles; + } else if (cookies.has('showDesktopDragHandles')) { + showDesktopDragHandles = true; } else { - if (cookies.has('showDesktopDragHandles')) { - showDesktopDragHandles = true; - } else { - showDesktopDragHandles = false; - } + showDesktopDragHandles = false; } - if (!Utils.isMiniScreen() && showDesktopDragHandles) { + if (Utils.isMiniScreen() || showDesktopDragHandles) { $listsDom.sortable({ handle: '.js-list-handle', }); - } else { + } else if (!Utils.isMiniScreen() && !showDesktopDragHandles) { $listsDom.sortable({ handle: '.js-list-header', }); } const $listDom = $listsDom; - if ($listDom.data('sortable')) { + if ($listDom.data('uiSortable') || $listDom.data('sortable')) { $listsDom.sortable( 'option', 'disabled', - // Disable drag-dropping when user is not member - !userIsMember(), + // Disable drag-dropping when user is not member/is worker + !userIsMember() || Meteor.user().isWorker(), // Not disable drag-dropping while in multi-selection mode // MultiSelection.isActive() || !userIsMember(), ); @@ -182,17 +178,14 @@ BlazeComponent.extendComponent({ if (currentUser) { showDesktopDragHandles = (currentUser.profile || {}) .showDesktopDragHandles; + } else if (cookies.has('showDesktopDragHandles')) { + showDesktopDragHandles = true; } else { - if (cookies.has('showDesktopDragHandles')) { - showDesktopDragHandles = true; - } else { - showDesktopDragHandles = false; - } + showDesktopDragHandles = false; } const noDragInside = ['a', 'input', 'textarea', 'p'].concat( - Utils.isMiniScreen() || - (!Utils.isMiniScreen() && showDesktopDragHandles) + Utils.isMiniScreen() || showDesktopDragHandles ? ['.js-list-handle', '.js-swimlane-header-handle'] : ['.js-list-header'], ); @@ -276,19 +269,18 @@ Template.swimlane.helpers({ currentUser = Meteor.user(); if (currentUser) { return (currentUser.profile || {}).showDesktopDragHandles; + } else if (cookies.has('showDesktopDragHandles')) { + return true; } else { - if (cookies.has('showDesktopDragHandles')) { - return true; - } else { - return false; - } + return false; } }, canSeeAddList() { return ( Meteor.user() && Meteor.user().isBoardMember() && - !Meteor.user().isCommentOnly() + !Meteor.user().isCommentOnly() && + !Meteor.user().isWorker() ); }, }); diff --git a/client/components/users/userAvatar.jade b/client/components/users/userAvatar.jade index ebfa48ba..7f2067ce 100644 --- a/client/components/users/userAvatar.jade +++ b/client/components/users/userAvatar.jade @@ -73,6 +73,7 @@ template(name="cardMemberPopup") p.quiet @{{ user.username }} ul.pop-over-list if currentUser.isNotCommentOnly + if currentUser.isNotWorker li: a.js-remove-member {{_ 'remove-member-from-card'}} if $eq currentUser._id user._id diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade index 50a80396..d0adf29d 100644 --- a/client/components/users/userHeader.jade +++ b/client/components/users/userHeader.jade @@ -13,21 +13,46 @@ template(name="headerUserBar") template(name="memberMenuPopup") ul.pop-over-list with currentUser - li: a.js-edit-profile {{_ 'edit-profile'}} - li: a.js-change-settings {{_ 'change-settings'}} - li: a.js-change-avatar {{_ 'edit-avatar'}} + li + a.js-edit-profile + i.fa.fa-user + | {{_ 'edit-profile'}} + li + a.js-change-settings + i.fa.fa-cog + | {{_ 'change-settings'}} + li + a.js-change-avatar + i.fa.fa-picture-o + | {{_ 'edit-avatar'}} unless isSandstorm - li: a.js-change-password {{_ 'changePasswordPopup-title'}} - li: a.js-change-language {{_ 'changeLanguagePopup-title'}} + li + a.js-change-password + i.fa.fa-key + | {{_ 'changePasswordPopup-title'}} + li + a.js-change-language + i.fa.fa-flag + | {{_ 'changeLanguagePopup-title'}} if currentUser.isAdmin - li: a.js-go-setting(href="{{pathFor 'setting'}}") {{_ 'admin-panel'}} - hr - ul.pop-over-list - li: a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") {{_ 'templates'}} + li + a.js-go-setting(href="{{pathFor 'setting'}}") + i.fa.fa-lock + | {{_ 'admin-panel'}} + unless currentUser.isWorker + hr + ul.pop-over-list + li + a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") + i.fa.fa-clone + | {{_ 'templates'}} unless isSandstorm hr ul.pop-over-list - li: a.js-logout {{_ 'log-out'}} + li + a.js-logout + i.fa.fa-sign-out + | {{_ 'log-out'}} template(name="editProfilePopup") form @@ -73,23 +98,36 @@ template(name="changeLanguagePopup") template(name="changeSettingsPopup") ul.pop-over-list - li - a.js-toggle-system-messages - | {{_ 'hide-system-messages'}} - if hiddenSystemMessages - i.fa.fa-check + //li + // a.js-toggle-system-messages + // i.fa.fa-comments-o + // | {{_ 'hide-system-messages'}} + // if hiddenSystemMessages + // i.fa.fa-check li a.js-toggle-desktop-drag-handles + i.fa.fa-arrows | {{_ '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") - input.js-apply-show-cards-at.left(type="submit" value="{{_ 'apply'}}") - + unless currentUser.isWorker + li + label.bold.clear + i.fa.fa-sort-numeric-asc + | {{_ 'show-cards-minimum-count'}} + input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="0" max="99" onkeydown="return false") + label.bold.clear + i.fa.fa-calendar + | {{_ 'start-day-of-week'}} + select#start-day-of-week.inline-input.left + each day in weekDays startDayOfWeek + if day.isSelected + option(selected="true", value="#{day.value}") #{day.name} + else + option(value="#{day.value}") #{day.name} + input.js-apply-user-settings.left(type="submit" value="{{_ 'apply'}}") template(name="userDeletePopup") - p {{_ 'delete-user-confirm-popup'}} - button.js-confirm.negate.full(type="submit") {{_ 'delete'}} + unless currentUser.isWorker + p {{_ 'delete-user-confirm-popup'}} + button.js-confirm.negate.full(type="submit") {{_ 'delete'}} diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js index 5f36ef54..b7bb284e 100644 --- a/client/components/users/userHeader.js +++ b/client/components/users/userHeader.js @@ -45,13 +45,31 @@ Template.memberMenuPopup.events({ Template.editProfilePopup.helpers({ allowEmailChange() { - return AccountSettings.findOne('accounts-allowEmailChange').booleanValue; + Meteor.call('AccountSettings.allowEmailChange', (_, result) => { + if (result) { + return true; + } else { + return false; + } + }); }, allowUserNameChange() { - return AccountSettings.findOne('accounts-allowUserNameChange').booleanValue; + Meteor.call('AccountSettings.allowUserNameChange', (_, result) => { + if (result) { + return true; + } else { + return false; + } + }); }, allowUserDelete() { - return AccountSettings.findOne('accounts-allowUserDelete').booleanValue; + Meteor.call('AccountSettings.allowUserDelete', (_, result) => { + if (result) { + return true; + } else { + return false; + } + }); }, }); @@ -148,6 +166,8 @@ Template.changeLanguagePopup.helpers({ name = 'Igbo'; } else if (lang.name === 'oc') { name = 'Occitan'; + } else if (lang.name === '繁体中文(台湾)') { + name = '繁體中文(台灣)'; } return { tag, name }; }).sort(function(a, b) { @@ -204,6 +224,27 @@ Template.changeSettingsPopup.helpers({ return cookies.get('limitToShowCardsCount'); } }, + weekDays(startDay) { + return [ + TAPi18n.__('sunday'), + TAPi18n.__('monday'), + TAPi18n.__('tuesday'), + TAPi18n.__('wednesday'), + TAPi18n.__('thursday'), + TAPi18n.__('friday'), + TAPi18n.__('saturday'), + ].map(function(day, index) { + return { name: day, value: index, isSelected: index === startDay }; + }); + }, + startDayOfWeek() { + currentUser = Meteor.user(); + if (currentUser) { + return currentUser.getStartDayOfWeek(); + } else { + return cookies.get('startDayOfWeek'); + } + }, }); Template.changeSettingsPopup.events({ @@ -227,20 +268,31 @@ Template.changeSettingsPopup.events({ cookies.set('hasHiddenSystemMessages', 'true'); } }, - 'click .js-apply-show-cards-at'(event, templateInstance) { + 'click .js-apply-user-settings'(event, templateInstance) { event.preventDefault(); const minLimit = parseInt( templateInstance.$('#show-cards-count-at').val(), 10, ); + const startDay = parseInt( + templateInstance.$('#start-day-of-week').val(), + 10, + ); + const currentUser = Meteor.user(); if (!isNaN(minLimit)) { - currentUser = Meteor.user(); if (currentUser) { Meteor.call('changeLimitToShowCardsCount', minLimit); } else { cookies.set('limitToShowCardsCount', minLimit); } - Popup.back(); } + if (!isNaN(startDay)) { + if (currentUser) { + Meteor.call('changeStartDayOfWeek', startDay); + } else { + cookies.set('startDayOfWeek', startDay); + } + } + Popup.back(); }, }); diff --git a/client/lib/datepicker.js b/client/lib/datepicker.js index 8ad66c5f..aa05310c 100644 --- a/client/lib/datepicker.js +++ b/client/lib/datepicker.js @@ -10,12 +10,22 @@ DatePicker = BlazeComponent.extendComponent({ this.defaultTime = defaultTime; }, + startDayOfWeek() { + const currentUser = Meteor.user(); + if (currentUser) { + return currentUser.getStartDayOfWeek(); + } else { + return 1; + } + }, + onRendered() { const $picker = this.$('.js-datepicker') .datepicker({ todayHighlight: true, todayBtn: 'linked', language: TAPi18n.getLanguage(), + weekStart: this.startDayOfWeek(), }) .on( 'changeDate', diff --git a/client/lib/filter.js b/client/lib/filter.js index 592eb4ab..24ca320b 100644 --- a/client/lib/filter.js +++ b/client/lib/filter.js @@ -459,13 +459,21 @@ Filter = { // before changing the schema. labelIds: new SetFilter(), members: new SetFilter(), + assignees: new SetFilter(), archive: new SetFilter(), 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'], + _fields: [ + 'labelIds', + 'members', + 'assignees', + 'archive', + 'hideEmpty', + '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 diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js index d3f974be..e861e416 100755 --- a/client/lib/keyboard.js +++ b/client/lib/keyboard.js @@ -1,6 +1,16 @@ // XXX There is no reason to define these shortcuts globally, they should be // attached to a template (most of them will go in the `board` template). +function getHoveredCardId() { + const card = $('.js-minicard:hover').get(0); + if (!card) return null; + return Blaze.getData(card)._id; +} + +function getSelectedCardId() { + return Session.get('selectedCard') || getHoveredCardId(); +} + Mousetrap.bind('?', () => { FlowRouter.go('shortcuts'); }); @@ -50,9 +60,9 @@ Mousetrap.bind(['down', 'up'], (evt, key) => { } }); -// XXX This shortcut should also work when hovering over a card in board view Mousetrap.bind('space', evt => { - if (!Session.get('currentCard')) { + const cardId = getSelectedCardId(); + if (!cardId) { return; } @@ -62,7 +72,7 @@ Mousetrap.bind('space', evt => { } if (Meteor.user().isBoardMember()) { - const card = Cards.findOne(Session.get('currentCard')); + const card = Cards.findOne(cardId); card.toggleMember(currentUserId); // We should prevent scrolling in card when spacebar is clicked // This should do it according to Mousetrap docs, but it doesn't @@ -70,22 +80,46 @@ Mousetrap.bind('space', evt => { } }); +Mousetrap.bind('c', evt => { + const cardId = getSelectedCardId(); + if (!cardId) { + return; + } + + const currentUserId = Meteor.userId(); + if (currentUserId === null) { + return; + } + + if ( + Meteor.user().isBoardMember() && + !Meteor.user().isCommentOnly() && + !Meteor.user().isWorker() + ) { + const card = Cards.findOne(cardId); + card.archive(); + // We should prevent scrolling in card when spacebar is clicked + // This should do it according to Mousetrap docs, but it doesn't + evt.preventDefault(); + } +}); + Template.keyboardShortcuts.helpers({ mapping: [ { - keys: ['W'], + keys: ['w'], action: 'shortcut-toggle-sidebar', }, { - keys: ['Q'], + keys: ['q'], action: 'shortcut-filter-my-cards', }, { - keys: ['F'], + keys: ['f'], action: 'shortcut-toggle-filterbar', }, { - keys: ['X'], + keys: ['x'], action: 'shortcut-clear-filters', }, { @@ -104,5 +138,9 @@ Template.keyboardShortcuts.helpers({ keys: ['SPACE'], action: 'shortcut-assign-self', }, + { + keys: ['c'], + action: 'archive-card', + }, ], }); diff --git a/client/lib/textComplete.js b/client/lib/textComplete.js index 8b6dc1f7..e97d3853 100644 --- a/client/lib/textComplete.js +++ b/client/lib/textComplete.js @@ -48,6 +48,11 @@ $.fn.escapeableTextComplete = function(strategies, options, ...otherArgs) { return this; }; -EscapeActions.register('textcomplete', () => {}, () => dropdownMenuIsOpened, { - noClickEscapeOn: '.textcomplete-dropdown', -}); +EscapeActions.register( + 'textcomplete', + () => {}, + () => dropdownMenuIsOpened, + { + noClickEscapeOn: '.textcomplete-dropdown', + }, +); diff --git a/client/lib/utils.js b/client/lib/utils.js index f4fc170a..c921fddc 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -24,18 +24,14 @@ Utils = { currentUser = Meteor.user(); if (currentUser) { return (currentUser.profile || {}).boardView; + } else if (cookies.get('boardView') === 'board-view-lists') { + return 'board-view-lists'; + } else if (cookies.get('boardView') === 'board-view-swimlanes') { + return 'board-view-swimlanes'; + } else if (cookies.get('boardView') === 'board-view-cal') { + return 'board-view-cal'; } else { - if (cookies.get('boardView') === 'board-view-lists') { - return 'board-view-lists'; - } else if ( - cookies.get('boardView') === 'board-view-swimlanes' - ) { - return 'board-view-swimlanes'; - } else if (cookies.get('boardView') === 'board-view-cal') { - return 'board-view-cal'; - } else { - return false; - } + return false; } }, @@ -43,8 +39,8 @@ Utils = { goBoardId(_id) { const board = Boards.findOne(_id); return ( - board - && FlowRouter.go('board', { + board && + FlowRouter.go('board', { id: board._id, slug: board.slug, }) @@ -55,8 +51,8 @@ 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, @@ -151,8 +147,38 @@ Utils = { // in a small window (even on desktop), Wekan run in compact mode. // we can easily debug with a small window of desktop browser. :-) isMiniScreen() { + // OLD WINDOW WIDTH DETECTION: this.windowResizeDep.depend(); return $(window).width() <= 800; + + // NEW TOUCH DEVICE DETECTION: + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent + + /* + var hasTouchScreen = false; + if ("maxTouchPoints" in navigator) { + hasTouchScreen = navigator.maxTouchPoints > 0; + } else if ("msMaxTouchPoints" in navigator) { + hasTouchScreen = navigator.msMaxTouchPoints > 0; + } else { + var mQ = window.matchMedia && matchMedia("(pointer:coarse)"); + if (mQ && mQ.media === "(pointer:coarse)") { + hasTouchScreen = !!mQ.matches; + } else if ('orientation' in window) { + hasTouchScreen = true; // deprecated, but good fallback + } else { + // Only as a last resort, fall back to user agent sniffing + var UA = navigator.userAgent; + hasTouchScreen = ( + /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) || + /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA) + ); + } + } + */ + //if (hasTouchScreen) + // document.getElementById("exampleButton").style.padding="1em"; + //return false; }, calculateIndexData(prevData, nextData, nItems = 1) { @@ -227,8 +253,8 @@ Utils = { }; if ( - 'ontouchstart' in window - || (window.DocumentTouch && document instanceof window.DocumentTouch) + 'ontouchstart' in window || + (window.DocumentTouch && document instanceof window.DocumentTouch) ) { return true; } @@ -249,8 +275,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), ); }, @@ -267,9 +293,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'); |