Commit 36919be5 authored by Phil Hughes's avatar Phil Hughes

Merge branch '50347-fix-scrolling-to-diff-note-after-incremental-rendering' into 'master'

Resolve "Link to file in Changes tab of MR no longer works for all files after incremental rendering improvement"

Closes #50347

See merge request gitlab-org/gitlab-ce!21727
parents 33142335 38beacc9
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import DiffTableCell from './diff_table_cell.vue'; import DiffTableCell from './diff_table_cell.vue';
import { import {
NEW_LINE_TYPE, NEW_LINE_TYPE,
...@@ -63,7 +63,11 @@ export default { ...@@ -63,7 +63,11 @@ export default {
this.linePositionLeft = LINE_POSITION_LEFT; this.linePositionLeft = LINE_POSITION_LEFT;
this.linePositionRight = LINE_POSITION_RIGHT; this.linePositionRight = LINE_POSITION_RIGHT;
}, },
mounted() {
this.scrollToLineIfNeededInline(this.line);
},
methods: { methods: {
...mapActions('diffs', ['scrollToLineIfNeededInline']),
handleMouseMove(e) { handleMouseMove(e) {
// To show the comment icon on the gutter we need to know if we hover the line. // To show the comment icon on the gutter we need to know if we hover the line.
// Current table structure doesn't allow us to do this with CSS in both of the diff view types // Current table structure doesn't allow us to do this with CSS in both of the diff view types
......
<script> <script>
import { mapActions } from 'vuex';
import $ from 'jquery'; import $ from 'jquery';
import DiffTableCell from './diff_table_cell.vue'; import DiffTableCell from './diff_table_cell.vue';
import { import {
...@@ -64,7 +65,11 @@ export default { ...@@ -64,7 +65,11 @@ export default {
this.oldLineType = OLD_LINE_TYPE; this.oldLineType = OLD_LINE_TYPE;
this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE; this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE;
}, },
mounted() {
this.scrollToLineIfNeededParallel(this.line);
},
methods: { methods: {
...mapActions('diffs', ['scrollToLineIfNeededParallel']),
handleMouseMove(e) { handleMouseMove(e) {
const isHover = e.type === 'mouseover'; const isHover = e.type === 'mouseover';
const hoveringCell = e.target.closest('td'); const hoveringCell = e.target.closest('td');
......
...@@ -2,7 +2,7 @@ import Vue from 'vue'; ...@@ -2,7 +2,7 @@ import Vue from 'vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils'; import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
import { getDiffPositionByLineCode } from './utils'; import { getDiffPositionByLineCode } from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
...@@ -120,6 +120,25 @@ export const loadMoreLines = ({ commit }, options) => { ...@@ -120,6 +120,25 @@ export const loadMoreLines = ({ commit }, options) => {
}); });
}; };
export const scrollToLineIfNeededInline = (_, line) => {
const hash = getLocationHash();
if (hash && line.lineCode === hash) {
handleLocationHash();
}
};
export const scrollToLineIfNeededParallel = (_, line) => {
const hash = getLocationHash();
if (
hash &&
((line.left && line.left.lineCode === hash) || (line.right && line.right.lineCode === hash))
) {
handleLocationHash();
}
};
export const loadCollapsedDiff = ({ commit }, file) => export const loadCollapsedDiff = ({ commit }, file) =>
axios.get(file.loadCollapsedDiffUrl).then(res => { axios.get(file.loadCollapsedDiffUrl).then(res => {
commit(types.ADD_COLLAPSED_DIFFS, { commit(types.ADD_COLLAPSED_DIFFS, {
......
...@@ -56,7 +56,8 @@ export const rstrip = val => { ...@@ -56,7 +56,8 @@ export const rstrip = val => {
return val; return val;
}; };
export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('_fixTitle'); export const updateTooltipTitle = ($tooltipEl, newTitle) =>
$tooltipEl.attr('title', newTitle).tooltip('_fixTitle');
export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => { export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => {
const field = $(fieldSelector); const field = $(fieldSelector);
...@@ -86,6 +87,7 @@ export const handleLocationHash = () => { ...@@ -86,6 +87,7 @@ export const handleLocationHash = () => {
const fixedTabs = document.querySelector('.js-tabs-affix'); const fixedTabs = document.querySelector('.js-tabs-affix');
const fixedDiffStats = document.querySelector('.js-diff-files-changed'); const fixedDiffStats = document.querySelector('.js-diff-files-changed');
const fixedNav = document.querySelector('.navbar-gitlab'); const fixedNav = document.querySelector('.navbar-gitlab');
const performanceBar = document.querySelector('#js-peek');
let adjustment = 0; let adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight; if (fixedNav) adjustment -= fixedNav.offsetHeight;
...@@ -102,6 +104,10 @@ export const handleLocationHash = () => { ...@@ -102,6 +104,10 @@ export const handleLocationHash = () => {
adjustment -= fixedDiffStats.offsetHeight; adjustment -= fixedDiffStats.offsetHeight;
} }
if (performanceBar) {
adjustment -= performanceBar.offsetHeight;
}
window.scrollBy(0, adjustment); window.scrollBy(0, adjustment);
}; };
...@@ -131,21 +137,20 @@ export const parseUrlPathname = url => { ...@@ -131,21 +137,20 @@ export const parseUrlPathname = url => {
return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`; return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`;
}; };
const splitPath = (path = '') => path const splitPath = (path = '') => path.replace(/^\?/, '').split('&');
.replace(/^\?/, '')
.split('&');
export const urlParamsToArray = (path = '') => splitPath(path) export const urlParamsToArray = (path = '') =>
.filter(param => param.length > 0) splitPath(path)
.map(param => { .filter(param => param.length > 0)
const split = param.split('='); .map(param => {
return [decodeURI(split[0]), split[1]].join('='); const split = param.split('=');
}); return [decodeURI(split[0]), split[1]].join('=');
});
export const getUrlParamsArray = () => urlParamsToArray(window.location.search); export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
export const urlParamsToObject = (path = '') => splitPath(path) export const urlParamsToObject = (path = '') =>
.reduce((dataParam, filterParam) => { splitPath(path).reduce((dataParam, filterParam) => {
if (filterParam === '') { if (filterParam === '') {
return dataParam; return dataParam;
} }
...@@ -216,7 +221,7 @@ export const getParameterByName = (name, urlToParse) => { ...@@ -216,7 +221,7 @@ export const getParameterByName = (name, urlToParse) => {
return decodeURIComponent(results[2].replace(/\+/g, ' ')); return decodeURIComponent(results[2].replace(/\+/g, ' '));
}; };
const handleSelectedRange = (range) => { const handleSelectedRange = range => {
const container = range.commonAncestorContainer; const container = range.commonAncestorContainer;
// add context to fragment if needed // add context to fragment if needed
if (container.tagName === 'OL') { if (container.tagName === 'OL') {
...@@ -453,7 +458,7 @@ export const backOff = (fn, timeout = 60000) => { ...@@ -453,7 +458,7 @@ export const backOff = (fn, timeout = 60000) => {
export const createOverlayIcon = (iconPath, overlayPath) => { export const createOverlayIcon = (iconPath, overlayPath) => {
const faviconImage = document.createElement('img'); const faviconImage = document.createElement('img');
return new Promise((resolve) => { return new Promise(resolve => {
faviconImage.onload = () => { faviconImage.onload = () => {
const size = 32; const size = 32;
...@@ -464,13 +469,29 @@ export const createOverlayIcon = (iconPath, overlayPath) => { ...@@ -464,13 +469,29 @@ export const createOverlayIcon = (iconPath, overlayPath) => {
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
context.clearRect(0, 0, size, size); context.clearRect(0, 0, size, size);
context.drawImage( context.drawImage(
faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size, faviconImage,
0,
0,
faviconImage.width,
faviconImage.height,
0,
0,
size,
size,
); );
const overlayImage = document.createElement('img'); const overlayImage = document.createElement('img');
overlayImage.onload = () => { overlayImage.onload = () => {
context.drawImage( context.drawImage(
overlayImage, 0, 0, overlayImage.width, overlayImage.height, 0, 0, size, size, overlayImage,
0,
0,
overlayImage.width,
overlayImage.height,
0,
0,
size,
size,
); );
const faviconWithOverlayUrl = canvas.toDataURL(); const faviconWithOverlayUrl = canvas.toDataURL();
...@@ -483,17 +504,21 @@ export const createOverlayIcon = (iconPath, overlayPath) => { ...@@ -483,17 +504,21 @@ export const createOverlayIcon = (iconPath, overlayPath) => {
}); });
}; };
export const setFaviconOverlay = (overlayPath) => { export const setFaviconOverlay = overlayPath => {
const faviconEl = document.getElementById('favicon'); const faviconEl = document.getElementById('favicon');
if (!faviconEl) { return null; } if (!faviconEl) {
return null;
}
const iconPath = faviconEl.getAttribute('data-original-href'); const iconPath = faviconEl.getAttribute('data-original-href');
return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => faviconEl.setAttribute('href', faviconWithOverlayUrl)); return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl =>
faviconEl.setAttribute('href', faviconWithOverlayUrl),
);
}; };
export const setFavicon = (faviconPath) => { export const setFavicon = faviconPath => {
const faviconEl = document.getElementById('favicon'); const faviconEl = document.getElementById('favicon');
if (faviconEl && faviconPath) { if (faviconEl && faviconPath) {
faviconEl.setAttribute('href', faviconPath); faviconEl.setAttribute('href', faviconPath);
...@@ -518,7 +543,7 @@ export const setCiStatusFavicon = pageUrl => ...@@ -518,7 +543,7 @@ export const setCiStatusFavicon = pageUrl =>
} }
return resetFavicon(); return resetFavicon();
}) })
.catch((error) => { .catch(error => {
resetFavicon(); resetFavicon();
throw error; throw error;
}); });
......
...@@ -5,7 +5,23 @@ import { ...@@ -5,7 +5,23 @@ import {
INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE,
} from '~/diffs/constants'; } from '~/diffs/constants';
import * as actions from '~/diffs/store/actions'; import actions, {
setBaseConfig,
fetchDiffFiles,
assignDiscussionsToDiff,
removeDiscussionsFromDiff,
startRenderDiffsQueue,
setInlineDiffViewType,
setParallelDiffViewType,
showCommentForm,
cancelCommentForm,
loadMoreLines,
scrollToLineIfNeededInline,
scrollToLineIfNeededParallel,
loadCollapsedDiff,
expandAllFiles,
toggleFileDiscussions,
} from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types'; import * as types from '~/diffs/store/mutation_types';
import { reduceDiscussionsToLineCodes } from '~/notes/stores/utils'; import { reduceDiscussionsToLineCodes } from '~/notes/stores/utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -37,7 +53,7 @@ describe('DiffsStoreActions', () => { ...@@ -37,7 +53,7 @@ describe('DiffsStoreActions', () => {
const projectPath = '/root/project'; const projectPath = '/root/project';
testAction( testAction(
actions.setBaseConfig, setBaseConfig,
{ endpoint, projectPath }, { endpoint, projectPath },
{ endpoint: '', projectPath: '' }, { endpoint: '', projectPath: '' },
[{ type: types.SET_BASE_CONFIG, payload: { endpoint, projectPath } }], [{ type: types.SET_BASE_CONFIG, payload: { endpoint, projectPath } }],
...@@ -55,7 +71,7 @@ describe('DiffsStoreActions', () => { ...@@ -55,7 +71,7 @@ describe('DiffsStoreActions', () => {
mock.onGet(endpoint).reply(200, res); mock.onGet(endpoint).reply(200, res);
testAction( testAction(
actions.fetchDiffFiles, fetchDiffFiles,
{}, {},
{ endpoint }, { endpoint },
[ [
...@@ -139,7 +155,7 @@ describe('DiffsStoreActions', () => { ...@@ -139,7 +155,7 @@ describe('DiffsStoreActions', () => {
const discussions = reduceDiscussionsToLineCodes([singleDiscussion]); const discussions = reduceDiscussionsToLineCodes([singleDiscussion]);
testAction( testAction(
actions.assignDiscussionsToDiff, assignDiscussionsToDiff,
discussions, discussions,
state, state,
[ [
...@@ -208,7 +224,7 @@ describe('DiffsStoreActions', () => { ...@@ -208,7 +224,7 @@ describe('DiffsStoreActions', () => {
}; };
testAction( testAction(
actions.removeDiscussionsFromDiff, removeDiscussionsFromDiff,
singleDiscussion, singleDiscussion,
state, state,
[ [
...@@ -252,8 +268,7 @@ describe('DiffsStoreActions', () => { ...@@ -252,8 +268,7 @@ describe('DiffsStoreActions', () => {
}); });
}; };
actions startRenderDiffsQueue({ state, commit: pseudoCommit })
.startRenderDiffsQueue({ state, commit: pseudoCommit })
.then(() => { .then(() => {
expect(state.diffFiles[0].renderIt).toBeTruthy(); expect(state.diffFiles[0].renderIt).toBeTruthy();
expect(state.diffFiles[1].renderIt).toBeTruthy(); expect(state.diffFiles[1].renderIt).toBeTruthy();
...@@ -269,7 +284,7 @@ describe('DiffsStoreActions', () => { ...@@ -269,7 +284,7 @@ describe('DiffsStoreActions', () => {
describe('setInlineDiffViewType', () => { describe('setInlineDiffViewType', () => {
it('should set diff view type to inline and also set the cookie properly', done => { it('should set diff view type to inline and also set the cookie properly', done => {
testAction( testAction(
actions.setInlineDiffViewType, setInlineDiffViewType,
null, null,
{}, {},
[{ type: types.SET_DIFF_VIEW_TYPE, payload: INLINE_DIFF_VIEW_TYPE }], [{ type: types.SET_DIFF_VIEW_TYPE, payload: INLINE_DIFF_VIEW_TYPE }],
...@@ -287,7 +302,7 @@ describe('DiffsStoreActions', () => { ...@@ -287,7 +302,7 @@ describe('DiffsStoreActions', () => {
describe('setParallelDiffViewType', () => { describe('setParallelDiffViewType', () => {
it('should set diff view type to parallel and also set the cookie properly', done => { it('should set diff view type to parallel and also set the cookie properly', done => {
testAction( testAction(
actions.setParallelDiffViewType, setParallelDiffViewType,
null, null,
{}, {},
[{ type: types.SET_DIFF_VIEW_TYPE, payload: PARALLEL_DIFF_VIEW_TYPE }], [{ type: types.SET_DIFF_VIEW_TYPE, payload: PARALLEL_DIFF_VIEW_TYPE }],
...@@ -307,7 +322,7 @@ describe('DiffsStoreActions', () => { ...@@ -307,7 +322,7 @@ describe('DiffsStoreActions', () => {
const payload = { lineCode: 'lineCode' }; const payload = { lineCode: 'lineCode' };
testAction( testAction(
actions.showCommentForm, showCommentForm,
payload, payload,
{}, {},
[{ type: types.ADD_COMMENT_FORM_LINE, payload }], [{ type: types.ADD_COMMENT_FORM_LINE, payload }],
...@@ -322,7 +337,7 @@ describe('DiffsStoreActions', () => { ...@@ -322,7 +337,7 @@ describe('DiffsStoreActions', () => {
const payload = { lineCode: 'lineCode' }; const payload = { lineCode: 'lineCode' };
testAction( testAction(
actions.cancelCommentForm, cancelCommentForm,
payload, payload,
{}, {},
[{ type: types.REMOVE_COMMENT_FORM_LINE, payload }], [{ type: types.REMOVE_COMMENT_FORM_LINE, payload }],
...@@ -344,7 +359,7 @@ describe('DiffsStoreActions', () => { ...@@ -344,7 +359,7 @@ describe('DiffsStoreActions', () => {
mock.onGet(endpoint).reply(200, contextLines); mock.onGet(endpoint).reply(200, contextLines);
testAction( testAction(
actions.loadMoreLines, loadMoreLines,
options, options,
{}, {},
[ [
...@@ -370,7 +385,7 @@ describe('DiffsStoreActions', () => { ...@@ -370,7 +385,7 @@ describe('DiffsStoreActions', () => {
mock.onGet(file.loadCollapsedDiffUrl).reply(200, data); mock.onGet(file.loadCollapsedDiffUrl).reply(200, data);
testAction( testAction(
actions.loadCollapsedDiff, loadCollapsedDiff,
file, file,
{}, {},
[ [
...@@ -391,7 +406,7 @@ describe('DiffsStoreActions', () => { ...@@ -391,7 +406,7 @@ describe('DiffsStoreActions', () => {
describe('expandAllFiles', () => { describe('expandAllFiles', () => {
it('should change the collapsed prop from the diffFiles', done => { it('should change the collapsed prop from the diffFiles', done => {
testAction( testAction(
actions.expandAllFiles, expandAllFiles,
null, null,
{}, {},
[ [
...@@ -415,7 +430,7 @@ describe('DiffsStoreActions', () => { ...@@ -415,7 +430,7 @@ describe('DiffsStoreActions', () => {
const dispatch = jasmine.createSpy('dispatch'); const dispatch = jasmine.createSpy('dispatch');
actions.toggleFileDiscussions({ getters, dispatch }); toggleFileDiscussions({ getters, dispatch });
expect(dispatch).toHaveBeenCalledWith( expect(dispatch).toHaveBeenCalledWith(
'collapseDiscussion', 'collapseDiscussion',
...@@ -433,7 +448,7 @@ describe('DiffsStoreActions', () => { ...@@ -433,7 +448,7 @@ describe('DiffsStoreActions', () => {
const dispatch = jasmine.createSpy(); const dispatch = jasmine.createSpy();
actions.toggleFileDiscussions({ getters, dispatch }); toggleFileDiscussions({ getters, dispatch });
expect(dispatch).toHaveBeenCalledWith( expect(dispatch).toHaveBeenCalledWith(
'expandDiscussion', 'expandDiscussion',
...@@ -451,7 +466,7 @@ describe('DiffsStoreActions', () => { ...@@ -451,7 +466,7 @@ describe('DiffsStoreActions', () => {
const dispatch = jasmine.createSpy(); const dispatch = jasmine.createSpy();
actions.toggleFileDiscussions({ getters, dispatch }); toggleFileDiscussions({ getters, dispatch });
expect(dispatch).toHaveBeenCalledWith( expect(dispatch).toHaveBeenCalledWith(
'expandDiscussion', 'expandDiscussion',
...@@ -460,4 +475,111 @@ describe('DiffsStoreActions', () => { ...@@ -460,4 +475,111 @@ describe('DiffsStoreActions', () => {
); );
}); });
}); });
describe('scrollToLineIfNeededInline', () => {
const lineMock = {
lineCode: 'ABC_123',
};
it('should not call handleLocationHash when there is not hash', () => {
window.location.hash = '';
const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub();
scrollToLineIfNeededInline({}, lineMock);
expect(handleLocationHashSpy).not.toHaveBeenCalled();
});
it('should not call handleLocationHash when the hash does not match any line', () => {
window.location.hash = 'XYZ_456';
const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub();
scrollToLineIfNeededInline({}, lineMock);
expect(handleLocationHashSpy).not.toHaveBeenCalled();
});
it('should call handleLocationHash only when the hash matches a line', () => {
window.location.hash = 'ABC_123';
const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub();
scrollToLineIfNeededInline(
{},
{
lineCode: 'ABC_456',
},
);
scrollToLineIfNeededInline({}, lineMock);
scrollToLineIfNeededInline(
{},
{
lineCode: 'XYZ_456',
},
);
expect(handleLocationHashSpy).toHaveBeenCalled();
expect(handleLocationHashSpy).toHaveBeenCalledTimes(1);
});
});
describe('scrollToLineIfNeededParallel', () => {
const lineMock = {
left: null,
right: {
lineCode: 'ABC_123',
},
};
it('should not call handleLocationHash when there is not hash', () => {
window.location.hash = '';
const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub();
scrollToLineIfNeededParallel({}, lineMock);
expect(handleLocationHashSpy).not.toHaveBeenCalled();
});
it('should not call handleLocationHash when the hash does not match any line', () => {
window.location.hash = 'XYZ_456';
const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub();
scrollToLineIfNeededParallel({}, lineMock);
expect(handleLocationHashSpy).not.toHaveBeenCalled();
});
it('should call handleLocationHash only when the hash matches a line', () => {
window.location.hash = 'ABC_123';
const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub();
scrollToLineIfNeededParallel(
{},
{
left: null,
right: {
lineCode: 'ABC_456',
},
},
);
scrollToLineIfNeededParallel({}, lineMock);
scrollToLineIfNeededParallel(
{},
{
left: null,
right: {
lineCode: 'XYZ_456',
},
},
);
expect(handleLocationHashSpy).toHaveBeenCalled();
expect(handleLocationHashSpy).toHaveBeenCalledTimes(1);
});
});
}); });
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