diff options
Diffstat (limited to 'app/assets/javascripts/packages/list/components')
4 files changed, 321 insertions, 0 deletions
diff --git a/app/assets/javascripts/packages/list/components/packages_filter.vue b/app/assets/javascripts/packages/list/components/packages_filter.vue new file mode 100644 index 00000000000..17398071217 --- /dev/null +++ b/app/assets/javascripts/packages/list/components/packages_filter.vue @@ -0,0 +1,21 @@ +<script> +import { GlSearchBoxByClick } from '@gitlab/ui'; +import { mapActions } from 'vuex'; + +export default { + components: { + GlSearchBoxByClick, + }, + methods: { + ...mapActions(['setFilter']), + }, +}; +</script> + +<template> + <gl-search-box-by-click + :placeholder="s__('PackageRegistry|Filter by name')" + @submit="$emit('filter')" + @input="setFilter" + /> +</template> diff --git a/app/assets/javascripts/packages/list/components/packages_list.vue b/app/assets/javascripts/packages/list/components/packages_list.vue new file mode 100644 index 00000000000..b26c6087e14 --- /dev/null +++ b/app/assets/javascripts/packages/list/components/packages_list.vue @@ -0,0 +1,129 @@ +<script> +import { mapState, mapGetters } from 'vuex'; +import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui'; +import Tracking from '~/tracking'; +import { s__ } from '~/locale'; +import { TrackingActions } from '../../shared/constants'; +import { packageTypeToTrackCategory } from '../../shared/utils'; +import PackagesListLoader from '../../shared/components/packages_list_loader.vue'; +import PackagesListRow from '../../shared/components/package_list_row.vue'; + +export default { + components: { + GlPagination, + GlModal, + GlSprintf, + PackagesListLoader, + PackagesListRow, + }, + mixins: [Tracking.mixin()], + data() { + return { + itemToBeDeleted: null, + }; + }, + computed: { + ...mapState({ + perPage: state => state.pagination.perPage, + totalItems: state => state.pagination.total, + page: state => state.pagination.page, + isGroupPage: state => state.config.isGroupPage, + isLoading: 'isLoading', + }), + ...mapGetters({ list: 'getList' }), + currentPage: { + get() { + return this.page; + }, + set(value) { + this.$emit('page:changed', value); + }, + }, + isListEmpty() { + return !this.list || this.list.length === 0; + }, + modalAction() { + return s__('PackageRegistry|Delete package'); + }, + deletePackageName() { + return this.itemToBeDeleted?.name ?? ''; + }, + tracking() { + const category = this.itemToBeDeleted + ? packageTypeToTrackCategory(this.itemToBeDeleted.package_type) + : undefined; + return { + category, + }; + }, + }, + methods: { + setItemToBeDeleted(item) { + this.itemToBeDeleted = { ...item }; + this.track(TrackingActions.REQUEST_DELETE_PACKAGE); + this.$refs.packageListDeleteModal.show(); + }, + deleteItemConfirmation() { + this.$emit('package:delete', this.itemToBeDeleted); + this.track(TrackingActions.DELETE_PACKAGE); + this.itemToBeDeleted = null; + }, + deleteItemCanceled() { + this.track(TrackingActions.CANCEL_DELETE_PACKAGE); + this.itemToBeDeleted = null; + }, + }, + i18n: { + deleteModalContent: s__( + 'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?', + ), + }, +}; +</script> + +<template> + <div class="d-flex flex-column"> + <slot v-if="isListEmpty && !isLoading" name="empty-state"></slot> + + <div v-else-if="isLoading"> + <packages-list-loader :is-group="isGroupPage" /> + </div> + + <template v-else> + <div data-qa-selector="packages-table"> + <packages-list-row + v-for="packageEntity in list" + :key="packageEntity.id" + :package-entity="packageEntity" + :package-link="packageEntity._links.web_path" + :is-group="isGroupPage" + @packageToDelete="setItemToBeDeleted" + /> + </div> + + <gl-pagination + v-model="currentPage" + :per-page="perPage" + :total-items="totalItems" + align="center" + class="w-100 mt-2" + /> + + <gl-modal + ref="packageListDeleteModal" + modal-id="confirm-delete-pacakge" + ok-variant="danger" + @ok="deleteItemConfirmation" + @cancel="deleteItemCanceled" + > + <template #modal-title>{{ modalAction }}</template> + <template #modal-ok>{{ modalAction }}</template> + <gl-sprintf :message="$options.i18n.deleteModalContent"> + <template #name> + <strong>{{ deletePackageName }}</strong> + </template> + </gl-sprintf> + </gl-modal> + </template> + </div> +</template> diff --git a/app/assets/javascripts/packages/list/components/packages_list_app.vue b/app/assets/javascripts/packages/list/components/packages_list_app.vue new file mode 100644 index 00000000000..ef242ea5f75 --- /dev/null +++ b/app/assets/javascripts/packages/list/components/packages_list_app.vue @@ -0,0 +1,111 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { GlEmptyState, GlTab, GlTabs, GlLink, GlSprintf } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; +import PackageFilter from './packages_filter.vue'; +import PackageList from './packages_list.vue'; +import PackageSort from './packages_sort.vue'; +import { PACKAGE_REGISTRY_TABS } from '../constants'; +import PackagesComingSoon from '../coming_soon/packages_coming_soon.vue'; + +export default { + components: { + GlEmptyState, + GlTab, + GlTabs, + GlLink, + GlSprintf, + PackageFilter, + PackageList, + PackageSort, + PackagesComingSoon, + }, + computed: { + ...mapState({ + emptyListIllustration: state => state.config.emptyListIllustration, + emptyListHelpUrl: state => state.config.emptyListHelpUrl, + comingSoon: state => state.config.comingSoon, + filterQuery: state => state.filterQuery, + selectedType: state => state.selectedType, + }), + tabsToRender() { + return PACKAGE_REGISTRY_TABS; + }, + }, + mounted() { + this.requestPackagesList(); + }, + methods: { + ...mapActions(['requestPackagesList', 'requestDeletePackage', 'setSelectedType']), + onPageChanged(page) { + return this.requestPackagesList({ page }); + }, + onPackageDeleteRequest(item) { + return this.requestDeletePackage(item); + }, + tabChanged(index) { + const selected = PACKAGE_REGISTRY_TABS[index]; + + if (selected !== this.selectedType) { + this.setSelectedType(selected); + this.requestPackagesList(); + } + }, + emptyStateTitle({ title, type }) { + if (this.filterQuery) { + return s__('PackageRegistry|Sorry, your filter produced no results'); + } + + if (type) { + return sprintf(s__('PackageRegistry|There are no %{packageType} packages yet'), { + packageType: title, + }); + } + + return s__('PackageRegistry|There are no packages yet'); + }, + }, + i18n: { + widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'), + noResults: s__( + 'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.', + ), + }, +}; +</script> + +<template> + <gl-tabs @input="tabChanged"> + <template #tabs-end> + <div class="d-flex align-self-center ml-md-auto py-1 py-md-0"> + <package-filter class="mr-1" @filter="requestPackagesList" /> + <package-sort @sort:changed="requestPackagesList" /> + </div> + </template> + + <gl-tab v-for="(tab, index) in tabsToRender" :key="index" :title="tab.title"> + <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest"> + <template #empty-state> + <gl-empty-state :title="emptyStateTitle(tab)" :svg-path="emptyListIllustration"> + <template #description> + <gl-sprintf v-if="filterQuery" :message="$options.i18n.widenFilters" /> + <gl-sprintf v-else :message="$options.i18n.noResults"> + <template #noPackagesLink="{content}"> + <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </template> + </gl-empty-state> + </template> + </package-list> + </gl-tab> + + <gl-tab v-if="comingSoon" :title="__('Coming soon')" lazy> + <packages-coming-soon + :illustration="emptyListIllustration" + :project-path="comingSoon.projectPath" + :suggested-contributions-path="comingSoon.suggestedContributions" + /> + </gl-tab> + </gl-tabs> +</template> diff --git a/app/assets/javascripts/packages/list/components/packages_sort.vue b/app/assets/javascripts/packages/list/components/packages_sort.vue new file mode 100644 index 00000000000..fa8f4f39d54 --- /dev/null +++ b/app/assets/javascripts/packages/list/components/packages_sort.vue @@ -0,0 +1,60 @@ +<script> +import { GlSorting, GlSortingItem } from '@gitlab/ui'; +import { mapState, mapActions } from 'vuex'; +import { ASCENDING_ODER, DESCENDING_ORDER } from '../constants'; +import getTableHeaders from '../utils'; + +export default { + name: 'PackageSort', + components: { + GlSorting, + GlSortingItem, + }, + computed: { + ...mapState({ + isGroupPage: state => state.config.isGroupPage, + orderBy: state => state.sorting.orderBy, + sort: state => state.sorting.sort, + }), + sortText() { + const field = this.sortableFields.find(s => s.orderBy === this.orderBy); + return field ? field.label : ''; + }, + sortableFields() { + return getTableHeaders(this.isGroupPage); + }, + isSortAscending() { + return this.sort === ASCENDING_ODER; + }, + }, + methods: { + ...mapActions(['setSorting']), + onDirectionChange() { + const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER; + this.setSorting({ sort }); + this.$emit('sort:changed'); + }, + onSortItemClick(item) { + this.setSorting({ orderBy: item }); + this.$emit('sort:changed'); + }, + }, +}; +</script> + +<template> + <gl-sorting + :text="sortText" + :is-ascending="isSortAscending" + @sortDirectionChange="onDirectionChange" + > + <gl-sorting-item + v-for="item in sortableFields" + ref="packageListSortItem" + :key="item.key" + @click="onSortItemClick(item.orderBy)" + > + {{ item.label }} + </gl-sorting-item> + </gl-sorting> +</template> |