mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 21:16:26 +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"
 | 
			
		||||
	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"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
 | 
			
		||||
func IssueSuggestions(ctx *context.Context) {
 | 
			
		||||
	keyword := ctx.Req.FormValue("q")
 | 
			
		||||
@ -61,13 +52,14 @@ func IssueSuggestions(ctx *context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	suggestions := make([]*issueSuggestion, 0, len(issues))
 | 
			
		||||
	suggestions := make([]*structs.Issue, 0, len(issues))
 | 
			
		||||
 | 
			
		||||
	for _, issue := range issues {
 | 
			
		||||
		suggestion := &issueSuggestion{
 | 
			
		||||
		suggestion := &structs.Issue{
 | 
			
		||||
			ID:    issue.ID,
 | 
			
		||||
			Index: issue.Index,
 | 
			
		||||
			Title: issue.Title,
 | 
			
		||||
			State: string(issue.State()),
 | 
			
		||||
			State: issue.State(),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if issue.IsPull {
 | 
			
		||||
@ -76,12 +68,9 @@ func IssueSuggestions(ctx *context.Context) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if issue.PullRequest != nil {
 | 
			
		||||
				suggestion.PullRequest = &struct {
 | 
			
		||||
					Merged bool `json:"merged"`
 | 
			
		||||
					Draft  bool `json:"draft"`
 | 
			
		||||
				}{
 | 
			
		||||
					Merged: issue.PullRequest.HasMerged,
 | 
			
		||||
					Draft:  issue.PullRequest.IsWorkInProgress(ctx),
 | 
			
		||||
				suggestion.PullRequest = &structs.PullRequestMeta{
 | 
			
		||||
					HasMerged:        issue.PullRequest.HasMerged,
 | 
			
		||||
					IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx),
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import {SvgIcon} from '../svg.ts';
 | 
			
		||||
import {GET} from '../modules/fetch.ts';
 | 
			
		||||
import {getIssueColor, getIssueIcon} from '../features/issue.ts';
 | 
			
		||||
import {computed, onMounted, ref} from 'vue';
 | 
			
		||||
import type {IssuePathInfo} from '../types.ts';
 | 
			
		||||
 | 
			
		||||
const {appSubUrl, i18n} = window.config;
 | 
			
		||||
 | 
			
		||||
@ -25,19 +26,19 @@ const root = ref<HTMLElement | null>(null);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  root.value.addEventListener('ce-load-context-popup', (e: CustomEvent) => {
 | 
			
		||||
    const data = e.detail;
 | 
			
		||||
    const data: IssuePathInfo = e.detail;
 | 
			
		||||
    if (!loading.value && issue.value === null) {
 | 
			
		||||
      load(data);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function load(data) {
 | 
			
		||||
async function load(issuePathInfo: IssuePathInfo) {
 | 
			
		||||
  loading.value = true;
 | 
			
		||||
  i18nErrorMessage.value = null;
 | 
			
		||||
 | 
			
		||||
  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();
 | 
			
		||||
    if (!response.ok) {
 | 
			
		||||
      i18nErrorMessage.value = respJson.message ?? i18n.network_error;
 | 
			
		||||
 | 
			
		||||
@ -1,39 +1,29 @@
 | 
			
		||||
import {matchEmoji, matchMention, matchIssue} from '../../utils/match.ts';
 | 
			
		||||
import {emojiString} from '../emoji.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 {getIssueColor, getIssueIcon} from '../issue.ts';
 | 
			
		||||
import {debounce} from 'perfect-debounce';
 | 
			
		||||
 | 
			
		||||
const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => {
 | 
			
		||||
  const {owner, repo, index} = parseIssueHref(window.location.href);
 | 
			
		||||
  const matches = await matchIssue(owner, repo, index, text);
 | 
			
		||||
  let issuePathInfo = parseIssueHref(window.location.href);
 | 
			
		||||
  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});
 | 
			
		||||
 | 
			
		||||
  const ul = document.createElement('ul');
 | 
			
		||||
  ul.classList.add('suggestions');
 | 
			
		||||
  const ul = createElementFromAttrs('ul', {class: 'suggestions'});
 | 
			
		||||
  for (const issue of matches) {
 | 
			
		||||
    const li = createElementFromAttrs('li', {
 | 
			
		||||
      role: 'option',
 | 
			
		||||
      'data-value': `${key}${issue.id}`,
 | 
			
		||||
      class: 'tw-flex tw-gap-2',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
    const li = createElementFromAttrs(
 | 
			
		||||
      'li', {role: 'option', class: 'tw-flex tw-gap-2', 'data-value': `${key}${issue.number}`},
 | 
			
		||||
      createElementFromHTML(svg(getIssueIcon(issue), 16, ['text', getIssueColor(issue)])),
 | 
			
		||||
      createElementFromAttrs('span', null, `#${issue.number}`),
 | 
			
		||||
      createElementFromAttrs('span', null, issue.title),
 | 
			
		||||
    );
 | 
			
		||||
    ul.append(li);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  resolve({matched: true, fragment: ul});
 | 
			
		||||
}), 100);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,12 +10,10 @@ export function initContextPopups() {
 | 
			
		||||
 | 
			
		||||
export function attachRefIssueContextPopup(refIssues) {
 | 
			
		||||
  for (const refIssue of refIssues) {
 | 
			
		||||
    if (refIssue.classList.contains('ref-external-issue')) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (refIssue.classList.contains('ref-external-issue')) continue;
 | 
			
		||||
 | 
			
		||||
    const {owner, repo, index} = parseIssueHref(refIssue.getAttribute('href'));
 | 
			
		||||
    if (!owner) return;
 | 
			
		||||
    const issuePathInfo = parseIssueHref(refIssue.getAttribute('href'));
 | 
			
		||||
    if (!issuePathInfo.ownerName) continue;
 | 
			
		||||
 | 
			
		||||
    const el = document.createElement('div');
 | 
			
		||||
    el.classList.add('tw-p-3');
 | 
			
		||||
@ -38,7 +36,7 @@ export function attachRefIssueContextPopup(refIssues) {
 | 
			
		||||
      role: 'dialog',
 | 
			
		||||
      interactiveBorder: 5,
 | 
			
		||||
      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.
 | 
			
		||||
 | 
			
		||||
// 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 (size === 16 && !className) return svgs[name];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -30,15 +30,16 @@ export type RequestOpts = {
 | 
			
		||||
  data?: RequestData,
 | 
			
		||||
} & RequestInit;
 | 
			
		||||
 | 
			
		||||
export type IssueData = {
 | 
			
		||||
  owner: string,
 | 
			
		||||
  repo: string,
 | 
			
		||||
  type: string,
 | 
			
		||||
  index: string,
 | 
			
		||||
export type IssuePathInfo = {
 | 
			
		||||
  ownerName: string,
 | 
			
		||||
  repoName: string,
 | 
			
		||||
  pathType: string,
 | 
			
		||||
  indexString?: string,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Issue = {
 | 
			
		||||
  id: number;
 | 
			
		||||
  number: number;
 | 
			
		||||
  title: string;
 | 
			
		||||
  state: 'open' | 'closed';
 | 
			
		||||
  pull_request?: {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import {
 | 
			
		||||
  basename, extname, isObject, stripTags, parseIssueHref,
 | 
			
		||||
  parseUrl, translateMonth, translateDay, blobToDataURI,
 | 
			
		||||
  toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile,
 | 
			
		||||
  toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, parseIssueNewHref,
 | 
			
		||||
} from './utils.ts';
 | 
			
		||||
 | 
			
		||||
test('basename', () => {
 | 
			
		||||
@ -28,21 +28,27 @@ test('stripTags', () => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('parseIssueHref', () => {
 | 
			
		||||
  expect(parseIssueHref('/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
 | 
			
		||||
  expect(parseIssueHref('/owner/repo/pulls/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'});
 | 
			
		||||
  expect(parseIssueHref('/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
 | 
			
		||||
  expect(parseIssueHref('/sub/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
 | 
			
		||||
  expect(parseIssueHref('/sub/sub2/owner/repo/pulls/1')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '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#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '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/pulls/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '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/sub/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '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/issues/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
 | 
			
		||||
  expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
 | 
			
		||||
  expect(parseIssueHref('')).toEqual({owner: undefined, repo: undefined, type: undefined, index: undefined});
 | 
			
		||||
  expect(parseIssueHref('/owner/repo/issues/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '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({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '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({ownerName: 'owner', repoName: 'repo', pathType: 'pulls', indexString: '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({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '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({ownerName: 'owner', repoName: 'repo', pathType: 'pulls', indexString: '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({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '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({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '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({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', () => {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
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
 | 
			
		||||
export function basename(path: string): string {
 | 
			
		||||
@ -31,10 +31,16 @@ export function stripTags(text: string): string {
 | 
			
		||||
  return text.replace(/<[^>]*>?/g, '');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function parseIssueHref(href: string): IssueData {
 | 
			
		||||
export function parseIssueHref(href: string): IssuePathInfo {
 | 
			
		||||
  const path = (href || '').replace(/[#?].*$/, '');
 | 
			
		||||
  const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
 | 
			
		||||
  return {owner, repo, type, index};
 | 
			
		||||
  const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
 | 
			
		||||
  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'
 | 
			
		||||
 | 
			
		||||
@ -8,11 +8,11 @@ test('createElementFromAttrs', () => {
 | 
			
		||||
  const el = createElementFromAttrs('button', {
 | 
			
		||||
    id: 'the-id',
 | 
			
		||||
    class: 'cls-1 cls-2',
 | 
			
		||||
    'data-foo': 'the-data',
 | 
			
		||||
    disabled: true,
 | 
			
		||||
    checked: false,
 | 
			
		||||
    required: null,
 | 
			
		||||
    tabindex: 0,
 | 
			
		||||
  });
 | 
			
		||||
  expect(el.outerHTML).toEqual('<button id="the-id" class="cls-1 cls-2" data-foo="the-data" disabled="" tabindex="0"></button>');
 | 
			
		||||
    'data-foo': 'the-data',
 | 
			
		||||
  }, '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
 | 
			
		||||
export function createElementFromHTML(htmlString: string) {
 | 
			
		||||
export function createElementFromHTML(htmlString: string): HTMLElement {
 | 
			
		||||
  const div = document.createElement('div');
 | 
			
		||||
  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);
 | 
			
		||||
  for (const [key, value] of Object.entries(attrs)) {
 | 
			
		||||
  for (const [key, value] of Object.entries(attrs || {})) {
 | 
			
		||||
    if (value === undefined || value === null) continue;
 | 
			
		||||
    if (typeof value === 'boolean') {
 | 
			
		||||
      el.toggleAttribute(key, value);
 | 
			
		||||
    } else {
 | 
			
		||||
      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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import emojis from '../../../assets/emoji.json';
 | 
			
		||||
import type {Issue} from '../features/issue.ts';
 | 
			
		||||
import {GET} from '../modules/fetch.ts';
 | 
			
		||||
import type {Issue} from '../features/issue.ts';
 | 
			
		||||
 | 
			
		||||
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 issues: Issue[] = await res.json();
 | 
			
		||||
  const issueIndex = parseInt(issueIndexStr);
 | 
			
		||||
  const issueNumber = parseInt(issueIndexStr);
 | 
			
		||||
 | 
			
		||||
  // 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