diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
commit | 4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch) | |
tree | 5423a1c7516cffe36384133ade12572cf709398d /app/assets/javascripts/releases | |
parent | e570267f2f6b326480d284e0164a6464ba4081bc (diff) | |
download | gitlab-ce-4555e1b21c365ed8303ffb7a3325d773c9b8bf31.tar.gz |
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'app/assets/javascripts/releases')
24 files changed, 327 insertions, 329 deletions
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue index 262b5614d65..31d335fa15d 100644 --- a/app/assets/javascripts/releases/components/app_index.vue +++ b/app/assets/javascripts/releases/components/app_index.vue @@ -51,12 +51,8 @@ export default { }), fetchReleases() { this.fetchReleasesStoreAction({ - // these two parameters are only used in "GraphQL mode" before: getParameterByName('before'), after: getParameterByName('after'), - - // this parameter is only used when in "REST mode" - page: getParameterByName('page'), }); }, }, @@ -73,17 +69,17 @@ export default { :aria-describedby="shouldRenderEmptyState && 'releases-description'" category="primary" variant="success" - class="js-new-release-btn" + data-testid="new-release-button" > {{ __('New release') }} </gl-button> </div> - <release-skeleton-loader v-if="isLoading" class="js-loading" /> + <release-skeleton-loader v-if="isLoading" /> <gl-empty-state v-else-if="shouldRenderEmptyState" - class="js-empty-state" + data-testid="empty-state" :title="__('Getting started with releases')" :svg-path="illustrationPath" > @@ -101,7 +97,7 @@ export default { </template> </gl-empty-state> - <div v-else-if="shouldRenderSuccessState" class="js-success-state"> + <div v-else-if="shouldRenderSuccessState" data-testid="success-state"> <release-block v-for="(release, index) in releases" :key="index" diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue index c38e93d420b..fdb0f99b735 100644 --- a/app/assets/javascripts/releases/components/app_show.vue +++ b/app/assets/javascripts/releases/components/app_show.vue @@ -1,7 +1,7 @@ <script> import createFlash from '~/flash'; import { s__ } from '~/locale'; -import oneReleaseQuery from '../queries/one_release.query.graphql'; +import oneReleaseQuery from '../graphql/queries/one_release.query.graphql'; import { convertGraphQLRelease } from '../util'; import ReleaseBlock from './release_block.vue'; import ReleaseSkeletonLoader from './release_skeleton_loader.vue'; diff --git a/app/assets/javascripts/releases/components/releases_pagination.vue b/app/assets/javascripts/releases/components/releases_pagination.vue index 062c72b445b..fddf85ead1e 100644 --- a/app/assets/javascripts/releases/components/releases_pagination.vue +++ b/app/assets/javascripts/releases/components/releases_pagination.vue @@ -1,20 +1,37 @@ <script> -import { mapGetters } from 'vuex'; -import ReleasesPaginationGraphql from './releases_pagination_graphql.vue'; -import ReleasesPaginationRest from './releases_pagination_rest.vue'; +import { GlKeysetPagination } from '@gitlab/ui'; +import { mapActions, mapState } from 'vuex'; +import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; export default { - name: 'ReleasesPagination', - components: { ReleasesPaginationGraphql, ReleasesPaginationRest }, + name: 'ReleasesPaginationGraphql', + components: { GlKeysetPagination }, computed: { - ...mapGetters(['useGraphQLEndpoint']), + ...mapState('index', ['pageInfo']), + showPagination() { + return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage; + }, + }, + methods: { + ...mapActions('index', ['fetchReleases']), + onPrev(before) { + historyPushState(buildUrlWithCurrentLocation(`?before=${before}`)); + this.fetchReleases({ before }); + }, + onNext(after) { + historyPushState(buildUrlWithCurrentLocation(`?after=${after}`)); + this.fetchReleases({ after }); + }, }, }; </script> - <template> <div class="gl-display-flex gl-justify-content-center"> - <releases-pagination-graphql v-if="useGraphQLEndpoint" /> - <releases-pagination-rest v-else /> + <gl-keyset-pagination + v-if="showPagination" + v-bind="pageInfo" + @prev="onPrev($event)" + @next="onNext($event)" + /> </div> </template> diff --git a/app/assets/javascripts/releases/components/releases_pagination_graphql.vue b/app/assets/javascripts/releases/components/releases_pagination_graphql.vue deleted file mode 100644 index 13cbf95b9af..00000000000 --- a/app/assets/javascripts/releases/components/releases_pagination_graphql.vue +++ /dev/null @@ -1,35 +0,0 @@ -<script> -import { GlKeysetPagination } from '@gitlab/ui'; -import { mapActions, mapState } from 'vuex'; -import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; - -export default { - name: 'ReleasesPaginationGraphql', - components: { GlKeysetPagination }, - computed: { - ...mapState('index', ['graphQlPageInfo']), - showPagination() { - return this.graphQlPageInfo.hasPreviousPage || this.graphQlPageInfo.hasNextPage; - }, - }, - methods: { - ...mapActions('index', ['fetchReleases']), - onPrev(before) { - historyPushState(buildUrlWithCurrentLocation(`?before=${before}`)); - this.fetchReleases({ before }); - }, - onNext(after) { - historyPushState(buildUrlWithCurrentLocation(`?after=${after}`)); - this.fetchReleases({ after }); - }, - }, -}; -</script> -<template> - <gl-keyset-pagination - v-if="showPagination" - v-bind="graphQlPageInfo" - @prev="onPrev($event)" - @next="onNext($event)" - /> -</template> diff --git a/app/assets/javascripts/releases/components/releases_pagination_rest.vue b/app/assets/javascripts/releases/components/releases_pagination_rest.vue deleted file mode 100644 index 5e97a5a0450..00000000000 --- a/app/assets/javascripts/releases/components/releases_pagination_rest.vue +++ /dev/null @@ -1,24 +0,0 @@ -<script> -import { mapActions, mapState } from 'vuex'; -import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; -import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; - -export default { - name: 'ReleasesPaginationRest', - components: { TablePagination }, - computed: { - ...mapState('index', ['restPageInfo']), - }, - methods: { - ...mapActions('index', ['fetchReleases']), - onChangePage(page) { - historyPushState(buildUrlWithCurrentLocation(`?page=${page}`)); - this.fetchReleases({ page }); - }, - }, -}; -</script> - -<template> - <table-pagination :change="onChangePage" :page-info="restPageInfo" /> -</template> diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue index 9df646ca798..80f59485426 100644 --- a/app/assets/javascripts/releases/components/tag_field_new.vue +++ b/app/assets/javascripts/releases/components/tag_field_new.vue @@ -74,6 +74,21 @@ export default { // we need to show the "create from" input. this.showCreateFrom = true; }, + shouldShowCreateTagOption(isLoading, matches, query) { + // Show the "create tag" option if: + return ( + // we're not currently loading any results, and + !isLoading && + // the search query isn't just whitespace, and + query.trim() && + // the `matches` object is non-null, and + matches && + // the tag name doesn't already exist + !matches.tags.list.some( + (tagInfo) => tagInfo.name.toUpperCase() === query.toUpperCase().trim(), + ) + ); + }, }, translations: { tagName: { @@ -111,7 +126,7 @@ export default { > <template #footer="{ isLoading, matches, query }"> <gl-dropdown-item - v-if="!isLoading && matches && matches.tags.totalCount === 0" + v-if="shouldShowCreateTagOption(isLoading, matches, query)" is-check-item :is-checked="tagName === query" @click="createTagClicked(query)" diff --git a/app/assets/javascripts/releases/queries/release.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql index 3a742db7d9e..3a742db7d9e 100644 --- a/app/assets/javascripts/releases/queries/release.fragment.graphql +++ b/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql diff --git a/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql new file mode 100644 index 00000000000..47c5afefd78 --- /dev/null +++ b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql @@ -0,0 +1,23 @@ +fragment ReleaseForEditing on Release { + name + tagName + description + assets { + links { + nodes { + id + name + url + linkType + } + } + } + links { + selfUrl + } + milestones { + nodes { + title + } + } +} diff --git a/app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql b/app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql new file mode 100644 index 00000000000..56bfe7c23d6 --- /dev/null +++ b/app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql @@ -0,0 +1,10 @@ +mutation createRelease($input: ReleaseCreateInput!) { + releaseCreate(input: $input) { + release { + links { + selfUrl + } + } + errors + } +} diff --git a/app/assets/javascripts/releases/graphql/mutations/create_release_link.mutation.graphql b/app/assets/javascripts/releases/graphql/mutations/create_release_link.mutation.graphql new file mode 100644 index 00000000000..4bdfc79dbc4 --- /dev/null +++ b/app/assets/javascripts/releases/graphql/mutations/create_release_link.mutation.graphql @@ -0,0 +1,5 @@ +mutation createReleaseAssetLink($input: ReleaseAssetLinkCreateInput!) { + releaseAssetLinkCreate(input: $input) { + errors + } +} diff --git a/app/assets/javascripts/releases/graphql/mutations/delete_release_link.mutation.graphql b/app/assets/javascripts/releases/graphql/mutations/delete_release_link.mutation.graphql new file mode 100644 index 00000000000..a75eddcd288 --- /dev/null +++ b/app/assets/javascripts/releases/graphql/mutations/delete_release_link.mutation.graphql @@ -0,0 +1,5 @@ +mutation deleteReleaseAssetLink($input: ReleaseAssetLinkDeleteInput!) { + releaseAssetLinkDelete(input: $input) { + errors + } +} diff --git a/app/assets/javascripts/releases/graphql/mutations/update_release.mutation.graphql b/app/assets/javascripts/releases/graphql/mutations/update_release.mutation.graphql new file mode 100644 index 00000000000..9c6a861d2f1 --- /dev/null +++ b/app/assets/javascripts/releases/graphql/mutations/update_release.mutation.graphql @@ -0,0 +1,5 @@ +mutation updateRelease($input: ReleaseUpdateInput!) { + releaseUpdate(input: $input) { + errors + } +} diff --git a/app/assets/javascripts/releases/queries/all_releases.query.graphql b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql index a07dabb9fd6..10e4d883e62 100644 --- a/app/assets/javascripts/releases/queries/all_releases.query.graphql +++ b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql @@ -1,4 +1,4 @@ -#import "./release.fragment.graphql" +#import "../fragments/release.fragment.graphql" query allReleases( $fullPath: ID! diff --git a/app/assets/javascripts/releases/queries/one_release.query.graphql b/app/assets/javascripts/releases/graphql/queries/one_release.query.graphql index b893aea94b0..c80d6e753ab 100644 --- a/app/assets/javascripts/releases/queries/one_release.query.graphql +++ b/app/assets/javascripts/releases/graphql/queries/one_release.query.graphql @@ -1,4 +1,4 @@ -#import "./release.fragment.graphql" +#import "../fragments/release.fragment.graphql" query oneRelease($fullPath: ID!, $tagName: String!) { project(fullPath: $fullPath) { diff --git a/app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql b/app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql new file mode 100644 index 00000000000..767ba4aeca0 --- /dev/null +++ b/app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql @@ -0,0 +1,9 @@ +#import "../fragments/release_for_editing.fragment.graphql" + +query oneReleaseForEditing($fullPath: ID!, $tagName: String!) { + project(fullPath: $fullPath) { + release(tagName: $tagName) { + ...ReleaseForEditing + } + } +} diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js index 0b453467c13..bb21ec7c43f 100644 --- a/app/assets/javascripts/releases/mount_index.js +++ b/app/assets/javascripts/releases/mount_index.js @@ -15,11 +15,6 @@ export default () => { modules: { index: createIndexModule(el.dataset), }, - featureFlags: { - graphqlReleaseData: Boolean(gon.features?.graphqlReleaseData), - graphqlReleasesPage: Boolean(gon.features?.graphqlReleasesPage), - graphqlMilestoneStats: Boolean(gon.features?.graphqlMilestoneStats), - }, }), render: (h) => h(ReleaseIndexApp), }); diff --git a/app/assets/javascripts/releases/stores/getters.js b/app/assets/javascripts/releases/stores/getters.js deleted file mode 100644 index 2a06f398e26..00000000000 --- a/app/assets/javascripts/releases/stores/getters.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @returns {Boolean} `true` if all the feature flags - * required to enable the GraphQL endpoint are enabled - */ -export const useGraphQLEndpoint = (rootState) => { - return Boolean( - rootState.featureFlags.graphqlReleaseData && - rootState.featureFlags.graphqlReleasesPage && - rootState.featureFlags.graphqlMilestoneStats, - ); -}; diff --git a/app/assets/javascripts/releases/stores/index.js b/app/assets/javascripts/releases/stores/index.js index cc8b586964f..b2e93d789d7 100644 --- a/app/assets/javascripts/releases/stores/index.js +++ b/app/assets/javascripts/releases/stores/index.js @@ -1,9 +1,7 @@ import Vuex from 'vuex'; -import * as getters from './getters'; export default ({ modules, featureFlags }) => new Vuex.Store({ modules, state: { featureFlags }, - getters, }); diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js index 8dc2083dd2b..b312c2a7506 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js @@ -1,14 +1,12 @@ -import api from '~/api'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { redirectTo } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; -import oneReleaseQuery from '~/releases/queries/one_release.query.graphql'; -import { - releaseToApiJson, - apiJsonToRelease, - gqClient, - convertOneReleaseGraphQLResponse, -} from '~/releases/util'; +import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql'; +import createReleaseAssetLinkMutation from '~/releases/graphql/mutations/create_release_link.mutation.graphql'; +import deleteReleaseAssetLinkMutation from '~/releases/graphql/mutations/delete_release_link.mutation.graphql'; +import updateReleaseMutation from '~/releases/graphql/mutations/update_release.mutation.graphql'; +import oneReleaseForEditingQuery from '~/releases/graphql/queries/one_release_for_editing.query.graphql'; +import { gqClient, convertOneReleaseGraphQLResponse } from '~/releases/util'; import * as types from './mutation_types'; export const initializeRelease = ({ commit, dispatch, getters }) => { @@ -24,38 +22,25 @@ export const initializeRelease = ({ commit, dispatch, getters }) => { return Promise.resolve(); }; -export const fetchRelease = ({ commit, state, rootState }) => { +export const fetchRelease = async ({ commit, state }) => { commit(types.REQUEST_RELEASE); - if (rootState.featureFlags?.graphqlIndividualReleasePage) { - return gqClient - .query({ - query: oneReleaseQuery, - variables: { - fullPath: state.projectPath, - tagName: state.tagName, - }, - }) - .then((response) => { - const { data: release } = convertOneReleaseGraphQLResponse(response); - - commit(types.RECEIVE_RELEASE_SUCCESS, release); - }) - .catch((error) => { - commit(types.RECEIVE_RELEASE_ERROR, error); - createFlash(s__('Release|Something went wrong while getting the release details.')); - }); - } - - return api - .release(state.projectId, state.tagName) - .then(({ data }) => { - commit(types.RECEIVE_RELEASE_SUCCESS, apiJsonToRelease(data)); - }) - .catch((error) => { - commit(types.RECEIVE_RELEASE_ERROR, error); - createFlash(s__('Release|Something went wrong while getting the release details.')); + try { + const fetchResponse = await gqClient.query({ + query: oneReleaseForEditingQuery, + variables: { + fullPath: state.projectPath, + tagName: state.tagName, + }, }); + + const { data: release } = convertOneReleaseGraphQLResponse(fetchResponse); + + commit(types.RECEIVE_RELEASE_SUCCESS, release); + } catch (error) { + commit(types.RECEIVE_RELEASE_ERROR, error); + createFlash(s__('Release|Something went wrong while getting the release details.')); + } }; export const updateReleaseTagName = ({ commit }, tagName) => @@ -94,9 +79,9 @@ export const removeAssetLink = ({ commit }, linkIdToRemove) => { commit(types.REMOVE_ASSET_LINK, linkIdToRemove); }; -export const receiveSaveReleaseSuccess = ({ commit }, release) => { +export const receiveSaveReleaseSuccess = ({ commit }, urlToRedirectTo) => { commit(types.RECEIVE_SAVE_RELEASE_SUCCESS); - redirectTo(release._links.self); + redirectTo(urlToRedirectTo); }; export const saveRelease = ({ commit, dispatch, getters }) => { @@ -105,83 +90,130 @@ export const saveRelease = ({ commit, dispatch, getters }) => { dispatch(getters.isExistingRelease ? 'updateRelease' : 'createRelease'); }; -export const createRelease = ({ commit, dispatch, state, getters }) => { - const apiJson = releaseToApiJson( - { - ...state.release, - assets: { - links: getters.releaseLinksToCreate, - }, - }, - state.createFrom, - ); +/** + * Tests a GraphQL mutation response for the existence of any errors-as-data + * (See https://docs.gitlab.com/ee/development/fe_guide/graphql.html#errors-as-data). + * If any errors occurred, throw a JavaScript `Error` object, so that this can be + * handled by the global error handler. + * + * @param {Object} gqlResponse The response object returned by the GraphQL client + * @param {String} mutationName The name of the mutation that was executed + * @param {String} messageIfError An message to build into the error object if something went wrong + */ +const checkForErrorsAsData = (gqlResponse, mutationName, messageIfError) => { + const allErrors = gqlResponse.data[mutationName].errors; + if (allErrors.length > 0) { + const allErrorMessages = JSON.stringify(allErrors); + throw new Error(`${messageIfError}: ${allErrorMessages}`); + } +}; - return api - .createRelease(state.projectId, apiJson) - .then(({ data }) => { - dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(data)); - }) - .catch((error) => { - commit(types.RECEIVE_SAVE_RELEASE_ERROR, error); - createFlash(s__('Release|Something went wrong while creating a new release')); +export const createRelease = async ({ commit, dispatch, state, getters }) => { + try { + const response = await gqClient.mutate({ + mutation: createReleaseMutation, + variables: getters.releaseCreateMutatationVariables, }); + + checkForErrorsAsData( + response, + 'releaseCreate', + `Something went wrong while creating a new release with projectPath "${state.projectPath}" and tagName "${state.release.tagName}"`, + ); + + dispatch('receiveSaveReleaseSuccess', response.data.releaseCreate.release.links.selfUrl); + } catch (error) { + commit(types.RECEIVE_SAVE_RELEASE_ERROR, error); + createFlash(s__('Release|Something went wrong while creating a new release.')); + } }; -export const updateRelease = ({ commit, dispatch, state, getters }) => { - const apiJson = releaseToApiJson({ - ...state.release, - assets: { - links: getters.releaseLinksToCreate, +/** + * Deletes a single release link. + * Throws an error if any network or validation errors occur. + */ +const deleteReleaseLinks = async ({ state, id }) => { + const deleteResponse = await gqClient.mutate({ + mutation: deleteReleaseAssetLinkMutation, + variables: { + input: { id }, }, }); - let updatedRelease = null; - - return ( - api - .updateRelease(state.projectId, state.tagName, apiJson) - - /** - * Currently, we delete all existing links and then - * recreate new ones on each edit. This is because the - * REST API doesn't support bulk updating of Release links, - * and updating individual links can lead to validation - * race conditions (in particular, the "URLs must be unique") - * constraint. - * - * This isn't ideal since this is no longer an atomic - * operation - parts of it can fail while others succeed, - * leaving the Release in an inconsistent state. - * - * This logic should be refactored to use GraphQL once - * https://gitlab.com/gitlab-org/gitlab/-/issues/208702 - * is closed. - */ - .then(({ data }) => { - // Save this response since we need it later in the Promise chain - updatedRelease = data; - - // Delete all links currently associated with this Release - return Promise.all( - getters.releaseLinksToDelete.map((l) => - api.deleteReleaseLink(state.projectId, state.release.tagName, l.id), - ), - ); - }) - .then(() => { - // Create a new link for each link in the form - return Promise.all( - apiJson.assets.links.map((l) => - api.createReleaseLink(state.projectId, state.release.tagName, l), - ), - ); - }) - .then(() => { - dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(updatedRelease)); - }) - .catch((error) => { - commit(types.RECEIVE_SAVE_RELEASE_ERROR, error); - createFlash(s__('Release|Something went wrong while saving the release details')); - }) + checkForErrorsAsData( + deleteResponse, + 'releaseAssetLinkDelete', + `Something went wrong while deleting release asset link for release with projectPath "${state.projectPath}", tagName "${state.tagName}", and link id "${id}"`, ); }; + +/** + * Creates a single release link. + * Throws an error if any network or validation errors occur. + */ +const createReleaseLink = async ({ state, link }) => { + const createResponse = await gqClient.mutate({ + mutation: createReleaseAssetLinkMutation, + variables: { + input: { + projectPath: state.projectPath, + tagName: state.tagName, + name: link.name, + url: link.url, + linkType: link.linkType.toUpperCase(), + }, + }, + }); + + checkForErrorsAsData( + createResponse, + 'releaseAssetLinkCreate', + `Something went wrong while creating a release asset link for release with projectPath "${state.projectPath}" and tagName "${state.tagName}"`, + ); +}; + +export const updateRelease = async ({ commit, dispatch, state, getters }) => { + try { + /** + * Currently, we delete all existing links and then + * recreate new ones on each edit. This is because the + * backend doesn't support bulk updating of Release links, + * and updating individual links can lead to validation + * race conditions (in particular, the "URLs must be unique") + * constraint. + * + * This isn't ideal since this is no longer an atomic + * operation - parts of it can fail while others succeed, + * leaving the Release in an inconsistent state. + * + * This logic should be refactored to take place entirely + * in the backend. This is being discussed in + * https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50300 + */ + const updateReleaseResponse = await gqClient.mutate({ + mutation: updateReleaseMutation, + variables: getters.releaseUpdateMutatationVariables, + }); + + checkForErrorsAsData( + updateReleaseResponse, + 'releaseUpdate', + `Something went wrong while updating release with projectPath "${state.projectPath}" and tagName "${state.tagName}"`, + ); + + // Delete all links currently associated with this Release + await Promise.all( + getters.releaseLinksToDelete.map(({ id }) => deleteReleaseLinks({ state, id })), + ); + + // Create a new link for each link in the form + await Promise.all( + getters.releaseLinksToCreate.map((link) => createReleaseLink({ state, link })), + ); + + dispatch('receiveSaveReleaseSuccess', state.release._links.self); + } catch (error) { + commit(types.RECEIVE_SAVE_RELEASE_ERROR, error); + createFlash(s__('Release|Something went wrong while saving the release details.')); + } +}; diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js index 831037c8861..d83ec05872a 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js @@ -103,3 +103,39 @@ export const isValid = (_state, getters) => { const errors = getters.validationErrors; return Object.values(errors.assets.links).every(isEmpty) && !errors.isTagNameEmpty; }; + +/** Returns all the variables for a `releaseUpdate` GraphQL mutation */ +export const releaseUpdateMutatationVariables = (state) => { + const name = state.release.name?.trim().length > 0 ? state.release.name.trim() : null; + + // Milestones may be either a list of milestone objects OR just a list + // of milestone titles. The GraphQL mutation requires only the titles be sent. + const milestones = (state.release.milestones || []).map((m) => m.title || m); + + return { + input: { + projectPath: state.projectPath, + tagName: state.release.tagName, + name, + description: state.release.description, + milestones, + }, + }; +}; + +/** Returns all the variables for a `releaseCreate` GraphQL mutation */ +export const releaseCreateMutatationVariables = (state, getters) => { + return { + input: { + ...getters.releaseUpdateMutatationVariables.input, + ref: state.createFrom, + assets: { + links: getters.releaseLinksToCreate.map(({ name, url, linkType }) => ({ + name, + url, + linkType: linkType.toUpperCase(), + })), + }, + }, + }; +}; diff --git a/app/assets/javascripts/releases/stores/modules/index/actions.js b/app/assets/javascripts/releases/stores/modules/index/actions.js index f1add54626a..00be25f089b 100644 --- a/app/assets/javascripts/releases/stores/modules/index/actions.js +++ b/app/assets/javascripts/releases/stores/modules/index/actions.js @@ -1,45 +1,21 @@ -import api from '~/api'; import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { - normalizeHeaders, - parseIntPagination, - convertObjectPropsToCamelCase, -} from '~/lib/utils/common_utils'; import { __ } from '~/locale'; -import allReleasesQuery from '~/releases/queries/all_releases.query.graphql'; -import { PAGE_SIZE } from '../../../constants'; -import { gqClient, convertAllReleasesGraphQLResponse } from '../../../util'; +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 server + * Gets a paginated list of releases from the GraphQL endpoint * * @param {Object} vuexParams * @param {Object} actionParams - * @param {Number} [actionParams.page] The page number of results to fetch - * (this parameter is only used when fetching results from the REST API) * @param {String} [actionParams.before] A GraphQL cursor. If provided, - * the items returned will proceed the provided cursor (this parameter is only - * used when fetching results from the GraphQL API). + * 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 (this parameter is only - * used when fetching results from the GraphQL API). + * the items returned will follow the provided cursor. */ -export const fetchReleases = ({ dispatch, rootGetters }, { page = 1, before, after }) => { - if (rootGetters.useGraphQLEndpoint) { - dispatch('fetchReleasesGraphQl', { before, after }); - } else { - dispatch('fetchReleasesRest', { page }); - } -}; - -/** - * Gets a paginated list of releases from the GraphQL endpoint - */ -export const fetchReleasesGraphQl = ( - { dispatch, commit, state }, - { before = null, after = null }, -) => { +export const fetchReleases = ({ dispatch, commit, state }, { before, after }) => { commit(types.REQUEST_RELEASES); const { sort, orderBy } = state.sorting; @@ -55,7 +31,7 @@ export const fetchReleasesGraphQl = ( paginationParams = { first: PAGE_SIZE, after }; } else { throw new Error( - 'Both a `before` and an `after` parameter were provided to fetchReleasesGraphQl. These parameters cannot be used together.', + 'Both a `before` and an `after` parameter were provided to fetchReleases. These parameters cannot be used together.', ); } @@ -69,33 +45,11 @@ export const fetchReleasesGraphQl = ( }, }) .then((response) => { - const { data, paginationInfo: graphQlPageInfo } = convertAllReleasesGraphQLResponse(response); + const { data, paginationInfo: pageInfo } = convertAllReleasesGraphQLResponse(response); commit(types.RECEIVE_RELEASES_SUCCESS, { data, - graphQlPageInfo, - }); - }) - .catch(() => dispatch('receiveReleasesError')); -}; - -/** - * Gets a paginated list of releases from the REST endpoint - */ -export const fetchReleasesRest = ({ dispatch, commit, state }, { page }) => { - commit(types.REQUEST_RELEASES); - - const { sort, orderBy } = state.sorting; - - api - .releases(state.projectId, { page, sort, order_by: orderBy }) - .then(({ data, headers }) => { - const restPageInfo = parseIntPagination(normalizeHeaders(headers)); - const camelCasedReleases = convertObjectPropsToCamelCase(data, { deep: true }); - - commit(types.RECEIVE_RELEASES_SUCCESS, { - data: camelCasedReleases, - restPageInfo, + pageInfo, }); }) .catch(() => dispatch('receiveReleasesError')); diff --git a/app/assets/javascripts/releases/stores/modules/index/mutations.js b/app/assets/javascripts/releases/stores/modules/index/mutations.js index e1aaa2e2a19..55a8a488be8 100644 --- a/app/assets/javascripts/releases/stores/modules/index/mutations.js +++ b/app/assets/javascripts/releases/stores/modules/index/mutations.js @@ -17,12 +17,11 @@ export default { * @param {Object} state * @param {Object} resp */ - [types.RECEIVE_RELEASES_SUCCESS](state, { data, restPageInfo, graphQlPageInfo }) { + [types.RECEIVE_RELEASES_SUCCESS](state, { data, pageInfo }) { state.hasError = false; state.isLoading = false; state.releases = data; - state.restPageInfo = restPageInfo; - state.graphQlPageInfo = graphQlPageInfo; + state.pageInfo = pageInfo; }, /** @@ -36,8 +35,7 @@ export default { state.isLoading = false; state.releases = []; state.hasError = true; - state.restPageInfo = {}; - state.graphQlPageInfo = {}; + state.pageInfo = {}; }, [types.SET_SORTING](state, sorting) { diff --git a/app/assets/javascripts/releases/stores/modules/index/state.js b/app/assets/javascripts/releases/stores/modules/index/state.js index 164a496d450..5e1aaab7b58 100644 --- a/app/assets/javascripts/releases/stores/modules/index/state.js +++ b/app/assets/javascripts/releases/stores/modules/index/state.js @@ -16,8 +16,7 @@ export default ({ isLoading: false, hasError: false, releases: [], - restPageInfo: {}, - graphQlPageInfo: {}, + pageInfo: {}, sorting: { sort: DESCENDING_ORDER, orderBy: RELEASED_AT, diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js index 36c17b5b252..22d5fb4f620 100644 --- a/app/assets/javascripts/releases/util.js +++ b/app/assets/javascripts/releases/util.js @@ -1,50 +1,7 @@ import { pick } from 'lodash'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; -import { - convertObjectPropsToCamelCase, - convertObjectPropsToSnakeCase, -} from '~/lib/utils/common_utils'; import { truncateSha } from '~/lib/utils/text_utility'; -/** - * Converts a release object into a JSON object that can sent to the public - * API to create or update a release. - * @param {Object} release The release object to convert - * @param {string} createFrom The ref to create a new tag from, if necessary - */ -export const releaseToApiJson = (release, createFrom = null) => { - const name = release.name?.trim().length > 0 ? release.name.trim() : null; - - // Milestones may be either a list of milestone objects OR just a list - // of milestone titles. The API requires only the titles be sent. - const milestones = (release.milestones || []).map((m) => m.title || m); - - return convertObjectPropsToSnakeCase( - { - name, - tagName: release.tagName, - ref: createFrom, - description: release.description, - milestones, - assets: release.assets, - }, - { deep: true }, - ); -}; - -/** - * Converts a JSON release object returned by the Release API - * into the structure this Vue application can work with. - * @param {Object} json The JSON object received from the release API - */ -export const apiJsonToRelease = (json) => { - const release = convertObjectPropsToCamelCase(json, { deep: true }); - - release.milestones = release.milestones || []; - - return release; -}; - export const gqClient = createGqClient({}, { fetchPolicy: fetchPolicies.NO_CACHE }); const convertScalarProperties = (graphQLRelease) => @@ -52,24 +9,37 @@ const convertScalarProperties = (graphQLRelease) => 'name', 'tagName', 'tagPath', + 'description', 'descriptionHtml', 'releasedAt', 'upcomingRelease', ]); -const convertAssets = (graphQLRelease) => ({ - assets: { - count: graphQLRelease.assets.count, - sources: [...graphQLRelease.assets.sources.nodes], - links: graphQLRelease.assets.links.nodes.map((l) => ({ +const convertAssets = (graphQLRelease) => { + let sources = []; + if (graphQLRelease.assets.sources?.nodes) { + sources = [...graphQLRelease.assets.sources.nodes]; + } + + let links = []; + if (graphQLRelease.assets.links?.nodes) { + links = graphQLRelease.assets.links.nodes.map((l) => ({ ...l, linkType: l.linkType?.toLowerCase(), - })), - }, -}); + })); + } + + return { + assets: { + count: graphQLRelease.assets.count, + sources, + links, + }, + }; +}; const convertEvidences = (graphQLRelease) => ({ - evidences: graphQLRelease.evidences.nodes.map((e) => e), + evidences: (graphQLRelease.evidences?.nodes ?? []).map((e) => ({ ...e })), }); const convertLinks = (graphQLRelease) => ({ @@ -100,18 +70,19 @@ const convertMilestones = (graphQLRelease) => ({ ...m, webUrl: m.webPath, webPath: undefined, - issueStats: { - total: m.stats.totalIssuesCount, - closed: m.stats.closedIssuesCount, - }, + issueStats: m.stats + ? { + total: m.stats.totalIssuesCount, + closed: m.stats.closedIssuesCount, + } + : {}, stats: undefined, })), }); /** * Converts a single release object fetched from GraphQL - * into a release object that matches the shape of the REST API - * (the same shape that is returned by `apiJsonToRelease` above.) + * into a release object that matches the general structure of the REST API * * @param graphQLRelease The release object returned from a GraphQL query */ |