diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 18f975b4a6bd..b9605880b8df 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -2181,8 +2181,8 @@ func GetIssueInfo(ctx *context.Context) { } ctx.JSON(http.StatusOK, map[string]any{ - "convertedIssue": convert.ToIssue(ctx, ctx.Doer, issue), - "renderedLabels": templates.RenderLabels(ctx, ctx.Locale, issue.Labels, ctx.Repo.RepoLink, issue), + "issue": convert.ToIssue(ctx, ctx.Doer, issue), + "labelsHtml": templates.RenderLabels(ctx, ctx.Locale, issue.Labels, ctx.Repo.RepoLink, issue), }) } diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 22e08e9c8f69..fcd5567742f6 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -36,7 +36,6 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. i18n: { copy_success: {{ctx.Locale.Tr "copy_success"}}, copy_error: {{ctx.Locale.Tr "copy_error"}}, - error_occurred: {{ctx.Locale.Tr "error.occurred"}}, network_error: {{ctx.Locale.Tr "error.network_error"}}, remove_label_str: {{ctx.Locale.Tr "remove_label_str"}}, modal_confirm: {{ctx.Locale.Tr "modal.confirm"}}, diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index 308b82d4b950..130bd2425004 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -641,13 +641,13 @@ func TestGetIssueInfo(t *testing.T) { req := NewRequest(t, "GET", urlStr) resp := session.MakeRequest(t, req, http.StatusOK) var respStruct struct { - ConvertedIssue api.Issue - RenderedLabels template.HTML + Issue api.Issue `json:"issue"` + LabelsHTML template.HTML `json:"labelsHtml"` } DecodeJSON(t, resp, &respStruct) - assert.EqualValues(t, issue.ID, respStruct.ConvertedIssue.ID) - assert.Contains(t, string(respStruct.RenderedLabels), `"labels-list"`) + assert.EqualValues(t, issue.ID, respStruct.Issue.ID) + assert.Contains(t, string(respStruct.LabelsHTML), `"labels-list"`) } func TestUpdateIssueDeadline(t *testing.T) { diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index 8f389ea003e4..040452118bee 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -1,18 +1,22 @@ 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; + } +}