Merge branch '35224-transform-user-profile-javascript-into-async-bundle' into 'master'

Resolve "Transform user profile javascript into async bundle"

Closes #35224

See merge request !12929
This commit is contained in:
Tim Zallmann 2017-07-28 07:27:13 +00:00
commit 7e17f9bc89
10 changed files with 241 additions and 268 deletions

View file

@ -538,6 +538,13 @@ import GpgBadges from './gpg_badges';
case 'protected_branches':
shortcut_handler = new ShortcutsNavigation();
}
break;
case 'users':
const action = path[1];
import(/* webpackChunkName: 'user_profile' */ './users')
.then(user => user.default(action))
.catch(() => {});
break;
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {

View file

@ -1,10 +1,28 @@
/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len, class-methods-use-this */
import d3 from 'd3';
const LOADING_HTML = `
<div class="text-center">
<i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i>
</div>
`;
function formatTooltipText({ date, count }) {
const dateObject = new Date(date);
const dateDayName = gl.utils.getDayName(dateObject);
const dateText = dateObject.format('mmm d, yyyy');
let contribText = 'No contributions';
if (count > 0) {
contribText = `${count} contribution${count > 1 ? 's' : ''}`;
}
return `${contribText}<br />${dateDayName} ${dateText}`;
}
const initColorKey = () => d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]);
export default class ActivityCalendar {
constructor(timestamps, calendar_activities_path) {
this.calendar_activities_path = calendar_activities_path;
constructor(container, timestamps, calendarActivitiesPath) {
this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
this.daySpace = 1;
@ -12,25 +30,26 @@ export default class ActivityCalendar {
this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
this.months = [];
// Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are
this.timestampsTmp = [];
var group = 0;
let group = 0;
var today = new Date();
const today = new Date();
today.setHours(0, 0, 0, 0, 0);
var oneYearAgo = new Date(today);
const oneYearAgo = new Date(today);
oneYearAgo.setFullYear(today.getFullYear() - 1);
var days = gl.utils.getDayDifference(oneYearAgo, today);
const days = gl.utils.getDayDifference(oneYearAgo, today);
for (var i = 0; i <= days; i += 1) {
var date = new Date(oneYearAgo);
for (let i = 0; i <= days; i += 1) {
const date = new Date(oneYearAgo);
date.setDate(date.getDate() + i);
var day = date.getDay();
var count = timestamps[date.format('yyyy-mm-dd')];
const day = date.getDay();
const count = timestamps[date.format('yyyy-mm-dd')] || 0;
// Create a new group array if this is the first day of the week
// or if is first object
@ -39,129 +58,119 @@ export default class ActivityCalendar {
group += 1;
}
var innerArray = this.timestampsTmp[group - 1];
// Push to the inner array the values that will be used to render map
innerArray.push({
count: count || 0,
date: date,
day: day
});
const innerArray = this.timestampsTmp[group - 1];
innerArray.push({ count, date, day });
}
// Init color functions
this.colorKey = this.initColorKey();
this.colorKey = initColorKey();
this.color = this.initColor();
// Init the svg element
this.renderSvg(group);
this.svg = this.renderSvg(container, group);
this.renderDays();
this.renderMonths();
this.renderDayTitles();
this.renderKey();
this.initTooltips();
// Init tooltips
$(`${container} .js-tooltip`).tooltip({ html: true });
}
// Add extra padding for the last month label if it is also the last column
getExtraWidthPadding(group) {
var extraWidthPadding = 0;
var lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth();
var secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth();
let extraWidthPadding = 0;
const lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth();
const secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth();
if (lastColMonth != secondLastColMonth) {
if (lastColMonth !== secondLastColMonth) {
extraWidthPadding = 3;
}
return extraWidthPadding;
}
renderSvg(group) {
var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', width).attr('height', 167).attr('class', 'contrib-calendar');
renderSvg(container, group) {
const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group);
return d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', 167)
.attr('class', 'contrib-calendar');
}
renderDays() {
return this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g').attr('transform', (function(_this) {
return function(group, i) {
_.each(group, function(stamp, a) {
var lastMonth, lastMonthX, month, x;
this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g')
.attr('transform', (group, i) => {
_.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) {
month = stamp.date.getMonth();
x = (_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace;
lastMonth = _.last(_this.months);
if (lastMonth != null) {
lastMonthX = lastMonth.x;
}
if (lastMonth == null) {
return _this.months.push({
month: month,
x: x
});
} else if (month !== lastMonth.month && x - _this.daySizeWithSpace !== lastMonthX) {
return _this.months.push({
month: month,
x: x
});
const month = stamp.date.getMonth();
const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months);
if (
lastMonth == null ||
(month !== lastMonth.month && x - this.daySizeWithSpace !== lastMonth.x)
) {
this.months.push({ month, x });
}
}
});
return "translate(" + ((_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace) + ", 18)";
};
})(this)).selectAll('rect').data(function(stamp) {
return stamp;
}).enter().append('rect').attr('x', '0').attr('y', (function(_this) {
return function(stamp, i) {
return _this.daySizeWithSpace * stamp.day;
};
})(this)).attr('width', this.daySize).attr('height', this.daySize).attr('title', (function(_this) {
return function(stamp) {
var contribText, date, dateText;
date = new Date(stamp.date);
contribText = 'No contributions';
if (stamp.count > 0) {
contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : '');
}
dateText = date.format('mmm d, yyyy');
return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText;
};
})(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) {
return function(stamp) {
if (stamp.count !== 0) {
return _this.color(Math.min(stamp.count, 40));
} else {
return '#ededed';
}
};
})(this)).attr('data-container', 'body').on('click', this.clickDay);
return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`;
})
.selectAll('rect')
.data(stamp => stamp)
.enter()
.append('rect')
.attr('x', '0')
.attr('y', stamp => this.daySizeWithSpace * stamp.day)
.attr('width', this.daySize)
.attr('height', this.daySize)
.attr('fill', stamp => (
stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'
))
.attr('title', stamp => formatTooltipText(stamp))
.attr('class', 'user-contrib-cell js-tooltip')
.attr('data-container', 'body')
.on('click', this.clickDay);
}
renderDayTitles() {
var days;
days = [
const days = [
{
text: 'M',
y: 29 + (this.daySizeWithSpace * 1)
y: 29 + (this.daySizeWithSpace * 1),
}, {
text: 'W',
y: 29 + (this.daySizeWithSpace * 3)
y: 29 + (this.daySizeWithSpace * 3),
}, {
text: 'F',
y: 29 + (this.daySizeWithSpace * 5)
}
y: 29 + (this.daySizeWithSpace * 5),
},
];
return this.svg.append('g').selectAll('text').data(days).enter().append('text').attr('text-anchor', 'middle').attr('x', 8).attr('y', function(day) {
return day.y;
}).text(function(day) {
return day.text;
}).attr('class', 'user-contrib-text');
this.svg.append('g')
.selectAll('text')
.data(days)
.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('x', 8)
.attr('y', day => day.y)
.text(day => day.text)
.attr('class', 'user-contrib-text');
}
renderMonths() {
return this.svg.append('g').attr('direction', 'ltr').selectAll('text').data(this.months).enter().append('text').attr('x', function(date) {
return date.x;
}).attr('y', 10).attr('class', 'user-contrib-text').text((function(_this) {
return function(date) {
return _this.monthNames[date.month];
};
})(this));
this.svg.append('g')
.attr('direction', 'ltr')
.selectAll('text')
.data(this.months)
.enter()
.append('text')
.attr('x', date => date.x)
.attr('y', 10)
.attr('class', 'user-contrib-text')
.text(date => this.monthNames[date.month]);
}
renderKey() {
@ -169,7 +178,7 @@ export default class ActivityCalendar {
const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
this.svg.append('g')
.attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`)
.selectAll('rect')
.data(keyColors)
.enter()
@ -185,43 +194,31 @@ export default class ActivityCalendar {
}
initColor() {
var colorRange;
colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange);
}
initColorKey() {
return d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]);
}
clickDay(stamp) {
var formatted_date;
if (this.currentSelectedDate !== stamp.date) {
this.currentSelectedDate = stamp.date;
formatted_date = this.currentSelectedDate.getFullYear() + "-" + (this.currentSelectedDate.getMonth() + 1) + "-" + this.currentSelectedDate.getDate();
return $.ajax({
url: this.calendar_activities_path,
data: {
date: formatted_date
},
const date = [
this.currentSelectedDate.getFullYear(),
this.currentSelectedDate.getMonth() + 1,
this.currentSelectedDate.getDate(),
].join('-');
$.ajax({
url: this.calendarActivitiesPath,
data: { date },
cache: false,
dataType: 'html',
beforeSend: function() {
return $('.user-calendar-activities').html('<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>');
},
success: function(data) {
return $('.user-calendar-activities').html(data);
}
beforeSend: () => $('.user-calendar-activities').html(LOADING_HTML),
success: data => $('.user-calendar-activities').html(data),
});
} else {
this.currentSelectedDate = '';
return $('.user-calendar-activities').html('');
$('.user-calendar-activities').html('');
}
}
initTooltips() {
return $('.js-contrib-calendar .js-tooltip').tooltip({
html: true
});
}
}

View file

@ -1,7 +1,19 @@
import ActivityCalendar from './activity_calendar';
import User from './user';
import Cookies from 'js-cookie';
import UserTabs from './user_tabs';
// use legacy exports until embedded javascript is refactored
window.Calendar = ActivityCalendar;
window.gl = window.gl || {};
window.gl.User = User;
export default function initUserProfile(action) {
// place profile avatars to top
$('.profile-groups-avatars').tooltip({
placement: 'top',
});
// eslint-disable-next-line no-new
new UserTabs({ parentEl: '.user-profile', action });
// hide project limit message
$('.hide-project-limit-message').on('click', (e) => {
e.preventDefault();
Cookies.set('hide_project_limit_message', 'false');
$(this).parents('.project-limit-message').remove();
});
}

View file

@ -1,34 +0,0 @@
/* eslint-disable class-methods-use-this */
import Cookies from 'js-cookie';
import UserTabs from './user_tabs';
export default class User {
constructor({ action }) {
this.action = action;
this.placeProfileAvatarsToTop();
this.initTabs();
this.hideProjectLimitMessage();
}
placeProfileAvatarsToTop() {
$('.profile-groups-avatars').tooltip({
placement: 'top',
});
}
initTabs() {
return new UserTabs({
parentEl: '.user-profile',
action: this.action,
});
}
hideProjectLimitMessage() {
$('.hide-project-limit-message').on('click', (e) => {
e.preventDefault();
Cookies.set('hide_project_limit_message', 'false');
$(this).parents('.project-limit-message').remove();
});
}
}

View file

@ -1,72 +1,76 @@
/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign, class-methods-use-this */
import ActivityCalendar from './activity_calendar';
/*
UserTabs
/**
* UserTabs
*
* Handles persisting and restoring the current tab selection and lazily-loading
* content on the Users#show page.
*
* ### Example Markup
*
* <ul class="nav-links">
* <li class="activity-tab active">
* <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
* Activity
* </a>
* </li>
* <li class="groups-tab">
* <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
* Groups
* </a>
* </li>
* <li class="contributed-tab">
* ...
* </li>
* <li class="projects-tab">
* ...
* </li>
* <li class="snippets-tab">
* ...
* </li>
* </ul>
*
* <div class="tab-content">
* <div class="tab-pane" id="activity">
* Activity Content
* </div>
* <div class="tab-pane" id="groups">
* Groups Content
* </div>
* <div class="tab-pane" id="contributed">
* Contributed projects content
* </div>
* <div class="tab-pane" id="projects">
* Projects content
* </div>
* <div class="tab-pane" id="snippets">
* Snippets content
* </div>
* </div>
*
* <div class="loading-status">
* <div class="loading">
* Loading Animation
* </div>
* </div>
*/
Handles persisting and restoring the current tab selection and lazily-loading
content on the Users#show page.
### Example Markup
<ul class="nav-links">
<li class="activity-tab active">
<a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
Activity
</a>
</li>
<li class="groups-tab">
<a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
Groups
</a>
</li>
<li class="contributed-tab">
<a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
Contributed projects
</a>
</li>
<li class="projects-tab">
<a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
Personal projects
</a>
</li>
<li class="snippets-tab">
<a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane" id="activity">
Activity Content
</div>
<div class="tab-pane" id="groups">
Groups Content
</div>
<div class="tab-pane" id="contributed">
Contributed projects content
</div>
<div class="tab-pane" id="projects">
Projects content
</div>
<div class="tab-pane" id="snippets">
Snippets content
</div>
const CALENDAR_TEMPLATE = `
<div class="clearfix calendar">
<div class="js-contrib-calendar"></div>
<div class="calendar-hint">
Summary of issues, merge requests, push events, and comments
</div>
</div>
<div class="loading-status">
<div class="loading">
Loading Animation
</div>
</div>
*/
`;
export default class UserTabs {
constructor ({ defaultAction, action, parentEl }) {
constructor({ defaultAction, action, parentEl }) {
this.loaded = {};
this.defaultAction = defaultAction || 'activity';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this._location = window.location;
this.windowLocation = window.location;
this.$parentEl.find('.nav-links a')
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
@ -82,12 +86,10 @@ export default class UserTabs {
}
bindEvents() {
this.changeProjectsPageWrapper = this.changeProjectsPage.bind(this);
this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
this.$parentEl.on('click', '.gl-pagination a', this.changeProjectsPageWrapper);
this.$parentEl
.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event))
.on('click', '.gl-pagination a', event => this.changeProjectsPage(event));
}
changeProjectsPage(e) {
@ -122,7 +124,7 @@ export default class UserTabs {
const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
if (loadableActions.indexOf(action) > -1) {
return this.loadTab(action, endpoint);
this.loadTab(action, endpoint);
}
}
@ -131,25 +133,38 @@ export default class UserTabs {
beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
url: endpoint,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
}
gl.utils.localTimeAgo($('.js-timeago', tabSelector));
},
});
}
loadActivities() {
if (this.loaded['activity']) {
if (this.loaded.activity) {
return;
}
const $calendarWrap = this.$parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href'));
const calendarPath = $calendarWrap.data('calendarPath');
const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath');
$.ajax({
dataType: 'json',
url: calendarPath,
success: (activityData) => {
$calendarWrap.html(CALENDAR_TEMPLATE);
// eslint-disable-next-line no-new
new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath);
},
});
// eslint-disable-next-line no-new
new gl.Activities();
return this.loaded['activity'] = true;
this.loaded.activity = true;
}
toggleLoading(status) {
@ -158,13 +173,13 @@ export default class UserTabs {
}
setCurrentAction(source) {
let new_state = source;
new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
let newState = source;
newState = newState.replace(/\/+$/, '');
newState += this.windowLocation.search + this.windowLocation.hash;
history.replaceState({
url: new_state
}, document.title, new_state);
return new_state;
url: newState,
}, document.title, newState);
return newState;
}
getCurrentAction() {

View file

@ -73,10 +73,7 @@ class UsersController < ApplicationController
end
def calendar
calendar = contributions_calendar
@activity_dates = calendar.activity_dates
render 'calendar', layout: false
render json: contributions_calendar.activity_dates
end
def calendar_activities

View file

@ -1,9 +0,0 @@
.clearfix.calendar
.js-contrib-calendar
.calendar-hint
Summary of issues, merge requests, push events, and comments
:javascript
new Calendar(
#{@activity_dates.to_json},
'#{user_calendar_activities_path}'
);

View file

@ -2,9 +2,6 @@
- @hide_breadcrumbs = true
- page_title @user.name
- page_description @user.bio
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('users')
- header_title @user.name, user_path(@user)
- @no_container = true
@ -107,7 +104,7 @@
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
.user-calendar{ data: { href: user_calendar_path } }
.user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path } }
%h4.center.light
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
@ -131,10 +128,3 @@
.loading-status
= spinner
:javascript
var userProfile;
userProfile = new gl.User({
action: "#{controller.action_name}"
});

View file

@ -67,7 +67,6 @@ var config = {
stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js',
u2f: ['vendor/u2f'],
users: './users/index.js',
raven: './raven/index.js',
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
@ -185,7 +184,6 @@ var config = {
name: 'common_d3',
chunks: [
'graphs',
'users',
'monitoring',
],
}),

View file

@ -80,9 +80,9 @@ describe UsersController do
it 'renders calendar' do
sign_in(user)
get :calendar, username: user.username
get :calendar, username: user.username, format: :json
expect(response).to render_template('calendar')
expect(response).to have_http_status(200)
end
context 'forked project' do