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:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 18:44:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 18:44:42 +0300
commit4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch)
tree5423a1c7516cffe36384133ade12572cf709398d /app/assets/javascripts/releases
parente570267f2f6b326480d284e0164a6464ba4081bc (diff)
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'app/assets/javascripts/releases')
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue12
-rw-r--r--app/assets/javascripts/releases/components/app_show.vue2
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination.vue35
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_graphql.vue35
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_rest.vue24
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue17
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql (renamed from app/assets/javascripts/releases/queries/release.fragment.graphql)0
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql23
-rw-r--r--app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql10
-rw-r--r--app/assets/javascripts/releases/graphql/mutations/create_release_link.mutation.graphql5
-rw-r--r--app/assets/javascripts/releases/graphql/mutations/delete_release_link.mutation.graphql5
-rw-r--r--app/assets/javascripts/releases/graphql/mutations/update_release.mutation.graphql5
-rw-r--r--app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql (renamed from app/assets/javascripts/releases/queries/all_releases.query.graphql)2
-rw-r--r--app/assets/javascripts/releases/graphql/queries/one_release.query.graphql (renamed from app/assets/javascripts/releases/queries/one_release.query.graphql)2
-rw-r--r--app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql9
-rw-r--r--app/assets/javascripts/releases/mount_index.js5
-rw-r--r--app/assets/javascripts/releases/stores/getters.js11
-rw-r--r--app/assets/javascripts/releases/stores/index.js2
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/actions.js252
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/getters.js36
-rw-r--r--app/assets/javascripts/releases/stores/modules/index/actions.js66
-rw-r--r--app/assets/javascripts/releases/stores/modules/index/mutations.js8
-rw-r--r--app/assets/javascripts/releases/stores/modules/index/state.js3
-rw-r--r--app/assets/javascripts/releases/util.js87
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
*/