+
-
-
{{ i18nErrorOccurred }}
-
{{ i18nErrorMessage }}
+
+
+
+ {{ issue.title }}
+ #{{ issue.number }}
+
+
{{ body }}
+
+
diff --git a/web_src/js/features/contextpopup.js b/web_src/js/features/contextpopup.js
index 6a9325ed1cd8..ba985668f624 100644
--- a/web_src/js/features/contextpopup.js
+++ b/web_src/js/features/contextpopup.js
@@ -1,45 +1,71 @@
-import {createApp} from 'vue';
import ContextPopup from '../components/ContextPopup.vue';
+import {createVueRoot} from '../utils/vue.js';
import {parseIssueHref} from '../utils.js';
import {createTippy} from '../modules/tippy.js';
+import {GET} from '../modules/fetch.js';
-export function initContextPopups() {
- const refIssues = document.querySelectorAll('.ref-issue');
- attachRefIssueContextPopup(refIssues);
-}
+const {appSubUrl} = window.config;
-export function attachRefIssueContextPopup(refIssues) {
- for (const refIssue of refIssues) {
- if (refIssue.classList.contains('ref-external-issue')) {
- return;
- }
+async function attach(e) {
+ const link = e.currentTarget;
- const {owner, repo, index} = parseIssueHref(refIssue.getAttribute('href'));
- if (!owner) return;
+ // ignore external issues
+ if (link.classList.contains('ref-external-issue')) return;
+ // ignore links that are already loading
+ if (link.hasAttribute('data-issue-ref-loading')) return;
- const el = document.createElement('div');
- el.classList.add('tw-p-3');
- refIssue.parentNode.insertBefore(el, refIssue.nextSibling);
+ const {owner, repo, index} = parseIssueHref(link.getAttribute('href'));
+ if (!owner) return;
- const view = createApp(ContextPopup);
+ const url = `${appSubUrl}/${owner}/${repo}/issues/${index}/info`; // backend: GetIssueInfo
+ if (link.getAttribute('data-issue-ref-info-url') === url) return; // link already has a tooltip with this url
+
+ try {
+ link.setAttribute('data-issue-ref-loading', 'true');
+ let res;
+ try {
+ res = await GET(url);
+ } catch {}
+ if (!res.ok) return;
+ let issue, labelsHtml;
try {
- view.mount(el);
- } catch (err) {
- console.error(err);
- el.textContent = 'ContextPopup failed to load';
- }
+ ({issue, labelsHtml} = await res.json());
+ } catch {}
+ if (!issue) return;
- createTippy(refIssue, {
+ const repoUrl = `${appSubUrl}/${owner}/${repo}`;
+ const content = createVueRoot(ContextPopup, {issue, labelsHtml, repoUrl});
+ if (!content) return;
+
+ const tippy = createTippy(link, {
theme: 'default',
- content: el,
+ trigger: 'mouseenter focus',
+ content,
placement: 'top-start',
interactive: true,
- role: 'dialog',
- interactiveBorder: 5,
- onShow: () => {
- el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}}));
- },
+ role: 'tooltip',
+ interactiveBorder: 15,
});
+
+ // set attribute on the link that indicates which url the tooltip currently renders
+ link.setAttribute('data-issue-ref-info-url', url);
+
+ // show immediately because this runs during mouseenter and focus
+ tippy.show();
+ } finally {
+ link.removeAttribute('data-issue-ref-loading');
}
}
+
+export function attachRefIssueContextPopup(els) {
+ for (const el of els) {
+ el.addEventListener('mouseenter', attach);
+ el.addEventListener('focus', attach);
+ }
+}
+
+export function initContextPopups() {
+ // TODO: Use MutationObserver to detect newly inserted .ref-issue
+ attachRefIssueContextPopup(document.querySelectorAll('.ref-issue'));
+}
diff --git a/web_src/js/utils/vue.js b/web_src/js/utils/vue.js
new file mode 100644
index 000000000000..1558cfa1a0b2
--- /dev/null
+++ b/web_src/js/utils/vue.js
@@ -0,0 +1,14 @@
+import {createApp} from 'vue';
+
+// create a new vue root and container and mount a component into it
+export function createVueRoot(component, props) {
+ const container = document.createElement('div');
+ const view = createApp(component, props);
+ try {
+ view.mount(container);
+ return container;
+ } catch (err) {
+ console.error(err);
+ return null;
+ }
+}