summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
blob: 42ceed81c00adace3890c6deb4a5c5ca2dfcf01f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
import { getTag } from '~/rest_api';
import { createAlert } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql';
import deleteReleaseMutation from '~/releases/graphql/mutations/delete_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,
  deleteReleaseSessionKey,
} from '~/releases/util';

import * as types from './mutation_types';

class GraphQLError extends Error {}

export const initializeRelease = ({ commit, dispatch, state }) => {
  if (state.isExistingRelease) {
    // When editing an existing release,
    // fetch the release object from the API
    return dispatch('fetchRelease');
  }

  // When creating a new release, initialize the
  // store with an empty release object
  commit(types.INITIALIZE_EMPTY_RELEASE);
  return Promise.resolve();
};

export const fetchRelease = async ({ commit, state }) => {
  commit(types.REQUEST_RELEASE);

  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);
    createAlert({
      message: s__('Release|Something went wrong while getting the release details.'),
    });
  }
};

export const updateReleaseTagName = ({ commit }, tagName) =>
  commit(types.UPDATE_RELEASE_TAG_NAME, tagName);

export const updateReleaseTagMessage = ({ commit }, tagMessage) =>
  commit(types.UPDATE_RELEASE_TAG_MESSAGE, tagMessage);

export const updateCreateFrom = ({ commit }, createFrom) =>
  commit(types.UPDATE_CREATE_FROM, createFrom);

export const updateShowCreateFrom = ({ commit }, showCreateFrom) =>
  commit(types.UPDATE_SHOW_CREATE_FROM, showCreateFrom);

export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);

export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);

export const updateReleaseMilestones = ({ commit }, milestones) =>
  commit(types.UPDATE_RELEASE_MILESTONES, milestones);

export const updateReleaseGroupMilestones = ({ commit }, groupMilestones) =>
  commit(types.UPDATE_RELEASE_GROUP_MILESTONES, groupMilestones);

export const addEmptyAssetLink = ({ commit }) => {
  commit(types.ADD_EMPTY_ASSET_LINK);
};

export const updateAssetLinkUrl = ({ commit }, { linkIdToUpdate, newUrl }) => {
  commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl });
};

export const updateAssetLinkName = ({ commit }, { linkIdToUpdate, newName }) => {
  commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName });
};

export const updateAssetLinkType = ({ commit }, { linkIdToUpdate, newType }) => {
  commit(types.UPDATE_ASSET_LINK_TYPE, { linkIdToUpdate, newType });
};

export const removeAssetLink = ({ commit }, linkIdToRemove) => {
  commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
};

export const receiveSaveReleaseSuccess = ({ commit }, urlToRedirectTo) => {
  commit(types.RECEIVE_SAVE_RELEASE_SUCCESS);
  redirectTo(urlToRedirectTo);
};

export const saveRelease = ({ commit, dispatch, state }) => {
  commit(types.REQUEST_SAVE_RELEASE);

  dispatch(state.isExistingRelease ? 'updateRelease' : 'createRelease');
};

/**
 * 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
 */
const checkForErrorsAsData = (gqlResponse, mutationName) => {
  const allErrors = gqlResponse.data[mutationName].errors;
  if (allErrors.length > 0) {
    throw new GraphQLError(allErrors[0]);
  }
};

export const createRelease = async ({ commit, dispatch, getters }) => {
  try {
    const response = await gqClient.mutate({
      mutation: createReleaseMutation,
      variables: getters.releaseCreateMutatationVariables,
    });

    checkForErrorsAsData(response, 'releaseCreate');

    dispatch('receiveSaveReleaseSuccess', response.data.releaseCreate.release.links.selfUrl);
  } catch (error) {
    commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
    if (error instanceof GraphQLError) {
      createAlert({
        message: error.message,
      });
    } else {
      createAlert({
        message: s__('Release|Something went wrong while creating a new release.'),
      });
    }
  }
};

/**
 * Deletes a single release link.
 * Throws an error if any network or validation errors occur.
 */
const deleteReleaseLinks = async ({ id }) => {
  const deleteResponse = await gqClient.mutate({
    mutation: deleteReleaseAssetLinkMutation,
    variables: {
      input: { id },
    },
  });

  checkForErrorsAsData(deleteResponse, 'releaseAssetLinkDelete');
};

/**
 * 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.trim(),
        url: link.url,
        linkType: link.linkType.toUpperCase(),
        directAssetPath: link.directAssetPath,
      },
    },
  });

  checkForErrorsAsData(createResponse, 'releaseAssetLinkCreate');
};

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');

    // 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);
    createAlert({
      message: s__('Release|Something went wrong while saving the release details.'),
    });
  }
};

export const fetchTagNotes = ({ commit, state }, tagName) => {
  commit(types.REQUEST_TAG_NOTES);

  return getTag(state.projectId, tagName)
    .then(({ data }) => {
      commit(types.RECEIVE_TAG_NOTES_SUCCESS, data);
    })
    .catch((error) => {
      createAlert({
        message: s__('Release|Unable to fetch the tag notes.'),
      });

      commit(types.RECEIVE_TAG_NOTES_ERROR, error);
    });
};

export const updateIncludeTagNotes = ({ commit }, includeTagNotes) => {
  commit(types.UPDATE_INCLUDE_TAG_NOTES, includeTagNotes);
};

export const updateReleasedAt = ({ commit }, releasedAt) => {
  commit(types.UPDATE_RELEASED_AT, releasedAt);
};

export const deleteRelease = ({ commit, getters, dispatch, state }) => {
  commit(types.REQUEST_SAVE_RELEASE);
  return gqClient
    .mutate({
      mutation: deleteReleaseMutation,
      variables: getters.releaseDeleteMutationVariables,
    })
    .then((response) => checkForErrorsAsData(response, 'releaseDelete'))
    .then(() => {
      window.sessionStorage.setItem(
        deleteReleaseSessionKey(state.projectPath),
        state.originalRelease.name,
      );
      return dispatch('receiveSaveReleaseSuccess', state.releasesPagePath);
    })
    .catch((error) => {
      commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
      createAlert({
        message: s__('Release|Something went wrong while deleting the release.'),
      });
    });
};