mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-30 06:38:37 -04:00 
			
		
		
		
	Fix "ref-issue" handling in markup (#35739)
This is a follow up for #35662, and also fix #31181, help #30275, fix #31161
This commit is contained in:
		| @@ -36,7 +36,6 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. | |||||||
| 			copy_success: {{ctx.Locale.Tr "copy_success"}}, | 			copy_success: {{ctx.Locale.Tr "copy_success"}}, | ||||||
| 			copy_error: {{ctx.Locale.Tr "copy_error"}}, | 			copy_error: {{ctx.Locale.Tr "copy_error"}}, | ||||||
| 			error_occurred: {{ctx.Locale.Tr "error.occurred"}}, | 			error_occurred: {{ctx.Locale.Tr "error.occurred"}}, | ||||||
| 			network_error: {{ctx.Locale.Tr "error.network_error"}}, |  | ||||||
| 			remove_label_str: {{ctx.Locale.Tr "remove_label_str"}}, | 			remove_label_str: {{ctx.Locale.Tr "remove_label_str"}}, | ||||||
| 			modal_confirm: {{ctx.Locale.Tr "modal.confirm"}}, | 			modal_confirm: {{ctx.Locale.Tr "modal.confirm"}}, | ||||||
| 			modal_cancel: {{ctx.Locale.Tr "modal.cancel"}}, | 			modal_cancel: {{ctx.Locale.Tr "modal.cancel"}}, | ||||||
|   | |||||||
| @@ -2,62 +2,53 @@ | |||||||
| import {SvgIcon} from '../svg.ts'; | import {SvgIcon} from '../svg.ts'; | ||||||
| import {GET} from '../modules/fetch.ts'; | import {GET} from '../modules/fetch.ts'; | ||||||
| import {getIssueColor, getIssueIcon} from '../features/issue.ts'; | import {getIssueColor, getIssueIcon} from '../features/issue.ts'; | ||||||
| import {computed, onMounted, shallowRef, useTemplateRef} from 'vue'; | import {computed, onMounted, shallowRef} from 'vue'; | ||||||
| import type {IssuePathInfo} from '../types.ts'; |  | ||||||
|  |  | ||||||
| const {appSubUrl, i18n} = window.config; | const props = defineProps<{ | ||||||
|  |   repoLink: string, | ||||||
|  |   loadIssueInfoUrl: string, | ||||||
|  | }>(); | ||||||
|  |  | ||||||
| const loading = shallowRef(false); | const loading = shallowRef(false); | ||||||
| const issue = shallowRef(null); | const issue = shallowRef(null); | ||||||
| const renderedLabels = shallowRef(''); | const renderedLabels = shallowRef(''); | ||||||
| const i18nErrorOccurred = i18n.error_occurred; | const errorMessage = shallowRef(null); | ||||||
| const i18nErrorMessage = shallowRef(null); |  | ||||||
|  | const createdAt = computed(() => { | ||||||
|  |   return new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'}); | ||||||
|  | }); | ||||||
|  |  | ||||||
| const createdAt = computed(() => new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'})); |  | ||||||
| const body = computed(() => { | const body = computed(() => { | ||||||
|   const body = issue.value.body.replace(/\n+/g, ' '); |   const body = issue.value.body.replace(/\n+/g, ' '); | ||||||
|   if (body.length > 85) { |   return body.length > 85 ? `${body.substring(0, 85)}…` : body; | ||||||
|     return `${body.substring(0, 85)}…`; |  | ||||||
|   } |  | ||||||
|   return body; |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const root = useTemplateRef('root'); | onMounted(async () => { | ||||||
|  |  | ||||||
| onMounted(() => { |  | ||||||
|   root.value.addEventListener('ce-load-context-popup', (e: CustomEventInit<IssuePathInfo>) => { |  | ||||||
|     if (!loading.value && issue.value === null) { |  | ||||||
|       load(e.detail); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| async function load(issuePathInfo: IssuePathInfo) { |  | ||||||
|   loading.value = true; |   loading.value = true; | ||||||
|   i18nErrorMessage.value = null; |   errorMessage.value = null; | ||||||
|  |  | ||||||
|   try { |   try { | ||||||
|     const response = await GET(`${appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`); // backend: GetIssueInfo |     const resp = await GET(props.loadIssueInfoUrl); | ||||||
|     const respJson = await response.json(); |     if (!resp.ok) { | ||||||
|     if (!response.ok) { |       errorMessage.value = resp.status ? resp.statusText : 'Unknown network error'; | ||||||
|       i18nErrorMessage.value = respJson.message ?? i18n.network_error; |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |     const respJson = await resp.json(); | ||||||
|     issue.value = respJson.convertedIssue; |     issue.value = respJson.convertedIssue; | ||||||
|     renderedLabels.value = respJson.renderedLabels; |     renderedLabels.value = respJson.renderedLabels; | ||||||
|   } catch { |  | ||||||
|     i18nErrorMessage.value = i18n.network_error; |  | ||||||
|   } finally { |   } finally { | ||||||
|     loading.value = false; |     loading.value = false; | ||||||
|   } |   } | ||||||
| } | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <template> | <template> | ||||||
|   <div ref="root"> |   <div class="tw-p-4"> | ||||||
|     <div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/> |     <div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/> | ||||||
|     <div v-if="!loading && issue !== null" class="tw-flex tw-flex-col tw-gap-2"> |     <div v-else-if="issue" class="tw-flex tw-flex-col tw-gap-2"> | ||||||
|       <div class="tw-text-12">{{ issue.repository.full_name }} on {{ createdAt }}</div> |       <div class="tw-text-12"> | ||||||
|  |         <a :href="repoLink" class="muted">{{ issue.repository.full_name }}</a> | ||||||
|  |         on {{ createdAt }} | ||||||
|  |       </div> | ||||||
|       <div class="flex-text-block"> |       <div class="flex-text-block"> | ||||||
|         <svg-icon :name="getIssueIcon(issue)" :class="['text', getIssueColor(issue)]"/> |         <svg-icon :name="getIssueIcon(issue)" :class="['text', getIssueColor(issue)]"/> | ||||||
|         <span class="issue-title tw-font-semibold tw-break-anywhere"> |         <span class="issue-title tw-font-semibold tw-break-anywhere"> | ||||||
| @@ -69,9 +60,8 @@ async function load(issuePathInfo: IssuePathInfo) { | |||||||
|       <!-- eslint-disable-next-line vue/no-v-html --> |       <!-- eslint-disable-next-line vue/no-v-html --> | ||||||
|       <div v-if="issue.labels.length" v-html="renderedLabels"/> |       <div v-if="issue.labels.length" v-html="renderedLabels"/> | ||||||
|     </div> |     </div> | ||||||
|     <div class="tw-flex tw-flex-col tw-gap-2" v-if="!loading && issue === null"> |     <div v-else> | ||||||
|       <div class="tw-text-12">{{ i18nErrorOccurred }}</div> |       {{ errorMessage }} | ||||||
|       <div>{{ i18nErrorMessage }}</div> |  | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|   | |||||||
| @@ -1,43 +0,0 @@ | |||||||
| import {createApp} from 'vue'; |  | ||||||
| import ContextPopup from '../components/ContextPopup.vue'; |  | ||||||
| import {parseIssueHref} from '../utils.ts'; |  | ||||||
| import {createTippy} from '../modules/tippy.ts'; |  | ||||||
|  |  | ||||||
| export function initContextPopups() { |  | ||||||
|   const refIssues = document.querySelectorAll<HTMLElement>('.ref-issue'); |  | ||||||
|   attachRefIssueContextPopup(refIssues); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function attachRefIssueContextPopup(refIssues: NodeListOf<HTMLElement>) { |  | ||||||
|   for (const refIssue of refIssues) { |  | ||||||
|     if (refIssue.classList.contains('ref-external-issue')) continue; |  | ||||||
|  |  | ||||||
|     const issuePathInfo = parseIssueHref(refIssue.getAttribute('href')); |  | ||||||
|     if (!issuePathInfo.ownerName) continue; |  | ||||||
|  |  | ||||||
|     const el = document.createElement('div'); |  | ||||||
|     el.classList.add('tw-p-3'); |  | ||||||
|     refIssue.parentNode.insertBefore(el, refIssue.nextSibling); |  | ||||||
|  |  | ||||||
|     const view = createApp(ContextPopup); |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       view.mount(el); |  | ||||||
|     } catch (err) { |  | ||||||
|       console.error(err); |  | ||||||
|       el.textContent = 'ContextPopup failed to load'; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     createTippy(refIssue, { |  | ||||||
|       theme: 'default', |  | ||||||
|       content: el, |  | ||||||
|       placement: 'top-start', |  | ||||||
|       interactive: true, |  | ||||||
|       role: 'dialog', |  | ||||||
|       interactiveBorder: 5, |  | ||||||
|       onShow: () => { |  | ||||||
|         el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: issuePathInfo})); |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -12,8 +12,6 @@ import {invertFileFolding} from './file-fold.ts'; | |||||||
| import {parseDom, sleep} from '../utils.ts'; | import {parseDom, sleep} from '../utils.ts'; | ||||||
| import {registerGlobalSelectorFunc} from '../modules/observer.ts'; | import {registerGlobalSelectorFunc} from '../modules/observer.ts'; | ||||||
|  |  | ||||||
| const {i18n} = window.config; |  | ||||||
|  |  | ||||||
| function initRepoDiffFileBox(el: HTMLElement) { | function initRepoDiffFileBox(el: HTMLElement) { | ||||||
|   // switch between "rendered" and "source", for image and CSV files |   // switch between "rendered" and "source", for image and CSV files | ||||||
|   queryElems(el, '.file-view-toggle', (btn) => btn.addEventListener('click', () => { |   queryElems(el, '.file-view-toggle', (btn) => btn.addEventListener('click', () => { | ||||||
| @@ -86,7 +84,7 @@ function initRepoDiffConversationForm() { | |||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error('Error:', error); |       console.error('Error:', error); | ||||||
|       showErrorToast(i18n.network_error); |       showErrorToast(`Submit form failed: ${error}`); | ||||||
|     } finally { |     } finally { | ||||||
|       form?.classList.remove('is-loading'); |       form?.classList.remove('is-loading'); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| import {html, htmlRaw} from '../utils/html.ts'; | import {html, htmlRaw} from '../utils/html.ts'; | ||||||
| import {createCodeEditor} from './codeeditor.ts'; | import {createCodeEditor} from './codeeditor.ts'; | ||||||
| import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts'; | import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts'; | ||||||
| import {attachRefIssueContextPopup} from './contextpopup.ts'; |  | ||||||
| import {POST} from '../modules/fetch.ts'; | import {POST} from '../modules/fetch.ts'; | ||||||
| import {initDropzone} from './dropzone.ts'; | import {initDropzone} from './dropzone.ts'; | ||||||
| import {confirmModal} from './comp/ConfirmModal.ts'; | import {confirmModal} from './comp/ConfirmModal.ts'; | ||||||
| @@ -199,5 +198,4 @@ export function initRepoEditor() { | |||||||
| export function renderPreviewPanelContent(previewPanel: Element, htmlContent: string) { | export function renderPreviewPanelContent(previewPanel: Element, htmlContent: string) { | ||||||
|   // the content is from the server, so it is safe to use innerHTML |   // the content is from the server, so it is safe to use innerHTML | ||||||
|   previewPanel.innerHTML = html`<div class="render-content markup">${htmlRaw(htmlContent)}</div>`; |   previewPanel.innerHTML = html`<div class="render-content markup">${htmlRaw(htmlContent)}</div>`; | ||||||
|   attachRefIssueContextPopup(previewPanel.querySelectorAll('p .ref-issue')); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} fr | |||||||
| import {POST} from '../modules/fetch.ts'; | import {POST} from '../modules/fetch.ts'; | ||||||
| import {showErrorToast} from '../modules/toast.ts'; | import {showErrorToast} from '../modules/toast.ts'; | ||||||
| import {hideElem, querySingleVisibleElem, showElem, type DOMEvent} from '../utils/dom.ts'; | import {hideElem, querySingleVisibleElem, showElem, type DOMEvent} from '../utils/dom.ts'; | ||||||
| import {attachRefIssueContextPopup} from './contextpopup.ts'; |  | ||||||
| import {triggerUploadStateChanged} from './comp/EditorUpload.ts'; | import {triggerUploadStateChanged} from './comp/EditorUpload.ts'; | ||||||
| import {convertHtmlToMarkdown} from '../markup/html2markdown.ts'; | import {convertHtmlToMarkdown} from '../markup/html2markdown.ts'; | ||||||
| import {applyAreYouSure, reinitializeAreYouSure} from '../vendor/jquery.are-you-sure.ts'; | import {applyAreYouSure, reinitializeAreYouSure} from '../vendor/jquery.are-you-sure.ts'; | ||||||
| @@ -62,8 +61,6 @@ async function tryOnEditContent(e: DOMEvent<MouseEvent>) { | |||||||
|       renderContent = newRenderContent; |       renderContent = newRenderContent; | ||||||
|  |  | ||||||
|       rawContent.textContent = comboMarkdownEditor.value(); |       rawContent.textContent = comboMarkdownEditor.value(); | ||||||
|       const refIssues = renderContent.querySelectorAll<HTMLElement>('p .ref-issue'); |  | ||||||
|       attachRefIssueContextPopup(refIssues); |  | ||||||
|  |  | ||||||
|       if (!commentContent.querySelector('.dropzone-attachments')) { |       if (!commentContent.querySelector('.dropzone-attachments')) { | ||||||
|         if (data.attachments !== '') { |         if (data.attachments !== '') { | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ import '../../node_modules/easymde/dist/easymde.min.css'; // TODO: lazy load in | |||||||
| import {initHtmx} from './htmx.ts'; | import {initHtmx} from './htmx.ts'; | ||||||
| import {initDashboardRepoList} from './features/dashboard.ts'; | import {initDashboardRepoList} from './features/dashboard.ts'; | ||||||
| import {initGlobalCopyToClipboardListener} from './features/clipboard.ts'; | import {initGlobalCopyToClipboardListener} from './features/clipboard.ts'; | ||||||
| import {initContextPopups} from './features/contextpopup.ts'; |  | ||||||
| import {initRepoGraphGit} from './features/repo-graph.ts'; | import {initRepoGraphGit} from './features/repo-graph.ts'; | ||||||
| import {initHeatmap} from './features/heatmap.ts'; | import {initHeatmap} from './features/heatmap.ts'; | ||||||
| import {initImageDiff} from './features/imagediff.ts'; | import {initImageDiff} from './features/imagediff.ts'; | ||||||
| @@ -97,7 +96,6 @@ const initPerformanceTracer = callInitFunctions([ | |||||||
|   initHeadNavbarContentToggle, |   initHeadNavbarContentToggle, | ||||||
|   initFootLanguageMenu, |   initFootLanguageMenu, | ||||||
|  |  | ||||||
|   initContextPopups, |  | ||||||
|   initHeatmap, |   initHeatmap, | ||||||
|   initImageDiff, |   initImageDiff, | ||||||
|   initMarkupAnchors, |   initMarkupAnchors, | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import {initMarkupRenderAsciicast} from './asciicast.ts'; | |||||||
| import {initMarkupTasklist} from './tasklist.ts'; | import {initMarkupTasklist} from './tasklist.ts'; | ||||||
| import {registerGlobalSelectorFunc} from '../modules/observer.ts'; | import {registerGlobalSelectorFunc} from '../modules/observer.ts'; | ||||||
| import {initMarkupRenderIframe} from './render-iframe.ts'; | import {initMarkupRenderIframe} from './render-iframe.ts'; | ||||||
|  | import {initMarkupRefIssue} from './refissue.ts'; | ||||||
|  |  | ||||||
| // code that runs for all markup content | // code that runs for all markup content | ||||||
| export function initMarkupContent(): void { | export function initMarkupContent(): void { | ||||||
| @@ -15,5 +16,6 @@ export function initMarkupContent(): void { | |||||||
|     initMarkupCodeMath(el); |     initMarkupCodeMath(el); | ||||||
|     initMarkupRenderAsciicast(el); |     initMarkupRenderAsciicast(el); | ||||||
|     initMarkupRenderIframe(el); |     initMarkupRenderIframe(el); | ||||||
|  |     initMarkupRefIssue(el); | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								web_src/js/markup/refissue.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								web_src/js/markup/refissue.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | import {queryElems} from '../utils/dom.ts'; | ||||||
|  | import {parseIssueHref} from '../utils.ts'; | ||||||
|  | import {createApp} from 'vue'; | ||||||
|  | import ContextPopup from '../components/ContextPopup.vue'; | ||||||
|  | import {createTippy, getAttachedTippyInstance} from '../modules/tippy.ts'; | ||||||
|  |  | ||||||
|  | export function initMarkupRefIssue(el: HTMLElement) { | ||||||
|  |   queryElems(el, '.ref-issue', (el) => { | ||||||
|  |     el.addEventListener('mouseenter', showMarkupRefIssuePopup); | ||||||
|  |     el.addEventListener('focus', showMarkupRefIssuePopup); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function showMarkupRefIssuePopup(e: MouseEvent | FocusEvent) { | ||||||
|  |   const refIssue = e.currentTarget as HTMLElement; | ||||||
|  |   if (getAttachedTippyInstance(refIssue)) return; | ||||||
|  |   if (refIssue.classList.contains('ref-external-issue')) return; | ||||||
|  |  | ||||||
|  |   const issuePathInfo = parseIssueHref(refIssue.getAttribute('href')); | ||||||
|  |   if (!issuePathInfo.ownerName) return; | ||||||
|  |  | ||||||
|  |   const el = document.createElement('div'); | ||||||
|  |   const tippy = createTippy(refIssue, { | ||||||
|  |     theme: 'default', | ||||||
|  |     content: el, | ||||||
|  |     trigger: 'mouseenter focus', | ||||||
|  |     placement: 'top-start', | ||||||
|  |     interactive: true, | ||||||
|  |     role: 'dialog', | ||||||
|  |     interactiveBorder: 5, | ||||||
|  |     // onHide() { return false }, // help to keep the popup and debug the layout | ||||||
|  |     onShow: () => { | ||||||
|  |       const view = createApp(ContextPopup, { | ||||||
|  |         // backend: GetIssueInfo | ||||||
|  |         loadIssueInfoUrl: `${window.config.appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`, | ||||||
|  |       }); | ||||||
|  |       view.mount(el); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  |   tippy.show(); | ||||||
|  | } | ||||||
| @@ -209,3 +209,7 @@ export function showTemporaryTooltip(target: Element, content: Content): void { | |||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function getAttachedTippyInstance(el: Element): Instance | null { | ||||||
|  |   return el._tippy ?? null; | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user