Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/issues/list/components/issues_list_app.vue')
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue821
1 files changed, 821 insertions, 0 deletions
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
new file mode 100644
index 00000000000..8b15e801f02
--- /dev/null
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -0,0 +1,821 @@
+<script>
+import {
+ GlButton,
+ GlEmptyState,
+ GlFilteredSearchToken,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { orderBy } from 'lodash';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
+import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
+import createFlash, { FLASH_TYPES } from '~/flash';
+import { TYPE_USER } from '~/graphql_shared/constants';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { ITEM_TYPE } from '~/groups/constants';
+import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
+import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
+import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
+import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
+import {
+ CREATED_DESC,
+ i18n,
+ MAX_LIST_SIZE,
+ PAGE_SIZE,
+ PARAM_DUE_DATE,
+ PARAM_SORT,
+ PARAM_STATE,
+ RELATIVE_POSITION_ASC,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
+ UPDATED_DESC,
+ urlSortParams,
+} from '~/issues/list/constants';
+import {
+ convertToApiParams,
+ convertToSearchQuery,
+ convertToUrlParams,
+ getDueDateValue,
+ getFilterTokens,
+ getInitialPageParams,
+ getSortKey,
+ getSortOptions,
+} from '~/issues/list/utils';
+import axios from '~/lib/utils/axios_utils';
+import { scrollUp } from '~/lib/utils/scroll_utils';
+import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
+import {
+ DEFAULT_NONE_ANY,
+ OPERATOR_IS_ONLY,
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_CONFIDENTIAL,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_MY_REACTION,
+ TOKEN_TITLE_RELEASE,
+ TOKEN_TITLE_TYPE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import eventHub from '../eventhub';
+import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql';
+import searchLabelsQuery from '../queries/search_labels.query.graphql';
+import searchMilestonesQuery from '../queries/search_milestones.query.graphql';
+import searchUsersQuery from '../queries/search_users.query.graphql';
+import NewIssueDropdown from './new_issue_dropdown.vue';
+
+const AuthorToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/author_token.vue');
+const EmojiToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
+const LabelToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue');
+const MilestoneToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
+const ReleaseToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/release_token.vue');
+
+export default {
+ i18n,
+ IssuableListTabs,
+ components: {
+ CsvImportExportButtons,
+ GlButton,
+ GlEmptyState,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ IssuableByEmail,
+ IssuableList,
+ IssueCardTimeInfo,
+ NewIssueDropdown,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: {
+ autocompleteAwardEmojisPath: {
+ default: '',
+ },
+ calendarPath: {
+ default: '',
+ },
+ canBulkUpdate: {
+ default: false,
+ },
+ emptyStateSvgPath: {
+ default: '',
+ },
+ exportCsvPath: {
+ default: '',
+ },
+ fullPath: {
+ default: '',
+ },
+ hasAnyIssues: {
+ default: false,
+ },
+ hasAnyProjects: {
+ default: false,
+ },
+ hasBlockedIssuesFeature: {
+ default: false,
+ },
+ hasIssueWeightsFeature: {
+ default: false,
+ },
+ hasMultipleIssueAssigneesFeature: {
+ default: false,
+ },
+ initialEmail: {
+ default: '',
+ },
+ isAnonymousSearchDisabled: {
+ default: false,
+ },
+ isIssueRepositioningDisabled: {
+ default: false,
+ },
+ isProject: {
+ default: false,
+ },
+ isSignedIn: {
+ default: false,
+ },
+ jiraIntegrationPath: {
+ default: '',
+ },
+ newIssuePath: {
+ default: '',
+ },
+ releasesPath: {
+ default: '',
+ },
+ rssPath: {
+ default: '',
+ },
+ showNewIssueLink: {
+ default: false,
+ },
+ signInPath: {
+ default: '',
+ },
+ },
+ props: {
+ eeSearchTokens: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ const state = getParameterByName(PARAM_STATE);
+ const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
+ let sortKey = getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey;
+
+ if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
+ this.showIssueRepositioningMessage();
+ sortKey = defaultSortKey;
+ }
+
+ const isSearchDisabled =
+ this.isAnonymousSearchDisabled &&
+ !this.isSignedIn &&
+ window.location.search.includes('search=');
+
+ if (isSearchDisabled) {
+ this.showAnonymousSearchingMessage();
+ }
+
+ return {
+ dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
+ exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
+ filterTokens: isSearchDisabled ? [] : getFilterTokens(window.location.search),
+ issues: [],
+ issuesCounts: {},
+ issuesError: null,
+ pageInfo: {},
+ pageParams: getInitialPageParams(sortKey),
+ showBulkEditSidebar: false,
+ sortKey,
+ state: state || IssuableStates.Opened,
+ };
+ },
+ apollo: {
+ issues: {
+ query: getIssuesQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return data[this.namespace]?.issues.nodes ?? [];
+ },
+ result({ data }) {
+ this.pageInfo = data[this.namespace]?.issues.pageInfo ?? {};
+ this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
+ },
+ error(error) {
+ this.issuesError = this.$options.i18n.errorFetchingIssues;
+ Sentry.captureException(error);
+ },
+ skip() {
+ return !this.hasAnyIssues;
+ },
+ debounce: 200,
+ },
+ issuesCounts: {
+ query: getIssuesCountsQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return data[this.namespace] ?? {};
+ },
+ error(error) {
+ this.issuesError = this.$options.i18n.errorFetchingCounts;
+ Sentry.captureException(error);
+ },
+ skip() {
+ return !this.hasAnyIssues;
+ },
+ debounce: 200,
+ context: {
+ isSingleRequest: true,
+ },
+ },
+ },
+ computed: {
+ queryVariables() {
+ return {
+ fullPath: this.fullPath,
+ isProject: this.isProject,
+ isSignedIn: this.isSignedIn,
+ search: this.searchQuery,
+ sort: this.sortKey,
+ state: this.state,
+ ...this.pageParams,
+ ...this.apiFilterParams,
+ };
+ },
+ namespace() {
+ return this.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
+ },
+ hasSearch() {
+ return this.searchQuery || Object.keys(this.urlFilterParams).length;
+ },
+ isBulkEditButtonDisabled() {
+ return this.showBulkEditSidebar || !this.issues.length;
+ },
+ isManualOrdering() {
+ return this.sortKey === RELATIVE_POSITION_ASC;
+ },
+ isOpenTab() {
+ return this.state === IssuableStates.Opened;
+ },
+ showCsvButtons() {
+ return this.isProject && this.isSignedIn;
+ },
+ showNewIssueDropdown() {
+ return !this.isProject && this.hasAnyProjects;
+ },
+ apiFilterParams() {
+ return convertToApiParams(this.filterTokens);
+ },
+ urlFilterParams() {
+ return convertToUrlParams(this.filterTokens);
+ },
+ searchQuery() {
+ return convertToSearchQuery(this.filterTokens) || undefined;
+ },
+ searchTokens() {
+ const preloadedAuthors = [];
+
+ if (gon.current_user_id) {
+ preloadedAuthors.push({
+ id: convertToGraphQLId(TYPE_USER, gon.current_user_id),
+ name: gon.current_user_fullname,
+ username: gon.current_username,
+ avatar_url: gon.current_user_avatar_url,
+ });
+ }
+
+ const tokens = [
+ {
+ type: TOKEN_TYPE_AUTHOR,
+ title: TOKEN_TITLE_AUTHOR,
+ icon: 'pencil',
+ token: AuthorToken,
+ dataType: 'user',
+ unique: true,
+ defaultAuthors: [],
+ fetchAuthors: this.fetchUsers,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-author`,
+ preloadedAuthors,
+ },
+ {
+ type: TOKEN_TYPE_ASSIGNEE,
+ title: TOKEN_TITLE_ASSIGNEE,
+ icon: 'user',
+ token: AuthorToken,
+ dataType: 'user',
+ unique: !this.hasMultipleIssueAssigneesFeature,
+ defaultAuthors: DEFAULT_NONE_ANY,
+ fetchAuthors: this.fetchUsers,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
+ preloadedAuthors,
+ },
+ {
+ type: TOKEN_TYPE_MILESTONE,
+ title: TOKEN_TITLE_MILESTONE,
+ icon: 'clock',
+ token: MilestoneToken,
+ fetchMilestones: this.fetchMilestones,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-milestone`,
+ },
+ {
+ type: TOKEN_TYPE_LABEL,
+ title: TOKEN_TITLE_LABEL,
+ icon: 'labels',
+ token: LabelToken,
+ defaultLabels: DEFAULT_NONE_ANY,
+ fetchLabels: this.fetchLabels,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`,
+ },
+ {
+ type: TOKEN_TYPE_TYPE,
+ title: TOKEN_TITLE_TYPE,
+ icon: 'issues',
+ token: GlFilteredSearchToken,
+ options: [
+ { icon: 'issue-type-issue', title: 'issue', value: 'issue' },
+ { icon: 'issue-type-incident', title: 'incident', value: 'incident' },
+ { icon: 'issue-type-test-case', title: 'test_case', value: 'test_case' },
+ ],
+ },
+ ];
+
+ if (this.isProject) {
+ tokens.push({
+ type: TOKEN_TYPE_RELEASE,
+ title: TOKEN_TITLE_RELEASE,
+ icon: 'rocket',
+ token: ReleaseToken,
+ fetchReleases: this.fetchReleases,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-release`,
+ });
+ }
+
+ if (this.isSignedIn) {
+ tokens.push({
+ type: TOKEN_TYPE_MY_REACTION,
+ title: TOKEN_TITLE_MY_REACTION,
+ icon: 'thumb-up',
+ token: EmojiToken,
+ unique: true,
+ fetchEmojis: this.fetchEmojis,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-my_reaction`,
+ });
+
+ 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.eeSearchTokens.length) {
+ tokens.push(...this.eeSearchTokens);
+ }
+
+ tokens.sort((a, b) => a.title.localeCompare(b.title));
+
+ return orderBy(tokens, ['title']);
+ },
+ showPaginationControls() {
+ return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
+ },
+ sortOptions() {
+ return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
+ },
+ tabCounts() {
+ const { openedIssues, closedIssues, allIssues } = this.issuesCounts;
+ return {
+ [IssuableStates.Opened]: openedIssues?.count,
+ [IssuableStates.Closed]: closedIssues?.count,
+ [IssuableStates.All]: allIssues?.count,
+ };
+ },
+ currentTabCount() {
+ return this.tabCounts[this.state] ?? 0;
+ },
+ urlParams() {
+ return {
+ due_date: this.dueDateFilter,
+ search: this.searchQuery,
+ sort: urlSortParams[this.sortKey],
+ state: this.state,
+ ...this.urlFilterParams,
+ };
+ },
+ },
+ created() {
+ this.cache = {};
+ },
+ mounted() {
+ eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
+ },
+ 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);
+ },
+ fetchReleases(search) {
+ return this.fetchWithCache(this.releasesPath, 'releases', 'tag', search);
+ },
+ fetchLabels(search) {
+ return this.$apollo
+ .query({
+ query: searchLabelsQuery,
+ variables: { fullPath: this.fullPath, search, isProject: this.isProject },
+ })
+ .then(({ data }) => data[this.namespace]?.labels.nodes)
+ .then((labels) =>
+ // TODO remove once we can search by title-only on the backend
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/346353
+ labels.filter((label) => label.title.toLowerCase().includes(search.toLowerCase())),
+ );
+ },
+ fetchMilestones(search) {
+ return this.$apollo
+ .query({
+ query: searchMilestonesQuery,
+ variables: { fullPath: this.fullPath, search, isProject: this.isProject },
+ })
+ .then(({ data }) => data[this.namespace]?.milestones.nodes);
+ },
+ fetchUsers(search) {
+ return this.$apollo
+ .query({
+ query: searchUsersQuery,
+ variables: { fullPath: this.fullPath, search, isProject: this.isProject },
+ })
+ .then(({ data }) =>
+ data[this.namespace]?.[`${this.namespace}Members`].nodes.map((member) => member.user),
+ );
+ },
+ getExportCsvPathWithQuery() {
+ return `${this.exportCsvPath}${window.location.search}`;
+ },
+ getStatus(issue) {
+ if (issue.closedAt && issue.moved) {
+ 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 bulkUpdateSidebar = await import('~/issuable/bulk_update_sidebar');
+ bulkUpdateSidebar.initBulkUpdateSidebar('issuable_');
+ bulkUpdateSidebar.initIssueStatusSelect();
+
+ 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.pageParams = getInitialPageParams(this.sortKey);
+ }
+ this.state = state;
+ },
+ handleDismissAlert() {
+ this.issuesError = null;
+ },
+ handleFilter(filter) {
+ if (this.isAnonymousSearchDisabled && !this.isSignedIn) {
+ this.showAnonymousSearchingMessage();
+ return;
+ }
+ this.pageParams = getInitialPageParams(this.sortKey);
+ this.filterTokens = filter;
+ },
+ handleNextPage() {
+ this.pageParams = {
+ afterCursor: this.pageInfo.endCursor,
+ firstPageSize: PAGE_SIZE,
+ };
+ scrollUp();
+ },
+ handlePreviousPage() {
+ this.pageParams = {
+ beforeCursor: this.pageInfo.startCursor,
+ lastPageSize: PAGE_SIZE,
+ };
+ scrollUp();
+ },
+ 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(joinPaths(issueToMove.webPath, 'reorder'), {
+ move_before_id: isMovingToBeginning ? null : getIdFromGraphQLId(moveBeforeId),
+ move_after_id: isMovingToEnd ? null : getIdFromGraphQLId(moveAfterId),
+ group_full_path: this.isProject ? undefined : this.fullPath,
+ })
+ .then(() => {
+ const serializedVariables = JSON.stringify(this.queryVariables);
+ return this.$apollo.mutate({
+ mutation: reorderIssuesMutation,
+ variables: { oldIndex, newIndex, namespace: this.namespace, serializedVariables },
+ });
+ })
+ .catch((error) => {
+ this.issuesError = this.$options.i18n.reorderError;
+ Sentry.captureException(error);
+ });
+ },
+ handleSort(sortKey) {
+ if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
+ this.showIssueRepositioningMessage();
+ return;
+ }
+
+ if (this.sortKey !== sortKey) {
+ this.pageParams = getInitialPageParams(sortKey);
+ }
+ this.sortKey = sortKey;
+ },
+ showAnonymousSearchingMessage() {
+ createFlash({
+ message: this.$options.i18n.anonymousSearchingMessage,
+ type: FLASH_TYPES.NOTICE,
+ });
+ },
+ showIssueRepositioningMessage() {
+ createFlash({
+ message: this.$options.i18n.issueRepositioningMessage,
+ type: FLASH_TYPES.NOTICE,
+ });
+ },
+ toggleBulkEditSidebar(showBulkEditSidebar) {
+ this.showBulkEditSidebar = showBulkEditSidebar;
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="hasAnyIssues">
+ <issuable-list
+ :namespace="fullPath"
+ 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"
+ :error="issuesError"
+ label-filter-param="label_name"
+ :tabs="$options.IssuableListTabs"
+ :current-tab="state"
+ :tab-counts="tabCounts"
+ :issuables-loading="$apollo.queries.issues.loading"
+ :is-manual-ordering="isManualOrdering"
+ :show-bulk-edit-sidebar="showBulkEditSidebar"
+ :show-pagination-controls="showPaginationControls"
+ :use-keyset-pagination="true"
+ :has-next-page="pageInfo.hasNextPage"
+ :has-previous-page="pageInfo.hasPreviousPage"
+ :url-params="urlParams"
+ @click-tab="handleClickTab"
+ @dismiss-alert="handleDismissAlert"
+ @filter="handleFilter"
+ @next-page="handleNextPage"
+ @previous-page="handlePreviousPage"
+ @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="showCsvButtons"
+ class="gl-md-mr-3"
+ :export-csv-path="exportCsvPathWithQuery"
+ :issuable-count="currentTabCount"
+ />
+ <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>
+ <new-issue-dropdown v-if="showNewIssueDropdown" />
+ </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="merge-requests"
+ >
+ <gl-icon name="merge-request" />
+ {{ issuable.mergeRequestsCount }}
+ </li>
+ <li
+ v-if="issuable.upvotes"
+ v-gl-tooltip
+ class="issuable-upvotes 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="issuable-downvotes gl-display-none gl-sm-display-block"
+ :title="$options.i18n.downvotes"
+ data-testid="issuable-downvotes"
+ >
+ <gl-icon name="thumb-down" />
+ {{ issuable.downvotes }}
+ </li>
+ <slot :issuable="issuable"></slot>
+ </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
+ v-if="showCsvButtons"
+ class="gl-mr-3"
+ :export-csv-path="exportCsvPathWithQuery"
+ :issuable-count="currentTabCount"
+ />
+ <new-issue-dropdown v-if="showNewIssueDropdown" />
+ </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>