diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
commit | 43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch) | |
tree | dceebdc68925362117480a5d672bcff122fb625b /app/assets/javascripts/releases | |
parent | 20c84b99005abd1c82101dfeff264ac50d2df211 (diff) |
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/releases')
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}`; |