summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
authorRomulus Tsai 蔡仲明 <urakagi@gmail.com>2020-05-14 16:47:05 +0800
committerRomulus Tsai 蔡仲明 <urakagi@gmail.com>2020-05-14 16:47:05 +0800
commit0735981366047dd9ac41defe5e9fc5f9c4d7e913 (patch)
treeabf05428dc79dba2d3dd82a4f5c33c0becfd507d /client
parent09ce3e464fd609b3ecc8bec5263ab06093c3a442 (diff)
parent5d8cca40d217b6a3895f1f6eb154b6aba9576b37 (diff)
downloadwekan-0735981366047dd9ac41defe5e9fc5f9c4d7e913.tar.gz
wekan-0735981366047dd9ac41defe5e9fc5f9c4d7e913.tar.bz2
wekan-0735981366047dd9ac41defe5e9fc5f9c4d7e913.zip
Merge branch 'master' into lib-change
Diffstat (limited to 'client')
-rw-r--r--client/components/boards/boardsList.js3
-rw-r--r--client/components/cards/cardDate.js27
-rw-r--r--client/components/cards/cardDetails.jade81
-rw-r--r--client/components/cards/cardDetails.js120
-rw-r--r--client/components/cards/cardDetails.styl10
-rw-r--r--client/components/cards/minicard.jade2
-rw-r--r--client/components/import/csvMembersMapper.js37
-rw-r--r--client/components/import/import.jade2
-rw-r--r--client/components/import/import.js52
-rw-r--r--client/components/lists/listBody.js9
-rw-r--r--client/components/lists/listHeader.js31
-rwxr-xr-xclient/components/main/editor.js2
-rw-r--r--client/components/sidebar/sidebar.jade19
-rw-r--r--client/components/sidebar/sidebar.js58
-rw-r--r--client/components/users/userAvatar.styl2
15 files changed, 377 insertions, 78 deletions
diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js
index 9208fdb2..b99c0c31 100644
--- a/client/components/boards/boardsList.js
+++ b/client/components/boards/boardsList.js
@@ -25,7 +25,6 @@ BlazeComponent.extendComponent({
},
onRendered() {
- const self = this;
function userIsAllowedToMove() {
return Meteor.user();
}
@@ -78,7 +77,7 @@ BlazeComponent.extendComponent({
},
boards() {
- let query = {
+ const query = {
archived: false,
type: 'board',
};
diff --git a/client/components/cards/cardDate.js b/client/components/cards/cardDate.js
index c4b5c6d8..9b2268e9 100644
--- a/client/components/cards/cardDate.js
+++ b/client/components/cards/cardDate.js
@@ -386,3 +386,30 @@ CardEndDate.register('cardEndDate');
return this.date.get().format('l');
}
}.register('minicardEndDate'));
+
+class VoteEndDate extends CardDate {
+ onCreated() {
+ super.onCreated();
+ const self = this;
+ self.autorun(() => {
+ self.date.set(moment(self.data().getVoteEnd()));
+ });
+ }
+ classes() {
+ const classes = 'end-date' + ' ';
+ return classes;
+ }
+ showDate() {
+ return this.date.get().format('l LT');
+ }
+ showTitle() {
+ return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
+ }
+
+ events() {
+ return super.events().concat({
+ 'click .js-edit-date': Popup.open('editVoteEndDate'),
+ });
+ }
+}
+VoteEndDate.register('voteEndDate');
diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade
index ae97e0e9..2aa77627 100644
--- a/client/components/cards/cardDetails.jade
+++ b/client/components/cards/cardDetails.jade
@@ -202,9 +202,12 @@ template(name="cardDetails")
if getVoteQuestion
hr
.vote-title
- h3
- i.fa.fa-thumbs-up
- card-details-item-title {{_ 'vote-question'}}
+ div.flex
+ h3
+ i.fa.fa-thumbs-up
+ | {{_ 'vote-question'}}
+ if getVoteEnd
+ +voteEndDate
.vote-result
if votePublic
a.card-label.card-label-green.js-show-positive-votes {{ voteCountPositive }}
@@ -212,10 +215,13 @@ template(name="cardDetails")
else
.card-label.card-label-green {{ voteCountPositive }}
.card-label.card-label-red {{ voteCountNegative }}
+ unless ($and currentBoard.isPublic voteAllowNonBoardMembers )
+ .card-label.card-label-gray {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
+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'}}
+ if showVotingButtons
+ 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
@@ -333,16 +339,10 @@ 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-start-voting
+ i.fa.fa-thumbs-up
+ | {{_ 'card-edit-voting'}}
li
a.js-custom-fields
i.fa.fa-list-alt
@@ -465,14 +465,14 @@ template(name="cardAssigneesPopup")
i.fa.fa-check
if currentUser.isWorker
ul.pop-over-list.js-card-assignee-list
- li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
- a.name.js-select-assignee(href="#")
- +userAvatar(userId=currentUser._id)
- span.full-name
- = currentUser.profile.fullname
- | (<span class="username">{{ currentUser.username }}</span>)
- if currentUser.isCardAssignee
- i.fa.fa-check
+ li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
+ a.name.js-select-assignee(href="#")
+ +userAvatar(userId=currentUser._id)
+ span.full-name
+ = currentUser.profile.fullname
+ | (<span class="username">{{ currentUser.username }}</span>)
+ if currentUser.isCardAssignee
+ i.fa.fa-check
template(name="userAvatarAssignee")
a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})")
@@ -519,7 +519,7 @@ template(name="cardMorePopup")
= ' '
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
input.inline-input(type="text" id="cardURL" readonly value="{{ absoluteUrl }}" autofocus="autofocus")
- button.js-copy-card-link-to-clipboard(class="btn") {{_ 'copy-card-link-to-clipboard'}}
+ button.js-copy-card-link-to-clipboard(class="btn" id="clipboard") {{_ 'copy-card-link-to-clipboard'}}
span.clearfix
br
h2 {{_ 'change-card-parent'}}
@@ -564,20 +564,39 @@ template(name="setCardColorPopup")
template(name="cardDeletePopup")
p {{_ "card-delete-pop"}}
unless archived
- p {{_ "card-delete-suggest-archive"}}
+ p {{_ "card-delete-suggest-archive"}}
+ button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+
+template(name="deleteVotePopup")
+ p {{_ "vote-delete-pop"}}
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")
+ input.js-vote-field#vote(type="text" name="vote" value="{{getVoteQuestion}}" autofocus disabled="{{#if getVoteQuestion}}disabled{{/if}}")
+ .check-div
+ a.flex(class="{{#if getVoteQuestion}}is-disabled{{else}}js-toggle-vote-allow-non-members{{/if}}")
+ .materialCheckBox#vote-allow-non-members(name="vote-allow-non-members" class="{{#if voteAllowNonBoardMembers}}is-checked{{/if}}")
+ span {{_ 'allowNonBoardMembers'}}
+ .check-div
+ a.flex(class="{{#if getVoteQuestion}}is-disabled{{else}}js-toggle-vote-public{{/if}}")
+ .materialCheckBox#vote-public(name="vote-public" class="{{#if votePublic}}is-checked{{/if}}")
+ span {{_ 'vote-public'}}
+ .check-div.flex
+ i.fa.fa-hourglass-end
+ a.js-end-date
+ span
+ | {{_ 'card-end'}}
+ unless getVoteEnd
+ i.fa.fa-plus
+ if getVoteEnd
+ +voteEndDate
- button.primary.confirm.js-submit {{_ 'save'}}
- //- button.js-remove-color.negate.wide.right {{_ 'delete'}}
+ button.primary.js-submit {{_ 'save'}}
+ if getVoteQuestion
+ button.js-remove-vote.negate.wide.right {{_ 'delete'}}
template(name="positiveVoteMembersPopup")
ul.pop-over-list.js-card-member-list
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index 271fbe2f..441068b0 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -54,21 +54,6 @@ BlazeComponent.extendComponent({
}
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());
@@ -148,6 +133,15 @@ BlazeComponent.extendComponent({
return result;
},
+ showVotingButtons() {
+ const card = this.currentData();
+ return (
+ (currentUser.isBoardMember() ||
+ (currentUser && card.voteAllowNonBoardMembers())) &&
+ !card.expiredVote()
+ );
+ },
+
onRendered() {
if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) {
// Send Webhook but not create Activities records ---
@@ -611,11 +605,6 @@ 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(
@@ -967,7 +956,23 @@ BlazeComponent.extendComponent({
},
'click .js-delete': Popup.afterConfirm('cardDelete', function() {
Popup.close();
- Cards.remove(this._id);
+ // verify that there are no linked cards
+ if (Cards.find({ linkedId: this._id }).count() === 0) {
+ Cards.remove(this._id);
+ } else {
+ // TODO: Maybe later we can list where the linked cards are.
+ // Now here is popup with a hint that the card cannot be deleted
+ // as there are linked cards.
+ // Related:
+ // client/components/lists/listHeader.js about line 248
+ // https://github.com/wekan/wekan/issues/2785
+ const message = `${TAPi18n.__(
+ 'delete-linked-card-before-this-card',
+ )} linkedId: ${
+ this._id
+ } at client/components/cards/cardDetails.js and https://github.com/wekan/wekan/issues/2785`;
+ alert(message);
+ }
Utils.goBoardId(this.boardId);
}),
'change .js-field-parent-board'(event) {
@@ -1000,22 +1005,93 @@ BlazeComponent.extendComponent({
events() {
return [
{
+ 'click .js-end-date': Popup.open('editVoteEndDate'),
'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);
+ const allowNonBoardMembers = $('#vote-allow-non-members').hasClass(
+ 'is-checked',
+ );
+ const endString = this.currentCard.getVoteEnd();
+
+ this.currentCard.setVoteQuestion(
+ voteQuestion,
+ publicVote,
+ allowNonBoardMembers,
+ );
+ if (endString) {
+ this.currentCard.setVoteEnd(endString);
+ }
Popup.close();
},
+ 'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
+ event.preventDefault();
+ this.currentCard.unsetVote();
+ Popup.close();
+ }),
'click a.js-toggle-vote-public'(event) {
event.preventDefault();
$('#vote-public').toggleClass('is-checked');
},
+ 'click a.js-toggle-vote-allow-non-members'(event) {
+ event.preventDefault();
+ $('#vote-allow-non-members').toggleClass('is-checked');
+ },
},
];
},
}).register('cardStartVotingPopup');
+// editVoteEndDatePopup
+(class extends DatePicker {
+ onCreated() {
+ super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
+ this.data().getVoteEnd() && this.date.set(moment(this.data().getVoteEnd()));
+ }
+ events() {
+ return [
+ {
+ 'submit .edit-date'(evt) {
+ evt.preventDefault();
+
+ // if no time was given, init with 12:00
+ const time =
+ evt.target.time.value ||
+ moment(new Date().setHours(12, 0, 0)).format('LT');
+
+ const dateString = `${evt.target.date.value} ${time}`;
+ const newDate = moment(dateString, 'L LT', true);
+ if (newDate.isValid()) {
+ // if active vote - store it
+ if (this.currentData().getVoteQuestion()) {
+ this._storeDate(newDate.toDate());
+ Popup.close();
+ } else {
+ this.currentData().vote = { end: newDate.toDate() }; // set vote end temp
+ Popup.back();
+ }
+ } else {
+ this.error.set('invalid-date');
+ evt.target.date.focus();
+ }
+ },
+ 'click .js-delete-date'(evt) {
+ evt.preventDefault();
+ this._deleteDate();
+ Popup.close();
+ },
+ },
+ ];
+ }
+ _storeDate(newDate) {
+ this.card.setVoteEnd(newDate);
+ }
+ _deleteDate() {
+ this.card.unsetVoteEnd();
+ }
+}.register('editVoteEndDatePopup'));
+
// 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 3e2beadd..12d0dda9 100644
--- a/client/components/cards/cardDetails.styl
+++ b/client/components/cards/cardDetails.styl
@@ -10,6 +10,9 @@ avatar-radius = 50%
left: -2000px
top: 0px
+#clipboard
+ white-space: normal
+
.assignee
border-radius: 3px
display: block
@@ -37,6 +40,8 @@ avatar-radius = 50%
position: absolute
&.avatar-image
+ object-fit: cover;
+ object-position: center;
height: 100%
width: @height
@@ -337,6 +342,11 @@ card-details-color(background, color...)
.vote-title
display: flex
justify-content: space-between
+
+ .js-edit-date
+ align-self: baseline
+ margin-left: 5px
+
.vote-result
display: flex
.js-show-positive-votes
diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade
index e8efc6ac..e9de2fea 100644
--- a/client/components/cards/minicard.jade
+++ b/client/components/cards/minicard.jade
@@ -103,7 +103,9 @@ template(name="minicard")
if getVoteQuestion
.badge.badge-state-image-only(title=getVoteQuestion)
span.badge-icon.fa.fa-thumbs-up
+ span.badge-text {{ voteCountPositive }}
span.badge-icon.fa.fa-thumbs-down
+ span.badge-text {{ voteCountNegative }}
if attachments.count
.badge
span.badge-icon.fa.fa-paperclip
diff --git a/client/components/import/csvMembersMapper.js b/client/components/import/csvMembersMapper.js
new file mode 100644
index 00000000..cf8d5837
--- /dev/null
+++ b/client/components/import/csvMembersMapper.js
@@ -0,0 +1,37 @@
+export function getMembersToMap(data) {
+ // we will work on the list itself (an ordered array of objects) when a
+ // mapping is done, we add a 'wekan' field to the object representing the
+ // imported member
+
+ const membersToMap = [];
+ const importedMembers = [];
+ let membersIndex;
+
+ for (let i = 0; i < data[0].length; i++) {
+ if (data[0][i].toLowerCase() === 'members') {
+ membersIndex = i;
+ }
+ }
+
+ for (let i = 1; i < data.length; i++) {
+ if (data[i][membersIndex]) {
+ for (const importedMember of data[i][membersIndex].split(' ')) {
+ if (importedMember && importedMembers.indexOf(importedMember) === -1) {
+ importedMembers.push(importedMember);
+ }
+ }
+ }
+ }
+
+ for (let importedMember of importedMembers) {
+ importedMember = {
+ username: importedMember,
+ id: importedMember,
+ };
+ const wekanUser = Users.findOne({ username: importedMember.username });
+ if (wekanUser) importedMember.wekanId = wekanUser._id;
+ membersToMap.push(importedMember);
+ }
+
+ return membersToMap;
+}
diff --git a/client/components/import/import.jade b/client/components/import/import.jade
index 1551a7dd..2bea24ae 100644
--- a/client/components/import/import.jade
+++ b/client/components/import/import.jade
@@ -13,7 +13,7 @@ template(name="import")
template(name="importTextarea")
form
p: label(for='import-textarea') {{_ instruction}} {{_ 'import-board-instruction-about-errors'}}
- textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
+ textarea.js-import-json(placeholder="{{_ importPlaceHolder}}" autofocus)
| {{jsonText}}
input.primary.wide(type="submit" value="{{_ 'import'}}")
diff --git a/client/components/import/import.js b/client/components/import/import.js
index 6368885b..673900fd 100644
--- a/client/components/import/import.js
+++ b/client/components/import/import.js
@@ -1,5 +1,8 @@
import trelloMembersMapper from './trelloMembersMapper';
import wekanMembersMapper from './wekanMembersMapper';
+import csvMembersMapper from './csvMembersMapper';
+
+const Papa = require('papaparse');
BlazeComponent.extendComponent({
title() {
@@ -30,20 +33,30 @@ BlazeComponent.extendComponent({
}
},
- importData(evt) {
+ importData(evt, dataSource) {
evt.preventDefault();
- const dataJson = this.find('.js-import-json').value;
- try {
- const dataObject = JSON.parse(dataJson);
- this.setError('');
- this.importedData.set(dataObject);
- const membersToMap = this._prepareAdditionalData(dataObject);
- // store members data and mapping in Session
- // (we go deep and 2-way, so storing in data context is not a viable option)
+ const input = this.find('.js-import-json').value;
+ if (dataSource === 'csv') {
+ const csv = input.indexOf('\t') > 0 ? input.replace(/(\t)/g, ',') : input;
+ const ret = Papa.parse(csv);
+ if (ret && ret.data && ret.data.length) this.importedData.set(ret.data);
+ else throw new Meteor.Error('error-csv-schema');
+ const membersToMap = this._prepareAdditionalData(ret.data);
this.membersToMap.set(membersToMap);
this.nextStep();
- } catch (e) {
- this.setError('error-json-malformed');
+ } else {
+ try {
+ const dataObject = JSON.parse(input);
+ this.setError('');
+ this.importedData.set(dataObject);
+ const membersToMap = this._prepareAdditionalData(dataObject);
+ // store members data and mapping in Session
+ // (we go deep and 2-way, so storing in data context is not a viable option)
+ this.membersToMap.set(membersToMap);
+ this.nextStep();
+ } catch (e) {
+ this.setError('error-json-malformed');
+ }
}
},
@@ -91,6 +104,9 @@ BlazeComponent.extendComponent({
case 'wekan':
membersToMap = wekanMembersMapper.getMembersToMap(dataObject);
break;
+ case 'csv':
+ membersToMap = csvMembersMapper.getMembersToMap(dataObject);
+ break;
}
return membersToMap;
},
@@ -109,11 +125,23 @@ BlazeComponent.extendComponent({
return `import-board-instruction-${Session.get('importSource')}`;
},
+ importPlaceHolder() {
+ const importSource = Session.get('importSource');
+ if (importSource === 'csv') {
+ return 'import-csv-placeholder';
+ } else {
+ return 'import-json-placeholder';
+ }
+ },
+
events() {
return [
{
submit(evt) {
- return this.parentComponent().importData(evt);
+ return this.parentComponent().importData(
+ evt,
+ Session.get('importSource'),
+ );
},
},
];
diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js
index 88f88db0..2d913aa9 100644
--- a/client/components/lists/listBody.js
+++ b/client/components/lists/listBody.js
@@ -77,7 +77,7 @@ BlazeComponent.extendComponent({
else if (
Utils.boardView() === 'board-view-lists' ||
Utils.boardView() === 'board-view-cal' ||
- !Utils.boardView
+ !Utils.boardView()
)
swimlaneId = board.getDefaultSwimline()._id;
@@ -658,10 +658,7 @@ BlazeComponent.extendComponent({
_id = element.copy(this.boardId, this.swimlaneId, this.listId);
// 1.B Linked card
} else {
- delete element._id;
- element.type = 'cardType-linkedCard';
- element.linkedId = element.linkedId || element._id;
- _id = Cards.insert(element);
+ _id = element.link(this.boardId, this.swimlaneId, this.listId);
}
Filter.addException(_id);
// List insertion
@@ -675,7 +672,7 @@ BlazeComponent.extendComponent({
element.sort = Boards.findOne(this.boardId)
.swimlanes()
.count();
- element.type = 'swimlalne';
+ element.type = 'swimlane';
_id = element.copy(this.boardId);
} else if (this.isBoardTemplateSearch) {
board = Boards.findOne(element.linkedId);
diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js
index 46dbd748..7cd4309f 100644
--- a/client/components/lists/listHeader.js
+++ b/client/components/lists/listHeader.js
@@ -223,8 +223,35 @@ BlazeComponent.extendComponent({
Template.listMorePopup.events({
'click .js-delete': Popup.afterConfirm('listDelete', function() {
Popup.close();
- this.allCards().map(card => Cards.remove(card._id));
- Lists.remove(this._id);
+ // TODO how can we avoid the fetch call?
+ const allCards = this.allCards().fetch();
+ const allCardIds = _.pluck(allCards, '_id');
+ // it's okay if the linked cards are on the same list
+ if (
+ Cards.find({
+ $and: [
+ { listId: { $ne: this._id } },
+ { linkedId: { $in: allCardIds } },
+ ],
+ }).count() === 0
+ ) {
+ allCardIds.map(_id => Cards.remove(_id));
+ Lists.remove(this._id);
+ } else {
+ // TODO: Figure out more informative message.
+ // Popup with a hint that the list cannot be deleted as there are
+ // linked cards. We can adapt the query above so we can list the linked
+ // cards.
+ // Related:
+ // client/components/cards/cardDetails.js about line 969
+ // https://github.com/wekan/wekan/issues/2785
+ const message = `${TAPi18n.__(
+ 'delete-linked-cards-before-this-list',
+ )} linkedId: ${
+ this._id
+ } at client/components/lists/listHeader.js and https://github.com/wekan/wekan/issues/2785`;
+ alert(message);
+ }
Utils.goBoardId(this.boardId);
}),
});
diff --git a/client/components/main/editor.js b/client/components/main/editor.js
index de598da8..abe4160f 100755
--- a/client/components/main/editor.js
+++ b/client/components/main/editor.js
@@ -328,7 +328,7 @@ Template.viewer.events({
// the corresponding text). Clicking a link shouldn't fire these actions, stop
// we stop these event at the viewer component level.
'click a'(event, templateInstance) {
- let prevent = true;
+ const prevent = true;
const userId = event.currentTarget.dataset.userid;
if (userId) {
Popup.open('member').call({ userId }, event, templateInstance);
diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade
index 6bfedc9c..280eaeaf 100644
--- a/client/components/sidebar/sidebar.jade
+++ b/client/components/sidebar/sidebar.jade
@@ -230,6 +230,8 @@ template(name="chooseBoardSource")
a(href="{{pathFor '/import/trello'}}") {{_ 'from-trello'}}
li
a(href="{{pathFor '/import/wekan'}}") {{_ 'from-wekan'}}
+ li
+ a(href="{{pathFor '/import/csv'}}") {{_ 'from-csv'}}
template(name="archiveBoardPopup")
p {{_ 'close-board-pop'}}
@@ -300,7 +302,7 @@ template(name="boardMenuPopup")
ul.pop-over-list
if withApi
li
- a(href="{{exportUrl}}", download="{{exportFilename}}")
+ a.js-export-board
i.fa.fa-share-alt
| {{_ 'export-board'}}
li
@@ -360,6 +362,21 @@ template(name="boardMenuPopup")
i.fa.fa-sitemap
| {{_ 'subtask-settings'}}
+template(name="exportBoard")
+ ul.pop-over-list
+ li
+ a(href="{{exportUrl}}", download="{{exportJsonFilename}}")
+ i.fa.fa-share-alt
+ | {{_ 'export-board-json'}}
+ li
+ a(href="{{exportCsvUrl}}", download="{{exportCsvFilename}}")
+ i.fa.fa-share-alt
+ | {{_ 'export-board-csv'}}
+ li
+ a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}")
+ i.fa.fa-share-alt
+ | {{_ 'export-board-tsv'}}
+
template(name="labelsWidget")
.board-widget.board-widget-labels
h3
diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js
index cbe00797..2c1cfd75 100644
--- a/client/components/sidebar/sidebar.js
+++ b/client/components/sidebar/sidebar.js
@@ -213,6 +213,7 @@ Template.boardMenuPopup.events({
'click .js-import-board': Popup.open('chooseBoardSource'),
'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
'click .js-card-settings': Popup.open('boardCardSettings'),
+ 'click .js-export-board': Popup.open('exportBoard'),
});
Template.boardMenuPopup.onCreated(function() {
@@ -405,6 +406,63 @@ BlazeComponent.extendComponent({
},
}).register('chooseBoardSourcePopup');
+BlazeComponent.extendComponent({
+ template() {
+ return 'exportBoard';
+ },
+ withApi() {
+ return Template.instance().apiEnabled.get();
+ },
+ exportUrl() {
+ const params = {
+ boardId: Session.get('currentBoard'),
+ };
+ const queryParams = {
+ authToken: Accounts._storedLoginToken(),
+ };
+ return FlowRouter.path('/api/boards/:boardId/export', params, queryParams);
+ },
+ exportCsvUrl() {
+ const params = {
+ boardId: Session.get('currentBoard'),
+ };
+ const queryParams = {
+ authToken: Accounts._storedLoginToken(),
+ };
+ return FlowRouter.path(
+ '/api/boards/:boardId/export/csv',
+ params,
+ queryParams,
+ );
+ },
+ exportTsvUrl() {
+ const params = {
+ boardId: Session.get('currentBoard'),
+ };
+ const queryParams = {
+ authToken: Accounts._storedLoginToken(),
+ delimiter: '\t',
+ };
+ return FlowRouter.path(
+ '/api/boards/:boardId/export/csv',
+ params,
+ queryParams,
+ );
+ },
+ exportJsonFilename() {
+ const boardId = Session.get('currentBoard');
+ return `wekan-export-board-${boardId}.json`;
+ },
+ exportCsvFilename() {
+ const boardId = Session.get('currentBoard');
+ return `wekan-export-board-${boardId}.csv`;
+ },
+ exportTsvFilename() {
+ const boardId = Session.get('currentBoard');
+ return `wekan-export-board-${boardId}.tsv`;
+ },
+}).register('exportBoardPopup');
+
Template.labelsWidget.events({
'click .js-label': Popup.open('editLabel'),
'click .js-add-label': Popup.open('createLabel'),
diff --git a/client/components/users/userAvatar.styl b/client/components/users/userAvatar.styl
index b962b01c..8209f4a1 100644
--- a/client/components/users/userAvatar.styl
+++ b/client/components/users/userAvatar.styl
@@ -29,6 +29,8 @@ avatar-radius = 50%
position: absolute
&.avatar-image
+ object-fit: cover;
+ object-position: center;
height: 100%
width: @height