debian-mirror-gitlab/app/assets/javascripts/issues_list/components/issuables_list_app.vue
2021-04-29 21:17:54 +05:30

432 lines
11 KiB
Vue

<script>
import {
GlEmptyState,
GlPagination,
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { toNumber, omit } from 'lodash';
import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
scrollToElement,
urlParamsToObject,
historyPushState,
getParameterByName,
} from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import initManualOrdering from '~/manual_ordering';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import {
sortOrderMap,
availableSortOptionsJira,
RELATIVE_POSITION,
PAGE_SIZE,
PAGE_SIZE_MANUAL,
LOADING_LIST_ITEMS_LENGTH,
} from '../constants';
import issueableEventHub from '../eventhub';
import { emptyStateHelper } from '../service_desk_helper';
import Issuable from './issuable.vue';
/**
* @deprecated Use app/assets/javascripts/issuable_list/components/issuable_list_root.vue instead
*/
export default {
LOADING_LIST_ITEMS_LENGTH,
directives: {
SafeHtml,
},
components: {
GlEmptyState,
GlPagination,
GlSkeletonLoading,
Issuable,
FilteredSearchBar,
},
props: {
canBulkEdit: {
type: Boolean,
required: false,
default: false,
},
emptyStateMeta: {
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
projectPath: {
type: String,
required: false,
default: '',
},
sortKey: {
type: String,
required: false,
default: '',
},
type: {
type: String,
required: false,
default: '',
},
},
data() {
return {
availableSortOptionsJira,
filters: {},
isBulkEditing: false,
issuables: [],
loading: false,
page:
getParameterByName('page', window.location.href) !== null
? toNumber(getParameterByName('page'))
: 1,
selection: {},
totalItems: 0,
};
},
computed: {
allIssuablesSelected() {
// WARNING: Because we are only keeping track of selected values
// this works, we will need to rethink this if we start tracking
// [id]: false for not selected values.
return this.issuables.length === Object.keys(this.selection).length;
},
emptyState() {
if (this.issuables.length) {
return {}; // Empty state shouldn't be shown here
}
if (this.isServiceDesk) {
return emptyStateHelper(this.emptyStateMeta);
}
if (this.hasFilters) {
return {
title: __('Sorry, your filter produced no results'),
svgPath: this.emptyStateMeta.svgPath,
description: __('To widen your search, change or remove filters above'),
primaryLink: this.emptyStateMeta.createIssuePath,
primaryText: __('New issue'),
};
}
if (this.filters.state === 'opened') {
return {
title: __('There are no open issues'),
svgPath: this.emptyStateMeta.svgPath,
description: __('To keep this project going, create a new issue'),
primaryLink: this.emptyStateMeta.createIssuePath,
primaryText: __('New issue'),
};
} else if (this.filters.state === 'closed') {
return {
title: __('There are no closed issues'),
svgPath: this.emptyStateMeta.svgPath,
};
}
return {
title: __('There are no issues to show'),
svgPath: this.emptyStateMeta.svgPath,
description: __(
'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.',
),
};
},
hasFilters() {
const ignored = ['utf8', 'state', 'scope', 'order_by', 'sort'];
return Object.keys(omit(this.filters, ignored)).length > 0;
},
isManualOrdering() {
return this.sortKey === RELATIVE_POSITION;
},
itemsPerPage() {
return this.isManualOrdering ? PAGE_SIZE_MANUAL : PAGE_SIZE;
},
baseUrl() {
return window.location.href.replace(/(\?.*)?(#.*)?$/, '');
},
paginationNext() {
return this.page + 1;
},
paginationPrev() {
return this.page - 1;
},
paginationProps() {
const paginationProps = { value: this.page };
if (this.totalItems) {
return {
...paginationProps,
perPage: this.itemsPerPage,
totalItems: this.totalItems,
};
}
return {
...paginationProps,
prevPage: this.paginationPrev,
nextPage: this.paginationNext,
};
},
isServiceDesk() {
return this.type === 'service_desk';
},
isJira() {
return this.type === 'jira';
},
initialFilterValue() {
const value = [];
const { search } = this.getQueryObject();
if (search) {
value.push(search);
}
return value;
},
initialSortBy() {
const { sort } = this.getQueryObject();
return sort || 'created_desc';
},
},
watch: {
selection() {
// We need to call nextTick here to wait for all of the boxes to be checked and rendered
// before we query the dom in issuable_bulk_update_actions.js.
this.$nextTick(() => {
issueableEventHub.$emit('issuables:updateBulkEdit');
});
},
issuables() {
this.$nextTick(() => {
initManualOrdering();
});
},
},
mounted() {
if (this.canBulkEdit) {
this.unsubscribeToggleBulkEdit = issueableEventHub.$on('issuables:toggleBulkEdit', (val) => {
this.isBulkEditing = val;
});
}
this.fetchIssuables();
},
beforeDestroy() {
// eslint-disable-next-line @gitlab/no-global-event-off
issueableEventHub.$off('issuables:toggleBulkEdit');
},
methods: {
isSelected(issuableId) {
return Boolean(this.selection[issuableId]);
},
setSelection(ids) {
ids.forEach((id) => {
this.select(id, true);
});
},
clearSelection() {
this.selection = {};
},
select(id, isSelect = true) {
if (isSelect) {
this.$set(this.selection, id, true);
} else {
this.$delete(this.selection, id);
}
},
fetchIssuables(pageToFetch) {
this.loading = true;
this.clearSelection();
this.setFilters();
return axios
.get(this.endpoint, {
params: {
...this.filters,
with_labels_details: true,
page: pageToFetch || this.page,
per_page: this.itemsPerPage,
},
})
.then((response) => {
this.loading = false;
this.issuables = response.data;
this.totalItems = Number(response.headers['x-total']);
this.page = Number(response.headers['x-page']);
})
.catch(() => {
this.loading = false;
return flash(__('An error occurred while loading issues'));
});
},
getQueryObject() {
return urlParamsToObject(window.location.search);
},
onPaginate(newPage) {
if (newPage === this.page) return;
scrollToElement('#content-body');
// NOTE: This allows for the params to be updated on pagination
historyPushState(
setUrlParams({ ...this.filters, page: newPage }, window.location.href, true),
);
this.fetchIssuables(newPage);
},
onSelectAll() {
if (this.allIssuablesSelected) {
this.selection = {};
} else {
this.setSelection(this.issuables.map(({ id }) => id));
}
},
onSelectIssuable({ issuable, selected }) {
if (!this.canBulkEdit) return;
this.select(issuable.id, selected);
},
setFilters() {
const {
label_name: labels,
milestone_title: milestoneTitle,
'not[label_name]': excludedLabels,
'not[milestone_title]': excludedMilestone,
...filters
} = this.getQueryObject();
// TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/227880
if (milestoneTitle) {
filters.milestone = milestoneTitle;
}
if (Array.isArray(labels)) {
filters.labels = labels.join(',');
}
if (!filters.state) {
filters.state = 'opened';
}
if (excludedLabels) {
filters['not[labels]'] = excludedLabels;
}
if (excludedMilestone) {
filters['not[milestone]'] = excludedMilestone;
}
Object.assign(filters, sortOrderMap[this.sortKey]);
this.filters = filters;
},
refetchIssuables() {
const ignored = ['utf8'];
const params = omit(this.filters, ignored);
historyPushState(setUrlParams(params, window.location.href, true, true));
this.fetchIssuables();
},
handleFilter(filters) {
const searchTokens = [];
filters.forEach((filter) => {
if (filter.type === 'filtered-search-term') {
if (filter.value.data) {
searchTokens.push(filter.value.data);
}
}
});
if (searchTokens.length) {
this.filters.search = searchTokens.join(' ');
}
this.page = 1;
this.refetchIssuables();
},
handleSort(sort) {
this.filters.sort = sort;
this.page = 1;
this.refetchIssuables();
},
},
};
</script>
<template>
<div>
<filtered-search-bar
v-if="isJira"
:namespace="projectPath"
:search-input-placeholder="__('Search Jira issues')"
:tokens="[]"
:sort-options="availableSortOptionsJira"
:initial-filter-value="initialFilterValue"
:initial-sort-by="initialSortBy"
class="row-content-block"
@onFilter="handleFilter"
@onSort="handleSort"
/>
<ul v-if="loading" class="content-list">
<li v-for="n in $options.LOADING_LIST_ITEMS_LENGTH" :key="n" class="issue gl-px-5! gl-py-5!">
<gl-skeleton-loading />
</li>
</ul>
<div v-else-if="issuables.length">
<div v-if="isBulkEditing" class="issue px-3 py-3 border-bottom border-light">
<input
id="check-all-issues"
type="checkbox"
:checked="allIssuablesSelected"
class="mr-2"
@click="onSelectAll"
/>
<strong>{{ __('Select all') }}</strong>
</div>
<ul
class="content-list issuable-list issues-list"
:class="{ 'manual-ordering': isManualOrdering }"
>
<issuable
v-for="issuable in issuables"
:key="issuable.id"
class="pr-3"
:class="{ 'user-can-drag': isManualOrdering }"
:issuable="issuable"
:is-bulk-editing="isBulkEditing"
:selected="isSelected(issuable.id)"
:base-url="baseUrl"
@select="onSelectIssuable"
/>
</ul>
<div class="mt-3">
<gl-pagination
v-bind="paginationProps"
class="gl-justify-content-center"
@input="onPaginate"
/>
</div>
</div>
<gl-empty-state
v-else
:title="emptyState.title"
:svg-path="emptyState.svgPath"
:primary-button-link="emptyState.primaryLink"
:primary-button-text="emptyState.primaryText"
>
<template #description>
<div v-safe-html="emptyState.description"></div>
</template>
</gl-empty-state>
</div>
</template>