diff --git a/.eslintrc b/.eslintrc
index 640956f73424f..90c07d5d750ce 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -9,6 +9,8 @@ ignorePatterns:
parserOptions:
sourceType: module
ecmaVersion: 2021
+ ecmaFeatures:
+ jsx: true
plugins:
- eslint-plugin-unicorn
diff --git a/package-lock.json b/package-lock.json
index 0ec6f4345605c..94a9d7a439209 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
@@ -3044,6 +3045,11 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"dev": true
},
+ "node_modules/csstype": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
+ "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
+ },
"node_modules/d3": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz",
@@ -7498,6 +7504,14 @@
"verror": "1.10.0"
}
},
+ "node_modules/jsx-dom": {
+ "version": "7.0.0-beta.5",
+ "resolved": "https://registry.npmjs.org/jsx-dom/-/jsx-dom-7.0.0-beta.5.tgz",
+ "integrity": "sha512-SJRvQmFwVItkI3/pXXZ3piW07hLCSbrA5hGTxpsbAKoZ2VFl0j8QFwgSytdq3F7VsoC9TWZWheIpk8qT2ZecpQ==",
+ "dependencies": {
+ "csstype": "^3.0.7"
+ }
+ },
"node_modules/khroma": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/khroma/-/khroma-1.4.1.tgz",
@@ -16022,6 +16036,11 @@
}
}
},
+ "csstype": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
+ "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
+ },
"d3": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz",
@@ -19506,6 +19525,14 @@
"verror": "1.10.0"
}
},
+ "jsx-dom": {
+ "version": "7.0.0-beta.5",
+ "resolved": "https://registry.npmjs.org/jsx-dom/-/jsx-dom-7.0.0-beta.5.tgz",
+ "integrity": "sha512-SJRvQmFwVItkI3/pXXZ3piW07hLCSbrA5hGTxpsbAKoZ2VFl0j8QFwgSytdq3F7VsoC9TWZWheIpk8qT2ZecpQ==",
+ "requires": {
+ "csstype": "^3.0.7"
+ }
+ },
"khroma": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/khroma/-/khroma-1.4.1.tgz",
diff --git a/package.json b/package.json
index 5da3dfab20800..4eda9ed69dfcf 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/web_src/js/components.js b/web_src/js/components.js
new file mode 100644
index 0000000000000..38e533f91bc0c
--- /dev/null
+++ b/web_src/js/components.js
@@ -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 (
+
+ {label.name}
+
+ );
+}
diff --git a/web_src/js/features/contextpopup.js b/web_src/js/features/contextpopup.js
index c16820cf1f740..5e28caff190fc 100644
--- a/web_src/js/features/contextpopup.js
+++ b/web_src/js/features/contextpopup.js
@@ -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;
@@ -22,40 +22,24 @@ 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 += `${htmlEscape(label.name)}
`;
- }
- if (labels.length > 0) {
- labels = `${labels}
`;
- }
-
- 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({
@@ -63,14 +47,24 @@ function issuePopup(owner, repo, index, $element) {
delay: {
show: 250
},
- html: `
-
-
${htmlEscape(issue.repository.full_name)} on ${createdAt}
-
${svg(octicon)} ${htmlEscape(issue.title)} #${index}
-
${htmlEscape(body)}
- ${labels}
-
-`
+ html: (
+
+
{issue.repository.full_name} on {createdAt}
+
+
+ {issue.title}
+ #{index}
+
+
{body}
+ {issue.labels && issue.labels.length && (
+
+ {issue.labels.map((label) => (
+
+ )}
+
+ )
});
});
}
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 0960256e2121c..f57139b032cc4 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -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