summaryrefslogtreecommitdiffstats
path: root/client/components
diff options
context:
space:
mode:
Diffstat (limited to 'client/components')
-rw-r--r--client/components/activities/activities.jade14
-rw-r--r--client/components/activities/activities.js4
-rw-r--r--client/components/activities/activities.styl8
-rw-r--r--client/components/boards/boardBody.js7
-rw-r--r--client/components/boards/boardHeader.jade2
-rw-r--r--client/components/boards/boardHeader.js2
-rw-r--r--client/components/cards/attachments.jade3
-rw-r--r--client/components/cards/attachments.styl10
-rw-r--r--client/components/cards/cardDate.jade20
-rw-r--r--client/components/cards/cardDate.js228
-rw-r--r--client/components/cards/cardDate.styl58
-rw-r--r--client/components/cards/cardDetails.jade17
-rw-r--r--client/components/cards/cardDetails.js9
-rw-r--r--client/components/cards/cardDetails.styl7
-rw-r--r--client/components/cards/checklists.jade61
-rw-r--r--client/components/cards/checklists.js89
-rw-r--r--client/components/cards/checklists.styl68
-rw-r--r--client/components/cards/labels.js4
-rw-r--r--client/components/cards/minicard.jade16
-rw-r--r--client/components/cards/minicard.styl28
-rw-r--r--client/components/import/import.jade4
-rw-r--r--client/components/lists/list.js2
-rw-r--r--client/components/lists/listHeader.jade7
-rw-r--r--client/components/lists/listHeader.js14
-rwxr-xr-xclient/components/main/editor.js28
-rw-r--r--client/components/main/header.jade9
-rw-r--r--client/components/main/header.js4
-rw-r--r--client/components/main/header.styl12
-rw-r--r--client/components/main/layouts.styl12
-rw-r--r--client/components/sidebar/sidebar.jade11
-rw-r--r--client/components/sidebar/sidebar.js7
-rw-r--r--client/components/sidebar/sidebar.styl1
-rw-r--r--client/components/sidebar/sidebarFilters.jade13
-rw-r--r--client/components/users/userHeader.jade24
-rw-r--r--client/components/users/userHeader.js39
35 files changed, 786 insertions, 56 deletions
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade
index 9ff73864..fad147fc 100644
--- a/client/components/activities/activities.jade
+++ b/client/components/activities/activities.jade
@@ -26,6 +26,12 @@ template(name="boardActivities")
+viewer
= comment.text
+ if($eq activityType 'addChecklist')
+ | {{{_ 'activity-checklist-added' cardLink}}}.
+ .activity-checklist(href="{{ card.absoluteUrl }}")
+ +viewer
+ = checklist.title
+
if($eq activityType 'archivedCard')
| {{{_ 'activity-archived' cardLink}}}.
@@ -41,6 +47,9 @@ template(name="boardActivities")
if($eq activityType 'createList')
| {{_ 'activity-added' list.title boardLabel}}.
+ if($eq activityType 'removeList')
+ | {{_ 'activity-removed' title boardLabel}}.
+
if($eq activityType 'importBoard')
| {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
@@ -103,6 +112,11 @@ template(name="cardActivities")
| {{{_ 'activity-attached' attachmentLink cardLabel}}}.
if attachment.isImage
img.attachment-image-preview(src=attachment.url)
+ if($eq activityType 'addChecklist')
+ | {{{_ 'activity-checklist-added' cardLabel}}}.
+ .activity-checklist
+ +viewer
+ = checklist.title
if($eq activityType 'addComment')
+inlinedForm(classNames='js-edit-comment')
diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js
index 0bd6a272..6600849c 100644
--- a/client/components/activities/activities.js
+++ b/client/components/activities/activities.js
@@ -12,10 +12,12 @@ BlazeComponent.extendComponent({
const capitalizedMode = Utils.capitalize(mode);
const id = Session.get(`current${capitalizedMode}`);
const limit = this.page.get() * activitiesPerPage;
+ const user = Meteor.user();
+ const hideSystem = user ? user.hasHiddenSystemMessages() : false;
if (id === null)
return;
- this.subscribe('activities', mode, id, limit, () => {
+ this.subscribe('activities', mode, id, limit, hideSystem, () => {
this.loadNextPageLocked = false;
// If the sibear peak hasn't increased, that mean that there are no more
diff --git a/client/components/activities/activities.styl b/client/components/activities/activities.styl
index 1f0494c7..2285fc0a 100644
--- a/client/components/activities/activities.styl
+++ b/client/components/activities/activities.styl
@@ -26,6 +26,14 @@
margin-top: 5px
padding: 5px
+ .activity-checklist
+ display: block
+ border-radius: 3px
+ background: white
+ text-decoration: none
+ box-shadow: 0 1px 2px rgba(0,0,0,.2)
+ margin-top: 5px
+ padding: 5px
.activity-meta
font-size: 0.8em
color: darken(white, 40%)
diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js
index 15268a6b..4703bc91 100644
--- a/client/components/boards/boardBody.js
+++ b/client/components/boards/boardBody.js
@@ -163,8 +163,11 @@ Template.boardBody.onRendered(function() {
// Disable drag-dropping while in multi-selection mode, or if the current user
// is not a board member
self.autorun(() => {
- $(self.listsDom).sortable('option', 'disabled',
- MultiSelection.isActive() || !userIsMember());
+ const $listDom = $(self.listsDom);
+ if ($listDom.data('sortable')) {
+ $(self.listsDom).sortable('option', 'disabled',
+ MultiSelection.isActive() || !userIsMember());
+ }
});
// If there is no data in the board (ie, no lists) we autofocus the list
diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade
index e61831a1..03768b36 100644
--- a/client/components/boards/boardHeader.jade
+++ b/client/components/boards/boardHeader.jade
@@ -89,7 +89,7 @@ template(name="boardHeaderBar")
span {{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
- i.fa.fa-times-thin
+ i.fa.fa-times-thin
.separator
a.board-header-btn.js-open-board-menu
diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js
index b72bb162..a76b566a 100644
--- a/client/components/boards/boardHeader.js
+++ b/client/components/boards/boardHeader.js
@@ -33,7 +33,7 @@ Template.boardChangeTitlePopup.events({
const newDesc = tpl.$('.js-board-desc').val().trim();
if (newTitle) {
this.rename(newTitle);
- this.setDesciption(newDesc);
+ this.setDescription(newDesc);
Popup.close();
}
evt.preventDefault();
diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade
index 2cb3bb85..da728114 100644
--- a/client/components/cards/attachments.jade
+++ b/client/components/cards/attachments.jade
@@ -49,4 +49,5 @@ template(name="attachmentsGalery")
| {{_ 'delete'}}
if currentUser.isBoardMember
- a.attachment-item.add-attachment.js-add-attachment {{_ 'add-attachment' }}
+ li.attachment-item.add-attachment
+ a.js-add-attachment {{_ 'add-attachment' }}
diff --git a/client/components/cards/attachments.styl b/client/components/cards/attachments.styl
index f322ebc8..9a5d0645 100644
--- a/client/components/cards/attachments.styl
+++ b/client/components/cards/attachments.styl
@@ -20,6 +20,10 @@
display: flex
align-items: center
+ a
+ display: block
+ margin: auto
+
.attachment-thumbnail
height: 80px
display: flex
@@ -28,8 +32,8 @@
position: relative
.attachment-thumbnail-img
- height: 100%
- width: 100%
+ max-height: 100%
+ max-width: 100%
.attachment-thumbnail-ext
text-transform: uppercase
@@ -39,7 +43,7 @@
font-size: 0.75em
margin: 3px
- .attachment-details-actions
+ .attachment-details-actions a
display: block
.attachment-image-preview
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..cf113951 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'}}
@@ -61,6 +72,10 @@ template(name="cardDetails")
h3.card-details-item-title {{_ 'description'}}
+viewer
= description
+
+ hr
+ +checklists(cardId = _id)
+
if attachments.count
hr
h2
@@ -91,6 +106,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 299bbf34..b7e0ef76 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -62,6 +62,9 @@ BlazeComponent.extendComponent({
events() {
const events = {
+ [`${CSSEvents.transitionend} .js-card-details`]() {
+ this.isLoaded.set(true);
+ },
[`${CSSEvents.animationend} .js-card-details`]() {
this.isLoaded.set(true);
},
@@ -143,16 +146,18 @@ 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();
const minOrder = _.min(this.list().cards().map((c) => c.sort));
- this.move(this.listId, minOrder / 2);
+ this.move(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, Math.floor(maxOrder) + 1);
+ this.move(this.listId, maxOrder + 1);
},
'click .js-archive'(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/checklists.jade b/client/components/cards/checklists.jade
new file mode 100644
index 00000000..987e6cd9
--- /dev/null
+++ b/client/components/cards/checklists.jade
@@ -0,0 +1,61 @@
+template(name="checklists")
+ h2 {{_ 'checklists'}}
+ .card-checklist-items
+ each checklist in currentCard.checklists
+ +checklistDetail(checklist = checklist)
+ +inlinedForm(classNames="js-add-checklist" cardId = cardId)
+ +addChecklistItemForm
+ else
+ a.js-open-inlined-form
+ i.fa.fa-plus
+ | {{_ '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
+ a.js-delete-checklist {{_ "delete"}}...
+ span.checklist-stat(class="{{#if checklist.isFinished}}is-finished{{/if}}") {{checklist.finishedCount}}/{{checklist.itemCount}}
+ h2.title.js-open-inlined-form.is-editable {{checklist.title}}
+ +checklistItems(checklist = checklist)
+
+template(name="addChecklistItemForm")
+ textarea.js-add-checklist-item(rows='1' autofocus)
+ .edit-controls.clearfix
+ button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+
+template(name="editChecklistItemForm")
+ textarea.js-edit-checklist-item(rows='1' autofocus)
+ if $eq type 'item'
+ = item.title
+ else
+ = checklist.title
+ .edit-controls.clearfix
+ button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
+ a.fa.fa-times-thin.js-close-inlined-form
+ span(title=createdAt) {{ moment createdAt }}
+ if currentUser.isBoardMember
+ a.js-delete-checklist-item {{_ "delete"}}...
+
+template(name="checklistItems")
+ .checklist-items
+ each item in checklist.items
+ +inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
+ +editChecklistItemForm(type = 'item' item = item checklist = checklist)
+ else
+ +itemDetail(item = item checklist = checklist)
+ if currentUser.isBoardMember
+ +inlinedForm(classNames="js-add-checklist-item" checklist = checklist)
+ +addChecklistItemForm
+ else
+ a.add-checklist-item.js-open-inlined-form
+ i.fa.fa-plus
+ | {{_ 'add-checklist-item'}}...
+
+template(name='itemDetail')
+ .item
+ .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}}
diff --git a/client/components/cards/checklists.js b/client/components/cards/checklists.js
new file mode 100644
index 00000000..5dac7bd3
--- /dev/null
+++ b/client/components/cards/checklists.js
@@ -0,0 +1,89 @@
+BlazeComponent.extendComponent({
+ addChecklist(event) {
+ event.preventDefault();
+ const textarea = this.find('textarea.js-add-checklist-item');
+ const title = textarea.value.trim();
+ const cardId = this.currentData().cardId;
+ Checklists.insert({
+ cardId,
+ title,
+ });
+ },
+
+ addChecklistItem(event) {
+ event.preventDefault();
+ const textarea = this.find('textarea.js-add-checklist-item');
+ const title = textarea.value.trim();
+ const checklist = this.currentData().checklist;
+ checklist.addItem(title);
+ },
+
+ editChecklist(event) {
+ event.preventDefault();
+ const textarea = this.find('textarea.js-edit-checklist-item');
+ const title = textarea.value.trim();
+ const checklist = this.currentData().checklist;
+ checklist.setTitle(title);
+ },
+
+ 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);
+ }
+ },
+
+ deleteChecklist() {
+ const checklist = this.currentData().checklist;
+ if (checklist && checklist._id) {
+ Checklists.remove(checklist._id);
+ }
+ },
+
+ 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) {
+ event.preventDefault();
+ const $form = $(event.currentTarget).closest('form');
+ $form.find('button[type=submit]').click();
+ }
+ },
+
+ events() {
+ return [{
+ '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,
+ keydown: this.pressKey,
+ }];
+ },
+}).register('checklists');
+
+BlazeComponent.extendComponent({
+ toggleItem() {
+ const checklist = this.currentData().checklist;
+ const item = this.currentData().item;
+ if (checklist && item && item._id) {
+ checklist.toggleItem(item._id);
+ }
+ },
+ events() {
+ return [{
+ 'click .item .check-box': this.toggleItem,
+ }];
+ },
+}).register('itemDetail');
diff --git a/client/components/cards/checklists.styl b/client/components/cards/checklists.styl
new file mode 100644
index 00000000..885d7528
--- /dev/null
+++ b/client/components/cards/checklists.styl
@@ -0,0 +1,68 @@
+.js-add-checklist
+ color: #8c8c8c
+
+textarea.js-add-checklist-item, textarea.js-edit-checklist-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
+
+.checklist-title
+ .checkbox
+ float: left
+ width: 30px
+ height 30px
+ font-size: 18px
+ line-height: 30px
+
+ .title
+ font-size: 18px
+ line-height: 30px
+
+ .checklist-stat
+ margin: 0 0.5em
+ float: right
+ padding-top: 6px
+ &.is-finished
+ color: #3cb500
+
+ .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
+
+ .check-box
+ margin-top: 5px
+ &.is-checked
+ border-bottom: 2px solid #3cb500
+ border-right: 2px solid #3cb500
+
+ .item-title
+ padding-left: 10px;
+ &.is-checked
+ color: #8c8c8c
+ font-style: italic
+
+ .js-delete-checklist-item
+ @extends .delete-text
+ padding: 12px 0 0 0
+
+ .add-checklist-item
+ padding-top: 0.5em
+ display: inline-block
diff --git a/client/components/cards/labels.js b/client/components/cards/labels.js
index 20d95bc6..cdd5a700 100644
--- a/client/components/cards/labels.js
+++ b/client/components/cards/labels.js
@@ -9,9 +9,7 @@ BlazeComponent.extendComponent({
},
labels() {
- return labelColors.map((color) => {
- return { color, name: '' };
- });
+ return labelColors.map((color) => ({ color, name: '' }));
},
isSelected(color) {
diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade
index 573b3da1..8b46ee74 100644
--- a/client/components/cards/minicard.jade
+++ b/client/components/cards/minicard.jade
@@ -1,8 +1,7 @@
template(name="minicard")
.minicard
if cover
- .minicard-cover
- img(src="{{pathFor cover.url}}")
+ .minicard-cover(style="background-image: url('{{pathFor cover.url}}');")
if labels
.minicard-labels
each labels
@@ -15,7 +14,7 @@ template(name="minicard")
.badges
if comments.count
.badge(title="{{_ 'card-comments-title' comments.count }}")
- span.badge-icon.fa.fa-comment-o
+ span.badge-icon.fa.fa-comment-o.badge-comment
span.badge-text= comments.count
if description
.badge.badge-state-image-only(title=description)
@@ -24,3 +23,14 @@ template(name="minicard")
.badge
span.badge-icon.fa.fa-paperclip
span.badge-text= attachments.count
+ if startAt
+ .badge
+ +minicardStartDate
+ if dueAt
+ .badge
+ +minicardDueDate
+ if checklists.count
+ .badge(class="{{#if checklistFinished}}is-finished{{/if}}")
+ span.badge-icon.fa.fa-check-square-o
+ span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
+
diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl
index 3b8c8a9a..12a89785 100644
--- a/client/components/cards/minicard.styl
+++ b/client/components/cards/minicard.styl
@@ -62,16 +62,11 @@
.minicard-cover
background-position: center
background-repeat: no-repeat
- background-size: cover
+ background-size: contain
height: 145px
user-select: none
margin: -6px -8px 6px -8px
border-radius: top 2px
- position: relative
-
- img
- height: 100%
- width: 100%
.minicard-labels
float: right
@@ -96,15 +91,34 @@
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
+
+ &.is-finished
+ background: #3cb500
+ padding: 0px 3px
+ border-radius: 3px
+ color: white
+
+ .badge-icon,
+ .badge-text
+ vertical-align: middle//didn't figure why use top, it'd be easier to fill bg if it's middle. This was introduced in commit "91cfcf7b12b5e7c137c2e765b2c378dde6b82966" & "* Improve the design of the minicards badges" was mentioned.
+ &.badge-comment
+ margin-bottom: 0.1rem
.badge-text
font-size: 0.9em
padding-left: 2px
line-height: 14px
+ .check-list-text
+ padding-left: 0px
+ line-height: 12px
+
.minicard-members
float: right
diff --git a/client/components/import/import.jade b/client/components/import/import.jade
index e10072b4..d4def7d8 100644
--- a/client/components/import/import.jade
+++ b/client/components/import/import.jade
@@ -55,10 +55,10 @@ template(name="importMapMembersAddPopup")
ul.pop-over-list
+esEach(index="users")
li.item.js-member-item
- a.name.js-select-import(title="{{profile.name}} ({{username}})" data-id="{{_id}}")
+ a.name.js-select-import(title="{{profile.fullname}} ({{username}})" data-id="{{_id}}")
+userAvatar(userId=_id esSearch=true)
span.full-name
- = profile.name
+ = profile.fullname
| (<span class="username">{{username}}</span>)
+ifEsIsSearching(index='users')
+spinner
diff --git a/client/components/lists/list.js b/client/components/lists/list.js
index e1909783..187d8ec3 100644
--- a/client/components/lists/list.js
+++ b/client/components/lists/list.js
@@ -44,7 +44,7 @@ BlazeComponent.extendComponent({
placeholder: 'minicard-wrapper placeholder',
start(evt, ui) {
ui.placeholder.height(ui.helper.height());
- EscapeActions.executeUpTo('popup');
+ EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade
index f9fe065f..29aa4282 100644
--- a/client/components/lists/listHeader.jade
+++ b/client/components/lists/listHeader.jade
@@ -6,6 +6,10 @@ template(name="listHeader")
h2.list-header-name(
class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
= title
+ if showCardsCountForList cards.count
+ = cards.count
+ span.lowercase
+ | {{_ 'cards'}}
if currentUser.isBoardMember
if isWatching
i.list-header-watch-icon.fa.fa-eye
@@ -29,6 +33,9 @@ template(name="listActionPopup")
hr
ul.pop-over-list
li: a.js-close-list {{_ 'archive-list'}}
+ hr
+ ul.pop-over-list
+ li: a.js-remove-list {{_ 'remove-list'}}
template(name="boardLists")
ul.pop-over-list
diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js
index c7ae8e62..f352022d 100644
--- a/client/components/lists/listHeader.js
+++ b/client/components/lists/listHeader.js
@@ -13,6 +13,14 @@ BlazeComponent.extendComponent({
return list.findWatcher(Meteor.userId());
},
+ limitToShowCardsCount() {
+ return Meteor.user().getLimitToShowCardsCount();
+ },
+
+ showCardsCountForList(count) {
+ return count > this.limitToShowCardsCount();
+ },
+
events() {
return [{
'click .js-open-list-menu': Popup.open('listAction'),
@@ -52,4 +60,10 @@ Template.listActionPopup.events({
this.archive();
Popup.close();
},
+ 'click .js-remove-list'(evt) {
+ const currentList = this;
+ evt.preventDefault();
+ Lists.remove(currentList._id);
+ Popup.close();
+ },
});
diff --git a/client/components/main/editor.js b/client/components/main/editor.js
index da66bb74..da15407a 100755
--- a/client/components/main/editor.js
+++ b/client/components/main/editor.js
@@ -10,7 +10,7 @@ Template.editor.onRendered(() => {
search(term, callback) {
callback(Emoji.values.map((emoji) => {
return emoji.includes(term) ? emoji : null;
- }));
+ }).filter(Boolean));
},
template(value) {
const imgSrc = Emoji.baseImagePath + value;
@@ -31,7 +31,7 @@ Template.editor.onRendered(() => {
callback(currentBoard.activeMembers().map((member) => {
const username = Users.findOne(member.userId).username;
return username.includes(term) ? username : null;
- }));
+ }).filter(Boolean));
},
template(value) {
return value;
@@ -44,6 +44,8 @@ Template.editor.onRendered(() => {
]);
});
+import sanitizeXss from 'xss';
+
// XXX I believe we should compute a HTML rendered field on the server that
// would handle markdown, emoji and user mentions. We can simply have two
// fields, one source, and one compiled version (in HTML) and send only the
@@ -86,19 +88,14 @@ Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
content = content.replace(fullMention, Blaze.toHTML(link));
}
- return HTML.Raw(content);
+ return HTML.Raw(sanitizeXss(content));
}));
Template.viewer.events({
- 'click .js-open-member'(evt, tpl) {
- const userId = evt.currentTarget.dataset.userid;
- Popup.open('member').call({ userId }, evt, tpl);
- },
-
// Viewer sometimes have click-able wrapper around them (for instance to edit
// the corresponding text). Clicking a link shouldn't fire these actions, stop
// we stop these event at the viewer component level.
- 'click a'(evt) {
+ 'click a'(evt, tpl) {
evt.stopPropagation();
// XXX We hijack the build-in browser action because we currently don't have
@@ -106,9 +103,16 @@ Template.viewer.events({
// handled by a third party package that we can't configure easily. Fix that
// by using directly `_blank` attribute in the rendered HTML.
evt.preventDefault();
- const href = evt.currentTarget.href;
- if (href) {
- window.open(href, '_blank');
+
+ const userId = evt.currentTarget.dataset.userid;
+ if (userId) {
+ Popup.open('member').call({ userId }, evt, tpl);
+ }
+ else {
+ const href = evt.currentTarget.href;
+ if (href) {
+ window.open(href, '_blank');
+ }
}
},
});
diff --git a/client/components/main/header.jade b/client/components/main/header.jade
index 711e272c..bd0af880 100644
--- a/client/components/main/header.jade
+++ b/client/components/main/header.jade
@@ -45,3 +45,12 @@ template(name="header")
else
a.wekan-logo(href="{{pathFor 'home'}}" title="{{_ 'header-logo-title'}}")
img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan")
+
+ if appIsOffline
+ +offlineWarning
+
+template(name="offlineWarning")
+ .offline-warning
+ p
+ i.fa.fa-warning
+ | {{_ 'app-is-offline'}}
diff --git a/client/components/main/header.js b/client/components/main/header.js
index 73024aa9..0337c72b 100644
--- a/client/components/main/header.js
+++ b/client/components/main/header.js
@@ -6,6 +6,10 @@ Template.header.helpers({
hideLogo() {
return Utils.isMiniScreen() && Session.get('currentBoard');
},
+
+ appIsOffline() {
+ return !Meteor.status().connected;
+ },
});
Template.header.events({
diff --git a/client/components/main/header.styl b/client/components/main/header.styl
index 0b2ad860..0e35d38a 100644
--- a/client/components/main/header.styl
+++ b/client/components/main/header.styl
@@ -196,3 +196,15 @@
li
height: 28px
+
+.offline-warning
+ width: 100%
+ text-align: center
+ padding: 0
+ margin: 0
+ background: #F8ECBD
+ clear: both
+
+ p
+ margin: 7px
+ padding: 0
diff --git a/client/components/main/layouts.styl b/client/components/main/layouts.styl
index 83d4d693..1bc92b42 100644
--- a/client/components/main/layouts.styl
+++ b/client/components/main/layouts.styl
@@ -3,7 +3,7 @@
global-reset()
html, body, input, select, textarea, button
- font: 14px Roboto, "Helvetica Neue", Arial, Helvetica, sans-serif
+ font: 14px Roboto, "Helvetica Neue", Arial, Helvetica, "Yu Gothic", "Meiryo", sans-serif
line-height: 18px
color: #4d4d4d
@@ -351,6 +351,11 @@ a
background: darken(white, 13%)
overflow-y: auto
+ a
+ text-decoration: underline
+ &:hover
+ color: #333
+
.basicTabs-container .tabs-content-container
padding: 0
padding-top: 15px
@@ -374,3 +379,8 @@ a
.wrapper
height: 100%
margin: 0px
+
+.inline-input
+ height: 37px
+ margin: 8px 10px 0 0
+ width: 50px
diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade
index 0702b7b9..cb0845af 100644
--- a/client/components/sidebar/sidebar.jade
+++ b/client/components/sidebar/sidebar.jade
@@ -35,10 +35,13 @@ template(name="membersWidget")
.board-widget-content
each currentBoard.activeMembers
+userAvatar(userId=this.userId showStatus=true)
- unless isSandstorm
- if currentUser.isBoardAdmin
- a.member.add-member.js-manage-board-members
+ if isSandstorm
+ if currentUser.isBoardMember
+ a.member.add-member.sandstorm-powerbox-request-identity
i.fa.fa-plus
+ else if currentUser.isBoardAdmin
+ a.member.add-member.js-manage-board-members
+ i.fa.fa-plus
.clearfix
if isInvited
hr
@@ -107,7 +110,7 @@ template(name="addMemberPopup")
ul.pop-over-list
+esEach(index="users")
li.item.js-member-item(class="{{#if isBoardMember}}disabled{{/if}}")
- a.name.js-select-member(title="{{profile.name}} ({{username}})")
+ a.name.js-select-member(title="{{profile.fullname}} ({{username}})")
+userAvatar(userId=_id esSearch=true)
span.full-name
= profile.fullname
diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js
index 2f66d738..9eab1656 100644
--- a/client/components/sidebar/sidebar.js
+++ b/client/components/sidebar/sidebar.js
@@ -137,8 +137,8 @@ Template.memberPopup.events({
Popup.close();
}),
'click .js-leave-member'() {
- const currentBoard = Boards.findOne(Session.get('currentBoard'));
- Meteor.call('quitBoard', currentBoard, (err, ret) => {
+ const boardId = Session.get('currentBoard');
+ Meteor.call('quitBoard', boardId, (err, ret) => {
if (!ret && ret) {
Popup.close();
FlowRouter.go('home');
@@ -166,6 +166,9 @@ Template.membersWidget.helpers({
Template.membersWidget.events({
'click .js-member': Popup.open('member'),
'click .js-manage-board-members': Popup.open('addMember'),
+ 'click .sandstorm-powerbox-request-identity'() {
+ window.sandstormRequestIdentity();
+ },
'click .js-member-invite-accept'() {
const boardId = Session.get('currentBoard');
Meteor.user().removeInvite(boardId);
diff --git a/client/components/sidebar/sidebar.styl b/client/components/sidebar/sidebar.styl
index 95f1239b..8f2f493e 100644
--- a/client/components/sidebar/sidebar.styl
+++ b/client/components/sidebar/sidebar.styl
@@ -59,6 +59,7 @@
.member, .card-label
margin-right: 7px
+ margin-top: 5px
.sidebar-list-item-description
flex: 1
diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade
index 2b326493..9a9774bb 100644
--- a/client/components/sidebar/sidebarFilters.jade
+++ b/client/components/sidebar/sidebarFilters.jade
@@ -5,6 +5,12 @@
template(name="filterSidebar")
ul.sidebar-list
+ li(class="{{#if Filter.labelIds.isSelected undefined}}active{{/if}}")
+ a.name.js-toggle-label-filter
+ span.sidebar-list-item-description
+ | {{_ 'filter-no-label'}}
+ if Filter.labelIds.isSelected undefined
+ i.fa.fa-check
each currentBoard.labels
li
a.name.js-toggle-label-filter
@@ -18,6 +24,12 @@ template(name="filterSidebar")
i.fa.fa-check
hr
ul.sidebar-list
+ li(class="{{#if Filter.members.isSelected undefined}}active{{/if}}")
+ a.name.js-toggle-member-filter
+ span.sidebar-list-item-description
+ | {{_ 'filter-no-member'}}
+ if Filter.members.isSelected undefined
+ i.fa.fa-check
each currentBoard.activeMembers
with getUser userId
li(class="{{#if Filter.members.isSelected _id}}active{{/if}}")
@@ -86,4 +98,3 @@ template(name="disambiguateMultiMemberPopup")
template(name="moveSelectionPopup")
+boardLists
-
diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade
index a71e7fc7..ad41e8aa 100644
--- a/client/components/users/userHeader.jade
+++ b/client/components/users/userHeader.jade
@@ -12,10 +12,11 @@ template(name="memberMenuPopup")
ul.pop-over-list
with currentUser
li: a.js-edit-profile {{_ 'edit-profile'}}
- 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'}}
+ 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'}}
hr
ul.pop-over-list
li: a.js-logout {{_ 'log-out'}}
@@ -27,6 +28,8 @@ template(name="editProfilePopup")
input.js-profile-fullname(type="text" value=profile.fullname autofocus)
label
| {{_ 'username'}}
+ span.error.hide.username-taken
+ | {{_ 'error-username-taken'}}
input.js-profile-username(type="text" value=username)
label
| {{_ 'initials'}}
@@ -61,3 +64,16 @@ template(name="changeLanguagePopup")
= name
if isCurrentLanguage
i.fa.fa-check
+
+template(name="changeSettingsPopup")
+ ul.pop-over-list
+ li
+ a.js-toggle-system-messages
+ | {{_ 'hide-system-messages'}}
+ if hiddenSystemMessages
+ i.fa.fa-check
+ li
+ label.bold
+ | {{_ 'show-cards-minimum-count'}}
+ input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="1" max="99" onkeydown="return false")
+ input.js-apply-show-cards-at.left(type="submit" value="{{_ 'apply'}}")
diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js
index 10fdf699..98053ed1 100644
--- a/client/components/users/userHeader.js
+++ b/client/components/users/userHeader.js
@@ -5,6 +5,7 @@ Template.headerUserBar.events({
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'),
@@ -26,11 +27,18 @@ Template.editProfilePopup.events({
'profile.fullname': fullname,
'profile.initials': initials,
}});
- // XXX We should report the error to the user.
+
if (username !== Meteor.user().username) {
- Meteor.call('setUsername', username);
- }
- Popup.back();
+ Meteor.call('setUsername', username, function(error) {
+ const messageElement = tpl.$('.username-taken');
+ if (error) {
+ messageElement.show();
+ } else {
+ messageElement.hide();
+ Popup.back();
+ }
+ });
+ } else Popup.back();
},
});
@@ -82,3 +90,26 @@ Template.changeLanguagePopup.events({
evt.preventDefault();
},
});
+
+Template.changeSettingsPopup.helpers({
+ hiddenSystemMessages() {
+ return Meteor.user().hasHiddenSystemMessages();
+ },
+ showCardsCountAt() {
+ return Meteor.user().getLimitToShowCardsCount();
+ },
+});
+
+Template.changeSettingsPopup.events({
+ 'click .js-toggle-system-messages'() {
+ Meteor.call('toggleSystemMessages');
+ },
+ 'click .js-apply-show-cards-at'(evt, tpl) {
+ evt.preventDefault();
+ const minLimit = parseInt(tpl.$('#show-cards-count-at').val(), 10);
+ if (!isNaN(minLimit)) {
+ Meteor.call('changeLimitToShowCardsCount', minLimit);
+ Popup.back();
+ }
+ },
+});