719 lines
21 KiB
Vue
719 lines
21 KiB
Vue
<script>
|
|
import {
|
|
GlButton,
|
|
GlEmptyState,
|
|
GlFilteredSearchToken,
|
|
GlIcon,
|
|
GlLink,
|
|
GlSprintf,
|
|
GlTooltipDirective,
|
|
} from '@gitlab/ui';
|
|
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
|
import { toNumber } from 'lodash';
|
|
import createFlash from '~/flash';
|
|
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
|
|
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
|
|
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
|
|
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
|
|
import {
|
|
API_PARAM,
|
|
apiSortParams,
|
|
CREATED_DESC,
|
|
i18n,
|
|
MAX_LIST_SIZE,
|
|
PAGE_SIZE,
|
|
PARAM_DUE_DATE,
|
|
PARAM_PAGE,
|
|
PARAM_SORT,
|
|
PARAM_STATE,
|
|
RELATIVE_POSITION_DESC,
|
|
TOKEN_TYPE_ASSIGNEE,
|
|
TOKEN_TYPE_AUTHOR,
|
|
TOKEN_TYPE_CONFIDENTIAL,
|
|
TOKEN_TYPE_MY_REACTION,
|
|
TOKEN_TYPE_EPIC,
|
|
TOKEN_TYPE_ITERATION,
|
|
TOKEN_TYPE_LABEL,
|
|
TOKEN_TYPE_MILESTONE,
|
|
TOKEN_TYPE_WEIGHT,
|
|
UPDATED_DESC,
|
|
URL_PARAM,
|
|
urlSortParams,
|
|
} from '~/issues_list/constants';
|
|
import {
|
|
convertToParams,
|
|
convertToSearchQuery,
|
|
getDueDateValue,
|
|
getFilterTokens,
|
|
getSortKey,
|
|
getSortOptions,
|
|
} from '~/issues_list/utils';
|
|
import axios from '~/lib/utils/axios_utils';
|
|
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
|
|
import {
|
|
DEFAULT_NONE_ANY,
|
|
OPERATOR_IS_ONLY,
|
|
TOKEN_TITLE_ASSIGNEE,
|
|
TOKEN_TITLE_AUTHOR,
|
|
TOKEN_TITLE_CONFIDENTIAL,
|
|
TOKEN_TITLE_EPIC,
|
|
TOKEN_TITLE_ITERATION,
|
|
TOKEN_TITLE_LABEL,
|
|
TOKEN_TITLE_MILESTONE,
|
|
TOKEN_TITLE_MY_REACTION,
|
|
TOKEN_TITLE_WEIGHT,
|
|
} from '~/vue_shared/components/filtered_search_bar/constants';
|
|
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
|
|
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
|
|
import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
|
|
import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
|
|
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
|
|
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
|
|
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
|
|
import eventHub from '../eventhub';
|
|
import IssueCardTimeInfo from './issue_card_time_info.vue';
|
|
|
|
export default {
|
|
i18n,
|
|
IssuableListTabs,
|
|
components: {
|
|
CsvImportExportButtons,
|
|
GlButton,
|
|
GlEmptyState,
|
|
GlIcon,
|
|
GlLink,
|
|
GlSprintf,
|
|
IssuableByEmail,
|
|
IssuableList,
|
|
IssueCardTimeInfo,
|
|
BlockingIssuesCount: () => import('ee_component/issues/components/blocking_issues_count.vue'),
|
|
},
|
|
directives: {
|
|
GlTooltip: GlTooltipDirective,
|
|
},
|
|
inject: {
|
|
autocompleteAwardEmojisPath: {
|
|
default: '',
|
|
},
|
|
autocompleteUsersPath: {
|
|
default: '',
|
|
},
|
|
calendarPath: {
|
|
default: '',
|
|
},
|
|
canBulkUpdate: {
|
|
default: false,
|
|
},
|
|
emptyStateSvgPath: {
|
|
default: '',
|
|
},
|
|
endpoint: {
|
|
default: '',
|
|
},
|
|
exportCsvPath: {
|
|
default: '',
|
|
},
|
|
groupEpicsPath: {
|
|
default: '',
|
|
},
|
|
hasBlockedIssuesFeature: {
|
|
default: false,
|
|
},
|
|
hasIssueWeightsFeature: {
|
|
default: false,
|
|
},
|
|
hasMultipleIssueAssigneesFeature: {
|
|
default: false,
|
|
},
|
|
hasProjectIssues: {
|
|
default: false,
|
|
},
|
|
initialEmail: {
|
|
default: '',
|
|
},
|
|
isSignedIn: {
|
|
default: false,
|
|
},
|
|
issuesPath: {
|
|
default: '',
|
|
},
|
|
jiraIntegrationPath: {
|
|
default: '',
|
|
},
|
|
newIssuePath: {
|
|
default: '',
|
|
},
|
|
projectIterationsPath: {
|
|
default: '',
|
|
},
|
|
projectLabelsPath: {
|
|
default: '',
|
|
},
|
|
projectMilestonesPath: {
|
|
default: '',
|
|
},
|
|
projectPath: {
|
|
default: '',
|
|
},
|
|
rssPath: {
|
|
default: '',
|
|
},
|
|
showNewIssueLink: {
|
|
default: false,
|
|
},
|
|
signInPath: {
|
|
default: '',
|
|
},
|
|
},
|
|
data() {
|
|
const state = getParameterByName(PARAM_STATE);
|
|
const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
|
|
|
|
return {
|
|
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
|
|
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
|
|
filterTokens: getFilterTokens(window.location.search),
|
|
isLoading: false,
|
|
issues: [],
|
|
page: toNumber(getParameterByName(PARAM_PAGE)) || 1,
|
|
showBulkEditSidebar: false,
|
|
sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
|
|
state: state || IssuableStates.Opened,
|
|
totalIssues: 0,
|
|
};
|
|
},
|
|
computed: {
|
|
hasSearch() {
|
|
return this.searchQuery || Object.keys(this.urlFilterParams).length;
|
|
},
|
|
isBulkEditButtonDisabled() {
|
|
return this.showBulkEditSidebar || !this.issues.length;
|
|
},
|
|
isManualOrdering() {
|
|
return this.sortKey === RELATIVE_POSITION_DESC;
|
|
},
|
|
isOpenTab() {
|
|
return this.state === IssuableStates.Opened;
|
|
},
|
|
apiFilterParams() {
|
|
return convertToParams(this.filterTokens, API_PARAM);
|
|
},
|
|
urlFilterParams() {
|
|
return convertToParams(this.filterTokens, URL_PARAM);
|
|
},
|
|
searchQuery() {
|
|
return convertToSearchQuery(this.filterTokens) || undefined;
|
|
},
|
|
searchTokens() {
|
|
const tokens = [
|
|
{
|
|
type: TOKEN_TYPE_AUTHOR,
|
|
title: TOKEN_TITLE_AUTHOR,
|
|
icon: 'pencil',
|
|
token: AuthorToken,
|
|
dataType: 'user',
|
|
unique: true,
|
|
defaultAuthors: [],
|
|
fetchAuthors: this.fetchUsers,
|
|
},
|
|
{
|
|
type: TOKEN_TYPE_ASSIGNEE,
|
|
title: TOKEN_TITLE_ASSIGNEE,
|
|
icon: 'user',
|
|
token: AuthorToken,
|
|
dataType: 'user',
|
|
unique: !this.hasMultipleIssueAssigneesFeature,
|
|
defaultAuthors: DEFAULT_NONE_ANY,
|
|
fetchAuthors: this.fetchUsers,
|
|
},
|
|
{
|
|
type: TOKEN_TYPE_MILESTONE,
|
|
title: TOKEN_TITLE_MILESTONE,
|
|
icon: 'clock',
|
|
token: MilestoneToken,
|
|
unique: true,
|
|
defaultMilestones: [],
|
|
fetchMilestones: this.fetchMilestones,
|
|
},
|
|
{
|
|
type: TOKEN_TYPE_LABEL,
|
|
title: TOKEN_TITLE_LABEL,
|
|
icon: 'labels',
|
|
token: LabelToken,
|
|
defaultLabels: [],
|
|
fetchLabels: this.fetchLabels,
|
|
},
|
|
];
|
|
|
|
if (this.isSignedIn) {
|
|
tokens.push({
|
|
type: TOKEN_TYPE_MY_REACTION,
|
|
title: TOKEN_TITLE_MY_REACTION,
|
|
icon: 'thumb-up',
|
|
token: EmojiToken,
|
|
unique: true,
|
|
operators: OPERATOR_IS_ONLY,
|
|
fetchEmojis: this.fetchEmojis,
|
|
});
|
|
|
|
tokens.push({
|
|
type: TOKEN_TYPE_CONFIDENTIAL,
|
|
title: TOKEN_TITLE_CONFIDENTIAL,
|
|
icon: 'eye-slash',
|
|
token: GlFilteredSearchToken,
|
|
unique: true,
|
|
operators: OPERATOR_IS_ONLY,
|
|
options: [
|
|
{ icon: 'eye-slash', value: 'yes', title: this.$options.i18n.confidentialYes },
|
|
{ icon: 'eye', value: 'no', title: this.$options.i18n.confidentialNo },
|
|
],
|
|
});
|
|
}
|
|
|
|
if (this.projectIterationsPath) {
|
|
tokens.push({
|
|
type: TOKEN_TYPE_ITERATION,
|
|
title: TOKEN_TITLE_ITERATION,
|
|
icon: 'iteration',
|
|
token: IterationToken,
|
|
unique: true,
|
|
fetchIterations: this.fetchIterations,
|
|
});
|
|
}
|
|
|
|
if (this.groupEpicsPath) {
|
|
tokens.push({
|
|
type: TOKEN_TYPE_EPIC,
|
|
title: TOKEN_TITLE_EPIC,
|
|
icon: 'epic',
|
|
token: EpicToken,
|
|
unique: true,
|
|
idProperty: 'id',
|
|
fetchEpics: this.fetchEpics,
|
|
});
|
|
}
|
|
|
|
if (this.hasIssueWeightsFeature) {
|
|
tokens.push({
|
|
type: TOKEN_TYPE_WEIGHT,
|
|
title: TOKEN_TITLE_WEIGHT,
|
|
icon: 'weight',
|
|
token: WeightToken,
|
|
unique: true,
|
|
});
|
|
}
|
|
|
|
return tokens;
|
|
},
|
|
showPaginationControls() {
|
|
return this.issues.length > 0;
|
|
},
|
|
sortOptions() {
|
|
return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
|
|
},
|
|
tabCounts() {
|
|
return Object.values(IssuableStates).reduce(
|
|
(acc, state) => ({
|
|
...acc,
|
|
[state]: this.state === state ? this.totalIssues : undefined,
|
|
}),
|
|
{},
|
|
);
|
|
},
|
|
urlParams() {
|
|
const filterParams = {
|
|
...this.urlFilterParams,
|
|
};
|
|
|
|
if (filterParams.epic_id) {
|
|
filterParams.epic_id = encodeURIComponent(filterParams.epic_id);
|
|
} else if (filterParams['not[epic_id]']) {
|
|
filterParams['not[epic_id]'] = encodeURIComponent(filterParams['not[epic_id]']);
|
|
}
|
|
|
|
return {
|
|
due_date: this.dueDateFilter,
|
|
page: this.page,
|
|
search: this.searchQuery,
|
|
state: this.state,
|
|
...urlSortParams[this.sortKey],
|
|
...filterParams,
|
|
};
|
|
},
|
|
},
|
|
created() {
|
|
this.cache = {};
|
|
},
|
|
mounted() {
|
|
eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
|
|
this.fetchIssues();
|
|
},
|
|
beforeDestroy() {
|
|
eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
|
|
},
|
|
methods: {
|
|
fetchWithCache(path, cacheName, searchKey, search, wrapData = false) {
|
|
if (this.cache[cacheName]) {
|
|
const data = search
|
|
? fuzzaldrinPlus.filter(this.cache[cacheName], search, { key: searchKey })
|
|
: this.cache[cacheName].slice(0, MAX_LIST_SIZE);
|
|
return wrapData ? Promise.resolve({ data }) : Promise.resolve(data);
|
|
}
|
|
|
|
return axios.get(path).then(({ data }) => {
|
|
this.cache[cacheName] = data;
|
|
const result = data.slice(0, MAX_LIST_SIZE);
|
|
return wrapData ? { data: result } : result;
|
|
});
|
|
},
|
|
fetchEmojis(search) {
|
|
return this.fetchWithCache(this.autocompleteAwardEmojisPath, 'emojis', 'name', search);
|
|
},
|
|
async fetchEpics({ search }) {
|
|
const epics = await this.fetchWithCache(this.groupEpicsPath, 'epics');
|
|
if (!search) {
|
|
return epics.slice(0, MAX_LIST_SIZE);
|
|
}
|
|
const number = Number(search);
|
|
return Number.isNaN(number)
|
|
? fuzzaldrinPlus.filter(epics, search, { key: 'title' })
|
|
: epics.filter((epic) => epic.id === number);
|
|
},
|
|
fetchLabels(search) {
|
|
return this.fetchWithCache(this.projectLabelsPath, 'labels', 'title', search);
|
|
},
|
|
fetchMilestones(search) {
|
|
return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true);
|
|
},
|
|
fetchIterations(search) {
|
|
return axios.get(this.projectIterationsPath, { params: { search } });
|
|
},
|
|
fetchUsers(search) {
|
|
return axios.get(this.autocompleteUsersPath, { params: { search } });
|
|
},
|
|
fetchIssues() {
|
|
if (!this.hasProjectIssues) {
|
|
return undefined;
|
|
}
|
|
|
|
this.isLoading = true;
|
|
|
|
const filterParams = {
|
|
...this.apiFilterParams,
|
|
};
|
|
|
|
if (filterParams.epic_id) {
|
|
filterParams.epic_id = filterParams.epic_id.split('::&').pop();
|
|
} else if (filterParams['not[epic_id]']) {
|
|
filterParams['not[epic_id]'] = filterParams['not[epic_id]'].split('::&').pop();
|
|
}
|
|
|
|
return axios
|
|
.get(this.endpoint, {
|
|
params: {
|
|
due_date: this.dueDateFilter,
|
|
page: this.page,
|
|
per_page: PAGE_SIZE,
|
|
search: this.searchQuery,
|
|
state: this.state,
|
|
with_labels_details: true,
|
|
...apiSortParams[this.sortKey],
|
|
...filterParams,
|
|
},
|
|
})
|
|
.then(({ data, headers }) => {
|
|
this.page = Number(headers['x-page']);
|
|
this.totalIssues = Number(headers['x-total']);
|
|
this.issues = data.map((issue) => convertObjectPropsToCamelCase(issue, { deep: true }));
|
|
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
|
|
})
|
|
.catch(() => {
|
|
createFlash({ message: this.$options.i18n.errorFetchingIssues });
|
|
})
|
|
.finally(() => {
|
|
this.isLoading = false;
|
|
});
|
|
},
|
|
getExportCsvPathWithQuery() {
|
|
return `${this.exportCsvPath}${window.location.search}`;
|
|
},
|
|
getStatus(issue) {
|
|
if (issue.closedAt && issue.movedToId) {
|
|
return this.$options.i18n.closedMoved;
|
|
}
|
|
if (issue.closedAt) {
|
|
return this.$options.i18n.closed;
|
|
}
|
|
return undefined;
|
|
},
|
|
handleUpdateLegacyBulkEdit() {
|
|
// If "select all" checkbox was checked, wait for all checkboxes
|
|
// to be checked before updating IssuableBulkUpdateSidebar class
|
|
this.$nextTick(() => {
|
|
eventHub.$emit('issuables:updateBulkEdit');
|
|
});
|
|
},
|
|
async handleBulkUpdateClick() {
|
|
if (!this.hasInitBulkEdit) {
|
|
const initBulkUpdateSidebar = await import('~/issuable_init_bulk_update_sidebar');
|
|
initBulkUpdateSidebar.default.init('issuable_');
|
|
|
|
const usersSelect = await import('~/users_select');
|
|
const UsersSelect = usersSelect.default;
|
|
new UsersSelect(); // eslint-disable-line no-new
|
|
|
|
this.hasInitBulkEdit = true;
|
|
}
|
|
|
|
eventHub.$emit('issuables:enableBulkEdit');
|
|
},
|
|
handleClickTab(state) {
|
|
if (this.state !== state) {
|
|
this.page = 1;
|
|
}
|
|
this.state = state;
|
|
this.fetchIssues();
|
|
},
|
|
handleFilter(filter) {
|
|
this.filterTokens = filter;
|
|
this.fetchIssues();
|
|
},
|
|
handlePageChange(page) {
|
|
this.page = page;
|
|
this.fetchIssues();
|
|
},
|
|
handleReorder({ newIndex, oldIndex }) {
|
|
const issueToMove = this.issues[oldIndex];
|
|
const isDragDropDownwards = newIndex > oldIndex;
|
|
const isMovingToBeginning = newIndex === 0;
|
|
const isMovingToEnd = newIndex === this.issues.length - 1;
|
|
|
|
let moveBeforeId;
|
|
let moveAfterId;
|
|
|
|
if (isDragDropDownwards) {
|
|
const afterIndex = isMovingToEnd ? newIndex : newIndex + 1;
|
|
moveBeforeId = this.issues[newIndex].id;
|
|
moveAfterId = this.issues[afterIndex].id;
|
|
} else {
|
|
const beforeIndex = isMovingToBeginning ? newIndex : newIndex - 1;
|
|
moveBeforeId = this.issues[beforeIndex].id;
|
|
moveAfterId = this.issues[newIndex].id;
|
|
}
|
|
|
|
return axios
|
|
.put(`${this.issuesPath}/${issueToMove.iid}/reorder`, {
|
|
move_before_id: isMovingToBeginning ? null : moveBeforeId,
|
|
move_after_id: isMovingToEnd ? null : moveAfterId,
|
|
})
|
|
.then(() => {
|
|
// Move issue to new position in list
|
|
this.issues.splice(oldIndex, 1);
|
|
this.issues.splice(newIndex, 0, issueToMove);
|
|
})
|
|
.catch(() => {
|
|
createFlash({ message: this.$options.i18n.reorderError });
|
|
});
|
|
},
|
|
handleSort(value) {
|
|
this.sortKey = value;
|
|
this.fetchIssues();
|
|
},
|
|
toggleBulkEditSidebar(showBulkEditSidebar) {
|
|
this.showBulkEditSidebar = showBulkEditSidebar;
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="hasProjectIssues">
|
|
<issuable-list
|
|
:namespace="projectPath"
|
|
recent-searches-storage-key="issues"
|
|
:search-input-placeholder="$options.i18n.searchPlaceholder"
|
|
:search-tokens="searchTokens"
|
|
:initial-filter-value="filterTokens"
|
|
:sort-options="sortOptions"
|
|
:initial-sort-by="sortKey"
|
|
:issuables="issues"
|
|
label-filter-param="label_name"
|
|
:tabs="$options.IssuableListTabs"
|
|
:current-tab="state"
|
|
:tab-counts="tabCounts"
|
|
:issuables-loading="isLoading"
|
|
:is-manual-ordering="isManualOrdering"
|
|
:show-bulk-edit-sidebar="showBulkEditSidebar"
|
|
:show-pagination-controls="showPaginationControls"
|
|
:total-items="totalIssues"
|
|
:current-page="page"
|
|
:previous-page="page - 1"
|
|
:next-page="page + 1"
|
|
:url-params="urlParams"
|
|
@click-tab="handleClickTab"
|
|
@filter="handleFilter"
|
|
@page-change="handlePageChange"
|
|
@reorder="handleReorder"
|
|
@sort="handleSort"
|
|
@update-legacy-bulk-edit="handleUpdateLegacyBulkEdit"
|
|
>
|
|
<template #nav-actions>
|
|
<gl-button
|
|
v-gl-tooltip
|
|
:href="rssPath"
|
|
icon="rss"
|
|
:title="$options.i18n.rssLabel"
|
|
:aria-label="$options.i18n.rssLabel"
|
|
/>
|
|
<gl-button
|
|
v-gl-tooltip
|
|
:href="calendarPath"
|
|
icon="calendar"
|
|
:title="$options.i18n.calendarLabel"
|
|
:aria-label="$options.i18n.calendarLabel"
|
|
/>
|
|
<csv-import-export-buttons
|
|
v-if="isSignedIn"
|
|
class="gl-md-mr-3"
|
|
:export-csv-path="exportCsvPathWithQuery"
|
|
:issuable-count="totalIssues"
|
|
/>
|
|
<gl-button
|
|
v-if="canBulkUpdate"
|
|
:disabled="isBulkEditButtonDisabled"
|
|
@click="handleBulkUpdateClick"
|
|
>
|
|
{{ $options.i18n.editIssues }}
|
|
</gl-button>
|
|
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
|
|
{{ $options.i18n.newIssueLabel }}
|
|
</gl-button>
|
|
</template>
|
|
|
|
<template #timeframe="{ issuable = {} }">
|
|
<issue-card-time-info :issue="issuable" />
|
|
</template>
|
|
|
|
<template #status="{ issuable = {} }">
|
|
{{ getStatus(issuable) }}
|
|
</template>
|
|
|
|
<template #statistics="{ issuable = {} }">
|
|
<li
|
|
v-if="issuable.mergeRequestsCount"
|
|
v-gl-tooltip
|
|
class="gl-display-none gl-sm-display-block"
|
|
:title="$options.i18n.relatedMergeRequests"
|
|
data-testid="issuable-mr"
|
|
>
|
|
<gl-icon name="merge-request" />
|
|
{{ issuable.mergeRequestsCount }}
|
|
</li>
|
|
<li
|
|
v-if="issuable.upvotes"
|
|
v-gl-tooltip
|
|
class="gl-display-none gl-sm-display-block"
|
|
:title="$options.i18n.upvotes"
|
|
data-testid="issuable-upvotes"
|
|
>
|
|
<gl-icon name="thumb-up" />
|
|
{{ issuable.upvotes }}
|
|
</li>
|
|
<li
|
|
v-if="issuable.downvotes"
|
|
v-gl-tooltip
|
|
class="gl-display-none gl-sm-display-block"
|
|
:title="$options.i18n.downvotes"
|
|
data-testid="issuable-downvotes"
|
|
>
|
|
<gl-icon name="thumb-down" />
|
|
{{ issuable.downvotes }}
|
|
</li>
|
|
<blocking-issues-count
|
|
class="gl-display-none gl-sm-display-block"
|
|
:blocking-issues-count="issuable.blockingIssuesCount"
|
|
:is-list-item="true"
|
|
/>
|
|
</template>
|
|
|
|
<template #empty-state>
|
|
<gl-empty-state
|
|
v-if="hasSearch"
|
|
:description="$options.i18n.noSearchResultsDescription"
|
|
:title="$options.i18n.noSearchResultsTitle"
|
|
:svg-path="emptyStateSvgPath"
|
|
>
|
|
<template #actions>
|
|
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
|
|
{{ $options.i18n.newIssueLabel }}
|
|
</gl-button>
|
|
</template>
|
|
</gl-empty-state>
|
|
|
|
<gl-empty-state
|
|
v-else-if="isOpenTab"
|
|
:description="$options.i18n.noOpenIssuesDescription"
|
|
:title="$options.i18n.noOpenIssuesTitle"
|
|
:svg-path="emptyStateSvgPath"
|
|
>
|
|
<template #actions>
|
|
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
|
|
{{ $options.i18n.newIssueLabel }}
|
|
</gl-button>
|
|
</template>
|
|
</gl-empty-state>
|
|
|
|
<gl-empty-state
|
|
v-else
|
|
:title="$options.i18n.noClosedIssuesTitle"
|
|
:svg-path="emptyStateSvgPath"
|
|
/>
|
|
</template>
|
|
</issuable-list>
|
|
|
|
<issuable-by-email v-if="initialEmail" class="gl-text-center gl-pt-5 gl-pb-7" />
|
|
</div>
|
|
|
|
<div v-else-if="isSignedIn">
|
|
<gl-empty-state
|
|
:description="$options.i18n.noIssuesSignedInDescription"
|
|
:title="$options.i18n.noIssuesSignedInTitle"
|
|
:svg-path="emptyStateSvgPath"
|
|
>
|
|
<template #actions>
|
|
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
|
|
{{ $options.i18n.newIssueLabel }}
|
|
</gl-button>
|
|
<csv-import-export-buttons
|
|
class="gl-mr-3"
|
|
:export-csv-path="exportCsvPathWithQuery"
|
|
:issuable-count="totalIssues"
|
|
/>
|
|
</template>
|
|
</gl-empty-state>
|
|
<hr />
|
|
<p class="gl-text-center gl-font-weight-bold gl-mb-0">
|
|
{{ $options.i18n.jiraIntegrationTitle }}
|
|
</p>
|
|
<p class="gl-text-center gl-mb-0">
|
|
<gl-sprintf :message="$options.i18n.jiraIntegrationMessage">
|
|
<template #jiraDocsLink="{ content }">
|
|
<gl-link :href="jiraIntegrationPath">{{ content }}</gl-link>
|
|
</template>
|
|
</gl-sprintf>
|
|
</p>
|
|
<p class="gl-text-center gl-text-gray-500">
|
|
{{ $options.i18n.jiraIntegrationSecondaryMessage }}
|
|
</p>
|
|
</div>
|
|
|
|
<gl-empty-state
|
|
v-else
|
|
:description="$options.i18n.noIssuesSignedOutDescription"
|
|
:title="$options.i18n.noIssuesSignedOutTitle"
|
|
:svg-path="emptyStateSvgPath"
|
|
:primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
|
|
:primary-button-link="signInPath"
|
|
/>
|
|
</template>
|