From 205cc4bff46e2f99dde7ed68b7c10dc4916d6e4c Mon Sep 17 00:00:00 2001
From: Sascha Eggenberger <seggenberger@gitlab.com>
Date: Mon, 9 Sep 2024 08:24:15 +0000
Subject: [PATCH] Diff threads: Align toggle function with regular threads

Changelog: changed
---
 .../components/diff_discussion_header.vue     | 29 ++++-----
 .../notes/components/discussion_notes.vue     |  2 +-
 .../notes/components/note_header.vue          | 41 +-----------
 .../components/toggle_replies_widget.vue      | 10 ++-
 app/assets/stylesheets/pages/notes.scss       |  5 +-
 locale/gitlab.pot                             |  6 --
 ...diff_notes_and_discussions_resolve_spec.rb | 11 ++--
 .../user_sees_discussions_navigation_spec.rb  |  2 +-
 .../components/diff_discussion_header_spec.js | 43 ++++++++++++-
 .../notes/components/note_header_spec.js      | 62 -------------------
 .../components/toggle_replies_widget_spec.js  |  4 ++
 .../work_item_note_replying_spec.js.snap      |  1 -
 12 files changed, 77 insertions(+), 139 deletions(-)

diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
index 29f847ae526bb..662e278dacfdc 100644
--- a/app/assets/javascripts/notes/components/diff_discussion_header.vue
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -9,6 +9,7 @@ import { s__, __, sprintf } from '~/locale';
 import { FILE_DIFF_POSITION_TYPE } from '~/diffs/constants';
 import NoteEditedText from './note_edited_text.vue';
 import NoteHeader from './note_header.vue';
