mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-05-16 06:52:54 +00:00
Merge pull request '[gitea] week 2024-20-v7.0 cherry pick (release/v1.22 -> v7.0/forgejo)' (#3772) from earl-warren/wcp/2024-20-v7.0 into v7.0/forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3772 Reviewed-by: Beowulf <beowulf@noreply.codeberg.org>
This commit is contained in:
commit
4ecbb2ef1b
53 changed files with 948 additions and 509 deletions
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import {SvgIcon} from '../svg.js';
|
||||
import {useLightTextOnBackground} from '../utils/color.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import {contrastColor} from '../utils/color.js';
|
||||
import {GET} from '../modules/fetch.js';
|
||||
import {emojiHTML} from '../features/emoji.js';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
|
@ -61,20 +60,13 @@ export default {
|
|||
},
|
||||
|
||||
labels() {
|
||||
return this.issue.labels.map((label) => {
|
||||
let textColor;
|
||||
const {r, g, b} = tinycolor(label.color).toRgb();
|
||||
if (useLightTextOnBackground(r, g, b)) {
|
||||
textColor = '#eeeeee';
|
||||
} else {
|
||||
textColor = '#111111';
|
||||
}
|
||||
label.name = htmlEscape(label.name);
|
||||
label.name = label.name.replaceAll(/:[-+\w]+:/g, (emoji) => {
|
||||
return this.issue.labels.map((label) => ({
|
||||
name: htmlEscape(label.name).replaceAll(/:[-+\w]+:/g, (emoji) => {
|
||||
return emojiHTML(emoji.substring(1, emoji.length - 1));
|
||||
});
|
||||
return {name: label.name, color: `#${label.color}`, textColor};
|
||||
});
|
||||
}),
|
||||
color: `#${label.color}`,
|
||||
textColor: contrastColor(`#${label.color}`),
|
||||
}));
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
@ -114,7 +106,7 @@ export default {
|
|||
<p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p>
|
||||
<p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p>
|
||||
<p>{{ body }}</p>
|
||||
<div>
|
||||
<div class="labels-list">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-for="label in labels" :key="label.name" class="ui label" :style="{ color: label.textColor, backgroundColor: label.color }" v-html="label.name"/>
|
||||
</div>
|
||||
|
|
|
@ -67,7 +67,7 @@ export default {
|
|||
const weekValues = Object.values(this.data);
|
||||
const start = weekValues[0].week;
|
||||
const end = firstStartDateAfterDate(new Date());
|
||||
const startDays = startDaysBetween(new Date(start), new Date(end));
|
||||
const startDays = startDaysBetween(start, end);
|
||||
this.data = fillEmptyStartDaysWithZeroes(startDays, this.data);
|
||||
this.errorText = '';
|
||||
} else {
|
||||
|
|
|
@ -114,7 +114,7 @@ export default {
|
|||
const weekValues = Object.values(total.weeks);
|
||||
this.xAxisStart = weekValues[0].week;
|
||||
this.xAxisEnd = firstStartDateAfterDate(new Date());
|
||||
const startDays = startDaysBetween(new Date(this.xAxisStart), new Date(this.xAxisEnd));
|
||||
const startDays = startDaysBetween(this.xAxisStart, this.xAxisEnd);
|
||||
total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks);
|
||||
this.xAxisMin = this.xAxisStart;
|
||||
this.xAxisMax = this.xAxisEnd;
|
||||
|
|
|
@ -62,7 +62,7 @@ export default {
|
|||
const data = await response.json();
|
||||
const start = Object.values(data)[0].week;
|
||||
const end = firstStartDateAfterDate(new Date());
|
||||
const startDays = startDaysBetween(new Date(start), new Date(end));
|
||||
const startDays = startDaysBetween(start, end);
|
||||
this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
|
||||
this.errorText = '';
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import $ from 'jquery';
|
||||
import {useLightTextOnBackground} from '../utils/color.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import {contrastColor} from '../utils/color.js';
|
||||
import {createSortable} from '../modules/sortable.js';
|
||||
import {POST, DELETE, PUT} from '../modules/fetch.js';
|
||||
|
||||
|
@ -63,20 +62,20 @@ async function initRepoProjectSortable() {
|
|||
delay: 500,
|
||||
onSort: async () => {
|
||||
boardColumns = mainBoard.getElementsByClassName('project-column');
|
||||
for (let i = 0; i < boardColumns.length; i++) {
|
||||
const column = boardColumns[i];
|
||||
if (parseInt($(column).data('sorting')) !== i) {
|
||||
try {
|
||||
await PUT($(column).data('url'), {
|
||||
data: {
|
||||
sorting: i,
|
||||
color: rgbToHex(window.getComputedStyle($(column)[0]).backgroundColor),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const columnSorting = {
|
||||
columns: Array.from(boardColumns, (column, i) => ({
|
||||
columnID: parseInt(column.getAttribute('data-id')),
|
||||
sorting: i,
|
||||
})),
|
||||
};
|
||||
|
||||
try {
|
||||
await POST(mainBoard.getAttribute('data-url'), {
|
||||
data: columnSorting,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -94,47 +93,51 @@ async function initRepoProjectSortable() {
|
|||
}
|
||||
|
||||
export function initRepoProject() {
|
||||
if (!$('.repository.projects').length) {
|
||||
if (!document.querySelector('.repository.projects')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _promise = initRepoProjectSortable();
|
||||
|
||||
$('.edit-project-column-modal').each(function () {
|
||||
const $projectHeader = $(this).closest('.project-column-header');
|
||||
const $projectTitleLabel = $projectHeader.find('.project-column-title');
|
||||
const $projectTitleInput = $(this).find('.project-column-title-input');
|
||||
const $projectColorInput = $(this).find('#new_project_column_color');
|
||||
const $boardColumn = $(this).closest('.project-column');
|
||||
|
||||
const bgColor = $boardColumn[0].style.backgroundColor;
|
||||
if (bgColor) {
|
||||
setLabelColor($projectHeader, rgbToHex(bgColor));
|
||||
}
|
||||
|
||||
$(this).find('.edit-project-column-button').on('click', async function (e) {
|
||||
for (const modal of document.getElementsByClassName('edit-project-column-modal')) {
|
||||
const projectHeader = modal.closest('.project-column-header');
|
||||
const projectTitleLabel = projectHeader?.querySelector('.project-column-title-label');
|
||||
const projectTitleInput = modal.querySelector('.project-column-title-input');
|
||||
const projectColorInput = modal.querySelector('#new_project_column_color');
|
||||
const boardColumn = modal.closest('.project-column');
|
||||
modal.querySelector('.edit-project-column-button')?.addEventListener('click', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await PUT($(this).data('url'), {
|
||||
await PUT(this.getAttribute('data-url'), {
|
||||
data: {
|
||||
title: $projectTitleInput.val(),
|
||||
color: $projectColorInput.val(),
|
||||
title: projectTitleInput?.value,
|
||||
color: projectColorInput?.value,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
$projectTitleLabel.text($projectTitleInput.val());
|
||||
$projectTitleInput.closest('form').removeClass('dirty');
|
||||
if ($projectColorInput.val()) {
|
||||
setLabelColor($projectHeader, $projectColorInput.val());
|
||||
projectTitleLabel.textContent = projectTitleInput?.value;
|
||||
projectTitleInput.closest('form')?.classList.remove('dirty');
|
||||
const dividers = boardColumn.querySelectorAll(':scope > .divider');
|
||||
if (projectColorInput.value) {
|
||||
const color = contrastColor(projectColorInput.value);
|
||||
boardColumn.style.setProperty('background', projectColorInput.value, 'important');
|
||||
boardColumn.style.setProperty('color', color, 'important');
|
||||
for (const divider of dividers) {
|
||||
divider.style.setProperty('color', color);
|
||||
}
|
||||
} else {
|
||||
boardColumn.style.removeProperty('background');
|
||||
boardColumn.style.removeProperty('color');
|
||||
for (const divider of dividers) {
|
||||
divider.style.removeProperty('color');
|
||||
}
|
||||
}
|
||||
$boardColumn[0].style = `background: ${$projectColorInput.val()} !important`;
|
||||
$('.ui.modal').modal('hide');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$('.default-project-column-modal').each(function () {
|
||||
const $boardColumn = $(this).closest('.project-column');
|
||||
|
@ -183,22 +186,3 @@ export function initRepoProject() {
|
|||
createNewColumn(url, $columnTitle, $projectColorInput);
|
||||
});
|
||||
}
|
||||
|
||||
function setLabelColor(label, color) {
|
||||
const {r, g, b} = tinycolor(color).toRgb();
|
||||
if (useLightTextOnBackground(r, g, b)) {
|
||||
label.removeClass('dark-label').addClass('light-label');
|
||||
} else {
|
||||
label.removeClass('light-label').addClass('dark-label');
|
||||
}
|
||||
}
|
||||
|
||||
function rgbToHex(rgb) {
|
||||
rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+).*\)$/);
|
||||
return `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}`;
|
||||
}
|
||||
|
||||
function hex(x) {
|
||||
const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
|
||||
return Number.isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16];
|
||||
}
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
// Check similar implementation in modules/util/color.go and keep synchronization
|
||||
// Return R, G, B values defined in reletive luminance
|
||||
function getLuminanceRGB(channel) {
|
||||
const sRGB = channel / 255;
|
||||
return (sRGB <= 0.03928) ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ** 2.4;
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
|
||||
// Keep this in sync with modules/util/color.go
|
||||
function getRelativeLuminance(color) {
|
||||
const {r, g, b} = tinycolor(color).toRgb();
|
||||
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
|
||||
}
|
||||
|
||||
// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
|
||||
function getLuminance(r, g, b) {
|
||||
const R = getLuminanceRGB(r);
|
||||
const G = getLuminanceRGB(g);
|
||||
const B = getLuminanceRGB(b);
|
||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||
function useLightText(backgroundColor) {
|
||||
return getRelativeLuminance(backgroundColor) < 0.453;
|
||||
}
|
||||
|
||||
// Reference from: https://firsching.ch/github_labels.html
|
||||
// In the future WCAG 3 APCA may be a better solution.
|
||||
// Check if text should use light color based on RGB of background
|
||||
export function useLightTextOnBackground(r, g, b) {
|
||||
return getLuminance(r, g, b) < 0.453;
|
||||
// Given a background color, returns a black or white foreground color that the highest
|
||||
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
|
||||
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
|
||||
export function contrastColor(backgroundColor) {
|
||||
return useLightText(backgroundColor) ? '#fff' : '#000';
|
||||
}
|
||||
|
||||
function resolveColors(obj) {
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import {useLightTextOnBackground} from './color.js';
|
||||
import {contrastColor} from './color.js';
|
||||
|
||||
test('useLightTextOnBackground', () => {
|
||||
expect(useLightTextOnBackground(215, 58, 74)).toBe(true);
|
||||
expect(useLightTextOnBackground(0, 117, 202)).toBe(true);
|
||||
expect(useLightTextOnBackground(207, 211, 215)).toBe(false);
|
||||
expect(useLightTextOnBackground(162, 238, 239)).toBe(false);
|
||||
expect(useLightTextOnBackground(112, 87, 255)).toBe(true);
|
||||
expect(useLightTextOnBackground(0, 134, 114)).toBe(true);
|
||||
expect(useLightTextOnBackground(228, 230, 105)).toBe(false);
|
||||
expect(useLightTextOnBackground(216, 118, 227)).toBe(true);
|
||||
expect(useLightTextOnBackground(255, 255, 255)).toBe(false);
|
||||
expect(useLightTextOnBackground(43, 134, 133)).toBe(true);
|
||||
expect(useLightTextOnBackground(43, 135, 134)).toBe(true);
|
||||
expect(useLightTextOnBackground(44, 135, 134)).toBe(true);
|
||||
expect(useLightTextOnBackground(59, 182, 179)).toBe(true);
|
||||
expect(useLightTextOnBackground(124, 114, 104)).toBe(true);
|
||||
expect(useLightTextOnBackground(126, 113, 108)).toBe(true);
|
||||
expect(useLightTextOnBackground(129, 112, 109)).toBe(true);
|
||||
expect(useLightTextOnBackground(128, 112, 112)).toBe(true);
|
||||
test('contrastColor', () => {
|
||||
expect(contrastColor('#d73a4a')).toBe('#fff');
|
||||
expect(contrastColor('#0075ca')).toBe('#fff');
|
||||
expect(contrastColor('#cfd3d7')).toBe('#000');
|
||||
expect(contrastColor('#a2eeef')).toBe('#000');
|
||||
expect(contrastColor('#7057ff')).toBe('#fff');
|
||||
expect(contrastColor('#008672')).toBe('#fff');
|
||||
expect(contrastColor('#e4e669')).toBe('#000');
|
||||
expect(contrastColor('#d876e3')).toBe('#000');
|
||||
expect(contrastColor('#ffffff')).toBe('#000');
|
||||
expect(contrastColor('#2b8684')).toBe('#fff');
|
||||
expect(contrastColor('#2b8786')).toBe('#fff');
|
||||
expect(contrastColor('#2c8786')).toBe('#000');
|
||||
expect(contrastColor('#3bb6b3')).toBe('#000');
|
||||
expect(contrastColor('#7c7268')).toBe('#fff');
|
||||
expect(contrastColor('#7e716c')).toBe('#fff');
|
||||
expect(contrastColor('#81706d')).toBe('#fff');
|
||||
expect(contrastColor('#807070')).toBe('#fff');
|
||||
expect(contrastColor('#84b6eb')).toBe('#000');
|
||||
});
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc.js';
|
||||
import {getCurrentLocale} from '../utils.js';
|
||||
|
||||
// Returns an array of millisecond-timestamps of start-of-week days (Sundays)
|
||||
export function startDaysBetween(startDate, endDate) {
|
||||
// Ensure the start date is a Sunday
|
||||
while (startDate.getDay() !== 0) {
|
||||
startDate.setDate(startDate.getDate() + 1);
|
||||
}
|
||||
dayjs.extend(utc);
|
||||
|
||||
const start = dayjs(startDate);
|
||||
const end = dayjs(endDate);
|
||||
const startDays = [];
|
||||
/**
|
||||
* Returns an array of millisecond-timestamps of start-of-week days (Sundays)
|
||||
*
|
||||
* @param startConfig The start date. Can take any type that `Date` accepts.
|
||||
* @param endConfig The end date. Can take any type that `Date` accepts.
|
||||
*/
|
||||
export function startDaysBetween(startDate, endDate) {
|
||||
const start = dayjs.utc(startDate);
|
||||
const end = dayjs.utc(endDate);
|
||||
|
||||
let current = start;
|
||||
|
||||
// Ensure the start date is a Sunday
|
||||
while (current.day() !== 0) {
|
||||
current = current.add(1, 'day');
|
||||
}
|
||||
|
||||
const startDays = [];
|
||||
while (current.isBefore(end)) {
|
||||
startDays.push(current.valueOf());
|
||||
// we are adding 7 * 24 hours instead of 1 week because we don't want
|
||||
// date library to use local time zone to calculate 1 week from now.
|
||||
// local time zone is problematic because of daylight saving time (dst)
|
||||
// used on some countries
|
||||
current = current.add(7 * 24, 'hour');
|
||||
current = current.add(1, 'week');
|
||||
}
|
||||
|
||||
return startDays;
|
||||
|
@ -29,10 +34,10 @@ export function firstStartDateAfterDate(inputDate) {
|
|||
if (!(inputDate instanceof Date)) {
|
||||
throw new Error('Invalid date');
|
||||
}
|
||||
const dayOfWeek = inputDate.getDay();
|
||||
const dayOfWeek = inputDate.getUTCDay();
|
||||
const daysUntilSunday = 7 - dayOfWeek;
|
||||
const resultDate = new Date(inputDate.getTime());
|
||||
resultDate.setDate(resultDate.getDate() + daysUntilSunday);
|
||||
resultDate.setUTCDate(resultDate.getUTCDate() + daysUntilSunday);
|
||||
return resultDate.valueOf();
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue