debian-mirror-gitlab/app/assets/javascripts/pages/users/activity_calendar.js

315 lines
8.9 KiB
JavaScript
Raw Normal View History

2018-03-17 18:26:18 +05:30
import { select } from 'd3-selection';
2018-11-08 19:23:39 +05:30
import dateFormat from 'dateformat';
2021-03-11 19:13:27 +05:30
import $ from 'jquery';
import { last } from 'lodash';
2021-09-30 23:02:18 +05:30
import createFlash from '~/flash';
2021-03-11 19:13:27 +05:30
import axios from '~/lib/utils/axios_utils';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
2021-12-11 22:18:48 +05:30
import { formatDate } from '~/lib/utils/datetime/date_format_utility';
2019-07-31 22:56:46 +05:30
import { n__, s__, __ } from '~/locale';
2022-05-07 20:08:51 +05:30
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
2018-03-17 18:26:18 +05:30
2021-09-04 01:27:46 +05:30
const d3 = { select };
2017-09-10 17:25:29 +05:30
2019-07-07 11:18:12 +05:30
const firstDayOfWeekChoices = Object.freeze({
sunday: 0,
monday: 1,
saturday: 6,
});
2021-09-04 01:27:46 +05:30
const CONTRIB_LEGENDS = [
{ title: __('No contributions'), min: 0 },
{ title: __('1-9 contributions'), min: 1 },
{ title: __('10-19 contributions'), min: 10 },
{ title: __('20-29 contributions'), min: 20 },
{ title: __('30+ contributions'), min: 30 },
];
2017-09-10 17:25:29 +05:30
function getSystemDate(systemUtcOffsetSeconds) {
const date = new Date();
const localUtcOffsetMinutes = 0 - date.getTimezoneOffset();
const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60;
2018-10-15 14:42:47 +05:30
date.setMinutes(date.getMinutes() - localUtcOffsetMinutes + systemUtcOffsetMinutes);
2017-09-10 17:25:29 +05:30
return date;
}
function formatTooltipText({ date, count }) {
const dateObject = new Date(date);
2018-03-17 18:26:18 +05:30
const dateDayName = getDayName(dateObject);
2018-11-08 19:23:39 +05:30
const dateText = dateFormat(dateObject, 'mmm d, yyyy');
2017-09-10 17:25:29 +05:30
2019-07-31 22:56:46 +05:30
let contribText = __('No contributions');
2017-09-10 17:25:29 +05:30
if (count > 0) {
2019-07-31 22:56:46 +05:30
contribText = n__('%d contribution', '%d contributions', count);
2017-09-10 17:25:29 +05:30
}
2021-04-29 21:17:54 +05:30
return `${contribText}<br /><span class="gl-text-gray-300">${dateDayName} ${dateText}</span>`;
2017-09-10 17:25:29 +05:30
}
2021-09-04 01:27:46 +05:30
// Return the contribution level from the number of contributions
export const getLevelFromContributions = (count) => {
if (count <= 0) {
return 0;
}
const nextLevel = CONTRIB_LEGENDS.findIndex(({ min }) => count < min);
// If there is no higher level, we are at the end
return nextLevel >= 0 ? nextLevel - 1 : CONTRIB_LEGENDS.length - 1;
};
2017-09-10 17:25:29 +05:30
export default class ActivityCalendar {
2018-12-05 23:21:45 +05:30
constructor(
container,
activitiesContainer,
timestamps,
calendarActivitiesPath,
utcOffset = 0,
2019-07-07 11:18:12 +05:30
firstDayOfWeek = firstDayOfWeekChoices.sunday,
2018-12-05 23:21:45 +05:30
monthsAgo = 12,
) {
2017-09-10 17:25:29 +05:30
this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
this.daySpace = 1;
this.daySize = 15;
2018-10-15 14:42:47 +05:30
this.daySizeWithSpace = this.daySize + this.daySpace * 2;
this.monthNames = [
2019-07-31 22:56:46 +05:30
__('Jan'),
__('Feb'),
__('Mar'),
__('Apr'),
__('May'),
__('Jun'),
__('Jul'),
__('Aug'),
__('Sep'),
__('Oct'),
__('Nov'),
__('Dec'),
2018-10-15 14:42:47 +05:30
];
2017-09-10 17:25:29 +05:30
this.months = [];
2018-10-15 14:42:47 +05:30
this.firstDayOfWeek = firstDayOfWeek;
2018-12-05 23:21:45 +05:30
this.activitiesContainer = activitiesContainer;
this.container = container;
2017-09-10 17:25:29 +05:30
// 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 = [];
let group = 0;
const today = getSystemDate(utcOffset);
today.setHours(0, 0, 0, 0, 0);
2018-12-05 23:21:45 +05:30
const timeAgo = new Date(today);
timeAgo.setMonth(today.getMonth() - monthsAgo);
2017-09-10 17:25:29 +05:30
2018-12-05 23:21:45 +05:30
const days = getDayDifference(timeAgo, today);
2017-09-10 17:25:29 +05:30
for (let i = 0; i <= days; i += 1) {
2018-12-05 23:21:45 +05:30
const date = new Date(timeAgo);
2017-09-10 17:25:29 +05:30
date.setDate(date.getDate() + i);
const day = date.getDay();
2018-11-08 19:23:39 +05:30
const count = timestamps[dateFormat(date, 'yyyy-mm-dd')] || 0;
2017-09-10 17:25:29 +05:30
// Create a new group array if this is the first day of the week
// or if is first object
2018-10-15 14:42:47 +05:30
if ((day === this.firstDayOfWeek && i !== 0) || i === 0) {
2017-09-10 17:25:29 +05:30
this.timestampsTmp.push([]);
group += 1;
}
// Push to the inner array the values that will be used to render map
const innerArray = this.timestampsTmp[group - 1];
innerArray.push({ count, date, day });
}
// Init the svg element
this.svg = this.renderSvg(container, group);
this.renderDays();
this.renderMonths();
this.renderDayTitles();
this.renderKey();
}
// Add extra padding for the last month label if it is also the last column
getExtraWidthPadding(group) {
let extraWidthPadding = 0;
const lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth();
const secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth();
if (lastColMonth !== secondLastColMonth) {
2018-03-17 18:26:18 +05:30
extraWidthPadding = 6;
2017-09-10 17:25:29 +05:30
}
return extraWidthPadding;
}
renderSvg(container, group) {
2018-10-15 14:42:47 +05:30
const width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
return d3
.select(container)
2017-09-10 17:25:29 +05:30
.append('svg')
2018-10-15 14:42:47 +05:30
.attr('width', width)
.attr('height', 167)
.attr('class', 'contrib-calendar');
}
dayYPos(day) {
return this.daySizeWithSpace * ((day + 7 - this.firstDayOfWeek) % 7);
2017-09-10 17:25:29 +05:30
}
renderDays() {
2018-10-15 14:42:47 +05:30
this.svg
.selectAll('g')
.data(this.timestampsTmp)
.enter()
.append('g')
2017-09-10 17:25:29 +05:30
.attr('transform', (group, i) => {
2020-04-08 14:13:33 +05:30
group.forEach((stamp, a) => {
2019-03-02 22:35:43 +05:30
if (a === 0 && stamp.day === this.firstDayOfWeek) {
2017-09-10 17:25:29 +05:30
const month = stamp.date.getMonth();
2018-10-15 14:42:47 +05:30
const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
2020-04-08 14:13:33 +05:30
const lastMonth = last(this.months);
2017-09-10 17:25:29 +05:30
if (
lastMonth == null ||
(month !== lastMonth.month && x - this.daySizeWithSpace !== lastMonth.x)
) {
this.months.push({ month, x });
}
}
});
2018-10-15 14:42:47 +05:30
return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`;
2017-09-10 17:25:29 +05:30
})
.selectAll('rect')
2021-03-08 18:12:59 +05:30
.data((stamp) => stamp)
2018-10-15 14:42:47 +05:30
.enter()
.append('rect')
.attr('x', '0')
2021-03-08 18:12:59 +05:30
.attr('y', (stamp) => this.dayYPos(stamp.day))
2018-10-15 14:42:47 +05:30
.attr('width', this.daySize)
.attr('height', this.daySize)
2021-09-04 01:27:46 +05:30
.attr('data-level', (stamp) => getLevelFromContributions(stamp.count))
2021-03-08 18:12:59 +05:30
.attr('title', (stamp) => formatTooltipText(stamp))
2021-01-29 00:20:46 +05:30
.attr('class', 'user-contrib-cell has-tooltip')
.attr('data-html', true)
2018-10-15 14:42:47 +05:30
.attr('data-container', 'body')
.on('click', this.clickDay);
2017-09-10 17:25:29 +05:30
}
renderDayTitles() {
const days = [
{
2019-07-31 22:56:46 +05:30
text: s__('DayTitle|M'),
2018-10-15 14:42:47 +05:30
y: 29 + this.dayYPos(1),
},
{
2019-07-31 22:56:46 +05:30
text: s__('DayTitle|W'),
2018-10-15 14:42:47 +05:30
y: 29 + this.dayYPos(3),
},
{
2019-07-31 22:56:46 +05:30
text: s__('DayTitle|F'),
2018-10-15 14:42:47 +05:30
y: 29 + this.dayYPos(5),
2017-09-10 17:25:29 +05:30
},
];
2019-03-02 22:35:43 +05:30
2019-07-07 11:18:12 +05:30
if (this.firstDayOfWeek === firstDayOfWeekChoices.monday) {
2019-03-02 22:35:43 +05:30
days.push({
2019-07-31 22:56:46 +05:30
text: s__('DayTitle|S'),
2019-03-02 22:35:43 +05:30
y: 29 + this.dayYPos(7),
});
2019-07-07 11:18:12 +05:30
} else if (this.firstDayOfWeek === firstDayOfWeekChoices.saturday) {
days.push({
2019-07-31 22:56:46 +05:30
text: s__('DayTitle|S'),
2019-07-07 11:18:12 +05:30
y: 29 + this.dayYPos(6),
});
2019-03-02 22:35:43 +05:30
}
2018-10-15 14:42:47 +05:30
this.svg
.append('g')
2017-09-10 17:25:29 +05:30
.selectAll('text')
2018-10-15 14:42:47 +05:30
.data(days)
.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('x', 8)
2021-03-08 18:12:59 +05:30
.attr('y', (day) => day.y)
.text((day) => day.text)
2018-10-15 14:42:47 +05:30
.attr('class', 'user-contrib-text');
2017-09-10 17:25:29 +05:30
}
renderMonths() {
2018-10-15 14:42:47 +05:30
this.svg
.append('g')
2017-09-10 17:25:29 +05:30
.attr('direction', 'ltr')
.selectAll('text')
2018-10-15 14:42:47 +05:30
.data(this.months)
.enter()
.append('text')
2021-03-08 18:12:59 +05:30
.attr('x', (date) => date.x)
2018-10-15 14:42:47 +05:30
.attr('y', 10)
.attr('class', 'user-contrib-text')
2021-03-08 18:12:59 +05:30
.text((date) => this.monthNames[date.month]);
2017-09-10 17:25:29 +05:30
}
renderKey() {
2018-10-15 14:42:47 +05:30
this.svg
.append('g')
.attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
2017-09-10 17:25:29 +05:30
.selectAll('rect')
2021-09-04 01:27:46 +05:30
.data(CONTRIB_LEGENDS)
2018-10-15 14:42:47 +05:30
.enter()
.append('rect')
.attr('width', this.daySize)
.attr('height', this.daySize)
2021-09-04 01:27:46 +05:30
.attr('x', (_, i) => this.daySizeWithSpace * i)
2018-10-15 14:42:47 +05:30
.attr('y', 0)
2021-09-04 01:27:46 +05:30
.attr('data-level', (_, i) => i)
.attr('class', 'user-contrib-cell has-tooltip contrib-legend')
.attr('title', (x) => x.title)
2021-01-29 00:20:46 +05:30
.attr('data-container', 'body')
.attr('data-html', true);
2017-09-10 17:25:29 +05:30
}
clickDay(stamp) {
if (this.currentSelectedDate !== stamp.date) {
this.currentSelectedDate = stamp.date;
const date = [
this.currentSelectedDate.getFullYear(),
this.currentSelectedDate.getMonth() + 1,
this.currentSelectedDate.getDate(),
].join('-');
2022-05-07 20:08:51 +05:30
$(this.activitiesContainer)
.empty()
.append(loadingIconForLegacyJS({ size: 'lg' }));
2018-03-17 18:26:18 +05:30
2018-10-15 14:42:47 +05:30
axios
.get(this.calendarActivitiesPath, {
params: {
date,
},
responseType: 'text',
})
2021-12-11 22:18:48 +05:30
.then(({ data }) => {
$(this.activitiesContainer).html(data);
document
.querySelector(this.activitiesContainer)
.querySelectorAll('.js-localtime')
.forEach((el) => {
el.setAttribute('title', formatDate(el.getAttribute('data-datetime')));
});
})
2021-09-30 23:02:18 +05:30
.catch(() =>
createFlash({
message: __('An error occurred while retrieving calendar activity'),
}),
);
2017-09-10 17:25:29 +05:30
} else {
this.currentSelectedDate = '';
2018-12-05 23:21:45 +05:30
$(this.activitiesContainer).html('');
2017-09-10 17:25:29 +05:30
}
}
}