{
}
};
-export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
+export const fetchDiffFilesBatch = ({ commit, state, dispatch }, pinnedFileLoading = false) => {
let perPage = state.viewDiffsFileByFile ? 1 : state.perPage;
let increaseAmount = 1.4;
const startPage = 0;
@@ -224,8 +223,10 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
let totalLoaded = 0;
let scrolledVirtualScroller = hash === '';
- commit(types.SET_BATCH_LOADING_STATE, 'loading');
- commit(types.SET_RETRIEVING_BATCHES, true);
+ if (!pinnedFileLoading) {
+ commit(types.SET_BATCH_LOADING_STATE, 'loading');
+ commit(types.SET_RETRIEVING_BATCHES, true);
+ }
eventHub.$emit(EVT_PERF_MARK_DIFF_FILES_START);
const getBatch = (page = startPage) =>
@@ -237,7 +238,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
commit(types.SET_DIFF_DATA_BATCH, { diff_files: diffFiles });
commit(types.SET_BATCH_LOADING_STATE, 'loaded');
- if (!scrolledVirtualScroller) {
+ if (!scrolledVirtualScroller && !pinnedFileLoading) {
const index = state.diffFiles.findIndex(
(f) =>
f.file_hash === hash || f[INLINE_DIFF_LINES_KEY].find((l) => l.line_code === hash),
@@ -301,9 +302,10 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
return null;
})
- .catch(() => {
+ .catch((error) => {
commit(types.SET_RETRIEVING_BATCHES, false);
commit(types.SET_BATCH_LOADING_STATE, 'error');
+ throw error;
});
return getBatch();
@@ -384,7 +386,11 @@ export const fetchCoverageFiles = ({ commit, state }) => {
coveragePoll.makeRequest();
};
-export const setHighlightedRow = ({ commit }, lineCode) => {
+export const setHighlightedRow = ({ commit }, { lineCode, event }) => {
+ if (event && event.target.href) {
+ event.preventDefault();
+ window.history.replaceState(null, undefined, event.target.href);
+ }
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
commit(types.SET_CURRENT_DIFF_FILE, fileHash);
@@ -657,6 +663,8 @@ export const goToFile = ({ state, commit, dispatch, getters }, { path }) => {
} else {
if (!state.treeEntries[path]) return;
+ dispatch('unpinFile');
+
const { fileHash } = state.treeEntries[path];
commit(types.SET_CURRENT_DIFF_FILE, fileHash);
@@ -667,11 +675,7 @@ export const goToFile = ({ state, commit, dispatch, getters }, { path }) => {
scrollToElement('.diff-files-holder', { duration: 0 });
if (!getters.isTreePathLoaded(path)) {
- dispatch('fetchFileByFile').catch(() => {
- createAlert({
- message: LOAD_SINGLE_DIFF_FAILED,
- });
- });
+ dispatch('fetchFileByFile');
}
}
};
@@ -995,6 +999,8 @@ export const setCurrentDiffFileIdFromNote = ({ commit, getters, rootGetters }, n
};
export const navigateToDiffFileIndex = ({ state, getters, commit, dispatch }, index) => {
+ dispatch('unpinFile');
+
const { fileHash } = getters.flatBlobsList[index];
document.location.hash = fileHash;
@@ -1055,3 +1061,50 @@ export const toggleFileCommentForm = ({ state, commit }, filePath) => {
export const addDraftToFile = ({ commit }, { filePath, draft }) =>
commit(types.ADD_DRAFT_TO_FILE, { filePath, draft });
+
+export const fetchPinnedFile = ({ state, commit }, pinnedFileUrl) => {
+ const isNoteLink = isUrlHashNoteLink(window?.location?.hash);
+
+ commit(types.SET_BATCH_LOADING_STATE, 'loading');
+ commit(types.SET_RETRIEVING_BATCHES, true);
+
+ return axios
+ .get(pinnedFileUrl)
+ .then(({ data: diffData }) => {
+ const [{ file_hash }] = diffData.diff_files;
+
+ // we must store pinned file in the `diffs`, otherwise collapsing and commenting on a file won't work
+ // once the same file arrives in a file batch we must only update its' position
+ // we also must not update file's position since it's loaded out of order
+ commit(types.SET_DIFF_DATA_BATCH, { diff_files: diffData.diff_files, updatePosition: false });
+ commit(types.SET_PINNED_FILE_HASH, file_hash);
+
+ if (!isNoteLink && !state.currentDiffFileId) {
+ commit(types.SET_CURRENT_DIFF_FILE, file_hash);
+ }
+
+ commit(types.SET_BATCH_LOADING_STATE, 'loaded');
+
+ setTimeout(() => {
+ handleLocationHash();
+ });
+
+ eventHub.$emit('diffFilesModified');
+ })
+ .catch((error) => {
+ commit(types.SET_BATCH_LOADING_STATE, 'error');
+ throw error;
+ })
+ .finally(() => {
+ commit(types.SET_RETRIEVING_BATCHES, false);
+ });
+};
+
+export const unpinFile = ({ getters, commit }) => {
+ if (!getters.pinnedFile) return;
+ commit(types.SET_PINNED_FILE_HASH, null);
+ const newUrl = new URL(window.location);
+ newUrl.searchParams.delete('pin');
+ newUrl.hash = '';
+ window.history.replaceState(null, undefined, newUrl);
+};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index bfafb4d281d0a929329f293aba8fe49600852665..f15c3d1693c4e6ed8bca061dececf74a29214f6d 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -201,3 +201,18 @@ export const isVirtualScrollingEnabled = (state) => {
export const isBatchLoading = (state) => state.batchLoadingState === 'loading';
export const isBatchLoadingError = (state) => state.batchLoadingState === 'error';
+
+export const diffFiles = (state, getters) => {
+ const { pinnedFile } = getters;
+ if (pinnedFile) {
+ const diffs = state.diffFiles.slice(0);
+ diffs.splice(diffs.indexOf(pinnedFile), 1);
+ return [pinnedFile, ...diffs];
+ }
+ return state.diffFiles;
+};
+
+export const pinnedFile = (state) => {
+ if (!state.pinnedFileHash) return null;
+ return state.diffFiles.find((file) => file.file_hash === state.pinnedFileHash);
+};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index d5e1a05f4a59c6d27ba56d0b281892dc57ff539e..3b6408e6d786b413b5ad409834e448f0b6db4eef 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -37,4 +37,5 @@ export default () => ({
mrReviews: {},
latestDiff: true,
disableVirtualScroller: false,
+ pinnedFileHash: null,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index b155804c70cf6e9dfdb13d1aa7fa96e906051139..7b19f96b1514d6b80799411e412162867b6fc7f1 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -6,6 +6,7 @@ export const SET_RETRIEVING_BATCHES = 'SET_RETRIEVING_BATCHES';
export const SET_DIFF_METADATA = 'SET_DIFF_METADATA';
export const SET_DIFF_DATA_BATCH = 'SET_DIFF_DATA_BATCH';
export const SET_DIFF_FILES = 'SET_DIFF_FILES';
+export const SET_DIFF_TREE_ENTRY = 'SET_DIFF_TREE_ENTRY';
export const SET_MR_FILE_REVIEWS = 'SET_MR_FILE_REVIEWS';
@@ -23,6 +24,7 @@ export const TREE_ENTRY_DIFF_LOADING = 'TREE_ENTRY_DIFF_LOADING';
export const SET_SHOW_TREE_LIST = 'SET_SHOW_TREE_LIST';
export const SET_CURRENT_DIFF_FILE = 'SET_CURRENT_DIFF_FILE';
export const SET_DIFF_FILE_VIEWED = 'SET_DIFF_FILE_VIEWED';
+export const SET_PINNED_FILE_HASH = 'SET_PINNED_FILE_HASH';
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index bc5ed3c40dfe23a689d61f0e6d1f0c5602e31231..3c750830baaf566d667efd364e1bc861f7b41dd7 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -81,15 +81,27 @@ export default {
});
},
- [types.SET_DIFF_DATA_BATCH](state, data) {
+ [types.SET_DIFF_DATA_BATCH](state, { diff_files: diffFiles, updatePosition = true }) {
Object.assign(state, {
diffFiles: prepareDiffData({
- diff: data,
+ diff: { diff_files: diffFiles },
priorFiles: state.diffFiles,
+ // when a pinned file is added to diffs its position may be incorrect since it's loaded out of order
+ // we need to ensure when we load it in batched request it updates it position
+ updatePosition,
}),
treeEntries: markTreeEntriesLoaded({
priorEntries: state.treeEntries,
- loadedFiles: data.diff_files,
+ loadedFiles: diffFiles,
+ }),
+ });
+ },
+
+ [types.SET_DIFF_TREE_ENTRY](state, diffFile) {
+ Object.assign(state, {
+ treeEntries: markTreeEntriesLoaded({
+ priorEntries: state.treeEntries,
+ loadedFiles: [diffFile],
}),
});
},
@@ -404,4 +416,7 @@ export default {
file?.drafts.push(draft);
},
+ [types.SET_PINNED_FILE_HASH](state, fileHash) {
+ state.pinnedFileHash = fileHash;
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index fb467a606b9d40e2234130d3555d469dc1064170..ad8cacf8504de6f47d7efd54d5b5d758c1d45cf3 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -395,9 +395,15 @@ function finalizeDiffFile(file) {
return file;
}
-function deduplicateFilesList(files) {
+function deduplicateFilesList(files, updatePosition) {
const dedupedFiles = files.reduce((newList, file) => {
const id = diffFileUniqueId(file);
+ if (updatePosition && id in newList) {
+ // Object.values preserves key order but doesn't update order when writing to the same key
+ // In order to update position of the item we have to delete it first and then add it back
+ // eslint-disable-next-line no-param-reassign
+ delete newList[id];
+ }
return {
...newList,
@@ -408,14 +414,20 @@ function deduplicateFilesList(files) {
return Object.values(dedupedFiles);
}
-export function prepareDiffData({ diff, priorFiles = [], meta = false }) {
- const cleanedFiles = (diff.diff_files || [])
- .map((file, index, allFiles) => prepareRawDiffFile({ file, allFiles, meta, index }))
- .map(ensureBasicDiffFileLines)
- .map(prepareDiffFileLines)
- .map((file) => finalizeDiffFile(file));
+export function prepareDiffData({ diff, priorFiles = [], meta = false, updatePosition = false }) {
+ const transformersChain = [
+ (file, index, allFiles) => prepareRawDiffFile({ file, index, allFiles, meta }),
+ ensureBasicDiffFileLines,
+ prepareDiffFileLines,
+ finalizeDiffFile,
+ ];
+ const cleanedFiles = (diff.diff_files || []).map((file, index, allFiles) => {
+ return transformersChain.reduce((fileResult, transformer) => {
+ return transformer(fileResult, index, allFiles);
+ }, file);
+ });
- return deduplicateFilesList([...priorFiles, ...cleanedFiles]);
+ return deduplicateFilesList([...priorFiles, ...cleanedFiles], updatePosition);
}
export function getDiffPositionByLineCode(diffFiles) {
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 6ac75230d8891c749bb90c633c5e19e0064a12c9..3419e6eb1b699c8fa19ca43ef16a8e7a6822ff88 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -1,5 +1,5 @@