Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable JSX support in frontend #15293

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ ignorePatterns:
parserOptions:
sourceType: module
ecmaVersion: 2021
ecmaFeatures:
jsx: true

plugins:
- eslint-plugin-unicorn
Expand Down
27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"font-awesome": "4.7.0",
"jquery": "3.6.0",
"jquery.are-you-sure": "1.9.0",
"jsx-dom": "7.0.0-beta.5",
"less": "4.1.1",
"less-loader": "8.1.1",
"license-checker-webpack-plugin": "0.2.1",
Expand Down
17 changes: 17 additions & 0 deletions web_src/js/components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {contrastColor} from './utils.js';

// These components might look like React components but they are
// not. They return DOM nodes via JSX transformation using jsx-dom.
// https://github.com/proteriax/jsx-dom

export function Label({label}) {
const backgroundColor = `#${label.color}`;
const color = contrastColor(backgroundColor);
const style = `color: ${color}; background-color: ${backgroundColor}`;

return (
<div class="ui label" style={style}>
{label.name}
</div>
);
}
58 changes: 26 additions & 32 deletions web_src/js/features/contextpopup.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {htmlEscape} from 'escape-goat';
import {svg} from '../svg.js';
import {Label} from '../components.js';
import {SVG} from '../svg.js';

const {AppSubUrl} = window.config;

Expand All @@ -22,55 +22,49 @@ function issuePopup(owner, repo, index, $element) {
body = `${body.substring(0, 85)}...`;
}

