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>2020-04-01 15:08:00 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-01 15:08:00 +0300
commit1a0d6dbdc2ac3047f4953a359ef27ba6e26074ae (patch)
treeddb78a8a0d1350dc767f049a21e0f7d37edaa82c /app/assets/javascripts/releases
parentb11f7057d067885619ee3e513751f180b2e8ad85 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/releases')
-rw-r--r--app/assets/javascripts/releases/components/app_edit.vue9
-rw-r--r--app/assets/javascripts/releases/components/asset_links_form.vue124
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/actions.js74
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/getters.js24
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/index.js2
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/mutation_types.js5
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/mutations.js28
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/state.js10
8 files changed, 266 insertions, 10 deletions
diff --git a/app/assets/javascripts/releases/components/app_edit.vue b/app/assets/javascripts/releases/components/app_edit.vue
index 6f4baaa5d74..4fa0e96217a 100644
--- a/app/assets/javascripts/releases/components/app_edit.vue
+++ b/app/assets/javascripts/releases/components/app_edit.vue
@@ -7,6 +7,8 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
+import AssetLinksForm from './asset_links_form.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'ReleaseEditApp',
@@ -16,10 +18,12 @@ export default {
GlButton,
GlLink,
MarkdownField,
+ AssetLinksForm,
},
directives: {
autofocusonshow,
},
+ mixins: [glFeatureFlagsMixin()],
computed: {
...mapState('detail', [
'isFetchingRelease',
@@ -80,6 +84,9 @@ export default {
cancelPath() {
return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath;
},
+ showAssetLinksForm() {
+ return this.glFeatures.releaseAssetLinkEditing;
+ },
},
created() {
this.fetchRelease();
@@ -153,6 +160,8 @@ export default {
</div>
</gl-form-group>
+ <asset-links-form v-if="showAssetLinksForm" />
+
<div class="d-flex pt-3">
<gl-button
class="mr-auto js-submit-button"
diff --git a/app/assets/javascripts/releases/components/asset_links_form.vue b/app/assets/javascripts/releases/components/asset_links_form.vue
new file mode 100644
index 00000000000..f4c92477775
--- /dev/null
+++ b/app/assets/javascripts/releases/components/asset_links_form.vue
@@ -0,0 +1,124 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import {
+ GlSprintf,
+ GlLink,
+ GlFormGroup,
+ GlButton,
+ GlIcon,
+ GlTooltipDirective,
+ GlFormInput,
+} from '@gitlab/ui';
+
+export default {
+ name: 'AssetLinksForm',
+ components: { GlSprintf, GlLink, GlFormGroup, GlButton, GlIcon, GlFormInput },
+ directives: { GlTooltip: GlTooltipDirective },
+ computed: {
+ ...mapState('detail', ['release', 'releaseAssetsDocsPath']),
+ },
+ created() {
+ this.addEmptyAssetLink();
+ },
+ methods: {
+ ...mapActions('detail', [
+ 'addEmptyAssetLink',
+ 'updateAssetLinkUrl',
+ 'updateAssetLinkName',
+ 'removeAssetLink',
+ ]),
+ onAddAnotherClicked() {
+ this.addEmptyAssetLink();
+ },
+ onRemoveClicked(linkId) {
+ this.removeAssetLink(linkId);
+ },
+ onUrlInput(linkIdToUpdate, newUrl) {
+ this.updateAssetLinkUrl({ linkIdToUpdate, newUrl });
+ },
+ onLinkTitleInput(linkIdToUpdate, newName) {
+ this.updateAssetLinkName({ linkIdToUpdate, newName });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="d-flex flex-column release-assets-links-form">
+ <h2 class="text-4">{{ __('Release assets') }}</h2>
+ <p class="m-0">
+ <gl-sprintf
+ :message="
+ __(
+ 'Add %{linkStart}assets%{linkEnd} to your Release. GitLab automatically includes read-only assets, like source code and release evidence.',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link
+ :href="releaseAssetsDocsPath"
+ target="_blank"
+ :aria-label="__('Release assets documentation')"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <h3 class="text-3">{{ __('Links') }}</h3>
+ <p>
+ {{
+ __(
+ 'Point to any links you like: documentation, built binaries, or other related materials. These can be internal or external links from your GitLab instance.',
+ )
+ }}
+ </p>
+ <div
+ v-for="(link, index) in release.assets.links"
+ :key="link.id"
+ class="d-flex flex-column flex-sm-row align-items-stretch align-items-sm-end"
+ >
+ <gl-form-group
+ class="url-field form-group flex-grow-1 mr-sm-4"
+ :label="__('URL')"
+ :label-for="`asset-url-${index}`"
+ >
+ <gl-form-input
+ :id="`asset-url-${index}`"
+ :value="link.url"
+ type="text"
+ class="form-control"
+ @change="onUrlInput(link.id, $event)"
+ />
+ </gl-form-group>
+
+ <gl-form-group
+ class="link-title-field flex-grow-1 mr-sm-4"
+ :label="__('Link title')"
+ :label-for="`asset-link-name-${index}`"
+ >
+ <gl-form-input
+ :id="`asset-link-name-${index}`"
+ :value="link.name"
+ type="text"
+ class="form-control"
+ @change="onLinkTitleInput(link.id, $event)"
+ />
+ </gl-form-group>
+
+ <gl-button
+ v-gl-tooltip
+ class="mb-5 mb-sm-3 flex-grow-0 flex-shrink-0 remove-button"
+ :aria-label="__('Remove asset link')"
+ :title="__('Remove asset link')"
+ @click="onRemoveClicked(link.id)"
+ >
+ <gl-icon class="m-0" name="remove" />
+ <span class="d-inline d-sm-none">{{ __('Remove asset link') }}</span>
+ </gl-button>
+ </div>
+ <gl-button variant="link" class="align-self-end mb-5 mb-sm-0" @click="onAddAnotherClicked">
+ {{ __('Add another link') }}
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js
index 1b77f01368e..7b84c18242c 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js
@@ -41,20 +41,74 @@ export const receiveUpdateReleaseError = ({ commit }, error) => {
createFlash(s__('Release|Something went wrong while saving the release details'));
};
-export const updateRelease = ({ dispatch, state }) => {
+export const updateRelease = ({ dispatch, state, getters }) => {
dispatch('requestUpdateRelease');
- return api
- .updateRelease(state.projectId, state.tagName, {
- name: state.release.name,
- description: state.release.description,
- })
- .then(() => dispatch('receiveUpdateReleaseSuccess'))
- .catch(error => {
- dispatch('receiveUpdateReleaseError', error);
- });
+ const { release } = state;
+
+ return (
+ api
+ .updateRelease(state.projectId, state.tagName, {
+ name: release.name,
+ description: release.description,
+ })
+
+ /**
+ * 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(() => {
+ // Delete all links currently associated with this Release
+ return Promise.all(
+ getters.releaseLinksToDelete.map(l =>
+ api.deleteReleaseLink(state.projectId, release.tagName, l.id),
+ ),
+ );
+ })
+ .then(() => {
+ // Create a new link for each link in the form
+ return Promise.all(
+ getters.releaseLinksToCreate.map(l =>
+ api.createReleaseLink(state.projectId, release.tagName, l),
+ ),
+ );
+ })
+ .then(() => dispatch('receiveUpdateReleaseSuccess'))
+ .catch(error => {
+ dispatch('receiveUpdateReleaseError', error);
+ })
+ );
};
export const navigateToReleasesPage = ({ state }) => {
redirectTo(state.releasesPagePath);
};
+
+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 removeAssetLink = ({ commit }, linkIdToRemove) => {
+ commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
+};
diff --git a/app/assets/javascripts/releases/stores/modules/detail/getters.js b/app/assets/javascripts/releases/stores/modules/detail/getters.js
new file mode 100644
index 00000000000..562284dc48d
--- /dev/null
+++ b/app/assets/javascripts/releases/stores/modules/detail/getters.js
@@ -0,0 +1,24 @@
+/**
+ * @returns {Boolean} `true` if the release link is empty, i.e. it has
+ * empty (or whitespace-only) values for both `url` and `name`.
+ * Otherwise, `false`.
+ */
+const isEmptyReleaseLink = l => !/\S/.test(l.url) && !/\S/.test(l.name);
+
+/** Returns all release links that aren't empty */
+export const releaseLinksToCreate = state => {
+ if (!state.release) {
+ return [];
+ }
+
+ return state.release.assets.links.filter(l => !isEmptyReleaseLink(l));
+};
+
+/** Returns all release links that should be deleted */
+export const releaseLinksToDelete = state => {
+ if (!state.originalRelease) {
+ return [];
+ }
+
+ return state.originalRelease.assets.links;
+};
diff --git a/app/assets/javascripts/releases/stores/modules/detail/index.js b/app/assets/javascripts/releases/stores/modules/detail/index.js
index b4430cff2ab..40fdb04f2eb 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/index.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/index.js
@@ -1,10 +1,12 @@
import * as actions from './actions';
+import * as getters from './getters';
import mutations from './mutations';
import createState from './state';
export default initialState => ({
namespaced: true,
actions,
+ getters,
mutations,
state: createState(initialState),
});
diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js b/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
index 51c0590012a..04944b76e42 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
@@ -8,3 +8,8 @@ export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE';
export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS';
export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR';
+
+export const ADD_EMPTY_ASSET_LINK = 'ADD_EMPTY_ASSET_LINK';
+export const UPDATE_ASSET_LINK_URL = 'UPDATE_ASSET_LINK_URL';
+export const UPDATE_ASSET_LINK_NAME = 'UPDATE_ASSET_LINK_NAME';
+export const REMOVE_ASSET_LINK = 'REMOVE_ASSET_LINK';
diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutations.js b/app/assets/javascripts/releases/stores/modules/detail/mutations.js
index 913db6c2b2a..3d97e3a75c2 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/mutations.js
@@ -1,4 +1,9 @@
import * as types from './mutation_types';
+import { uniqueId, cloneDeep } from 'lodash';
+
+const findReleaseLink = (release, id) => {
+ return release.assets.links.find(l => l.id === id);
+};
export default {
[types.REQUEST_RELEASE](state) {
@@ -8,6 +13,7 @@ export default {
state.fetchError = undefined;
state.isFetchingRelease = false;
state.release = data;
+ state.originalRelease = Object.freeze(cloneDeep(state.release));
},
[types.RECEIVE_RELEASE_ERROR](state, error) {
state.fetchError = error;
@@ -33,4 +39,26 @@ export default {
state.updateError = error;
state.isUpdatingRelease = false;
},
+
+ [types.ADD_EMPTY_ASSET_LINK](state) {
+ state.release.assets.links.push({
+ id: uniqueId('new-link-'),
+ url: '',
+ name: '',
+ });
+ },
+
+ [types.UPDATE_ASSET_LINK_URL](state, { linkIdToUpdate, newUrl }) {
+ const linkToUpdate = findReleaseLink(state.release, linkIdToUpdate);
+ linkToUpdate.url = newUrl;
+ },
+
+ [types.UPDATE_ASSET_LINK_NAME](state, { linkIdToUpdate, newName }) {
+ const linkToUpdate = findReleaseLink(state.release, linkIdToUpdate);
+ linkToUpdate.name = newName;
+ },
+
+ [types.REMOVE_ASSET_LINK](state, linkIdToRemove) {
+ state.release.assets.links = state.release.assets.links.filter(l => l.id !== linkIdToRemove);
+ },
};
diff --git a/app/assets/javascripts/releases/stores/modules/detail/state.js b/app/assets/javascripts/releases/stores/modules/detail/state.js
index a19e8d044e2..b513e1bed79 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/state.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/state.js
@@ -5,6 +5,7 @@ export default ({
markdownDocsPath,
markdownPreviewPath,
updateReleaseApiDocsPath,
+ releaseAssetsDocsPath,
}) => ({
projectId,
tagName,
@@ -12,9 +13,18 @@ export default ({
markdownDocsPath,
markdownPreviewPath,
updateReleaseApiDocsPath,
+ releaseAssetsDocsPath,
+ /** The Release object */
release: null,
+ /**
+ * A deep clone of the Release object above.
+ * Used when editing this Release so that
+ * changes can be computed.
+ */
+ originalRelease: null,
+
isFetchingRelease: false,
fetchError: null,