Commit 5a08ff83 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'tz-preload-mr-list' into 'master'

Prefetches Issue, MR and ToDo Lists

See merge request gitlab-org/gitlab!76540
parents b60c838b 949bf033
...@@ -13,3 +13,42 @@ export default function findAndFollowLink(selector) { ...@@ -13,3 +13,42 @@ export default function findAndFollowLink(selector) {
visitUrl(link); visitUrl(link);
} }
} }
export function prefetchDocument(url) {
const newPrefetchLink = document.createElement('link');
newPrefetchLink.rel = 'prefetch';
newPrefetchLink.href = url;
newPrefetchLink.setAttribute('as', 'document');
document.head.appendChild(newPrefetchLink);
}
export function initPrefetchLinks(selector) {
document.querySelectorAll(selector).forEach((el) => {
let mouseOverTimer;
const mouseOutHandler = () => {
if (mouseOverTimer) {
clearTimeout(mouseOverTimer);
mouseOverTimer = undefined;
}
};
const mouseOverHandler = () => {
el.addEventListener('mouseout', mouseOutHandler, { once: true, passive: true });
mouseOverTimer = setTimeout(() => {
if (el.href) prefetchDocument(el.href);
// Only execute once
el.removeEventListener('mouseover', mouseOverHandler, true);
mouseOverTimer = undefined;
}, 100);
};
el.addEventListener('mouseover', mouseOverHandler, {
capture: true,
passive: true,
});
});
}
...@@ -14,6 +14,7 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; ...@@ -14,6 +14,7 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { initRails } from '~/lib/utils/rails_ujs'; import { initRails } from '~/lib/utils/rails_ujs';
import * as popovers from '~/popovers'; import * as popovers from '~/popovers';
import * as tooltips from '~/tooltips'; import * as tooltips from '~/tooltips';
import { initPrefetchLinks } from '~/lib/utils/navigation_utility';
import initAlertHandler from './alert_handler'; import initAlertHandler from './alert_handler';
import { addDismissFlashClickListener } from './flash'; import { addDismissFlashClickListener } from './flash';
import initTodoToggle from './header'; import initTodoToggle from './header';
...@@ -90,6 +91,7 @@ function deferredInitialisation() { ...@@ -90,6 +91,7 @@ function deferredInitialisation() {
initTopNav(); initTopNav();
initBreadcrumbs(); initBreadcrumbs();
initTodoToggle(); initTodoToggle();
initPrefetchLinks('.js-prefetch-document');
initLogoAnimation(); initLogoAnimation();
initServicePingConsent(); initServicePingConsent();
initUserPopovers(); initUserPopovers();
......
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
= sprite_icon(search_menu_item.fetch(:icon)) = sprite_icon(search_menu_item.fetch(:icon))
- if header_link?(:issues) - if header_link?(:issues)
= nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do = nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do
= link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') }, = link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues js-prefetch-document', aria: { label: _('Issues') },
data: { qa_selector: 'issues_shortcut_button', toggle: 'tooltip', placement: 'bottom', data: { qa_selector: 'issues_shortcut_button', toggle: 'tooltip', placement: 'bottom',
track_label: 'main_navigation', track_label: 'main_navigation',
track_action: 'click_issues_link', track_action: 'click_issues_link',
...@@ -75,18 +75,18 @@ ...@@ -75,18 +75,18 @@
%li.dropdown-header %li.dropdown-header
= _('Merge requests') = _('Merge requests')
%li %li
= link_to assigned_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center' do = link_to assigned_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center js-prefetch-document' do
= _('Assigned to you') = _('Assigned to you')
%span.badge.gl-badge.badge-pill.badge-muted.merge-request-badge.gl-ml-auto.js-assigned-mr-count{ class: "" } %span.badge.gl-badge.badge-pill.badge-muted.merge-request-badge.gl-ml-auto.js-assigned-mr-count{ class: "" }
= user_merge_requests_counts[:assigned] = user_merge_requests_counts[:assigned]
%li %li
= link_to reviewer_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center' do = link_to reviewer_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center js-prefetch-document' do
= _('Review requests for you') = _('Review requests for you')
%span.badge.gl-badge.badge-pill.badge-muted.merge-request-badge.gl-ml-auto.js-reviewer-mr-count{ class: "" } %span.badge.gl-badge.badge-pill.badge-muted.merge-request-badge.gl-ml-auto.js-reviewer-mr-count{ class: "" }
= user_merge_requests_counts[:review_requested] = user_merge_requests_counts[:review_requested]
- if header_link?(:todos) - if header_link?(:todos)
= nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
= link_to dashboard_todos_path, title: _('To-Do List'), aria: { label: _('To-Do List') }, class: 'shortcuts-todos', = link_to dashboard_todos_path, title: _('To-Do List'), aria: { label: _('To-Do List') }, class: 'shortcuts-todos js-prefetch-document',
data: { qa_selector: 'todos_shortcut_button', toggle: 'tooltip', placement: 'bottom', data: { qa_selector: 'todos_shortcut_button', toggle: 'tooltip', placement: 'bottom',
track_label: 'main_navigation', track_label: 'main_navigation',
track_action: 'click_to_do_link', track_action: 'click_to_do_link',
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.issuable-main-info .issuable-main-info
.merge-request-title.title .merge-request-title.title
%span.merge-request-title-text.js-onboarding-mr-item %span.merge-request-title-text.js-onboarding-mr-item
= link_to merge_request.title, merge_request_path(merge_request) = link_to merge_request.title, merge_request_path(merge_request), class: 'js-prefetch-document'
- if merge_request.tasks? - if merge_request.tasks?
%span.task-status.d-none.d-sm-inline-block %span.task-status.d-none.d-sm-inline-block
   
......
import findAndFollowLink from '~/lib/utils/navigation_utility'; import findAndFollowLink from '~/lib/utils/navigation_utility';
import * as navigationUtils from '~/lib/utils/navigation_utility';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility'); jest.mock('~/lib/utils/url_utility');
...@@ -21,3 +22,91 @@ describe('findAndFollowLink', () => { ...@@ -21,3 +22,91 @@ describe('findAndFollowLink', () => {
expect(visitUrl).not.toHaveBeenCalled(); expect(visitUrl).not.toHaveBeenCalled();
}); });
}); });
describe('prefetchDocument', () => {
it('creates a prefetch link tag', () => {
const linkElement = document.createElement('link');
jest.spyOn(document, 'createElement').mockImplementation(() => linkElement);
jest.spyOn(document.head, 'appendChild');
navigationUtils.prefetchDocument('index.htm');
expect(document.head.appendChild).toHaveBeenCalledWith(linkElement);
expect(linkElement.href).toEqual('http://test.host/index.htm');
expect(linkElement.rel).toEqual('prefetch');
expect(linkElement.getAttribute('as')).toEqual('document');
});
});
describe('initPrefetchLinks', () => {
let newLink;
beforeEach(() => {
newLink = document.createElement('a');
newLink.href = 'index_prefetch.htm';
newLink.classList.add('js-test-prefetch-link');
document.body.appendChild(newLink);
});
it('adds to all links mouse out handlers when hovered', () => {
const mouseOverEvent = new Event('mouseover');
jest.spyOn(newLink, 'addEventListener');
navigationUtils.initPrefetchLinks('.js-test-prefetch-link');
newLink.dispatchEvent(mouseOverEvent);
expect(newLink.addEventListener).toHaveBeenCalled();
});
it('it is not fired when less then 100ms over link', () => {
const mouseOverEvent = new Event('mouseover');
const mouseOutEvent = new Event('mouseout');
jest.spyOn(newLink, 'addEventListener');
jest.spyOn(navigationUtils, 'prefetchDocument').mockImplementation(() => true);
navigationUtils.initPrefetchLinks('.js-test-prefetch-link');
newLink.dispatchEvent(mouseOverEvent);
newLink.dispatchEvent(mouseOutEvent);
expect(navigationUtils.prefetchDocument).not.toHaveBeenCalled();
});
describe('executes correctly when hovering long enough', () => {
const mouseOverEvent = new Event('mouseover');
beforeEach(() => {
jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');
jest.spyOn(newLink, 'removeEventListener');
});
it('calls prefetchDocument which adds to document', () => {
jest.spyOn(document.head, 'appendChild');
navigationUtils.initPrefetchLinks('.js-test-prefetch-link');
newLink.dispatchEvent(mouseOverEvent);
jest.runAllTimers();
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 100);
expect(document.head.appendChild).toHaveBeenCalled();
});
it('removes Event Listener when fired so only done once', () => {
navigationUtils.initPrefetchLinks('.js-test-prefetch-link');
newLink.dispatchEvent(mouseOverEvent);
jest.runAllTimers();
expect(newLink.removeEventListener).toHaveBeenCalledWith(
'mouseover',
expect.any(Function),
true,
);
});
});
});
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