let labels = '';
for (let i = 0; i < issue.labels.length; i++) {
const label = issue.labels[i];
const red = parseInt(label.color.substring(0, 2), 16);
const green = parseInt(label.color.substring(2, 4), 16);
const blue = parseInt(label.color.substring(4, 6), 16);
let color = '#ffffff';
if ((red * 0.299 + green * 0.587 + blue * 0.114) > 125) {
color = '#000000';
}
labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${htmlEscape(label.name)}</div>`;
}
if (labels.length > 0) {
labels = `<p>${labels}</p>`;
}

let octicon, color;
let icon, color;
if (issue.pull_request !== null) {
if (issue.state === 'open') {
color = 'green';
octicon = 'octicon-git-pull-request'; // Open PR
icon = 'octicon-git-pull-request'; // Open PR
} else if (issue.pull_request.merged === true) {
color = 'purple';
octicon = 'octicon-git-merge'; // Merged PR
icon = 'octicon-git-merge'; // Merged PR
} else {
color = 'red';
octicon = 'octicon-git-pull-request'; // Closed PR
icon = 'octicon-git-pull-request'; // Closed PR
}
} else if (issue.state === 'open') {
color = 'green';
octicon = 'octicon-issue-opened'; // Open Issue
icon = 'octicon-issue-opened'; // Open Issue
} else {
color = 'red';
octicon = 'octicon-issue-closed'; // Closed Issue
icon = 'octicon-issue-closed'; // Closed Issue
}

$element.popup({
variation: 'wide',
delay: {
show: 250
},
html: `
<div>
<p><small>${htmlEscape(issue.repository.full_name)} on ${createdAt}</small></p>
<p><span class="${color}">${svg(octicon)}</span> <strong>${htmlEscape(issue.title)}</strong> #${index}</p>
<p>${htmlEscape(body)}</p>
${labels}
</div>
`
html: (
<div>
<p><small>{issue.repository.full_name} on {createdAt}</small></p>
<p>
<span class={color}><SVG name={icon}/></span>
<strong class="mx-2">{issue.title}</strong>
#{index}
</p>
<p>{body}</p>
{issue.labels && issue.labels.length && (
<p>
{issue.labels.map((label) => (
<Label label={label}/>
))}
</p>
)}
</div>
)
});
});
}
22 changes: 15 additions & 7 deletions web_src/js/svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,23 @@ export const svgs = {
const parser = new DOMParser();
const serializer = new XMLSerializer();

// retrieve a HTML string for given SVG icon name, size and additional classes
export function svg(name, size = 16, className = '') {
if (!(name in svgs)) return '';
if (size === 16 && !className) return svgs[name];
// returns <svg> DOM node for given SVG icon name, size and additional classes
export function SVG({name, size = 16, className = ''}) {
if (!(name in svgs)) return null;

const document = parser.parseFromString(svgs[name], 'image/svg+xml');
const svgNode = document.firstChild;
// parse as html to avoid namespace issues
const document = parser.parseFromString(svgs[name], 'text/html');
const svgNode = document.body.firstChild;
if (size !== 16) svgNode.setAttribute('width', String(size));
if (size !== 16) svgNode.setAttribute('height', String(size));
if (className) svgNode.classList.add(...className.split(/\s+/));
return serializer.serializeToString(svgNode);
return svgNode;
}

// returns a HTML string for given SVG icon name, size and additional classes
export function svg(name, size = 16, className = '') {
if (!(name in svgs)) return '';
if (size === 16 && !className) return svgs[name];
const svgElement = <SVG name={name} size={size} className={className}/>;
return serializer.serializeToString(svgElement);
}
10 changes: 10 additions & 0 deletions web_src/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,13 @@ export function mqBinarySearch(feature, minValue, maxValue, step, unit) {
}
return mqBinarySearch(feature, minValue, mid - step, step, unit); // feature is < mid
}

// get a contrasting foreground color for a given 6-digit background color
export function contrastColor(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) return '#fff';
const r = parseInt(result[1], 16);
const g = parseInt(result[2], 16);
const b = parseInt(result[3], 16);
return ((r * 299) + (g * 587) + (b * 114)) / 1000 > 125 ? '#000' : '#fff';
}
13 changes: 12 additions & 1 deletion web_src/js/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
basename, extname, isObject, uniq, stripTags, joinPaths,
basename, extname, isObject, uniq, stripTags, joinPaths, contrastColor,
} from './utils.js';

test('basename', () => {
Expand Down Expand Up @@ -66,3 +66,14 @@ test('uniq', () => {
test('stripTags', () => {
expect(stripTags('<a>test</a>')).toEqual('test');
});

test('contrastColor', () => {
expect(contrastColor('#000000')).toEqual('#fff');
expect(contrastColor('#333333')).toEqual('#fff');
expect(contrastColor('#ff0000')).toEqual('#fff');
expect(contrastColor('#0000ff')).toEqual('#fff');
expect(contrastColor('#cccccc')).toEqual('#000');
expect(contrastColor('#ffffff')).toEqual('#000');
expect(contrastColor('000000')).toEqual('#fff');
expect(contrastColor('ffffff')).toEqual('#000');
});
11 changes: 9 additions & 2 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {fileURLToPath} from 'url';

const {VueLoaderPlugin} = VueLoader;
const {ESBuildMinifyPlugin} = EsBuildLoader;
const {SourceMapDevToolPlugin} = webpack;
const {SourceMapDevToolPlugin, ProvidePlugin} = webpack;
const __dirname = dirname(fileURLToPath(import.meta.url));
const glob = (pattern) => fastGlob.sync(pattern, {cwd: __dirname, absolute: true});

Expand Down Expand Up @@ -122,7 +122,10 @@ export default {
{
loader: 'esbuild-loader',
options: {
target: 'es2015'
target: 'es2015',
loader: 'jsx',
jsxFactory: 'h',
jsxFragment: 'Fragment',
},
},
],
Expand Down Expand Up @@ -205,6 +208,10 @@ export default {
new MonacoWebpackPlugin({
filename: 'js/monaco-[name].worker.js',
}),
new ProvidePlugin({
h: ['jsx-dom', 'h'],
Fragment: ['jsx-dom', 'Fragment'],
}),
isProduction ? new LicenseCheckerWebpackPlugin({
outputFilename: 'js/licenses.txt',
outputWriter: ({dependencies}) => {
Expand Down