diff options
Diffstat (limited to 'client')
37 files changed, 1004 insertions, 740 deletions
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade index bddc4dad..54066da8 100644 --- a/client/components/activities/activities.jade +++ b/client/components/activities/activities.jade @@ -99,6 +99,9 @@ template(name="boardActivities") 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}}}. @@ -114,6 +117,12 @@ template(name="boardActivities") if($eq activityType 'removedLabel') | {{{_ 'activity-removed-label' lastLabel cardLink}}}. + if($eq activityType 'setCustomField') + | {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}. + + if($eq activityType 'unsetCustomField') + | {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}. + if($eq activityType 'unjoinMember') if($eq user._id member._id) | {{{_ 'activity-unjoined' cardLink}}}. @@ -129,7 +138,7 @@ template(name="cardActivities") p.activity-desc +memberName(user=user) if($eq activityType 'createCard') - | {{_ 'activity-added' cardLabel list.title}}. + | {{_ 'activity-added' cardLabel listName}}. if($eq activityType 'importCard') | {{{_ 'activity-imported' cardLabel list.title sourceLink}}}. if($eq activityType 'joinMember') @@ -170,6 +179,10 @@ template(name="cardActivities") | {{_ 'activity-sent' cardLabel boardLabel}}. if($eq activityType 'moveCard') | {{_ 'activity-moved' cardLabel oldList.title list.title}}. + + if($eq activityType 'moveCardBoard') + | {{{_ 'activity-moved' cardLink oldBoardName boardName}}}. + if($eq activityType 'addAttachment') | {{{_ 'activity-attached' attachmentLink cardLabel}}}. if attachment.isImage diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index b3fe8f50..0476897f 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -74,6 +74,8 @@ BlazeComponent.extendComponent({ lastLabel(){ const lastLabelId = this.currentData().labelId; + if (!lastLabelId) + return null; const lastLabel = Boards.findOne(Session.get('currentBoard')).getLabelById(lastLabelId); if(lastLabel.name === undefined || lastLabel.name === ''){ return lastLabel.color; @@ -82,6 +84,28 @@ BlazeComponent.extendComponent({ } }, + lastCustomField(){ + const lastCustomField = CustomFields.findOne(this.currentData().customFieldId); + if (!lastCustomField) + return null; + return lastCustomField.name; + }, + + lastCustomFieldValue(){ + const lastCustomField = CustomFields.findOne(this.currentData().customFieldId); + if (!lastCustomField) + return null; + const value = this.currentData().value; + if (lastCustomField.settings.dropdownItems && lastCustomField.settings.dropdownItems.length > 0) { + const dropDownValue = _.find(lastCustomField.settings.dropdownItems, (item) => { + return item._id === value; + }); + if (dropDownValue) + return dropDownValue.name; + } + return value; + }, + listLabel() { return this.currentData().list().title; }, @@ -117,6 +141,8 @@ BlazeComponent.extendComponent({ customField() { const customField = this.currentData().customField(); + if (!customField) + return null; return customField.name; }, diff --git a/client/components/boards/boardArchive.js b/client/components/boards/boardArchive.js index 8f4d5434..c8bbb341 100644 --- a/client/components/boards/boardArchive.js +++ b/client/components/boards/boardArchive.js @@ -1,9 +1,3 @@ -Template.boardListHeaderBar.events({ - 'click .js-open-archived-board'() { - Modal.open('archivedBoards'); - }, -}); - BlazeComponent.extendComponent({ onCreated() { this.subscribe('archivedBoards'); diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade index 3a40921d..32f8629f 100644 --- a/client/components/boards/boardBody.jade +++ b/client/components/boards/boardBody.jade @@ -20,12 +20,15 @@ template(name="boardBody") class="{{#if draggingActive.get}}is-dragging-active{{/if}}") if showOverlay.get .board-overlay - if isViewSwimlanes + if currentBoard.isTemplatesBoard each currentBoard.swimlanes +swimlane(this) - if isViewLists - +listsGroup - if isViewCalendar + else if isViewSwimlanes + each currentBoard.swimlanes + +swimlane(this) + else if isViewLists + +listsGroup(currentBoard) + else if isViewCalendar +calendarView template(name="calendarView") diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index 75b2f02b..823bd806 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -7,71 +7,69 @@ template(name="boardHeaderBar") .board-header-btns.left unless isMiniScreen - unless isSandstorm - if currentBoard - if currentUser - a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}" - title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}") - i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") - if showStarCounter - span - = currentBoard.stars - - a.board-header-btn( - class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}" - title="{{_ currentBoard.permission}}") - i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}") - span {{_ currentBoard.permission}} - - a.board-header-btn.js-watch-board( - title="{{_ watchLevel }}") - if $eq watchLevel "watching" - i.fa.fa-eye - if $eq watchLevel "tracking" - i.fa.fa-bell - if $eq watchLevel "muted" - i.fa.fa-bell-slash - span {{_ watchLevel}} + if currentBoard + if currentUser + a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}" + title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}") + i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") + if showStarCounter + span + = currentBoard.stars + + a.board-header-btn( + class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}" + title="{{_ currentBoard.permission}}") + i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}") + span {{_ currentBoard.permission}} + + a.board-header-btn.js-watch-board( + title="{{_ watchLevel }}") + if $eq watchLevel "watching" + i.fa.fa-eye + if $eq watchLevel "tracking" + i.fa.fa-bell + if $eq watchLevel "muted" + i.fa.fa-bell-slash + span {{_ watchLevel}} - else - a.board-header-btn.js-log-in( - title="{{_ 'log-in'}}") - i.fa.fa-sign-in - span {{_ 'log-in'}} + else + a.board-header-btn.js-log-in( + title="{{_ 'log-in'}}") + i.fa.fa-sign-in + span {{_ 'log-in'}} .board-header-btns.right if currentBoard if isMiniScreen - unless isSandstorm - if currentUser - a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}" - title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}") - i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") - if showStarCounter - span - = currentBoard.stars - - a.board-header-btn( - class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}" - title="{{_ currentBoard.permission}}") - i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}") - span {{_ currentBoard.permission}} - - a.board-header-btn.js-watch-board( - title="{{_ watchLevel }}") - if $eq watchLevel "watching" - i.fa.fa-eye - if $eq watchLevel "tracking" - i.fa.fa-bell - if $eq watchLevel "muted" - i.fa.fa-bell-slash - span {{_ watchLevel}} + if currentUser + a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}" + title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}") + i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") + if showStarCounter + span + = currentBoard.stars + + a.board-header-btn( + class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}" + title="{{_ currentBoard.permission}}") + i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}") + span {{_ currentBoard.permission}} + + a.board-header-btn.js-watch-board( + title="{{_ watchLevel }}") + if $eq watchLevel "watching" + i.fa.fa-eye + if $eq watchLevel "tracking" + i.fa.fa-bell + if $eq watchLevel "muted" + i.fa.fa-bell-slash + span {{_ watchLevel}} - else - a.board-header-btn.js-log-in( - title="{{_ 'log-in'}}") - i.fa.fa-sign-in - span {{_ 'log-in'}} + else + a.board-header-btn.js-log-in( + title="{{_ 'log-in'}}") + i.fa.fa-sign-in + span {{_ 'log-in'}} if isSandstorm if currentUser @@ -96,10 +94,11 @@ template(name="boardHeaderBar") i.fa.fa-search span {{_ 'search'}} - a.board-header-btn.js-toggle-board-view( - title="{{_ 'board-view'}}") - i.fa.fa-th-large - span {{_ currentUser.profile.boardView}} + unless currentBoard.isTemplatesBoard + a.board-header-btn.js-toggle-board-view( + title="{{_ 'board-view'}}") + i.fa.fa-th-large + span {{_ currentUser.profile.boardView}} if canModifyBoard a.board-header-btn.js-multiselection-activate( @@ -112,40 +111,8 @@ template(name="boardHeaderBar") i.fa.fa-times-thin .separator - a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}") - i.board-header-btn-icon.fa.fa-navicon - -template(name="boardMenuPopup") - ul.pop-over-list - li: a.js-custom-fields {{_ 'custom-fields'}} - li: a.js-open-archives {{_ 'archived-items'}} - if currentUser.isBoardAdmin - li: a.js-change-board-color {{_ 'board-change-color'}} - //- - XXX Language should be handled by sandstorm, but for now display a - language selection link in the board menu. This link is normally present - in the header bar that is not displayed on sandstorm. - if isSandstorm - li: a.js-change-language {{_ 'language'}} - unless isSandstorm - if currentUser.isBoardAdmin - hr - ul.pop-over-list - li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}} - li: a.js-archive-board {{_ 'archive-board'}} - li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}} - hr - ul.pop-over-list - li: a.js-subtask-settings {{_ 'subtask-settings'}} - - if isSandstorm - hr - ul.pop-over-list - li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}} - li: a.js-import-board {{_ 'import-board-c'}} - hr - ul.pop-over-list - li: a.js-subtask-settings {{_ 'subtask-settings'}} + a.board-header-btn.js-toggle-sidebar + i.fa.fa-navicon template(name="boardVisibilityList") ul.pop-over-list @@ -196,64 +163,6 @@ template(name="boardChangeWatchPopup") i.fa.fa-check span.sub-name {{_ 'muted-info'}} -template(name="boardChangeColorPopup") - .board-backgrounds-list.clearfix - each backgroundColors - .board-background-select.js-select-background - span.background-box(class="board-color-{{this}}") - if isSelected - i.fa.fa-check - -template(name="boardSubtaskSettingsPopup") - form.board-subtask-settings - h3 {{_ 'show-parent-in-minicard'}} - a#prefix-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}") - .materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}") - span {{_ 'prefix-with-full-path'}} - a#prefix-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}") - .materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}") - span {{_ 'prefix-with-parent'}} - a#subtext-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}") - .materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}") - span {{_ 'subtext-with-full-path'}} - a#subtext-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}") - .materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}") - span {{_ 'subtext-with-parent'}} - a#no-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}") - .materialCheckBox(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}") - span {{_ 'no-parent'}} - div - hr - - div.check-div - a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}") - .materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}") - span {{_ 'show-subtasks-field'}} - - label - | {{_ 'deposit-subtasks-board'}} - select.js-field-deposit-board(disabled="{{#unless allowsSubtasks}}disabled{{/unless}}") - each boards - if isBoardSelected - option(value=_id selected="selected") {{title}} - else - option(value=_id) {{title}} - if isNullBoardSelected - option(value='null' selected="selected") {{_ 'custom-field-dropdown-none'}} - else - option(value='null') {{_ 'custom-field-dropdown-none'}} - div - hr - - label - | {{_ 'deposit-subtasks-list'}} - select.js-field-deposit-list(disabled="{{#unless hasLists}}disabled{{/unless}}") - each lists - if isListSelected - option(value=_id selected="selected") {{title}} - else - option(value=_id) {{title}} - template(name="createBoard") form label @@ -275,14 +184,10 @@ template(name="createBoard") input.primary.wide(type="submit" value="{{_ 'create'}}") span.quiet | {{_ 'or'}} - a.js-import-board {{_ 'import-board'}} - -template(name="chooseBoardSource") - ul.pop-over-list - li - a(href="{{pathFor '/import/trello'}}") {{_ 'from-trello'}} - li - a(href="{{pathFor '/import/wekan'}}") {{_ 'from-wekan'}} + a.js-import-board {{_ 'import'}} + span.quiet + | / + a.js-board-template {{_ 'template'}} template(name="boardChangeTitlePopup") form @@ -297,28 +202,3 @@ template(name="boardChangeTitlePopup") template(name="boardCreateRulePopup") p {{_ 'close-board-pop'}} button.js-confirm.negate.full(type="submit") {{_ 'archive'}} - - -template(name="archiveBoardPopup") - p {{_ 'close-board-pop'}} - button.js-confirm.negate.full(type="submit") {{_ 'archive'}} - -template(name="outgoingWebhooksPopup") - each integrations - form.integration-form - if title - h4 {{title}} - else - h4 {{_ 'no-name'}} - label - | URL - input.js-outgoing-webhooks-url(type="text" name="url" value=url) - input(type="hidden" value=_id name="id") - input.primary.wide(type="submit" value="{{_ 'save'}}") - form.integration-form - h4 - | {{_ 'new-outgoing-webhook'}} - label - | URL - input.js-outgoing-webhooks-url(type="text" name="url" autofocus) - input.primary.wide(type="submit" value="{{_ 'save'}}") diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index 89f686ab..86fbebb3 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -97,6 +97,9 @@ BlazeComponent.extendComponent({ currentUser.setBoardView('board-view-lists'); } }, + 'click .js-toggle-sidebar'() { + Sidebar.toggle(); + }, 'click .js-open-filter-view'() { Sidebar.setView('filter'); }, @@ -135,124 +138,6 @@ Template.boardHeaderBar.helpers({ }, }); -BlazeComponent.extendComponent({ - backgroundColors() { - return Boards.simpleSchema()._schema.color.allowedValues; - }, - - isSelected() { - const currentBoard = Boards.findOne(Session.get('currentBoard')); - return currentBoard.color === this.currentData().toString(); - }, - - events() { - return [{ - 'click .js-select-background'(evt) { - const currentBoard = Boards.findOne(Session.get('currentBoard')); - const newColor = this.currentData().toString(); - currentBoard.setColor(newColor); - evt.preventDefault(); - }, - }]; - }, -}).register('boardChangeColorPopup'); - -BlazeComponent.extendComponent({ - onCreated() { - this.currentBoard = Boards.findOne(Session.get('currentBoard')); - }, - - allowsSubtasks() { - return this.currentBoard.allowsSubtasks; - }, - - isBoardSelected() { - return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id; - }, - - isNullBoardSelected() { - return (this.currentBoard.subtasksDefaultBoardId === null) || (this.currentBoard.subtasksDefaultBoardId === undefined); - }, - - boards() { - return Boards.find({ - archived: false, - 'members.userId': Meteor.userId(), - }, { - sort: ['title'], - }); - }, - - lists() { - return Lists.find({ - boardId: this.currentBoard._id, - archived: false, - }, { - sort: ['title'], - }); - }, - - hasLists() { - return this.lists().count() > 0; - }, - - isListSelected() { - return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id; - }, - - presentParentTask() { - let result = this.currentBoard.presentParentTask; - if ((result === null) || (result === undefined)) { - result = 'no-parent'; - } - return result; - }, - - events() { - return [{ - 'click .js-field-has-subtasks'(evt) { - evt.preventDefault(); - this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks; - this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks); - $('.js-field-has-subtasks .materialCheckBox').toggleClass('is-checked', this.currentBoard.allowsSubtasks); - $('.js-field-has-subtasks').toggleClass('is-checked', this.currentBoard.allowsSubtasks); - $('.js-field-deposit-board').prop('disabled', !this.currentBoard.allowsSubtasks); - }, - 'change .js-field-deposit-board'(evt) { - let value = evt.target.value; - if (value === 'null') { - value = null; - } - this.currentBoard.setSubtasksDefaultBoardId(value); - evt.preventDefault(); - }, - 'change .js-field-deposit-list'(evt) { - this.currentBoard.setSubtasksDefaultListId(evt.target.value); - evt.preventDefault(); - }, - 'click .js-field-show-parent-in-minicard'(evt) { - const value = evt.target.id || $(evt.target).parent()[0].id || $(evt.target).parent()[0].parent()[0].id; - const options = [ - 'prefix-with-full-path', - 'prefix-with-parent', - 'subtext-with-full-path', - 'subtext-with-parent', - 'no-parent']; - options.forEach(function(element) { - if (element !== value) { - $(`#${element} .materialCheckBox`).toggleClass('is-checked', false); - $(`#${element}`).toggleClass('is-checked', false); - } - }); - $(`#${value} .materialCheckBox`).toggleClass('is-checked', true); - $(`#${value}`).toggleClass('is-checked', true); - this.currentBoard.setPresentParentTask(value); - evt.preventDefault(); - }, - }]; - }, -}).register('boardSubtaskSettingsPopup'); - const CreateBoard = BlazeComponent.extendComponent({ template() { return 'createBoard'; @@ -304,16 +189,11 @@ const CreateBoard = BlazeComponent.extendComponent({ 'click .js-import': Popup.open('boardImportBoard'), submit: this.onSubmit, 'click .js-import-board': Popup.open('chooseBoardSource'), + 'click .js-board-template': Popup.open('searchElement'), }]; }, }).register('createBoardPopup'); -BlazeComponent.extendComponent({ - template() { - return 'chooseBoardSource'; - }, -}).register('chooseBoardSourcePopup'); - (class HeaderBarCreateBoard extends CreateBoard { onSubmit(evt) { super.onSubmit(evt); @@ -363,50 +243,3 @@ BlazeComponent.extendComponent({ }]; }, }).register('boardChangeWatchPopup'); - -BlazeComponent.extendComponent({ - integrations() { - const boardId = Session.get('currentBoard'); - return Integrations.find({ boardId: `${boardId}` }).fetch(); - }, - - integration(id) { - const boardId = Session.get('currentBoard'); - return Integrations.findOne({ _id: id, boardId: `${boardId}` }); - }, - - events() { - return [{ - 'submit'(evt) { - evt.preventDefault(); - const url = evt.target.url.value; - const boardId = Session.get('currentBoard'); - let id = null; - let integration = null; - if (evt.target.id) { - id = evt.target.id.value; - integration = this.integration(id); - if (url) { - Integrations.update(integration._id, { - $set: { - url: `${url}`, - }, - }); - } else { - Integrations.remove(integration._id); - } - } else if (url) { - Integrations.insert({ - userId: Meteor.userId(), - enabled: true, - type: 'outgoing-webhooks', - url: `${url}`, - boardId: `${boardId}`, - activities: ['all'], - }); - } - Popup.close(); - }, - }]; - }, -}).register('outgoingWebhooksPopup'); diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade index 753724fc..abb009f7 100644 --- a/client/components/boards/boardsList.jade +++ b/client/components/boards/boardsList.jade @@ -36,3 +36,6 @@ template(name="boardListHeaderBar") a.board-header-btn.js-open-archived-board i.fa.fa-archive span {{_ 'archives'}} + a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") + i.fa.fa-clone + span {{_ 'templates'}} diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js index 68f4a1dc..ad28fee8 100644 --- a/client/components/boards/boardsList.js +++ b/client/components/boards/boardsList.js @@ -1,5 +1,20 @@ const subManager = new SubsManager(); +Template.boardListHeaderBar.events({ + 'click .js-open-archived-board'() { + Modal.open('archivedBoards'); + }, +}); + +Template.boardListHeaderBar.helpers({ + templatesBoardId() { + return Meteor.user().getTemplatesBoardId(); + }, + templatesBoardSlug() { + return Meteor.user().getTemplatesBoardSlug(); + }, +}); + BlazeComponent.extendComponent({ onCreated() { Meteor.subscribe('setting'); @@ -9,11 +24,9 @@ BlazeComponent.extendComponent({ return Boards.find({ archived: false, 'members.userId': Meteor.userId(), - }, { - sort: ['title'], - }); + type: 'board', + }, { sort: ['title'] }); }, - isStarred() { const user = Meteor.user(); return user && user.hasStarred(this.currentData()._id); diff --git a/client/components/boards/miniboard.jade b/client/components/boards/miniboard.jade new file mode 100644 index 00000000..d1fb0b07 --- /dev/null +++ b/client/components/boards/miniboard.jade @@ -0,0 +1,8 @@ +template(name="miniboard") + .minicard( + class="minicard-{{colorClass}}") + .minicard-title + .handle + .fa.fa-arrows + +viewer + = title diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index 25316d04..5fd7b748 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -19,16 +19,14 @@ template(name="cardDetails") a.js-parent-card(href=linkForCard) {{title}} // else {{_ 'top-level-card'}} - unless isSandstorm - if isLinkedCard - h3.linked-card-location - +viewer - | {{getBoardTitle}} > {{getTitle}} + if isLinkedCard + h3.linked-card-location + +viewer + | {{getBoardTitle}} > {{getTitle}} if getArchived if isLinkedBoard - unless isSandstorm - p.warning {{_ 'board-archived'}} + p.warning {{_ 'board-archived'}} else p.warning {{_ 'card-archived'}} @@ -192,11 +190,9 @@ template(name="cardDetails") unless currentUser.isNoComments if isLoaded.get if isLinkedCard - unless isSandstorm - +activities(card=this mode="linkedcard") + +activities(card=this mode="linkedcard") else if isLinkedBoard - unless isSandstorm - +activities(card=this mode="linkedboard") + +activities(card=this mode="linkedboard") else +activities(card=this mode="card") @@ -310,27 +306,27 @@ template(name="cardMorePopup") h2 {{_ 'change-card-parent'}} label {{_ 'source-board'}}: select.js-field-parent-board + if isTopLevel + option(value="none" selected) {{_ 'custom-field-dropdown-none'}} + else + option(value="none") {{_ 'custom-field-dropdown-none'}} each boards if isParentBoard option(value="{{_id}}" selected) {{title}} else option(value="{{_id}}") {{title}} - if isTopLevel - option(value="none" selected) {{_ 'custom-field-dropdown-none'}} - else - option(value="none") {{_ 'custom-field-dropdown-none'}} label {{_ 'parent-card'}}: select.js-field-parent-card if isTopLevel option(value="none" selected) {{_ 'custom-field-dropdown-none'}} else + option(value="none") {{_ 'custom-field-dropdown-none'}} each cards if isParentCard option(value="{{_id}}" selected) {{title}} else option(value="{{_id}}") {{title}} - option(value="none") {{_ 'custom-field-dropdown-none'}} br | {{_ 'added'}} span.date(title=card.createdAt) {{ moment createdAt 'LLL' }} diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index a571e21a..79e9e311 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -412,11 +412,13 @@ Template.moveCardPopup.events({ // XXX We should *not* get the currentCard from the global state, but // instead from a “component” state. const card = Cards.findOne(Session.get('currentCard')); + const bSelect = $('.js-select-boards')[0]; + const boardId = bSelect.options[bSelect.selectedIndex].value; const lSelect = $('.js-select-lists')[0]; - const newListId = lSelect.options[lSelect.selectedIndex].value; + const listId = lSelect.options[lSelect.selectedIndex].value; const slSelect = $('.js-select-swimlanes')[0]; - card.swimlaneId = slSelect.options[slSelect.selectedIndex].value; - card.move(card.swimlaneId, newListId, 0); + const swimlaneId = slSelect.options[slSelect.selectedIndex].value; + card.move(boardId, swimlaneId, listId, 0); Popup.close(); }, }); @@ -430,6 +432,7 @@ BlazeComponent.extendComponent({ const boards = Boards.find({ archived: false, 'members.userId': Meteor.userId(), + _id: {$ne: Meteor.user().getTemplatesBoardId()}, }, { sort: ['title'], }); @@ -456,32 +459,15 @@ BlazeComponent.extendComponent({ }, }).register('boardsAndLists'); - -function cloneCheckList(_id, checklist) { - 'use strict'; - const checklistId = checklist._id; - checklist.cardId = _id; - checklist._id = null; - const newChecklistId = Checklists.insert(checklist); - ChecklistItems.find({checklistId}).forEach(function(item) { - item._id = null; - item.checklistId = newChecklistId; - item.cardId = _id; - ChecklistItems.insert(item); - }); -} - Template.copyCardPopup.events({ 'click .js-done'() { const card = Cards.findOne(Session.get('currentCard')); - const oldId = card._id; - card._id = null; const lSelect = $('.js-select-lists')[0]; - card.listId = lSelect.options[lSelect.selectedIndex].value; + listId = lSelect.options[lSelect.selectedIndex].value; const slSelect = $('.js-select-swimlanes')[0]; - card.swimlaneId = slSelect.options[slSelect.selectedIndex].value; + const swimlaneId = slSelect.options[slSelect.selectedIndex].value; const bSelect = $('.js-select-boards')[0]; - card.boardId = bSelect.options[bSelect.selectedIndex].value; + const boardId = bSelect.options[bSelect.selectedIndex].value; const textarea = $('#copy-card-title'); const title = textarea.val().trim(); // insert new card to the bottom of new list @@ -490,38 +476,13 @@ Template.copyCardPopup.events({ if (title) { card.title = title; card.coverId = ''; - const _id = Cards.insert(card); + const _id = card.copy(boardId, swimlaneId, listId); // In case the filter is active we need to add the newly inserted card in // the list of exceptions -- cards that are not filtered. Otherwise the // card will disappear instantly. // See https://github.com/wekan/wekan/issues/80 Filter.addException(_id); - // copy checklists - let cursor = Checklists.find({cardId: oldId}); - cursor.forEach(function() { - cloneCheckList(_id, arguments[0]); - }); - - // copy subtasks - cursor = Cards.find({parentId: oldId}); - cursor.forEach(function() { - 'use strict'; - const subtask = arguments[0]; - subtask.parentId = _id; - subtask._id = null; - /* const newSubtaskId = */ Cards.insert(subtask); - }); - - // copy card comments - cursor = CardComments.find({cardId: oldId}); - cursor.forEach(function () { - 'use strict'; - const comment = arguments[0]; - comment.cardId = _id; - comment._id = null; - CardComments.insert(comment); - }); Popup.close(); } }, @@ -558,9 +519,8 @@ Template.copyChecklistToManyCardsPopup.events({ Filter.addException(_id); // copy checklists - let cursor = Checklists.find({cardId: oldId}); - cursor.forEach(function() { - cloneCheckList(_id, arguments[0]); + Checklists.find({cardId: oldId}).forEach((ch) => { + ch.copy(_id); }); // copy subtasks @@ -574,13 +534,8 @@ Template.copyChecklistToManyCardsPopup.events({ }); // copy card comments - cursor = CardComments.find({cardId: oldId}); - cursor.forEach(function () { - 'use strict'; - const comment = arguments[0]; - comment.cardId = _id; - comment._id = null; - CardComments.insert(comment); + CardComments.find({cardId: oldId}).forEach((cmt) => { + cmt.copy(_id); }); } Popup.close(); @@ -625,11 +580,14 @@ BlazeComponent.extendComponent({ BlazeComponent.extendComponent({ onCreated() { this.currentCard = this.currentData(); + this.parentBoard = new ReactiveVar(null); this.parentCard = this.currentCard.parentCard(); if (this.parentCard) { - this.parentBoard = this.parentCard.board(); + const list = $('.js-field-parent-card'); + list.val(this.parentCard._id); + this.parentBoard.set(this.parentCard.board()._id); } else { - this.parentBoard = null; + this.parentBoard.set(null); } }, @@ -637,6 +595,9 @@ BlazeComponent.extendComponent({ const boards = Boards.find({ archived: false, 'members.userId': Meteor.userId(), + _id: { + $ne: Meteor.user().getTemplatesBoardId(), + }, }, { sort: ['title'], }); @@ -644,8 +605,12 @@ BlazeComponent.extendComponent({ }, cards() { - if (this.parentBoard) { - return this.parentBoard.cards(); + const currentId = Session.get('currentCard'); + if (this.parentBoard.get()) { + return Cards.find({ + boardId: this.parentBoard.get(), + _id: {$ne: currentId}, + }); } else { return []; } @@ -653,8 +618,8 @@ BlazeComponent.extendComponent({ isParentBoard() { const board = this.currentData(); - if (this.parentBoard) { - return board._id === this.parentBoard; + if (this.parentBoard.get()) { + return board._id === this.parentBoard.get(); } return false; }, @@ -668,11 +633,10 @@ BlazeComponent.extendComponent({ }, setParentCardId(cardId) { - if (cardId === 'null') { - cardId = null; - this.parentCard = null; - } else { + if (cardId) { this.parentCard = Cards.findOne(cardId); + } else { + this.parentCard = null; } this.currentCard.setParentId(cardId); }, @@ -709,23 +673,14 @@ BlazeComponent.extendComponent({ 'change .js-field-parent-board'(evt) { const selection = $(evt.currentTarget).val(); const list = $('.js-field-parent-card'); - list.empty(); if (selection === 'none') { - this.parentBoard = null; - list.prop('disabled', true); + this.parentBoard.set(null); } else { - this.parentBoard = Boards.findOne(selection); - this.parentBoard.cards().forEach(function(card) { - list.append( - $('<option></option>').val(card._id).html(card.title) - ); - }); + subManager.subscribe('board', $(evt.currentTarget).val()); + this.parentBoard.set(selection); list.prop('disabled', false); } - list.append( - `<option value='none' selected='selected'>${TAPi18n.__('custom-field-dropdown-none')}</option>` - ); - this.setParentCardId('null'); + this.setParentCardId(null); }, 'change .js-field-parent-card'(evt) { const selection = $(evt.currentTarget).val(); diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl index bf50c071..c1d6b7e1 100644 --- a/client/components/cards/cardDetails.styl +++ b/client/components/cards/cardDetails.styl @@ -133,7 +133,8 @@ input[type="submit"].attachment-add-link-submit .card-details-canvas width: 100% - + padding-left: 0px; + .card-details-header .close-card-details margin-right: 0px diff --git a/client/components/lists/list.js b/client/components/lists/list.js index 043cb77c..12932a82 100644 --- a/client/components/lists/list.js +++ b/client/components/lists/list.js @@ -67,7 +67,13 @@ BlazeComponent.extendComponent({ const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1; const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards); const listId = Blaze.getData(ui.item.parents('.list').get(0))._id; - const swimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))._id; + const currentBoard = Boards.findOne(Session.get('currentBoard')); + let swimlaneId = ''; + const boardView = Meteor.user().profile.boardView; + if (boardView === 'board-view-swimlanes' || currentBoard.isTemplatesBoard()) + swimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))._id; + else if ((boardView === 'board-view-lists') || (boardView === 'board-view-cal')) + swimlaneId = currentBoard.getDefaultSwimline()._id; // Normally the jquery-ui sortable library moves the dragged DOM element // to its new position, which disrupts Blaze reactive updates mechanism @@ -80,12 +86,12 @@ BlazeComponent.extendComponent({ if (MultiSelection.isActive()) { Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => { - card.move(swimlaneId, listId, sortIndex.base + i * sortIndex.increment); + card.move(currentBoard._id, swimlaneId, listId, sortIndex.base + i * sortIndex.increment); }); } else { const cardDomElement = ui.item.get(0); const card = Blaze.getData(cardDomElement); - card.move(swimlaneId, listId, sortIndex.base); + card.move(currentBoard._id, swimlaneId, listId, sortIndex.base); } boardComponent.setIsDragging(false); }, diff --git a/client/components/lists/list.styl b/client/components/lists/list.styl index 70502083..7e4550a4 100644 --- a/client/components/lists/list.styl +++ b/client/components/lists/list.styl @@ -84,6 +84,7 @@ padding-left: 10px color: #a6a6a6 + .list-header-menu position: absolute padding: 27px 19px @@ -155,6 +156,9 @@ float: left @media screen and (max-width: 800px) + .list-header-menu + margin-right: 30px + .mini-list flex: 0 0 60px height: 60px diff --git a/client/components/lists/listBody.jade b/client/components/lists/listBody.jade index f030833b..61fec93a 100644 --- a/client/components/lists/listBody.jade +++ b/client/components/lists/listBody.jade @@ -13,14 +13,7 @@ template(name="listBody") class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}") +minicard(this) if (showSpinner (idOrNull ../../_id)) - .sk-spinner.sk-spinner-wave.sk-spinner-list( - class=currentBoard.colorClass - id="showMoreResults") - .sk-rect1 - .sk-rect2 - .sk-rect3 - .sk-rect4 - .sk-rect5 + +spinnerList if canSeeAddCard +inlinedForm(autoclose=false position="bottom") @@ -30,6 +23,16 @@ template(name="listBody") i.fa.fa-plus | {{_ 'add-card'}} +template(name="spinnerList") + .sk-spinner.sk-spinner-wave.sk-spinner-list( + class=currentBoard.colorClass + id="showMoreResults") + .sk-rect1 + .sk-rect2 + .sk-rect3 + .sk-rect4 + .sk-rect5 + template(name="addCardForm") .minicard.minicard-composer.js-composer if getLabels @@ -44,14 +47,19 @@ template(name="addCardForm") .add-controls.clearfix button.primary.confirm(type="submit") {{_ 'add'}} - unless isSandstorm - span.quiet - | {{_ 'or'}} - a.js-link {{_ 'link'}} - span.quiet - | - | / - a.js-search {{_ 'search'}} + unless currentBoard.isTemplatesBoard + unless currentBoard.isTemplateBoard + span.quiet + | {{_ 'or'}} + a.js-link {{_ 'link'}} + span.quiet + | + | / + a.js-search {{_ 'search'}} + span.quiet + | + | / + a.js-card-template {{_ 'template'}} template(name="autocompleteLabelLine") .minicard-label(class="card-label-{{colorName}}" title=labelName) @@ -61,11 +69,9 @@ template(name="linkCardPopup") label {{_ 'boards'}}: .link-board-wrapper select.js-select-boards + option(value="") each boards - if $eq _id currentBoard._id - option(value="{{_id}}" selected) {{_ 'current'}} - else - option(value="{{_id}}") {{title}} + option(value="{{_id}}") {{title}} input.primary.confirm.js-link-board(type="button" value="{{_ 'link'}}") label {{_ 'swimlanes'}}: @@ -84,22 +90,41 @@ template(name="linkCardPopup") option(value="{{getId}}") {{getTitle}} .edit-controls.clearfix - unless isSandstorm - input.primary.confirm.js-done(type="button" value="{{_ 'link'}}") + input.primary.confirm.js-done(type="button" value="{{_ 'link'}}") -template(name="searchCardPopup") - label {{_ 'boards'}}: - .link-board-wrapper - select.js-select-boards - each boards - if $eq _id currentBoard._id - option(value="{{_id}}" selected) {{_ 'current'}} - else +template(name="searchElementPopup") + form + label + | {{_ 'title'}} + input.js-element-title(type="text" placeholder="{{_ 'title'}}" autofocus required) + unless isTemplateSearch + label {{_ 'boards'}}: + .link-board-wrapper + select.js-select-boards + option(value="") + each boards option(value="{{_id}}") {{title}} form.js-search-term-form input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus) .list-body.js-perfect-scrollbar.search-card-results .minicards.clearfix.js-minicards - each results - a.minicard-wrapper.js-minicard - +minicard(this) + if isBoardTemplateSearch + each results + a.minicard-wrapper.js-minicard + +miniboard(this) + if isListTemplateSearch + each results + a.minicard-wrapper.js-minicard + +minilist(this) + if isSwimlaneTemplateSearch + each results + a.minicard-wrapper.js-minicard + +miniswimlane(this) + if isCardTemplateSearch + each results + a.minicard-wrapper.js-minicard + +minicard(this) + unless isTemplateSearch + each results + a.minicard-wrapper.js-minicard + +minicard(this) diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js index 0f5caac5..43619890 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -7,28 +7,6 @@ BlazeComponent.extendComponent({ this.cardlimit = new ReactiveVar(InfiniteScrollIter); }, - onRendered() { - const domElement = this.find('.js-perfect-scrollbar'); - - this.$(domElement).on('scroll', () => this.updateList(domElement)); - $(window).on(`resize.${this.data().listId}`, () => this.updateList(domElement)); - - // we add a Mutation Observer to allow propagations of cardlimit - // when the spinner stays in the current view (infinite scrolling) - this.mutationObserver = new MutationObserver(() => this.updateList(domElement)); - - this.mutationObserver.observe(domElement, { - childList: true, - }); - - this.updateList(domElement); - }, - - onDestroyed() { - $(window).off(`resize.${this.data().listId}`); - this.mutationObserver.disconnect(); - }, - mixins() { return [Mixins.PerfectScrollbar]; }, @@ -67,25 +45,47 @@ BlazeComponent.extendComponent({ const labelIds = formComponent.labels.get(); const customFields = formComponent.customFields.get(); - const boardId = this.data().board(); + const board = this.data().board(); + let linkedId = ''; let swimlaneId = ''; const boardView = Meteor.user().profile.boardView; - if (boardView === 'board-view-swimlanes') - swimlaneId = this.parentComponent().parentComponent().data()._id; - else if ((boardView === 'board-view-lists') || (boardView === 'board-view-cal')) - swimlaneId = boardId.getDefaultSwimline()._id; - + let cardType = 'cardType-card'; if (title) { + if (board.isTemplatesBoard()) { + swimlaneId = this.parentComponent().parentComponent().data()._id; // Always swimlanes view + const swimlane = Swimlanes.findOne(swimlaneId); + // If this is the card templates swimlane, insert a card template + if (swimlane.isCardTemplatesSwimlane()) + cardType = 'template-card'; + // If this is the board templates swimlane, insert a board template and a linked card + else if (swimlane.isBoardTemplatesSwimlane()) { + linkedId = Boards.insert({ + title, + permission: 'private', + type: 'template-board', + }); + Swimlanes.insert({ + title: TAPi18n.__('default'), + boardId: linkedId, + }); + cardType = 'cardType-linkedBoard'; + } + } else if (boardView === 'board-view-swimlanes') + swimlaneId = this.parentComponent().parentComponent().data()._id; + else if ((boardView === 'board-view-lists') || (boardView === 'board-view-cal')) + swimlaneId = board.getDefaultSwimline()._id; + const _id = Cards.insert({ title, members, labelIds, customFields, listId: this.data()._id, - boardId: boardId._id, + boardId: board._id, sort: sortIndex, swimlaneId, - type: 'cardType-card', + type: cardType, + linkedId, }); // if the displayed card count is less than the total cards in the list, @@ -127,9 +127,9 @@ BlazeComponent.extendComponent({ const methodName = evt.shiftKey ? 'toggleRange' : 'toggle'; MultiSelection[methodName](this.currentData()._id); - // If the card is already selected, we want to de-select it. - // XXX We should probably modify the minicard href attribute instead of - // overwriting the event in case the card is already selected. + // If the card is already selected, we want to de-select it. + // XXX We should probably modify the minicard href attribute instead of + // overwriting the event in case the card is already selected. } else if (Session.equals('currentCard', this.currentData()._id)) { evt.stopImmediatePropagation(); evt.preventDefault(); @@ -149,7 +149,8 @@ BlazeComponent.extendComponent({ idOrNull(swimlaneId) { const currentUser = Meteor.user(); - if (currentUser.profile.boardView === 'board-view-swimlanes') + if (currentUser.profile.boardView === 'board-view-swimlanes' || + this.data().board().isTemplatesBoard()) return swimlaneId; return undefined; }, @@ -168,38 +169,11 @@ BlazeComponent.extendComponent({ }); }, - spinnerInView(container) { - const parentViewHeight = container.clientHeight; - const bottomViewPosition = container.scrollTop + parentViewHeight; - - const spinner = this.find('.sk-spinner-list'); - - const threshold = spinner.offsetTop; - - return bottomViewPosition > threshold; - }, - showSpinner(swimlaneId) { const list = Template.currentData(); return list.cards(swimlaneId).count() > this.cardlimit.get(); }, - updateList(container) { - // first, if the spinner is not rendered, we have reached the end of - // the list of cards, so skip and disable firing the events - const target = this.find('.sk-spinner-list'); - if (!target) { - this.$(container).off('scroll'); - $(window).off(`resize.${this.data().listId}`); - return; - } - - if (this.spinnerInView(container)) { - this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter); - Ps.update(container); - } - }, - canSeeAddCard() { return !this.reachedWipLimit() && Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly(); }, @@ -269,8 +243,8 @@ BlazeComponent.extendComponent({ // work. $form.find('button[type=submit]').click(); - // Pressing Tab should open the form of the next column, and Maj+Tab go - // in the reverse order + // Pressing Tab should open the form of the next column, and Maj+Tab go + // in the reverse order } else if (evt.keyCode === 9) { evt.preventDefault(); const isReverse = evt.shiftKey; @@ -292,7 +266,8 @@ BlazeComponent.extendComponent({ return [{ keydown: this.pressKey, 'click .js-link': Popup.open('linkCard'), - 'click .js-search': Popup.open('searchCard'), + 'click .js-search': Popup.open('searchElement'), + 'click .js-card-template': Popup.open('searchElement'), }]; }, @@ -330,7 +305,7 @@ BlazeComponent.extendComponent({ const currentBoard = Boards.findOne(Session.get('currentBoard')); callback($.map(currentBoard.labels, (label) => { if (label.name.indexOf(term) > -1 || - label.color.indexOf(term) > -1) { + label.color.indexOf(term) > -1) { return label; } return null; @@ -367,17 +342,7 @@ BlazeComponent.extendComponent({ BlazeComponent.extendComponent({ onCreated() { - // Prefetch first non-current board id - const boardId = Boards.findOne({ - archived: false, - 'members.userId': Meteor.userId(), - _id: {$ne: Session.get('currentBoard')}, - }, { - sort: ['title'], - })._id; - // Subscribe to this board - subManager.subscribe('board', boardId); - this.selectedBoardId = new ReactiveVar(boardId); + this.selectedBoardId = new ReactiveVar(''); this.selectedSwimlaneId = new ReactiveVar(''); this.selectedListId = new ReactiveVar(''); @@ -403,6 +368,7 @@ BlazeComponent.extendComponent({ archived: false, 'members.userId': Meteor.userId(), _id: {$ne: Session.get('currentBoard')}, + type: 'board', }, { sort: ['title'], }); @@ -410,7 +376,7 @@ BlazeComponent.extendComponent({ }, swimlanes() { - if (!this.selectedBoardId) { + if (!this.selectedBoardId.get()) { return []; } const swimlanes = Swimlanes.find({boardId: this.selectedBoardId.get()}); @@ -420,7 +386,7 @@ BlazeComponent.extendComponent({ }, lists() { - if (!this.selectedBoardId) { + if (!this.selectedBoardId.get()) { return []; } const lists = Lists.find({boardId: this.selectedBoardId.get()}); @@ -441,6 +407,7 @@ BlazeComponent.extendComponent({ archived: false, linkedId: {$nin: ownCardsIds}, _id: {$nin: ownCardsIds}, + type: {$nin: ['template-card']}, }); }, @@ -508,12 +475,25 @@ BlazeComponent.extendComponent({ }, onCreated() { - // Prefetch first non-current board id - let board = Boards.findOne({ - archived: false, - 'members.userId': Meteor.userId(), - _id: {$ne: Session.get('currentBoard')}, - }); + this.isCardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-card-template'); + this.isListTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-list-template'); + this.isSwimlaneTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-open-add-swimlane-menu'); + this.isBoardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-add-board'); + this.isTemplateSearch = this.isCardTemplateSearch || + this.isListTemplateSearch || + this.isSwimlaneTemplateSearch || + this.isBoardTemplateSearch; + let board = {}; + if (this.isTemplateSearch) { + board = Boards.findOne(Meteor.user().profile.templatesBoardId); + } else { + // Prefetch first non-current board id + board = Boards.findOne({ + archived: false, + 'members.userId': Meteor.userId(), + _id: {$nin: [Session.get('currentBoard'), Meteor.user().profile.templatesBoardId]}, + }); + } if (!board) { Popup.close(); return; @@ -523,20 +503,21 @@ BlazeComponent.extendComponent({ subManager.subscribe('board', boardId); this.selectedBoardId = new ReactiveVar(boardId); - this.boardId = Session.get('currentBoard'); - // In order to get current board info - subManager.subscribe('board', this.boardId); - board = Boards.findOne(this.boardId); - // List where to insert card - const list = $(Popup._getTopStack().openerElement).closest('.js-list'); - this.listId = Blaze.getData(list[0])._id; - // Swimlane where to insert card - const swimlane = $(Popup._getTopStack().openerElement).closest('.js-swimlane'); - this.swimlaneId = ''; - if (board.view === 'board-view-swimlanes') - this.swimlaneId = Blaze.getData(swimlane[0])._id; - else - this.swimlaneId = Swimlanes.findOne({boardId: this.boardId})._id; + if (!this.isBoardTemplateSearch) { + this.boardId = Session.get('currentBoard'); + // In order to get current board info + subManager.subscribe('board', this.boardId); + this.swimlaneId = ''; + // Swimlane where to insert card + const swimlane = $(Popup._getTopStack().openerElement).parents('.js-swimlane'); + if (Meteor.user().profile.boardView === 'board-view-swimlanes') + this.swimlaneId = Blaze.getData(swimlane[0])._id; + else + this.swimlaneId = Swimlanes.findOne({boardId: this.boardId})._id; + // List where to insert card + const list = $(Popup._getTopStack().openerElement).closest('.js-list'); + this.listId = Blaze.getData(list[0])._id; + } this.term = new ReactiveVar(''); }, @@ -545,6 +526,7 @@ BlazeComponent.extendComponent({ archived: false, 'members.userId': Meteor.userId(), _id: {$ne: Session.get('currentBoard')}, + type: 'board', }, { sort: ['title'], }); @@ -556,7 +538,21 @@ BlazeComponent.extendComponent({ return []; } const board = Boards.findOne(this.selectedBoardId.get()); - return board.searchCards(this.term.get(), false); + if (!this.isTemplateSearch || this.isCardTemplateSearch) { + return board.searchCards(this.term.get(), false); + } else if (this.isListTemplateSearch) { + return board.searchLists(this.term.get()); + } else if (this.isSwimlaneTemplateSearch) { + return board.searchSwimlanes(this.term.get()); + } else if (this.isBoardTemplateSearch) { + const boards = board.searchBoards(this.term.get()); + boards.forEach((board) => { + subManager.subscribe('board', board.linkedId); + }); + return boards; + } else { + return []; + } }, events() { @@ -570,20 +566,99 @@ BlazeComponent.extendComponent({ this.term.set(evt.target.searchTerm.value); }, 'click .js-minicard'(evt) { - // LINK CARD - const card = Blaze.getData(evt.currentTarget); - const _id = Cards.insert({ - title: card.title, //dummy - listId: this.listId, - swimlaneId: this.swimlaneId, - boardId: this.boardId, - sort: Lists.findOne(this.listId).cards().count(), - type: 'cardType-linkedCard', - linkedId: card.linkedId || card._id, - }); - Filter.addException(_id); + // 0. Common + const title = $('.js-element-title').val().trim(); + if (!title) + return; + const element = Blaze.getData(evt.currentTarget); + element.title = title; + let _id = ''; + if (!this.isTemplateSearch || this.isCardTemplateSearch) { + // Card insertion + // 1. Common + element.sort = Lists.findOne(this.listId).cards().count(); + // 1.A From template + if (this.isTemplateSearch) { + element.type = 'cardType-card'; + element.linkedId = ''; + _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); + } + Filter.addException(_id); + // List insertion + } else if (this.isListTemplateSearch) { + element.sort = Swimlanes.findOne(this.swimlaneId).lists().count(); + element.type = 'list'; + _id = element.copy(this.boardId, this.swimlaneId); + } else if (this.isSwimlaneTemplateSearch) { + element.sort = Boards.findOne(this.boardId).swimlanes().count(); + element.type = 'swimlalne'; + _id = element.copy(this.boardId); + } else if (this.isBoardTemplateSearch) { + board = Boards.findOne(element.linkedId); + board.sort = Boards.find({archived: false}).count(); + board.type = 'board'; + board.title = element.title; + delete board.slug; + _id = board.copy(); + } Popup.close(); }, }]; }, -}).register('searchCardPopup'); +}).register('searchElementPopup'); + +BlazeComponent.extendComponent({ + onCreated() { + this.cardlimit = this.parentComponent().cardlimit; + + this.listId = this.parentComponent().data()._id; + this.swimlaneId = ''; + + const boardView = Meteor.user().profile.boardView; + if (boardView === 'board-view-swimlanes') + this.swimlaneId = this.parentComponent().parentComponent().parentComponent().data()._id; + }, + + onRendered() { + this.spinner = this.find('.sk-spinner-list'); + this.container = this.$(this.spinner).parents('.js-perfect-scrollbar')[0]; + + $(this.container).on(`scroll.spinner_${this.swimlaneId}_${this.listId}`, () => this.updateList()); + $(window).on(`resize.spinner_${this.swimlaneId}_${this.listId}`, () => this.updateList()); + + this.updateList(); + }, + + onDestroyed() { + $(this.container).off(`scroll.spinner_${this.swimlaneId}_${this.listId}`); + $(window).off(`resize.spinner_${this.swimlaneId}_${this.listId}`); + }, + + updateList() { + if (this.spinnerInView()) { + this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter); + window.requestIdleCallback(() => this.updateList()); + } + }, + + spinnerInView() { + const parentViewHeight = this.container.clientHeight; + const bottomViewPosition = this.container.scrollTop + parentViewHeight; + + const threshold = this.spinner.offsetTop; + + // spinner deleted + if (!this.spinner.offsetTop) { + return false; + } + + return bottomViewPosition > threshold; + }, + +}).register('spinnerList'); diff --git a/client/components/lists/minilist.jade b/client/components/lists/minilist.jade new file mode 100644 index 00000000..e34214c4 --- /dev/null +++ b/client/components/lists/minilist.jade @@ -0,0 +1,8 @@ +template(name="minilist") + .minicard( + class="minicard-{{colorClass}}") + .minicard-title + .handle + .fa.fa-arrows + +viewer + = title diff --git a/client/components/main/editor.js b/client/components/main/editor.js index 20ece562..88d8abf0 100755 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -36,7 +36,10 @@ import sanitizeXss from 'xss'; const at = HTML.CharRef({html: '@', str: '@'}); Blaze.Template.registerHelper('mentions', new Template('mentions', function() { const view = this; + let content = Blaze.toHTML(view.templateContentBlock); const currentBoard = Boards.findOne(Session.get('currentBoard')); + if (!currentBoard) + return HTML.Raw(sanitizeXss(content)); const knowedUsers = currentBoard.members.map((member) => { const u = Users.findOne(member.userId); if(u){ @@ -45,7 +48,6 @@ Blaze.Template.registerHelper('mentions', new Template('mentions', function() { return member; }); const mentionRegex = /\B@([\w.]*)/gi; - let content = Blaze.toHTML(view.templateContentBlock); let currentMention; while ((currentMention = mentionRegex.exec(content)) !== null) { diff --git a/client/components/main/header.jade b/client/components/main/header.jade index e21ce096..75e84c0c 100644 --- a/client/components/main/header.jade +++ b/client/components/main/header.jade @@ -4,39 +4,38 @@ template(name="header") list all starred boards with a link to go there. This is inspired by the Reddit "subreddit" bar. The first link goes to the boards page. - unless isSandstorm - if currentUser - #header-quick-access(class=currentBoard.colorClass) - if isMiniScreen - ul - li - a(href="{{pathFor 'home'}}") - span.fa.fa-home + if currentUser + #header-quick-access(class=currentBoard.colorClass) + if isMiniScreen + ul + li + a(href="{{pathFor 'home'}}") + span.fa.fa-home - if currentList - each currentBoard.lists - li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}") - a.js-select-list - = title - #header-new-board-icon - else - ul - li - a(href="{{pathFor 'home'}}") - span.fa.fa-home - | {{_ 'all-boards'}} - each currentUser.starredBoards - li.separator - - li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}") - a(href="{{pathFor 'board' id=_id slug=slug}}") + if currentList + each currentBoard.lists + li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}") + a.js-select-list = title - else - li.current {{_ 'quick-access-description'}} + #header-new-board-icon + else + ul + li + a(href="{{pathFor 'home'}}") + span.fa.fa-home + | {{_ 'all-boards'}} + each currentUser.starredBoards + li.separator - + li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}") + a(href="{{pathFor 'board' id=_id slug=slug}}") + = title + else + li.current {{_ 'quick-access-description'}} - a#header-new-board-icon.js-create-board - i.fa.fa-plus(title="Create a new board") + a#header-new-board-icon.js-create-board + i.fa.fa-plus(title="Create a new board") - +headerUserBar + +headerUserBar #header(class=currentBoard.colorClass) //- @@ -46,19 +45,16 @@ template(name="header") #header-main-bar(class="{{#if wrappedHeader}}wrapper{{/if}}") +Template.dynamic(template=headerBar) - unless hideLogo + //unless hideLogo //- On sandstorm, the logo shouldn't be clickable, because we only have one page/document on it, and we don't want to see the home page containing the list of all boards. - if isSandstorm - .wekan-logo - img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan") - else - unless currentSetting.hideLogo - a.wekan-logo(href="{{pathFor 'home'}}" title="{{_ 'header-logo-title'}}") - img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan") + + // unless currentSetting.hideLogo + // a.wekan-logo(href="{{pathFor 'home'}}" title="{{_ 'header-logo-title'}}") + // img(src="{{pathFor '/logo-header.png'}}" alt="") if appIsOffline +offlineWarning diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade index a6115ec1..ac2e4bf3 100644 --- a/client/components/main/layouts.jade +++ b/client/components/main/layouts.jade @@ -1,5 +1,5 @@ head - title Wekan + title meta(name="viewport" content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0") meta(http-equiv="X-UA-Compatible" content="IE=edge") @@ -7,24 +7,20 @@ 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="/public/wekan-favicon.png") - link(rel="apple-touch-icon" href="/public/wekan-favicon.png") - link(rel="mask-icon" href="/public/wekan-150.svg") - link(rel="manifest" href="/public/wekan-manifest.json") + 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") template(name="userFormsLayout") section.auth-layout - unless currentSetting.hideLogo - h1.at-form-landing-logo - img(src="{{pathFor '/wekan-logo.png'}}" alt="Wekan") - if currentSetting.hideLogo - h1 - br - br + h1 + br + br section.auth-dialog +Template.dynamic(template=content) if currentSetting.displayAuthenticationMethod - +connectionMethod + +connectionMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod) div.at-form-lang select.select-lang.js-userform-set-language each languages diff --git a/client/components/settings/connectionMethod.jade b/client/components/settings/connectionMethod.jade index ac4c8c64..d191929f 100644 --- a/client/components/settings/connectionMethod.jade +++ b/client/components/settings/connectionMethod.jade @@ -2,5 +2,8 @@ template(name='connectionMethod') div.at-form-authentication label {{_ 'authentication-method'}} select.select-authentication - each authentications - option(value="{{value}}") {{_ value}} + each authentications + if isSelected value + option(value="{{value}}" selected) {{_ value}} + else + option(value="{{value}}") {{_ value}}
\ No newline at end of file diff --git a/client/components/settings/connectionMethod.js b/client/components/settings/connectionMethod.js index 9fe8f382..db9da25f 100644 --- a/client/components/settings/connectionMethod.js +++ b/client/components/settings/connectionMethod.js @@ -31,4 +31,7 @@ Template.connectionMethod.helpers({ authentications() { return Template.instance().authenticationMethods.get(); }, + isSelected(match) { + return Template.instance().data.authenticationMethod === match; + }, }); diff --git a/client/components/settings/settingBody.jade b/client/components/settings/settingBody.jade index 220dbb50..89911e09 100644 --- a/client/components/settings/settingBody.jade +++ b/client/components/settings/settingBody.jade @@ -134,7 +134,7 @@ template(name='announcementSettings') template(name='layoutSettings') ul#layout-setting.setting-detail - li.layout-form + //li.layout-form .title {{_ 'hide-logo'}} .form-group.flex input.form-control#hide-logo(type="radio" name="hideLogo" value="true" checked="{{#if currentSetting.hideLogo}}checked{{/if}}") @@ -154,7 +154,7 @@ template(name='layoutSettings') li.layout-form .title {{_ 'custom-product-name'}} .form-group - input.form-control#product-name(type="text", placeholder="Wekan" value="{{currentSetting.productName}}") + input.form-control#product-name(type="text", placeholder="" value="{{currentSetting.productName}}") li.layout-form .title {{_ 'add-custom-html-after-body-start'}} textarea#customHTMLafterBodyStart.form-control= currentSetting.customHTMLafterBodyStart @@ -171,4 +171,4 @@ template(name='selectAuthenticationMethod') if isSelected value option(value="{{value}}" selected) {{_ value}} else - option(value="{{value}}") {{_ value}}
\ No newline at end of file + option(value="{{value}}") {{_ value}} diff --git a/client/components/settings/settingBody.js b/client/components/settings/settingBody.js index 2f58d551..8279a092 100644 --- a/client/components/settings/settingBody.js +++ b/client/components/settings/settingBody.js @@ -90,7 +90,7 @@ BlazeComponent.extendComponent({ }, inviteThroughEmail() { - const emails = $('#email-to-invite').val().trim().split('\n').join(',').split(','); + const emails = $('#email-to-invite').val().toLowerCase().trim().split('\n').join(',').split(','); const boardsToInvite = []; $('.js-toggle-board-choose .materialCheckBox.is-checked').each(function () { boardsToInvite.push($(this).data('id')); diff --git a/client/components/settings/settingBody.styl b/client/components/settings/settingBody.styl index 7f8bd4c0..dbf91a6c 100644 --- a/client/components/settings/settingBody.styl +++ b/client/components/settings/settingBody.styl @@ -52,6 +52,10 @@ .main-body padding: 0.1em 1em + -webkit-user-select: auto // Safari 3.1+ + -moz-user-select: auto // Firefox 2+ + -ms-user-select: auto // IE 10+ + user-select: auto // Standard syntax ul li diff --git a/client/components/settings/settingHeader.jade b/client/components/settings/settingHeader.jade index c2d4db3a..221c1b79 100644 --- a/client/components/settings/settingHeader.jade +++ b/client/components/settings/settingHeader.jade @@ -4,22 +4,21 @@ template(name="settingHeaderBar") .setting-header-btns.left unless isMiniScreen - unless isSandstorm - if currentUser - a.setting-header-btn.settings(href="{{pathFor 'setting'}}") - i.fa(class="fa-cog") - span {{_ 'settings'}} + if currentUser + a.setting-header-btn.settings(href="{{pathFor 'setting'}}") + i.fa(class="fa-cog") + span {{_ 'settings'}} - a.setting-header-btn.people(href="{{pathFor 'people'}}") - i.fa(class="fa-users") - span {{_ 'people'}} + a.setting-header-btn.people(href="{{pathFor 'people'}}") + i.fa(class="fa-users") + span {{_ 'people'}} - a.setting-header-btn.informations(href="{{pathFor 'information'}}") - i.fa(class="fa-info-circle") - span {{_ 'info'}} + a.setting-header-btn.informations(href="{{pathFor 'information'}}") + i.fa(class="fa-info-circle") + span {{_ 'info'}} - else - a.setting-header-btn.js-log-in( - title="{{_ 'log-in'}}") - i.fa.fa-sign-in - span {{_ 'log-in'}} + else + a.setting-header-btn.js-log-in( + title="{{_ 'log-in'}}") + i.fa.fa-sign-in + span {{_ 'log-in'}} diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index ec88ce7e..4e4d355c 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -1,9 +1,9 @@ template(name="sidebar") .board-sidebar.sidebar(class="{{#if isOpen}}is-open{{/if}}") - a.sidebar-tongue.js-toggle-sidebar( - class="{{#if isTongueHidden}}is-hidden{{/if}}", - title="{{showTongueTitle}}") - i.fa.fa-angle-left + //a.sidebar-tongue.js-toggle-sidebar( + // class="{{#if isTongueHidden}}is-hidden{{/if}}", + // title="{{showTongueTitle}}") + // i.fa.fa-navicon .sidebar-shadow .sidebar-content.sidebar-shortcuts a.board-header-btn.js-shortcuts @@ -11,7 +11,7 @@ template(name="sidebar") span {{_ 'keyboard-shortcuts' }} .sidebar-content.js-board-sidebar-content.js-perfect-scrollbar a.hide-btn.js-hide-sidebar - i.fa.fa-angle-right + i.fa.fa-navicon unless isDefaultView h2 a.fa.fa-chevron-left.js-back-home @@ -34,6 +34,9 @@ template(name="membersWidget") h3 i.fa.fa-user | {{_ 'members'}} + a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}").right + i.board-header-btn-icon.fa.fa-cog + .board-widget-content each currentBoard.activeMembers +userAvatar(userId=this.userId showStatus=true) @@ -53,6 +56,130 @@ template(name="membersWidget") button.js-member-invite-accept.primary {{_ 'accept'}} button.js-member-invite-decline {{_ 'decline'}} +template(name="boardChangeColorPopup") + .board-backgrounds-list.clearfix + each backgroundColors + .board-background-select.js-select-background + span.background-box(class="board-color-{{this}}") + if isSelected + i.fa.fa-check + +template(name="boardSubtaskSettingsPopup") + form.board-subtask-settings + h3 {{_ 'show-parent-in-minicard'}} + a#prefix-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}") + .materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}") + span {{_ 'prefix-with-full-path'}} + a#prefix-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}") + .materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}") + span {{_ 'prefix-with-parent'}} + a#subtext-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}") + .materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}") + span {{_ 'subtext-with-full-path'}} + a#subtext-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}") + .materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}") + span {{_ 'subtext-with-parent'}} + a#no-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}") + .materialCheckBox(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}") + span {{_ 'no-parent'}} + div + hr + + div.check-div + a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}") + span {{_ 'show-subtasks-field'}} + + label + | {{_ 'deposit-subtasks-board'}} + select.js-field-deposit-board(disabled="{{#unless allowsSubtasks}}disabled{{/unless}}") + each boards + if isBoardSelected + option(value=_id selected="selected") {{title}} + else + option(value=_id) {{title}} + if isNullBoardSelected + option(value='null' selected="selected") {{_ 'custom-field-dropdown-none'}} + else + option(value='null') {{_ 'custom-field-dropdown-none'}} + div + hr + + label + | {{_ 'deposit-subtasks-list'}} + select.js-field-deposit-list(disabled="{{#unless hasLists}}disabled{{/unless}}") + each lists + if isListSelected + option(value=_id selected="selected") {{title}} + else + option(value=_id) {{title}} + +template(name="chooseBoardSource") + ul.pop-over-list + li + a(href="{{pathFor '/import/trello'}}") {{_ 'from-trello'}} + li + a(href="{{pathFor '/import/wekan'}}") {{_ 'from-wekan'}} + +template(name="archiveBoardPopup") + p {{_ 'close-board-pop'}} + button.js-confirm.negate.full(type="submit") {{_ 'archive'}} + +template(name="outgoingWebhooksPopup") + each integrations + form.integration-form + if title + h4 {{title}} + else + h4 {{_ 'no-name'}} + label + | URL + input.js-outgoing-webhooks-url(type="text" name="url" value=url) + input(type="hidden" value=_id name="id") + input.primary.wide(type="submit" value="{{_ 'save'}}") + form.integration-form + h4 + | {{_ 'new-outgoing-webhook'}} + label + | URL + input.js-outgoing-webhooks-url(type="text" name="url" autofocus) + input.primary.wide(type="submit" value="{{_ 'save'}}") + +template(name="boardMenuPopup") + ul.pop-over-list + li: a.js-custom-fields {{_ 'custom-fields'}} + li: a.js-open-archives {{_ 'archived-items'}} + if currentUser.isBoardAdmin + li: a.js-change-board-color {{_ 'board-change-color'}} + //- + XXX Language should be handled by sandstorm, but for now display a + language selection link in the board menu. This link is normally present + in the header bar that is not displayed on sandstorm. + if isSandstorm + li: a.js-change-language {{_ 'language'}} + unless isSandstorm + if currentUser.isBoardAdmin + hr + ul.pop-over-list + li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}} + unless currentBoard.isTemplatesBoard + li: a.js-archive-board {{_ 'archive-board'}} + li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}} + hr + ul.pop-over-list + li: a.js-subtask-settings {{_ 'subtask-settings'}} + + if isSandstorm + hr + ul.pop-over-list + li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}} + li: a.js-import-board {{_ 'import-board-c'}} + li: a.js-archive-board {{_ 'archive-board'}} + li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}} + hr + ul.pop-over-list + li: a.js-subtask-settings {{_ 'subtask-settings'}} + template(name="labelsWidget") .board-widget.board-widget-labels h3 @@ -83,17 +210,16 @@ template(name="memberPopup") ul.pop-over-list li a.js-filter-member {{_ 'filter-cards'}} - unless isSandstorm - if currentUser.isBoardAdmin - li - a.js-change-role - | {{_ 'change-permissions'}} - span.quiet (#{memberType}) + if currentUser.isBoardAdmin li - if $eq currentUser._id userId - a.js-leave-member {{_ 'leave-board'}} - else if currentUser.isBoardAdmin - a.js-remove-member {{_ 'remove-from-board'}} + a.js-change-role + | {{_ 'change-permissions'}} + span.quiet (#{memberType}) + li + if $eq currentUser._id userId + a.js-leave-member {{_ 'leave-board'}} + else if currentUser.isBoardAdmin + a.js-remove-member {{_ 'remove-from-board'}} template(name="removeMemberPopup") diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 83b12666..e8de3c96 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -142,6 +142,52 @@ Template.memberPopup.helpers({ }, }); +Template.boardMenuPopup.events({ + 'click .js-rename-board': Popup.open('boardChangeTitle'), + 'click .js-custom-fields'() { + Sidebar.setView('customFields'); + Popup.close(); + }, + 'click .js-open-archives'() { + Sidebar.setView('archives'); + Popup.close(); + }, + 'click .js-change-board-color': Popup.open('boardChangeColor'), + 'click .js-change-language': Popup.open('changeLanguage'), + 'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() { + const currentBoard = Boards.findOne(Session.get('currentBoard')); + currentBoard.archive(); + // XXX We should have some kind of notification on top of the page to + // confirm that the board was successfully archived. + FlowRouter.go('home'); + }), + 'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() { + const currentBoard = Boards.findOne(Session.get('currentBoard')); + Popup.close(); + Boards.remove(currentBoard._id); + FlowRouter.go('home'); + }), + 'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'), + 'click .js-import-board': Popup.open('chooseBoardSource'), + 'click .js-subtask-settings': Popup.open('boardSubtaskSettings'), +}); + +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.memberPopup.events({ 'click .js-filter-member'() { Filter.members.toggle(this.userId); @@ -190,7 +236,14 @@ Template.membersWidget.helpers({ Template.membersWidget.events({ 'click .js-member': Popup.open('member'), + 'click .js-open-board-menu': Popup.open('boardMenu'), 'click .js-manage-board-members': Popup.open('addMember'), + 'click .js-import': Popup.open('boardImportBoard'), + submit: this.onSubmit, + 'click .js-import-board': Popup.open('chooseBoardSource'), + 'click .js-open-archived-board'() { + Modal.open('archivedBoards'); + }, 'click .sandstorm-powerbox-request-identity'() { window.sandstormRequestIdentity(); }, @@ -209,6 +262,59 @@ Template.membersWidget.events({ }, }); +BlazeComponent.extendComponent({ + integrations() { + const boardId = Session.get('currentBoard'); + return Integrations.find({ boardId: `${boardId}` }).fetch(); + }, + + integration(id) { + const boardId = Session.get('currentBoard'); + return Integrations.findOne({ _id: id, boardId: `${boardId}` }); + }, + + events() { + return [{ + 'submit'(evt) { + evt.preventDefault(); + const url = evt.target.url.value; + const boardId = Session.get('currentBoard'); + let id = null; + let integration = null; + if (evt.target.id) { + id = evt.target.id.value; + integration = this.integration(id); + if (url) { + Integrations.update(integration._id, { + $set: { + url: `${url}`, + }, + }); + } else { + Integrations.remove(integration._id); + } + } else if (url) { + Integrations.insert({ + userId: Meteor.userId(), + enabled: true, + type: 'outgoing-webhooks', + url: `${url}`, + boardId: `${boardId}`, + activities: ['all'], + }); + } + Popup.close(); + }, + }]; + }, +}).register('outgoingWebhooksPopup'); + +BlazeComponent.extendComponent({ + template() { + return 'chooseBoardSource'; + }, +}).register('chooseBoardSourcePopup'); + Template.labelsWidget.events({ 'click .js-label': Popup.open('editLabel'), 'click .js-add-label': Popup.open('createLabel'), @@ -259,6 +365,124 @@ Template.membersWidget.onRendered(draggableMembersLabelsWidgets); Template.labelsWidget.onRendered(draggableMembersLabelsWidgets); BlazeComponent.extendComponent({ + backgroundColors() { + return Boards.simpleSchema()._schema.color.allowedValues; + }, + + isSelected() { + const currentBoard = Boards.findOne(Session.get('currentBoard')); + return currentBoard.color === this.currentData().toString(); + }, + + events() { + return [{ + 'click .js-select-background'(evt) { + const currentBoard = Boards.findOne(Session.get('currentBoard')); + const newColor = this.currentData().toString(); + currentBoard.setColor(newColor); + evt.preventDefault(); + }, + }]; + }, +}).register('boardChangeColorPopup'); + +BlazeComponent.extendComponent({ + onCreated() { + this.currentBoard = Boards.findOne(Session.get('currentBoard')); + }, + + allowsSubtasks() { + return this.currentBoard.allowsSubtasks; + }, + + isBoardSelected() { + return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id; + }, + + isNullBoardSelected() { + return (this.currentBoard.subtasksDefaultBoardId === null) || (this.currentBoard.subtasksDefaultBoardId === undefined); + }, + + boards() { + return Boards.find({ + archived: false, + 'members.userId': Meteor.userId(), + }, { + sort: ['title'], + }); + }, + + lists() { + return Lists.find({ + boardId: this.currentBoard._id, + archived: false, + }, { + sort: ['title'], + }); + }, + + hasLists() { + return this.lists().count() > 0; + }, + + isListSelected() { + return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id; + }, + + presentParentTask() { + let result = this.currentBoard.presentParentTask; + if ((result === null) || (result === undefined)) { + result = 'no-parent'; + } + return result; + }, + + events() { + return [{ + 'click .js-field-has-subtasks'(evt) { + evt.preventDefault(); + this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks; + this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks); + $('.js-field-has-subtasks .materialCheckBox').toggleClass('is-checked', this.currentBoard.allowsSubtasks); + $('.js-field-has-subtasks').toggleClass('is-checked', this.currentBoard.allowsSubtasks); + $('.js-field-deposit-board').prop('disabled', !this.currentBoard.allowsSubtasks); + }, + 'change .js-field-deposit-board'(evt) { + let value = evt.target.value; + if (value === 'null') { + value = null; + } + this.currentBoard.setSubtasksDefaultBoardId(value); + evt.preventDefault(); + }, + 'change .js-field-deposit-list'(evt) { + this.currentBoard.setSubtasksDefaultListId(evt.target.value); + evt.preventDefault(); + }, + 'click .js-field-show-parent-in-minicard'(evt) { + const value = evt.target.id || $(evt.target).parent()[0].id || $(evt.target).parent()[0].parent()[0].id; + const options = [ + 'prefix-with-full-path', + 'prefix-with-parent', + 'subtext-with-full-path', + 'subtext-with-parent', + 'no-parent']; + options.forEach(function(element) { + if (element !== value) { + $(`#${element} .materialCheckBox`).toggleClass('is-checked', false); + $(`#${element}`).toggleClass('is-checked', false); + } + }); + $(`#${value} .materialCheckBox`).toggleClass('is-checked', true); + $(`#${value}`).toggleClass('is-checked', true); + this.currentBoard.setPresentParentTask(value); + evt.preventDefault(); + }, + }]; + }, +}).register('boardSubtaskSettingsPopup'); + +BlazeComponent.extendComponent({ onCreated() { this.error = new ReactiveVar(''); this.loading = new ReactiveVar(false); diff --git a/client/components/sidebar/sidebarCustomFields.js b/client/components/sidebar/sidebarCustomFields.js index ccc8ffb9..28af973b 100644 --- a/client/components/sidebar/sidebarCustomFields.js +++ b/client/components/sidebar/sidebarCustomFields.js @@ -2,7 +2,7 @@ BlazeComponent.extendComponent({ customFields() { return CustomFields.find({ - boardId: Session.get('currentBoard'), + boardIds: {$in: [Session.get('currentBoard')]}, }); }, @@ -103,7 +103,6 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({ evt.preventDefault(); const data = { - boardId: Session.get('currentBoard'), name: this.find('.js-field-name').value.trim(), type: this.type.get(), settings: this.getSettings(), @@ -114,6 +113,7 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({ // insert or update if (!this.data()._id) { + data.boardIds = [Session.get('currentBoard')]; CustomFields.insert(data); } else { CustomFields.update(this.data()._id, {$set: data}); @@ -122,8 +122,16 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({ Popup.back(); }, 'click .js-delete-custom-field': Popup.afterConfirm('deleteCustomField', function() { - const customFieldId = this._id; - CustomFields.remove(customFieldId); + const customField = CustomFields.findOne(this._id); + if (customField.boardIds.length > 1) { + CustomFields.update(customField._id, { + $pull: { + boardIds: Session.get('currentBoard'), + }, + }); + } else { + CustomFields.remove(customField._id); + } Popup.close(); }), }]; diff --git a/client/components/swimlanes/miniswimlane.jade b/client/components/swimlanes/miniswimlane.jade new file mode 100644 index 00000000..d4be8599 --- /dev/null +++ b/client/components/swimlanes/miniswimlane.jade @@ -0,0 +1,8 @@ +template(name="miniswimlane") + .minicard( + class="minicard-{{colorClass}}") + .minicard-title + .handle + .fa.fa-arrows + +viewer + = title diff --git a/client/components/swimlanes/swimlaneHeader.jade b/client/components/swimlanes/swimlaneHeader.jade index 33eb5731..de9621d5 100644 --- a/client/components/swimlanes/swimlaneHeader.jade +++ b/client/components/swimlanes/swimlaneHeader.jade @@ -1,15 +1,21 @@ template(name="swimlaneHeader") .swimlane-header-wrap.js-swimlane-header(class='{{#if colorClass}}swimlane-{{colorClass}}{{/if}}') - +inlinedForm - +editSwimlaneTitleForm + if this.isTemplateContainer + +swimlaneFixedHeader(this) else - .swimlane-header( - class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}") - = title - .swimlane-header-menu - unless currentUser.isCommentOnly - a.fa.fa-plus.js-open-add-swimlane-menu.swimlane-header-plus-icon - a.fa.fa-navicon.js-open-swimlane-menu + +inlinedForm + +editSwimlaneTitleForm + else + +swimlaneFixedHeader(this) + +template(name="swimlaneFixedHeader") + .swimlane-header( + class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}") + = title + .swimlane-header-menu + unless currentUser.isCommentOnly + a.fa.fa-plus.js-open-add-swimlane-menu.swimlane-header-plus-icon + a.fa.fa-navicon.js-open-swimlane-menu template(name="editSwimlaneTitleForm") .list-composer @@ -22,9 +28,10 @@ template(name="swimlaneActionPopup") unless currentUser.isCommentOnly ul.pop-over-list li: a.js-set-swimlane-color {{_ 'select-color'}} - hr - ul.pop-over-list - li: a.js-close-swimlane {{_ 'archive-swimlane'}} + unless this.isTemplateContainer + hr + ul.pop-over-list + li: a.js-close-swimlane {{_ 'archive-swimlane'}} template(name="swimlaneAddPopup") unless currentUser.isCommentOnly @@ -33,6 +40,11 @@ template(name="swimlaneAddPopup") autocomplete="off" autofocus) .edit-controls.clearfix button.primary.confirm(type="submit") {{_ 'add'}} + unless currentBoard.isTemplatesBoard + unless currentBoard.isTemplateBoard + span.quiet + | {{_ 'or'}} + a.js-swimlane-template {{_ 'template'}} template(name="setSwimlaneColorPopup") form.edit-label diff --git a/client/components/swimlanes/swimlaneHeader.js b/client/components/swimlanes/swimlaneHeader.js index 1004cb25..e7f3cc76 100644 --- a/client/components/swimlanes/swimlaneHeader.js +++ b/client/components/swimlanes/swimlaneHeader.js @@ -47,12 +47,14 @@ BlazeComponent.extendComponent({ const titleInput = this.find('.swimlane-name-input'); const title = titleInput.value.trim(); const sortValue = calculateIndexData(this.currentSwimlane, nextSwimlane, 1); + const swimlaneType = (currentBoard.isTemplatesBoard())?'template-swimlane':'swimlane'; if (title) { Swimlanes.insert({ title, boardId: Session.get('currentBoard'), sort: sortValue.base, + type: swimlaneType, }); titleInput.value = ''; @@ -63,6 +65,7 @@ BlazeComponent.extendComponent({ // with a minimum of interactions Popup.close(); }, + 'click .js-swimlane-template': Popup.open('searchElement'), }]; }, }).register('swimlaneAddPopup'); diff --git a/client/components/swimlanes/swimlanes.jade b/client/components/swimlanes/swimlanes.jade index 34177a02..c56834df 100644 --- a/client/components/swimlanes/swimlanes.jade +++ b/client/components/swimlanes/swimlanes.jade @@ -3,15 +3,15 @@ template(name="swimlane") +swimlaneHeader .swimlane.js-lists.js-swimlane if isMiniScreen - if currentList + if currentListIsInThisSwimlane _id +list(currentList) - else - each currentBoard.lists + unless currentList + each lists +miniList(this) if currentUser.isBoardMember +addListForm else - each currentBoard.lists + each lists +list(this) if currentCardIsInThisList _id ../_id +cardDetails(currentCard) @@ -24,12 +24,12 @@ template(name="listsGroup") if currentList +list(currentList) else - each currentBoard.lists + each lists +miniList(this) if currentUser.isBoardMember +addListForm else - each currentBoard.lists + each lists +list(this) if currentCardIsInThisList _id null +cardDetails(currentCard) @@ -44,7 +44,11 @@ template(name="addListForm") autocomplete="off" autofocus) .edit-controls.clearfix button.primary.confirm(type="submit") {{_ 'save'}} - a.fa.fa-times-thin.js-close-inlined-form + unless currentBoard.isTemplatesBoard + unless currentBoard.isTemplateBoard + span.quiet + | {{_ 'or'}} + a.js-list-template {{_ 'template'}} else a.open-list-composer.js-open-inlined-form i.fa.fa-plus diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index ce327f54..519b00d2 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -1,5 +1,10 @@ const { calculateIndex, enableClickOnTouch } = Utils; +function currentListIsInThisSwimlane(swimlaneId) { + const currentList = Lists.findOne(Session.get('currentList')); + return currentList && (currentList.swimlaneId === swimlaneId || currentList.swimlaneId === ''); +} + function currentCardIsInThisList(listId, swimlaneId) { const currentCard = Cards.findOne(Session.get('currentCard')); const currentUser = Meteor.user(); @@ -114,6 +119,10 @@ BlazeComponent.extendComponent({ return currentCardIsInThisList(listId, swimlaneId); }, + currentListIsInThisSwimlane(swimlaneId) { + return currentListIsInThisSwimlane(swimlaneId); + }, + events() { return [{ // Click-and-drag action @@ -153,6 +162,12 @@ BlazeComponent.extendComponent({ }).register('swimlane'); BlazeComponent.extendComponent({ + onCreated() { + this.currentBoard = Boards.findOne(Session.get('currentBoard')); + this.isListTemplatesSwimlane = this.currentBoard.isTemplatesBoard() && this.currentData().isListTemplatesSwimlane(); + this.currentSwimlane = this.currentData(); + }, + // Proxy open() { this.childComponents('inlinedForm')[0].open(); @@ -169,12 +184,15 @@ BlazeComponent.extendComponent({ title, boardId: Session.get('currentBoard'), sort: $('.list').length, + type: (this.isListTemplatesSwimlane)?'template-list':'list', + swimlaneId: (this.currentBoard.isTemplatesBoard())?this.currentSwimlane._id:'', }); titleInput.value = ''; titleInput.focus(); } }, + 'click .js-list-template': Popup.open('searchElement'), }]; }, }).register('addListForm'); diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade index b6e10d8a..c55b65c2 100644 --- a/client/components/users/userHeader.jade +++ b/client/components/users/userHeader.jade @@ -4,10 +4,11 @@ template(name="headerUserBar") .header-user-bar-avatar +userAvatar(userId=currentUser._id) unless isMiniScreen - if currentUser.profile.fullname - = currentUser.profile.fullname - else - = currentUser.username + unless isSandstorm + if currentUser.profile.fullname + = currentUser.profile.fullname + else + = currentUser.username template(name="memberMenuPopup") ul.pop-over-list @@ -15,13 +16,18 @@ template(name="memberMenuPopup") li: a.js-edit-profile {{_ 'edit-profile'}} li: a.js-change-settings {{_ 'change-settings'}} li: a.js-change-avatar {{_ 'edit-avatar'}} - li: a.js-change-password {{_ 'changePasswordPopup-title'}} - li: a.js-change-language {{_ 'changeLanguagePopup-title'}} + unless isSandstorm + li: a.js-change-password {{_ 'changePasswordPopup-title'}} + li: a.js-change-language {{_ 'changeLanguagePopup-title'}} if currentUser.isAdmin li: a.js-go-setting(href="{{pathFor 'setting'}}") {{_ 'admin-panel'}} hr ul.pop-over-list - li: a.js-logout {{_ 'log-out'}} + li: a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") {{_ 'templates'}} + unless isSandstorm + hr + ul.pop-over-list + li: a.js-logout {{_ 'log-out'}} template(name="editProfilePopup") form diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js index 63cbb14f..6a2397a4 100644 --- a/client/components/users/userHeader.js +++ b/client/components/users/userHeader.js @@ -3,6 +3,15 @@ Template.headerUserBar.events({ 'click .js-change-avatar': Popup.open('changeAvatar'), }); +Template.memberMenuPopup.helpers({ + templatesBoardId() { + return Meteor.user().getTemplatesBoardId(); + }, + templatesBoardSlug() { + return Meteor.user().getTemplatesBoardSlug(); + }, +}); + Template.memberMenuPopup.events({ 'click .js-edit-profile': Popup.open('editProfile'), 'click .js-change-settings': Popup.open('changeSettings'), diff --git a/client/lib/popup.js b/client/lib/popup.js index 5b640f50..9abe48aa 100644 --- a/client/lib/popup.js +++ b/client/lib/popup.js @@ -184,7 +184,7 @@ window.Popup = new class { // positives. const title = TAPi18n.__(translationKey); // when popup showed as full of small screen, we need a default header to clearly see [X] button - const defaultTitle = Utils.isMiniScreen() ? 'Wekan' : false; + const defaultTitle = Utils.isMiniScreen() ? '' : false; return title !== translationKey ? title : defaultTitle; }; } |