From b799e33c0a376d35e031bd292a780163b6732add Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Fri, 5 Dec 2025 12:51:03 -0700 Subject: [PATCH 01/30] Draft of showing timeline comments on commit --- .../rapid_diffs/app/commit/timeline.vue | 24 +++++++++++++++++++ .../javascripts/rapid_diffs/commit_app.js | 24 +++++++++++++++++++ .../rapid_diffs/app_component.html.haml | 1 + app/components/rapid_diffs/app_component.rb | 1 + .../commit_app_component.html.haml | 2 ++ 5 files changed, 52 insertions(+) create mode 100644 app/assets/javascripts/rapid_diffs/app/commit/timeline.vue diff --git a/app/assets/javascripts/rapid_diffs/app/commit/timeline.vue b/app/assets/javascripts/rapid_diffs/app/commit/timeline.vue new file mode 100644 index 00000000000000..99d513bef9cd98 --- /dev/null +++ b/app/assets/javascripts/rapid_diffs/app/commit/timeline.vue @@ -0,0 +1,24 @@ + + + diff --git a/app/assets/javascripts/rapid_diffs/commit_app.js b/app/assets/javascripts/rapid_diffs/commit_app.js index 268f889329dd3b..142f3bb27bf93c 100644 --- a/app/assets/javascripts/rapid_diffs/commit_app.js +++ b/app/assets/javascripts/rapid_diffs/commit_app.js @@ -1,3 +1,4 @@ +import Vue from 'vue'; import { pinia } from '~/pinia/instance'; import { RapidDiffsFacade } from '~/rapid_diffs/app'; import { adapters } from '~/rapid_diffs/app/adapter_configs/commit'; @@ -8,6 +9,28 @@ import { s__ } from '~/locale'; import { initNewDiscussionToggle } from '~/rapid_diffs/app/init_new_discussions_toggle'; import { useDiffsView } from '~/rapid_diffs/stores/diffs_view'; import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; +import CommitTimeline from '~/rapid_diffs/app/commit/timeline.vue'; + +function initTimeline(appData) { + const timelineContainer = document.querySelector('[data-commit-timeline]'); + + if (!timelineContainer) return; + + // eslint-disable-next-line no-new + new Vue({ + el: timelineContainer, + pinia, + provide: { + userPermissions: appData.userPermissions, + endpoints: { + createNote: appData.discussionsEndpoint, + }, + }, + render(h) { + return h(CommitTimeline); + }, + }); +} class CommitRapidDiffsApp extends RapidDiffsFacade { adapterConfig = adapters; @@ -38,6 +61,7 @@ class CommitRapidDiffsApp extends RapidDiffsFacade { } = await axios.get(this.appData.discussionsEndpoint); useDiffDiscussions(pinia).setInitialDiscussions(discussions); initNewDiscussionToggle(this.root); + initTimeline(this.appData); } catch (error) { createAlert({ message: s__('RapidDiffs|Failed to load discussions. Try to reload the page.'), diff --git a/app/components/rapid_diffs/app_component.html.haml b/app/components/rapid_diffs/app_component.html.haml index ff5f84fa98a96b..45635b207fe8ca 100644 --- a/app/components/rapid_diffs/app_component.html.haml +++ b/app/components/rapid_diffs/app_component.html.haml @@ -52,3 +52,4 @@ }) .rd-app-diffs-loading{ data: { list_loading: true }, hidden: !diffs_stream_url } = render Pajamas::SpinnerComponent.new(size: :lg) + = after_diffs_list diff --git a/app/components/rapid_diffs/app_component.rb b/app/components/rapid_diffs/app_component.rb index 3c745bd9af176d..f68d08a526b61f 100644 --- a/app/components/rapid_diffs/app_component.rb +++ b/app/components/rapid_diffs/app_component.rb @@ -4,6 +4,7 @@ module RapidDiffs class AppComponent < ViewComponent::Base renders_one :before_diffs_list renders_one :diffs_list + renders_one :after_diffs_list attr_reader :presenter diff --git a/app/components/rapid_diffs/commit_app_component.html.haml b/app/components/rapid_diffs/commit_app_component.html.haml index 89be627d07503a..aba04642917039 100644 --- a/app/components/rapid_diffs/commit_app_component.html.haml +++ b/app/components/rapid_diffs/commit_app_component.html.haml @@ -3,3 +3,5 @@ - c.with_before_diffs_list do %button.rd-new-discussion-toggle{ 'aria-label': s_('RapidDiffs|Add comment'), data: { new_discussion_toggle: true, click: 'newDiscussion' }, hidden: 'true' } = helpers.sprite_icon('comment', size: 12) + - c.with_after_diffs_list do + %div{ data: { commit_timeline: true } } -- GitLab From 076c95450c9f6ba6f6d63321e7defd6bb13ac1de Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Fri, 5 Dec 2025 18:03:47 -0700 Subject: [PATCH 02/30] Test Rapid Diffs commit view timeline rendering --- .../rapid_diffs/app/init_timeline.js | 23 ++++++++ .../javascripts/rapid_diffs/commit_app.js | 24 +------- .../rapid_diffs/app_component_spec.rb | 9 +++ .../rapid_diffs/commit_app_component_spec.rb | 7 +++ .../rapid_diffs/app/commit/timeline_spec.js | 55 +++++++++++++++++++ spec/frontend/rapid_diffs/commit_app_spec.js | 24 ++++++++ 6 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 app/assets/javascripts/rapid_diffs/app/init_timeline.js create mode 100644 spec/frontend/rapid_diffs/app/commit/timeline_spec.js diff --git a/app/assets/javascripts/rapid_diffs/app/init_timeline.js b/app/assets/javascripts/rapid_diffs/app/init_timeline.js new file mode 100644 index 00000000000000..f59a0ba20356f2 --- /dev/null +++ b/app/assets/javascripts/rapid_diffs/app/init_timeline.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import { pinia } from '~/pinia/instance'; +import CommitTimeline from './commit/timeline.vue'; + +export function initTimeline(appData) { + const timelineContainer = document.querySelector('[data-commit-timeline]'); + if (!timelineContainer) return; + + // eslint-disable-next-line no-new + new Vue({ + el: timelineContainer, + pinia, + provide: { + userPermissions: appData.userPermissions, + endpoints: { + createNote: appData.discussionsEndpoint, + }, + }, + render(h) { + return h(CommitTimeline); + }, + }); +} diff --git a/app/assets/javascripts/rapid_diffs/commit_app.js b/app/assets/javascripts/rapid_diffs/commit_app.js index 142f3bb27bf93c..ab3b7346e85cba 100644 --- a/app/assets/javascripts/rapid_diffs/commit_app.js +++ b/app/assets/javascripts/rapid_diffs/commit_app.js @@ -1,4 +1,3 @@ -import Vue from 'vue'; import { pinia } from '~/pinia/instance'; import { RapidDiffsFacade } from '~/rapid_diffs/app'; import { adapters } from '~/rapid_diffs/app/adapter_configs/commit'; @@ -9,28 +8,7 @@ import { s__ } from '~/locale'; import { initNewDiscussionToggle } from '~/rapid_diffs/app/init_new_discussions_toggle'; import { useDiffsView } from '~/rapid_diffs/stores/diffs_view'; import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; -import CommitTimeline from '~/rapid_diffs/app/commit/timeline.vue'; - -function initTimeline(appData) { - const timelineContainer = document.querySelector('[data-commit-timeline]'); - - if (!timelineContainer) return; - - // eslint-disable-next-line no-new - new Vue({ - el: timelineContainer, - pinia, - provide: { - userPermissions: appData.userPermissions, - endpoints: { - createNote: appData.discussionsEndpoint, - }, - }, - render(h) { - return h(CommitTimeline); - }, - }); -} +import { initTimeline } from '~/rapid_diffs/app/init_timeline'; class CommitRapidDiffsApp extends RapidDiffsFacade { adapterConfig = adapters; diff --git a/spec/components/rapid_diffs/app_component_spec.rb b/spec/components/rapid_diffs/app_component_spec.rb index 5607b8a9b8cca8..629edeb9973a48 100644 --- a/spec/components/rapid_diffs/app_component_spec.rb +++ b/spec/components/rapid_diffs/app_component_spec.rb @@ -160,6 +160,15 @@ expect(result).to have_text('custom_list') end + it "renders after_diffs_list slot" do + result = render_component do |c| + c.with_after_diffs_list do + 'custom_after_content' + end + end + expect(result).to have_text('custom_after_content') + end + it "renders diffs list" do render_component expect(page).to have_css('[data-diffs-list]') diff --git a/spec/components/rapid_diffs/commit_app_component_spec.rb b/spec/components/rapid_diffs/commit_app_component_spec.rb index 90d4882332171e..19bff87f09ef33 100644 --- a/spec/components/rapid_diffs/commit_app_component_spec.rb +++ b/spec/components/rapid_diffs/commit_app_component_spec.rb @@ -34,6 +34,7 @@ allow(RapidDiffs::AppComponent).to receive(:new).and_return(app_component) allow(app_component).to receive(:render_in).and_yield(app_component) allow(app_component).to receive(:with_before_diffs_list).and_yield + allow(app_component).to receive(:with_after_diffs_list).and_yield end it "renders app with correct arguments" do @@ -75,6 +76,12 @@ end end + it "renders commit timeline container in after_diffs_list slot" do + render_component + + expect(page).to have_selector('[data-commit-timeline]', visible: :all) + end + def render_component render_inline(component) end diff --git a/spec/frontend/rapid_diffs/app/commit/timeline_spec.js b/spec/frontend/rapid_diffs/app/commit/timeline_spec.js new file mode 100644 index 00000000000000..17709371a44fea --- /dev/null +++ b/spec/frontend/rapid_diffs/app/commit/timeline_spec.js @@ -0,0 +1,55 @@ +import Vue from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import { PiniaVuePlugin } from 'pinia'; +import { createTestingPinia } from '@pinia/testing'; +import CommitTimeline from '~/rapid_diffs/app/commit/timeline.vue'; +import DiffDiscussions from '~/rapid_diffs/app/discussions/diff_discussions.vue'; +import { useDiffDiscussions } from '~/rapid_diffs/stores/diff_discussions'; + +Vue.use(PiniaVuePlugin); + +describe('CommitTimeline', () => { + let pinia; + let wrapper; + + const createDiscussion = (overrides = {}) => ({ + id: 'discussion-1', + notes: [{ id: 'note-1', body: 'Test note' }], + isForm: false, + ...overrides, + }); + + const findDiffDiscussions = () => wrapper.findComponent(DiffDiscussions); + + beforeEach(() => { + pinia = createTestingPinia({ stubActions: false }); + }); + + const createComponent = (discussions = []) => { + useDiffDiscussions(pinia).$patch({ discussions }); + + wrapper = shallowMount(CommitTimeline, { + pinia, + }); + }; + + it('does not render when there are no discussions', () => { + createComponent([]); + + expect(findDiffDiscussions().exists()).toBe(false); + }); + + it('does not render when all discussions are forms', () => { + createComponent([createDiscussion({ id: 'form-1', isForm: true })]); + + expect(findDiffDiscussions().exists()).toBe(false); + }); + + it('renders filtered discussions', () => { + const regularDiscussion = createDiscussion({ id: 'regular-1' }); + const formDiscussion = createDiscussion({ id: 'form-1', isForm: true }); + createComponent([regularDiscussion, formDiscussion]); + + expect(findDiffDiscussions().props('discussions')).toEqual([regularDiscussion]); + }); +}); diff --git a/spec/frontend/rapid_diffs/commit_app_spec.js b/spec/frontend/rapid_diffs/commit_app_spec.js index 41f55587687158..522290d13011a0 100644 --- a/spec/frontend/rapid_diffs/commit_app_spec.js +++ b/spec/frontend/rapid_diffs/commit_app_spec.js @@ -11,6 +11,7 @@ import { createAlert } from '~/alert'; import { initNewDiscussionToggle } from '~/rapid_diffs/app/init_new_discussions_toggle'; import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants'; import { useDiffsList } from '~/rapid_diffs/stores/diffs_list'; +import { initTimeline } from '~/rapid_diffs/app/init_timeline'; jest.mock('~/alert'); jest.mock('~/lib/graphql'); @@ -20,6 +21,7 @@ jest.mock('~/rapid_diffs/app/file_browser'); jest.mock('~/rapid_diffs/app/quirks/safari_fix'); jest.mock('~/rapid_diffs/app/quirks/content_visibility_fix'); jest.mock('~/rapid_diffs/app/init_new_discussions_toggle'); +jest.mock('~/rapid_diffs/app/init_timeline'); describe('Commit Rapid Diffs app', () => { let axiosMock; @@ -53,6 +55,7 @@ describe('Commit Rapid Diffs app', () => {
+
@@ -108,4 +111,25 @@ describe('Commit Rapid Diffs app', () => { true, ); }); + + it('initializes timeline', async () => { + axiosMock.onGet(appData.discussionsEndpoint).reply(HTTP_STATUS_OK, { discussions: [] }); + createApp(); + app.init(); + await app.initDiscussions(); + expect(initTimeline).toHaveBeenCalledWith( + expect.objectContaining({ + discussionsEndpoint: appData.discussionsEndpoint, + }), + ); + }); + + it('does not error when timeline container is absent', async () => { + axiosMock.onGet(appData.discussionsEndpoint).reply(HTTP_STATUS_OK, { discussions: [] }); + createApp(); + app.init(); + document.querySelector('[data-commit-timeline]').remove(); + + await expect(app.initDiscussions()).resolves.not.toThrow(); + }); }); -- GitLab From ac0e422f649c3f61cdc3bcbdc029bc3d171003f8 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Thu, 11 Dec 2025 23:57:32 -0700 Subject: [PATCH 03/30] Remove diff discussions from the commit timelne --- app/assets/javascripts/rapid_diffs/app/commit/timeline.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/rapid_diffs/app/commit/timeline.vue b/app/assets/javascripts/rapid_diffs/app/commit/timeline.vue index 99d513bef9cd98..5f836deab4fe32 100644 --- a/app/assets/javascripts/rapid_diffs/app/commit/timeline.vue +++ b/app/assets/javascripts/rapid_diffs/app/commit/timeline.vue @@ -11,7 +11,7 @@ export default { computed: { ...mapState(useDiffDiscussions, ['discussions']), timelineDiscussions() { - return this.discussions.filter((discussion) => !discussion.isForm); + return this.discussions.filter((discussion) => !discussion.isForm && !discussion.diff_discussion); }, }, }; -- GitLab From 461897ca6c7e7129c21fd1a25c0659a7a4cc312a Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Fri, 12 Dec 2025 00:04:50 -0700 Subject: [PATCH 04/30] Move the timeline for commits into the discussions folder --- .../rapid_diffs/app/{commit => discussions}/timeline.vue | 2 +- app/assets/javascripts/rapid_diffs/app/init_timeline.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/assets/javascripts/rapid_diffs/app/{commit => discussions}/timeline.vue (89%) diff --git a/app/assets/javascripts/rapid_diffs/app/commit/timeline.vue b/app/assets/javascripts/rapid_diffs/app/discussions/timeline.vue similarity index 89% rename from app/assets/javascripts/rapid_diffs/app/commit/timeline.vue rename to app/assets/javascripts/rapid_diffs/app/discussions/timeline.vue index 5f836deab4fe32..f2ebe7d12825ba 100644 --- a/app/assets/javascripts/rapid_diffs/app/commit/timeline.vue +++ b/app/assets/javascripts/rapid_diffs/app/discussions/timeline.vue @@ -1,7 +1,7 @@ diff --git a/app/assets/javascripts/rapid_diffs/stores/diff_discussions.js b/app/assets/javascripts/rapid_diffs/stores/diff_discussions.js index bd6be8555574a7..7c5ed335b8296e 100644 --- a/app/assets/javascripts/rapid_diffs/stores/diff_discussions.js +++ b/app/assets/javascripts/rapid_diffs/stores/diff_discussions.js @@ -62,6 +62,9 @@ export const useDiffDiscussions = defineStore('diffDiscussions', { discussion.notes.splice(discussion.notes.indexOf(note), 1); if (discussion.notes.length === 0) this.deleteDiscussion(discussion); }, + addDiscussion(discussion) { + this.discussions.push(addReactiveDiscussionProps(discussion)); + }, deleteDiscussion(discussion) { this.discussions.splice(this.discussions.indexOf(discussion), 1); }, -- GitLab From 1aa8c1736ccc3ad066db8eb4b90d2e2f3b7ca2d2 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Fri, 12 Dec 2025 00:47:21 -0700 Subject: [PATCH 07/30] Remove short-circuit to enable adding the mountpoint for discussions --- app/assets/javascripts/rapid_diffs/adapters/discussions.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/rapid_diffs/adapters/discussions.js b/app/assets/javascripts/rapid_diffs/adapters/discussions.js index fc9bc026a7479d..3a8d3d021e4b9e 100644 --- a/app/assets/javascripts/rapid_diffs/adapters/discussions.js +++ b/app/assets/javascripts/rapid_diffs/adapters/discussions.js @@ -98,8 +98,6 @@ function addParallelCells(lineRow) { function createDiscussionMount(createCell) { return ({ diffElement, id, position, appData }) => { - if (document.querySelector(`[data-discussion-id="${id}"]`)) return; - const cell = createCell(diffElement, position.old_line, position.new_line); if (cell.hasMountedApp) return; const mountTarget = document.createElement('div'); -- GitLab From 1c3f95e5f39d7f2c32dabfdb9ffe8fde5d1ce6c1 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Fri, 12 Dec 2025 01:39:10 -0700 Subject: [PATCH 08/30] Prevent the reply form from showing for system notes --- .../rapid_diffs/app/discussions/noteable_discussion.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/rapid_diffs/app/discussions/noteable_discussion.vue b/app/assets/javascripts/rapid_diffs/app/discussions/noteable_discussion.vue index 58fa4c78c02e99..696cb6d0044579 100644 --- a/app/assets/javascripts/rapid_diffs/app/discussions/noteable_discussion.vue +++ b/app/assets/javascripts/rapid_diffs/app/discussions/noteable_discussion.vue @@ -54,6 +54,9 @@ export default { saveButtonTitle() { return this.discussion.internal ? __('Reply internally') : __('Reply'); }, + canReply() { + return !this.discussion.notes[0]?.system; + }, }, methods: { showReplyForm(text) { @@ -149,6 +152,7 @@ export default {