diff options
Diffstat (limited to 'client/components')
52 files changed, 1289 insertions, 557 deletions
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade index 8ecbdee8..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}}. - - if($eq activityType 'addedLabel') - | {{{_ 'activity-added-label-card' lastLabel }}}. + //- list activity ------------------------------------------------------- + if($neq mode 'card') + if($eq activity.activityType 'createList') + | {{{_ 'activity-added' listLabel boardLabel}}}. - 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 9697d28c..72af4c35 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,31 +146,32 @@ 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(); const link = attachment.link('original', '/'); // trying to display url before file is stored generates js errors return ( - attachment && - link && - Blaze.toHTML( - HTML.A( - { - href: link, - target: '_blank', - }, - attachment.get('name'), - ), - ) + (attachment && + link && + Blaze.toHTML( + HTML.A( + { + href: link, + 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; }, @@ -180,7 +181,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) { @@ -188,7 +189,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: { @@ -200,4 +201,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/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 e70a9f67..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,7 +202,7 @@ BlazeComponent.extendComponent({ } else { showDesktopDragHandles = false; } - if (!Utils.isMiniScreen() && showDesktopDragHandles) { + if (Utils.isMiniScreen() || showDesktopDragHandles) { $swimlanesDom.sortable({ handle: '.js-swimlane-header-handle', }); @@ -215,9 +212,8 @@ BlazeComponent.extendComponent({ }); } - // Disable drag-dropping if the current user is not a board member or is miniscreen + // Disable drag-dropping if the current user is not a board member $swimlanesDom.sortable('option', 'disabled', !userIsMember()); - $swimlanesDom.sortable('option', 'disabled', Utils.isMiniScreen()); }); function 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 9040ed83..be0146ec 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -33,22 +33,6 @@ Template.boardMenuPopup.events({ 'click .js-card-settings': Popup.open('boardCardSettings'), }); -Template.boardMenuPopup.helpers({ - exportUrl() { - const params = { - boardId: Session.get('currentBoard'), - }; - const queryParams = { - authToken: Accounts._storedLoginToken(), - }; - return FlowRouter.path('/api/boards/:boardId/export', params, queryParams); - }, - exportFilename() { - const boardId = Session.get('currentBoard'); - return `wekan-export-board-${boardId}.json`; - }, -}); - Template.boardChangeTitlePopup.events({ submit(event, templateInstance) { const newTitle = templateInstance @@ -191,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 57e46e39..eda6d118 100644 --- a/client/components/cards/attachments.jade +++ b/client/components/cards/attachments.jade @@ -62,5 +62,5 @@ template(name="attachmentsGalery") unless currentUser.isWorker //li.attachment-item.add-attachment a.js-add-attachment - i.fa.fa-paperclip + i.fa.fa-plus | {{_ 'add-attachment' }} diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index 615ae1d5..ae97e0e9 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -32,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}} @@ -199,10 +199,29 @@ template(name="cardDetails") +viewer = getAssignedBy + if getVoteQuestion + hr + .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'}} + //- 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'}} @@ -229,6 +248,7 @@ template(name="cardDetails") a.js-close-inlined-form {{_ 'discard'}} else if getDescription if currentBoard.allowsDescriptionTitle + hr h3.card-details-item-title {{_ 'description'}} if currentBoard.allowsDescriptionText +viewer @@ -237,15 +257,16 @@ template(name="cardDetails") .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'}} + hr + h3 + i.fa.fa-paperclip + | {{_ 'attachments'}} .card-checklist-attachmentGalery.card-attachmentGalery +attachmentsGalery @@ -312,6 +333,16 @@ template(name="cardDetailsActionsPopup") //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 @@ -535,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 5fdc5579..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()); @@ -200,9 +231,6 @@ BlazeComponent.extendComponent({ }, }); - // ugly touch event hotfix - enableClickOnTouch('.card-checklist-items .js-checklist'); - const $subtasksDom = this.$('.card-subtasks-items'); $subtasksDom.sortable({ @@ -238,26 +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()); - } - if ($subtasksDom.data('sortable')) { - $subtasksDom.sortable('option', 'disabled', !userIsMember()); - } - if ($checklistsDom.data('sortable')) { - $checklistsDom.sortable('option', 'disabled', Utils.isMiniScreen()); + 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', Utils.isMiniScreen()); + if ($subtasksDom.data('uiSortable') || $subtasksDom.data('sortable')) { + $subtasksDom.sortable('option', 'disabled', disabled); } }); }, @@ -347,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'), @@ -356,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. @@ -379,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); + }, }, ]; }, @@ -560,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'), @@ -570,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( @@ -672,7 +718,7 @@ BlazeComponent.extendComponent({ _id: { $ne: Meteor.user().getTemplatesBoardId() }, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; @@ -848,7 +894,7 @@ BlazeComponent.extendComponent({ }, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; @@ -945,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 80fa87c0..3e2beadd 100644 --- a/client/components/cards/cardDetails.styl +++ b/client/components/cards/cardDetails.styl @@ -94,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 @@ -241,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 @@ -330,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 391769e9..1b1e088a 100644 --- a/client/components/cards/checklists.jade +++ b/client/components/cards/checklists.jade @@ -88,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 c88fdd82..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,14 +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('sortable')) { - $(self.itemsDom).sortable('option', 'disabled', Utils.isMiniScreen()); + if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) { + $(self.itemsDom).sortable( + 'option', + 'disabled', + !userIsMember() || Utils.isMiniScreen(), + ); } }); }, @@ -177,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) { @@ -196,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, }, ]; @@ -250,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 6a073424..e8efc6ac 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -4,8 +4,8 @@ template(name="minicard") class="{{#if isLinkedBoard}}linked-board{{/if}}" class="minicard-{{colorClass}}") if isMiniScreen - //.handle - // .fa.fa-arrows + .handle + .fa.fa-arrows unless isMiniScreen if showDesktopDragHandles .handle @@ -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.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.js b/client/components/cards/subtasks.js index 34348fe1..4cd15c11 100644 --- a/client/components/cards/subtasks.js +++ b/client/components/cards/subtasks.js @@ -20,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({ 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 8574caf7..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,7 +126,7 @@ BlazeComponent.extendComponent({ showDesktopDragHandles = false; } - if (!Utils.isMiniScreen() && showDesktopDragHandles) { + if (Utils.isMiniScreen() || showDesktopDragHandles) { $cards.sortable({ handle: '.handle', }); @@ -139,27 +136,16 @@ BlazeComponent.extendComponent({ }); } - if ($cards.data('sortable')) { + if ($cards.data('uiSortable') || $cards.data('sortable')) { $cards.sortable( 'option', 'disabled', - // Disable drag-dropping when user is not member/is miniscreen + // Disable drag-dropping when user is not member !userIsMember(), // Not disable drag-dropping while in multi-selection mode // MultiSelection.isActive() || !userIsMember(), ); } - - if ($cards.data('sortable')) { - $cards.sortable( - 'option', - 'disabled', - // Disable drag-dropping when user is not member/is miniscreen - Utils.isMiniScreen(), - // 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. 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 89c27ec7..88f88db0 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -411,7 +411,7 @@ BlazeComponent.extendComponent({ type: 'board', }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; @@ -597,7 +597,7 @@ BlazeComponent.extendComponent({ type: 'board', }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; @@ -743,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 182fee9e..fa1faf34 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -30,10 +30,9 @@ 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 + a.list-header-handle.handle.fa.fa-arrows.js-list-handle else if currentUser.isBoardMember if isWatching i.list-header-watch-icon.fa.fa-eye diff --git a/client/components/main/editor.js b/client/components/main/editor.js index bc2e0bad..de598da8 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) { @@ -287,6 +212,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 @@ -329,6 +260,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 @@ -350,7 +283,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) { @@ -366,7 +299,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 @@ -374,16 +312,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 @@ -395,10 +334,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 f77c2aca..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 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.js b/client/components/rules/actions/boardActions.js index c2f2375a..02910cc1 100644 --- a/client/components/rules/actions/boardActions.js +++ b/client/components/rules/actions/boardActions.js @@ -11,7 +11,7 @@ BlazeComponent.extendComponent({ }, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; diff --git a/client/components/settings/peopleBody.jade b/client/components/settings/peopleBody.jade index b8a94337..fef1067e 100644 --- a/client/components/settings/peopleBody.jade +++ b/client/components/settings/peopleBody.jade @@ -40,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 @@ -104,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 @@ -148,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/settingBody.js b/client/components/settings/settingBody.js index 319c066b..62752084 100644 --- a/client/components/settings/settingBody.js +++ b/client/components/settings/settingBody.js @@ -48,7 +48,7 @@ BlazeComponent.extendComponent({ 'members.isAdmin': true, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); }, diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index ebcd8486..6bfedc9c 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -245,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 @@ -257,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") @@ -267,7 +267,14 @@ template(name="outgoingWebhooksPopup") template(name="boardMenuPopup") ul.pop-over-list - li: a.js-custom-fields {{_ 'custom-fields'}} + 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 @@ -291,10 +298,11 @@ template(name="boardMenuPopup") if currentUser.isBoardAdmin hr ul.pop-over-list - li - a(href="{{exportUrl}}", download="{{exportFilename}}") - i.fa.fa-share-alt - | {{_ 'export-board'}} + if withApi + li + a(href="{{exportUrl}}", download="{{exportFilename}}") + i.fa.fa-share-alt + | {{_ 'export-board'}} li a.js-outgoing-webhooks i.fa.fa-globe @@ -319,11 +327,12 @@ template(name="boardMenuPopup") if isSandstorm hr ul.pop-over-list - li - a(href="{{exportUrl}}", download="{{exportFilename}}") - i.fa.fa-share-alt - i.fa.fa-sign-out - | {{_ 'export-board'}} + 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 diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 8e640564..cbe00797 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -182,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(); @@ -211,7 +215,17 @@ Template.boardMenuPopup.events({ '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'), @@ -495,7 +509,7 @@ BlazeComponent.extendComponent({ 'members.userId': Meteor.userId(), }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); }, @@ -673,7 +687,7 @@ BlazeComponent.extendComponent({ 'members.userId': Meteor.userId(), }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); }, diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade index 7f31dada..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 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/swimlanes.js b/client/components/swimlanes/swimlanes.js index b7a55ce6..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,9 +87,6 @@ function initSortable(boardComponent, $listsDom) { }, }); - // ugly touch event hotfix - enableClickOnTouch('.js-list:not(.js-list-composer)'); - function userIsMember() { return ( Meteor.user() && @@ -111,7 +108,7 @@ function initSortable(boardComponent, $listsDom) { showDesktopDragHandles = false; } - if (!Utils.isMiniScreen() && showDesktopDragHandles) { + if (Utils.isMiniScreen() || showDesktopDragHandles) { $listsDom.sortable({ handle: '.js-list-handle', }); @@ -122,34 +119,12 @@ function initSortable(boardComponent, $listsDom) { } const $listDom = $listsDom; - if ($listDom.data('sortable')) { - $listsDom.sortable( - 'option', - 'disabled', - // Disable drag-dropping when user is not member/is worker/is miniscreen - !userIsMember(), - // Not disable drag-dropping while in multi-selection mode - // MultiSelection.isActive() || !userIsMember(), - ); - } - - if ($listDom.data('sortable')) { - $listsDom.sortable( - 'option', - 'disabled', - // Disable drag-dropping when user is not member/is worker/is miniscreen - Meteor.user().isWorker(), - // Not disable drag-dropping while in multi-selection mode - // MultiSelection.isActive() || !userIsMember(), - ); - } - - if ($listDom.data('sortable')) { + if ($listDom.data('uiSortable') || $listDom.data('sortable')) { $listsDom.sortable( 'option', 'disabled', - // Disable drag-dropping when user is not member/is worker/is miniscreen - Utils.isMiniScreen(), + // 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(), ); @@ -210,8 +185,7 @@ BlazeComponent.extendComponent({ } 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'], ); diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade index 9306d21d..d0adf29d 100644 --- a/client/components/users/userHeader.jade +++ b/client/components/users/userHeader.jade @@ -98,12 +98,12 @@ template(name="changeLanguagePopup") template(name="changeSettingsPopup") ul.pop-over-list - li - a.js-toggle-system-messages - i.fa.fa-comments-o - | {{_ '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 @@ -112,11 +112,20 @@ template(name="changeSettingsPopup") i.fa.fa-check unless currentUser.isWorker li - label.bold + 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") - input.js-apply-show-cards-at.left(type="submit" value="{{_ 'apply'}}") + 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") unless currentUser.isWorker diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js index 847d30fb..b7bb284e 100644 --- a/client/components/users/userHeader.js +++ b/client/components/users/userHeader.js @@ -166,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) { @@ -222,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({ @@ -245,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(); }, }); |