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/issuables_list_app.vue')
-rw-r--r--app/assets/javascripts/issues_list/components/issuables_list_app.vue424
1 files changed, 424 insertions, 0 deletions
diff --git a/app/assets/javascripts/issues_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
new file mode 100644
index 00000000000..0d4f5bce965
--- /dev/null
+++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
@@ -0,0 +1,424 @@
+<script>
+import { toNumber, omit } from 'lodash';
+import {
+ GlEmptyState,
+ GlPagination,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
+import { deprecatedCreateFlash as flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import {
+ scrollToElement,
+ urlParamsToObject,
+ historyPushState,
+ getParameterByName,
+} from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import initManualOrdering from '~/manual_ordering';
+import Issuable from './issuable.vue';
+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 { setUrlParams } from '~/lib/utils/url_utility';
+import issueableEventHub from '../eventhub';
+import { emptyStateHelper } from '../service_desk_helper';
+
+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() {
+ 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) {
+ let search = null;
+
+ filters.forEach(filter => {
+ if (typeof filter === 'string') {
+ search = filter;
+ }
+ });
+
+ this.filters.search = search;
+ 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>