diff options
-rw-r--r-- | .meteor/packages | 1 | ||||
-rw-r--r-- | .meteor/versions | 1 | ||||
-rw-r--r-- | client/components/cards/cardDate.jade | 20 | ||||
-rw-r--r-- | client/components/cards/cardDate.js | 228 | ||||
-rw-r--r-- | client/components/cards/cardDate.styl | 58 | ||||
-rw-r--r-- | client/components/cards/cardDetails.jade | 13 | ||||
-rw-r--r-- | client/components/cards/cardDetails.js | 2 | ||||
-rw-r--r-- | client/components/cards/cardDetails.styl | 7 | ||||
-rw-r--r-- | client/components/cards/minicard.jade | 6 | ||||
-rw-r--r-- | client/components/cards/minicard.styl | 5 | ||||
-rwxr-xr-x | i18n/en.i18n.json | 9 | ||||
-rw-r--r-- | models/cards.js | 24 | ||||
-rw-r--r-- | sandstorm.js | 7 | ||||
-rw-r--r-- | server/publications/boards.js | 34 |
14 files changed, 399 insertions, 16 deletions
diff --git a/.meteor/packages b/.meteor/packages index e57bdb19..32d7389c 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -75,3 +75,4 @@ seriousm:emoji-continued templates:tabs verron:autosize simple:json-routes +rajit:bootstrap3-datepicker diff --git a/.meteor/versions b/.meteor/versions index e1c0b9cf..4ca2f780 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -121,6 +121,7 @@ perak:markdown@1.0.5 promise@0.7.3 raix:eventemitter@0.1.3 raix:handlebar-helpers@0.2.5 +rajit:bootstrap3-datepicker@1.5.1 random@1.0.10 rate-limit@1.0.5 reactive-dict@1.1.8 diff --git a/client/components/cards/cardDate.jade b/client/components/cards/cardDate.jade new file mode 100644 index 00000000..a2a28bbd --- /dev/null +++ b/client/components/cards/cardDate.jade @@ -0,0 +1,20 @@ +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") + a.js-edit-date.card-date(title="{{showTitle}}" class="{{classes}}") + time(datetime="{{showISODate}}") + | {{showDate}} diff --git a/client/components/cards/cardDate.js b/client/components/cards/cardDate.js new file mode 100644 index 00000000..4d129e8e --- /dev/null +++ b/client/components/cards/cardDate.js @@ -0,0 +1,228 @@ +// Edit start & due 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(); + }, + }]; + }, +}); + +// editCardStartDatePopup +(class extends EditCardDate { + onCreated() { + super.onCreated(); + this.data().startAt && this.date.set(moment(this.data().startAt)); + } + + _storeDate(date) { + this.card.setStart(date); + } + + _deleteDate() { + this.card.unsetStart(); + } +}).register('editCardStartDatePopup'); + +// editCardDueDatePopup +(class extends EditCardDate { + onCreated() { + super.onCreated(); + this.data().dueAt && this.date.set(moment(this.data().dueAt)); + } + + onRendered() { + super.onRendered(); + if (moment.isDate(this.card.startAt)) { + this.$('.js-datepicker').datepicker('setStartDate', this.card.startAt); + } + } + + _storeDate(date) { + this.card.setDue(date); + } + + _deleteDate() { + this.card.unsetDue(); + } +}).register('editCardDueDatePopup'); + + +// Display start & due dates +const CardDate = BlazeComponent.extendComponent({ + template() { + return 'dateBadge'; + }, + + onCreated() { + const self = this; + self.date = ReactiveVar(); + self.now = ReactiveVar(moment()); + window.setInterval(() => { + self.now.set(moment()); + }, 60000); + }, + + 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(); + }, +}); + +class CardStartDate extends CardDate { + onCreated() { + super.onCreated(); + const self = this; + self.autorun(() => { + self.date.set(moment(self.data().startAt)); + }); + } + + classes() { + if (this.date.get().isBefore(this.now.get(), 'minute') && + this.now.get().isBefore(this.data().dueAt)) { + return 'current'; + } + return ''; + } + + showTitle() { + return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`; + } + + events() { + return super.events().concat({ + 'click .js-edit-date': Popup.open('editCardStartDate'), + }); + } +} +CardStartDate.register('cardStartDate'); + +class CardDueDate extends CardDate { + onCreated() { + super.onCreated(); + const self = this; + self.autorun(() => { + self.date.set(moment(self.data().dueAt)); + }); + } + + classes() { + if (this.now.get().diff(this.date.get(), 'days') >= 2) + return 'long-overdue'; + else if (this.now.get().diff(this.date.get(), 'minute') >= 0) + return 'due'; + else if (this.now.get().diff(this.date.get(), 'days') >= -1) + return 'almost-due'; + return ''; + } + + showTitle() { + return `${TAPi18n.__('card-due-on')} ${this.date.get().format('LLLL')}`; + } + + events() { + return super.events().concat({ + 'click .js-edit-date': Popup.open('editCardDueDate'), + }); + } +} +CardDueDate.register('cardDueDate'); + +(class extends CardStartDate { + showDate() { + return this.date.get().format('l'); + } +}).register('minicardStartDate'); + +(class extends CardDueDate { + showDate() { + return this.date.get().format('l'); + } +}).register('minicardDueDate'); diff --git a/client/components/cards/cardDate.styl b/client/components/cards/cardDate.styl new file mode 100644 index 00000000..1631baa5 --- /dev/null +++ b/client/components/cards/cardDate.styl @@ -0,0 +1,58 @@ +.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 + padding: 1px 3px + + background-color: #dbdbdb + &:hover, &.is-active + background-color: #b3b3b3 + + &.current, &.almost-due, &.due, &.long-overdue + color: #fff + + &.current + background-color: #5ba639 + &:hover, &.is-active + background-color: darken(#5ba639, 10) + + &.almost-due + background-color: #edc909 + &:hover, &.is-active + background-color: darken(#edc909, 10) + + &.due + background-color: #fa3f00 + &:hover, &.is-active + background-color: darken(#fa3f00, 10) + + &.long-overdue + background-color: #fd5d47 + &:hover, &.is-active + background-color: darken(#fd5d47, 7) + + 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 diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index 734fc7e3..f4212d83 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -35,6 +35,17 @@ template(name="cardDetails") a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}") i.fa.fa-plus + if startAt + .card-details-item.card-details-item-start + h3.card-details-item-title {{_ 'card-start'}} + +cardStartDate + + if dueAt + .card-details-item.card-details-item-due + h3.card-details-item-title {{_ 'card-due'}} + +cardDueDate + + //- XXX We should use "editable" to avoid repetiting ourselves if currentUser.isBoardMember h3.card-details-item-title {{_ 'description'}} @@ -91,6 +102,8 @@ 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-start-date {{_ 'editCardStartDatePopup-title'}} + li: a.js-due-date {{_ 'editCardDueDatePopup-title'}} hr ul.pop-over-list li: a.js-move-card-to-top {{_ 'moveCardToTop-title'}} diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 303f1632..b7e0ef76 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -146,6 +146,8 @@ Template.cardDetailsActionsPopup.events({ 'click .js-members': Popup.open('cardMembers'), 'click .js-labels': Popup.open('cardLabels'), 'click .js-attachments': Popup.open('cardAttachments'), + 'click .js-start-date': Popup.open('editCardStartDate'), + 'click .js-due-date': Popup.open('editCardDueDate'), 'click .js-move-card': Popup.open('moveCard'), 'click .js-move-card-to-top'(evt) { evt.preventDefault(); diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl index d7d29551..f209862c 100644 --- a/client/components/cards/cardDetails.styl +++ b/client/components/cards/cardDetails.styl @@ -73,8 +73,13 @@ margin: 15px 0 .card-details-item + margin-right: 0.5em + &:last-child + margin-right: 0 &.card-details-item-labels, - &.card-details-item-members + &.card-details-item-members, + &.card-details-item-start, + &.card-details-item-due width: 50% flex-shrink: 1 diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index 1dfd2f8e..edc7d2d3 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -23,3 +23,9 @@ template(name="minicard") .badge span.badge-icon.fa.fa-paperclip span.badge-text= attachments.count + if startAt + .badge + +minicardStartDate + if dueAt + .badge + +minicardDueDate diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl index 0f6f8ad2..a61f6067 100644 --- a/client/components/cards/minicard.styl +++ b/client/components/cards/minicard.styl @@ -91,10 +91,13 @@ margin-right: 11px margin-bottom: 3px font-size: 0.9em + + &:last-of-type + margin-right: 0 .badge-icon, .badge-text - vertical-align: top + vertical-align: middle .badge-text font-size: 0.9em diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 6e2098c4..83ff2975 100755 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -88,11 +88,15 @@ "card-delete-notice": "Deleting is permanent. You will lose all actions associated with this card.", "card-delete-pop": "All actions will be removed from the activity feed and you won't be able to re-open the card. There is no undo.", "card-delete-suggest-archive": "You can archive a card to remove it from the board and preserve the activity.", + "card-due": "Due", + "card-due-on": "Due on", "card-edit-attachments": "Edit attachments", "card-edit-labels": "Edit labels", "card-edit-members": "Edit members", "card-labels-title": "Change the labels for the card.", "card-members-title": "Add or remove members of the board from the card.", + "card-start": "Start", + "card-start-on": "Starts on", "cardAttachmentsPopup-title": "Attach From", "cardDeletePopup-title": "Delete Card?", "cardDetailsActionsPopup-title": "Card Actions", @@ -133,6 +137,7 @@ "createBoardPopup-title": "Create Board", "createLabelPopup-title": "Create Label", "current": "current", + "date": "Date", "decline": "Decline", "default-avatar": "Default avatar", "delete": "Delete", @@ -146,6 +151,8 @@ "edit": "Edit", "edit-avatar": "Change Avatar", "edit-profile": "Edit Profile", + "editCardStartDatePopup-title": "Change start date", + "editCardDueDatePopup-title": "Change due date", "editLabelPopup-title": "Change Label", "editNotificationPopup-title": "Edit Notification", "editProfilePopup-title": "Edit Profile", @@ -197,6 +204,7 @@ "importMapMembersAddPopup-title": "Select Wekan member", "info": "Infos", "initials": "Initials", + "invalid-date": "Invalid date", "joined": "joined", "just-invited": "You are just invited to this board", "keyboard-shortcuts": "Keyboard shortcuts", @@ -286,6 +294,7 @@ "team": "Team", "this-board": "this board", "this-card": "this card", + "time": "Time", "title": "Title", "tracking": "Tracking", "tracking-info": "You will be notified of any changes to those cards you are involved as creator or member.", diff --git a/models/cards.js b/models/cards.js index 84fbb6c2..9e7d58c8 100644 --- a/models/cards.js +++ b/models/cards.js @@ -56,6 +56,14 @@ Cards.attachSchema(new SimpleSchema({ type: [String], optional: true, }, + startAt: { + type: Date, + optional: true, + }, + dueAt: { + type: Date, + optional: true, + }, // XXX Should probably be called `authorId`. Is it even needed since we have // the `members` field? userId: { @@ -207,6 +215,22 @@ Cards.mutations({ unsetCover() { return { $unset: { coverId: '' }}; }, + + setStart(startAt) { + return { $set: { startAt }}; + }, + + unsetStart() { + return { $unset: { startAt: '' }}; + }, + + setDue(dueAt) { + return { $set: { dueAt }}; + }, + + unsetDue() { + return { $unset: { dueAt: '' }}; + }, }); if (Meteor.isServer) { diff --git a/sandstorm.js b/sandstorm.js index dc5b10d6..3e04d79f 100644 --- a/sandstorm.js +++ b/sandstorm.js @@ -428,6 +428,13 @@ if (isSandstorm && Meteor.isClient) { return url.replace(/^https?:\/\/127\.0\.0\.1:[0-9]{2,5}/, ''); }; Meteor.absoluteUrl.defaultOptions = _defaultOptions; + + // XXX Hack to fix https://github.com/wefork/wekan/issues/27 + // Sandstorm Wekan instances only ever have a single board, so there is no need + // to cache per-board subscriptions. + SubsManager.prototype.subscribe = function(...params) { + return Meteor.subscribe(...params); + }; } // We use this blaze helper in the UI to hide some templates that does not make diff --git a/server/publications/boards.js b/server/publications/boards.js index cd3ef238..89681978 100644 --- a/server/publications/boards.js +++ b/server/publications/boards.js @@ -60,6 +60,7 @@ Meteor.publish('archivedBoards', function() { Meteor.publishRelations('board', function(boardId) { check(boardId, String); + const thisUserId = this.userId; this.cursor(Boards.find({ _id: boardId, @@ -99,20 +100,25 @@ Meteor.publishRelations('board', function(boardId) { this.cursor(Attachments.find({ cardId })); }); - // Board members. This publication also includes former board members that - // aren't members anymore but may have some activities attached to them in - // the history. - // - this.cursor(Users.find({ - _id: { $in: _.pluck(board.members, 'userId') }, - }, { fields: { - 'username': 1, - 'profile.fullname': 1, - 'profile.avatarUrl': 1, - }}), function(userId) { - // Presence indicators - this.cursor(presences.find({ userId })); - }); + if (board.members) { + // Board members. This publication also includes former board members that + // aren't members anymore but may have some activities attached to them in + // the history. + const memberIds = _.pluck(board.members, 'userId'); + + // We omit the current user because the client should already have that data, + // and sending it triggers a subtle bug: + // https://github.com/wefork/wekan/issues/15 + this.cursor(Users.find({ + _id: { $in: _.without(memberIds, thisUserId)}, + }, { fields: { + 'username': 1, + 'profile.fullname': 1, + 'profile.avatarUrl': 1, + }})); + + this.cursor(presences.find({ userId: { $in: memberIds } })); + } }); return this.ready(); |