Browse Source

Merge pull request #26 from shoetten/duedate

Start- and Duedate for cards
Lauri Ojansivu 8 years ago
parent
commit
b540eef855

+ 1 - 0
.meteor/packages

@@ -75,3 +75,4 @@ seriousm:emoji-continued
 templates:tabs
 verron:autosize
 simple:json-routes
+rajit:bootstrap3-datepicker

+ 1 - 0
.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

+ 20 - 0
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}}

+ 228 - 0
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');

+ 58 - 0
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

+ 13 - 0
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'}}

+ 2 - 0
client/components/cards/cardDetails.js

@@ -143,6 +143,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();

+ 6 - 1
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
 

+ 6 - 0
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

+ 4 - 1
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

+ 9 - 0
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.",

+ 24 - 0
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) {