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