summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
authorRomulus Tsai 蔡仲明 <urakagi@gmail.com>2020-05-08 10:13:11 +0800
committerRomulus Tsai 蔡仲明 <urakagi@gmail.com>2020-05-08 10:13:11 +0800
commitc3458855bdb52c976ee6689ad5a0d4e92e96f2e3 (patch)
treed9dbbcc3087b5bfc520710b5f5624a3f4e2b78e6 /client
parent444848876759173ad80203129250d2f0311f30fc (diff)
parentcfcc73724fcd394150d1b815d0a7a4c466e216b5 (diff)
downloadwekan-c3458855bdb52c976ee6689ad5a0d4e92e96f2e3.tar.gz
wekan-c3458855bdb52c976ee6689ad5a0d4e92e96f2e3.tar.bz2
wekan-c3458855bdb52c976ee6689ad5a0d4e92e96f2e3.zip
Merge branch 'master' into lib-change
Diffstat (limited to 'client')
-rw-r--r--client/00-startup.js6
-rw-r--r--client/components/activities/activities.jade347
-rw-r--r--client/components/activities/activities.js115
-rw-r--r--client/components/boards/boardArchive.js2
-rw-r--r--client/components/boards/boardBody.js10
-rw-r--r--client/components/boards/boardHeader.jade14
-rw-r--r--client/components/boards/boardHeader.js20
-rw-r--r--client/components/boards/boardsList.jade8
-rw-r--r--client/components/boards/boardsList.js80
-rw-r--r--client/components/boards/boardsList.styl20
-rw-r--r--client/components/cards/attachments.jade2
-rw-r--r--client/components/cards/cardDetails.jade73
-rw-r--r--client/components/cards/cardDetails.js109
-rw-r--r--client/components/cards/cardDetails.styl17
-rw-r--r--client/components/cards/checklists.jade3
-rw-r--r--client/components/cards/checklists.js31
-rw-r--r--client/components/cards/checklists.styl5
-rw-r--r--client/components/cards/labels.styl2
-rw-r--r--client/components/cards/minicard.jade8
-rw-r--r--client/components/cards/minicard.styl2
-rw-r--r--client/components/cards/subtasks.js17
-rw-r--r--client/components/import/import.jade3
-rw-r--r--client/components/lists/list.js22
-rw-r--r--client/components/lists/list.styl6
-rw-r--r--client/components/lists/listBody.js22
-rw-r--r--client/components/lists/listHeader.jade3
-rwxr-xr-xclient/components/main/editor.js192
-rw-r--r--client/components/main/header.jade7
-rw-r--r--client/components/main/header.styl6
-rw-r--r--client/components/main/layouts.jade14
-rw-r--r--client/components/main/layouts.js7
-rw-r--r--client/components/main/popup.styl5
-rw-r--r--client/components/notifications/notification.jade10
-rw-r--r--client/components/notifications/notification.js28
-rw-r--r--client/components/notifications/notification.styl57
-rw-r--r--client/components/notifications/notificationIcon.jade53
-rw-r--r--client/components/notifications/notifications.jade5
-rw-r--r--client/components/notifications/notifications.js32
-rw-r--r--client/components/notifications/notifications.styl17
-rw-r--r--client/components/notifications/notificationsDrawer.jade20
-rw-r--r--client/components/notifications/notificationsDrawer.js53
-rw-r--r--client/components/notifications/notificationsDrawer.styl69
-rw-r--r--client/components/rules/actions/boardActions.js2
-rw-r--r--client/components/settings/peopleBody.jade54
-rw-r--r--client/components/settings/peopleBody.js95
-rw-r--r--client/components/settings/settingBody.js2
-rw-r--r--client/components/sidebar/sidebar.jade33
-rw-r--r--client/components/sidebar/sidebar.js18
-rw-r--r--client/components/sidebar/sidebarFilters.jade18
-rw-r--r--client/components/sidebar/sidebarFilters.js5
-rw-r--r--client/components/swimlanes/swimlanes.js38
-rw-r--r--client/components/users/userHeader.jade25
-rw-r--r--client/components/users/userHeader.js40
-rw-r--r--client/lib/datepicker.js10
-rw-r--r--client/lib/filter.js10
-rwxr-xr-xclient/lib/keyboard.js32
56 files changed, 1335 insertions, 569 deletions
diff --git a/client/00-startup.js b/client/00-startup.js
new file mode 100644
index 00000000..4a717b67
--- /dev/null
+++ b/client/00-startup.js
@@ -0,0 +1,6 @@
+// PWA
+if ('serviceWorker' in navigator) {
+ window.addEventListener('load', function() {
+ navigator.serviceWorker.register('/pwa-service-worker.js');
+ });
+}
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade
index 8ecbdee8..c86936a0 100644
--- a/client/components/activities/activities.jade
+++ b/client/components/activities/activities.jade
@@ -8,234 +8,201 @@ template(name="activities")
+cardActivities
template(name="boardActivities")
- each currentBoard.activities
- .activity
- +userAvatar(userId=user._id)
- p.activity-desc
- +memberName(user=user)
+ each activityData in currentBoard.activities
+ +activity(activity=activityData card=card mode=mode)
- if($eq activityType 'deleteAttachment')
- | {{{_ 'activity-delete-attach' cardLink}}}.
+template(name="cardActivities")
+ each activityData in currentCard.activities
+ +activity(activity=activityData card=card mode=mode)
+
+template(name="activity")
+ .activity
+ +userAvatar(userId=activity.user._id)
+ p.activity-desc
+ +memberName(user=activity.user)
+
+ //- attachment activity -------------------------------------------------
+ if($eq activity.activityType 'deleteAttachment')
+ | {{{_ 'activity-delete-attach' cardLink}}}.
+
+ if($eq activity.activityType 'addAttachment')
+ | {{{_ 'activity-attached' attachmentLink cardLink}}}.
+ if($neq mode 'board')
+ if activity.attachment.isImage
+ img.attachment-image-preview(src=activity.attachment.url)
+
+ //- board activity ------------------------------------------------------
+ if($eq mode 'board')
+ if($eq activity.activityType 'createBoard')
+ | {{_ 'activity-created' boardLabel}}.
- if($eq activityType 'addAttachment')
- | {{{_ 'activity-attached' attachmentLink cardLink}}}.
+ if($eq activity.activityType 'importBoard')
+ | {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
- if($eq activityType 'addBoardMember')
+ if($eq activity.activityType 'addBoardMember')
| {{{_ 'activity-added' memberLink boardLabel}}}.
- if($eq activityType 'addComment')
- | {{{_ 'activity-on' cardLink}}}
- a.activity-comment(href="{{ card.absoluteUrl }}")
- +viewer
- = comment.text
-
- if($eq activityType 'addChecklist')
- | {{{_ 'activity-checklist-added' cardLink}}}.
- .activity-checklist(href="{{ card.absoluteUrl }}")
- +viewer
- = checklist.title
- if($eq activityType 'removeChecklist')
- | {{{_ 'activity-checklist-removed' cardLink}}}.
-
- if($eq activityType 'checkedItem')
- | {{{_ 'activity-checked-item' checkItem checklist.title cardLink}}}.
-
- if($eq activityType 'uncheckedItem')
- | {{{_ 'activity-unchecked-item' checkItem checklist.title cardLink}}}.
-
- if($eq activityType 'checklistCompleted')
- | {{{_ 'activity-checklist-completed' checklist.title cardLink}}}.
+ if($eq activity.activityType 'removeBoardMember')
+ | {{{_ 'activity-excluded' memberLink boardLabel}}}.
- if($eq activityType 'checklistUncompleted')
- | {{{_ 'activity-checklist-uncompleted' checklist.title cardLink}}}.
+ //- card activity -------------------------------------------------------
+ if($eq activity.activityType 'createCard')
+ if($eq mode 'card')
+ | {{{_ 'activity-added' cardLabel activity.listName}}}.
+ else
+ | {{{_ 'activity-added' cardLabel boardLabel}}}.
- if($eq activityType 'addChecklistItem')
- | {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}.
- .activity-checklist(href="{{ card.absoluteUrl }}")
- +viewer
- = checklistItem.title
- if($eq activityType 'removedChecklistItem')
- | {{{_ 'activity-checklist-item-removed' checklist.title cardLink}}}.
+ if($eq activity.activityType 'importCard')
+ | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
- if($eq activityType 'archivedCard')
- | {{{_ 'activity-archived' cardLink}}}.
+ if($eq activity.activityType 'moveCard')
+ | {{{_ 'activity-moved' cardLabel activity.oldList.title activity.list.title}}}.
- if($eq activityType 'archivedList')
- | {{_ 'activity-archived' list.title}}.
+ if($eq activity.activityType 'moveCardBoard')
+ | {{{_ 'activity-moved' cardLink activity.oldBoardName activity.boardName}}}.
- if($eq activityType 'archivedSwimlane')
- | {{_ 'activity-archived' swimlane.title}}.
+ if($eq activity.activityType 'archivedCard')
+ | {{{_ 'activity-archived' cardLink}}}.
- if($eq activityType 'createBoard')
- | {{_ 'activity-created' boardLabel}}.
+ if($eq activity.activityType 'restoredCard')
+ | {{{_ 'activity-sent' cardLink boardLabel}}}.
- if($eq activityType 'createCard')
- | {{{_ 'activity-added' cardLink boardLabel}}}.
+ //- checklist activity --------------------------------------------------
+ if($eq activity.activityType 'addChecklist')
+ | {{{_ 'activity-checklist-added' cardLink}}}.
+ if($eq mode 'card')
+ .activity-checklist
+ +viewer
+ = activity.checklist.title
+ else
+ a.activity-checklist(href="{{ activity.card.absoluteUrl }}")
+ +viewer
+ = activity.checklist.title
- if($eq activityType 'createCustomField')
- | {{_ 'activity-customfield-created' customField}}.
+ if($eq activity.activityType 'removedChecklist')
+ | {{{_ 'activity-checklist-removed' cardLink}}}.
- if($eq activityType 'createList')
- | {{_ 'activity-added' list.title boardLabel}}.
+ if($eq activity.activityType 'completeChecklist')
+ | {{{_ 'activity-checklist-completed' activity.checklist.title cardLink}}}.
- if($eq activityType 'createSwimlane')
- | {{_ 'activity-added' swimlane.title boardLabel}}.
+ if($eq activity.activityType 'uncompleteChecklist')
+ | {{{_ 'activity-checklist-uncompleted' activity.checklist.title cardLink}}}.
- if($eq activityType 'removeList')
- | {{_ 'activity-removed' title boardLabel}}.
+ if($eq activity.activityType 'checkedItem')
+ | {{{_ 'activity-checked-item' checkItem activity.checklist.title cardLink}}}.
- if($eq activityType 'importBoard')
- | {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
+ if($eq activity.activityType 'uncheckedItem')
+ | {{{_ 'activity-unchecked-item' checkItem activity.checklist.title cardLink}}}.
- if($eq activityType 'importCard')
- | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
+ if($eq activity.activityType 'addChecklistItem')
+ | {{{_ 'activity-checklist-item-added' activity.checklist.title cardLink}}}.
+ .activity-checklist(href="{{ activity.card.absoluteUrl }}")
+ +viewer
+ = activity.checklistItem.title
- if($eq activityType 'importList')
- | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
+ if($eq activity.activityType 'removedChecklistItem')
+ | {{{_ 'activity-checklist-item-removed' activity.checklist.title cardLink}}}.
- if($eq activityType 'joinMember')
- if($eq user._id member._id)
- | {{{_ 'activity-joined' cardLink}}}.
+ //- comment activity ----------------------------------------------------
+ if($eq mode 'card')
+ //- if we are in card mode we display the comment in a way that it
+ //- can be edited by the owner
+ if($eq activity.activityType 'addComment')
+ +inlinedForm(classNames='js-edit-comment')
+ +editor(autofocus=true)
+ = activity.comment.text
+ .edit-controls
+ button.primary(type="submit") {{_ 'edit'}}
else
- | {{{_ 'activity-added' memberLink cardLink}}}.
-
- if($eq activityType 'moveCardBoard')
- | {{{_ 'activity-moved' cardLink oldBoardName boardName}}}.
-
- if($eq activityType 'moveCard')
- | {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
-
- if($eq activityType 'removeBoardMember')
- | {{{_ 'activity-excluded' memberLink boardLabel}}}.
+ .activity-comment
+ +viewer
+ = activity.comment.text
+ span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
+ if ($eq currentUser._id activity.comment.userId)
+ = ' - '
+ a.js-open-inlined-form {{_ "edit"}}
+ = ' - '
+ a.js-delete-comment {{_ "delete"}}
- if($eq activityType 'restoredCard')
- | {{{_ 'activity-sent' cardLink boardLabel}}}.
+ if($eq activity.activityType 'deleteComment')
+ | {{{_ 'activity-deleteComment' currentData.commentId}}}.
- if($eq activityType 'addedLabel')
- | {{{_ 'activity-added-label' lastLabel cardLink}}}.
+ if($eq activity.activityType 'editComment')
+ | {{{_ 'activity-editComment' currentData.commentId}}}.
+ else
+ //- if we are not in card mode we only display a summary of the comment
+ if($eq activity.activityType 'addComment')
+ | {{{_ 'activity-on' cardLink}}}
+ a.activity-comment(href="{{ activity.card.absoluteUrl }}")
+ +viewer
+ = activity.comment.text
- if($eq activityType 'removedLabel')
- | {{{_ 'activity-removed-label' lastLabel cardLink}}}.
+ //- customField activity ------------------------------------------------
+ if($eq mode 'board')
+ if($eq activity.activityType 'createCustomField')
+ | {{_ 'activity-customfield-created' customField}}.
- if($eq activityType 'setCustomField')
+ if($eq activity.activityType 'setCustomField')
| {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}.
- if($eq activityType 'unsetCustomField')
+ if($eq activity.activityType 'unsetCustomField')
| {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}.
- if($eq activityType 'unjoinMember')
- if($eq user._id member._id)
- | {{{_ 'activity-unjoined' cardLink}}}.
- else
- | {{{_ 'activity-removed' memberLink cardLink}}}.
+ //- label activity ------------------------------------------------------
+ if($eq activity.activityType 'addedLabel')
+ | {{{_ 'activity-added-label' lastLabel cardLink}}}.
- span(title=createdAt).activity-meta {{ moment createdAt }}
+ if($eq activity.activityType 'removedLabel')
+ | {{{_ 'activity-removed-label' lastLabel cardLink}}}.
-template(name="cardActivities")
- each currentCard.activities
- .activity
- +userAvatar(userId=user._id)
- p.activity-desc
- +memberName(user=user)
- if($eq activityType 'createCard')
- | {{_ 'activity-added' cardLabel listName}}.
- if($eq activityType 'importCard')
- | {{{_ 'activity-imported' cardLabel list.title sourceLink}}}.
- if($eq activityType 'joinMember')
- if($eq user._id member._id)
- | {{_ 'activity-joined' cardLabel}}.
- else
- | {{{_ 'activity-added' memberLink cardLabel}}}.
- if($eq activityType 'unjoinMember')
- if($eq user._id member._id)
- | {{_ 'activity-unjoined' cardLabel}}.
- else
- | {{{_ 'activity-removed' cardLabel memberLink}}}.
- if($eq activityType 'archivedCard')
- | {{_ 'activity-archived' cardLabel}}.
-
- if($eq activityType 'addedLabel')
- | {{{_ 'activity-added-label-card' lastLabel }}}.
+ //- list activity -------------------------------------------------------
+ if($neq mode 'card')
+ if($eq activity.activityType 'createList')
+ | {{{_ 'activity-added' listLabel boardLabel}}}.
- if($eq activityType 'removedLabel')
- | {{{_ 'activity-removed-label-card' lastLabel }}}.
+ if($eq activity.activityType 'importList')
+ | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
- if($eq activityType 'removeChecklist')
- | {{{_ 'activity-checklist-removed' cardLabel}}}.
+ if($eq activity.activityType 'removeList')
+ | {{{_ 'activity-removed' activity.title boardLabel}}}.
- if($eq activityType 'checkedItem')
- | {{{_ 'activity-checked-item-card' checkItem checklist.title }}}.
+ if($eq activity.activityType 'archivedList')
+ | {{_ 'activity-archived' listLabel}}.
- if($eq activityType 'uncheckedItem')
- | {{{_ 'activity-unchecked-item-card' checkItem checklist.title }}}.
+ //- member activity ----------------------------------------------------
+ if($eq activity.activityType 'joinMember')
+ if($eq user._id activity.member._id)
+ | {{{_ 'activity-joined' cardLink}}}.
+ else
+ | {{{_ 'activity-added' memberLink cardLink}}}.
- if($eq activityType 'checklistCompleted')
- | {{{_ 'activity-checklist-completed-card' checklist.title }}}.
+ if($eq activity.activityType 'unjoinMember')
+ if($eq user._id activity.member._id)
+ | {{{_ 'activity-unjoined' cardLink}}}.
+ else
+ | {{{_ 'activity-removed' memberLink cardLink}}}.
- if($eq activityType 'checklistUncompleted')
- | {{{_ 'activity-checklist-uncompleted-card' checklist.title }}}.
+ //- swimlane activity --------------------------------------------------
+ if($neq mode 'card')
+ if($eq activity.activityType 'createSwimlane')
+ | {{{_ 'activity-added' activity.swimlane.title boardLabel}}}.
- if($eq activityType 'restoredCard')
- | {{_ 'activity-sent' cardLabel boardLabel}}.
- if($eq activityType 'moveCard')
- | {{_ 'activity-moved' cardLabel oldList.title list.title}}.
+ if($eq activity.activityType 'archivedSwimlane')
+ | {{_ 'activity-archived' activity.swimlane.title}}.
- if($eq activityType 'moveCardBoard')
- | {{{_ 'activity-moved' cardLink oldBoardName boardName}}}.
- if($eq activityType 'addAttachment')
- | {{{_ 'activity-attached' attachmentLink cardLabel}}}.
- if attachment.isImage
- img.attachment-image-preview(src=attachment.url)
- if($eq activityType 'deleteAttachment')
- | {{{_ 'activity-delete-attach' cardLabel}}}.
- if($eq activityType 'removedChecklist')
- | {{{_ 'activity-checklist-removed' cardLabel}}}.
- if($eq activityType 'addChecklist')
- | {{{_ 'activity-checklist-added' cardLabel}}}.
- .activity-checklist
- +viewer
- = checklist.title
- if($eq activityType 'addChecklistItem')
- | {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}.
- .activity-checklist(href="{{ card.absoluteUrl }}")
- +viewer
- = checklistItem.title
-
- if(currentData.timeKey)
- | {{{_ activityType }}}
+ //- I don't understand this part ----------------------------------------
+ if(currentData.timeKey)
+ | {{{_ activity.activityType }}}
+ = ' '
+ i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
+ if (currentData.timeOldValue)
= ' '
- i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
- if (currentData.timeOldValue)
- = ' '
- | {{{_ "previous_as" }}}
- = ' '
- i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
- = ' @'
- else if(currentData.timeValue)
- | {{{_ activityType currentData.timeValue}}}
-
-
- if($eq activityType 'deleteComment')
- | {{{_ 'activity-deleteComment' currentData.commentId}}}.
- if($eq activityType 'editComment')
- | {{{_ 'activity-editComment' currentData.commentId}}}.
- if($eq activityType 'addComment')
- +inlinedForm(classNames='js-edit-comment')
- +editor(autofocus=true)
- = comment.text
- .edit-controls
- button.primary(type="submit") {{_ 'edit'}}
- else
- .activity-comment
- +viewer
- = comment.text
- span(title=createdAt).activity-meta {{ moment createdAt }}
- if ($eq currentUser._id comment.userId)
- = ' - '
- a.js-open-inlined-form {{_ "edit"}}
- = ' - '
- a.js-delete-comment {{_ "delete"}}
-
- else
- span(title=createdAt).activity-meta {{ moment createdAt }}
+ | {{{_ "previous_as" }}}
+ = ' '
+ i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
+ = ' @'
+ else if(currentData.timeValue)
+ | {{{_ activity.activityType currentData.timeValue}}}
+
+ span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js
index 9697d28c..72af4c35 100644
--- a/client/components/activities/activities.js
+++ b/client/components/activities/activities.js
@@ -41,7 +41,9 @@ BlazeComponent.extendComponent({
});
});
},
+}).register('activities');
+BlazeComponent.extendComponent({
loadNextPage() {
if (this.loadNextPageLocked === false) {
this.page.set(this.page.get() + 1);
@@ -50,41 +52,37 @@ BlazeComponent.extendComponent({
},
checkItem() {
- const checkItemId = this.currentData().checklistItemId;
+ const checkItemId = this.currentData().activity.checklistItemId;
const checkItem = ChecklistItems.findOne({ _id: checkItemId });
- return checkItem.title;
+ return checkItem && checkItem.title;
},
boardLabel() {
+ const data = this.currentData();
+ if (data.mode !== 'board') {
+ return createBoardLink(data.activity.board(), data.activity.listName);
+ }
return TAPi18n.__('this-board');
},
cardLabel() {
+ const data = this.currentData();
+ if (data.mode !== 'card') {
+ return createCardLink(this.currentData().activity.card());
+ }
return TAPi18n.__('this-card');
},
cardLink() {
- const card = this.currentData().card();
- return (
- card &&
- Blaze.toHTML(
- HTML.A(
- {
- href: card.absoluteUrl(),
- class: 'action-card',
- },
- card.title,
- ),
- )
- );
+ return createCardLink(this.currentData().activity.card());
},
lastLabel() {
- const lastLabelId = this.currentData().labelId;
+ const lastLabelId = this.currentData().activity.labelId;
if (!lastLabelId) return null;
- const lastLabel = Boards.findOne(Session.get('currentBoard')).getLabelById(
- lastLabelId,
- );
+ const lastLabel = Boards.findOne(
+ this.currentData().activity.boardId,
+ ).getLabelById(lastLabelId);
if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) {
return lastLabel.color;
} else {
@@ -94,7 +92,7 @@ BlazeComponent.extendComponent({
lastCustomField() {
const lastCustomField = CustomFields.findOne(
- this.currentData().customFieldId,
+ this.currentData().activity.customFieldId,
);
if (!lastCustomField) return null;
return lastCustomField.name;
@@ -102,10 +100,10 @@ BlazeComponent.extendComponent({
lastCustomFieldValue() {
const lastCustomField = CustomFields.findOne(
- this.currentData().customFieldId,
+ this.currentData().activity.customFieldId,
);
if (!lastCustomField) return null;
- const value = this.currentData().value;
+ const value = this.currentData().activity.value;
if (
lastCustomField.settings.dropdownItems &&
lastCustomField.settings.dropdownItems.length > 0
@@ -122,11 +120,13 @@ BlazeComponent.extendComponent({
},
listLabel() {
- return this.currentData().list().title;
+ const activity = this.currentData().activity;
+ const list = activity.list();
+ return (list && list.title) || activity.title;
},
sourceLink() {
- const source = this.currentData().source;
+ const source = this.currentData().activity.source;
if (source) {
if (source.url) {
return Blaze.toHTML(
@@ -146,31 +146,32 @@ BlazeComponent.extendComponent({
memberLink() {
return Blaze.toHTMLWithData(Template.memberName, {
- user: this.currentData().member(),
+ user: this.currentData().activity.member(),
});
},
attachmentLink() {
- const attachment = this.currentData().attachment();
+ const attachment = this.currentData().activity.attachment();
const link = attachment.link('original', '/');
// trying to display url before file is stored generates js errors
return (
- attachment &&
- link &&
- Blaze.toHTML(
- HTML.A(
- {
- href: link,
- target: '_blank',
- },
- attachment.get('name'),
- ),
- )
+ (attachment &&
+ link &&
+ Blaze.toHTML(
+ HTML.A(
+ {
+ href: link,
+ target: '_blank',
+ },
+ attachment.name(),
+ ),
+ )) ||
+ this.currentData().activity.attachmentName
);
},
customField() {
- const customField = this.currentData().customField();
+ const customField = this.currentData().activity.customField();
if (!customField) return null;
return customField.name;
},
@@ -180,7 +181,7 @@ BlazeComponent.extendComponent({
{
// XXX We should use Popup.afterConfirmation here
'click .js-delete-comment'() {
- const commentId = this.currentData().commentId;
+ const commentId = this.currentData().activity.commentId;
CardComments.remove(commentId);
},
'submit .js-edit-comment'(evt) {
@@ -188,7 +189,7 @@ BlazeComponent.extendComponent({
const commentText = this.currentComponent()
.getValue()
.trim();
- const commentId = Template.parentData().commentId;
+ const commentId = Template.parentData().activity.commentId;
if (commentText) {
CardComments.update(commentId, {
$set: {
@@ -200,4 +201,36 @@ BlazeComponent.extendComponent({
},
];
},
-}).register('activities');
+}).register('activity');
+
+function createCardLink(card) {
+ return (
+ card &&
+ Blaze.toHTML(
+ HTML.A(
+ {
+ href: card.absoluteUrl(),
+ class: 'action-card',
+ },
+ card.title,
+ ),
+ )
+ );
+}
+
+function createBoardLink(board, list) {
+ let text = board.title;
+ if (list) text += `: ${list}`;
+ return (
+ board &&
+ Blaze.toHTML(
+ HTML.A(
+ {
+ href: board.absoluteUrl(),
+ class: 'action-board',
+ },
+ text,
+ ),
+ )
+ );
+}
diff --git a/client/components/boards/boardArchive.js b/client/components/boards/boardArchive.js
index d3e65bd8..5a5cf772 100644
--- a/client/components/boards/boardArchive.js
+++ b/client/components/boards/boardArchive.js
@@ -7,7 +7,7 @@ BlazeComponent.extendComponent({
return Boards.find(
{ archived: true },
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
},
diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js
index e70a9f67..4e473f18 100644
--- a/client/components/boards/boardBody.js
+++ b/client/components/boards/boardBody.js
@@ -1,7 +1,7 @@
import { Cookies } from 'meteor/ostrio:cookies';
const cookies = new Cookies();
const subManager = new SubsManager();
-const { calculateIndex, enableClickOnTouch } = Utils;
+const { calculateIndex } = Utils;
const swimlaneWhileSortingHeight = 150;
BlazeComponent.extendComponent({
@@ -191,9 +191,6 @@ BlazeComponent.extendComponent({
},
});
- // ugly touch event hotfix
- enableClickOnTouch('.js-swimlane:not(.placeholder)');
-
this.autorun(() => {
let showDesktopDragHandles = false;
currentUser = Meteor.user();
@@ -205,7 +202,7 @@ BlazeComponent.extendComponent({
} else {
showDesktopDragHandles = false;
}
- if (!Utils.isMiniScreen() && showDesktopDragHandles) {
+ if (Utils.isMiniScreen() || showDesktopDragHandles) {
$swimlanesDom.sortable({
handle: '.js-swimlane-header-handle',
});
@@ -215,9 +212,8 @@ BlazeComponent.extendComponent({
});
}
- // Disable drag-dropping if the current user is not a board member or is miniscreen
+ // Disable drag-dropping if the current user is not a board member
$swimlanesDom.sortable('option', 'disabled', !userIsMember());
- $swimlanesDom.sortable('option', 'disabled', Utils.isMiniScreen());
});
function userIsMember() {
diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade
index 53a74f76..4c0edac4 100644
--- a/client/components/boards/boardHeader.jade
+++ b/client/components/boards/boardHeader.jade
@@ -193,20 +193,6 @@ template(name="boardChangeViewPopup")
| {{_ 'board-view-cal'}}
if $eq Utils.boardView "board-view-cal"
i.fa.fa-check
- if currentUser.isAdmin
- hr
- li
- with "board-view-rules"
- a.js-open-rules-view(title="{{_ 'rules'}}")
- i.fa.fa-magic
- | {{_ 'rules'}}
- else if currentUser.isBoardAdmin
- hr
- li
- with "board-view-rules"
- a.js-open-rules-view(title="{{_ 'rules'}}")
- i.fa.fa-magic
- | {{_ 'rules'}}
template(name="createBoard")
form
diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js
index 9040ed83..be0146ec 100644
--- a/client/components/boards/boardHeader.js
+++ b/client/components/boards/boardHeader.js
@@ -33,22 +33,6 @@ Template.boardMenuPopup.events({
'click .js-card-settings': Popup.open('boardCardSettings'),
});
-Template.boardMenuPopup.helpers({
- exportUrl() {
- const params = {
- boardId: Session.get('currentBoard'),
- };
- const queryParams = {
- authToken: Accounts._storedLoginToken(),
- };
- return FlowRouter.path('/api/boards/:boardId/export', params, queryParams);
- },
- exportFilename() {
- const boardId = Session.get('currentBoard');
- return `wekan-export-board-${boardId}.json`;
- },
-});
-
Template.boardChangeTitlePopup.events({
submit(event, templateInstance) {
const newTitle = templateInstance
@@ -191,10 +175,6 @@ Template.boardChangeViewPopup.events({
Utils.setBoardView('board-view-cal');
Popup.close();
},
- 'click .js-open-rules-view'() {
- Modal.openWide('rulesMain');
- Popup.close();
- },
});
const CreateBoard = BlazeComponent.extendComponent({
diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade
index 79bae502..bbce1d6f 100644
--- a/client/components/boards/boardsList.jade
+++ b/client/components/boards/boardsList.jade
@@ -1,10 +1,10 @@
template(name="boardList")
.wrapper
- ul.board-list.clearfix
+ ul.board-list.clearfix.js-boards
li.js-add-board
a.board-list-item.label {{_ 'add-board'}}
each boards
- li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
+ li(class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
if isInvited
.board-list-item
span.details
@@ -39,7 +39,7 @@ template(name="boardList")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
- else if currentUser.isBoardAdmin
+ else if isAdministrable
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
@@ -55,7 +55,7 @@ template(name="boardList")
title="{{_ 'archive-board'}}")
template(name="boardListHeaderBar")
- h1 {{_ 'my-boards'}}
+ h1 {{_ title }}
.board-header-btns.right
a.board-header-btn.js-open-archived-board
i.fa.fa-archive
diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js
index 3918af82..9208fdb2 100644
--- a/client/components/boards/boardsList.js
+++ b/client/components/boards/boardsList.js
@@ -1,4 +1,5 @@
const subManager = new SubsManager();
+const { calculateIndex, enableClickOnTouch } = Utils;
Template.boardListHeaderBar.events({
'click .js-open-archived-board'() {
@@ -7,6 +8,9 @@ Template.boardListHeaderBar.events({
});
Template.boardListHeaderBar.helpers({
+ title() {
+ return FlowRouter.getRouteName() === 'home' ? 'my-boards' : 'public';
+ },
templatesBoardId() {
return Meteor.user() && Meteor.user().getTemplatesBoardId();
},
@@ -20,20 +24,80 @@ BlazeComponent.extendComponent({
Meteor.subscribe('setting');
},
- boards() {
- return Boards.find(
- {
- archived: false,
- 'members.userId': Meteor.userId(),
- type: 'board',
+ onRendered() {
+ const self = this;
+ function userIsAllowedToMove() {
+ return Meteor.user();
+ }
+
+ const itemsSelector = '.js-board:not(.placeholder)';
+
+ const $boards = this.$('.js-boards');
+ $boards.sortable({
+ connectWith: '.js-boards',
+ tolerance: 'pointer',
+ appendTo: '.board-list',
+ helper: 'clone',
+ distance: 7,
+ items: itemsSelector,
+ placeholder: 'board-wrapper placeholder',
+ start(evt, ui) {
+ ui.helper.css('z-index', 1000);
+ ui.placeholder.height(ui.helper.height());
+ EscapeActions.executeUpTo('popup-close');
+ },
+ stop(evt, ui) {
+ // To attribute the new index number, we need to get the DOM element
+ // of the previous and the following card -- if any.
+ const prevBoardDom = ui.item.prev('.js-board').get(0);
+ const nextBoardBom = ui.item.next('.js-board').get(0);
+ const sortIndex = calculateIndex(prevBoardDom, nextBoardBom, 1);
+
+ const boardDomElement = ui.item.get(0);
+ const board = Blaze.getData(boardDomElement);
+ // Normally the jquery-ui sortable library moves the dragged DOM element
+ // to its new position, which disrupts Blaze reactive updates mechanism
+ // (especially when we move the last card of a list, or when multiple
+ // users move some cards at the same time). To prevent these UX glitches
+ // we ask sortable to gracefully cancel the move, and to put back the
+ // DOM in its initial state. The card move is then handled reactively by
+ // Blaze with the below query.
+ $boards.sortable('cancel');
+
+ board.move(sortIndex.base);
},
- { sort: ['title'] },
- );
+ });
+
+ // ugly touch event hotfix
+ enableClickOnTouch(itemsSelector);
+
+ // Disable drag-dropping if the current user is not a board member or is comment only
+ this.autorun(() => {
+ $boards.sortable('option', 'disabled', !userIsAllowedToMove());
+ });
+ },
+
+ boards() {
+ let query = {
+ archived: false,
+ type: 'board',
+ };
+ if (FlowRouter.getRouteName() === 'home')
+ query['members.userId'] = Meteor.userId();
+ else query.permission = 'public';
+
+ return Boards.find(query, {
+ sort: { sort: 1 /* boards default sorting */ },
+ });
},
isStarred() {
const user = Meteor.user();
return user && user.hasStarred(this.currentData()._id);
},
+ isAdministrable() {
+ const user = Meteor.user();
+ return user && user.isBoardAdmin(this.currentData()._id);
+ },
hasOvertimeCards() {
subManager.subscribe('board', this.currentData()._id, false);
diff --git a/client/components/boards/boardsList.styl b/client/components/boards/boardsList.styl
index ae366e83..97d4f195 100644
--- a/client/components/boards/boardsList.styl
+++ b/client/components/boards/boardsList.styl
@@ -11,6 +11,19 @@ $spaceBetweenTiles = 16px
box-sizing: border-box
position: relative
+ &.placeholder:after
+ content: '';
+ display: block;
+ background: darken(white, 20%)
+ border-radius: 3px;
+ height: 106px;
+ margin: 8px;
+
+ &.ui-sortable-helper
+ cursor: grabbing
+ transform: rotate(4deg)
+ display: block !important
+
&.starred
.fa-star,
.fa-star-o
@@ -20,7 +33,7 @@ $spaceBetweenTiles = 16px
overflow: hidden;
background-color: #999
color: #f6f6f6
- height: 90px
+ height: auto
font-size: 16px
line-height: 22px
border-radius: 3px
@@ -31,6 +44,7 @@ $spaceBetweenTiles = 16px
margin: ($spaceBetweenTiles/2)
position: relative
text-decoration: none
+ word-wrap: break-word
&.tile
background-size: auto
@@ -55,7 +69,7 @@ $spaceBetweenTiles = 16px
.label
font-weight: normal
- line-height:90px
+ line-height: 56px
:hover
background-color:#939393
@@ -183,7 +197,7 @@ $spaceBetweenTiles = 16px
overflow: scroll
li
- width: 50%
+ width: 50%
.board-list-item
overflow: hidden
diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade
index 57e46e39..eda6d118 100644
--- a/client/components/cards/attachments.jade
+++ b/client/components/cards/attachments.jade
@@ -62,5 +62,5 @@ template(name="attachmentsGalery")
unless currentUser.isWorker
//li.attachment-item.add-attachment
a.js-add-attachment
- i.fa.fa-paperclip
+ i.fa.fa-plus
| {{_ 'add-attachment' }}
diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade
index 615ae1d5..ae97e0e9 100644
--- a/client/components/cards/cardDetails.jade
+++ b/client/components/cards/cardDetails.jade
@@ -32,7 +32,7 @@ template(name="cardDetails")
// else
{{_ 'top-level-card'}}
if isLinkedCard
- h3.linked-card-location
+ a.linked-card-location.js-go-to-linked-card
+viewer
| {{getBoardTitle}} > {{getTitle}}
@@ -199,10 +199,29 @@ template(name="cardDetails")
+viewer
= getAssignedBy
+ if getVoteQuestion
+ hr
+ .vote-title
+ h3
+ i.fa.fa-thumbs-up
+ card-details-item-title {{_ 'vote-question'}}
+ .vote-result
+ if votePublic
+ a.card-label.card-label-green.js-show-positive-votes {{ voteCountPositive }}
+ a.card-label.card-label-red.js-show-negative-votes {{ voteCountNegative }}
+ else
+ .card-label.card-label-green {{ voteCountPositive }}
+ .card-label.card-label-red {{ voteCountNegative }}
+ +viewer
+ = getVoteQuestion
+ button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}") {{_ 'vote-for-it'}}
+ button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'vote-against'}}
+
//- XXX We should use "editable" to avoid repetiting ourselves
if canModifyCard
unless currentUser.isWorker
if currentBoard.allowsDescriptionTitle
+ hr
h3
i.fa.fa-align-left
card-details-item-title {{_ 'description'}}
@@ -229,6 +248,7 @@ template(name="cardDetails")
a.js-close-inlined-form {{_ 'discard'}}
else if getDescription
if currentBoard.allowsDescriptionTitle
+ hr
h3.card-details-item-title {{_ 'description'}}
if currentBoard.allowsDescriptionText
+viewer
@@ -237,15 +257,16 @@ template(name="cardDetails")
.card-checklist-attachmentGalerys
.card-checklist-attachmentGalery.card-checklists
if currentBoard.allowsChecklists
+ hr
+checklists(cardId = _id)
if currentBoard.allowsSubtasks
hr
+subtasks(cardId = _id)
if currentBoard.allowsAttachments
- //- hr
- //- h3
- //- i.fa.fa-paperclip
- //- | {{_ 'attachments'}}
+ hr
+ h3
+ i.fa.fa-paperclip
+ | {{_ 'attachments'}}
.card-checklist-attachmentGalery.card-attachmentGalery
+attachmentsGalery
@@ -312,6 +333,16 @@ template(name="cardDetailsActionsPopup")
//li: a.js-members {{_ 'card-edit-members'}}
//li: a.js-labels {{_ 'card-edit-labels'}}
//li: a.js-attachments {{_ 'card-edit-attachments'}}
+ if getVoteQuestion
+ li
+ a.js-cancel-voting
+ i.fa.fa-thumbs-up
+ | {{_ 'card-cancel-voting'}}
+ else
+ li
+ a.js-start-voting
+ i.fa.fa-thumbs-up
+ | {{_ 'card-start-voting'}}
li
a.js-custom-fields
i.fa.fa-list-alt
@@ -535,3 +566,35 @@ template(name="cardDeletePopup")
unless archived
p {{_ "card-delete-suggest-archive"}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+
+template(name="cardStartVotingPopup")
+ form.edit-vote-question
+ .fields
+ label(for="vote") {{_ 'vote-question'}}
+ input.js-vote-field#vote(type="text" name="vote" value="{{card.getVoteQuestion}}" autofocus)
+ label(for="vote-public") {{_ 'vote-public'}}
+ a.js-toggle-vote-public
+ .materialCheckBox#vote-public(name="vote-public")
+
+ button.primary.confirm.js-submit {{_ 'save'}}
+ //- button.js-remove-color.negate.wide.right {{_ 'delete'}}
+
+template(name="positiveVoteMembersPopup")
+ ul.pop-over-list.js-card-member-list
+ each m in voteMemberPositive
+ li.item
+ a.name
+ +userAvatar(userId=m._id)
+ span.full-name
+ = m.profile.fullname
+ | (<span class="username">{{ m.username }}</span>)
+
+template(name="negativeVoteMembersPopup")
+ ul.pop-over-list.js-card-member-list
+ each m in voteMemberNegative
+ li.item
+ a.name
+ +userAvatar(userId=m._id)
+ span.full-name
+ = m.profile.fullname
+ | (<span class="username">{{ m.username }}</span>)
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index 5fdc5579..271fbe2f 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -1,5 +1,5 @@
const subManager = new SubsManager();
-const { calculateIndexData, enableClickOnTouch } = Utils;
+const { calculateIndexData } = Utils;
let cardColors;
Meteor.startup(() => {
@@ -38,6 +38,37 @@ BlazeComponent.extendComponent({
Meteor.subscribe('unsaved-edits');
},
+ voteState() {
+ const card = this.currentData();
+ const userId = Meteor.userId();
+ let state;
+ if (card.vote) {
+ if (card.vote.positive) {
+ state = _.contains(card.vote.positive, userId);
+ if (state === true) return true;
+ }
+ if (card.vote.negative) {
+ state = _.contains(card.vote.negative, userId);
+ if (state === true) return false;
+ }
+ }
+ return null;
+ },
+ votePublic() {
+ const card = this.currentData();
+ if (card.vote) return card.vote.public;
+ return null;
+ },
+ voteCountPositive() {
+ const card = this.currentData();
+ if (card.vote && card.vote.positive) return card.vote.positive.length;
+ return null;
+ },
+ voteCountNegative() {
+ const card = this.currentData();
+ if (card.vote && card.vote.negative) return card.vote.negative.length;
+ return null;
+ },
isWatching() {
const card = this.currentData();
return card.findWatcher(Meteor.userId());
@@ -200,9 +231,6 @@ BlazeComponent.extendComponent({
},
});
- // ugly touch event hotfix
- enableClickOnTouch('.card-checklist-items .js-checklist');
-
const $subtasksDom = this.$('.card-subtasks-items');
$subtasksDom.sortable({
@@ -238,26 +266,21 @@ BlazeComponent.extendComponent({
},
});
- // ugly touch event hotfix
- enableClickOnTouch('.card-subtasks-items .js-subtasks');
-
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());
- }
- if ($subtasksDom.data('sortable')) {
- $subtasksDom.sortable('option', 'disabled', !userIsMember());
- }
- if ($checklistsDom.data('sortable')) {
- $checklistsDom.sortable('option', 'disabled', Utils.isMiniScreen());
+ const disabled = !userIsMember() || Utils.isMiniScreen();
+ if (
+ $checklistsDom.data('uiSortable') ||
+ $checklistsDom.data('sortable')
+ ) {
+ $checklistsDom.sortable('option', 'disabled', disabled);
}
- if ($subtasksDom.data('sortable')) {
- $subtasksDom.sortable('option', 'disabled', Utils.isMiniScreen());
+ if ($subtasksDom.data('uiSortable') || $subtasksDom.data('sortable')) {
+ $subtasksDom.sortable('option', 'disabled', disabled);
}
});
},
@@ -347,6 +370,9 @@ BlazeComponent.extendComponent({
this.data().setRequestedBy('');
}
},
+ 'click .js-go-to-linked-card'() {
+ Utils.goCardId(this.data().linkedId);
+ },
'click .js-member': Popup.open('cardMember'),
'click .js-add-members': Popup.open('cardMembers'),
'click .js-assignee': Popup.open('cardAssignee'),
@@ -356,6 +382,8 @@ BlazeComponent.extendComponent({
'click .js-start-date': Popup.open('editCardStartDate'),
'click .js-due-date': Popup.open('editCardDueDate'),
'click .js-end-date': Popup.open('editCardEndDate'),
+ 'click .js-show-positive-votes': Popup.open('positiveVoteMembers'),
+ 'click .js-show-negative-votes': Popup.open('negativeVoteMembers'),
'mouseenter .js-card-details'() {
const parentComponent = this.parentComponent().parentComponent();
//on mobile view parent is Board, not BoardBody.
@@ -379,6 +407,18 @@ BlazeComponent.extendComponent({
'click #toggleButton'() {
Meteor.call('toggleSystemMessages');
},
+ 'click .js-vote'(e) {
+ const forIt = $(e.target).hasClass('js-vote-positive');
+ let newState = null;
+ if (
+ this.voteState() === null ||
+ (this.voteState() === false && forIt) ||
+ (this.voteState() === true && !forIt)
+ ) {
+ newState = forIt;
+ }
+ this.data().setVote(Meteor.userId(), newState);
+ },
},
];
},
@@ -560,6 +600,7 @@ Template.cardDetailsActionsPopup.events({
'click .js-assignees': Popup.open('cardAssignees'),
'click .js-labels': Popup.open('cardLabels'),
'click .js-attachments': Popup.open('cardAttachments'),
+ 'click .js-start-voting': Popup.open('cardStartVoting'),
'click .js-custom-fields': Popup.open('cardCustomFields'),
'click .js-received-date': Popup.open('editCardReceivedDate'),
'click .js-start-date': Popup.open('editCardStartDate'),
@@ -570,6 +611,11 @@ Template.cardDetailsActionsPopup.events({
'click .js-copy-card': Popup.open('copyCard'),
'click .js-copy-checklist-cards': Popup.open('copyChecklistToManyCards'),
'click .js-set-card-color': Popup.open('setCardColor'),
+ 'click .js-cancel-voting'(event) {
+ event.preventDefault();
+ this.unsetVote();
+ Popup.close();
+ },
'click .js-move-card-to-top'(event) {
event.preventDefault();
const minOrder = _.min(
@@ -672,7 +718,7 @@ BlazeComponent.extendComponent({
_id: { $ne: Meteor.user().getTemplatesBoardId() },
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
return boards;
@@ -848,7 +894,7 @@ BlazeComponent.extendComponent({
},
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
return boards;
@@ -945,6 +991,31 @@ BlazeComponent.extendComponent({
},
}).register('cardMorePopup');
+BlazeComponent.extendComponent({
+ onCreated() {
+ this.currentCard = this.currentData();
+ this.voteQuestion = new ReactiveVar(this.currentCard.voteQuestion);
+ },
+
+ events() {
+ return [
+ {
+ 'submit .edit-vote-question'(evt) {
+ evt.preventDefault();
+ const voteQuestion = evt.target.vote.value;
+ const publicVote = $('#vote-public').hasClass('is-checked');
+ this.currentCard.setVoteQuestion(voteQuestion, publicVote);
+ Popup.close();
+ },
+ 'click a.js-toggle-vote-public'(event) {
+ event.preventDefault();
+ $('#vote-public').toggleClass('is-checked');
+ },
+ },
+ ];
+ },
+}).register('cardStartVotingPopup');
+
// Close the card details pane by pressing escape
EscapeActions.register(
'detailsPane',
diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl
index 80fa87c0..3e2beadd 100644
--- a/client/components/cards/cardDetails.styl
+++ b/client/components/cards/cardDetails.styl
@@ -94,17 +94,18 @@ avatar-radius = 50%
animation: flexGrowIn 0.1s
box-shadow: 0 0 7px 0 darken(white, 30%)
transition: flex-basis 0.1s
+ box-sizing: border-box
.mCustomScrollBox
padding-left: 0
.ps-scrollbar-y-rail
pointer-event: all
- position: absolute;
+ position: absolute
.card-details-canvas
width: 470px
- padding-left: 20px;
+ padding-left: 20px
.card-details-header
margin: 0 -20px 5px
@@ -241,7 +242,7 @@ input[type="submit"].attachment-add-link-submit
.card-details-canvas
width: 100%
- padding-left: 0px;
+ padding-left: 0px
.card-details-header
.close-card-details
@@ -330,3 +331,13 @@ card-details-color(background, color...)
.card-details-indigo
card-details-color(#4b0082, #ffffff) //White text for better visibility
+
+.voted
+ opacity: .7
+.vote-title
+ display: flex
+ justify-content: space-between
+.vote-result
+ display: flex
+.js-show-positive-votes
+ cursor: pointer
diff --git a/client/components/cards/checklists.jade b/client/components/cards/checklists.jade
index 391769e9..1b1e088a 100644
--- a/client/components/cards/checklists.jade
+++ b/client/components/cards/checklists.jade
@@ -88,7 +88,8 @@ template(name="checklistItems")
template(name='checklistItemDetail')
.js-checklist-item.checklist-item
if canModifyCard
- .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
+ .check-box-container
+ .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
= item.title
diff --git a/client/components/cards/checklists.js b/client/components/cards/checklists.js
index c88fdd82..29573d2b 100644
--- a/client/components/cards/checklists.js
+++ b/client/components/cards/checklists.js
@@ -1,4 +1,4 @@
-const { calculateIndexData, enableClickOnTouch } = Utils;
+const { calculateIndexData, capitalize } = Utils;
function initSorting(items) {
items.sortable({
@@ -36,9 +36,6 @@ function initSorting(items) {
checklistItem.move(checklistId, sortIndex.base);
},
});
-
- // ugly touch event hotfix
- enableClickOnTouch('.js-checklist-item:not(.placeholder)');
}
BlazeComponent.extendComponent({
@@ -54,14 +51,15 @@ BlazeComponent.extendComponent({
return Meteor.user() && Meteor.user().isBoardMember();
}
- // Disable sorting if the current user is not a board member
+ // Disable sorting if the current user is not a board member or is a miniscreen
self.autorun(() => {
const $itemsDom = $(self.itemsDom);
- if ($itemsDom.data('sortable')) {
- $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
- }
- if ($itemsDom.data('sortable')) {
- $(self.itemsDom).sortable('option', 'disabled', Utils.isMiniScreen());
+ if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
+ $(self.itemsDom).sortable(
+ 'option',
+ 'disabled',
+ !userIsMember() || Utils.isMiniScreen(),
+ );
}
});
},
@@ -177,6 +175,16 @@ BlazeComponent.extendComponent({
}
},
+ focusChecklistItem(event) {
+ // If a new checklist is created, pre-fill the title and select it.
+ const checklist = this.currentData().checklist;
+ if (!checklist) {
+ const textarea = event.target;
+ textarea.value = capitalize(TAPi18n.__('r-checklist'));
+ textarea.select();
+ }
+ },
+
events() {
const events = {
'click .toggle-delete-checklist-dialog'(event) {
@@ -196,6 +204,7 @@ BlazeComponent.extendComponent({
'submit .js-edit-checklist-item': this.editChecklistItem,
'click .js-delete-checklist-item': this.deleteItem,
'click .confirm-checklist-delete': this.deleteChecklist,
+ 'focus .js-add-checklist-item': this.focusChecklistItem,
keydown: this.pressKey,
},
];
@@ -250,7 +259,7 @@ BlazeComponent.extendComponent({
events() {
return [
{
- 'click .js-checklist-item .check-box': this.toggleItem,
+ 'click .js-checklist-item .check-box-container': this.toggleItem,
},
];
},
diff --git a/client/components/cards/checklists.styl b/client/components/cards/checklists.styl
index 8ac37a15..0a6d688b 100644
--- a/client/components/cards/checklists.styl
+++ b/client/components/cards/checklists.styl
@@ -113,6 +113,9 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
&:hover
background-color: darken(white, 8%)
+ .check-box-container
+ padding-right: 1px;
+
.check-box
margin: 0.1em 0 0 0;
&.is-checked
@@ -121,7 +124,7 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
.item-title
flex: 1
- padding-left: 10px;
+ margin-left: 10px;
&.is-checked
color: #8c8c8c
font-style: italic
diff --git a/client/components/cards/labels.styl b/client/components/cards/labels.styl
index 9d7c7553..ee946656 100644
--- a/client/components/cards/labels.styl
+++ b/client/components/cards/labels.styl
@@ -158,6 +158,8 @@
.edit-labels-pop-over
margin-bottom: 8px
+ .card-label .viewer p
+ margin: 0
.edit-labels-pop-over .shortcut
display: inline-block
diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade
index 6a073424..e8efc6ac 100644
--- a/client/components/cards/minicard.jade
+++ b/client/components/cards/minicard.jade
@@ -4,8 +4,8 @@ template(name="minicard")
class="{{#if isLinkedBoard}}linked-board{{/if}}"
class="minicard-{{colorClass}}")
if isMiniScreen
- //.handle
- // .fa.fa-arrows
+ .handle
+ .fa.fa-arrows
unless isMiniScreen
if showDesktopDragHandles
.handle
@@ -100,6 +100,10 @@ template(name="minicard")
if getDescription
.badge.badge-state-image-only(title=getDescription)
span.badge-icon.fa.fa-align-left
+ if getVoteQuestion
+ .badge.badge-state-image-only(title=getVoteQuestion)
+ span.badge-icon.fa.fa-thumbs-up
+ span.badge-icon.fa.fa-thumbs-down
if attachments.count
.badge
span.badge-icon.fa.fa-paperclip
diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl
index 8607e118..7d72a588 100644
--- a/client/components/cards/minicard.styl
+++ b/client/components/cards/minicard.styl
@@ -79,7 +79,7 @@
border-radius: top 2px
.minicard-labels
- float: right
+ float: none
display: flex
flex-wrap: wrap
diff --git a/client/components/cards/subtasks.js b/client/components/cards/subtasks.js
index 34348fe1..4cd15c11 100644
--- a/client/components/cards/subtasks.js
+++ b/client/components/cards/subtasks.js
@@ -20,7 +20,22 @@ BlazeComponent.extendComponent({
const crtBoard = Boards.findOne(card.boardId);
const targetBoard = crtBoard.getDefaultSubtasksBoard();
const listId = targetBoard.getDefaultSubtasksListId();
- const swimlaneId = targetBoard.getDefaultSwimline()._id;
+
+ //Get the full swimlane data for the parent task.
+ const parentSwimlane = Swimlanes.findOne({
+ boardId: crtBoard._id,
+ _id: card.swimlaneId,
+ });
+ //find the swimlane of the same name in the target board.
+ const targetSwimlane = Swimlanes.findOne({
+ boardId: targetBoard._id,
+ title: parentSwimlane.title,
+ });
+ //If no swimlane with a matching title exists in the target board, fall back to the default swimlane.
+ const swimlaneId =
+ targetSwimlane === undefined
+ ? targetBoard.getDefaultSwimline()._id
+ : targetSwimlane._id;
if (title) {
const _id = Cards.insert({
diff --git a/client/components/import/import.jade b/client/components/import/import.jade
index 5b52f417..1551a7dd 100644
--- a/client/components/import/import.jade
+++ b/client/components/import/import.jade
@@ -15,9 +15,6 @@ template(name="importTextarea")
p: label(for='import-textarea') {{_ instruction}} {{_ 'import-board-instruction-about-errors'}}
textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
| {{jsonText}}
- if isSandstorm
- h1.warning {{_ 'import-sandstorm-backup-warning'}}
- p.warning {{_ 'import-sandstorm-warning'}}
input.primary.wide(type="submit" value="{{_ 'import'}}")
template(name="importMapMembers")
diff --git a/client/components/lists/list.js b/client/components/lists/list.js
index 8574caf7..839304f8 100644
--- a/client/components/lists/list.js
+++ b/client/components/lists/list.js
@@ -1,6 +1,6 @@
import { Cookies } from 'meteor/ostrio:cookies';
const cookies = new Cookies();
-const { calculateIndex, enableClickOnTouch } = Utils;
+const { calculateIndex } = Utils;
BlazeComponent.extendComponent({
// Proxy
@@ -114,9 +114,6 @@ BlazeComponent.extendComponent({
},
});
- // ugly touch event hotfix
- enableClickOnTouch(itemsSelector);
-
this.autorun(() => {
let showDesktopDragHandles = false;
currentUser = Meteor.user();
@@ -129,7 +126,7 @@ BlazeComponent.extendComponent({
showDesktopDragHandles = false;
}
- if (!Utils.isMiniScreen() && showDesktopDragHandles) {
+ if (Utils.isMiniScreen() || showDesktopDragHandles) {
$cards.sortable({
handle: '.handle',
});
@@ -139,27 +136,16 @@ BlazeComponent.extendComponent({
});
}
- if ($cards.data('sortable')) {
+ if ($cards.data('uiSortable') || $cards.data('sortable')) {
$cards.sortable(
'option',
'disabled',
- // Disable drag-dropping when user is not member/is miniscreen
+ // Disable drag-dropping when user is not member
!userIsMember(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !userIsMember(),
);
}
-
- if ($cards.data('sortable')) {
- $cards.sortable(
- 'option',
- 'disabled',
- // Disable drag-dropping when user is not member/is miniscreen
- Utils.isMiniScreen(),
- // Not disable drag-dropping while in multi-selection mode
- // MultiSelection.isActive() || !userIsMember(),
- );
- }
});
// We want to re-run this function any time a card is added.
diff --git a/client/components/lists/list.styl b/client/components/lists/list.styl
index 27cf678c..bc7f763f 100644
--- a/client/components/lists/list.styl
+++ b/client/components/lists/list.styl
@@ -43,9 +43,6 @@
background: white
margin: -3px 0 8px
-.list-header-card-count
- height: 35px
-
.list-header-add
flex: 0 0 auto
padding: 20px 12px 4px
@@ -60,6 +57,9 @@
background-color: #e4e4e4;
border-bottom: 6px solid #e4e4e4;
+ &.list-header-card-count
+ min-height: 35px
+ height: auto
&.ui-sortable-handle
cursor: grab
diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js
index 89c27ec7..88f88db0 100644
--- a/client/components/lists/listBody.js
+++ b/client/components/lists/listBody.js
@@ -411,7 +411,7 @@ BlazeComponent.extendComponent({
type: 'board',
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
return boards;
@@ -597,7 +597,7 @@ BlazeComponent.extendComponent({
type: 'board',
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
return boards;
@@ -743,9 +743,25 @@ BlazeComponent.extendComponent({
},
updateList() {
+ // Use fallback when requestIdleCallback is not available on iOS and Safari
+ // https://www.afasterweb.com/2017/11/20/utilizing-idle-moments/
+ checkIdleTime =
+ window.requestIdleCallback ||
+ function(handler) {
+ const startTime = Date.now();
+ return setTimeout(function() {
+ handler({
+ didTimeout: false,
+ timeRemaining() {
+ return Math.max(0, 50.0 - (Date.now() - startTime));
+ },
+ });
+ }, 1);
+ };
+
if (this.spinnerInView()) {
this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
- window.requestIdleCallback(() => this.updateList());
+ checkIdleTime(() => this.updateList());
}
},
diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade
index 182fee9e..fa1faf34 100644
--- a/client/components/lists/listHeader.jade
+++ b/client/components/lists/listHeader.jade
@@ -30,10 +30,9 @@ template(name="listHeader")
if canSeeAddCard
a.js-add-card.fa.fa-plus.list-header-plus-icon
a.fa.fa-navicon.js-open-list-menu
- //a.list-header-handle.handle.fa.fa-arrows.js-list-handle
else
a.list-header-menu-icon.fa.fa-angle-right.js-select-list
- //a.list-header-handle.handle.fa.fa-arrows.js-list-handle
+ a.list-header-handle.handle.fa.fa-arrows.js-list-handle
else if currentUser.isBoardMember
if isWatching
i.list-header-watch-icon.fa.fa-eye
diff --git a/client/components/main/editor.js b/client/components/main/editor.js
index bc2e0bad..de598da8 100755
--- a/client/components/main/editor.js
+++ b/client/components/main/editor.js
@@ -1,87 +1,3 @@
-import _sanitizeXss from 'xss';
-const ASIS = 'asis';
-const sanitizeXss = (input, options) => {
- const defaultAllowedIframeSrc = /^(https:){0,1}\/\/.*?(youtube|vimeo|dailymotion|youku)/i;
- const allowedIframeSrcRegex = (function() {
- let reg = defaultAllowedIframeSrc;
- const SAFE_IFRAME_SRC_PATTERN =
- Meteor.settings.public.SAFE_IFRAME_SRC_PATTERN;
- try {
- if (SAFE_IFRAME_SRC_PATTERN !== undefined) {
- reg = new RegExp(SAFE_IFRAME_SRC_PATTERN, 'i');
- }
- } catch (e) {
- /*eslint no-console: ["error", { allow: ["warn", "error"] }] */
-
- console.error('Wrong pattern specified', SAFE_IFRAM_SRC_PATTERN, e);
- }
- return reg;
- })();
- const targetWindow = '_blank';
- const getHtmlDOM = html => {
- const i = document.createElement('i');
- i.innerHTML = html;
- return i.firstChild;
- };
- options = {
- onTag(tag, html, options) {
- const htmlDOM = getHtmlDOM(html);
- const getAttr = attr => {
- return htmlDOM && attr && htmlDOM.getAttribute(attr);
- };
- if (tag === 'iframe') {
- const clipCls = 'note-vide-clip';
- if (!options.isClosing) {
- const iframeCls = getAttr('class');
- let safe = iframeCls.indexOf(clipCls) > -1;
- const src = getAttr('src');
- if (allowedIframeSrcRegex.exec(src)) {
- safe = true;
- }
- if (safe)
- return `<iframe src='${src}' class="${clipCls}" width=100% height=auto allowfullscreen></iframe>`;
- } else {
- // remove </iframe> tag
- return '';
- }
- } else if (tag === 'a') {
- if (!options.isClosing) {
- if (getAttr(ASIS) === 'true') {
- // if has a ASIS attribute, don't do anything, it's a member id
- return html;
- } else {
- const href = getAttr('href');
- if (href.match(/^((http(s){0,1}:){0,1}\/\/|\/)/)) {
- // a valid url
- return `<a href=${href} target=${targetWindow}>`;
- }
- }
- }
- } else if (tag === 'img') {
- if (!options.isClosing) {
- const src = getAttr('src');
- if (src) {
- return `<a href='${src}' class='swipebox'><img src='${src}' class="attachment-image-preview mCS_img_loaded"></a>`;
- }
- }
- }
- return undefined;
- },
- onTagAttr(tag, name, value) {
- if (tag === 'img' && name === 'src') {
- if (value && value.substr(0, 5) === 'data:') {
- // allow image with dataURI src
- return `${name}='${value}'`;
- }
- } else if (tag === 'a' && name === 'target') {
- return `${name}='${targetWindow}'`; // always change a href target to a new window
- }
- return undefined;
- },
- ...options,
- };
- return _sanitizeXss(input, options);
-};
Template.editor.onRendered(() => {
const textareaSelector = 'textarea';
const mentions = [
@@ -94,13 +10,7 @@ Template.editor.onRendered(() => {
currentBoard
.activeMembers()
.map(member => {
- const user = Users.findOne(member.userId);
- if (user._id === Meteor.userId()) {
- return null;
- }
- const value = user.username;
- const username =
- value && value.match(/\s+/) ? `"${value}"` : value;
+ const username = Users.findOne(member.userId).username;
return username.includes(term) ? username : null;
})
.filter(Boolean),
@@ -126,10 +36,9 @@ Template.editor.onRendered(() => {
? [
['view', ['fullscreen']],
['table', ['table']],
- ['font', ['bold']],
- ['color', ['color']],
- ['insert', ['video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
+ ['font', ['bold', 'underline']],
//['fontsize', ['fontsize']],
+ ['color', ['color']],
]
: [
['style', ['style']],
@@ -139,11 +48,47 @@ Template.editor.onRendered(() => {
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
- ['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
+ //['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
//['insert', ['link', 'picture']], // modal popup has issue somehow :(
['view', ['fullscreen', 'help']],
];
- const cleanPastedHTML = sanitizeXss;
+ const cleanPastedHTML = function(input) {
+ const badTags = [
+ 'style',
+ 'script',
+ 'applet',
+ 'embed',
+ 'noframes',
+ 'noscript',
+ 'meta',
+ 'link',
+ 'button',
+ 'form',
+ ].join('|');
+ const badPatterns = new RegExp(
+ `(?:${[
+ `<(${badTags})s*[^>][\\s\\S]*?<\\/\\1>`,
+ `<(${badTags})[^>]*?\\/>`,
+ ].join('|')})`,
+ 'gi',
+ );
+ let output = input;
+ // remove bad Tags
+ output = output.replace(badPatterns, '');
+ // remove attributes ' style="..."'
+ const badAttributes = new RegExp(
+ `(?:${[
+ 'on\\S+=([\'"]?).*?\\1',
+ 'href=([\'"]?)javascript:.*?\\2',
+ 'style=([\'"]?).*?\\3',
+ 'target=\\S+',
+ ].join('|')})`,
+ 'gi',
+ );
+ output = output.replace(badAttributes, '');
+ output = output.replace(/(<a )/gi, '$1target=_ '); // always to new target
+ return output;
+ };
const editor = '.editor';
const selectors = [
`.js-new-comment-form ${editor}`,
@@ -163,37 +108,14 @@ Template.editor.onRendered(() => {
}
return undefined;
};
- let popupShown = false;
inputs.each(function(idx, input) {
mSummernotes[idx] = $(input).summernote({
placeholder,
callbacks: {
- onKeydown(e) {
- if (popupShown) {
- e.preventDefault();
- }
- },
- onKeyup(e) {
- if (popupShown) {
- e.preventDefault();
- }
- },
onInit(object) {
const originalInput = this;
- const setAutocomplete = function(jEditor) {
- if (jEditor !== undefined) {
- jEditor.escapeableTextComplete(mentions).on({
- 'textComplete:show'() {
- popupShown = true;
- },
- 'textComplete:hide'() {
- popupShown = false;
- },
- });
- }
- };
$(originalInput).on('submitted', function() {
- // resetCommentInput has been called
+ // when comment is submitted, the original textarea will be set to '', so shall we
if (!this.value) {
const sn = getSummernote(this);
sn && sn.summernote('code', '');
@@ -201,7 +123,9 @@ Template.editor.onRendered(() => {
});
const jEditor = object && object.editable;
const toolbar = object && object.toolbar;
- setAutocomplete(jEditor);
+ if (jEditor !== undefined) {
+ jEditor.escapeableTextComplete(mentions);
+ }
if (toolbar !== undefined) {
const fBtn = toolbar.find('.btn-fullscreen');
fBtn.on('click', function() {
@@ -211,6 +135,7 @@ Template.editor.onRendered(() => {
});
}
},
+
onImageUpload(files) {
const $summernote = getSummernote(this);
if (files && files.length > 0) {
@@ -287,6 +212,12 @@ Template.editor.onRendered(() => {
const thisNote = this;
const updatePastedText = function(object) {
const someNote = getSummernote(object);
+ // Fix Pasting text into a card is adding a line before and after
+ // (and multiplies by pasting more) by changing paste "p" to "br".
+ // Fixes https://github.com/wekan/wekan/2890 .
+ // == Fix Start ==
+ someNote.execCommand('defaultParagraphSeparator', false, 'br');
+ // == Fix End ==
const original = someNote.summernote('code');
const cleaned = cleanPastedHTML(original); //this is where to call whatever clean function you want. I have mine in a different file, called CleanPastedHTML.
someNote.summernote('code', ''); //clear original
@@ -329,6 +260,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 and user mentions. We can simply have two
// fields, one source, and one compiled version (in HTML) and send only the
@@ -350,7 +283,7 @@ Blaze.Template.registerHelper(
}
return member;
});
- const mentionRegex = /\B@(?:(?:"([\w.\s]*)")|([\w.]+))/gi; // including space in username
+ const mentionRegex = /\B@([\w.]*)/gi;
let currentMention;
while ((currentMention = mentionRegex.exec(content)) !== null) {
@@ -366,7 +299,12 @@ Blaze.Template.registerHelper(
if (knowedUser.userId === Meteor.userId()) {
linkClass += ' me';
}
- const link = HTML.A(
+ // This @user mention link generation did open same Wekan
+ // window in new tab, so now A is changed to U so it's
+ // underlined and there is no link popup. This way also
+ // text can be selected more easily.
+ //const link = HTML.A(
+ const link = HTML.U(
{
class: linkClass,
// XXX Hack. Since we stringify this render function result below with
@@ -374,16 +312,17 @@ Blaze.Template.registerHelper(
// `userId` to the popup as usual, and we need to store it in the DOM
// using a data attribute.
'data-userId': knowedUser.userId,
- [ASIS]: 'true',
},
linkValue,
);
content = content.replace(fullMention, Blaze.toHTML(link));
}
+
return HTML.Raw(sanitizeXss(content));
}),
);
+
Template.viewer.events({
// Viewer sometimes have click-able wrapper around them (for instance to edit
// the corresponding text). Clicking a link shouldn't fire these actions, stop
@@ -395,10 +334,7 @@ Template.viewer.events({
Popup.open('member').call({ userId }, event, templateInstance);
} else {
const href = event.currentTarget.href;
- const child = event.currentTarget.firstElementChild;
- if (child && child.tagName === 'IMG') {
- prevent = false;
- } else if (href) {
+ if (href) {
window.open(href, '_blank');
}
}
diff --git a/client/components/main/header.jade b/client/components/main/header.jade
index 75e84c0c..de7ead93 100644
--- a/client/components/main/header.jade
+++ b/client/components/main/header.jade
@@ -24,6 +24,11 @@ template(name="header")
a(href="{{pathFor 'home'}}")
span.fa.fa-home
| {{_ 'all-boards'}}
+ li.separator -
+ li
+ a(href="{{pathFor 'public'}}")
+ span.fa.fa-globe
+ | {{_ 'public'}}
each currentUser.starredBoards
li.separator -
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
@@ -35,6 +40,8 @@ template(name="header")
a#header-new-board-icon.js-create-board
i.fa.fa-plus(title="Create a new board")
+ +notifications
+
+headerUserBar
#header(class=currentBoard.colorClass)
diff --git a/client/components/main/header.styl b/client/components/main/header.styl
index f77c2aca..d8093861 100644
--- a/client/components/main/header.styl
+++ b/client/components/main/header.styl
@@ -99,7 +99,7 @@
height: 28px
font-size: 12px
display: flex
- z-index: 17
+ z-index: 21
#header-user-bar,
#header-new-board-icon,
@@ -127,7 +127,7 @@
&.current
color: darken(white, 5%)
- &:first-child .fa-home
+ &:first-child .fa-home,&:nth-child(3) .fa-globe
margin-right: 5px
a.js-create-board
@@ -175,7 +175,7 @@
.board-header-btn
height: 32px
line-height: @height
- font-size: 16px
+ font-size: 15px
i.fa
line-height: 32px
diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade
index 9543c5c5..08dfc58c 100644
--- a/client/components/main/layouts.jade
+++ b/client/components/main/layouts.jade
@@ -6,10 +6,16 @@ head
where the application is deployed with a path prefix, but it seems to be
difficult to do that cleanly with Blaze -- at least without adding extra
packages.
- link(rel="shortcut icon" href="/wekan-favicon.png")
- link(rel="apple-touch-icon" href="/wekan-favicon.png")
- link(rel="mask-icon" href="/wekan-logo-150.svg")
- link(rel="manifest" href="/wekan-manifest.json")
+ link(rel="shortcut icon" type="image/x-icon" href="/favicon.ico")
+ link(rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png")
+ link(rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png")
+ link(rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png")
+ link(rel="manifest" href="/site.webmanifest")
+ link(rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5")
+ meta(name="apple-mobile-web-app-title" content="Wekan")
+ meta(name="application-name" content="Wekan")
+ meta(name="msapplication-TileColor" content="#00aba9")
+ meta(name="theme-color" content="#ffffff")
template(name="userFormsLayout")
section.auth-layout
diff --git a/client/components/main/layouts.js b/client/components/main/layouts.js
index ec4a35cc..83678e73 100644
--- a/client/components/main/layouts.js
+++ b/client/components/main/layouts.js
@@ -31,6 +31,11 @@ Template.userFormsLayout.onCreated(function() {
return this.stop();
},
});
+ Meteor.call('isPasswordLoginDisabled', (_, result) => {
+ if (result) {
+ $('.at-pwd-form').hide();
+ }
+ });
});
Template.userFormsLayout.onRendered(() => {
@@ -73,6 +78,8 @@ Template.userFormsLayout.helpers({
name = 'Igbo';
} else if (lang.name === 'oc') {
name = 'Occitan';
+ } else if (lang.name === '繁体中文(台湾)') {
+ name = '繁體中文(台灣)';
}
return { tag, name };
}).sort(function(a, b) {
diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl
index 023cba3d..b4815ca6 100644
--- a/client/components/main/popup.styl
+++ b/client/components/main/popup.styl
@@ -135,6 +135,10 @@ $popupWidth = 300px
margin-bottom: 8px
.pop-over-list
+ li
+ display: block
+ clear: both
+
li > a
clear: both
cursor: pointer
@@ -316,6 +320,7 @@ $popupWidth = 300px
input[type="file"]
margin: 4px 0 12px
width: 100%
+ box-sizing: border-box
.pop-over-list
li > a
diff --git a/client/components/notifications/notification.jade b/client/components/notifications/notification.jade
new file mode 100644
index 00000000..c98bbdba
--- /dev/null
+++ b/client/components/notifications/notification.jade
@@ -0,0 +1,10 @@
+template(name='notification')
+ li.notification(class="{{#if read}}read{{/if}}")
+ .read-status
+ .materialCheckBox(class="{{#if read}}is-checked{{/if}}")
+ +notificationIcon(activityData)
+ .details
+ +activity(activity=activityData mode='none')
+ if read
+ .remove
+ a.fa.fa-trash
diff --git a/client/components/notifications/notification.js b/client/components/notifications/notification.js
new file mode 100644
index 00000000..89277520
--- /dev/null
+++ b/client/components/notifications/notification.js
@@ -0,0 +1,28 @@
+Template.notification.events({
+ 'click .read-status .materialCheckBox'() {
+ const update = {};
+ update[`profile.notifications.${this.index}.read`] = this.read
+ ? null
+ : Date.now();
+ Users.update(Meteor.userId(), { $set: update });
+ },
+ 'click .remove a'() {
+ Meteor.user().removeNotification(this.activityData._id);
+ },
+});
+
+Template.notification.helpers({
+ mode: 'board',
+ isOfActivityType(activityId, type) {
+ const activity = Activities.findOne(activityId);
+ return activity && activity.activityType === type;
+ },
+ activityType(activityId) {
+ const activity = Activities.findOne(activityId);
+ return activity ? activity.activityType : '';
+ },
+ activityUser(activityId) {
+ const activity = Activities.findOne(activityId);
+ return activity && activity.userId;
+ },
+});
diff --git a/client/components/notifications/notification.styl b/client/components/notifications/notification.styl
new file mode 100644
index 00000000..0cf0cfd5
--- /dev/null
+++ b/client/components/notifications/notification.styl
@@ -0,0 +1,57 @@
+#notifications-drawer
+ &.show-read .notification.read
+ display: flex
+
+ .notification
+ display: flex
+ float: none
+ padding: 12px 8px 8px
+ color: black
+ border-bottom: 1px solid #dbdbdb
+
+ &.read
+ display: none
+
+ .read-status
+ width: 30px
+
+ input
+ width: 24px
+ height: 24px
+
+ .activity-type
+ margin: 16px 0 0
+ width: 17px
+ height: 17px
+ font-size: 17px
+ display: block
+ color: #bbb
+
+ .details
+ width: calc(100% - 30px)
+
+ .activity
+ display: flex
+
+ .activity-desc
+ width: 100%;
+
+ .activity-comment
+ display: block
+ width: 100%
+ border-radius: 3px
+ background: #fff
+ text-decoration: none
+ box-shadow: 0 1px 2px rgba(0,0,0,0.2)
+ margin-top: 5px
+ padding: 5px
+
+ .activity-meta
+ display: block
+ font-size: 0.8em
+ color: #999
+ font-style: italic
+
+ .remove
+ a:hover
+ color #eb4646 !important
diff --git a/client/components/notifications/notificationIcon.jade b/client/components/notifications/notificationIcon.jade
new file mode 100644
index 00000000..04377606
--- /dev/null
+++ b/client/components/notifications/notificationIcon.jade
@@ -0,0 +1,53 @@
+template(name='notificationIcon')
+ if($in activityType 'deleteAttachment' 'addAttachment')
+ i.fa.fa-paperclip.activity-type(title="attachment")
+ else if($in activityType 'createBoard' 'importBoard')
+ i.fa.fa-chalkboard.activity-type(title="board")
+
+ else if($in activityType 'createCard' 'importCard' 'moveCard')
+ +cardNotificationIcon
+ else if($in activityType 'moveCardBoard' 'archivedCard' 'restoredCard')
+ +cardNotificationIcon
+ //- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it
+ //- DRY and consistant
+
+ else if($in activityType 'addChecklist' 'removedChecklist' 'completeChecklist')
+ +checklistNotificationIcon
+ else if($in activityType 'uncompleteChecklist')
+ +checklistNotificationIcon
+ //- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it
+ //- DRY and consistant
+
+ else if($in activityType 'checkedItem' 'uncheckedItem' 'addChecklistItem' 'removedChecklistItem')
+ i.fa.fa-check-square.activity-type(title="checklist item")
+ else if($in activityType 'addComment')
+ i.fa.fa-comment-o.activity-type(title="comment")
+ else if($in activityType 'createCustomField' 'setCustomField' 'unsetCustomField')
+ i.fa.fa-code.activity-type(title="custom field")
+ else if($in activityType 'addedLabel' 'removedLabel')
+ i.fa.fa-tag.activity-type(title="label")
+
+ else if($in activityType 'createList' 'removeList' 'archivedList')
+ +listNotificationIcon
+ else if($in activityType 'importList')
+ +listNotificationIcon
+ //- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it
+ //- DRY and consistant
+
+ //- elswhere in the app we use fa-trello to indicate lists...
+ //- i personally like fa-columns a bit better
+ else if($in activityType 'unjoinMember' 'addBoardMember' 'joinMember' 'removeBoardMember')
+ i.fa.fa-user.activity-type(title="member")
+ else if($in activityType 'createSwimlane' 'archivedSwimlane')
+ i.fa.fa-th-large.activity-type(title="swimlane")
+ else
+ i.fa.fa-bug.activity-type(title="can't find icon for #{activityType}")
+
+template(name='cardNotificationIcon')
+ i.fa.fa-clone.activity-type(title="card")
+
+template(name='checklistNotificationIcon')
+ i.fa.fa-list.activity-type(title="checklist")
+
+template(name='listNotificationIcon')
+ i.fa.fa-columns.activity-type(title="list")
diff --git a/client/components/notifications/notifications.jade b/client/components/notifications/notifications.jade
new file mode 100644
index 00000000..bf8acbbf
--- /dev/null
+++ b/client/components/notifications/notifications.jade
@@ -0,0 +1,5 @@
+template(name='notifications')
+ #notifications.board-header-btns.right
+ a.notifications-drawer-toggle.fa.fa-bell(class="{{#if $gt unreadNotifications 0}}alert{{/if}}")
+ if $.Session.get 'showNotificationsDrawer'
+ +notificationsDrawer(unreadNotifications=unreadNotifications)
diff --git a/client/components/notifications/notifications.js b/client/components/notifications/notifications.js
new file mode 100644
index 00000000..c0aa6cb5
--- /dev/null
+++ b/client/components/notifications/notifications.js
@@ -0,0 +1,32 @@
+// this hides the notifications drawer if anyone clicks off of the panel
+Template.body.events({
+ click(event) {
+ if (
+ !$(event.target).is('#notifications *') &&
+ Session.get('showNotificationsDrawer')
+ ) {
+ toggleNotificationsDrawer();
+ }
+ },
+});
+
+Template.notifications.helpers({
+ unreadNotifications() {
+ const notifications = Users.findOne(Meteor.userId()).notifications();
+ const unreadNotifications = _.filter(notifications, v => !v.read);
+ return unreadNotifications.length;
+ },
+});
+
+Template.notifications.events({
+ 'click .notifications-drawer-toggle'() {
+ toggleNotificationsDrawer();
+ },
+});
+
+export function toggleNotificationsDrawer() {
+ Session.set(
+ 'showNotificationsDrawer',
+ !Session.get('showNotificationsDrawer'),
+ );
+}
diff --git a/client/components/notifications/notifications.styl b/client/components/notifications/notifications.styl
new file mode 100644
index 00000000..710cd3f9
--- /dev/null
+++ b/client/components/notifications/notifications.styl
@@ -0,0 +1,17 @@
+#notifications
+ position: relative
+
+ .notifications-drawer-toggle
+ display: block
+ line-height: 28px
+ color: #f2f2f2
+ margin: 0 10px
+ width: 28px
+ height: 28px
+ text-align: center
+ border: 0
+ padding: 0
+
+ &.alert
+ background-color: #eb4646;
+
diff --git a/client/components/notifications/notificationsDrawer.jade b/client/components/notifications/notificationsDrawer.jade
new file mode 100644
index 00000000..fee6aef6
--- /dev/null
+++ b/client/components/notifications/notificationsDrawer.jade
@@ -0,0 +1,20 @@
+template(name='notificationsDrawer')
+ section#notifications-drawer(class="{{#if $.Session.get 'showReadNotifications'}}show-read{{/if}}")
+ .header
+ if $.Session.get 'showReadNotifications'
+ a.toggle-read {{_ 'filter-by-unread'}}
+ else
+ a.toggle-read {{_ 'view-all'}}
+ h5 {{_ 'notifications'}}
+ if($gt unreadNotifications 0)
+ |(#{unreadNotifications})
+ a.fa.fa-times-thin.close
+ ul.notifications
+ each transformedProfile.notifications
+ +notification(activityData=activity index=dbIndex read=read)
+ if($gt unreadNotifications 0)
+ a.all-read {{_ 'mark-all-as-read'}}
+ if ($and ($.Session.get 'showReadNotifications') ($gt readNotifications 0))
+ a.remove-read
+ i.fa.fa-trash
+ | {{_ 'remove-all-read'}}
diff --git a/client/components/notifications/notificationsDrawer.js b/client/components/notifications/notificationsDrawer.js
new file mode 100644
index 00000000..76abeea7
--- /dev/null
+++ b/client/components/notifications/notificationsDrawer.js
@@ -0,0 +1,53 @@
+import { toggleNotificationsDrawer } from './notifications.js';
+
+Template.notificationsDrawer.onCreated(function() {
+ Meteor.subscribe('notificationActivities');
+ Meteor.subscribe('notificationCards');
+ Meteor.subscribe('notificationUsers');
+ Meteor.subscribe('notificationsAttachments');
+ Meteor.subscribe('notificationChecklistItems');
+ Meteor.subscribe('notificationChecklists');
+ Meteor.subscribe('notificationComments');
+ Meteor.subscribe('notificationLists');
+ Meteor.subscribe('notificationSwimlanes');
+});
+
+Template.notificationsDrawer.helpers({
+ transformedProfile() {
+ return Users.findOne(Meteor.userId());
+ },
+ readNotifications() {
+ const readNotifications = _.filter(
+ Meteor.user().profile.notifications,
+ v => !!v.read,
+ );
+ return readNotifications.length;
+ },
+});
+
+Template.notificationsDrawer.events({
+ 'click .all-read'() {
+ const notifications = Meteor.user().profile.notifications;
+ for (const index in notifications) {
+ if (notifications.hasOwnProperty(index) && !notifications[index].read) {
+ const update = {};
+ update[`profile.notifications.${index}.read`] = Date.now();
+ Users.update(Meteor.userId(), { $set: update });
+ }
+ }
+ },
+ 'click .close'() {
+ toggleNotificationsDrawer();
+ },
+ 'click .toggle-read'() {
+ Session.set('showReadNotifications', !Session.get('showReadNotifications'));
+ },
+ 'click .remove-read'() {
+ const user = Meteor.user();
+ for (const notification of user.profile.notifications) {
+ if (notification.read) {
+ user.removeNotification(notification.activity);
+ }
+ }
+ },
+});
diff --git a/client/components/notifications/notificationsDrawer.styl b/client/components/notifications/notificationsDrawer.styl
new file mode 100644
index 00000000..f99e1299
--- /dev/null
+++ b/client/components/notifications/notificationsDrawer.styl
@@ -0,0 +1,69 @@
+belize = #2980b9
+
+section#notifications-drawer
+ position: fixed
+ top: 28px
+ right: 0
+ width: 400px
+ background-color: #fafafa
+ box-shadow: 0 1px 2px rgba(0,0,0,0.15)
+ border-radius: 2px
+ max-height: calc(100vh - 28px - 36px)
+ color: black
+ padding-top 36px
+
+ a:hover
+ color: belize !important
+
+ .header
+ position: fixed
+ top 28px
+ right 0
+ width calc(400px - 32px)
+ padding: 8px 16px
+ background: #ededed
+ border-bottom: 1px solid #dbdbdb
+ z-index 2
+
+ .toggle-read
+ position absolute
+ left 16px
+ top calc(50% - 8px)
+ color belize
+
+ h5
+ text-align: center
+ margin: 0
+
+ .close
+ position: absolute
+ top: calc(50% - 12px)
+ right: 12px
+ font-size: 24px
+ height: 24px
+ line-height: 24px
+ opacity 1
+
+ .all-read,
+ .remove-read
+ color belize
+ background-color: #fafafa
+ margin 8px 16px 12px
+ display inline-block
+
+ .remove-read
+ float right
+
+ &:hover
+ color #eb4646 !important
+
+ i.fa
+ color inherit
+
+
+ ul.notifications
+ display: block
+ padding: 0px 16px
+ margin: 0
+ height: calc(100vh - 102px)
+ overflow-y: scroll
diff --git a/client/components/rules/actions/boardActions.js b/client/components/rules/actions/boardActions.js
index c2f2375a..02910cc1 100644
--- a/client/components/rules/actions/boardActions.js
+++ b/client/components/rules/actions/boardActions.js
@@ -11,7 +11,7 @@ BlazeComponent.extendComponent({
},
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
return boards;
diff --git a/client/components/settings/peopleBody.jade b/client/components/settings/peopleBody.jade
index b8a94337..fef1067e 100644
--- a/client/components/settings/peopleBody.jade
+++ b/client/components/settings/peopleBody.jade
@@ -40,9 +40,15 @@ template(name="peopleGeneral")
th {{_ 'active'}}
th {{_ 'authentication-method'}}
th
+ +newUserRow
each user in peopleList
+peopleRow(userId=user._id)
+template(name="newUserRow")
+ a.new-user
+ i.fa.fa-edit
+ | {{_ 'new'}}
+
template(name="peopleRow")
tr
if userData.loginDisabled
@@ -104,7 +110,7 @@ template(name="editUserPopup")
label.hide.userId(type="text" value=user._id)
label
| {{_ 'fullname'}}
- input.js-profile-fullname(type="text" value=user.profile.fullname autofocus)
+ input.js-profile-fullname(type="text" value=user.profile.fullname)
label
| {{_ 'username'}}
span.error.hide.username-taken
@@ -148,3 +154,49 @@ template(name="editUserPopup")
// div
// input#deleteButton.primary.wide(type="button" value="{{_ 'delete'}}")
+template(name="newUserPopup")
+ form
+ //label.hide.userId(type="text" value=user._id)
+ label
+ | {{_ 'fullname'}}
+ input.js-profile-fullname(type="text" value="")
+ label
+ | {{_ 'username'}}
+ span.error.hide.username-taken
+ | {{_ 'error-username-taken'}}
+ //if isLdap
+ // input.js-profile-username(type="text" value=user.username readonly)
+ //else
+ input.js-profile-username(type="text" value="")
+ label
+ | {{_ 'email'}}
+ span.error.hide.email-taken
+ | {{_ 'error-email-taken'}}
+ //if isLdap
+ // input.js-profile-email(type="email" value="{{user.emails.[0].address}}" readonly)
+ //else
+ input.js-profile-email(type="email" value="")
+ label
+ | {{_ 'admin'}}
+ select.select-role.js-profile-isadmin
+ option(value="false" selected="selected") {{_ 'no'}}
+ option(value="true") {{_ 'yes'}}
+ label
+ | {{_ 'active'}}
+ select.select-active.js-profile-isactive
+ option(value="false" selected="selected") {{_ 'yes'}}
+ option(value="true") {{_ 'no'}}
+ label
+ | {{_ 'authentication-type'}}
+ select.select-authenticationMethod.js-authenticationMethod
+ each authentications
+ if isSelected value
+ option(value="{{value}}" selected) {{_ value}}
+ else
+ option(value="{{value}}") {{_ value}}
+ hr
+ label
+ | {{_ 'password'}}
+ input.js-profile-password(type="password")
+ div.buttonsContainer
+ input.primary.wide(type="submit" value="{{_ 'save'}}")
diff --git a/client/components/settings/peopleBody.js b/client/components/settings/peopleBody.js
index 8610034e..186afd58 100644
--- a/client/components/settings/peopleBody.js
+++ b/client/components/settings/peopleBody.js
@@ -39,6 +39,9 @@ BlazeComponent.extendComponent({
this.filterPeople();
}
},
+ 'click #newUserButton'() {
+ Popup.open('newUser');
+ },
},
];
},
@@ -141,6 +144,47 @@ Template.editUserPopup.helpers({
},
});
+Template.newUserPopup.onCreated(function() {
+ this.authenticationMethods = new ReactiveVar([]);
+ this.errorMessage = new ReactiveVar('');
+
+ Meteor.call('getAuthenticationsEnabled', (_, result) => {
+ if (result) {
+ // TODO : add a management of different languages
+ // (ex {value: ldap, text: TAPi18n.__('ldap', {}, T9n.getLanguage() || 'en')})
+ this.authenticationMethods.set([
+ { value: 'password' },
+ // Gets only the authentication methods availables
+ ...Object.entries(result)
+ .filter(e => e[1])
+ .map(e => ({ value: e[0] })),
+ ]);
+ }
+ });
+});
+
+Template.newUserPopup.helpers({
+ //user() {
+ // return Users.findOne(this.userId);
+ //},
+ authentications() {
+ return Template.instance().authenticationMethods.get();
+ },
+ //isSelected(match) {
+ // const userId = Template.instance().data.userId;
+ // const selected = Users.findOne(userId).authenticationMethod;
+ // return selected === match;
+ //},
+ //isLdap() {
+ // const userId = Template.instance().data.userId;
+ // const selected = Users.findOne(userId).authenticationMethod;
+ // return selected === 'ldap';
+ //},
+ errorMessage() {
+ return Template.instance().errorMessage.get();
+ },
+});
+
BlazeComponent.extendComponent({
onCreated() {},
user() {
@@ -155,6 +199,16 @@ BlazeComponent.extendComponent({
},
}).register('peopleRow');
+BlazeComponent.extendComponent({
+ events() {
+ return [
+ {
+ 'click a.new-user': Popup.open('newUser'),
+ },
+ ];
+ },
+}).register('newUserRow');
+
Template.editUserPopup.events({
submit(event, templateInstance) {
event.preventDefault();
@@ -248,3 +302,44 @@ Template.editUserPopup.events({
Popup.close();
}),
});
+
+Template.newUserPopup.events({
+ submit(event, templateInstance) {
+ event.preventDefault();
+ const fullname = templateInstance.find('.js-profile-fullname').value.trim();
+ const username = templateInstance.find('.js-profile-username').value.trim();
+ const password = templateInstance.find('.js-profile-password').value;
+ const isAdmin = templateInstance.find('.js-profile-isadmin').value.trim();
+ const isActive = templateInstance.find('.js-profile-isactive').value.trim();
+ const email = templateInstance.find('.js-profile-email').value.trim();
+
+ Meteor.call(
+ 'setCreateUser',
+ fullname,
+ username,
+ password,
+ isAdmin,
+ isActive,
+ email.toLowerCase(),
+ function(error) {
+ const usernameMessageElement = templateInstance.$('.username-taken');
+ const emailMessageElement = templateInstance.$('.email-taken');
+ if (error) {
+ const errorElement = error.error;
+ if (errorElement === 'username-already-taken') {
+ usernameMessageElement.show();
+ emailMessageElement.hide();
+ } else if (errorElement === 'email-already-taken') {
+ usernameMessageElement.hide();
+ emailMessageElement.show();
+ }
+ } else {
+ usernameMessageElement.hide();
+ emailMessageElement.hide();
+ Popup.close();
+ }
+ },
+ );
+ Popup.close();
+ },
+});
diff --git a/client/components/settings/settingBody.js b/client/components/settings/settingBody.js
index 319c066b..62752084 100644
--- a/client/components/settings/settingBody.js
+++ b/client/components/settings/settingBody.js
@@ -48,7 +48,7 @@ BlazeComponent.extendComponent({
'members.isAdmin': true,
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
},
diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade
index ebcd8486..6bfedc9c 100644
--- a/client/components/sidebar/sidebar.jade
+++ b/client/components/sidebar/sidebar.jade
@@ -245,7 +245,7 @@ template(name="outgoingWebhooksPopup")
b &nbsp;
.materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}")
input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title)
- input.js-outgoing-webhooks-url(type="text" name="url" value=url autofocus)
+ input.js-outgoing-webhooks-url(type="text" name="url" value=url)
input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token")
select.js-outgoing-webhooks-type(name="type")
each _type in types
@@ -257,7 +257,7 @@ template(name="outgoingWebhooksPopup")
input(type="hidden" value=_id name="id")
input.primary.wide(type="submit" value="{{_ 'save'}}")
form.integration-form
- input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" autofocus)
+ input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title")
input.js-outgoing-webhooks-url(placeholder="{{_ 'URL' }}" type="text" name="url")
input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" name="token")
select.js-outgoing-webhooks-type(name="type")
@@ -267,7 +267,14 @@ template(name="outgoingWebhooksPopup")
template(name="boardMenuPopup")
ul.pop-over-list
- li: a.js-custom-fields {{_ 'custom-fields'}}
+ li
+ a.js-open-rules-view(title="{{_ 'rules'}}")
+ i.fa.fa-magic
+ | {{_ 'rules'}}
+ li
+ a.js-custom-fields
+ i.fa.fa-list-alt
+ | {{_ 'custom-fields'}}
li
a.js-open-archives
i.fa.fa-archive
@@ -291,10 +298,11 @@ template(name="boardMenuPopup")
if currentUser.isBoardAdmin
hr
ul.pop-over-list
- li
- a(href="{{exportUrl}}", download="{{exportFilename}}")
- i.fa.fa-share-alt
- | {{_ 'export-board'}}
+ if withApi
+ li
+ a(href="{{exportUrl}}", download="{{exportFilename}}")
+ i.fa.fa-share-alt
+ | {{_ 'export-board'}}
li
a.js-outgoing-webhooks
i.fa.fa-globe
@@ -319,11 +327,12 @@ template(name="boardMenuPopup")
if isSandstorm
hr
ul.pop-over-list
- li
- a(href="{{exportUrl}}", download="{{exportFilename}}")
- i.fa.fa-share-alt
- i.fa.fa-sign-out
- | {{_ 'export-board'}}
+ if withApi
+ li
+ a(href="{{exportUrl}}", download="{{exportFilename}}")
+ i.fa.fa-share-alt
+ i.fa.fa-sign-out
+ | {{_ 'export-board'}}
li
a.js-import-board
i.fa.fa-share-alt
diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js
index 8e640564..cbe00797 100644
--- a/client/components/sidebar/sidebar.js
+++ b/client/components/sidebar/sidebar.js
@@ -182,6 +182,10 @@ Template.memberPopup.helpers({
Template.boardMenuPopup.events({
'click .js-rename-board': Popup.open('boardChangeTitle'),
+ 'click .js-open-rules-view'() {
+ Modal.openWide('rulesMain');
+ Popup.close();
+ },
'click .js-custom-fields'() {
Sidebar.setView('customFields');
Popup.close();
@@ -211,7 +215,17 @@ Template.boardMenuPopup.events({
'click .js-card-settings': Popup.open('boardCardSettings'),
});
+Template.boardMenuPopup.onCreated(function() {
+ this.apiEnabled = new ReactiveVar(false);
+ Meteor.call('_isApiEnabled', (e, result) => {
+ this.apiEnabled.set(result);
+ });
+});
+
Template.boardMenuPopup.helpers({
+ withApi() {
+ return Template.instance().apiEnabled.get();
+ },
exportUrl() {
const params = {
boardId: Session.get('currentBoard'),
@@ -495,7 +509,7 @@ BlazeComponent.extendComponent({
'members.userId': Meteor.userId(),
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
},
@@ -673,7 +687,7 @@ BlazeComponent.extendComponent({
'members.userId': Meteor.userId(),
},
{
- sort: ['title'],
+ sort: { sort: 1 /* boards default sorting */ },
},
);
},
diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade
index 7f31dada..6d899b70 100644
--- a/client/components/sidebar/sidebarFilters.jade
+++ b/client/components/sidebar/sidebarFilters.jade
@@ -46,6 +46,24 @@ template(name="filterSidebar")
i.fa.fa-check
hr
ul.sidebar-list
+ li(class="{{#if Filter.assignees.isSelected undefined}}active{{/if}}")
+ a.name.js-toggle-assignee-filter
+ span.sidebar-list-item-description
+ | {{_ 'filter-no-assignee'}}
+ if Filter.assignees.isSelected undefined
+ i.fa.fa-check
+ each currentBoard.activeMembers
+ with getUser userId
+ li(class="{{#if Filter.assignees.isSelected _id}}active{{/if}}")
+ a.name.js-toggle-assignee-filter
+ +userAvatar(userId=this._id)
+ span.sidebar-list-item-description
+ = profile.fullname
+ | (<span class="username">{{ username }}</span>)
+ if Filter.assignees.isSelected _id
+ i.fa.fa-check
+ hr
+ ul.sidebar-list
li(class="{{#if Filter.customFields.isSelected undefined}}active{{/if}}")
a.name.js-toggle-custom-fields-filter
span.sidebar-list-item-description
diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js
index ee0176b9..0d402ab5 100644
--- a/client/components/sidebar/sidebarFilters.js
+++ b/client/components/sidebar/sidebarFilters.js
@@ -18,6 +18,11 @@ BlazeComponent.extendComponent({
Filter.members.toggle(this.currentData()._id);
Filter.resetExceptions();
},
+ 'click .js-toggle-assignee-filter'(evt) {
+ evt.preventDefault();
+ Filter.assignees.toggle(this.currentData()._id);
+ Filter.resetExceptions();
+ },
'click .js-toggle-archive-filter'(evt) {
evt.preventDefault();
Filter.archive.toggle(this.currentData()._id);
diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js
index b7a55ce6..753fa88b 100644
--- a/client/components/swimlanes/swimlanes.js
+++ b/client/components/swimlanes/swimlanes.js
@@ -1,6 +1,6 @@
import { Cookies } from 'meteor/ostrio:cookies';
const cookies = new Cookies();
-const { calculateIndex, enableClickOnTouch } = Utils;
+const { calculateIndex } = Utils;
function currentListIsInThisSwimlane(swimlaneId) {
const currentList = Lists.findOne(Session.get('currentList'));
@@ -87,9 +87,6 @@ function initSortable(boardComponent, $listsDom) {
},
});
- // ugly touch event hotfix
- enableClickOnTouch('.js-list:not(.js-list-composer)');
-
function userIsMember() {
return (
Meteor.user() &&
@@ -111,7 +108,7 @@ function initSortable(boardComponent, $listsDom) {
showDesktopDragHandles = false;
}
- if (!Utils.isMiniScreen() && showDesktopDragHandles) {
+ if (Utils.isMiniScreen() || showDesktopDragHandles) {
$listsDom.sortable({
handle: '.js-list-handle',
});
@@ -122,34 +119,12 @@ function initSortable(boardComponent, $listsDom) {
}
const $listDom = $listsDom;
- if ($listDom.data('sortable')) {
- $listsDom.sortable(
- 'option',
- 'disabled',
- // Disable drag-dropping when user is not member/is worker/is miniscreen
- !userIsMember(),
- // Not disable drag-dropping while in multi-selection mode
- // MultiSelection.isActive() || !userIsMember(),
- );
- }
-
- if ($listDom.data('sortable')) {
- $listsDom.sortable(
- 'option',
- 'disabled',
- // Disable drag-dropping when user is not member/is worker/is miniscreen
- Meteor.user().isWorker(),
- // Not disable drag-dropping while in multi-selection mode
- // MultiSelection.isActive() || !userIsMember(),
- );
- }
-
- if ($listDom.data('sortable')) {
+ if ($listDom.data('uiSortable') || $listDom.data('sortable')) {
$listsDom.sortable(
'option',
'disabled',
- // Disable drag-dropping when user is not member/is worker/is miniscreen
- Utils.isMiniScreen(),
+ // Disable drag-dropping when user is not member/is worker
+ !userIsMember() || Meteor.user().isWorker(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !userIsMember(),
);
@@ -210,8 +185,7 @@ BlazeComponent.extendComponent({
}
const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
- Utils.isMiniScreen() ||
- (!Utils.isMiniScreen() && showDesktopDragHandles)
+ Utils.isMiniScreen() || showDesktopDragHandles
? ['.js-list-handle', '.js-swimlane-header-handle']
: ['.js-list-header'],
);
diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade
index 9306d21d..d0adf29d 100644
--- a/client/components/users/userHeader.jade
+++ b/client/components/users/userHeader.jade
@@ -98,12 +98,12 @@ template(name="changeLanguagePopup")
template(name="changeSettingsPopup")
ul.pop-over-list
- li
- a.js-toggle-system-messages
- i.fa.fa-comments-o
- | {{_ 'hide-system-messages'}}
- if hiddenSystemMessages
- i.fa.fa-check
+ //li
+ // a.js-toggle-system-messages
+ // i.fa.fa-comments-o
+ // | {{_ 'hide-system-messages'}}
+ // if hiddenSystemMessages
+ // i.fa.fa-check
li
a.js-toggle-desktop-drag-handles
i.fa.fa-arrows
@@ -112,11 +112,20 @@ template(name="changeSettingsPopup")
i.fa.fa-check
unless currentUser.isWorker
li
- label.bold
+ label.bold.clear
i.fa.fa-sort-numeric-asc
| {{_ 'show-cards-minimum-count'}}
input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="0" max="99" onkeydown="return false")
- input.js-apply-show-cards-at.left(type="submit" value="{{_ 'apply'}}")
+ label.bold.clear
+ i.fa.fa-calendar
+ | {{_ 'start-day-of-week'}}
+ select#start-day-of-week.inline-input.left
+ each day in weekDays startDayOfWeek
+ if day.isSelected
+ option(selected="true", value="#{day.value}") #{day.name}
+ else
+ option(value="#{day.value}") #{day.name}
+ input.js-apply-user-settings.left(type="submit" value="{{_ 'apply'}}")
template(name="userDeletePopup")
unless currentUser.isWorker
diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js
index 847d30fb..b7bb284e 100644
--- a/client/components/users/userHeader.js
+++ b/client/components/users/userHeader.js
@@ -166,6 +166,8 @@ Template.changeLanguagePopup.helpers({
name = 'Igbo';
} else if (lang.name === 'oc') {
name = 'Occitan';
+ } else if (lang.name === '繁体中文(台湾)') {
+ name = '繁體中文(台灣)';
}
return { tag, name };
}).sort(function(a, b) {
@@ -222,6 +224,27 @@ Template.changeSettingsPopup.helpers({
return cookies.get('limitToShowCardsCount');
}
},
+ weekDays(startDay) {
+ return [
+ TAPi18n.__('sunday'),
+ TAPi18n.__('monday'),
+ TAPi18n.__('tuesday'),
+ TAPi18n.__('wednesday'),
+ TAPi18n.__('thursday'),
+ TAPi18n.__('friday'),
+ TAPi18n.__('saturday'),
+ ].map(function(day, index) {
+ return { name: day, value: index, isSelected: index === startDay };
+ });
+ },
+ startDayOfWeek() {
+ currentUser = Meteor.user();
+ if (currentUser) {
+ return currentUser.getStartDayOfWeek();
+ } else {
+ return cookies.get('startDayOfWeek');
+ }
+ },
});
Template.changeSettingsPopup.events({
@@ -245,20 +268,31 @@ Template.changeSettingsPopup.events({
cookies.set('hasHiddenSystemMessages', 'true');
}
},
- 'click .js-apply-show-cards-at'(event, templateInstance) {
+ 'click .js-apply-user-settings'(event, templateInstance) {
event.preventDefault();
const minLimit = parseInt(
templateInstance.$('#show-cards-count-at').val(),
10,
);
+ const startDay = parseInt(
+ templateInstance.$('#start-day-of-week').val(),
+ 10,
+ );
+ const currentUser = Meteor.user();
if (!isNaN(minLimit)) {
- currentUser = Meteor.user();
if (currentUser) {
Meteor.call('changeLimitToShowCardsCount', minLimit);
} else {
cookies.set('limitToShowCardsCount', minLimit);
}
- Popup.back();
}
+ if (!isNaN(startDay)) {
+ if (currentUser) {
+ Meteor.call('changeStartDayOfWeek', startDay);
+ } else {
+ cookies.set('startDayOfWeek', startDay);
+ }
+ }
+ Popup.back();
},
});
diff --git a/client/lib/datepicker.js b/client/lib/datepicker.js
index 8ad66c5f..aa05310c 100644
--- a/client/lib/datepicker.js
+++ b/client/lib/datepicker.js
@@ -10,12 +10,22 @@ DatePicker = BlazeComponent.extendComponent({
this.defaultTime = defaultTime;
},
+ startDayOfWeek() {
+ const currentUser = Meteor.user();
+ if (currentUser) {
+ return currentUser.getStartDayOfWeek();
+ } else {
+ return 1;
+ }
+ },
+
onRendered() {
const $picker = this.$('.js-datepicker')
.datepicker({
todayHighlight: true,
todayBtn: 'linked',
language: TAPi18n.getLanguage(),
+ weekStart: this.startDayOfWeek(),
})
.on(
'changeDate',
diff --git a/client/lib/filter.js b/client/lib/filter.js
index 592eb4ab..24ca320b 100644
--- a/client/lib/filter.js
+++ b/client/lib/filter.js
@@ -459,13 +459,21 @@ Filter = {
// before changing the schema.
labelIds: new SetFilter(),
members: new SetFilter(),
+ assignees: new SetFilter(),
archive: new SetFilter(),
hideEmpty: new SetFilter(),
customFields: new SetFilter('_id'),
advanced: new AdvancedFilter(),
lists: new AdvancedFilter(), // we need the ability to filter list by name as well
- _fields: ['labelIds', 'members', 'archive', 'hideEmpty', 'customFields'],
+ _fields: [
+ 'labelIds',
+ 'members',
+ 'assignees',
+ 'archive',
+ 'hideEmpty',
+ 'customFields',
+ ],
// We don't filter cards that have been added after the last filter change. To
// implement this we keep the id of these cards in this `_exceptions` fields
diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js
index da33f806..e861e416 100755
--- a/client/lib/keyboard.js
+++ b/client/lib/keyboard.js
@@ -1,6 +1,16 @@
// XXX There is no reason to define these shortcuts globally, they should be
// attached to a template (most of them will go in the `board` template).
+function getHoveredCardId() {
+ const card = $('.js-minicard:hover').get(0);
+ if (!card) return null;
+ return Blaze.getData(card)._id;
+}
+
+function getSelectedCardId() {
+ return Session.get('selectedCard') || getHoveredCardId();
+}
+
Mousetrap.bind('?', () => {
FlowRouter.go('shortcuts');
});
@@ -50,9 +60,9 @@ Mousetrap.bind(['down', 'up'], (evt, key) => {
}
});
-// XXX This shortcut should also work when hovering over a card in board view
Mousetrap.bind('space', evt => {
- if (!Session.get('currentCard')) {
+ const cardId = getSelectedCardId();
+ if (!cardId) {
return;
}
@@ -62,7 +72,7 @@ Mousetrap.bind('space', evt => {
}
if (Meteor.user().isBoardMember()) {
- const card = Cards.findOne(Session.get('currentCard'));
+ const card = Cards.findOne(cardId);
card.toggleMember(currentUserId);
// We should prevent scrolling in card when spacebar is clicked
// This should do it according to Mousetrap docs, but it doesn't
@@ -70,9 +80,9 @@ Mousetrap.bind('space', evt => {
}
});
-// XXX This shortcut should also work when hovering over a card in board view
Mousetrap.bind('c', evt => {
- if (!Session.get('currentCard')) {
+ const cardId = getSelectedCardId();
+ if (!cardId) {
return;
}
@@ -86,7 +96,7 @@ Mousetrap.bind('c', evt => {
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
) {
- const card = Cards.findOne(Session.get('currentCard'));
+ const card = Cards.findOne(cardId);
card.archive();
// We should prevent scrolling in card when spacebar is clicked
// This should do it according to Mousetrap docs, but it doesn't
@@ -97,19 +107,19 @@ Mousetrap.bind('c', evt => {
Template.keyboardShortcuts.helpers({
mapping: [
{
- keys: ['W'],
+ keys: ['w'],
action: 'shortcut-toggle-sidebar',
},
{
- keys: ['Q'],
+ keys: ['q'],
action: 'shortcut-filter-my-cards',
},
{
- keys: ['F'],
+ keys: ['f'],
action: 'shortcut-toggle-filterbar',
},
{
- keys: ['X'],
+ keys: ['x'],
action: 'shortcut-clear-filters',
},
{
@@ -129,7 +139,7 @@ Template.keyboardShortcuts.helpers({
action: 'shortcut-assign-self',
},
{
- keys: ['C'],
+ keys: ['c'],
action: 'archive-card',
},
],