diff options
Diffstat (limited to 'client/components/cards')
-rw-r--r-- | client/components/cards/attachments.jade | 6 | ||||
-rw-r--r-- | client/components/cards/attachments.js | 18 | ||||
-rw-r--r-- | client/components/cards/attachments.styl | 21 | ||||
-rw-r--r-- | client/components/cards/cardDate.js | 227 | ||||
-rw-r--r-- | client/components/cards/cardDate.styl | 24 | ||||
-rw-r--r-- | client/components/cards/cardDetails.jade | 103 | ||||
-rw-r--r-- | client/components/cards/cardDetails.js | 221 | ||||
-rw-r--r-- | client/components/cards/cardDetails.styl | 9 | ||||
-rw-r--r-- | client/components/cards/cardTime.jade | 22 | ||||
-rw-r--r-- | client/components/cards/cardTime.js | 81 | ||||
-rw-r--r-- | client/components/cards/cardTime.styl | 17 | ||||
-rw-r--r-- | client/components/cards/checklists.jade | 64 | ||||
-rw-r--r-- | client/components/cards/checklists.js | 184 | ||||
-rw-r--r-- | client/components/cards/checklists.styl | 128 | ||||
-rw-r--r-- | client/components/cards/labels.jade | 3 | ||||
-rw-r--r-- | client/components/cards/minicard.jade | 16 | ||||
-rw-r--r-- | client/components/cards/minicard.styl | 3 |
17 files changed, 971 insertions, 176 deletions
diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade index e35b364a..0f79323b 100644 --- a/client/components/cards/attachments.jade +++ b/client/components/cards/attachments.jade @@ -21,11 +21,11 @@ template(name="attachmentDeletePopup") template(name="attachmentsGalery") .attachments-galery each attachments - a.attachment-item.js-open-viewer(title="{{_ 'added'}} {{ moment uploadedAt }}") - .attachment-thumbnail + .attachment-item + a.attachment-thumbnail.swipebox(href="{{url}}" title="{{name}}") if isUploaded if isImage - img.attachment-thumbnail-img.js-preview-image(src="{{url}}") + img.attachment-thumbnail-img(src="{{url}}") else span.attachment-thumbnail-ext= extension else diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index 95cb9f55..bc7d3979 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -11,9 +11,6 @@ Template.attachmentsGalery.events({ 'click .js-download'(event) { event.stopPropagation(); }, - 'click .js-open-viewer'() { - // XXX Not implemented! - }, 'click .js-add-cover'() { Cards.findOne(this.cardId).setCover(this._id); }, @@ -63,7 +60,13 @@ Template.cardAttachmentsPopup.events({ file.boardId = card.boardId; file.cardId = card._id; file.userId = Meteor.userId(); - Attachments.insert(file); + + const attachment = Attachments.insert(file); + + if (attachment && attachment._id && attachment.isImage()) { + card.setCover(attachment._id); + } + Popup.close(); }); }, @@ -110,7 +113,12 @@ Template.previewClipboardImagePopup.events({ file.boardId = card.boardId; file.cardId = card._id; file.userId = Meteor.userId(); - Attachments.insert(file); + const attachment = Attachments.insert(file); + + if (attachment && attachment._id && attachment.isImage()) { + card.setCover(attachment._id); + } + pastedResults = null; $(document.body).pasteImageReader(() => {}); Popup.close(); diff --git a/client/components/cards/attachments.styl b/client/components/cards/attachments.styl index 9a5d0645..4a22fd8a 100644 --- a/client/components/cards/attachments.styl +++ b/client/components/cards/attachments.styl @@ -58,7 +58,28 @@ .preview-clipboard-image width: 280px + max-width: 100%; height: 200px display: block border: 1px solid black box-shadow: 0 1px 2px rgba(0,0,0,.2) + +@media screen and (max-width: 800px) + .attachments-galery + flex-direction + row + .attachment-item + width: 50% - 2% + + .attachment-thumbnail + height: 130px + .attachment-details + font-size: 1.1em + +@media screen and (max-width: 360px) + .attachments-galery + .attachment-item + width: 100% + + .attachment-thumbnail + height: 200px diff --git a/client/components/cards/cardDate.js b/client/components/cards/cardDate.js index 09a6761b..f33e8c19 100644 --- a/client/components/cards/cardDate.js +++ b/client/components/cards/cardDate.js @@ -1,10 +1,114 @@ -// Edit start & due dates +// Edit received, start, due & end dates +const EditCardDate = BlazeComponent.extendComponent({ + template() { + return 'editCardDate'; + }, + + onCreated() { + this.error = new ReactiveVar(''); + this.card = this.data(); + this.date = new ReactiveVar(moment.invalid()); + }, + + onRendered() { + const $picker = this.$('.js-datepicker').datepicker({ + todayHighlight: true, + todayBtn: 'linked', + language: TAPi18n.getLanguage(), + }).on('changeDate', function(evt) { + this.find('#date').value = moment(evt.date).format('L'); + this.error.set(''); + this.find('#time').focus(); + }.bind(this)); + + if (this.date.get().isValid()) { + $picker.datepicker('update', this.date.get().toDate()); + } + }, + + showDate() { + if (this.date.get().isValid()) + return this.date.get().format('L'); + return ''; + }, + showTime() { + if (this.date.get().isValid()) + return this.date.get().format('LT'); + return ''; + }, + dateFormat() { + return moment.localeData().longDateFormat('L'); + }, + timeFormat() { + return moment.localeData().longDateFormat('LT'); + }, + + events() { + return [{ + 'keyup .js-date-field'() { + // parse for localized date format in strict mode + const dateMoment = moment(this.find('#date').value, 'L', true); + if (dateMoment.isValid()) { + this.error.set(''); + this.$('.js-datepicker').datepicker('update', dateMoment.toDate()); + } + }, + 'keyup .js-time-field'() { + // parse for localized time format in strict mode + const dateMoment = moment(this.find('#time').value, 'LT', true); + if (dateMoment.isValid()) { + this.error.set(''); + } + }, + 'submit .edit-date'(evt) { + evt.preventDefault(); + + // if no time was given, init with 12:00 + const time = evt.target.time.value || moment(new Date().setHours(12, 0, 0)).format('LT'); + + const dateString = `${evt.target.date.value} ${time}`; + const newDate = moment(dateString, 'L LT', true); + if (newDate.isValid()) { + this._storeDate(newDate.toDate()); + Popup.close(); + } + else { + this.error.set('invalid-date'); + evt.target.date.focus(); + } + }, + 'click .js-delete-date'(evt) { + evt.preventDefault(); + this._deleteDate(); + Popup.close(); + }, + }]; + }, +}); + Template.dateBadge.helpers({ canModifyCard() { return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly(); }, }); +// editCardReceivedDatePopup +(class extends EditCardDate { + onCreated() { + super.onCreated(); + this.data().receivedAt && this.date.set(moment(this.data().receivedAt)); + } + + _storeDate(date) { + this.card.setReceived(date); + } + + _deleteDate() { + this.card.unsetReceived(); + } +}).register('editCardReceivedDatePopup'); + + // editCardStartDatePopup (class extends DatePicker { onCreated() { @@ -12,6 +116,13 @@ Template.dateBadge.helpers({ this.data().startAt && this.date.set(moment(this.data().startAt)); } + onRendered() { + super.onRendered(); + if (moment.isDate(this.card.receivedAt)) { + this.$('.js-datepicker').datepicker('setStartDate', this.card.receivedAt); + } + } + _storeDate(date) { this.card.setStart(date); } @@ -44,8 +155,31 @@ Template.dateBadge.helpers({ } }).register('editCardDueDatePopup'); +// editCardEndDatePopup +(class extends EditCardDate { + onCreated() { + super.onCreated(); + this.data().endAt && this.date.set(moment(this.data().endAt)); + } + + onRendered() { + super.onRendered(); + if (moment.isDate(this.card.startAt)) { + this.$('.js-datepicker').datepicker('setStartDate', this.card.startAt); + } + } + + _storeDate(date) { + this.card.setEnd(date); + } + + _deleteDate() { + this.card.unsetEnd(); + } +}).register('editCardEndDatePopup'); -// Display start & due dates + +// Display received, start, due & end dates const CardDate = BlazeComponent.extendComponent({ template() { return 'dateBadge'; @@ -74,6 +208,36 @@ const CardDate = BlazeComponent.extendComponent({ }, }); +class CardReceivedDate extends CardDate { + onCreated() { + super.onCreated(); + const self = this; + self.autorun(() => { + self.date.set(moment(self.data().receivedAt)); + }); + } + + classes() { + let classes = 'received-date' + ' '; + if (this.date.get().isBefore(this.now.get(), 'minute') && + this.now.get().isBefore(this.data().dueAt)) { + classes += 'current'; + } + return classes; + } + + showTitle() { + return `${TAPi18n.__('card-received-on')} ${this.date.get().format('LLLL')}`; + } + + events() { + return super.events().concat({ + 'click .js-edit-date': Popup.open('editCardReceivedDate'), + }); + } +} +CardReceivedDate.register('cardReceivedDate'); + class CardStartDate extends CardDate { onCreated() { super.onCreated(); @@ -84,11 +248,12 @@ class CardStartDate extends CardDate { } classes() { + let classes = 'start-date' + ' '; if (this.date.get().isBefore(this.now.get(), 'minute') && this.now.get().isBefore(this.data().dueAt)) { - return 'current'; + classes += 'current'; } - return ''; + return classes; } showTitle() { @@ -113,13 +278,14 @@ class CardDueDate extends CardDate { } classes() { + let classes = 'due-date' + ' '; if (this.now.get().diff(this.date.get(), 'days') >= 2) - return 'long-overdue'; + classes += 'long-overdue'; else if (this.now.get().diff(this.date.get(), 'minute') >= 0) - return 'due'; + classes += 'due'; else if (this.now.get().diff(this.date.get(), 'days') >= -1) - return 'almost-due'; - return ''; + classes += 'almost-due'; + return classes; } showTitle() { @@ -134,6 +300,44 @@ class CardDueDate extends CardDate { } CardDueDate.register('cardDueDate'); +class CardEndDate extends CardDate { + onCreated() { + super.onCreated(); + const self = this; + self.autorun(() => { + self.date.set(moment(self.data().endAt)); + }); + } + + classes() { + let classes = 'end-date' + ' '; + if (this.data.dueAt.diff(this.date.get(), 'days') >= 2) + classes += 'long-overdue'; + else if (this.data.dueAt.diff(this.date.get(), 'days') >= 0) + classes += 'due'; + else if (this.data.dueAt.diff(this.date.get(), 'days') >= -2) + classes += 'almost-due'; + return classes; + } + + showTitle() { + return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`; + } + + events() { + return super.events().concat({ + 'click .js-edit-date': Popup.open('editCardEndDate'), + }); + } +} +CardEndDate.register('cardEndDate'); + +(class extends CardReceivedDate { + showDate() { + return this.date.get().format('l'); + } +}).register('minicardReceivedDate'); + (class extends CardStartDate { showDate() { return this.date.get().format('l'); @@ -145,3 +349,10 @@ CardDueDate.register('cardDueDate'); return this.date.get().format('l'); } }).register('minicardDueDate'); + +(class extends CardEndDate { + showDate() { + return this.date.get().format('l'); + } +}).register('minicardEndDate'); + diff --git a/client/components/cards/cardDate.styl b/client/components/cards/cardDate.styl index 87a3ed25..9775e82b 100644 --- a/client/components/cards/cardDate.styl +++ b/client/components/cards/cardDate.styl @@ -30,10 +30,30 @@ &:hover, &.is-active background-color: darken(#fd5d47, 7) + &.end-date + time + &::before + content: "\f253" // symbol: fa-hourglass-end + + &.due-date + time + &::before + content: "\f090" // symbol: fa-sign-in + + &.start-date + time + &::before + content: "\f08b" // symbol: fa-sign-out + + &.received-date + time + &::before + content: "\f251" // symbol: fa-hourglass-start + time &::before font: normal normal normal 14px/1 FontAwesome font-size: inherit -webkit-font-smoothing: antialiased - content: "\f017" // clock symbol - margin-right: 0.3em
\ No newline at end of file + margin-right: 0.3em + diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index f72abe6d..b888210b 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -9,14 +9,44 @@ template(name="cardDetails") a.fa.fa-navicon.card-details-menu.js-open-card-details-menu h2.card-details-title.js-card-title( class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}") - = title - if isWatching - i.fa.fa-eye.card-details-watch + +viewer + = title + if isWatching + i.fa.fa-eye.card-details-watch if archived p.warning {{_ 'card-archived'}} .card-details-items + .card-details-item.card-details-item-received + h3.card-details-item-title {{_ 'card-received'}} + if receivedAt + +cardReceivedDate + else + a.js-received-date {{_ 'add'}} + + .card-details-item.card-details-item-start + h3.card-details-item-title {{_ 'card-start'}} + if startAt + +cardStartDate + else + a.js-start-date {{_ 'add'}} + + .card-details-item.card-details-item-due + h3.card-details-item-title {{_ 'card-due'}} + if dueAt + +cardDueDate + else + a.js-due-date {{_ 'add'}} + + .card-details-item.card-details-item-end + h3.card-details-item-title {{_ 'card-end'}} + if endAt + +cardEndDate + else + a.js-end-date {{_ 'add'}} + + .card-details-items .card-details-item.card-details-item-members h3.card-details-item-title {{_ 'members'}} each members @@ -51,6 +81,15 @@ template(name="cardDetails") = definition.name +cardCustomField + .card-details-items + if spentTime + .card-details-item.card-details-item-spent + if isOvertime + h3.card-details-item-title {{_ 'overtime-hours'}} + else + h3.card-details-item-title {{_ 'spent-time-hours'}} + +cardSpentTime + //- XXX We should use "editable" to avoid repetiting ourselves if canModifyCard h3.card-details-item-title {{_ 'description'}} @@ -81,16 +120,24 @@ template(name="cardDetails") hr +checklists(cardId = _id) - if attachments.count - hr - h2 - i.fa.fa-paperclip - | {{_ 'attachments'}} + hr + h3 + i.fa.fa-paperclip + | {{_ 'attachments'}} - +attachmentsGalery + +attachmentsGalery hr - h2 {{ _ 'activity'}} + .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 @@ -112,9 +159,12 @@ template(name="cardDetailsActionsPopup") 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-custom-fields {{_ 'card-edit-custom-fields'}} 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'}} hr ul.pop-over-list li: a.js-move-card-to-top {{_ 'moveCardToTop-title'}} @@ -123,19 +173,48 @@ template(name="cardDetailsActionsPopup") ul.pop-over-list li: a.js-move-card {{_ 'moveCardPopup-title'}} li: a.js-copy-card {{_ 'copyCardPopup-title'}} + li: a.js-copy-checklist-cards {{_ 'copyChecklistToManyCardsPopup-title'}} unless archived li: a.js-archive {{_ 'archive-card'}} li: a.js-more {{_ 'cardMorePopup-title'}} template(name="moveCardPopup") - +boardLists + +boardsAndLists template(name="copyCardPopup") label(for='copy-card-title') {{_ 'title'}}: textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus) = title + +boardsAndLists + + +template(name="copyChecklistToManyCardsPopup") + label(for='copy-checklist-cards-title') {{_ 'copyChecklistToManyCardsPopup-instructions'}}: + textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus) + | {{_ 'copyChecklistToManyCardsPopup-format'}} + +boardsAndLists + +template(name="boardsAndLists") + label {{_ 'boards'}}: + select.js-select-boards + each boards + if $eq _id currentBoard._id + option(value="{{_id}}" selected) {{_ 'current'}} + else + option(value="{{_id}}") {{title}} + + label {{_ 'swimlanes'}}: + select.js-select-swimlanes + each swimlanes + option(value="{{_id}}") {{title}} + label {{_ 'lists'}}: - +boardLists + select.js-select-lists + each aBoardLists + option(value="{{_id}}") {{title}} + + .edit-controls.clearfix + button.primary.confirm.js-done {{_ 'done'}} template(name="cardMembersPopup") ul.pop-over-list.js-card-member-list diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 8d5c478d..26549fda 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -1,3 +1,6 @@ +const subManager = new SubsManager(); +const { calculateIndexData } = Utils; + BlazeComponent.extendComponent({ mixins() { return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar]; @@ -18,9 +21,11 @@ BlazeComponent.extendComponent({ onCreated() { this.isLoaded = new ReactiveVar(false); - this.parentComponent().showOverlay.set(true); - this.parentComponent().mouseHasEnterCardDetails = false; + this.parentComponent().parentComponent().showOverlay.set(true); + this.parentComponent().parentComponent().mouseHasEnterCardDetails = false; this.calculateNextPeak(); + + Meteor.subscribe('unsaved-edits'); }, isWatching() { @@ -28,16 +33,20 @@ BlazeComponent.extendComponent({ return card.findWatcher(Meteor.userId()); }, + hiddenSystemMessages() { + return Meteor.user().hasHiddenSystemMessages(); + }, + canModifyCard() { return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly(); }, scrollParentContainer() { const cardPanelWidth = 510; - const bodyBoardComponent = this.parentComponent(); + const bodyBoardComponent = this.parentComponent().parentComponent(); - const $cardContainer = bodyBoardComponent.$('.js-lists'); const $cardView = this.$(this.firstNode()); + const $cardContainer = bodyBoardComponent.$('.js-swimlanes'); const cardContainerScroll = $cardContainer.scrollLeft(); const cardContainerWidth = $cardContainer.width(); @@ -58,10 +67,55 @@ BlazeComponent.extendComponent({ onRendered() { if (!Utils.isMiniScreen()) this.scrollParentContainer(); + const $checklistsDom = this.$('.card-checklist-items'); + + $checklistsDom.sortable({ + tolerance: 'pointer', + helper: 'clone', + handle: '.checklist-title', + items: '.js-checklist', + placeholder: 'checklist 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-checklist').get(0); + if (prevChecklist) { + prevChecklist = Blaze.getData(prevChecklist).checklist; + } + let nextChecklist = ui.item.next('.js-checklist').get(0); + if (nextChecklist) { + nextChecklist = Blaze.getData(nextChecklist).checklist; + } + const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1); + + $checklistsDom.sortable('cancel'); + const checklist = Blaze.getData(ui.item.get(0)).checklist; + + Checklists.update(checklist._id, { + $set: { + sort: sortIndex.base, + }, + }); + }, + }); + + function userIsMember() { + return Meteor.user() && Meteor.user().isBoardMember(); + } + + // Disable sorting if the current user is not a board member + this.autorun(() => { + if ($checklistsDom.data('sortable')) { + $checklistsDom.sortable('option', 'disabled', !userIsMember()); + } + }); }, onDestroyed() { - this.parentComponent().showOverlay.set(false); + this.parentComponent().parentComponent().showOverlay.set(false); }, events() { @@ -95,9 +149,16 @@ BlazeComponent.extendComponent({ 'click .js-member': Popup.open('cardMember'), 'click .js-add-members': Popup.open('cardMembers'), 'click .js-add-labels': Popup.open('cardLabels'), + 'click .js-received-date': Popup.open('editCardReceivedDate'), + 'click .js-start-date': Popup.open('editCardStartDate'), + 'click .js-due-date': Popup.open('editCardDueDate'), + 'click .js-end-date': Popup.open('editCardEndDate'), 'mouseenter .js-card-details' () { - this.parentComponent().showOverlay.set(true); - this.parentComponent().mouseHasEnterCardDetails = true; + this.parentComponent().parentComponent().showOverlay.set(true); + this.parentComponent().parentComponent().mouseHasEnterCardDetails = true; + }, + 'click #toggleButton'() { + Meteor.call('toggleSystemMessages'); }, }]; }, @@ -154,20 +215,24 @@ Template.cardDetailsActionsPopup.events({ 'click .js-members': Popup.open('cardMembers'), 'click .js-labels': Popup.open('cardLabels'), 'click .js-attachments': Popup.open('cardAttachments'), + 'click .js-received-date': Popup.open('editCardReceivedDate'), 'click .js-custom-fields': Popup.open('cardCustomFields'), 'click .js-start-date': Popup.open('editCardStartDate'), 'click .js-due-date': Popup.open('editCardDueDate'), + 'click .js-end-date': Popup.open('editCardEndDate'), + 'click .js-spent-time': Popup.open('editCardSpentTime'), 'click .js-move-card': Popup.open('moveCard'), 'click .js-copy-card': Popup.open('copyCard'), + 'click .js-copy-checklist-cards': Popup.open('copyChecklistToManyCards'), 'click .js-move-card-to-top' (evt) { evt.preventDefault(); - const minOrder = _.min(this.list().cards().map((c) => c.sort)); - this.move(this.listId, minOrder - 1); + const minOrder = _.min(this.list().cards(this.swimlaneId).map((c) => c.sort)); + this.move(this.swimlaneId, this.listId, minOrder - 1); }, 'click .js-move-card-to-bottom' (evt) { evt.preventDefault(); - const maxOrder = _.max(this.list().cards().map((c) => c.sort)); - this.move(this.listId, maxOrder + 1); + const maxOrder = _.max(this.list().cards(this.swimlaneId).map((c) => c.sort)); + this.move(this.swimlaneId, this.listId, maxOrder + 1); }, 'click .js-archive' (evt) { evt.preventDefault(); @@ -191,36 +256,82 @@ Template.editCardTitleForm.onRendered(function () { Template.editCardTitleForm.events({ 'keydown .js-edit-card-title' (evt) { // If enter key was pressed, submit the data - if (evt.keyCode === 13) { + // Unless the shift key is also being pressed + if (evt.keyCode === 13 && !evt.shiftKey) { $('.js-submit-edit-card-title-form').click(); } }, }); Template.moveCardPopup.events({ - 'click .js-select-list' () { + '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 newListId = this._id; - card.move(newListId); + const lSelect = $('.js-select-lists')[0]; + const newListId = lSelect.options[lSelect.selectedIndex].value; + const slSelect = $('.js-select-swimlanes')[0]; + card.swimlaneId = slSelect.options[slSelect.selectedIndex].value; + card.move(card.swimlaneId, newListId, 0); Popup.close(); }, }); +BlazeComponent.extendComponent({ + onCreated() { + subManager.subscribe('board', Session.get('currentBoard')); + this.selectedBoardId = new ReactiveVar(Session.get('currentBoard')); + }, + + boards() { + const boards = Boards.find({ + archived: false, + 'members.userId': Meteor.userId(), + }, { + sort: ['title'], + }); + return boards; + }, + + swimlanes() { + const board = Boards.findOne(this.selectedBoardId.get()); + return board.swimlanes(); + }, + + aBoardLists() { + const board = Boards.findOne(this.selectedBoardId.get()); + return board.lists(); + }, + + events() { + return [{ + 'change .js-select-boards'(evt) { + this.selectedBoardId.set($(evt.currentTarget).val()); + subManager.subscribe('board', this.selectedBoardId.get()); + }, + }]; + }, +}).register('boardsAndLists'); + Template.copyCardPopup.events({ - 'click .js-select-list' (evt) { + 'click .js-done'() { const card = Cards.findOne(Session.get('currentCard')); const oldId = card._id; card._id = null; - card.listId = this._id; - const textarea = $(evt.currentTarget).parents('.content').find('textarea'); + const lSelect = $('.js-select-lists')[0]; + card.listId = lSelect.options[lSelect.selectedIndex].value; + const slSelect = $('.js-select-swimlanes')[0]; + card.swimlaneId = slSelect.options[slSelect.selectedIndex].value; + const bSelect = $('.js-select-boards')[0]; + card.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 - card.sort = Lists.findOne(this._id).cards().count(); + card.sort = Lists.findOne(card.listId).cards().count(); if (title) { card.title = title; + card.coverId = ''; const _id = Cards.insert(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 @@ -233,9 +344,16 @@ Template.copyCardPopup.events({ cursor.forEach(function() { 'use strict'; const checklist = arguments[0]; + const checklistId = checklist._id; checklist.cardId = _id; checklist._id = null; - Checklists.insert(checklist); + 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 @@ -252,6 +370,69 @@ Template.copyCardPopup.events({ }, }); +Template.copyChecklistToManyCardsPopup.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; + const slSelect = $('.js-select-swimlanes')[0]; + card.swimlaneId = slSelect.options[slSelect.selectedIndex].value; + const bSelect = $('.js-select-boards')[0]; + card.boardId = bSelect.options[bSelect.selectedIndex].value; + const textarea = $('#copy-card-title'); + const titleEntry = textarea.val().trim(); + // insert new card to the bottom of new list + card.sort = Lists.findOne(card.listId).cards().count(); + + if (titleEntry) { + const titleList = JSON.parse(titleEntry); + for (let i = 0; i < titleList.length; i++){ + const obj = titleList[i]; + card.title = obj.title; + card.description = obj.description; + card.coverId = ''; + const _id = Cards.insert(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); + + // 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(); + } + }, +}); + + Template.cardMorePopup.events({ 'click .js-copy-card-link-to-clipboard' () { // Clipboard code from: diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl index c981e2a2..e18c07a1 100644 --- a/client/components/cards/cardDetails.styl +++ b/client/components/cards/cardDetails.styl @@ -2,7 +2,6 @@ .card-details padding: 0 20px - height: 100% flex-shrink: 0 flex-basis: 470px will-change: flex-basis @@ -79,15 +78,19 @@ margin-right: 0 &.card-details-item-labels, &.card-details-item-members, + &.card-details-item-received, &.card-details-item-start, &.card-details-item-due, + &.card-details-item-end + width: 50% + flex-shrink: 1 &.card-details-item-customfield max-width: 50% flex-grow: 1 .card-details-item-title - font-size: 14px - color: darken(white, 45%) + font-size: 16px + color: #000 .card-label padding-top: 5px diff --git a/client/components/cards/cardTime.jade b/client/components/cards/cardTime.jade new file mode 100644 index 00000000..dcfc92f0 --- /dev/null +++ b/client/components/cards/cardTime.jade @@ -0,0 +1,22 @@ +template(name="editCardSpentTime") + .edit-card-time + 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) + label(for="overtime") {{_ 'overtime'}} + a.js-toggle-overtime + .materialCheckBox#overtime(class="{{#if card.isOvertime}}is-checked{{/if}}" name="overtime") + + if error.get + .warning {{_ error.get}} + button.primary.wide.left.js-submit-time(type="submit") {{_ 'save'}} + button.js-delete-time.negate.wide.right {{_ 'delete'}} + +template(name="timeBadge") + if canModifyCard + a.js-edit-time.card-time(title="{{showTitle}}" class="{{#if isOvertime}}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}}") + | {{showTime}} diff --git a/client/components/cards/cardTime.js b/client/components/cards/cardTime.js new file mode 100644 index 00000000..eadcc88e --- /dev/null +++ b/client/components/cards/cardTime.js @@ -0,0 +1,81 @@ +BlazeComponent.extendComponent({ + template() { + return 'editCardSpentTime'; + }, + onCreated() { + this.error = new ReactiveVar(''); + this.card = this.data(); + }, + toggleOvertime() { + this.card.isOvertime = !this.card.isOvertime; + $('#overtime .materialCheckBox').toggleClass('is-checked'); + + $('#overtime').toggleClass('is-checked'); + }, + storeTime(spentTime, isOvertime) { + this.card.setSpentTime(spentTime); + this.card.setOvertime(isOvertime); + }, + deleteTime() { + this.card.unsetSpentTime(); + }, + events() { + return [{ + //TODO : need checking this portion + 'submit .edit-time'(evt) { + evt.preventDefault(); + + const spentTime = parseFloat(evt.target.time.value); + const isOvertime = this.card.isOvertime; + + if (spentTime >= 0) { + this.storeTime(spentTime, isOvertime); + Popup.close(); + } else { + this.error.set('invalid-time'); + evt.target.time.focus(); + } + }, + 'click .js-delete-time'(evt) { + evt.preventDefault(); + this.deleteTime(); + Popup.close(); + }, + 'click a.js-toggle-overtime': this.toggleOvertime, + }]; + }, +}).register('editCardSpentTimePopup'); + +BlazeComponent.extendComponent({ + template() { + return 'timeBadge'; + }, + onCreated() { + const self = this; + self.time = ReactiveVar(); + }, + showTitle() { + if (this.data().isOvertime) { + return `${TAPi18n.__('overtime')} ${this.data().spentTime} ${TAPi18n.__('hours')}`; + } else { + return `${TAPi18n.__('card-spent')} ${this.data().spentTime} ${TAPi18n.__('hours')}`; + } + }, + showTime() { + return this.data().spentTime; + }, + isOvertime() { + return this.data().isOvertime; + }, + events() { + return [{ + 'click .js-edit-time': Popup.open('editCardSpentTime'), + }]; + }, +}).register('cardSpentTime'); + +Template.timeBadge.helpers({ + canModifyCard() { + return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly(); + }, +}); diff --git a/client/components/cards/cardTime.styl b/client/components/cards/cardTime.styl new file mode 100644 index 00000000..3c4b43ae --- /dev/null +++ b/client/components/cards/cardTime.styl @@ -0,0 +1,17 @@ +.card-time + display: block + border-radius: 4px + padding: 1px 3px + color: #fff + + background-color: #dbdbdb + &:hover, &.is-active + background-color: #b3b3b3 + + time + &::before + font: normal normal normal 14px/1 FontAwesome + font-size: inherit + -webkit-font-smoothing: antialiased + content: "\f017" // clock symbol + margin-right: 0.3em diff --git a/client/components/cards/checklists.jade b/client/components/cards/checklists.jade index 7ecc5dd3..ae680bd5 100644 --- a/client/components/cards/checklists.jade +++ b/client/components/cards/checklists.jade @@ -1,8 +1,14 @@ template(name="checklists") - h2 {{_ 'checklists'}} + h3 {{_ 'checklists'}} + if toggleDeleteDialog.get + .board-overlay#card-details-overlay + +checklistDeleteDialog(checklist = checklistToDelete) + + .card-checklist-items each checklist in currentCard.checklists +checklistDetail(checklist = checklist) + if canModifyCard +inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId) +addChecklistItemForm @@ -12,19 +18,37 @@ template(name="checklists") | {{_ 'add-checklist'}}... template(name="checklistDetail") - +inlinedForm(classNames="js-edit-checklist-title" checklist = checklist) - +editChecklistItemForm(checklist = checklist) - else - .checklist-title - .checkbox.fa.fa-check-square-o - if canModifyCard - a.js-delete-checklist {{_ "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 {{checklist.title}} - else - h2.title {{checklist.title}} - +checklistItems(checklist = checklist) + .js-checklist.checklist + +inlinedForm(classNames="js-edit-checklist-title" checklist = checklist) + +editChecklistItemForm(checklist = checklist) + else + .checklist-title + span + 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 + = checklist.title + else + h2.title + +viewer + = checklist.title + +checklistItems(checklist = checklist) + +template(name="checklistDeleteDialog") + .js-confirm-checklist-delete + p + i(class="fa fa-exclamation-triangle" aria-hidden="true") + p + | {{_ 'confirm-checklist-delete-dialog'}} + span {{checklist.title}} + | ? + .js-checklist-delete-buttons + button.confirm-checklist-delete(type="button") {{_ 'delete'}} + button.toggle-delete-checklist-dialog(type="button") {{_ 'cancel'}} template(name="addChecklistItemForm") textarea.js-add-checklist-item(rows='1' autofocus) @@ -47,7 +71,7 @@ template(name="editChecklistItemForm") template(name="checklistItems") .checklist-items.js-checklist-items - each item in checklist.getItems + each item in checklist.items +inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist) +editChecklistItemForm(type = 'item' item = item checklist = checklist) else @@ -61,10 +85,14 @@ template(name="checklistItems") | {{_ 'add-checklist-item'}}... template(name='itemDetail') - .item.js-checklist-item + .js-checklist-item.checklist-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}}") {{item.title}} + .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}}") {{item.title}} + .item-title(class="{{#if item.isFinished }}is-checked{{/if}}") + +viewer + = item.title diff --git a/client/components/cards/checklists.js b/client/components/cards/checklists.js index bd9d275a..1f05aded 100644 --- a/client/components/cards/checklists.js +++ b/client/components/cards/checklists.js @@ -1,11 +1,14 @@ +const { calculateIndexData } = Utils; + function initSorting(items) { items.sortable({ tolerance: 'pointer', helper: 'clone', items: '.js-checklist-item:not(.placeholder)', - axis: 'y', + connectWith: '.js-checklist-items', + appendTo: '.board-canvas', distance: 7, - placeholder: 'placeholder', + placeholder: 'checklist-item placeholder', scroll: false, start(evt, ui) { ui.placeholder.height(ui.helper.height()); @@ -13,57 +16,57 @@ function initSorting(items) { }, stop(evt, ui) { const parent = ui.item.parents('.js-checklist-items'); - const orderedItems = []; - parent.find('.js-checklist-item').each(function(i, item) { - const checklistItem = Blaze.getData(item).item; - orderedItems.push(checklistItem._id); - }); - items.sortable('cancel'); - const formerParent = ui.item.parents('.js-checklist-items'); - let checklist = Blaze.getData(parent.get(0)).checklist; - const oldChecklist = Blaze.getData(formerParent.get(0)).checklist; - if (oldChecklist._id !== checklist._id) { - const currentItem = Blaze.getData(ui.item.get(0)).item; - for (let i = 0; i < orderedItems.length; i++) { - let itemId = orderedItems[i]; - if (itemId !== currentItem._id) continue; - checklist.addItem(currentItem.title); - checklist = Checklists.findOne({_id: checklist._id}); - itemId = checklist._id + (checklist.newItemIndex - 1); - if (currentItem.finished) { - checklist.finishItem(itemId); - } - orderedItems[i] = itemId; - oldChecklist.removeItem(currentItem._id); - } + const checklistId = Blaze.getData(parent.get(0)).checklist._id; + let prevItem = ui.item.prev('.js-checklist-item').get(0); + if (prevItem) { + prevItem = Blaze.getData(prevItem).item; } - checklist.sortItems(orderedItems); + let nextItem = ui.item.next('.js-checklist-item').get(0); + if (nextItem) { + nextItem = Blaze.getData(nextItem).item; + } + const nItems = 1; + const sortIndex = calculateIndexData(prevItem, nextItem, nItems); + const checklistDomElement = ui.item.get(0); + const checklistData = Blaze.getData(checklistDomElement); + const checklistItem = checklistData.item; + + items.sortable('cancel'); + + checklistItem.move(checklistId, sortIndex.base); }, }); } -Template.checklists.onRendered(function () { - const self = BlazeComponent.getComponentForElement(this.firstNode); - self.itemsDom = this.$('.card-checklist-items'); - initSorting(self.itemsDom); - self.itemsDom.mousedown(function(evt) { - evt.stopPropagation(); - }); +BlazeComponent.extendComponent({ + onRendered() { + const self = this; + self.itemsDom = this.$('.js-checklist-items'); + initSorting(self.itemsDom); + self.itemsDom.mousedown(function(evt) { + evt.stopPropagation(); + }); + + function userIsMember() { + return Meteor.user() && Meteor.user().isBoardMember(); + } - function userIsMember() { - return Meteor.user() && Meteor.user().isBoardMember(); - } + // Disable sorting if the current user is not a board member + self.autorun(() => { + const $itemsDom = $(self.itemsDom); + if ($itemsDom.data('sortable')) { + $(self.itemsDom).sortable('option', 'disabled', !userIsMember()); + } + }); + }, - // Disable sorting if the current user is not a board member - self.autorun(() => { - const $itemsDom = $(self.itemsDom); - if ($itemsDom.data('sortable')) { - $(self.itemsDom).sortable('option', 'disabled', !userIsMember()); - } - }); -}); + canModifyCard() { + return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly(); + }, +}).register('checklistDetail'); BlazeComponent.extendComponent({ + addChecklist(event) { event.preventDefault(); const textarea = this.find('textarea.js-add-checklist-item'); @@ -92,13 +95,38 @@ BlazeComponent.extendComponent({ const checklist = this.currentData().checklist; if (title) { - checklist.addItem(title); + ChecklistItems.insert({ + title, + checklistId: checklist._id, + cardId: checklist.cardId, + sort: checklist.itemCount(), + }); } // We keep the form opened, empty it. textarea.value = ''; textarea.focus(); }, + canModifyCard() { + return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly(); + }, + + deleteChecklist() { + const checklist = this.currentData().checklist; + if (checklist && checklist._id) { + Checklists.remove(checklist._id); + this.toggleDeleteDialog.set(false); + } + }, + + deleteItem() { + const checklist = this.currentData().checklist; + const item = this.currentData().item; + if (checklist && item && item._id) { + ChecklistItems.remove(item._id); + } + }, + editChecklist(event) { event.preventDefault(); const textarea = this.find('textarea.js-edit-checklist-item'); @@ -107,38 +135,24 @@ BlazeComponent.extendComponent({ checklist.setTitle(title); }, - canModifyCard() { - return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly(); - }, - editChecklistItem(event) { event.preventDefault(); const textarea = this.find('textarea.js-edit-checklist-item'); const title = textarea.value.trim(); - const itemId = this.currentData().item._id; - const checklist = this.currentData().checklist; - checklist.editItem(itemId, title); - }, - - deleteItem() { - const checklist = this.currentData().checklist; const item = this.currentData().item; - if (checklist && item && item._id) { - checklist.removeItem(item._id); - } + item.setTitle(title); }, - deleteChecklist() { - const checklist = this.currentData().checklist; - if (checklist && checklist._id) { - Checklists.remove(checklist._id); - } + onCreated() { + this.toggleDeleteDialog = new ReactiveVar(false); + this.checklistToDelete = null; //Store data context to pass to checklistDeleteDialog template }, pressKey(event) { - //If user press enter key inside a form, submit it, so user doesn't have to leave keyboard to submit a form. - if (event.keyCode === 13) { + //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(); @@ -146,18 +160,50 @@ BlazeComponent.extendComponent({ }, events() { + const events = { + 'click .toggle-delete-checklist-dialog'(event) { + if($(event.target).hasClass('js-delete-checklist')){ + this.checklistToDelete = this.currentData().checklist; //Store data context + } + this.toggleDeleteDialog.set(!this.toggleDeleteDialog.get()); + }, + }; + return [{ + ...events, 'submit .js-add-checklist': this.addChecklist, 'submit .js-edit-checklist-title': this.editChecklist, 'submit .js-add-checklist-item': this.addChecklistItem, 'submit .js-edit-checklist-item': this.editChecklistItem, 'click .js-delete-checklist-item': this.deleteItem, - 'click .js-delete-checklist': this.deleteChecklist, + 'click .confirm-checklist-delete': this.deleteChecklist, keydown: this.pressKey, }]; }, }).register('checklists'); +Template.checklistDeleteDialog.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.checklistDeleteDialog.onDestroyed(() => { + const $cardDetails = this.$('.card-details'); + $cardDetails.off('scroll'); //Reactivate scrolling + $cardDetails.animate( { scrollTop: this.scrollState.position }); +}); + Template.itemDetail.helpers({ canModifyCard() { return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly(); @@ -169,12 +215,12 @@ BlazeComponent.extendComponent({ const checklist = this.currentData().checklist; const item = this.currentData().item; if (checklist && item && item._id) { - checklist.toggleItem(item._id); + item.toggleItem(); } }, events() { return [{ - 'click .item .check-box': this.toggleItem, + 'click .js-checklist-item .check-box': this.toggleItem, }]; }, }).register('itemDetail'); diff --git a/client/components/cards/checklists.styl b/client/components/cards/checklists.styl index 77668349..d48c1851 100644 --- a/client/components/cards/checklists.styl +++ b/client/components/cards/checklists.styl @@ -26,7 +26,7 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item .title font-size: 18px - line-height: 30px + line-height: 25px .checklist-stat margin: 0 0.5em @@ -38,34 +38,102 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item .js-delete-checklist @extends .delete-text -.checklist-items - margin: 0 0 0.5em 1.33em - .item - line-height: 25px - font-size: 1.1em - margin-top: 3px - display: flex - &:hover - background-color: darken(white, 8%) - - .check-box - margin-top: 5px - &.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 - - .js-delete-checklist-item - @extends .delete-text - padding: 12px 0 0 0 +.js-confirm-checklist-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-checklist-delete-buttons + position: relative + padding: left 2% right 2% + .confirm-checklist-delete + margin-left: 12% + float: left + .toggle-delete-checklist-dialog + margin-right: 12% + float: right + +#card-details-overlay + top: 0 + bottom: -600px + right: 0 + +.checklist + 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 + + +.checklist-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-checklist-item + margin: 0 0 0.5em 1.33em + @extends .delete-text + padding: 12px 0 0 0 - .add-checklist-item - padding-top: 0.5em - display: inline-block +.add-checklist-item + margin: 0.2em 0 0.5em 1.33em + display: inline-block diff --git a/client/components/cards/labels.jade b/client/components/cards/labels.jade index 31bd4d06..6c6efb08 100644 --- a/client/components/cards/labels.jade +++ b/client/components/cards/labels.jade @@ -34,4 +34,5 @@ template(name="cardLabelsPopup") = name if(isLabelSelected ../_id) i.card-label-selectable-icon.fa.fa-check - a.quiet-button.full.js-add-label {{_ 'label-create'}} + if currentUser.isBoardAdmin + a.quiet-button.full.js-add-label {{_ 'label-create'}} diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index 6fd83386..9fa4dd57 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -6,14 +6,20 @@ template(name="minicard") .minicard-labels each labels .minicard-label(class="card-label-{{color}}" title="{{name}}") - .minicard-title= title + .minicard-title + +viewer + = title .dates if startAt - .date - +minicardStartDate + .date + +minicardStartDate if dueAt - .date - +minicardDueDate + .date + +minicardDueDate + if spentTime + .date + +cardSpentTime + if members .minicard-members.js-minicard-members each members diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl index a6aad896..d59f1f63 100644 --- a/client/components/cards/minicard.styl +++ b/client/components/cards/minicard.styl @@ -77,6 +77,9 @@ height: @width border-radius: 2px margin-left: 3px + .minicard-title + p:last-child + margin-bottom: 0 .dates display: flex; flex-direction: row; |