Commit 674fe50e authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Frontend to fetch paginated discussions

parent 631d430d
...@@ -98,15 +98,17 @@ export default { ...@@ -98,15 +98,17 @@ export default {
return this.noteableData.noteableType; return this.noteableData.noteableType;
}, },
allDiscussions() { allDiscussions() {
let skeletonNotes = [];
if (this.renderSkeleton || this.isLoading) { if (this.renderSkeleton || this.isLoading) {
const prerenderedNotesCount = parseInt(this.notesData.prerenderedNotesCount, 10) || 0; const prerenderedNotesCount = parseInt(this.notesData.prerenderedNotesCount, 10) || 0;
return new Array(prerenderedNotesCount).fill({ skeletonNotes = new Array(prerenderedNotesCount).fill({
isSkeletonNote: true, isSkeletonNote: true,
}); });
} }
return this.discussions; return this.discussions.concat(skeletonNotes);
}, },
canReply() { canReply() {
return this.userCanReply && !this.commentsDisabled && !this.timelineEnabled; return this.userCanReply && !this.commentsDisabled && !this.timelineEnabled;
......
...@@ -70,7 +70,7 @@ export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, dat ...@@ -70,7 +70,7 @@ export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, dat
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, discussions) => export const setInitialNotes = ({ commit }, discussions) =>
commit(types.SET_INITIAL_DISCUSSIONS, discussions); commit(types.ADD_OR_UPDATE_DISCUSSIONS, discussions);
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
...@@ -89,14 +89,51 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi ...@@ -89,14 +89,51 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi
? { params: { notes_filter: filter, persist_filter: persistFilter } } ? { params: { notes_filter: filter, persist_filter: persistFilter } }
: null; : null;
if (window.gon?.features?.paginatedIssueDiscussions) {
return dispatch('fetchDiscussionsBatch', { path, config, perPage: 20 });
}
return axios.get(path, config).then(({ data }) => { return axios.get(path, config).then(({ data }) => {
commit(types.SET_INITIAL_DISCUSSIONS, data); commit(types.ADD_OR_UPDATE_DISCUSSIONS, data);
commit(types.SET_FETCHING_DISCUSSIONS, false); commit(types.SET_FETCHING_DISCUSSIONS, false);
dispatch('updateResolvableDiscussionsCounts'); dispatch('updateResolvableDiscussionsCounts');
}); });
}; };
export const fetchDiscussionsBatch = ({ commit, dispatch }, { path, config, cursor, perPage }) => {
const params = { ...config?.params, per_page: perPage };
if (cursor) {
params.cursor = cursor;
}
return axios.get(path, { params }).then(({ data, headers }) => {
commit(types.ADD_OR_UPDATE_DISCUSSIONS, data);
if (headers['x-next-page-cursor']) {
const nextConfig = { ...config };
if (config?.params?.persist_filter) {
delete nextConfig.params.notes_filter;
delete nextConfig.params.persist_filter;
}
return dispatch('fetchDiscussionsBatch', {
path,
config: nextConfig,
cursor: headers['x-next-page-cursor'],
perPage: Math.min(Math.round(perPage * 1.5), 100),
});
}
commit(types.SET_FETCHING_DISCUSSIONS, false);
dispatch('updateResolvableDiscussionsCounts');
return undefined;
});
};
export const updateDiscussion = ({ commit, state }, discussion) => { export const updateDiscussion = ({ commit, state }, discussion) => {
commit(types.UPDATE_DISCUSSION, discussion); commit(types.UPDATE_DISCUSSION, discussion);
......
export const ADD_NEW_NOTE = 'ADD_NEW_NOTE'; export const ADD_NEW_NOTE = 'ADD_NEW_NOTE';
export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION'; export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
export const ADD_OR_UPDATE_DISCUSSIONS = 'ADD_OR_UPDATE_DISCUSSIONS';
export const DELETE_NOTE = 'DELETE_NOTE'; export const DELETE_NOTE = 'DELETE_NOTE';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES'; export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA'; export const SET_NOTES_DATA = 'SET_NOTES_DATA';
export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA'; export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA'; export const SET_USER_DATA = 'SET_USER_DATA';
export const SET_INITIAL_DISCUSSIONS = 'SET_INITIAL_DISCUSSIONS';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT'; export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH'; export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH';
export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE'; export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
......
...@@ -129,8 +129,8 @@ export default { ...@@ -129,8 +129,8 @@ export default {
Object.assign(state, { userData: data }); Object.assign(state, { userData: data });
}, },
[types.SET_INITIAL_DISCUSSIONS](state, discussionsData) { [types.ADD_OR_UPDATE_DISCUSSIONS](state, discussionsData) {
const discussions = discussionsData.reduce((acc, d) => { discussionsData.forEach((d) => {
const discussion = { ...d }; const discussion = { ...d };
const diffData = {}; const diffData = {};
...@@ -145,27 +145,38 @@ export default { ...@@ -145,27 +145,38 @@ export default {
// To support legacy notes, should be very rare case. // To support legacy notes, should be very rare case.
if (discussion.individual_note && discussion.notes.length > 1) { if (discussion.individual_note && discussion.notes.length > 1) {
discussion.notes.forEach((n) => { discussion.notes.forEach((n) => {
acc.push({ const newDiscussion = {
...discussion, ...discussion,
...diffData, ...diffData,
notes: [n], // override notes array to only have one item to mimick individual_note notes: [n], // override notes array to only have one item to mimick individual_note
}); };
const oldDiscussion = state.discussions.find(
(existingDiscussion) =>
existingDiscussion.id === discussion.id && existingDiscussion.notes[0].id === n.id,
);
if (oldDiscussion) {
state.discussions.splice(state.discussions.indexOf(oldDiscussion), 1, newDiscussion);
} else {
state.discussions.push(newDiscussion);
}
}); });
} else { } else {
const oldNote = utils.findNoteObjectById(state.discussions, discussion.id); const oldDiscussion = utils.findNoteObjectById(state.discussions, discussion.id);
acc.push({ if (oldDiscussion) {
...discussion, state.discussions.splice(state.discussions.indexOf(oldDiscussion), 1, {
...diffData, ...discussion,
expanded: oldNote ? oldNote.expanded : discussion.expanded, ...diffData,
}); expanded: oldDiscussion.expanded,
});
} else {
state.discussions.push({ ...discussion, ...diffData });
}
} }
});
return acc;
}, []);
Object.assign(state, { discussions });
}, },
[types.SET_LAST_FETCHED_AT](state, fetchedAt) { [types.SET_LAST_FETCHED_AT](state, fetchedAt) {
Object.assign(state, { lastFetchedAt: fetchedAt }); Object.assign(state, { lastFetchedAt: fetchedAt });
}, },
......
...@@ -52,6 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -52,6 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml) push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml) push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, @project, default_enabled: :yaml) push_frontend_feature_flag(:labels_widget, @project, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_issue_discussions, @project, default_enabled: :yaml)
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance| experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
experiment_instance.exclude! unless helpers.can_admin_project_member?(@project) experiment_instance.exclude! unless helpers.can_admin_project_member?(@project)
......
- add_page_startup_api_call Feature.enabled?(:paginated_discussions, @project) ? discussions_path(@issue, per_page: 20) : discussions_path(@issue) - add_page_startup_api_call Feature.enabled?(:paginated_issue_discussions, @project, default_enabled: :yaml) ? discussions_path(@issue, per_page: 20) : discussions_path(@issue)
- @gfm_form = true - @gfm_form = true
......
---
name: paginated_issue_discussions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69933
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345351
milestone: '14.5'
type: development
group: group::project management
default_enabled: false
...@@ -53,7 +53,7 @@ describe('DiscussionCounter component', () => { ...@@ -53,7 +53,7 @@ describe('DiscussionCounter component', () => {
describe('has no resolvable discussions', () => { describe('has no resolvable discussions', () => {
it('does not render', () => { it('does not render', () => {
store.commit(types.SET_INITIAL_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]); store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]);
store.dispatch('updateResolvableDiscussionsCounts'); store.dispatch('updateResolvableDiscussionsCounts');
wrapper = shallowMount(DiscussionCounter, { store, localVue }); wrapper = shallowMount(DiscussionCounter, { store, localVue });
...@@ -64,7 +64,7 @@ describe('DiscussionCounter component', () => { ...@@ -64,7 +64,7 @@ describe('DiscussionCounter component', () => {
describe('has resolvable discussions', () => { describe('has resolvable discussions', () => {
const updateStore = (note = {}) => { const updateStore = (note = {}) => {
discussionMock.notes[0] = { ...discussionMock.notes[0], ...note }; discussionMock.notes[0] = { ...discussionMock.notes[0], ...note };
store.commit(types.SET_INITIAL_DISCUSSIONS, [discussionMock]); store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussionMock]);
store.dispatch('updateResolvableDiscussionsCounts'); store.dispatch('updateResolvableDiscussionsCounts');
}; };
...@@ -97,7 +97,7 @@ describe('DiscussionCounter component', () => { ...@@ -97,7 +97,7 @@ describe('DiscussionCounter component', () => {
let toggleAllButton; let toggleAllButton;
const updateStoreWithExpanded = (expanded) => { const updateStoreWithExpanded = (expanded) => {
const discussion = { ...discussionMock, expanded }; const discussion = { ...discussionMock, expanded };
store.commit(types.SET_INITIAL_DISCUSSIONS, [discussion]); store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussion]);
store.dispatch('updateResolvableDiscussionsCounts'); store.dispatch('updateResolvableDiscussionsCounts');
wrapper = shallowMount(DiscussionCounter, { store, localVue }); wrapper = shallowMount(DiscussionCounter, { store, localVue });
toggleAllButton = wrapper.find('.toggle-all-discussions-btn'); toggleAllButton = wrapper.find('.toggle-all-discussions-btn');
......
...@@ -119,7 +119,7 @@ describe('Actions Notes Store', () => { ...@@ -119,7 +119,7 @@ describe('Actions Notes Store', () => {
actions.setInitialNotes, actions.setInitialNotes,
[individualNote], [individualNote],
{ notes: [] }, { notes: [] },
[{ type: 'SET_INITIAL_DISCUSSIONS', payload: [individualNote] }], [{ type: 'ADD_OR_UPDATE_DISCUSSIONS', payload: [individualNote] }],
[], [],
done, done,
); );
......
...@@ -159,7 +159,7 @@ describe('Notes Store mutations', () => { ...@@ -159,7 +159,7 @@ describe('Notes Store mutations', () => {
}); });
}); });
describe('SET_INITIAL_DISCUSSIONS', () => { describe('ADD_OR_UPDATE_DISCUSSIONS', () => {
it('should set the initial notes received', () => { it('should set the initial notes received', () => {
const state = { const state = {
discussions: [], discussions: [],
...@@ -169,15 +169,17 @@ describe('Notes Store mutations', () => { ...@@ -169,15 +169,17 @@ describe('Notes Store mutations', () => {
individual_note: true, individual_note: true,
notes: [ notes: [
{ {
id: 100,
note: '1', note: '1',
}, },
{ {
id: 101,
note: '2', note: '2',
}, },
], ],
}; };
mutations.SET_INITIAL_DISCUSSIONS(state, [note, legacyNote]); mutations.ADD_OR_UPDATE_DISCUSSIONS(state, [note, legacyNote]);
expect(state.discussions[0].id).toEqual(note.id); expect(state.discussions[0].id).toEqual(note.id);
expect(state.discussions[1].notes[0].note).toBe(legacyNote.notes[0].note); expect(state.discussions[1].notes[0].note).toBe(legacyNote.notes[0].note);
...@@ -190,7 +192,7 @@ describe('Notes Store mutations', () => { ...@@ -190,7 +192,7 @@ describe('Notes Store mutations', () => {
discussions: [], discussions: [],
}; };
mutations.SET_INITIAL_DISCUSSIONS(state, [ mutations.ADD_OR_UPDATE_DISCUSSIONS(state, [
{ {
...note, ...note,
diff_file: { diff_file: {
...@@ -208,7 +210,7 @@ describe('Notes Store mutations', () => { ...@@ -208,7 +210,7 @@ describe('Notes Store mutations', () => {
discussions: [], discussions: [],
}; };
mutations.SET_INITIAL_DISCUSSIONS(state, [ mutations.ADD_OR_UPDATE_DISCUSSIONS(state, [
{ {
...note, ...note,
diff_file: { diff_file: {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment