diff options
Diffstat (limited to 'app/assets/javascripts/releases')
15 files changed, 382 insertions, 694 deletions
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue index 39140216bc5..8365e6a5ab0 100644 --- a/app/assets/javascripts/releases/components/app_edit_new.vue +++ b/app/assets/javascripts/releases/components/app_edit_new.vue @@ -185,7 +185,7 @@ export default { <gl-button class="mr-auto js-no-auto-disable" category="primary" - variant="success" + variant="confirm" type="submit" :disabled="isFormSubmissionDisabled" data-testid="submit-button" diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue index e53bfea7389..59fa2fca736 100644 --- a/app/assets/javascripts/releases/components/app_index.vue +++ b/app/assets/javascripts/releases/components/app_index.vue @@ -1,67 +1,237 @@ <script> -import { GlEmptyState, GlLink, GlButton } from '@gitlab/ui'; -import { mapState, mapActions } from 'vuex'; -import { getParameterByName } from '~/lib/utils/url_utility'; +import { GlButton } from '@gitlab/ui'; +import createFlash from '~/flash'; +import { historyPushState } from '~/lib/utils/common_utils'; +import { scrollUp } from '~/lib/utils/scroll_utils'; +import { setUrlParams, getParameterByName } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; +import { PAGE_SIZE, DEFAULT_SORT } from '~/releases/constants'; +import { convertAllReleasesGraphQLResponse } from '~/releases/util'; +import allReleasesQuery from '../graphql/queries/all_releases.query.graphql'; import ReleaseBlock from './release_block.vue'; import ReleaseSkeletonLoader from './release_skeleton_loader.vue'; +import ReleasesEmptyState from './releases_empty_state.vue'; import ReleasesPagination from './releases_pagination.vue'; import ReleasesSort from './releases_sort.vue'; export default { - name: 'ReleasesApp', + name: 'ReleasesIndexApp', components: { - GlEmptyState, - GlLink, GlButton, ReleaseBlock, - ReleasesPagination, ReleaseSkeletonLoader, + ReleasesEmptyState, + ReleasesPagination, ReleasesSort, }, + inject: { + projectPath: { + default: '', + }, + newReleasePath: { + default: '', + }, + }, + apollo: { + /** + * The same query as `fullGraphqlResponse`, except that it limits its + * results to a single item. This causes this request to complete much more + * quickly than `fullGraphqlResponse`, which allows the page to show + * meaningful content to the user much earlier. + */ + singleGraphqlResponse: { + query: allReleasesQuery, + // This trick only works when paginating _forward_. + // When paginating backwards, limiting the query to a single item loads + // the _last_ item in the page, which is not useful for our purposes. + skip() { + return !this.includeSingleQuery; + }, + variables() { + return { + ...this.queryVariables, + first: 1, + }; + }, + update(data) { + return { data }; + }, + error() { + this.singleRequestError = true; + }, + }, + fullGraphqlResponse: { + query: allReleasesQuery, + variables() { + return this.queryVariables; + }, + update(data) { + return { data }; + }, + error(error) { + this.fullRequestError = true; + + createFlash({ + message: this.$options.i18n.errorMessage, + captureError: true, + error, + }); + }, + }, + }, + data() { + return { + singleRequestError: false, + fullRequestError: false, + cursors: { + before: getParameterByName('before'), + after: getParameterByName('after'), + }, + sort: DEFAULT_SORT, + }; + }, computed: { - ...mapState('index', [ - 'documentationPath', - 'illustrationPath', - 'newReleasePath', - 'isLoading', - 'releases', - 'hasError', - ]), - shouldRenderEmptyState() { - return !this.releases.length && !this.hasError && !this.isLoading; + queryVariables() { + let paginationParams = { first: PAGE_SIZE }; + if (this.cursors.after) { + paginationParams = { + after: this.cursors.after, + first: PAGE_SIZE, + }; + } else if (this.cursors.before) { + paginationParams = { + before: this.cursors.before, + last: PAGE_SIZE, + }; + } + + return { + fullPath: this.projectPath, + ...paginationParams, + sort: this.sort, + }; + }, + /** + * @returns {Boolean} Whether or not to request/include + * the results of the single-item query + */ + includeSingleQuery() { + return Boolean(!this.cursors.before || this.cursors.after); + }, + isSingleRequestLoading() { + return this.$apollo.queries.singleGraphqlResponse.loading; }, - shouldRenderSuccessState() { - return this.releases.length && !this.isLoading && !this.hasError; + isFullRequestLoading() { + return this.$apollo.queries.fullGraphqlResponse.loading; + }, + /** + * @returns {Boolean} `true` if the `singleGraphqlResponse` + * query has finished loading without errors + */ + isSingleRequestLoaded() { + return Boolean(!this.isSingleRequestLoading && this.singleGraphqlResponse?.data.project); + }, + /** + * @returns {Boolean} `true` if the `fullGraphqlResponse` + * query has finished loading without errors + */ + isFullRequestLoaded() { + return Boolean(!this.isFullRequestLoading && this.fullGraphqlResponse?.data.project); + }, + releases() { + if (this.isFullRequestLoaded) { + return convertAllReleasesGraphQLResponse(this.fullGraphqlResponse).data; + } + + if (this.isSingleRequestLoaded && this.includeSingleQuery) { + return convertAllReleasesGraphQLResponse(this.singleGraphqlResponse).data; + } + + return []; }, - emptyStateText() { - return __( - "Releases are based on Git tags and mark specific points in a project's development history. They can contain information about the type of changes and can also deliver binaries, like compiled versions of your software.", + pageInfo() { + if (!this.isFullRequestLoaded) { + return { + hasPreviousPage: false, + hasNextPage: false, + }; + } + + return this.fullGraphqlResponse.data.project.releases.pageInfo; + }, + shouldRenderEmptyState() { + return this.isFullRequestLoaded && this.releases.length === 0; + }, + shouldRenderLoadingIndicator() { + return ( + (this.isSingleRequestLoading && !this.singleRequestError && !this.isFullRequestLoaded) || + (this.isFullRequestLoading && !this.fullRequestError) ); }, + shouldRenderPagination() { + return this.isFullRequestLoaded && !this.shouldRenderEmptyState; + }, }, created() { - this.fetchReleases(); + this.updateQueryParamsFromUrl(); - window.addEventListener('popstate', this.fetchReleases); + window.addEventListener('popstate', this.updateQueryParamsFromUrl); + }, + destroyed() { + window.removeEventListener('popstate', this.updateQueryParamsFromUrl); }, methods: { - ...mapActions('index', { - fetchReleasesStoreAction: 'fetchReleases', - }), - fetchReleases() { - this.fetchReleasesStoreAction({ - before: getParameterByName('before'), - after: getParameterByName('after'), - }); + getReleaseKey(release, index) { + return [release.tagName, release.name, index].join('|'); + }, + updateQueryParamsFromUrl() { + this.cursors.before = getParameterByName('before'); + this.cursors.after = getParameterByName('after'); + }, + onPaginationButtonPress() { + this.updateQueryParamsFromUrl(); + + // In some cases, Apollo Client is able to pull its results from the cache instead of making + // a new network request. In these cases, the page's content gets swapped out immediately without + // changing the page's scroll, leaving the user looking at the bottom of the new page. + // To make the experience consistent, regardless of how the data is sourced, we manually + // scroll to the top of the page every time a pagination button is pressed. + scrollUp(); + }, + onSortChanged(newSort) { + if (this.sort === newSort) { + return; + } + + // Remove the "before" and "after" query parameters from the URL, + // effectively placing the user back on page 1 of the results. + // This prevents the frontend from requesting the results sorted + // by one field (e.g. `released_at`) while using a pagination cursor + // intended for a different field (e.g.) `created_at`). + // For more details, see the MR that introduced this change: + // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63434 + historyPushState( + setUrlParams({ + before: null, + after: null, + }), + ); + + this.updateQueryParamsFromUrl(); + + this.sort = newSort; }, }, + i18n: { + newRelease: __('New release'), + errorMessage: __('An error occurred while fetching the releases. Please try again.'), + }, }; </script> <template> <div class="flex flex-column mt-2"> <div class="gl-align-self-end gl-mb-3"> - <releases-sort class="gl-mr-2" @sort:changed="fetchReleases" /> + <releases-sort :value="sort" class="gl-mr-2" @input="onSortChanged" /> <gl-button v-if="newReleasePath" @@ -69,44 +239,27 @@ export default { :aria-describedby="shouldRenderEmptyState && 'releases-description'" category="primary" variant="confirm" - data-testid="new-release-button" + >{{ $options.i18n.newRelease }}</gl-button > - {{ __('New release') }} - </gl-button> </div> - <release-skeleton-loader v-if="isLoading" /> - - <gl-empty-state - v-else-if="shouldRenderEmptyState" - data-testid="empty-state" - :title="__('Getting started with releases')" - :svg-path="illustrationPath" - > - <template #description> - <span id="releases-description"> - {{ emptyStateText }} - <gl-link - :href="documentationPath" - :aria-label="__('Releases documentation')" - target="_blank" - > - {{ __('More information') }} - </gl-link> - </span> - </template> - </gl-empty-state> - - <div v-else-if="shouldRenderSuccessState" data-testid="success-state"> - <release-block - v-for="(release, index) in releases" - :key="index" - :release="release" - :class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }" - /> - </div> + <releases-empty-state v-if="shouldRenderEmptyState" /> + + <release-block + v-for="(release, index) in releases" + :key="getReleaseKey(release, index)" + :release="release" + :class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }" + /> + + <release-skeleton-loader v-if="shouldRenderLoadingIndicator" /> - <releases-pagination v-if="!isLoading" /> + <releases-pagination + v-if="shouldRenderPagination" + :page-info="pageInfo" + @prev="onPaginationButtonPress" + @next="onPaginationButtonPress" + /> </div> </template> <style> diff --git a/app/assets/javascripts/releases/components/app_index_apollo_client.vue b/app/assets/javascripts/releases/components/app_index_apollo_client.vue deleted file mode 100644 index f49c44a399f..00000000000 --- a/app/assets/javascripts/releases/components/app_index_apollo_client.vue +++ /dev/null @@ -1,275 +0,0 @@ -<script> -import { GlButton } from '@gitlab/ui'; -import allReleasesQuery from 'shared_queries/releases/all_releases.query.graphql'; -import createFlash from '~/flash'; -import { historyPushState } from '~/lib/utils/common_utils'; -import { scrollUp } from '~/lib/utils/scroll_utils'; -import { setUrlParams, getParameterByName } from '~/lib/utils/url_utility'; -import { __ } from '~/locale'; -import { PAGE_SIZE, DEFAULT_SORT } from '~/releases/constants'; -import { convertAllReleasesGraphQLResponse } from '~/releases/util'; -import ReleaseBlock from './release_block.vue'; -import ReleaseSkeletonLoader from './release_skeleton_loader.vue'; -import ReleasesEmptyState from './releases_empty_state.vue'; -import ReleasesPaginationApolloClient from './releases_pagination_apollo_client.vue'; -import ReleasesSortApolloClient from './releases_sort_apollo_client.vue'; - -export default { - name: 'ReleasesIndexApolloClientApp', - components: { - GlButton, - ReleaseBlock, - ReleaseSkeletonLoader, - ReleasesEmptyState, - ReleasesPaginationApolloClient, - ReleasesSortApolloClient, - }, - inject: { - projectPath: { - default: '', - }, - newReleasePath: { - default: '', - }, - }, - apollo: { - /** - * The same query as `fullGraphqlResponse`, except that it limits its - * results to a single item. This causes this request to complete much more - * quickly than `fullGraphqlResponse`, which allows the page to show - * meaningful content to the user much earlier. - */ - singleGraphqlResponse: { - query: allReleasesQuery, - // This trick only works when paginating _forward_. - // When paginating backwards, limiting the query to a single item loads - // the _last_ item in the page, which is not useful for our purposes. - skip() { - return !this.includeSingleQuery; - }, - variables() { - return { - ...this.queryVariables, - first: 1, - }; - }, - update(data) { - return { data }; - }, - error() { - this.singleRequestError = true; - }, - }, - fullGraphqlResponse: { - query: allReleasesQuery, - variables() { - return this.queryVariables; - }, - update(data) { - return { data }; - }, - error(error) { - this.fullRequestError = true; - - createFlash({ - message: this.$options.i18n.errorMessage, - captureError: true, - error, - }); - }, - }, - }, - data() { - return { - singleRequestError: false, - fullRequestError: false, - cursors: { - before: getParameterByName('before'), - after: getParameterByName('after'), - }, - sort: DEFAULT_SORT, - }; - }, - computed: { - queryVariables() { - let paginationParams = { first: PAGE_SIZE }; - if (this.cursors.after) { - paginationParams = { - after: this.cursors.after, - first: PAGE_SIZE, - }; - } else if (this.cursors.before) { - paginationParams = { - before: this.cursors.before, - last: PAGE_SIZE, - }; - } - - return { - fullPath: this.projectPath, - ...paginationParams, - sort: this.sort, - }; - }, - /** - * @returns {Boolean} Whether or not to request/include - * the results of the single-item query - */ - includeSingleQuery() { - return Boolean(!this.cursors.before || this.cursors.after); - }, - isSingleRequestLoading() { - return this.$apollo.queries.singleGraphqlResponse.loading; - }, - isFullRequestLoading() { - return this.$apollo.queries.fullGraphqlResponse.loading; - }, - /** - * @returns {Boolean} `true` if the `singleGraphqlResponse` - * query has finished loading without errors - */ - isSingleRequestLoaded() { - return Boolean(!this.isSingleRequestLoading && this.singleGraphqlResponse?.data.project); - }, - /** - * @returns {Boolean} `true` if the `fullGraphqlResponse` - * query has finished loading without errors - */ - isFullRequestLoaded() { - return Boolean(!this.isFullRequestLoading && this.fullGraphqlResponse?.data.project); - }, - releases() { - if (this.isFullRequestLoaded) { - return convertAllReleasesGraphQLResponse(this.fullGraphqlResponse).data; - } - - if (this.isSingleRequestLoaded && this.includeSingleQuery) { - return convertAllReleasesGraphQLResponse(this.singleGraphqlResponse).data; - } - - return []; - }, - pageInfo() { - if (!this.isFullRequestLoaded) { - return { - hasPreviousPage: false, - hasNextPage: false, - }; - } - - return this.fullGraphqlResponse.data.project.releases.pageInfo; - }, - shouldRenderEmptyState() { - return this.isFullRequestLoaded && this.releases.length === 0; - }, - shouldRenderLoadingIndicator() { - return ( - (this.isSingleRequestLoading && !this.singleRequestError && !this.isFullRequestLoaded) || - (this.isFullRequestLoading && !this.fullRequestError) - ); - }, - shouldRenderPagination() { - return this.isFullRequestLoaded && !this.shouldRenderEmptyState; - }, - }, - created() { - this.updateQueryParamsFromUrl(); - - window.addEventListener('popstate', this.updateQueryParamsFromUrl); - }, - destroyed() { - window.removeEventListener('popstate', this.updateQueryParamsFromUrl); - }, - methods: { - getReleaseKey(release, index) { - return [release.tagName, release.name, index].join('|'); - }, - updateQueryParamsFromUrl() { - this.cursors.before = getParameterByName('before'); - this.cursors.after = getParameterByName('after'); - }, - onPaginationButtonPress() { - this.updateQueryParamsFromUrl(); - - // In some cases, Apollo Client is able to pull its results from the cache instead of making - // a new network request. In these cases, the page's content gets swapped out immediately without - // changing the page's scroll, leaving the user looking at the bottom of the new page. - // To make the experience consistent, regardless of how the data is sourced, we manually - // scroll to the top of the page every time a pagination button is pressed. - scrollUp(); - }, - onSortChanged(newSort) { - if (this.sort === newSort) { - return; - } - - // Remove the "before" and "after" query parameters from the URL, - // effectively placing the user back on page 1 of the results. - // This prevents the frontend from requesting the results sorted - // by one field (e.g. `released_at`) while using a pagination cursor - // intended for a different field (e.g.) `created_at`). - // For more details, see the MR that introduced this change: - // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63434 - historyPushState( - setUrlParams({ - before: null, - after: null, - }), - ); - - this.updateQueryParamsFromUrl(); - - this.sort = newSort; - }, - }, - i18n: { - newRelease: __('New release'), - errorMessage: __('An error occurred while fetching the releases. Please try again.'), - }, -}; -</script> -<template> - <div class="flex flex-column mt-2"> - <div class="gl-align-self-end gl-mb-3"> - <releases-sort-apollo-client :value="sort" class="gl-mr-2" @input="onSortChanged" /> - - <gl-button - v-if="newReleasePath" - :href="newReleasePath" - :aria-describedby="shouldRenderEmptyState && 'releases-description'" - category="primary" - variant="success" - >{{ $options.i18n.newRelease }}</gl-button - > - </div> - - <releases-empty-state v-if="shouldRenderEmptyState" /> - - <release-block - v-for="(release, index) in releases" - :key="getReleaseKey(release, index)" - :release="release" - :class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }" - /> - - <release-skeleton-loader v-if="shouldRenderLoadingIndicator" /> - - <releases-pagination-apollo-client - v-if="shouldRenderPagination" - :page-info="pageInfo" - @prev="onPaginationButtonPress" - @next="onPaginationButtonPress" - /> - </div> -</template> -<style> -.linked-card::after { - width: 1px; - content: ' '; - border: 1px solid #e5e5e5; - height: 17px; - top: 100%; - position: absolute; - left: 32px; -} -</style> diff --git a/app/assets/javascripts/releases/components/release_block_footer.vue b/app/assets/javascripts/releases/components/release_block_footer.vue index cb795b3cba7..91d6d0911a4 100644 --- a/app/assets/javascripts/releases/components/release_block_footer.vue +++ b/app/assets/javascripts/releases/components/release_block_footer.vue @@ -104,9 +104,11 @@ export default { <div v-if="author" class="d-flex"> <span class="text-secondary">{{ __('by') }} </span> <user-avatar-link + class="gl-my-n1" :link-href="author.webUrl" :img-src="author.avatarUrl" :img-alt="userImageAltDescription" + :img-size="24" :tooltip-text="author.username" tooltip-placement="bottom" /> diff --git a/app/assets/javascripts/releases/components/releases_pagination.vue b/app/assets/javascripts/releases/components/releases_pagination.vue index fddf85ead1e..52ad991d61a 100644 --- a/app/assets/javascripts/releases/components/releases_pagination.vue +++ b/app/assets/javascripts/releases/components/releases_pagination.vue @@ -1,26 +1,24 @@ <script> import { GlKeysetPagination } from '@gitlab/ui'; -import { mapActions, mapState } from 'vuex'; +import { isBoolean } from 'lodash'; import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; export default { - name: 'ReleasesPaginationGraphql', + name: 'ReleasesPagination', components: { GlKeysetPagination }, - computed: { - ...mapState('index', ['pageInfo']), - showPagination() { - return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage; + props: { + pageInfo: { + type: Object, + required: true, + validator: (info) => isBoolean(info.hasPreviousPage) && isBoolean(info.hasNextPage), }, }, methods: { - ...mapActions('index', ['fetchReleases']), onPrev(before) { historyPushState(buildUrlWithCurrentLocation(`?before=${before}`)); - this.fetchReleases({ before }); }, onNext(after) { historyPushState(buildUrlWithCurrentLocation(`?after=${after}`)); - this.fetchReleases({ after }); }, }, }; @@ -28,8 +26,10 @@ export default { <template> <div class="gl-display-flex gl-justify-content-center"> <gl-keyset-pagination - v-if="showPagination" v-bind="pageInfo" + :prev-text="__('Prev')" + :next-text="__('Next')" + v-on="$listeners" @prev="onPrev($event)" @next="onNext($event)" /> diff --git a/app/assets/javascripts/releases/components/releases_pagination_apollo_client.vue b/app/assets/javascripts/releases/components/releases_pagination_apollo_client.vue deleted file mode 100644 index 73339677a4b..00000000000 --- a/app/assets/javascripts/releases/components/releases_pagination_apollo_client.vue +++ /dev/null @@ -1,37 +0,0 @@ -<script> -import { GlKeysetPagination } from '@gitlab/ui'; -import { isBoolean } from 'lodash'; -import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; - -export default { - name: 'ReleasesPaginationApolloClient', - components: { GlKeysetPagination }, - props: { - pageInfo: { - type: Object, - required: true, - validator: (info) => isBoolean(info.hasPreviousPage) && isBoolean(info.hasNextPage), - }, - }, - methods: { - onPrev(before) { - historyPushState(buildUrlWithCurrentLocation(`?before=${before}`)); - }, - onNext(after) { - historyPushState(buildUrlWithCurrentLocation(`?after=${after}`)); - }, - }, -}; -</script> -<template> - <div class="gl-display-flex gl-justify-content-center"> - <gl-keyset-pagination - v-bind="pageInfo" - :prev-text="__('Prev')" - :next-text="__('Next')" - v-on="$listeners" - @prev="onPrev($event)" - @next="onNext($event)" - /> - </div> -</template> diff --git a/app/assets/javascripts/releases/components/releases_sort.vue b/app/assets/javascripts/releases/components/releases_sort.vue index d4210dad19c..0f14b579da0 100644 --- a/app/assets/javascripts/releases/components/releases_sort.vue +++ b/app/assets/javascripts/releases/components/releases_sort.vue @@ -1,7 +1,17 @@ <script> import { GlSorting, GlSortingItem } from '@gitlab/ui'; -import { mapState, mapActions } from 'vuex'; -import { ASCENDING_ORDER, DESCENDING_ORDER, SORT_OPTIONS } from '../constants'; +import { + ASCENDING_ORDER, + DESCENDING_ORDER, + SORT_OPTIONS, + RELEASED_AT, + CREATED_AT, + RELEASED_AT_ASC, + RELEASED_AT_DESC, + CREATED_ASC, + ALL_SORTS, + SORT_MAP, +} from '../constants'; export default { name: 'ReleasesSort', @@ -9,35 +19,54 @@ export default { GlSorting, GlSortingItem, }, + props: { + value: { + type: String, + required: true, + validator: (sort) => ALL_SORTS.includes(sort), + }, + }, computed: { - ...mapState('index', { - orderBy: (state) => state.sorting.orderBy, - sort: (state) => state.sorting.sort, - }), + orderBy() { + if (this.value === RELEASED_AT_ASC || this.value === RELEASED_AT_DESC) { + return RELEASED_AT; + } + + return CREATED_AT; + }, + direction() { + if (this.value === RELEASED_AT_ASC || this.value === CREATED_ASC) { + return ASCENDING_ORDER; + } + + return DESCENDING_ORDER; + }, sortOptions() { return SORT_OPTIONS; }, sortText() { - const option = this.sortOptions.find((s) => s.orderBy === this.orderBy); - return option.label; + return this.sortOptions.find((s) => s.orderBy === this.orderBy).label; }, - isSortAscending() { - return this.sort === ASCENDING_ORDER; + isDirectionAscending() { + return this.direction === ASCENDING_ORDER; }, }, methods: { - ...mapActions('index', ['setSorting']), onDirectionChange() { - const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ORDER; - this.setSorting({ sort }); - this.$emit('sort:changed'); + const direction = this.isDirectionAscending ? DESCENDING_ORDER : ASCENDING_ORDER; + this.emitInputEventIfChanged(this.orderBy, direction); }, onSortItemClick(item) { - this.setSorting({ orderBy: item }); - this.$emit('sort:changed'); + this.emitInputEventIfChanged(item.orderBy, this.direction); }, isActiveSortItem(item) { - return this.orderBy === item; + return this.orderBy === item.orderBy; + }, + emitInputEventIfChanged(orderBy, direction) { + const newSort = SORT_MAP[orderBy][direction]; + if (newSort !== this.value) { + this.$emit('input', SORT_MAP[orderBy][direction]); + } }, }, }; @@ -46,15 +75,15 @@ export default { <template> <gl-sorting :text="sortText" - :is-ascending="isSortAscending" + :is-ascending="isDirectionAscending" data-testid="releases-sort" @sortDirectionChange="onDirectionChange" > <gl-sorting-item - v-for="item in sortOptions" + v-for="item of sortOptions" :key="item.orderBy" - :active="isActiveSortItem(item.orderBy)" - @click="onSortItemClick(item.orderBy)" + :active="isActiveSortItem(item)" + @click="onSortItemClick(item)" > {{ item.label }} </gl-sorting-item> diff --git a/app/assets/javascripts/releases/components/releases_sort_apollo_client.vue b/app/assets/javascripts/releases/components/releases_sort_apollo_client.vue deleted file mode 100644 index 7257b34bbf6..00000000000 --- a/app/assets/javascripts/releases/components/releases_sort_apollo_client.vue +++ /dev/null @@ -1,91 +0,0 @@ -<script> -import { GlSorting, GlSortingItem } from '@gitlab/ui'; -import { - ASCENDING_ORDER, - DESCENDING_ORDER, - SORT_OPTIONS, - RELEASED_AT, - CREATED_AT, - RELEASED_AT_ASC, - RELEASED_AT_DESC, - CREATED_ASC, - ALL_SORTS, - SORT_MAP, -} from '../constants'; - -export default { - name: 'ReleasesSortApolloclient', - components: { - GlSorting, - GlSortingItem, - }, - props: { - value: { - type: String, - required: true, - validator: (sort) => ALL_SORTS.includes(sort), - }, - }, - computed: { - orderBy() { - if (this.value === RELEASED_AT_ASC || this.value === RELEASED_AT_DESC) { - return RELEASED_AT; - } - - return CREATED_AT; - }, - direction() { - if (this.value === RELEASED_AT_ASC || this.value === CREATED_ASC) { - return ASCENDING_ORDER; - } - - return DESCENDING_ORDER; - }, - sortOptions() { - return SORT_OPTIONS; - }, - sortText() { - return this.sortOptions.find((s) => s.orderBy === this.orderBy).label; - }, - isDirectionAscending() { - return this.direction === ASCENDING_ORDER; - }, - }, - methods: { - onDirectionChange() { - const direction = this.isDirectionAscending ? DESCENDING_ORDER : ASCENDING_ORDER; - this.emitInputEventIfChanged(this.orderBy, direction); - }, - onSortItemClick(item) { - this.emitInputEventIfChanged(item.orderBy, this.direction); - }, - isActiveSortItem(item) { - return this.orderBy === item.orderBy; - }, - emitInputEventIfChanged(orderBy, direction) { - const newSort = SORT_MAP[orderBy][direction]; - if (newSort !== this.value) { - this.$emit('input', SORT_MAP[orderBy][direction]); - } - }, - }, -}; -</script> - -<template> - <gl-sorting - :text="sortText" - :is-ascending="isDirectionAscending" - data-testid="releases-sort" - @sortDirectionChange="onDirectionChange" - > - <gl-sorting-item - v-for="item of sortOptions" - :key="item.orderBy" - :active="isActiveSortItem(item)" - @click="onSortItemClick(item)" - > - {{ item.label }} - </gl-sorting-item> - </gl-sorting> -</template> diff --git a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql index 7f67f7d11a3..bda7ac52a47 100644 --- a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql +++ b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql @@ -1,12 +1,4 @@ -#import "../fragments/release.fragment.graphql" - -# This query is identical to -# `app/graphql/queries/releases/all_releases.query.graphql`. -# These two queries should be kept in sync. -# When the `releases_index_apollo_client` feature flag is -# removed, this query should be removed entirely. - -query allReleasesDeprecated( +query allReleases( $fullPath: ID! $first: Int $last: Int @@ -20,7 +12,87 @@ query allReleasesDeprecated( releases(first: $first, last: $last, before: $before, after: $after, sort: $sort) { __typename nodes { - ...Release + __typename + name + tagName + tagPath + descriptionHtml + releasedAt + createdAt + upcomingRelease + assets { + __typename + count + sources { + __typename + nodes { + __typename + format + url + } + } + links { + __typename + nodes { + __typename + id + name + url + directAssetUrl + linkType + external + } + } + } + evidences { + __typename + nodes { + __typename + id + filepath + collectedAt + sha + } + } + links { + __typename + editUrl + selfUrl + openedIssuesUrl + closedIssuesUrl + openedMergeRequestsUrl + mergedMergeRequestsUrl + closedMergeRequestsUrl + } + commit { + __typename + id + sha + webUrl + title + } + author { + __typename + id + webUrl + avatarUrl + username + } + milestones { + __typename + nodes { + __typename + id + title + description + webPath + stats { + __typename + totalIssuesCount + closedIssuesCount + } + } + } } pageInfo { __typename diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js index 86fa72d1496..afb8ab461cd 100644 --- a/app/assets/javascripts/releases/mount_index.js +++ b/app/assets/javascripts/releases/mount_index.js @@ -1,50 +1,32 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import Vuex from 'vuex'; import createDefaultClient from '~/lib/graphql'; import ReleaseIndexApp from './components/app_index.vue'; -import ReleaseIndexApollopClientApp from './components/app_index_apollo_client.vue'; -import createStore from './stores'; -import createIndexModule from './stores/modules/index'; export default () => { const el = document.getElementById('js-releases-page'); - if (window.gon?.features?.releasesIndexApolloClient) { - Vue.use(VueApollo); + Vue.use(VueApollo); - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient( - {}, - { - // This page attempts to decrease the perceived loading time - // by sending two requests: one request for the first item only (which - // completes relatively quickly), and one for all the items (which is slower). - // By default, Apollo Client batches these requests together, which defeats - // the purpose of making separate requests. So we explicitly - // disable batching on this page. - batchMax: 1, - }, - ), - }); - - return new Vue({ - el, - apolloProvider, - provide: { ...el.dataset }, - render: (h) => h(ReleaseIndexApollopClientApp), - }); - } - - Vue.use(Vuex); + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient( + {}, + { + // This page attempts to decrease the perceived loading time + // by sending two requests: one request for the first item only (which + // completes relatively quickly), and one for all the items (which is slower). + // By default, Apollo Client batches these requests together, which defeats + // the purpose of making separate requests. So we explicitly + // disable batching on this page. + batchMax: 1, + }, + ), + }); return new Vue({ el, - store: createStore({ - modules: { - index: createIndexModule(el.dataset), - }, - }), + apolloProvider, + provide: { ...el.dataset }, render: (h) => h(ReleaseIndexApp), }); }; diff --git a/app/assets/javascripts/releases/stores/modules/index/actions.js b/app/assets/javascripts/releases/stores/modules/index/actions.js deleted file mode 100644 index d3bb11cab30..00000000000 --- a/app/assets/javascripts/releases/stores/modules/index/actions.js +++ /dev/null @@ -1,65 +0,0 @@ -import createFlash from '~/flash'; -import { __ } from '~/locale'; -import { PAGE_SIZE } from '~/releases/constants'; -import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql'; -import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util'; -import * as types from './mutation_types'; - -/** - * Gets a paginated list of releases from the GraphQL endpoint - * - * @param {Object} vuexParams - * @param {Object} actionParams - * @param {String} [actionParams.before] A GraphQL cursor. If provided, - * the items returned will proceed the provided cursor. - * @param {String} [actionParams.after] A GraphQL cursor. If provided, - * the items returned will follow the provided cursor. - */ -export const fetchReleases = ({ dispatch, commit, state }, { before, after }) => { - commit(types.REQUEST_RELEASES); - - const { sort, orderBy } = state.sorting; - const orderByParam = orderBy === 'created_at' ? 'created' : orderBy; - const sortParams = `${orderByParam}_${sort}`.toUpperCase(); - - let paginationParams; - if (!before && !after) { - paginationParams = { first: PAGE_SIZE }; - } else if (before && !after) { - paginationParams = { last: PAGE_SIZE, before }; - } else if (!before && after) { - paginationParams = { first: PAGE_SIZE, after }; - } else { - throw new Error( - 'Both a `before` and an `after` parameter were provided to fetchReleases. These parameters cannot be used together.', - ); - } - - gqClient - .query({ - query: allReleasesQuery, - variables: { - fullPath: state.projectPath, - sort: sortParams, - ...paginationParams, - }, - }) - .then((response) => { - const { data, paginationInfo: pageInfo } = convertAllReleasesGraphQLResponse(response); - - commit(types.RECEIVE_RELEASES_SUCCESS, { - data, - pageInfo, - }); - }) - .catch(() => dispatch('receiveReleasesError')); -}; - -export const receiveReleasesError = ({ commit }) => { - commit(types.RECEIVE_RELEASES_ERROR); - createFlash({ - message: __('An error occurred while fetching the releases. Please try again.'), - }); -}; - -export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data); diff --git a/app/assets/javascripts/releases/stores/modules/index/index.js b/app/assets/javascripts/releases/stores/modules/index/index.js deleted file mode 100644 index d5ca191153a..00000000000 --- a/app/assets/javascripts/releases/stores/modules/index/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as actions from './actions'; -import mutations from './mutations'; -import createState from './state'; - -export default (initialState) => ({ - namespaced: true, - actions, - mutations, - state: createState(initialState), -}); diff --git a/app/assets/javascripts/releases/stores/modules/index/mutation_types.js b/app/assets/javascripts/releases/stores/modules/index/mutation_types.js deleted file mode 100644 index 669168efb88..00000000000 --- a/app/assets/javascripts/releases/stores/modules/index/mutation_types.js +++ /dev/null @@ -1,4 +0,0 @@ -export const REQUEST_RELEASES = 'REQUEST_RELEASES'; -export const RECEIVE_RELEASES_SUCCESS = 'RECEIVE_RELEASES_SUCCESS'; -export const RECEIVE_RELEASES_ERROR = 'RECEIVE_RELEASES_ERROR'; -export const SET_SORTING = 'SET_SORTING'; diff --git a/app/assets/javascripts/releases/stores/modules/index/mutations.js b/app/assets/javascripts/releases/stores/modules/index/mutations.js deleted file mode 100644 index 55a8a488be8..00000000000 --- a/app/assets/javascripts/releases/stores/modules/index/mutations.js +++ /dev/null @@ -1,44 +0,0 @@ -import * as types from './mutation_types'; - -export default { - /** - * Sets isLoading to true while the request is being made. - * @param {Object} state - */ - [types.REQUEST_RELEASES](state) { - state.isLoading = true; - }, - - /** - * Sets isLoading to false. - * Sets hasError to false. - * Sets the received data - * Sets the received pagination information - * @param {Object} state - * @param {Object} resp - */ - [types.RECEIVE_RELEASES_SUCCESS](state, { data, pageInfo }) { - state.hasError = false; - state.isLoading = false; - state.releases = data; - state.pageInfo = pageInfo; - }, - - /** - * Sets isLoading to false. - * Sets hasError to true. - * Resets the data - * @param {Object} state - * @param {Object} data - */ - [types.RECEIVE_RELEASES_ERROR](state) { - state.isLoading = false; - state.releases = []; - state.hasError = true; - state.pageInfo = {}; - }, - - [types.SET_SORTING](state, sorting) { - state.sorting = { ...state.sorting, ...sorting }; - }, -}; diff --git a/app/assets/javascripts/releases/stores/modules/index/state.js b/app/assets/javascripts/releases/stores/modules/index/state.js deleted file mode 100644 index 5e1aaab7b58..00000000000 --- a/app/assets/javascripts/releases/stores/modules/index/state.js +++ /dev/null @@ -1,24 +0,0 @@ -import { DESCENDING_ORDER, RELEASED_AT } from '../../../constants'; - -export default ({ - projectId, - projectPath, - documentationPath, - illustrationPath, - newReleasePath = '', -}) => ({ - projectId, - projectPath, - documentationPath, - illustrationPath, - newReleasePath, - - isLoading: false, - hasError: false, - releases: [], - pageInfo: {}, - sorting: { - sort: DESCENDING_ORDER, - orderBy: RELEASED_AT, - }, -}); |