diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/components/boards/boardsList.js | 3 | ||||
-rw-r--r-- | client/components/cards/cardDate.js | 27 | ||||
-rw-r--r-- | client/components/cards/cardDetails.jade | 81 | ||||
-rw-r--r-- | client/components/cards/cardDetails.js | 120 | ||||
-rw-r--r-- | client/components/cards/cardDetails.styl | 10 | ||||
-rw-r--r-- | client/components/cards/minicard.jade | 2 | ||||
-rw-r--r-- | client/components/import/csvMembersMapper.js | 37 | ||||
-rw-r--r-- | client/components/import/import.jade | 2 | ||||
-rw-r--r-- | client/components/import/import.js | 52 | ||||
-rw-r--r-- | client/components/lists/listBody.js | 9 | ||||
-rw-r--r-- | client/components/lists/listHeader.js | 31 | ||||
-rwxr-xr-x | client/components/main/editor.js | 2 | ||||
-rw-r--r-- | client/components/sidebar/sidebar.jade | 19 | ||||
-rw-r--r-- | client/components/sidebar/sidebar.js | 58 | ||||
-rw-r--r-- | client/components/users/userAvatar.styl | 2 |
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 b6ccd4d7..79dd9127 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 081c6521..0c2e3186 100755 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -330,7 +330,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 |