Commit 07de43a7 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'fl-tech-debt' into 'master'

Moves boards components into a .vue file

See merge request gitlab-org/gitlab-ce!20177
parents c82b5778 1b617fa6
<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>
......
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>
`,
});
<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>
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>
`,
});
<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>
/* 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>
`,
});
<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>
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>
`,
});
<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>
......@@ -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,
......
......@@ -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 {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment