Moves boards components into a .vue file
This commit is contained in:
parent
4830c0c723
commit
1b617fa677
|
@ -1,78 +1,78 @@
|
||||||
<script>
|
<script>
|
||||||
/* eslint-disable vue/require-default-prop */
|
/* eslint-disable vue/require-default-prop */
|
||||||
import './issue_card_inner';
|
import IssueCardInner from './issue_card_inner.vue';
|
||||||
import eventHub from '../eventhub';
|
import eventHub from '../eventhub';
|
||||||
|
|
||||||
const Store = gl.issueBoards.BoardsStore;
|
const Store = gl.issueBoards.BoardsStore;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BoardsIssueCard',
|
name: 'BoardsIssueCard',
|
||||||
components: {
|
components: {
|
||||||
'issue-card-inner': gl.issueBoards.IssueCardInner,
|
IssueCardInner,
|
||||||
},
|
|
||||||
props: {
|
|
||||||
list: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
},
|
||||||
issue: {
|
props: {
|
||||||
type: Object,
|
list: {
|
||||||
default: () => ({}),
|
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: {
|
data() {
|
||||||
type: String,
|
return {
|
||||||
default: '',
|
showDetail: false,
|
||||||
|
detailIssue: Store.detail,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
disabled: {
|
computed: {
|
||||||
type: Boolean,
|
issueDetailVisible() {
|
||||||
default: false,
|
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
index: {
|
methods: {
|
||||||
type: Number,
|
mouseDown() {
|
||||||
default: 0,
|
this.showDetail = true;
|
||||||
},
|
},
|
||||||
rootPath: {
|
mouseMove() {
|
||||||
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) {
|
|
||||||
this.showDetail = false;
|
this.showDetail = false;
|
||||||
|
},
|
||||||
|
showIssue(e) {
|
||||||
|
if (e.target.classList.contains('js-no-trigger')) return;
|
||||||
|
|
||||||
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
|
if (this.showDetail) {
|
||||||
eventHub.$emit('clearDetailIssue');
|
this.showDetail = false;
|
||||||
} else {
|
|
||||||
eventHub.$emit('newDetailIssue', this.issue);
|
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
|
||||||
Store.detail.list = this.list;
|
eventHub.$emit('clearDetailIssue');
|
||||||
|
} else {
|
||||||
|
eventHub.$emit('newDetailIssue', this.issue);
|
||||||
|
Store.detail.list = this.list;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -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>
|
|
||||||
`,
|
|
||||||
});
|
|
|
@ -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>
|
|
@ -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>
|
|
||||||
`,
|
|
||||||
});
|
|
|
@ -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>
|
|
@ -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>
|
|
||||||
`,
|
|
||||||
});
|
|
|
@ -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>
|
|
@ -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>
|
|
||||||
`,
|
|
||||||
});
|
|
|
@ -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>
|
|
@ -25,7 +25,7 @@ import './filters/due_date_filters';
|
||||||
import './components/board';
|
import './components/board';
|
||||||
import './components/board_sidebar';
|
import './components/board_sidebar';
|
||||||
import './components/new_list_dropdown';
|
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
|
import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
@ -49,7 +49,7 @@ export default () => {
|
||||||
components: {
|
components: {
|
||||||
'board': gl.issueBoards.Board,
|
'board': gl.issueBoards.Board,
|
||||||
'board-sidebar': gl.issueBoards.BoardSidebar,
|
'board-sidebar': gl.issueBoards.BoardSidebar,
|
||||||
'board-add-issues-modal': gl.issueBoards.IssuesModal,
|
BoardAddIssuesModal,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
state: Store.state,
|
state: Store.state,
|
||||||
|
|
|
@ -9,7 +9,7 @@ import '~/vue_shared/models/assignee';
|
||||||
import '~/boards/models/issue';
|
import '~/boards/models/issue';
|
||||||
import '~/boards/models/list';
|
import '~/boards/models/list';
|
||||||
import '~/boards/stores/boards_store';
|
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';
|
import { listObj } from './mock_data';
|
||||||
|
|
||||||
describe('Issue card component', () => {
|
describe('Issue card component', () => {
|
||||||
|
@ -48,7 +48,7 @@ describe('Issue card component', () => {
|
||||||
component = new Vue({
|
component = new Vue({
|
||||||
el: document.querySelector('.test-container'),
|
el: document.querySelector('.test-container'),
|
||||||
components: {
|
components: {
|
||||||
'issue-card': gl.issueBoards.IssueCardInner,
|
'issue-card': IssueCardInner,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
Loading…
Reference in New Issue