diff options
author | Phil Hughes <me@iamphill.com> | 2018-06-27 14:01:45 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-06-27 14:01:45 +0000 |
commit | 07de43a7e07cefcbb60e7ed84c684f478eae8b93 (patch) | |
tree | e5dac8502e35e41f212d3085680178db223655af | |
parent | c82b57789dea2d95f725e87e20e6b763493c7677 (diff) | |
parent | 1b617fa677c59e228d0335cef78907fda77143a0 (diff) | |
download | gitlab-ce-07de43a7e07cefcbb60e7ed84c684f478eae8b93.tar.gz |
Merge branch 'fl-tech-debt' into 'master'
Moves boards components into a .vue file
See merge request gitlab-org/gitlab-ce!20177
11 files changed, 686 insertions, 673 deletions
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index b7d3574bc80..0398102ad02 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -1,78 +1,78 @@ <script> -/* eslint-disable vue/require-default-prop */ -import './issue_card_inner'; -import eventHub from '../eventhub'; + /* eslint-disable vue/require-default-prop */ + import IssueCardInner from './issue_card_inner.vue'; + import eventHub from '../eventhub'; -const Store = gl.issueBoards.BoardsStore; + const Store = gl.issueBoards.BoardsStore; -export default { - name: 'BoardsIssueCard', - components: { - 'issue-card-inner': gl.issueBoards.IssueCardInner, - }, - props: { - list: { - type: Object, - default: () => ({}), + export default { + name: 'BoardsIssueCard', + components: { + IssueCardInner, }, - issue: { - type: Object, - default: () => ({}), + props: { + list: { + type: Object, + default: () => ({}), + }, + issue: { + type: Object, + default: () => ({}), + }, + issueLinkBase: { + type: String, + default: '', + }, + disabled: { + type: Boolean, + default: false, + }, + index: { + type: Number, + default: 0, + }, + rootPath: { + type: String, + default: '', + }, + groupId: { + type: Number, + }, }, - issueLinkBase: { - type: String, - default: '', + data() { + return { + showDetail: false, + detailIssue: Store.detail, + }; }, - disabled: { - type: Boolean, - default: false, + computed: { + issueDetailVisible() { + return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; + }, }, - index: { - type: Number, - default: 0, - }, - rootPath: { - type: String, - default: '', - }, - groupId: { - type: Number, - }, - }, - data() { - return { - showDetail: false, - detailIssue: Store.detail, - }; - }, - computed: { - issueDetailVisible() { - return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; - }, - }, - methods: { - mouseDown() { - this.showDetail = true; - }, - mouseMove() { - this.showDetail = false; - }, - showIssue(e) { - if (e.target.classList.contains('js-no-trigger')) return; - - if (this.showDetail) { + methods: { + mouseDown() { + this.showDetail = true; + }, + mouseMove() { this.showDetail = false; + }, + showIssue(e) { + if (e.target.classList.contains('js-no-trigger')) return; + + if (this.showDetail) { + this.showDetail = false; - if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { - eventHub.$emit('clearDetailIssue'); - } else { - eventHub.$emit('newDetailIssue', this.issue); - Store.detail.list = this.list; + if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { + eventHub.$emit('clearDetailIssue'); + } else { + eventHub.$emit('newDetailIssue', this.issue); + Store.detail.list = this.list; + } } - } + }, }, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js deleted file mode 100644 index f7d7b910e2f..00000000000 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ /dev/null @@ -1,196 +0,0 @@ -import $ from 'jquery'; -import Vue from 'vue'; -import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import eventHub from '../eventhub'; - -const Store = gl.issueBoards.BoardsStore; - -window.gl = window.gl || {}; -window.gl.issueBoards = window.gl.issueBoards || {}; - -gl.issueBoards.IssueCardInner = Vue.extend({ - components: { - UserAvatarLink, - }, - props: { - issue: { - type: Object, - required: true, - }, - issueLinkBase: { - type: String, - required: true, - }, - list: { - type: Object, - required: false, - default: () => ({}), - }, - rootPath: { - type: String, - required: true, - }, - updateFilters: { - type: Boolean, - required: false, - default: false, - }, - groupId: { - type: Number, - required: false, - default: null, - }, - }, - data() { - return { - limitBeforeCounter: 3, - maxRender: 4, - maxCounter: 99, - }; - }, - computed: { - numberOverLimit() { - return this.issue.assignees.length - this.limitBeforeCounter; - }, - assigneeCounterTooltip() { - return `${this.assigneeCounterLabel} more`; - }, - assigneeCounterLabel() { - if (this.numberOverLimit > this.maxCounter) { - return `${this.maxCounter}+`; - } - - return `+${this.numberOverLimit}`; - }, - shouldRenderCounter() { - if (this.issue.assignees.length <= this.maxRender) { - return false; - } - - return this.issue.assignees.length > this.numberOverLimit; - }, - issueId() { - if (this.issue.iid) { - return `#${this.issue.iid}`; - } - return false; - }, - showLabelFooter() { - return this.issue.labels.find(l => this.showLabel(l)) !== undefined; - }, - }, - methods: { - isIndexLessThanlimit(index) { - return index < this.limitBeforeCounter; - }, - shouldRenderAssignee(index) { - // Eg. maxRender is 4, - // Render up to all 4 assignees if there are only 4 assigness - // Otherwise render up to the limitBeforeCounter - if (this.issue.assignees.length <= this.maxRender) { - return index < this.maxRender; - } - - return index < this.limitBeforeCounter; - }, - assigneeUrl(assignee) { - return `${this.rootPath}${assignee.username}`; - }, - assigneeUrlTitle(assignee) { - return `Assigned to ${assignee.name}`; - }, - avatarUrlTitle(assignee) { - return `Avatar for ${assignee.name}`; - }, - showLabel(label) { - if (!label.id) return false; - return true; - }, - filterByLabel(label, e) { - if (!this.updateFilters) return; - - const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&'); - const labelTitle = encodeURIComponent(label.title); - const param = `label_name[]=${labelTitle}`; - const labelIndex = filterPath.indexOf(param); - $(e.currentTarget).tooltip('hide'); - - if (labelIndex === -1) { - filterPath.push(param); - } else { - filterPath.splice(labelIndex, 1); - } - - gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); - - Store.updateFiltersUrl(); - - eventHub.$emit('updateTokens'); - }, - labelStyle(label) { - return { - backgroundColor: label.color, - color: label.textColor, - }; - }, - }, - template: ` - <div> - <div class="board-card-header"> - <h4 class="board-card-title"> - <i - class="fa fa-eye-slash confidential-icon" - v-if="issue.confidential" - aria-hidden="true" - /> - <a - class="js-no-trigger" - :href="issue.path" - :title="issue.title">{{ issue.title }}</a> - <span - class="board-card-number" - v-if="issueId" - > - {{ issue.referencePath }} - </span> - </h4> - <div class="board-card-assignee"> - <user-avatar-link - v-for="(assignee, index) in issue.assignees" - :key="assignee.id" - v-if="shouldRenderAssignee(index)" - class="js-no-trigger" - :link-href="assigneeUrl(assignee)" - :img-alt="avatarUrlTitle(assignee)" - :img-src="assignee.avatar" - :tooltip-text="assigneeUrlTitle(assignee)" - tooltip-placement="bottom" - /> - <span - class="avatar-counter has-tooltip" - :title="assigneeCounterTooltip" - v-if="shouldRenderCounter" - > - {{ assigneeCounterLabel }} - </span> - </div> - </div> - <div - class="board-card-footer" - v-if="showLabelFooter" - > - <button - class="badge color-label has-tooltip" - v-for="label in issue.labels" - type="button" - v-if="showLabel(label)" - @click="filterByLabel(label, $event)" - :style="labelStyle(label)" - :title="label.description" - data-container="body"> - {{ label.title }} - </button> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue new file mode 100644 index 00000000000..a0b7fe81a4a --- /dev/null +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -0,0 +1,196 @@ +<script> + import $ from 'jquery'; + import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import eventHub from '../eventhub'; + + const Store = gl.issueBoards.BoardsStore; + + export default { + components: { + UserAvatarLink, + }, + props: { + issue: { + type: Object, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + list: { + type: Object, + required: false, + default: () => ({}), + }, + rootPath: { + type: String, + required: true, + }, + updateFilters: { + type: Boolean, + required: false, + default: false, + }, + groupId: { + type: Number, + required: false, + default: null, + }, + }, + data() { + return { + limitBeforeCounter: 3, + maxRender: 4, + maxCounter: 99, + }; + }, + computed: { + numberOverLimit() { + return this.issue.assignees.length - this.limitBeforeCounter; + }, + assigneeCounterTooltip() { + return `${this.assigneeCounterLabel} more`; + }, + assigneeCounterLabel() { + if (this.numberOverLimit > this.maxCounter) { + return `${this.maxCounter}+`; + } + + return `+${this.numberOverLimit}`; + }, + shouldRenderCounter() { + if (this.issue.assignees.length <= this.maxRender) { + return false; + } + + return this.issue.assignees.length > this.numberOverLimit; + }, + issueId() { + if (this.issue.iid) { + return `#${this.issue.iid}`; + } + return false; + }, + showLabelFooter() { + return this.issue.labels.find(l => this.showLabel(l)) !== undefined; + }, + }, + methods: { + isIndexLessThanlimit(index) { + return index < this.limitBeforeCounter; + }, + shouldRenderAssignee(index) { + // Eg. maxRender is 4, + // Render up to all 4 assignees if there are only 4 assigness + // Otherwise render up to the limitBeforeCounter + if (this.issue.assignees.length <= this.maxRender) { + return index < this.maxRender; + } + + return index < this.limitBeforeCounter; + }, + assigneeUrl(assignee) { + return `${this.rootPath}${assignee.username}`; + }, + assigneeUrlTitle(assignee) { + return `Assigned to ${assignee.name}`; + }, + avatarUrlTitle(assignee) { + return `Avatar for ${assignee.name}`; + }, + showLabel(label) { + if (!label.id) return false; + return true; + }, + filterByLabel(label, e) { + if (!this.updateFilters) return; + + const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&'); + const labelTitle = encodeURIComponent(label.title); + const param = `label_name[]=${labelTitle}`; + const labelIndex = filterPath.indexOf(param); + $(e.currentTarget).tooltip('hide'); + + if (labelIndex === -1) { + filterPath.push(param); + } else { + filterPath.splice(labelIndex, 1); + } + + gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); + + Store.updateFiltersUrl(); + + eventHub.$emit('updateTokens'); + }, + labelStyle(label) { + return { + backgroundColor: label.color, + color: label.textColor, + }; + }, + }, + }; +</script> +<template> + <div> + <div class="board-card-header"> + <h4 class="board-card-title"> + <i + v-if="issue.confidential" + class="fa fa-eye-slash confidential-icon" + aria-hidden="true" + ></i> + <a + :href="issue.path" + :title="issue.title" + class="js-no-trigger">{{ issue.title }}</a> + <span + v-if="issueId" + class="board-card-number" + > + {{ issue.referencePath }} + </span> + </h4> + <div class="board-card-assignee"> + <user-avatar-link + v-for="(assignee, index) in issue.assignees" + v-if="shouldRenderAssignee(index)" + :key="assignee.id" + :link-href="assigneeUrl(assignee)" + :img-alt="avatarUrlTitle(assignee)" + :img-src="assignee.avatar" + :tooltip-text="assigneeUrlTitle(assignee)" + class="js-no-trigger" + tooltip-placement="bottom" + /> + <span + v-if="shouldRenderCounter" + :title="assigneeCounterTooltip" + class="avatar-counter has-tooltip" + > + {{ assigneeCounterLabel }} + </span> + </div> + </div> + <div + v-if="showLabelFooter" + class="board-card-footer" + > + <button + v-for="label in issue.labels" + v-if="showLabel(label)" + :key="label.id" + :style="labelStyle(label)" + :title="label.description" + class="badge color-label has-tooltip" + type="button" + data-container="body" + @click="filterByLabel(label, $event)" + > + {{ label.title }} + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js deleted file mode 100644 index cc9848058ca..00000000000 --- a/app/assets/javascripts/boards/components/modal/header.js +++ /dev/null @@ -1,79 +0,0 @@ -import Vue from 'vue'; -import modalFilters from './filters'; -import modalTabs from './tabs.vue'; -import ModalStore from '../../stores/modal_store'; -import modalMixin from '../../mixins/modal_mixins'; - -gl.issueBoards.ModalHeader = Vue.extend({ - components: { - modalTabs, - modalFilters, - }, - mixins: [modalMixin], - props: { - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, - }, - data() { - return ModalStore.store; - }, - computed: { - selectAllText() { - if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { - return 'Select all'; - } - - return 'Deselect all'; - }, - showSearch() { - return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; - }, - }, - methods: { - toggleAll() { - this.$refs.selectAllBtn.blur(); - - ModalStore.toggleAll(); - }, - }, - template: ` - <div> - <header class="add-issues-header form-actions"> - <h2> - Add issues - <button - type="button" - class="close" - data-dismiss="modal" - aria-label="Close" - @click="toggleModal(false)"> - <span aria-hidden="true">×</span> - </button> - </h2> - </header> - <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs> - <div - class="add-issues-search append-bottom-10" - v-if="showSearch"> - <modal-filters :store="filter" /> - <button - type="button" - class="btn btn-success btn-inverted prepend-left-10" - ref="selectAllBtn" - @click="toggleAll"> - {{ selectAllText }} - </button> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue new file mode 100644 index 00000000000..979fb4d7199 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/header.vue @@ -0,0 +1,82 @@ +<script> + import ModalFilters from './filters'; + import ModalTabs from './tabs.vue'; + import ModalStore from '../../stores/modal_store'; + import modalMixin from '../../mixins/modal_mixins'; + + export default { + components: { + ModalTabs, + ModalFilters, + }, + mixins: [modalMixin], + props: { + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + computed: { + selectAllText() { + if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { + return 'Select all'; + } + + return 'Deselect all'; + }, + showSearch() { + return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; + }, + }, + methods: { + toggleAll() { + this.$refs.selectAllBtn.blur(); + + ModalStore.toggleAll(); + }, + }, + }; +</script> +<template> + <div> + <header class="add-issues-header form-actions"> + <h2> + Add issues + <button + type="button" + class="close" + data-dismiss="modal" + aria-label="Close" + @click="toggleModal(false)" + > + <span aria-hidden="true">×</span> + </button> + </h2> + </header> + <modal-tabs v-if="!loading && issuesCount > 0"/> + <div + v-if="showSearch" + class="add-issues-search append-bottom-10"> + <modal-filters :store="filter" /> + <button + ref="selectAllBtn" + type="button" + class="btn btn-success btn-inverted prepend-left-10" + @click="toggleAll" + > + {{ selectAllText }} + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js deleted file mode 100644 index 983061f52ae..00000000000 --- a/app/assets/javascripts/boards/components/modal/index.js +++ /dev/null @@ -1,171 +0,0 @@ -/* global ListIssue */ - -import Vue from 'vue'; -import queryData from '~/boards/utils/query_data'; -import loadingIcon from '~/vue_shared/components/loading_icon.vue'; -import './header'; -import './list'; -import ModalFooter from './footer.vue'; -import EmptyState from './empty_state.vue'; -import ModalStore from '../../stores/modal_store'; - -gl.issueBoards.IssuesModal = Vue.extend({ - components: { - EmptyState, - 'modal-header': gl.issueBoards.ModalHeader, - 'modal-list': gl.issueBoards.ModalList, - ModalFooter, - loadingIcon, - }, - props: { - newIssuePath: { - type: String, - required: true, - }, - emptyStateSvg: { - type: String, - required: true, - }, - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, - }, - data() { - return ModalStore.store; - }, - computed: { - showList() { - if (this.activeTab === 'selected') { - return this.selectedIssues.length > 0; - } - - return this.issuesCount > 0; - }, - showEmptyState() { - if (!this.loading && this.issuesCount === 0) { - return true; - } - - return this.activeTab === 'selected' && this.selectedIssues.length === 0; - }, - }, - watch: { - page() { - this.loadIssues(); - }, - showAddIssuesModal() { - if (this.showAddIssuesModal && !this.issues.length) { - this.loading = true; - const loadingDone = () => { - this.loading = false; - }; - - this.loadIssues() - .then(loadingDone) - .catch(loadingDone); - } else if (!this.showAddIssuesModal) { - this.issues = []; - this.selectedIssues = []; - this.issuesCount = false; - } - }, - filter: { - handler() { - if (this.$el.tagName) { - this.page = 1; - this.filterLoading = true; - const loadingDone = () => { - this.filterLoading = false; - }; - - this.loadIssues(true) - .then(loadingDone) - .catch(loadingDone); - } - }, - deep: true, - }, - }, - created() { - this.page = 1; - }, - methods: { - loadIssues(clearIssues = false) { - if (!this.showAddIssuesModal) return false; - - return gl.boardService.getBacklog(queryData(this.filter.path, { - page: this.page, - per: this.perPage, - })) - .then(res => res.data) - .then((data) => { - if (clearIssues) { - this.issues = []; - } - - data.issues.forEach((issueObj) => { - const issue = new ListIssue(issueObj); - const foundSelectedIssue = ModalStore.findSelectedIssue(issue); - issue.selected = !!foundSelectedIssue; - - this.issues.push(issue); - }); - - this.loadingNewPage = false; - - if (!this.issuesCount) { - this.issuesCount = data.size; - } - }).catch(() => { - // TODO: handle request error - }); - }, - }, - template: ` - <div - class="add-issues-modal" - v-if="showAddIssuesModal"> - <div class="add-issues-container"> - <modal-header - :project-id="projectId" - :milestone-path="milestonePath" - :label-path="labelPath"> - </modal-header> - <modal-list - :issue-link-base="issueLinkBase" - :root-path="rootPath" - :empty-state-svg="emptyStateSvg" - v-if="!loading && showList && !filterLoading"></modal-list> - <empty-state - v-if="showEmptyState" - :new-issue-path="newIssuePath" - :empty-state-svg="emptyStateSvg"></empty-state> - <section - class="add-issues-list text-center" - v-if="loading || filterLoading"> - <div class="add-issues-list-loading"> - <loading-icon /> - </div> - </section> - <modal-footer></modal-footer> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue new file mode 100644 index 00000000000..33e72a6782e --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/index.vue @@ -0,0 +1,178 @@ +<script> + /* global ListIssue */ + import queryData from '~/boards/utils/query_data'; + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import ModalHeader from './header.vue'; + import ModalList from './list.vue'; + import ModalFooter from './footer.vue'; + import EmptyState from './empty_state.vue'; + import ModalStore from '../../stores/modal_store'; + + export default { + components: { + EmptyState, + ModalHeader, + ModalList, + ModalFooter, + loadingIcon, + }, + props: { + newIssuePath: { + type: String, + required: true, + }, + emptyStateSvg: { + type: String, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + computed: { + showList() { + if (this.activeTab === 'selected') { + return this.selectedIssues.length > 0; + } + + return this.issuesCount > 0; + }, + showEmptyState() { + if (!this.loading && this.issuesCount === 0) { + return true; + } + + return this.activeTab === 'selected' && this.selectedIssues.length === 0; + }, + }, + watch: { + page() { + this.loadIssues(); + }, + showAddIssuesModal() { + if (this.showAddIssuesModal && !this.issues.length) { + this.loading = true; + const loadingDone = () => { + this.loading = false; + }; + + this.loadIssues() + .then(loadingDone) + .catch(loadingDone); + } else if (!this.showAddIssuesModal) { + this.issues = []; + this.selectedIssues = []; + this.issuesCount = false; + } + }, + filter: { + handler() { + if (this.$el.tagName) { + this.page = 1; + this.filterLoading = true; + const loadingDone = () => { + this.filterLoading = false; + }; + + this.loadIssues(true) + .then(loadingDone) + .catch(loadingDone); + } + }, + deep: true, + }, + }, + created() { + this.page = 1; + }, + methods: { + loadIssues(clearIssues = false) { + if (!this.showAddIssuesModal) return false; + + return gl.boardService + .getBacklog( + queryData(this.filter.path, { + page: this.page, + per: this.perPage, + }), + ) + .then(res => res.data) + .then(data => { + if (clearIssues) { + this.issues = []; + } + + data.issues.forEach(issueObj => { + const issue = new ListIssue(issueObj); + const foundSelectedIssue = ModalStore.findSelectedIssue(issue); + issue.selected = !!foundSelectedIssue; + + this.issues.push(issue); + }); + + this.loadingNewPage = false; + + if (!this.issuesCount) { + this.issuesCount = data.size; + } + }) + .catch(() => { + // TODO: handle request error + }); + }, + }, + }; +</script> +<template> + <div + v-if="showAddIssuesModal" + class="add-issues-modal"> + <div class="add-issues-container"> + <modal-header + :project-id="projectId" + :milestone-path="milestonePath" + :label-path="labelPath" + /> + <modal-list + v-if="!loading && showList && !filterLoading" + :issue-link-base="issueLinkBase" + :root-path="rootPath" + :empty-state-svg="emptyStateSvg" + /> + <empty-state + v-if="showEmptyState" + :new-issue-path="newIssuePath" + :empty-state-svg="emptyStateSvg" + /> + <section + v-if="loading || filterLoading" + class="add-issues-list text-center" + > + <div class="add-issues-list-loading"> + <loading-icon /> + </div> + </section> + <modal-footer/> + </div> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js deleted file mode 100644 index 11061c72a7b..00000000000 --- a/app/assets/javascripts/boards/components/modal/list.js +++ /dev/null @@ -1,159 +0,0 @@ -import Vue from 'vue'; -import bp from '../../../breakpoints'; -import ModalStore from '../../stores/modal_store'; - -gl.issueBoards.ModalList = Vue.extend({ - components: { - 'issue-card-inner': gl.issueBoards.IssueCardInner, - }, - props: { - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - emptyStateSvg: { - type: String, - required: true, - }, - }, - data() { - return ModalStore.store; - }, - computed: { - loopIssues() { - if (this.activeTab === 'all') { - return this.issues; - } - - return this.selectedIssues; - }, - groupedIssues() { - const groups = []; - this.loopIssues.forEach((issue, i) => { - const index = i % this.columns; - - if (!groups[index]) { - groups.push([]); - } - - groups[index].push(issue); - }); - - return groups; - }, - }, - watch: { - activeTab() { - if (this.activeTab === 'all') { - ModalStore.purgeUnselectedIssues(); - } - }, - }, - mounted() { - this.scrollHandlerWrapper = this.scrollHandler.bind(this); - this.setColumnCountWrapper = this.setColumnCount.bind(this); - this.setColumnCount(); - - this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); - window.addEventListener('resize', this.setColumnCountWrapper); - }, - beforeDestroy() { - this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); - window.removeEventListener('resize', this.setColumnCountWrapper); - }, - methods: { - scrollHandler() { - const currentPage = Math.floor(this.issues.length / this.perPage); - - if ( - this.scrollTop() > this.scrollHeight() - 100 && - !this.loadingNewPage && - currentPage === this.page - ) { - this.loadingNewPage = true; - this.page += 1; - } - }, - toggleIssue(e, issue) { - if (e.target.tagName !== 'A') { - ModalStore.toggleIssue(issue); - } - }, - listHeight() { - return this.$refs.list.getBoundingClientRect().height; - }, - scrollHeight() { - return this.$refs.list.scrollHeight; - }, - scrollTop() { - return this.$refs.list.scrollTop + this.listHeight(); - }, - showIssue(issue) { - if (this.activeTab === 'all') return true; - - const index = ModalStore.selectedIssueIndex(issue); - - return index !== -1; - }, - setColumnCount() { - const breakpoint = bp.getBreakpointSize(); - - if (breakpoint === 'lg' || breakpoint === 'md') { - this.columns = 3; - } else if (breakpoint === 'sm') { - this.columns = 2; - } else { - this.columns = 1; - } - }, - }, - template: ` - <section - class="add-issues-list add-issues-list-columns" - ref="list"> - <div - class="empty-state add-issues-empty-state-filter text-center" - v-if="issuesCount > 0 && issues.length === 0"> - <div - class="svg-content"> - <img :src="emptyStateSvg"/> - </div> - <div class="text-content"> - <h4> - There are no issues to show. - </h4> - </div> - </div> - <div - v-for="group in groupedIssues" - class="add-issues-list-column"> - <div - v-for="issue in group" - v-if="showIssue(issue)" - class="board-card-parent"> - <div - class="board-card" - :class="{ 'is-active': issue.selected }" - @click="toggleIssue($event, issue)"> - <issue-card-inner - :issue="issue" - :issue-link-base="issueLinkBase" - :root-path="rootPath"> - </issue-card-inner> - <span - :aria-label="'Issue #' + issue.id + ' selected'" - aria-checked="true" - v-if="issue.selected" - class="issue-card-selected text-center"> - <i class="fa fa-check"></i> - </span> - </div> - </div> - </div> - </section> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue new file mode 100644 index 00000000000..02ac36d7367 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/list.vue @@ -0,0 +1,162 @@ +<script> + import bp from '../../../breakpoints'; + import ModalStore from '../../stores/modal_store'; + import IssueCardInner from '../issue_card_inner.vue'; + + export default { + components: { + IssueCardInner, + }, + props: { + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + emptyStateSvg: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + computed: { + loopIssues() { + if (this.activeTab === 'all') { + return this.issues; + } + + return this.selectedIssues; + }, + groupedIssues() { + const groups = []; + this.loopIssues.forEach((issue, i) => { + const index = i % this.columns; + + if (!groups[index]) { + groups.push([]); + } + + groups[index].push(issue); + }); + + return groups; + }, + }, + watch: { + activeTab() { + if (this.activeTab === 'all') { + ModalStore.purgeUnselectedIssues(); + } + }, + }, + mounted() { + this.scrollHandlerWrapper = this.scrollHandler.bind(this); + this.setColumnCountWrapper = this.setColumnCount.bind(this); + this.setColumnCount(); + + this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); + window.addEventListener('resize', this.setColumnCountWrapper); + }, + beforeDestroy() { + this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); + window.removeEventListener('resize', this.setColumnCountWrapper); + }, + methods: { + scrollHandler() { + const currentPage = Math.floor(this.issues.length / this.perPage); + + if ( + this.scrollTop() > this.scrollHeight() - 100 && + !this.loadingNewPage && + currentPage === this.page + ) { + this.loadingNewPage = true; + this.page += 1; + } + }, + toggleIssue(e, issue) { + if (e.target.tagName !== 'A') { + ModalStore.toggleIssue(issue); + } + }, + listHeight() { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight() { + return this.$refs.list.scrollHeight; + }, + scrollTop() { + return this.$refs.list.scrollTop + this.listHeight(); + }, + showIssue(issue) { + if (this.activeTab === 'all') return true; + + const index = ModalStore.selectedIssueIndex(issue); + + return index !== -1; + }, + setColumnCount() { + const breakpoint = bp.getBreakpointSize(); + + if (breakpoint === 'lg' || breakpoint === 'md') { + this.columns = 3; + } else if (breakpoint === 'sm') { + this.columns = 2; + } else { + this.columns = 1; + } + }, + }, + }; +</script> +<template> + <section + ref="list" + class="add-issues-list add-issues-list-columns"> + <div + v-if="issuesCount > 0 && issues.length === 0" + class="empty-state add-issues-empty-state-filter text-center"> + <div + class="svg-content"> + <img :src="emptyStateSvg" /> + </div> + <div class="text-content"> + <h4> + There are no issues to show. + </h4> + </div> + </div> + <div + v-for="(group, index) in groupedIssues" + :key="index" + class="add-issues-list-column"> + <div + v-for="issue in group" + v-if="showIssue(issue)" + :key="issue.id" + class="board-card-parent"> + <div + :class="{ 'is-active': issue.selected }" + class="board-card" + @click="toggleIssue($event, issue)"> + <issue-card-inner + :issue="issue" + :issue-link-base="issueLinkBase" + :root-path="rootPath"/> + <span + v-if="issue.selected" + :aria-label="'Issue #' + issue.id + ' selected'" + aria-checked="true" + class="issue-card-selected text-center"> + <i class="fa fa-check"></i> + </span> + </div> + </div> + </div> + </section> +</template> diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 751a66f89c6..200d1923635 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -25,7 +25,7 @@ import './filters/due_date_filters'; import './components/board'; import './components/board_sidebar'; import './components/new_list_dropdown'; -import './components/modal/index'; +import BoardAddIssuesModal from './components/modal/index.vue'; import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first export default () => { @@ -49,7 +49,7 @@ export default () => { components: { 'board': gl.issueBoards.Board, 'board-sidebar': gl.issueBoards.BoardSidebar, - 'board-add-issues-modal': gl.issueBoards.IssuesModal, + BoardAddIssuesModal, }, data: { state: Store.state, diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 05acf903933..72e961c8a10 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -9,7 +9,7 @@ import '~/vue_shared/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; import '~/boards/stores/boards_store'; -import '~/boards/components/issue_card_inner'; +import IssueCardInner from '~/boards/components/issue_card_inner.vue'; import { listObj } from './mock_data'; describe('Issue card component', () => { @@ -48,7 +48,7 @@ describe('Issue card component', () => { component = new Vue({ el: document.querySelector('.test-container'), components: { - 'issue-card': gl.issueBoards.IssueCardInner, + 'issue-card': IssueCardInner, }, data() { return { |