summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue')
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue363
1 files changed, 363 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
new file mode 100644
index 00000000000..2f8401b45f0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -0,0 +1,363 @@
+<script>
+import { GlAlert, GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+
+import { DEFAULT_SKELETON_COUNT } from '../constants';
+import IssuableBulkEditSidebar from './issuable_bulk_edit_sidebar.vue';
+import IssuableItem from './issuable_item.vue';
+import IssuableTabs from './issuable_tabs.vue';
+
+const VueDraggable = () => import('vuedraggable');
+
+export default {
+ vueDraggableAttributes: {
+ animation: 200,
+ ghostClass: 'gl-visibility-hidden',
+ tag: 'ul',
+ },
+ components: {
+ GlAlert,
+ GlKeysetPagination,
+ GlSkeletonLoading,
+ IssuableTabs,
+ FilteredSearchBar,
+ IssuableItem,
+ IssuableBulkEditSidebar,
+ GlPagination,
+ VueDraggable,
+ },
+ props: {
+ namespace: {
+ type: String,
+ required: true,
+ },
+ recentSearchesStorageKey: {
+ type: String,
+ required: true,
+ },
+ searchInputPlaceholder: {
+ type: String,
+ required: true,
+ },
+ searchTokens: {
+ type: Array,
+ required: true,
+ },
+ sortOptions: {
+ type: Array,
+ required: true,
+ },
+ urlParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ initialFilterValue: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ initialSortBy: {
+ type: String,
+ required: false,
+ default: 'created_desc',
+ },
+ issuables: {
+ type: Array,
+ required: true,
+ },
+ tabs: {
+ type: Array,
+ required: true,
+ },
+ tabCounts: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ currentTab: {
+ type: String,
+ required: true,
+ },
+ issuableSymbol: {
+ type: String,
+ required: false,
+ default: '#',
+ },
+ issuablesLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showPaginationControls: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showBulkEditSidebar: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ defaultPageSize: {
+ type: Number,
+ required: false,
+ default: 20,
+ },
+ totalItems: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ currentPage: {
+ type: Number,
+ required: false,
+ default: 1,
+ },
+ previousPage: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ nextPage: {
+ type: Number,
+ required: false,
+ default: 2,
+ },
+ enableLabelPermalinks: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ labelFilterParam: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ isManualOrdering: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ useKeysetPagination: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ hasNextPage: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ hasPreviousPage: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ error: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ checkedIssuables: {},
+ };
+ },
+ computed: {
+ skeletonItemCount() {
+ const { totalItems, defaultPageSize, currentPage } = this;
+ const totalPages = Math.ceil(totalItems / defaultPageSize);
+
+ if (totalPages) {
+ return currentPage < totalPages
+ ? defaultPageSize
+ : totalItems % defaultPageSize || defaultPageSize;
+ }
+ return DEFAULT_SKELETON_COUNT;
+ },
+ allIssuablesChecked() {
+ return this.bulkEditIssuables.length === this.issuables.length;
+ },
+ /**
+ * Returns all the checked issuables from `checkedIssuables` map.
+ */
+ bulkEditIssuables() {
+ return Object.keys(this.checkedIssuables).reduce((acc, issuableId) => {
+ if (this.checkedIssuables[issuableId].checked) {
+ acc.push(this.checkedIssuables[issuableId].issuable);
+ }
+ return acc;
+ }, []);
+ },
+ issuablesWrapper() {
+ return this.isManualOrdering ? VueDraggable : 'ul';
+ },
+ },
+ watch: {
+ issuables(list) {
+ this.checkedIssuables = list.reduce((acc, issuable) => {
+ const id = this.issuableId(issuable);
+ acc[id] = {
+ // By default, an issuable is not checked,
+ // But if `checkedIssuables` is already
+ // populated, use existing value.
+ checked:
+ typeof this.checkedIssuables[id] !== 'boolean'
+ ? false
+ : this.checkedIssuables[id].checked,
+ // We're caching issuable reference here
+ // for ease of populating in `bulkEditIssuables`.
+ issuable,
+ };
+ return acc;
+ }, {});
+ },
+ urlParams: {
+ deep: true,
+ immediate: true,
+ handler(params) {
+ if (Object.keys(params).length) {
+ updateHistory({
+ url: setUrlParams(params, window.location.href, true, false, true),
+ title: document.title,
+ replace: true,
+ });
+ }
+ },
+ },
+ },
+ methods: {
+ issuableId(issuable) {
+ return getIdFromGraphQLId(issuable.id) || issuable.iid || uniqueId();
+ },
+ issuableChecked(issuable) {
+ return this.checkedIssuables[this.issuableId(issuable)]?.checked;
+ },
+ handleIssuableCheckedInput(issuable, value) {
+ this.checkedIssuables[this.issuableId(issuable)].checked = value;
+ this.$emit('update-legacy-bulk-edit');
+ },
+ handleAllIssuablesCheckedInput(value) {
+ Object.keys(this.checkedIssuables).forEach((issuableId) => {
+ this.checkedIssuables[issuableId].checked = value;
+ });
+ this.$emit('update-legacy-bulk-edit');
+ },
+ handleVueDraggableUpdate({ newIndex, oldIndex }) {
+ this.$emit('reorder', { newIndex, oldIndex });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issuable-list-container">
+ <issuable-tabs
+ :tabs="tabs"
+ :tab-counts="tabCounts"
+ :current-tab="currentTab"
+ @click="$emit('click-tab', $event)"
+ >
+ <template #nav-actions>
+ <slot name="nav-actions"></slot>
+ </template>
+ </issuable-tabs>
+ <filtered-search-bar
+ :namespace="namespace"
+ :recent-searches-storage-key="recentSearchesStorageKey"
+ :search-input-placeholder="searchInputPlaceholder"
+ :tokens="searchTokens"
+ :sort-options="sortOptions"
+ :initial-filter-value="initialFilterValue"
+ :initial-sort-by="initialSortBy"
+ :show-checkbox="showBulkEditSidebar"
+ :checkbox-checked="allIssuablesChecked"
+ class="gl-flex-grow-1 gl-border-t-none row-content-block"
+ data-qa-selector="issuable_search_container"
+ @checked-input="handleAllIssuablesCheckedInput"
+ @onFilter="$emit('filter', $event)"
+ @onSort="$emit('sort', $event)"
+ />
+ <gl-alert v-if="error" variant="danger" @dismiss="$emit('dismiss-alert')">{{ error }}</gl-alert>
+ <issuable-bulk-edit-sidebar :expanded="showBulkEditSidebar">
+ <template #bulk-edit-actions>
+ <slot name="bulk-edit-actions" :checked-issuables="bulkEditIssuables"></slot>
+ </template>
+ <template #sidebar-items>
+ <slot name="sidebar-items" :checked-issuables="bulkEditIssuables"></slot>
+ </template>
+ </issuable-bulk-edit-sidebar>
+ <ul v-if="issuablesLoading" class="content-list">
+ <li v-for="n in skeletonItemCount" :key="n" class="issue gl-px-5! gl-py-5!">
+ <gl-skeleton-loading />
+ </li>
+ </ul>
+ <template v-else>
+ <component
+ :is="issuablesWrapper"
+ v-if="issuables.length > 0"
+ class="content-list issuable-list issues-list"
+ :class="{ 'manual-ordering': isManualOrdering }"
+ v-bind="$options.vueDraggableAttributes"
+ @update="handleVueDraggableUpdate"
+ >
+ <issuable-item
+ v-for="issuable in issuables"
+ :key="issuableId(issuable)"
+ :class="{ 'gl-cursor-grab': isManualOrdering }"
+ data-qa-selector="issuable_container"
+ :data-qa-issuable-title="issuable.title"
+ :issuable-symbol="issuableSymbol"
+ :issuable="issuable"
+ :enable-label-permalinks="enableLabelPermalinks"
+ :label-filter-param="labelFilterParam"
+ :show-checkbox="showBulkEditSidebar"
+ :checked="issuableChecked(issuable)"
+ @checked-input="handleIssuableCheckedInput(issuable, $event)"
+ >
+ <template #reference>
+ <slot name="reference" :issuable="issuable"></slot>
+ </template>
+ <template #author>
+ <slot name="author" :author="issuable.author"></slot>
+ </template>
+ <template #timeframe>
+ <slot name="timeframe" :issuable="issuable"></slot>
+ </template>
+ <template #status>
+ <slot name="status" :issuable="issuable"></slot>
+ </template>
+ <template #statistics>
+ <slot name="statistics" :issuable="issuable"></slot>
+ </template>
+ </issuable-item>
+ </component>
+ <slot v-else name="empty-state"></slot>
+ </template>
+
+ <div v-if="showPaginationControls && useKeysetPagination" class="gl-text-center gl-mt-3">
+ <gl-keyset-pagination
+ :has-next-page="hasNextPage"
+ :has-previous-page="hasPreviousPage"
+ @next="$emit('next-page')"
+ @prev="$emit('previous-page')"
+ />
+ </div>
+ <gl-pagination
+ v-else-if="showPaginationControls"
+ :per-page="defaultPageSize"
+ :total-items="totalItems"
+ :value="currentPage"
+ :prev-page="previousPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-pagination gl-mt-3"
+ @input="$emit('page-change', $event)"
+ />
+ </div>
+</template>