mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 05:11:41 +01:00 
			
		
		
		
	Fix suggestions for issues (#32380)
This commit is contained in:
		
							parent
							
								
									f4d3aaeeb9
								
							
						
					
					
						commit
						a4a121c684
					
				| @ -11,19 +11,10 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues" | 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues" | ||||||
| 	"code.gitea.io/gitea/modules/optional" | 	"code.gitea.io/gitea/modules/optional" | ||||||
|  | 	"code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/services/context" | 	"code.gitea.io/gitea/services/context" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type issueSuggestion struct { |  | ||||||
| 	ID          int64  `json:"id"` |  | ||||||
| 	Title       string `json:"title"` |  | ||||||
| 	State       string `json:"state"` |  | ||||||
| 	PullRequest *struct { |  | ||||||
| 		Merged bool `json:"merged"` |  | ||||||
| 		Draft  bool `json:"draft"` |  | ||||||
| 	} `json:"pull_request,omitempty"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // IssueSuggestions returns a list of issue suggestions | // IssueSuggestions returns a list of issue suggestions | ||||||
| func IssueSuggestions(ctx *context.Context) { | func IssueSuggestions(ctx *context.Context) { | ||||||
| 	keyword := ctx.Req.FormValue("q") | 	keyword := ctx.Req.FormValue("q") | ||||||
| @ -61,13 +52,14 @@ func IssueSuggestions(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	suggestions := make([]*issueSuggestion, 0, len(issues)) | 	suggestions := make([]*structs.Issue, 0, len(issues)) | ||||||
| 
 | 
 | ||||||
| 	for _, issue := range issues { | 	for _, issue := range issues { | ||||||
| 		suggestion := &issueSuggestion{ | 		suggestion := &structs.Issue{ | ||||||
| 			ID:    issue.ID, | 			ID:    issue.ID, | ||||||
|  | 			Index: issue.Index, | ||||||
| 			Title: issue.Title, | 			Title: issue.Title, | ||||||
| 			State: string(issue.State()), | 			State: issue.State(), | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if issue.IsPull { | 		if issue.IsPull { | ||||||
| @ -76,12 +68,9 @@ func IssueSuggestions(ctx *context.Context) { | |||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 			if issue.PullRequest != nil { | 			if issue.PullRequest != nil { | ||||||
| 				suggestion.PullRequest = &struct { | 				suggestion.PullRequest = &structs.PullRequestMeta{ | ||||||
| 					Merged bool `json:"merged"` | 					HasMerged:        issue.PullRequest.HasMerged, | ||||||
| 					Draft  bool `json:"draft"` | 					IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx), | ||||||
| 				}{ |  | ||||||
| 					Merged: issue.PullRequest.HasMerged, |  | ||||||
| 					Draft:  issue.PullRequest.IsWorkInProgress(ctx), |  | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ 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, ref} from 'vue'; | import {computed, onMounted, ref} from 'vue'; | ||||||
|  | import type {IssuePathInfo} from '../types.ts'; | ||||||
| 
 | 
 | ||||||
| const {appSubUrl, i18n} = window.config; | const {appSubUrl, i18n} = window.config; | ||||||
| 
 | 
 | ||||||
| @ -25,19 +26,19 @@ const root = ref<HTMLElement | null>(null); | |||||||
| 
 | 
 | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
|   root.value.addEventListener('ce-load-context-popup', (e: CustomEvent) => { |   root.value.addEventListener('ce-load-context-popup', (e: CustomEvent) => { | ||||||
|     const data = e.detail; |     const data: IssuePathInfo = e.detail; | ||||||
|     if (!loading.value && issue.value === null) { |     if (!loading.value && issue.value === null) { | ||||||
|       load(data); |       load(data); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| async function load(data) { | async function load(issuePathInfo: IssuePathInfo) { | ||||||
|   loading.value = true; |   loading.value = true; | ||||||
|   i18nErrorMessage.value = null; |   i18nErrorMessage.value = null; | ||||||
| 
 | 
 | ||||||
|   try { |   try { | ||||||
|     const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`); // backend: GetIssueInfo |     const response = await GET(`${appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`); // backend: GetIssueInfo | ||||||
|     const respJson = await response.json(); |     const respJson = await response.json(); | ||||||
|     if (!response.ok) { |     if (!response.ok) { | ||||||
|       i18nErrorMessage.value = respJson.message ?? i18n.network_error; |       i18nErrorMessage.value = respJson.message ?? i18n.network_error; | ||||||
|  | |||||||
| @ -1,39 +1,29 @@ | |||||||
| import {matchEmoji, matchMention, matchIssue} from '../../utils/match.ts'; | import {matchEmoji, matchMention, matchIssue} from '../../utils/match.ts'; | ||||||
| import {emojiString} from '../emoji.ts'; | import {emojiString} from '../emoji.ts'; | ||||||
| import {svg} from '../../svg.ts'; | import {svg} from '../../svg.ts'; | ||||||
| import {parseIssueHref} from '../../utils.ts'; | import {parseIssueHref, parseIssueNewHref} from '../../utils.ts'; | ||||||
| import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts'; | import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts'; | ||||||
| import {getIssueColor, getIssueIcon} from '../issue.ts'; | import {getIssueColor, getIssueIcon} from '../issue.ts'; | ||||||
| import {debounce} from 'perfect-debounce'; | import {debounce} from 'perfect-debounce'; | ||||||
| 
 | 
 | ||||||
| const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => { | const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => { | ||||||
|   const {owner, repo, index} = parseIssueHref(window.location.href); |   let issuePathInfo = parseIssueHref(window.location.href); | ||||||
|   const matches = await matchIssue(owner, repo, index, text); |   if (!issuePathInfo.ownerName) issuePathInfo = parseIssueNewHref(window.location.href); | ||||||
|  |   if (!issuePathInfo.ownerName) return resolve({matched: false}); | ||||||
|  | 
 | ||||||
|  |   const matches = await matchIssue(issuePathInfo.ownerName, issuePathInfo.repoName, issuePathInfo.indexString, text); | ||||||
|   if (!matches.length) return resolve({matched: false}); |   if (!matches.length) return resolve({matched: false}); | ||||||
| 
 | 
 | ||||||
|   const ul = document.createElement('ul'); |   const ul = createElementFromAttrs('ul', {class: 'suggestions'}); | ||||||
|   ul.classList.add('suggestions'); |  | ||||||
|   for (const issue of matches) { |   for (const issue of matches) { | ||||||
|     const li = createElementFromAttrs('li', { |     const li = createElementFromAttrs( | ||||||
|       role: 'option', |       'li', {role: 'option', class: 'tw-flex tw-gap-2', 'data-value': `${key}${issue.number}`}, | ||||||
|       'data-value': `${key}${issue.id}`, |       createElementFromHTML(svg(getIssueIcon(issue), 16, ['text', getIssueColor(issue)])), | ||||||
|       class: 'tw-flex tw-gap-2', |       createElementFromAttrs('span', null, `#${issue.number}`), | ||||||
|     }); |       createElementFromAttrs('span', null, issue.title), | ||||||
| 
 |     ); | ||||||
|     const icon = svg(getIssueIcon(issue), 16, ['text', getIssueColor(issue)].join(' ')); |  | ||||||
|     li.append(createElementFromHTML(icon)); |  | ||||||
| 
 |  | ||||||
|     const id = document.createElement('span'); |  | ||||||
|     id.textContent = issue.id.toString(); |  | ||||||
|     li.append(id); |  | ||||||
| 
 |  | ||||||
|     const nameSpan = document.createElement('span'); |  | ||||||
|     nameSpan.textContent = issue.title; |  | ||||||
|     li.append(nameSpan); |  | ||||||
| 
 |  | ||||||
|     ul.append(li); |     ul.append(li); | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   resolve({matched: true, fragment: ul}); |   resolve({matched: true, fragment: ul}); | ||||||
| }), 100); | }), 100); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -10,12 +10,10 @@ export function initContextPopups() { | |||||||
| 
 | 
 | ||||||
| export function attachRefIssueContextPopup(refIssues) { | export function attachRefIssueContextPopup(refIssues) { | ||||||
|   for (const refIssue of refIssues) { |   for (const refIssue of refIssues) { | ||||||
|     if (refIssue.classList.contains('ref-external-issue')) { |     if (refIssue.classList.contains('ref-external-issue')) continue; | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const {owner, repo, index} = parseIssueHref(refIssue.getAttribute('href')); |     const issuePathInfo = parseIssueHref(refIssue.getAttribute('href')); | ||||||
|     if (!owner) return; |     if (!issuePathInfo.ownerName) continue; | ||||||
| 
 | 
 | ||||||
|     const el = document.createElement('div'); |     const el = document.createElement('div'); | ||||||
|     el.classList.add('tw-p-3'); |     el.classList.add('tw-p-3'); | ||||||
| @ -38,7 +36,7 @@ export function attachRefIssueContextPopup(refIssues) { | |||||||
|       role: 'dialog', |       role: 'dialog', | ||||||
|       interactiveBorder: 5, |       interactiveBorder: 5, | ||||||
|       onShow: () => { |       onShow: () => { | ||||||
|         el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}})); |         el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: issuePathInfo})); | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -153,7 +153,8 @@ export type SvgName = keyof typeof svgs; | |||||||
| //  most of the SVG icons in assets couldn't be used directly.
 | //  most of the SVG icons in assets couldn't be used directly.
 | ||||||
| 
 | 
 | ||||||
| // retrieve an HTML string for given SVG icon name, size and additional classes
 | // retrieve an HTML string for given SVG icon name, size and additional classes
 | ||||||
| export function svg(name: SvgName, size = 16, className = '') { | export function svg(name: SvgName, size = 16, classNames: string|string[]): string { | ||||||
|  |   const className = Array.isArray(classNames) ? classNames.join(' ') : classNames; | ||||||
|   if (!(name in svgs)) throw new Error(`Unknown SVG icon: ${name}`); |   if (!(name in svgs)) throw new Error(`Unknown SVG icon: ${name}`); | ||||||
|   if (size === 16 && !className) return svgs[name]; |   if (size === 16 && !className) return svgs[name]; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -30,15 +30,16 @@ export type RequestOpts = { | |||||||
|   data?: RequestData, |   data?: RequestData, | ||||||
| } & RequestInit; | } & RequestInit; | ||||||
| 
 | 
 | ||||||
| export type IssueData = { | export type IssuePathInfo = { | ||||||
|   owner: string, |   ownerName: string, | ||||||
|   repo: string, |   repoName: string, | ||||||
|   type: string, |   pathType: string, | ||||||
|   index: string, |   indexString?: string, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type Issue = { | export type Issue = { | ||||||
|   id: number; |   id: number; | ||||||
|  |   number: number; | ||||||
|   title: string; |   title: string; | ||||||
|   state: 'open' | 'closed'; |   state: 'open' | 'closed'; | ||||||
|   pull_request?: { |   pull_request?: { | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { | import { | ||||||
|   basename, extname, isObject, stripTags, parseIssueHref, |   basename, extname, isObject, stripTags, parseIssueHref, | ||||||
|   parseUrl, translateMonth, translateDay, blobToDataURI, |   parseUrl, translateMonth, translateDay, blobToDataURI, | ||||||
|   toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, |   toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, parseIssueNewHref, | ||||||
| } from './utils.ts'; | } from './utils.ts'; | ||||||
| 
 | 
 | ||||||
| test('basename', () => { | test('basename', () => { | ||||||
| @ -28,21 +28,27 @@ test('stripTags', () => { | |||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('parseIssueHref', () => { | test('parseIssueHref', () => { | ||||||
|   expect(parseIssueHref('/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); |   expect(parseIssueHref('/owner/repo/issues/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); | ||||||
|   expect(parseIssueHref('/owner/repo/pulls/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'}); |   expect(parseIssueHref('/owner/repo/pulls/1?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls', indexString: '1'}); | ||||||
|   expect(parseIssueHref('/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); |   expect(parseIssueHref('/owner/repo/issues/1#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); | ||||||
|   expect(parseIssueHref('/sub/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); |   expect(parseIssueHref('/sub/owner/repo/issues/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); | ||||||
|   expect(parseIssueHref('/sub/sub2/owner/repo/pulls/1')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'}); |   expect(parseIssueHref('/sub/sub2/owner/repo/pulls/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls', indexString: '1'}); | ||||||
|   expect(parseIssueHref('/sub/sub2/owner/repo/issues/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); |   expect(parseIssueHref('/sub/sub2/owner/repo/issues/1?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); | ||||||
|   expect(parseIssueHref('/sub/sub2/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); |   expect(parseIssueHref('/sub/sub2/owner/repo/issues/1#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); | ||||||
|   expect(parseIssueHref('https://example.com/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); |   expect(parseIssueHref('https://example.com/owner/repo/issues/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); | ||||||
|   expect(parseIssueHref('https://example.com/owner/repo/pulls/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'}); |   expect(parseIssueHref('https://example.com/owner/repo/pulls/1?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls', indexString: '1'}); | ||||||
|   expect(parseIssueHref('https://example.com/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); |   expect(parseIssueHref('https://example.com/owner/repo/issues/1#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); | ||||||
|   expect(parseIssueHref('https://example.com/sub/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); |   expect(parseIssueHref('https://example.com/sub/owner/repo/issues/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); | ||||||
|   expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/pulls/1')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'}); |   expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/pulls/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls', indexString: '1'}); | ||||||
|   expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); |   expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); | ||||||
|   expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); |   expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); | ||||||
|   expect(parseIssueHref('')).toEqual({owner: undefined, repo: undefined, type: undefined, index: undefined}); |   expect(parseIssueHref('')).toEqual({ownerName: undefined, repoName: undefined, type: undefined, index: undefined}); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('parseIssueNewHref', () => { | ||||||
|  |   expect(parseIssueNewHref('/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'}); | ||||||
|  |   expect(parseIssueNewHref('/owner/repo/issues/new?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'}); | ||||||
|  |   expect(parseIssueNewHref('/sub/owner/repo/issues/new#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'}); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('parseUrl', () => { | test('parseUrl', () => { | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import {encode, decode} from 'uint8-to-base64'; | import {encode, decode} from 'uint8-to-base64'; | ||||||
| import type {IssueData} from './types.ts'; | import type {IssuePathInfo} from './types.ts'; | ||||||
| 
 | 
 | ||||||
| // transform /path/to/file.ext to file.ext
 | // transform /path/to/file.ext to file.ext
 | ||||||
| export function basename(path: string): string { | export function basename(path: string): string { | ||||||
| @ -31,10 +31,16 @@ export function stripTags(text: string): string { | |||||||
|   return text.replace(/<[^>]*>?/g, ''); |   return text.replace(/<[^>]*>?/g, ''); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function parseIssueHref(href: string): IssueData { | export function parseIssueHref(href: string): IssuePathInfo { | ||||||
|   const path = (href || '').replace(/[#?].*$/, ''); |   const path = (href || '').replace(/[#?].*$/, ''); | ||||||
|   const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || []; |   const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || []; | ||||||
|   return {owner, repo, type, index}; |   return {ownerName, repoName, pathType, indexString}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function parseIssueNewHref(href: string): IssuePathInfo { | ||||||
|  |   const path = (href || '').replace(/[#?].*$/, ''); | ||||||
|  |   const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/new/.exec(path) || []; | ||||||
|  |   return {ownerName, repoName, pathType, indexString}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // parse a URL, either relative '/path' or absolute 'https://localhost/path'
 | // parse a URL, either relative '/path' or absolute 'https://localhost/path'
 | ||||||
|  | |||||||
| @ -8,11 +8,11 @@ test('createElementFromAttrs', () => { | |||||||
|   const el = createElementFromAttrs('button', { |   const el = createElementFromAttrs('button', { | ||||||
|     id: 'the-id', |     id: 'the-id', | ||||||
|     class: 'cls-1 cls-2', |     class: 'cls-1 cls-2', | ||||||
|     'data-foo': 'the-data', |  | ||||||
|     disabled: true, |     disabled: true, | ||||||
|     checked: false, |     checked: false, | ||||||
|     required: null, |     required: null, | ||||||
|     tabindex: 0, |     tabindex: 0, | ||||||
|   }); |     'data-foo': 'the-data', | ||||||
|   expect(el.outerHTML).toEqual('<button id="the-id" class="cls-1 cls-2" data-foo="the-data" disabled="" tabindex="0"></button>'); |   }, 'txt', createElementFromHTML('<span>inner</span>')); | ||||||
|  |   expect(el.outerHTML).toEqual('<button id="the-id" class="cls-1 cls-2" disabled="" tabindex="0" data-foo="the-data">txt<span>inner</span></button>'); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -298,22 +298,24 @@ export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: st | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Warning: Do not enter any unsanitized variables here
 | // Warning: Do not enter any unsanitized variables here
 | ||||||
| export function createElementFromHTML(htmlString: string) { | export function createElementFromHTML(htmlString: string): HTMLElement { | ||||||
|   const div = document.createElement('div'); |   const div = document.createElement('div'); | ||||||
|   div.innerHTML = htmlString.trim(); |   div.innerHTML = htmlString.trim(); | ||||||
|   return div.firstChild as Element; |   return div.firstChild as HTMLElement; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function createElementFromAttrs(tagName: string, attrs: Record<string, any>) { | export function createElementFromAttrs(tagName: string, attrs: Record<string, any>, ...children: (Node|string)[]): HTMLElement { | ||||||
|   const el = document.createElement(tagName); |   const el = document.createElement(tagName); | ||||||
|   for (const [key, value] of Object.entries(attrs)) { |   for (const [key, value] of Object.entries(attrs || {})) { | ||||||
|     if (value === undefined || value === null) continue; |     if (value === undefined || value === null) continue; | ||||||
|     if (typeof value === 'boolean') { |     if (typeof value === 'boolean') { | ||||||
|       el.toggleAttribute(key, value); |       el.toggleAttribute(key, value); | ||||||
|     } else { |     } else { | ||||||
|       el.setAttribute(key, String(value)); |       el.setAttribute(key, String(value)); | ||||||
|     } |     } | ||||||
|     // TODO: in the future we could make it also support "textContent" and "innerHTML" properties if needed
 |   } | ||||||
|  |   for (const child of children) { | ||||||
|  |     el.append(child instanceof Node ? child : document.createTextNode(child)); | ||||||
|   } |   } | ||||||
|   return el; |   return el; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import emojis from '../../../assets/emoji.json'; | import emojis from '../../../assets/emoji.json'; | ||||||
| import type {Issue} from '../features/issue.ts'; |  | ||||||
| import {GET} from '../modules/fetch.ts'; | import {GET} from '../modules/fetch.ts'; | ||||||
|  | import type {Issue} from '../features/issue.ts'; | ||||||
| 
 | 
 | ||||||
| const maxMatches = 6; | const maxMatches = 6; | ||||||
| 
 | 
 | ||||||
| @ -49,8 +49,8 @@ export async function matchIssue(owner: string, repo: string, issueIndexStr: str | |||||||
|   const res = await GET(`${window.config.appSubUrl}/${owner}/${repo}/issues/suggestions?q=${encodeURIComponent(query)}`); |   const res = await GET(`${window.config.appSubUrl}/${owner}/${repo}/issues/suggestions?q=${encodeURIComponent(query)}`); | ||||||
| 
 | 
 | ||||||
|   const issues: Issue[] = await res.json(); |   const issues: Issue[] = await res.json(); | ||||||
|   const issueIndex = parseInt(issueIndexStr); |   const issueNumber = parseInt(issueIndexStr); | ||||||
| 
 | 
 | ||||||
|   // filter out issue with same id
 |   // filter out issue with same id
 | ||||||
|   return issues.filter((i) => i.id !== issueIndex); |   return issues.filter((i) => i.number !== issueNumber); | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user