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>2023-05-17 19:05:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 19:05:49 +0300
commit43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch)
treedceebdc68925362117480a5d672bcff122fb625b /app/assets/javascripts/releases
parent20c84b99005abd1c82101dfeff264ac50d2df211 (diff)
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/releases')
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue20
-rw-r--r--app/assets/javascripts/releases/components/app_show.vue2
-rw-r--r--app/assets/javascripts/releases/components/release_block_assets.vue18
-rw-r--r--app/assets/javascripts/releases/components/tag_create.vue91
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue283
-rw-r--r--app/assets/javascripts/releases/components/tag_search.vue121
-rw-r--r--app/assets/javascripts/releases/constants.js5
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql1
-rw-r--r--app/assets/javascripts/releases/mount_new.js2
-rw-r--r--app/assets/javascripts/releases/release_notification_service.js25
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/actions.js24
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/constants.js4
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/getters.js31
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js6
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutations.js14
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/state.js4
-rw-r--r--app/assets/javascripts/releases/util.js2
17 files changed, 419 insertions, 234 deletions
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index 9f200856db3..eebaeeea286 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -1,12 +1,13 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { historyPushState } from '~/lib/utils/common_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { setUrlParams, getParameterByName } from '~/lib/utils/url_utility';
-import { __, sprintf } from '~/locale';
+import { __ } from '~/locale';
import { PAGE_SIZE, DEFAULT_SORT } from '~/releases/constants';
-import { convertAllReleasesGraphQLResponse, deleteReleaseSessionKey } from '~/releases/util';
+import { convertAllReleasesGraphQLResponse } from '~/releases/util';
+import { popDeleteReleaseNotification } from '~/releases/release_notification_service';
import allReleasesQuery from '../graphql/queries/all_releases.query.graphql';
import ReleaseBlock from './release_block.vue';
import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
@@ -173,18 +174,7 @@ export default {
},
},
mounted() {
- const key = deleteReleaseSessionKey(this.projectPath);
- const deletedRelease = window.sessionStorage.getItem(key);
-
- if (deletedRelease) {
- this.$toast.show(
- sprintf(__('Release %{deletedRelease} has been successfully deleted.'), {
- deletedRelease,
- }),
- );
- }
-
- window.sessionStorage.removeItem(key);
+ popDeleteReleaseNotification(this.projectPath);
},
created() {
this.updateQueryParamsFromUrl();
diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue
index 544f2de5132..111d9e232c5 100644
--- a/app/assets/javascripts/releases/components/app_show.vue
+++ b/app/assets/javascripts/releases/components/app_show.vue
@@ -1,5 +1,5 @@
<script>
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import { popCreateReleaseNotification } from '~/releases/release_notification_service';
import oneReleaseQuery from '../graphql/queries/one_release.query.graphql';
diff --git a/app/assets/javascripts/releases/components/release_block_assets.vue b/app/assets/javascripts/releases/components/release_block_assets.vue
index cc28980a6bf..dd45a2b1762 100644
--- a/app/assets/javascripts/releases/components/release_block_assets.vue
+++ b/app/assets/javascripts/releases/components/release_block_assets.vue
@@ -83,8 +83,17 @@ export default {
linksForType(type) {
return this.assets.links.filter((l) => l.linkType === type);
},
+ getTooltipTitle(section) {
+ return section.title
+ ? this.$options.externalLinkTooltipText
+ : this.$options.downloadTooltipText;
+ },
+ getIconName(section) {
+ return section.title ? 'external-link' : 'download';
+ },
},
externalLinkTooltipText: __('This link points to external content'),
+ downloadTooltipText: __('Download'),
};
</script>
@@ -121,13 +130,12 @@ export default {
<gl-icon :name="section.iconName" class="gl-mr-2 gl-flex-shrink-0 gl-flex-grow-0" />
{{ link.name }}
<gl-icon
- v-if="section.title"
v-gl-tooltip
- name="external-link"
- :aria-label="$options.externalLinkTooltipText"
- :title="$options.externalLinkTooltipText"
+ :name="getIconName(section)"
+ :aria-label="getTooltipTitle(section)"
+ :title="getTooltipTitle(section)"
data-testid="external-link-indicator"
- class="gl-ml-2 gl-flex-shrink-0 gl-flex-grow-0 gl-text-gray-400"
+ class="gl-ml-2 gl-flex-shrink-0 gl-flex-grow-0"
/>
</gl-link>
</li>
diff --git a/app/assets/javascripts/releases/components/tag_create.vue b/app/assets/javascripts/releases/components/tag_create.vue
new file mode 100644
index 00000000000..44269bccec9
--- /dev/null
+++ b/app/assets/javascripts/releases/components/tag_create.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlButton, GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { uniqueId } from 'lodash';
+import { __, s__ } from '~/locale';
+import RefSelector from '~/ref/components/ref_selector.vue';
+
+export default {
+ components: {
+ GlButton,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ RefSelector,
+ },
+ model: {
+ prop: 'value',
+ event: 'change',
+ },
+ props: {
+ value: { type: String, required: true },
+ },
+ data() {
+ return {
+ nameId: uniqueId('tag-name-'),
+ refId: uniqueId('ref-'),
+ messageId: uniqueId('message-'),
+ };
+ },
+ computed: {
+ ...mapState('editNew', ['projectId', 'release', 'createFrom']),
+ },
+ methods: {
+ ...mapActions('editNew', ['updateReleaseTagMessage', 'updateCreateFrom']),
+ },
+ i18n: {
+ tagNameLabel: __('Tag name'),
+ refLabel: __('Create from'),
+ messageLabel: s__('CreateGitTag|Set tag message'),
+ messagePlaceholder: s__(
+ 'CreateGitTag|Add a message to the tag. Leaving this blank creates a lightweight tag.',
+ ),
+ create: __('Save'),
+ cancel: s__('Release|Select another tag'),
+ refSelector: {
+ noRefSelected: __('No source selected'),
+ searchPlaceholder: __('Search branches, tags, and commits'),
+ dropdownHeader: __('Select source'),
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-p-3" data-testid="create-from-field">
+ <gl-form-group
+ class="gl-mb-3"
+ :label="$options.i18n.tagNameLabel"
+ :label-for="nameId"
+ label-sr-only
+ >
+ <gl-form-input :id="nameId" :value="value" autofocus @input="$emit('change', $event)" />
+ </gl-form-group>
+ <gl-form-group class="gl-mb-3" :label="$options.i18n.refLabel" :label-for="refId" label-sr-only>
+ <ref-selector
+ :id="refId"
+ :project-id="projectId"
+ :value="createFrom"
+ :translations="$options.i18n.refSelector"
+ @input="updateCreateFrom"
+ />
+ </gl-form-group>
+ <gl-form-group
+ class="gl-mb-3"
+ :label="$options.i18n.messageLabel"
+ :label-for="messageId"
+ label-sr-only
+ >
+ <gl-form-textarea
+ :id="messageId"
+ :placeholder="$options.i18n.messagePlaceholder"
+ :no-resize="false"
+ :value="release.tagMessage"
+ @input="updateReleaseTagMessage"
+ />
+ </gl-form-group>
+ <gl-button class="gl-mr-3" variant="confirm" @click="$emit('create')">
+ {{ $options.i18n.create }}
+ </gl-button>
+ <gl-button @click="$emit('cancel')">{{ $options.i18n.cancel }}</gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index 2ddab5dddea..ec058cc3603 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -1,230 +1,133 @@
<script>
-import {
- GlCollapse,
- GlLink,
- GlFormGroup,
- GlFormTextarea,
- GlDropdownItem,
- GlSprintf,
-} from '@gitlab/ui';
-import { uniqueId } from 'lodash';
+import { GlDropdown, GlFormGroup, GlPopover } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import { __, s__ } from '~/locale';
-import RefSelector from '~/ref/components/ref_selector.vue';
-import { REF_TYPE_TAGS } from '~/ref/constants';
-import FormFieldContainer from './form_field_container.vue';
+
+import TagSearch from './tag_search.vue';
+import TagCreate from './tag_create.vue';
export default {
- name: 'TagFieldNew',
components: {
- GlCollapse,
+ GlDropdown,
GlFormGroup,
- GlFormTextarea,
- GlLink,
- RefSelector,
- FormFieldContainer,
- GlDropdownItem,
- GlSprintf,
+ GlPopover,
+ TagSearch,
+ TagCreate,
},
data() {
- return {
- // Keeps track of whether or not the user has interacted with
- // the input field. This is used to avoid showing validation
- // errors immediately when the page loads.
- isInputDirty: false,
- };
+ return { id: 'release-tag-name', newTagName: '', show: false, isInputDirty: false };
},
computed: {
- ...mapState('editNew', ['projectId', 'release', 'createFrom', 'showCreateFrom']),
- ...mapGetters('editNew', ['validationErrors']),
- tagName: {
- get() {
- return this.release.tagName;
- },
- set(tagName) {
- this.updateReleaseTagName(tagName);
-
- // This setter is used by the `v-model` on the `RefSelector`.
- // When this is called, the selection originated from the
- // dropdown list of existing tag names, so we know the tag
- // already exists and don't need to show the "create from" input
- this.updateShowCreateFrom(false);
- },
- },
- tagMessage: {
- get() {
- return this.release.tagMessage;
- },
- set(tagMessage) {
- this.updateReleaseTagMessage(tagMessage);
- },
- },
- createFromModel: {
- get() {
- return this.createFrom;
- },
- set(createFrom) {
- this.updateCreateFrom(createFrom);
- },
+ ...mapState('editNew', ['release', 'showCreateFrom']),
+ ...mapGetters('editNew', ['validationErrors', 'isSearching', 'isCreating']),
+ title() {
+ return this.isCreating ? this.$options.i18n.createTitle : this.$options.i18n.selectTitle;
},
showTagNameValidationError() {
- return (
- this.isInputDirty &&
- (this.validationErrors.isTagNameEmpty || this.validationErrors.existingRelease)
- );
+ return this.isInputDirty && !this.validationErrors.tagNameValidation.isValid;
},
- tagNameInputId() {
- return uniqueId('tag-name-input-');
+ tagFeedback() {
+ return this.validationErrors.tagNameValidation.validationErrors[0];
},
- createFromSelectorId() {
- return uniqueId('create-from-selector-');
+ buttonText() {
+ return this.release?.tagName || s__('Release|Search or create tag name');
},
- tagFeedback() {
- return this.validationErrors.existingRelease
- ? __('Selected tag is already in use. Choose another option.')
- : __('Tag name is required.');
+ buttonVariant() {
+ return this.showTagNameValidationError ? 'danger' : 'default';
+ },
+ createText() {
+ return this.newTagName ? this.$options.i18n.createTag : this.$options.i18n.typeNew;
},
},
methods: {
...mapActions('editNew', [
+ 'setSearching',
+ 'setCreating',
+ 'setNewTag',
+ 'setExistingTag',
'updateReleaseTagName',
- 'updateReleaseTagMessage',
- 'updateCreateFrom',
'fetchTagNotes',
- 'updateShowCreateFrom',
]),
- markInputAsDirty() {
- this.isInputDirty = true;
+ startCreate(query) {
+ this.newTagName = query;
+ this.setCreating();
},
- createTagClicked(newTagName) {
- this.updateReleaseTagName(newTagName);
+ selected(tag) {
+ this.updateReleaseTagName(tag);
- // This method is called when the user selects the "create tag"
- // option, so the tag does not already exist. Because of this,
- // we need to show the "create from" input.
- this.updateShowCreateFrom(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(),
- )
- );
+ if (this.isSearching) {
+ this.fetchTagNotes(tag);
+ this.setExistingTag();
+ this.newTagName = '';
+ } else {
+ this.setNewTag();
+ }
+
+ this.hidePopover();
},
- },
- translations: {
- tagName: {
- noRefSelected: __('No tag selected'),
- dropdownHeader: __('Tag name'),
- searchPlaceholder: __('Search or create tag'),
- label: __('Tag name'),
- labelDescription: __('*Required'),
+ markInputAsDirty() {
+ this.isInputDirty = true;
},
- createFrom: {
- noRefSelected: __('No source selected'),
- searchPlaceholder: __('Search branches, tags, and commits'),
- dropdownHeader: __('Select source'),
- label: __('Create from'),
- description: __('Existing branch name, tag, or commit SHA'),
+ showPopover() {
+ this.show = true;
},
- annotatedTag: {
- label: s__('CreateGitTag|Set tag message'),
- description: s__(
- 'CreateGitTag|Add a message to the tag. Leaving this blank creates a %{linkStart}lightweight tag%{linkEnd}.',
- ),
+ hidePopover() {
+ this.show = false;
},
},
- tagMessageId: uniqueId('tag-message-'),
-
- tagNameEnabledRefTypes: [REF_TYPE_TAGS],
- gitTagDocsLink: 'https://git-scm.com/book/en/v2/Git-Basics-Tagging/',
+ i18n: {
+ selectTitle: __('Tags'),
+ createTitle: s__('Release|Create tag'),
+ label: __('Tag name'),
+ required: __('(required)'),
+ create: __('Create'),
+ cancel: __('Cancel'),
+ },
};
</script>
<template>
- <div>
+ <div class="row">
<gl-form-group
- data-testid="tag-name-field"
+ class="col-md-4 col-sm-10"
+ :label="$options.i18n.label"
+ :label-for="id"
+ :optional-text="$options.i18n.required"
:state="!showTagNameValidationError"
:invalid-feedback="tagFeedback"
- :label="$options.translations.tagName.label"
- :label-for="tagNameInputId"
- :label-description="$options.translations.tagName.labelDescription"
+ optional
+ data-testid="tag-name-field"
>
- <form-field-container>
- <ref-selector
- :id="tagNameInputId"
- v-model="tagName"
- :project-id="projectId"
- :translations="$options.translations.tagName"
- :enabled-ref-types="$options.tagNameEnabledRefTypes"
- :state="!showTagNameValidationError"
- @input="fetchTagNotes"
- @hide.once="markInputAsDirty"
- >
- <template #footer="{ isLoading, matches, query }">
- <gl-dropdown-item
- v-if="shouldShowCreateTagOption(isLoading, matches, query)"
- is-check-item
- :is-checked="tagName === query"
- @click="createTagClicked(query)"
- >
- <gl-sprintf :message="__('Create tag %{tagName}')">
- <template #tagName>
- <b>{{ query }}</b>
- </template>
- </gl-sprintf>
- </gl-dropdown-item>
- </template>
- </ref-selector>
- </form-field-container>
+ <gl-dropdown
+ :id="id"
+ :variant="buttonVariant"
+ :text="buttonText"
+ :toggle-class="['gl-text-gray-900!']"
+ category="secondary"
+ class="gl-w-30"
+ @show.prevent="showPopover"
+ />
+ <gl-popover
+ :show="show"
+ :target="id"
+ :title="title"
+ :css-classes="['gl-z-index-200', 'release-tag-selector']"
+ placement="bottom"
+ triggers="manual"
+ container="content-body"
+ show-close-button
+ @close-button-clicked="hidePopover"
+ @hide.once="markInputAsDirty"
+ >
+ <div class="gl-border-t-solid gl-border-t-1 gl-border-gray-200">
+ <tag-create
+ v-if="isCreating"
+ v-model="newTagName"
+ @create="selected(newTagName)"
+ @cancel="setSearching"
+ />
+ <tag-search v-else v-model="newTagName" @create="startCreate" @select="selected" />
+ </div>
+ </gl-popover>
</gl-form-group>
- <gl-collapse :visible="showCreateFrom">
- <div class="gl-pl-6 gl-border-l-1 gl-border-l-solid gl-border-gray-300">
- <gl-form-group
- v-if="showCreateFrom"
- :label="$options.translations.createFrom.label"
- :label-for="createFromSelectorId"
- data-testid="create-from-field"
- >
- <form-field-container>
- <ref-selector
- :id="createFromSelectorId"
- v-model="createFromModel"
- :project-id="projectId"
- :translations="$options.translations.createFrom"
- />
- </form-field-container>
- <template #description>{{ $options.translations.createFrom.description }}</template>
- </gl-form-group>
- <gl-form-group
- v-if="showCreateFrom"
- :label="$options.translations.annotatedTag.label"
- :label-for="$options.tagMessageId"
- data-testid="annotated-tag-message-field"
- >
- <gl-form-textarea :id="$options.tagMessageId" v-model="tagMessage" />
- <template #description>
- <gl-sprintf :message="$options.translations.annotatedTag.description">
- <template #link="{ content }">
- <gl-link
- :href="$options.gitTagDocsLink"
- rel="noopener noreferrer"
- target="_blank"
- >{{ content }}</gl-link
- >
- </template>
- </gl-sprintf>
- </template>
- </gl-form-group>
- </div>
- </gl-collapse>
</div>
</template>
diff --git a/app/assets/javascripts/releases/components/tag_search.vue b/app/assets/javascripts/releases/components/tag_search.vue
new file mode 100644
index 00000000000..33b44c90e1f
--- /dev/null
+++ b/app/assets/javascripts/releases/components/tag_search.vue
@@ -0,0 +1,121 @@
+<script>
+import { GlButton, GlDropdownItem, GlSearchBoxByType, GlSprintf } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { debounce } from 'lodash';
+import { REF_TYPE_TAGS, SEARCH_DEBOUNCE_MS } from '~/ref/constants';
+import { __, s__ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlSprintf,
+ },
+ model: {
+ prop: 'query',
+ event: 'change',
+ },
+ props: {
+ query: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return { tagName: '' };
+ },
+ computed: {
+ ...mapState('ref', ['matches']),
+ ...mapState('editNew', ['projectId', 'release']),
+ tags() {
+ return this.matches?.tags?.list || [];
+ },
+ createText() {
+ return this.query ? this.$options.i18n.createTag : this.$options.i18n.typeNew;
+ },
+ selectedNotShown() {
+ return this.release.tagName && !this.tags.some((tag) => tag.name === this.release.tagName);
+ },
+ },
+ created() {
+ this.debouncedSearch = debounce(this.search, SEARCH_DEBOUNCE_MS);
+ },
+ mounted() {
+ this.setProjectId(this.projectId);
+ this.setEnabledRefTypes([REF_TYPE_TAGS]);
+ this.search(this.query);
+ },
+ methods: {
+ ...mapActions('ref', ['setEnabledRefTypes', 'setProjectId', 'search']),
+ onSearchBoxInput(searchQuery = '') {
+ const query = searchQuery.trim();
+ this.$emit('change', query);
+ this.debouncedSearch(query);
+ },
+ selected(tagName) {
+ return (this.release?.tagName ?? '') === tagName;
+ },
+ },
+ i18n: {
+ noResults: __('No results found'),
+ createTag: s__('Release|Create tag %{tag}'),
+ typeNew: s__('Release|Or type a new tag name'),
+ },
+};
+</script>
+<template>
+ <div data-testid="tag-name-search">
+ <gl-search-box-by-type
+ :value="query"
+ class="gl-border-b-solid gl-border-b-1 gl-border-gray-200"
+ borderless
+ autofocus
+ @input="onSearchBoxInput"
+ />
+ <div class="gl-overflow-y-auto release-tag-list">
+ <div v-if="tags.length || release.tagName">
+ <gl-dropdown-item
+ v-if="selectedNotShown"
+ is-checked
+ is-check-item
+ class="gl-list-style-none"
+ >
+ {{ release.tagName }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-for="tag in tags"
+ :key="tag.name"
+ :is-checked="selected(tag.name)"
+ is-check-item
+ class="gl-list-style-none"
+ @click="$emit('select', tag.name)"
+ >
+ {{ tag.name }}
+ </gl-dropdown-item>
+ </div>
+ <div
+ v-else
+ class="gl-my-5 gl-text-gray-500 gl-display-flex gl-font-base gl-justify-content-center"
+ >
+ {{ $options.i18n.noResults }}
+ </div>
+ </div>
+ <div class="gl-border-t-solid gl-border-t-1 gl-border-gray-200 gl-py-3">
+ <gl-button
+ category="tertiary"
+ class="gl-justify-content-start! gl-rounded-0!"
+ block
+ :disabled="!query"
+ @click="$emit('create', query)"
+ >
+ <gl-sprintf :message="createText">
+ <template #tag>
+ <span class="gl-font-weight-bold">{{ query }}</span>
+ </template>
+ </gl-sprintf>
+ </gl-button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/constants.js b/app/assets/javascripts/releases/constants.js
index 4f862741e11..5e9e65a01b3 100644
--- a/app/assets/javascripts/releases/constants.js
+++ b/app/assets/javascripts/releases/constants.js
@@ -49,3 +49,8 @@ export const SORT_MAP = {
};
export const DEFAULT_SORT = RELEASED_AT_DESC;
+
+export const i18n = {
+ tagNameIsRequiredMessage: __('Tag name is required.'),
+ tagIsAlredyInUseMessage: __('Selected tag is already in use. Choose another option.'),
+};
diff --git a/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
index e22726f27a7..e6d981600b9 100644
--- a/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
+++ b/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
@@ -23,7 +23,6 @@ fragment Release on Release {
url
directAssetUrl
linkType
- external
}
}
}
diff --git a/app/assets/javascripts/releases/mount_new.js b/app/assets/javascripts/releases/mount_new.js
index 0a3f8b5e63b..efd82edcdf0 100644
--- a/app/assets/javascripts/releases/mount_new.js
+++ b/app/assets/javascripts/releases/mount_new.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import Vuex from 'vuex';
+import { createRefModule } from '../ref/stores';
import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
import createEditNewModule from './stores/modules/edit_new';
@@ -12,6 +13,7 @@ export default () => {
const store = createStore({
modules: {
editNew: createEditNewModule({ ...el.dataset, isExistingRelease: false }),
+ ref: createRefModule(),
},
});
diff --git a/app/assets/javascripts/releases/release_notification_service.js b/app/assets/javascripts/releases/release_notification_service.js
index a4f926d7561..20fbab3241e 100644
--- a/app/assets/javascripts/releases/release_notification_service.js
+++ b/app/assets/javascripts/releases/release_notification_service.js
@@ -1,5 +1,5 @@
-import { s__, sprintf } from '~/locale';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
+import { s__, __, sprintf } from '~/locale';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
const createReleaseSessionKey = (projectPath) => `createRelease:${projectPath}`;
@@ -21,3 +21,24 @@ export const popCreateReleaseNotification = (projectPath) => {
window.sessionStorage.removeItem(key);
}
};
+
+export const deleteReleaseSessionKey = (projectPath) => `deleteRelease:${projectPath}`;
+
+export const putDeleteReleaseNotification = (projectPath, releaseName) => {
+ window.sessionStorage.setItem(deleteReleaseSessionKey(projectPath), releaseName);
+};
+
+export const popDeleteReleaseNotification = (projectPath) => {
+ const key = deleteReleaseSessionKey(projectPath);
+ const deletedRelease = window.sessionStorage.getItem(key);
+
+ if (deletedRelease) {
+ createAlert({
+ message: sprintf(__('Release %{deletedRelease} has been successfully deleted.'), {
+ deletedRelease,
+ }),
+ variant: VARIANT_SUCCESS,
+ });
+ window.sessionStorage.removeItem(key);
+ }
+};
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 42ceed81c00..2e3cf3bf9b8 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
@@ -1,6 +1,6 @@
import { getTag } from '~/rest_api';
-import { createAlert } from '~/flash';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { createAlert } from '~/alert';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { s__ } from '~/locale';
import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql';
import deleteReleaseMutation from '~/releases/graphql/mutations/delete_release.mutation.graphql';
@@ -8,11 +8,8 @@ import createReleaseAssetLinkMutation from '~/releases/graphql/mutations/create_
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 { gqClient, convertOneReleaseGraphQLResponse } from '~/releases/util';
+import { putDeleteReleaseNotification } from '~/releases/release_notification_service';
import * as types from './mutation_types';
@@ -98,7 +95,7 @@ export const removeAssetLink = ({ commit }, linkIdToRemove) => {
export const receiveSaveReleaseSuccess = ({ commit }, urlToRedirectTo) => {
commit(types.RECEIVE_SAVE_RELEASE_SUCCESS);
- redirectTo(urlToRedirectTo);
+ redirectTo(urlToRedirectTo); // eslint-disable-line import/no-deprecated
};
export const saveRelease = ({ commit, dispatch, state }) => {
@@ -261,10 +258,7 @@ export const deleteRelease = ({ commit, getters, dispatch, state }) => {
})
.then((response) => checkForErrorsAsData(response, 'releaseDelete'))
.then(() => {
- window.sessionStorage.setItem(
- deleteReleaseSessionKey(state.projectPath),
- state.originalRelease.name,
- );
+ putDeleteReleaseNotification(state.projectPath, state.originalRelease.name);
return dispatch('receiveSaveReleaseSuccess', state.releasesPagePath);
})
.catch((error) => {
@@ -274,3 +268,9 @@ export const deleteRelease = ({ commit, getters, dispatch, state }) => {
});
});
};
+
+export const setSearching = ({ commit }) => commit(types.SET_SEARCHING);
+export const setCreating = ({ commit }) => commit(types.SET_CREATING);
+
+export const setExistingTag = ({ commit }) => commit(types.SET_EXISTING_TAG);
+export const setNewTag = ({ commit }) => commit(types.SET_NEW_TAG);
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/constants.js b/app/assets/javascripts/releases/stores/modules/edit_new/constants.js
new file mode 100644
index 00000000000..0f12f150525
--- /dev/null
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/constants.js
@@ -0,0 +1,4 @@
+export const SEARCH = 'SEARCH';
+export const CREATE = 'CREATE';
+export const EXISTING_TAG = 'EXISTING_TAG';
+export const NEW_TAG = 'NEW_TAG';
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 0d77095d099..edf6c81c9e9 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
@@ -2,6 +2,9 @@ import { isEmpty } from 'lodash';
import { s__ } from '~/locale';
import { hasContent } from '~/lib/utils/text_utility';
import { getDuplicateItemsFromArray } from '~/lib/utils/array_utility';
+import { validateTag, ValidationResult } from '~/lib/utils/ref_validator';
+import { i18n } from '~/releases/constants';
+import { SEARCH, CREATE, EXISTING_TAG, NEW_TAG } from './constants';
/**
* @param {Object} link The link to test
@@ -35,18 +38,21 @@ export const validationErrors = (state) => {
assets: {
links: {},
},
+ tagNameValidation: new ValidationResult(),
};
if (!state.release) {
return errors;
}
- if (!state.release.tagName?.trim?.().length) {
- errors.isTagNameEmpty = true;
+ if (!state.release.tagName || typeof state.release.tagName !== 'string') {
+ errors.tagNameValidation.addValidationError(i18n.tagNameIsRequiredMessage);
+ } else {
+ errors.tagNameValidation = validateTag(state.release.tagName);
}
if (state.existingRelease) {
- errors.existingRelease = true;
+ errors.tagNameValidation.addValidationError(i18n.tagIsAlredyInUseMessage);
}
// Each key of this object is a URL, and the value is an
@@ -164,10 +170,23 @@ export const releaseDeleteMutationVariables = (state) => ({
},
});
-export const formattedReleaseNotes = ({ includeTagNotes, release: { description }, tagNotes }) =>
- includeTagNotes && tagNotes
- ? `${description}\n\n### ${s__('Releases|Tag message')}\n\n${tagNotes}\n`
+export const formattedReleaseNotes = ({
+ includeTagNotes,
+ release: { description, tagMessage },
+ tagNotes,
+ showCreateFrom,
+}) => {
+ const notes = showCreateFrom ? tagMessage : tagNotes;
+ return includeTagNotes && notes
+ ? `${description}\n\n### ${s__('Releases|Tag message')}\n\n${notes}\n`
: description;
+};
export const releasedAtChanged = ({ originalReleasedAt, release }) =>
originalReleasedAt !== release.releasedAt;
+
+export const isSearching = ({ step }) => step === SEARCH;
+export const isCreating = ({ step }) => step === CREATE;
+
+export const isExistingTag = ({ tagStep }) => tagStep === EXISTING_TAG;
+export const isNewTag = ({ tagStep }) => tagStep === NEW_TAG;
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
index e52eccd6a21..fc450970cde 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
@@ -29,3 +29,9 @@ export const RECEIVE_TAG_NOTES_ERROR = 'RECEIVE_TAG_NOTES_ERROR';
export const UPDATE_INCLUDE_TAG_NOTES = 'UPDATE_INCLUDE_TAG_NOTES';
export const UPDATE_RELEASED_AT = 'UPDATE_RELEASED_AT';
+
+export const SET_SEARCHING = 'SET_SEARCHING';
+export const SET_CREATING = 'SET_CREATING';
+
+export const SET_EXISTING_TAG = 'SET_EXISTING_TAG';
+export const SET_NEW_TAG = 'SET_NEW_TAG';
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
index ccd168aafc9..7ff18245a80 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
@@ -1,6 +1,7 @@
import { uniqueId, cloneDeep } from 'lodash';
import { DEFAULT_ASSET_LINK_TYPE } from '../../../constants';
import * as types from './mutation_types';
+import { SEARCH, CREATE, EXISTING_TAG, NEW_TAG } from './constants';
const findReleaseLink = (release, id) => {
return release.assets.links.find((l) => l.id === id);
@@ -127,4 +128,17 @@ export default {
[types.UPDATE_RELEASED_AT](state, releasedAt) {
state.release.releasedAt = releasedAt;
},
+
+ [types.SET_SEARCHING](state) {
+ state.step = SEARCH;
+ },
+ [types.SET_CREATING](state) {
+ state.step = CREATE;
+ },
+ [types.SET_EXISTING_TAG](state) {
+ state.tagStep = EXISTING_TAG;
+ },
+ [types.SET_NEW_TAG](state) {
+ state.tagStep = NEW_TAG;
+ },
};
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/state.js b/app/assets/javascripts/releases/stores/modules/edit_new/state.js
index 3112becfa9e..7bd3968dd93 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/state.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/state.js
@@ -1,3 +1,5 @@
+import { SEARCH, EXISTING_TAG } from './constants';
+
export default ({
isExistingRelease,
projectId,
@@ -62,4 +64,6 @@ export default ({
includeTagNotes: false,
existingRelease: null,
originalReleasedAt: new Date(),
+ step: SEARCH,
+ tagStep: EXISTING_TAG,
});
diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js
index 10d7887c0b1..018a9352beb 100644
--- a/app/assets/javascripts/releases/util.js
+++ b/app/assets/javascripts/releases/util.js
@@ -135,5 +135,3 @@ export const convertOneReleaseGraphQLResponse = (response) => {
return { data: release };
};
-
-export const deleteReleaseSessionKey = (projectPath) => `deleteRelease:${projectPath}`;