+import ToggleRepliesWidget from './toggle_replies_widget.vue';
 
 export default {
   name: 'DiffDiscussionHeader',
@@ -17,6 +18,7 @@ export default {
     GlAvatarLink,
     NoteEditedText,
     NoteHeader,
+    ToggleRepliesWidget,
   },
   directives: {
     SafeHtml,
@@ -95,6 +97,9 @@ export default {
     toggleClass() {
       return this.discussion.expanded ? 'expanded' : 'collapsed';
     },
+    replies() {
+      return this.notes.filter((note) => !note.system);
+    },
   },
   methods: {
     ...mapActions(['toggleDiscussion']),
@@ -107,7 +112,7 @@ export default {
 
 <template>
   <div class="discussion-header gl-flex gl-items-center">
-    <div v-once class="timeline-avatar gl-flex-shrink gl-shrink-0 gl-self-start">
+    <div v-once class="timeline-avatar gl-shrink-0 gl-self-start">
       <gl-avatar-link
         v-if="author"
         :href="author.path"
@@ -119,14 +124,7 @@ export default {
       </gl-avatar-link>
     </div>
     <div class="timeline-content gl-ml-3 gl-w-full" :class="toggleClass">
-      <note-header
-        :author="author"
-        :created-at="firstNote.created_at"
-        :note-id="firstNote.id"
-        :include-toggle="true"
-        :expanded="discussion.expanded"
-        @toggleHandler="toggleDiscussionHandler"
-      >
+      <note-header :author="author" :created-at="firstNote.created_at" :note-id="firstNote.id">
         <span v-safe-html="headerText"></span>
       </note-header>
       <note-edited-text
@@ -134,14 +132,13 @@ export default {
         :edited-at="discussion.resolved_at"
         :edited-by="discussion.resolved_by"
         :action-text="resolvedText"
-        class-name="discussion-headline-light js-discussion-headline gl-pl-3"
+        class-name="discussion-headline-light js-discussion-headline gl-mt-1 gl-pl-3"
       />
-      <note-edited-text
-        v-else-if="lastUpdatedAt"
-        :edited-at="lastUpdatedAt"
-        :edited-by="lastUpdatedBy"
-        :action-text="__('Last updated')"
-        class-name="discussion-headline-light js-discussion-headline gl-pl-3"
+      <toggle-replies-widget
+        :collapsed="!discussion.expanded"
+        :replies="replies"
+        class="gl-border-t -gl-mx-3 -gl-mb-3 gl-mt-4 !gl-border-x-0 !gl-border-b-0 gl-border-t-subtle"
+        @toggle="toggleDiscussionHandler"
       />
     </div>
   </div>
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 3dd0799ca402c..c9194734f4350 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -158,7 +158,7 @@ export default {
               :edited-at="discussion.resolved_at"
               :edited-by="discussion.resolved_by"
               :action-text="resolvedText"
-              class-name="discussion-headline-light js-discussion-headline discussion-resolved-text gl-mb-2 gl-ml-3"
+              class-name="discussion-headline-light js-discussion-headline discussion-resolved-text -gl-mt-2 gl-mb-3 gl-ml-3"
             />
           </template>
           <template #avatar-badge>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 491ed9634801f..36bb5c407acf6 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,10 +1,10 @@
 <script>
-import { GlIcon, GlBadge, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlBadge, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
 // eslint-disable-next-line no-restricted-imports
 import { mapActions } from 'vuex';
 import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
 import { TYPE_ACTIVITY, TYPE_COMMENT } from '~/import/constants';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
 import ImportedBadge from '~/vue_shared/components/imported_badge.vue';
 import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
 
@@ -12,7 +12,6 @@ export default {
   components: {
     ImportedBadge,
     TimeAgoTooltip,
-    GlIcon,
     GlBadge,
     GlLoadingIcon,
   },
@@ -45,16 +44,6 @@ export default {
       required: false,
       default: '',
     },
-    includeToggle: {
-      type: Boolean,
-      required: false,
-      default: false,
-    },
-    expanded: {
-      type: Boolean,
-      required: false,
-      default: true,
-    },
     showSpinner: {
       type: Boolean,
       required: false,
@@ -98,9 +87,6 @@ export default {
     authorHref() {
       return this.author.path || this.author.webUrl;
     },
-    toggleChevronIconName() {
-      return this.expanded ? 'chevron-up' : 'chevron-down';
-    },
     noteTimestampLink() {
       if (this.noteUrl) return this.noteUrl;
 
@@ -144,9 +130,6 @@ export default {
   },
   methods: {
     ...mapActions(['setTargetNoteHash']),
-    handleToggle() {
-      this.$emit('toggleHandler');
-    },
     updateTargetNoteHash() {
       if (this.$store) {
         this.setTargetNoteHash(this.noteTimestampLink);
@@ -161,31 +144,11 @@ export default {
       this.isUsernameLinkHovered = false;
     },
   },
-  i18n: {
-    showThread: __('Show thread'),
-    hideThread: __('Hide thread'),
-  },
 };
 </script>
 
 <template>
   <div class="note-header-info">
-    <div v-if="includeToggle" ref="discussionActions" class="discussion-actions">
-      <button
-        class="note-action-button discussion-toggle-button js-vue-toggle-button"
-        type="button"
-        data-testid="thread-toggle"
-        @click="handleToggle"
-      >
-        <gl-icon ref="chevronIcon" :name="toggleChevronIconName" />
-        <template v-if="expanded">
-          {{ $options.i18n.hideThread }}
-        </template>
-        <template v-else>
-          {{ $options.i18n.showThread }}
-        </template>
-      </button>
-    </div>
     <template v-if="hasAuthor">
       <span
         v-if="emailParticipant"
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index 667cec8a4313b..6c15f94ed4e6d 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -66,10 +66,11 @@ export default {
     <gl-button
       ref="toggle"
       class="gl-my-2 gl-mr-3 !gl-p-0"
-      :class="{ '!gl-text-blue-600': !collapsed }"
+      :class="{ '!gl-text-link': !collapsed }"
       category="tertiary"
       :icon="buttonIcon"
       :aria-label="buttonLabel"
+      data-testid="replies-toggle"
       @click="toggle"
     />
     <template v-if="collapsed">
@@ -92,7 +93,12 @@ export default {
           </gl-avatar-link>
         </template>
       </gl-avatars-inline>
-      <gl-button class="gl-mr-2" variant="link" data-testid="expand-replies-button" @click="toggle">
+      <gl-button
+        class="gl-mr-2 gl-self-center"
+        variant="link"
+        data-testid="expand-replies-button"
+        @click="toggle"
+      >
         {{ n__('%d reply', '%d replies', replies.length) }}
       </gl-button>
       <gl-sprintf :message="$options.i18n.lastReplyBy">
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 8eed5c4a694f1..5f0fac58057fa 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -516,6 +516,8 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
     }
 
     &.expanded {
+      // Render border invisible
+      border-bottom: 1px solid var(--gl-background-color-subtle);
       border-bottom-left-radius: 0;
       border-bottom-right-radius: 0;
     }
@@ -598,9 +600,6 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
   }
 }
 
-
-
-
 .user-activity-content {
   position: relative;
 
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5667dc3f8b89f..6006559e00c38 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -27099,9 +27099,6 @@ msgstr ""
 msgid "Hide sidebar"
 msgstr ""
 
-msgid "Hide thread"
-msgstr ""
-
 msgid "Hide tooltips or popovers"
 msgstr ""
 
@@ -51180,9 +51177,6 @@ msgstr ""
 msgid "Show the Open list"
 msgstr ""
 
-msgid "Show thread"
-msgstr ""
-
 msgid "Show whitespace changes"
 msgstr ""
 
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index 713f8abb94bce..caaaca9f1d088 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -98,13 +98,13 @@
           end
 
           it 'shows resolved thread when toggled' do
-            find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click
+            find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] [data-testid='replies-toggle']").click
 
             expect(page.find(".timeline-content #note_#{note.id}")).to be_visible
           end
 
           it 'renders tables in lazy-loaded resolved diff dicussions' do
-            find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click
+            find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] [data-testid='replies-toggle']").click
 
             wait_for_requests
 
@@ -133,7 +133,7 @@
 
         describe 'reply form' do
           before do
-            click_button _('Show thread')
+            click_button _('Expand replies')
           end
 
           it 'allows user to comment' do
@@ -211,10 +211,11 @@
 
       it 'allows user to quickly scroll to next unresolved thread', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410109' do
         page.within(first('.discussions-counter')) do
-          page.find('.discussion-next-btn').click
+          click_button _('Next unresolved thread')
         end
 
         expect(page).to have_button('Resolve thread', visible: true)
+
         expect(page.evaluate_script("window.pageYOffset")).to be > 0
       end
 
@@ -350,7 +351,7 @@
       it 'displays next thread even if hidden' do
         page.all('.note-discussion', count: 2).each do |discussion|
           page.within discussion do
-            click_button _('Hide thread')
+            click_button text: _('Collapse replies')
           end
         end
 
diff --git a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
index add42914521a0..1c63dfde8aac5 100644
--- a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
@@ -115,7 +115,7 @@
       context 'with collapsed threads' do
         before do
           page.within(first_discussion_selector) do
-            click_button 'Hide thread'
+            click_button text: 'Collapse replies'
           end
         end
 
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
index 123d53de3f3bc..e437e01d6a61c 100644
--- a/spec/frontend/notes/components/diff_discussion_header_spec.js
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
-
 import { nextTick } from 'vue';
 import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue';
+import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
 import createStore from '~/notes/stores';
 
 import mockDiffFile from 'jest/diffs/mock_data/diff_discussions';
@@ -13,7 +13,7 @@ describe('diff_discussion_header component', () => {
   let wrapper;
 
   const createComponent = ({ propsData = {} } = {}) => {
-    wrapper = shallowMount(diffDiscussionHeader, {
+    wrapper = shallowMountExtended(diffDiscussionHeader, {
       store,
       propsData: {
         discussion: discussionMock,
@@ -166,4 +166,41 @@ describe('diff_discussion_header component', () => {
       });
     });
   });
+
+  describe('replies toggle', () => {
+    it('renders replies toggle', () => {
+      createComponent();
+      expect(wrapper.findComponent(ToggleRepliesWidget).exists()).toBe(true);
+      expect(wrapper.findComponent(ToggleRepliesWidget).props()).toMatchObject({
+        collapsed: false,
+        replies: discussionMock.notes,
+      });
+    });
+
+    it('skips system notes', () => {
+      createComponent({
+        propsData: {
+          discussion: {
+            ...discussionMock,
+            notes: [...discussionMock.notes, { system: true }],
+          },
+        },
+      });
+      expect(wrapper.findComponent(ToggleRepliesWidget).props('replies')).toStrictEqual(
+        discussionMock.notes,
+      );
+    });
+
+    it('passes collapsed state', () => {
+      createComponent({
+        propsData: {
+          discussion: {
+            ...discussionMock,
+            expanded: false,
+          },
+        },
+      });
+      expect(wrapper.findComponent(ToggleRepliesWidget).props('collapsed')).toBe(true);
+    });
+  });
 });
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index f2e6a54d01bae..291644e273222 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -14,9 +14,6 @@ const actions = {
 describe('NoteHeader component', () => {
   let wrapper;
 
-  const findActionsWrapper = () => wrapper.findComponent({ ref: 'discussionActions' });
-  const findToggleThreadButton = () => wrapper.findByTestId('thread-toggle');
-  const findChevronIcon = () => wrapper.findComponent({ ref: 'chevronIcon' });
   const findActionText = () => wrapper.findComponent({ ref: 'actionText' });
   const findTimestampLink = () => wrapper.findComponent({ ref: 'noteTimestampLink' });
   const findTimestamp = () => wrapper.findComponent({ ref: 'noteTimestamp' });
@@ -61,65 +58,6 @@ describe('NoteHeader component', () => {
     });
   };
 
-  it('does not render discussion actions when includeToggle is false', () => {
-    createComponent({
-      includeToggle: false,
-    });
-
-    expect(findActionsWrapper().exists()).toBe(false);
-  });
-
-  describe('when includes a toggle', () => {
-    it('renders discussion actions', () => {
-      createComponent({
-        includeToggle: true,
-      });
-
-      expect(findActionsWrapper().exists()).toBe(true);
-    });
-
-    it('emits toggleHandler event on button click', () => {
-      createComponent({
-        includeToggle: true,
-      });
-
-      wrapper.find('.note-action-button').trigger('click');
-      expect(wrapper.emitted('toggleHandler')).toBeDefined();
-      expect(wrapper.emitted('toggleHandler')).toHaveLength(1);
-    });
-
-    it('has chevron-up icon if expanded prop is true', () => {
-      createComponent({
-        includeToggle: true,
-        expanded: true,
-      });
-
-      expect(findChevronIcon().props('name')).toBe('chevron-up');
-    });
-
-    it('has chevron-down icon if expanded prop is false', () => {
-      createComponent({
-        includeToggle: true,
-        expanded: false,
-      });
-
-      expect(findChevronIcon().props('name')).toBe('chevron-down');
-    });
-
-    it.each`
-      text                          | expanded
-      ${NoteHeader.i18n.showThread} | ${false}
-      ${NoteHeader.i18n.hideThread} | ${true}
-    `('toggle button has text $text is expanded is $expanded', ({ text, expanded }) => {
-      createComponent({
-        includeToggle: true,
-        expanded,
-      });
-
-      expect(findToggleThreadButton().text()).toBe(text);
-    });
-  });
-
   it('renders an author link if author is passed to props', () => {
     createComponent({ author });
 
diff --git a/spec/frontend/notes/components/toggle_replies_widget_spec.js b/spec/frontend/notes/components/toggle_replies_widget_spec.js
index d72fcd49ba0af..5d47ae1237dea 100644
--- a/spec/frontend/notes/components/toggle_replies_widget_spec.js
+++ b/spec/frontend/notes/components/toggle_replies_widget_spec.js
@@ -37,6 +37,8 @@ describe('toggle replies widget for notes', () => {
 
     it('renders collapsed state elements', () => {
       expect(findExpandToggleButton().exists()).toBe(true);
+      expect(findExpandToggleButton().props('icon')).toBe('chevron-right');
+      expect(findExpandToggleButton().attributes('aria-label')).toBe('Expand replies');
       expect(findAvatars().props('avatars')).toHaveLength(3);
       expect(findRepliesButton().exists()).toBe(true);
       expect(wrapper.text()).toContain('Last reply by');
@@ -64,6 +66,8 @@ describe('toggle replies widget for notes', () => {
 
     it('renders expanded state elements', () => {
       expect(findCollapseToggleButton().exists()).toBe(true);
+      expect(findCollapseToggleButton().props('icon')).toBe('chevron-down');
+      expect(findCollapseToggleButton().attributes('aria-label')).toBe('Collapse replies');
     });
 
     it('emits "toggle" event when collapse toggle button is clicked', () => {
diff --git a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
index af930f565090c..796a087d869ea 100644
--- a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
+++ b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
@@ -5,7 +5,6 @@ exports[`Work Item Note Replying should have the note body and header 1`] = `
   actiontext=""
   author="[object Object]"
   emailparticipant=""
-  expanded="true"
   noteabletype=""
   noteurl=""
   showspinner="true"
-- 
GitLab