Moves boards components into a .vue file
This commit is contained in:
parent
4830c0c723
commit
1b617fa677
|
@ -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 (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 (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;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<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_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 {
|
||||
|
|
Loading…
Reference in New Issue