From 10e74ca062e89ec353d1cf7dec2457839bf61911 Mon Sep 17 00:00:00 2001
From: Andrew Fontaine <afontaine@gitlab.com>
Date: Mon, 8 Jan 2024 09:26:06 -0500
Subject: [PATCH 1/5] Fetch tag notes on release refresh

We want to ensure that when a draft release is loaded from local
storage, that we fetch the tag details associated with the pre-loaded
tag. This ensures that the tag message is loaded and the "existing tag
in use" message is displayed if another release is created in the
mean-time.

Changelog: fixed
---
 .../stores/modules/edit_new/actions.js        |  6 +++-
 .../stores/modules/detail/actions_spec.js     | 32 +++++++++++++++++++
 2 files changed, 37 insertions(+), 1 deletion(-)

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 8bdfb057adca68..a2a19afde8bf02 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
@@ -326,7 +326,7 @@ export const clearDraftRelease = ({ getters }) => {
   }
 };
 
-export const loadDraftRelease = ({ commit, getters, state }) => {
+export const loadDraftRelease = ({ commit, getters, state, dispatch }) => {
   try {
     const release = window.localStorage.getItem(getters.localStorageKey);
     const createFrom = window.localStorage.getItem(getters.localStorageCreateFromKey);
@@ -340,6 +340,10 @@ export const loadDraftRelease = ({ commit, getters, state }) => {
           : state.originalReleasedAt,
       });
       commit(types.UPDATE_CREATE_FROM, JSON.parse(createFrom));
+
+      if (parsedRelease.tagName) {
+        dispatch('fetchTagNotes', parsedRelease.tagName);
+      }
     } else {
       commit(types.INITIALIZE_EMPTY_RELEASE);
     }
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index a55b6cdef92f85..8182b6f5698d88 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -121,6 +121,38 @@ describe('Release edit/new actions', () => {
           JSON.stringify(createFrom),
         );
 
+        return testAction({
+          action: actions.loadDraftRelease,
+          state,
+          expectedMutations: [
+            { type: types.INITIALIZE_RELEASE, payload: release },
+            { type: types.UPDATE_CREATE_FROM, payload: createFrom },
+          ],
+          expectedActions: [{ type: 'fetchTagNotes', payload: release.tagName }],
+        });
+      });
+
+      it('with no tag name, does not fetch tag information', () => {
+        const release = {
+          tagName: '',
+          tagMessage: 'hello',
+          name: '',
+          description: '',
+          milestones: [],
+          groupMilestones: [],
+          releasedAt: new Date(),
+          assets: {
+            links: [],
+          },
+        };
+        const createFrom = 'main';
+
+        window.localStorage.setItem(`${state.projectPath}/release/new`, JSON.stringify(release));
+        window.localStorage.setItem(
+          `${state.projectPath}/release/new/createFrom`,
+          JSON.stringify(createFrom),
+        );
+
         return testAction({
           action: actions.loadDraftRelease,
           state,
-- 
GitLab


From 9b95aa8dcdfcf0c283c7ba2d5ed0d4204d34047d Mon Sep 17 00:00:00 2001
From: Andrew Fontaine <afontaine@gitlab.com>
Date: Mon, 8 Jan 2024 09:40:12 -0500
Subject: [PATCH 2/5] Disable submit button while fetching tag notes

When creating releases, we can only include annotated tag notes after we
have fetched the tag details, so we should disable the button until the
notes have successfully loaded.
---
 .../releases/components/app_edit_new.vue      |  3 ++-
 .../releases/stores/modules/edit_new/state.js |  1 +
 .../releases/components/app_edit_new_spec.js  | 19 +++++++++++++++++++
 3 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index 228007dd7d6088..6fce9b4a12953c 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -55,6 +55,7 @@ export default {
       'groupId',
       'groupMilestonesAvailable',
       'tagNotes',
+      'isFetchingTagNotes',
     ]),
     ...mapGetters('editNew', ['isValid', 'formattedReleaseNotes']),
     showForm() {
@@ -113,7 +114,7 @@ export default {
       return this.isExistingRelease ? __('Save changes') : __('Create release');
     },
     isFormSubmissionDisabled() {
-      return this.isUpdatingRelease || !this.isValid;
+      return this.isUpdatingRelease || !this.isValid || this.isFetchingTagNotes;
     },
     milestoneComboboxExtraLinks() {
       return [
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 7bd3968dd93e3c..a02949568b223a 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/state.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/state.js
@@ -61,6 +61,7 @@ export default ({
   updateError: null,
 
   tagNotes: '',
+  isFetchingTagNotes: false,
   includeTagNotes: false,
   existingRelease: null,
   originalReleasedAt: new Date(),
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 15436832be8d24..90f31dca232020 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -319,6 +319,25 @@ describe('Release edit/new component', () => {
         expect(actions.saveRelease).not.toHaveBeenCalled();
       });
     });
+
+    describe('when tag notes are loading', () => {
+      beforeEach(async () => {
+        await factory({
+          store: {
+            modules: {
+              editNew: {
+                state: {
+                  isFetchingTagNotes: true,
+                },
+              },
+            },
+          },
+        });
+      });
+      it('renders the submit button as disabled', () => {
+        expect(findSubmitButton().attributes('disabled')).toBeDefined();
+      });
+    });
   });
 
   describe('delete', () => {
-- 
GitLab


From 0047b989018e40c84cb3797e979af84b21529219 Mon Sep 17 00:00:00 2001
From: Andrew Fontaine <afontaine@gitlab.com>
Date: Tue, 9 Jan 2024 15:11:56 -0500
Subject: [PATCH 3/5] Correctly include new tag notes on new release

This was not updated when the create tag flow was redesigned.

Now we check to ensure we are on the NEW_TAG step and include the added
tag notes (if they exist).

Changelog: fixed
---
 .../releases/stores/modules/edit_new/getters.js   | 12 +++++-------
 .../stores/modules/detail/getters_spec.js         | 15 +++++++--------
 2 files changed, 12 insertions(+), 15 deletions(-)

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 0b37c2b81d1a3e..d1cde8b9029f70 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
@@ -170,13 +170,11 @@ export const releaseDeleteMutationVariables = (state) => ({
   },
 });
 
-export const formattedReleaseNotes = ({
-  includeTagNotes,
-  release: { description, tagMessage },
-  tagNotes,
-  showCreateFrom,
-}) => {
-  const notes = showCreateFrom ? tagMessage : tagNotes;
+export const formattedReleaseNotes = (
+  { includeTagNotes, release: { description, tagMessage }, tagNotes },
+  { isNewTag },
+) => {
+  const notes = isNewTag ? tagMessage : tagNotes;
   return includeTagNotes && notes
     ? `${description}\n\n### ${s__('Releases|Tag message')}\n\n${notes}\n`
     : description;
diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js
index 24490e19296f4d..30a3c78641c78f 100644
--- a/spec/frontend/releases/stores/modules/detail/getters_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -424,7 +424,7 @@ describe('Release edit/new getters', () => {
 
   describe('formattedReleaseNotes', () => {
     it.each`
-      description        | includeTagNotes | tagNotes       | included | showCreateFrom
+      description        | includeTagNotes | tagNotes       | included | isNewTag
       ${'release notes'} | ${true}         | ${'tag notes'} | ${true}  | ${false}
       ${'release notes'} | ${true}         | ${''}          | ${false} | ${false}
       ${'release notes'} | ${false}        | ${'tag notes'} | ${false} | ${false}
@@ -432,25 +432,24 @@ describe('Release edit/new getters', () => {
       ${'release notes'} | ${true}         | ${''}          | ${false} | ${true}
       ${'release notes'} | ${false}        | ${'tag notes'} | ${false} | ${true}
     `(
-      'should include tag notes=$included when includeTagNotes=$includeTagNotes and tagNotes=$tagNotes and showCreateFrom=$showCreateFrom',
-      ({ description, includeTagNotes, tagNotes, included, showCreateFrom }) => {
+      'should include tag notes=$included when includeTagNotes=$includeTagNotes and tagNotes=$tagNotes and isNewTag=$isNewTag',
+      ({ description, includeTagNotes, tagNotes, included, isNewTag }) => {
         let state;
 
-        if (showCreateFrom) {
+        if (isNewTag) {
           state = {
             release: { description, tagMessage: tagNotes },
             includeTagNotes,
-            showCreateFrom,
           };
         } else {
-          state = { release: { description }, includeTagNotes, tagNotes, showCreateFrom };
+          state = { release: { description }, includeTagNotes, tagNotes };
         }
 
         const text = `### ${s__('Releases|Tag message')}\n\n${tagNotes}\n`;
         if (included) {
-          expect(getters.formattedReleaseNotes(state)).toContain(text);
+          expect(getters.formattedReleaseNotes(state, { isNewTag })).toContain(text);
         } else {
-          expect(getters.formattedReleaseNotes(state)).not.toContain(text);
+          expect(getters.formattedReleaseNotes(state, { isNewTag })).not.toContain(text);
         }
       },
     );
-- 
GitLab


From 7b6b93d6ac904739aa75faca40110bd2c05521e3 Mon Sep 17 00:00:00 2001
From: Andrew Fontaine <afontaine@gitlab.com>
Date: Tue, 9 Jan 2024 15:46:59 -0500
Subject: [PATCH 4/5] Properly rehydrate creating tag form in release

Now if a draft tag creation is ongoing, we can properly rehydrate that
information and still include the tag message when creating the release.

Changelog: fixed
---
 .../releases/components/tag_field_new.vue     |  3 +++
 .../stores/modules/edit_new/actions.js        |  9 +++++++--
 .../stores/modules/detail/actions_spec.js     | 20 +++++++++++++++++++
 3 files changed, 30 insertions(+), 2 deletions(-)

diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index 04f3d73235bc26..ee6f3efb516d92 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -43,6 +43,9 @@ export default {
       return this.newTagName ? this.$options.i18n.createTag : this.$options.i18n.typeNew;
     },
   },
+  mounted() {
+    this.newTagName = this.release.tagName;
+  },
   methods: {
     ...mapActions('editNew', [
       'setSearching',
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 a2a19afde8bf02..a0d782a02a16a4 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
@@ -3,6 +3,7 @@ import { getTag } from '~/rest_api';
 import { createAlert } from '~/alert';
 import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
 import AccessorUtilities from '~/lib/utils/accessor';
+import { HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
 import { s__ } from '~/locale';
 import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql';
 import deleteReleaseMutation from '~/releases/graphql/mutations/delete_release.mutation.graphql';
@@ -245,7 +246,7 @@ export const updateRelease = async ({ commit, dispatch, state, getters }) => {
   }
 };
 
-export const fetchTagNotes = ({ commit, state }, tagName) => {
+export const fetchTagNotes = ({ commit, state, dispatch }, tagName) => {
   commit(types.REQUEST_TAG_NOTES);
 
   return getTag(state.projectId, tagName)
@@ -253,11 +254,15 @@ export const fetchTagNotes = ({ commit, state }, tagName) => {
       commit(types.RECEIVE_TAG_NOTES_SUCCESS, data);
     })
     .catch((error) => {
+      if (error?.response?.status === HTTP_STATUS_NOT_FOUND) {
+        commit(types.RECEIVE_TAG_NOTES_SUCCESS, {});
+        return Promise.all([dispatch('setNewTag'), dispatch('setCreating')]);
+      }
       createAlert({
         message: s__('Release|Unable to fetch the tag notes.'),
       });
 
-      commit(types.RECEIVE_TAG_NOTES_ERROR, error);
+      return commit(types.RECEIVE_TAG_NOTES_ERROR, error);
     });
 };
 
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 8182b6f5698d88..4dc55c12464e01 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -4,6 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
 import { useLocalStorageSpy } from 'helpers/local_storage_helper';
 import { getTag } from '~/api/tags_api';
 import { createAlert } from '~/alert';
+import { HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
 import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
 import AccessorUtilities from '~/lib/utils/accessor';
 import { s__ } from '~/locale';
@@ -1020,6 +1021,7 @@ describe('Release edit/new actions', () => {
 
       expect(getTag).toHaveBeenCalledWith(state.projectId, tagName);
     });
+
     it('creates an alert on error', async () => {
       error = new Error();
       getTag.mockRejectedValue(error);
@@ -1039,5 +1041,23 @@ describe('Release edit/new actions', () => {
       });
       expect(getTag).toHaveBeenCalledWith(state.projectId, tagName);
     });
+
+    it('assumes creating a tag on 404', async () => {
+      error = { response: { status: HTTP_STATUS_NOT_FOUND } };
+      getTag.mockRejectedValue(error);
+
+      await testAction({
+        action: actions.fetchTagNotes,
+        payload: tagName,
+        state,
+        expectedMutations: [
+          { type: types.REQUEST_TAG_NOTES },
+          { type: types.RECEIVE_TAG_NOTES_SUCCESS, payload: {} },
+        ],
+        expectedActions: [{ type: 'setNewTag' }, { type: 'setCreating' }],
+      });
+
+      expect(getTag).toHaveBeenCalledWith(state.projectId, tagName);
+    });
   });
 });
-- 
GitLab


From 51cea57cd8f611a4993cb263d4adf687ee06001d Mon Sep 17 00:00:00 2001
From: Andrew Fontaine <afontaine@gitlab.com>
Date: Wed, 10 Jan 2024 09:42:29 -0500
Subject: [PATCH 5/5] Null-check release tag name on mount

---
 app/assets/javascripts/releases/components/tag_field_new.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index ee6f3efb516d92..370e920be02237 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -44,7 +44,7 @@ export default {
     },
   },
   mounted() {
-    this.newTagName = this.release.tagName;
+    this.newTagName = this.release?.tagName || '';
   },
   methods: {
     ...mapActions('editNew', [
-- 
GitLab