Commit 9091a5c4 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee-2018-10-02' into 'master'

CE upstream - 2018-10-02 10:46 UTC

See merge request gitlab-org/gitlab-ee!7765
parents 76faa18c 70cdaa7c
......@@ -758,7 +758,7 @@ GEM
retriable (3.1.2)
rinku (2.0.0)
rotp (2.1.2)
rouge (3.2.1)
rouge (3.3.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
......
......@@ -767,7 +767,7 @@ GEM
retriable (3.1.2)
rinku (2.0.0)
rotp (2.1.2)
rouge (3.2.1)
rouge (3.3.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
......
......@@ -22,6 +22,7 @@ import './components/board_sidebar';
import './components/new_list_dropdown';
import BoardAddIssuesModal from './components/modal/index.vue';
import '~/vue_shared/vue_resource_interceptor';
import { NavigationType } from '~/lib/utils/common_utils';
import 'ee/boards/models/list';
import 'ee/boards/models/issue';
......@@ -42,6 +43,16 @@ export default () => {
window.gl = window.gl || {};
// check for browser back and trigger a hard reload to circumvent browser caching.
window.addEventListener('pageshow', (event) => {
const isNavTypeBackForward = window.performance &&
window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
if (event.persisted || isNavTypeBackForward) {
window.location.reload();
}
});
if (gl.IssueBoardsApp) {
gl.IssueBoardsApp.$destroy(true);
}
......
......@@ -2,54 +2,114 @@ import _ from 'underscore';
export const placeholderImage =
'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
const SCROLL_THRESHOLD = 300;
const SCROLL_THRESHOLD = 500;
export default class LazyLoader {
constructor(options = {}) {
this.intersectionObserver = null;
this.lazyImages = [];
this.observerNode = options.observerNode || '#content-body';
const throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300);
const debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300);
window.addEventListener('scroll', throttledScrollCheck);
window.addEventListener('resize', debouncedElementsInView);
const scrollContainer = options.scrollContainer || window;
scrollContainer.addEventListener('load', () => this.loadCheck());
scrollContainer.addEventListener('load', () => this.register());
}
static supportsIntersectionObserver() {
return 'IntersectionObserver' in window;
}
searchLazyImages() {
const that = this;
requestIdleCallback(
() => {
that.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
const lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
if (that.lazyImages.length) {
that.checkElementsInView();
if (LazyLoader.supportsIntersectionObserver()) {
if (this.intersectionObserver) {
lazyImages.forEach(img => this.intersectionObserver.observe(img));
}
} else if (lazyImages.length) {
this.lazyImages = lazyImages;
this.checkElementsInView();
}
},
{ timeout: 500 },
);
}
startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
if (contentNode) {
const observer = new MutationObserver(() => this.searchLazyImages());
this.mutationObserver = new MutationObserver(() => this.searchLazyImages());
observer.observe(contentNode, {
this.mutationObserver.observe(contentNode, {
childList: true,
subtree: true,
});
}
}
loadCheck() {
this.searchLazyImages();
stopContentObserver() {
if (this.mutationObserver) {
this.mutationObserver.takeRecords();
this.mutationObserver.disconnect();
this.mutationObserver = null;
}
}
unregister() {
this.stopContentObserver();
if (this.intersectionObserver) {
this.intersectionObserver.takeRecords();
this.intersectionObserver.disconnect();
this.intersectionObserver = null;
}
if (this.throttledScrollCheck) {
window.removeEventListener('scroll', this.throttledScrollCheck);
}
if (this.debouncedElementsInView) {
window.removeEventListener('resize', this.debouncedElementsInView);
}
}
register() {
if (LazyLoader.supportsIntersectionObserver()) {
this.startIntersectionObserver();
} else {
this.startLegacyObserver();
}
this.startContentObserver();
this.searchLazyImages();
}
startIntersectionObserver = () => {
this.throttledElementsInView = _.throttle(() => this.checkElementsInView(), 300);
this.intersectionObserver = new IntersectionObserver(this.onIntersection, {
rootMargin: `${SCROLL_THRESHOLD}px 0px`,
thresholds: 0.1,
});
};
onIntersection = entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.intersectionObserver.unobserve(entry.target);
this.lazyImages.push(entry.target);
}
});
this.throttledElementsInView();
};
startLegacyObserver() {
this.throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300);
this.debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300);
window.addEventListener('scroll', this.throttledScrollCheck);
window.addEventListener('resize', this.debouncedElementsInView);
}
scrollCheck() {
requestAnimationFrame(() => this.checkElementsInView());
}
checkElementsInView() {
const scrollTop = window.pageYOffset;
const visHeight = scrollTop + window.innerHeight + SCROLL_THRESHOLD;
......@@ -61,18 +121,29 @@ export default class LazyLoader {
const imgTop = scrollTop + imgBoundRect.top;
const imgBound = imgTop + imgBoundRect.height;
if (scrollTop < imgBound && visHeight > imgTop) {
if (scrollTop <= imgBound && visHeight >= imgTop) {
requestAnimationFrame(() => {
LazyLoader.loadImage(selectedImage);
});
return false;
}
/*
If we are scrolling fast, the img we watched intersecting could have left the view port.
So we are going watch for new intersections.
*/
if (LazyLoader.supportsIntersectionObserver()) {
if (this.intersectionObserver) {
this.intersectionObserver.observe(selectedImage);
}
return false;
}
return true;
}
return false;
});
}
static loadImage(img) {
if (img.getAttribute('data-src')) {
let imgUrl = img.getAttribute('data-src');
......
......@@ -616,6 +616,17 @@ export const roundOffFloat = (number, precision = 0) => {
return Math.round(number * multiplier) / multiplier;
};
/**
* Represents navigation type constants of the Performance Navigation API.
* Detailed explanation see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigation.
*/
export const NavigationType = {
TYPE_NAVIGATE: 0,
TYPE_RELOAD: 1,
TYPE_BACK_FORWARD: 2,
TYPE_RESERVED: 255,
};
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
......
......@@ -236,16 +236,16 @@ class IssuableFinder
# rubocop: enable CodeReuse/ActiveRecord
def assignee_id?
params[:assignee_id].present? && params[:assignee_id] != NONE
params[:assignee_id].present? && params[:assignee_id].to_s != NONE
end
def assignee_username?
params[:assignee_username].present? && params[:assignee_username] != NONE
params[:assignee_username].present? && params[:assignee_username].to_s != NONE
end
def no_assignee?
# Assignee_id takes precedence over assignee_username
params[:assignee_id] == NONE || params[:assignee_username] == NONE
params[:assignee_id].to_s == NONE || params[:assignee_username].to_s == NONE
end
# rubocop: disable CodeReuse/ActiveRecord
......
---
title: Fix stale issue boards after browser back
merge_request: 22006
author: Johann Hubert Sonntagbauer
type: fixed
---
title: Improve lazy image loading performance by using IntersectionObserver where
available
merge_request: 21565
author:
type: performance
---
title: Filter issues without an Assignee via the API
merge_request: 22009
author: Eva Kadlecová
type: fixed
---
title: Update to Rouge 3.3.0 including frozen string literals for improved memory
usage
merge_request:
author:
type: changed
......@@ -110,7 +110,8 @@ At this point the script would ask you to select the category of the change (map
4. New deprecation
5. Feature removal
6. Security fix
7. Other
7. Performance improvement
8. Other
```
The entry filename is based on the name of the current Git branch. If you run
......
require 'rails_helper'
describe 'Ensure Boards do not show stale data on browser back', :js do
let(:project) {create(:project, :public)}
let(:board) {create(:board, project: project)}
let(:user) {create(:user)}
context 'authorized user' do
before do
project.add_maintainer(user)
sign_in(user)
visit project_board_path(project, board)
wait_for_requests
page.within(first('.board .issue-count-badge-count')) do
expect(page).to have_content('0')
end
end
it 'created issue is listed on board' do
visit new_project_issue_path(project)
wait_for_requests
fill_in 'issue_title', with: 'issue should be shown'
click_button 'Submit issue'
page.go_back
wait_for_requests
page.go_back
wait_for_requests
page.within(first('.board .issue-count-badge-count')) do
expect(page).to have_content('1')
end
page.within(first('.board-card')) do
issue = project.issues.find_by_title('issue should be shown')
expect(page).to have_content(issue.to_reference)
expect(page).to have_link(issue.title, href: issue_path(issue))
end
end
end
end
......@@ -102,6 +102,14 @@ describe IssuesFinder do
end
end
context 'filtering by no assignee' do
let(:params) { { assignee_id: 0 } }
it 'returns issues not assign to any assignee' do
expect(issues).to contain_exactly(issue4)
end
end
context 'filtering by group_id' do
let(:params) { { group_id: group.id } }
......
import LazyLoader from '~/lazy_loader';
import { TEST_HOST } from './test_constants';
let lazyLoader = null;
const execImmediately = callback => {
callback();
};
describe('LazyLoader', function() {
preloadFixtures('issues/issue_with_comment.html.raw');
describe('with IntersectionObserver disabled', () => {
beforeEach(function() {
loadFixtures('issues/issue_with_comment.html.raw');
lazyLoader = new LazyLoader({
observerNode: 'foobar',
});
spyOn(LazyLoader, 'supportsIntersectionObserver').and.callFake(() => false);
spyOn(LazyLoader, 'loadImage').and.callThrough();
spyOn(window, 'requestAnimationFrame').and.callFake(execImmediately);
spyOn(window, 'requestIdleCallback').and.callFake(execImmediately);
// Doing everything that happens normally in onload
lazyLoader.register();
});
afterEach(() => {
lazyLoader.unregister();
});
it('should copy value from data-src to src for img 1', function(done) {
const img = document.querySelectorAll('img[data-src]')[0];
const originalDataSrc = img.getAttribute('data-src');
img.scrollIntoView();
setTimeout(() => {
expect(LazyLoader.loadImage).toHaveBeenCalled();
expect(img.getAttribute('src')).toBe(originalDataSrc);
expect(img).toHaveClass('js-lazy-loaded');
done();
}, 50);
});
it('should lazy load dynamically added data-src images', function(done) {
const newImg = document.createElement('img');
const testPath = `${TEST_HOST}/img/testimg.png`;
newImg.className = 'lazy';
newImg.setAttribute('data-src', testPath);
document.body.appendChild(newImg);
newImg.scrollIntoView();
setTimeout(() => {
expect(LazyLoader.loadImage).toHaveBeenCalled();
expect(newImg.getAttribute('src')).toBe(testPath);
expect(newImg).toHaveClass('js-lazy-loaded');
done();
}, 50);
});
it('should not alter normal images', function(done) {
const newImg = document.createElement('img');
const testPath = `${TEST_HOST}/img/testimg.png`;
newImg.setAttribute('src', testPath);
document.body.appendChild(newImg);
newImg.scrollIntoView();
setTimeout(() => {
expect(LazyLoader.loadImage).not.toHaveBeenCalled();
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
}, 50);
});
it('should not load dynamically added pictures if content observer is turned off', done => {
lazyLoader.stopContentObserver();
const newImg = document.createElement('img');
const testPath = `${TEST_HOST}/img/testimg.png`;
newImg.className = 'lazy';
newImg.setAttribute('data-src', testPath);
document.body.appendChild(newImg);
newImg.scrollIntoView();
setTimeout(() => {
expect(LazyLoader.loadImage).not.toHaveBeenCalled();
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
}, 50);
});
it('should load dynamically added pictures if content observer is turned off and on again', done => {
lazyLoader.stopContentObserver();
lazyLoader.startContentObserver();
const newImg = document.createElement('img');
const testPath = `${TEST_HOST}/img/testimg.png`;
newImg.className = 'lazy';
newImg.setAttribute('data-src', testPath);
document.body.appendChild(newImg);
newImg.scrollIntoView();
setTimeout(() => {
expect(LazyLoader.loadImage).toHaveBeenCalled();
expect(newImg).toHaveClass('js-lazy-loaded');
done();
}, 50);
});
});
describe('with IntersectionObserver enabled', () => {
beforeEach(function() {
loadFixtures('issues/issue_with_comment.html.raw');
lazyLoader = new LazyLoader({
observerNode: 'body',
observerNode: 'foobar',
});
spyOn(LazyLoader, 'loadImage').and.callThrough();
spyOn(window, 'requestAnimationFrame').and.callFake(execImmediately);
spyOn(window, 'requestIdleCallback').and.callFake(execImmediately);
// Doing everything that happens normally in onload
lazyLoader.loadCheck();
lazyLoader.register();
});
describe('behavior', function() {
afterEach(() => {
lazyLoader.unregister();
});
it('should copy value from data-src to src for img 1', function(done) {
const img = document.querySelectorAll('img[data-src]')[0];
const originalDataSrc = img.getAttribute('data-src');
img.scrollIntoView();
setTimeout(() => {
expect(LazyLoader.loadImage).toHaveBeenCalled();
expect(img.getAttribute('src')).toBe(originalDataSrc);
expect(document.getElementsByClassName('js-lazy-loaded').length).toBeGreaterThan(0);
expect(img).toHaveClass('js-lazy-loaded');
done();
}, 100);
}, 50);
});
it('should lazy load dynamically added data-src images', function(done) {
const newImg = document.createElement('img');
const testPath = '/img/testimg.png';
const testPath = `${TEST_HOST}/img/testimg.png`;
newImg.className = 'lazy';
newImg.setAttribute('data-src', testPath);
document.body.appendChild(newImg);
newImg.scrollIntoView();
setTimeout(() => {
expect(LazyLoader.loadImage).toHaveBeenCalled();
expect(newImg.getAttribute('src')).toBe(testPath);
expect(document.getElementsByClassName('js-lazy-loaded').length).toBeGreaterThan(0);
expect(newImg).toHaveClass('js-lazy-loaded');
done();
}, 100);
}, 50);
});
it('should not alter normal images', function(done) {
const newImg = document.createElement('img');
const testPath = '/img/testimg.png';
const testPath = `${TEST_HOST}/img/testimg.png`;
newImg.setAttribute('src', testPath);
document.body.appendChild(newImg);
newImg.scrollIntoView();
setTimeout(() => {
expect(LazyLoader.loadImage).not.toHaveBeenCalled();
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
}, 100);
}, 50);
});
it('should not load dynamically added pictures if content observer is turned off', done => {
lazyLoader.stopContentObserver();
const newImg = document.createElement('img');
const testPath = `${TEST_HOST}/img/testimg.png`;
newImg.className = 'lazy';
newImg.setAttribute('data-src', testPath);
document.body.appendChild(newImg);
newImg.scrollIntoView();
setTimeout(() => {
expect(LazyLoader.loadImage).not.toHaveBeenCalled();
expect(newImg).not.toHaveClass('js-lazy-loaded');
done();
}, 50);
});
it('should load dynamically added pictures if content observer is turned off and on again', done => {
lazyLoader.stopContentObserver();
lazyLoader.startContentObserver();
const newImg = document.createElement('img');
const testPath = `${TEST_HOST}/img/testimg.png`;
newImg.className = 'lazy';
newImg.setAttribute('data-src', testPath);
document.body.appendChild(newImg);
newImg.scrollIntoView();
setTimeout(() => {
expect(LazyLoader.loadImage).toHaveBeenCalled();
expect(newImg).toHaveClass('js-lazy-loaded');
done();
}, 50);
});
});
});
......@@ -172,6 +172,15 @@ describe API::Issues do
expect(first_issue['id']).to eq(issue2.id)
end
it 'returns issues with no assignee' do
issue2 = create(:issue, author: user2, project: project)
get api('/issues', user), assignee_id: 0, scope: 'all'
expect_paginated_array_response(size: 1)
expect(first_issue['id']).to eq(issue2.id)
end
it 'returns issues reacted by the authenticated user by the given emoji' do
issue2 = create(:issue, project: project, author: user, assignees: [user])
award_emoji = create(:award_emoji, awardable: issue2, user: user2, name: 'star')
......
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