mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-10-05 19:30:58 +00:00
feat: add issue suggestions
This commit is contained in:
parent
5e9e146545
commit
3d4372c8bf
11 changed files with 302 additions and 5 deletions
|
@ -17,6 +17,7 @@ import (
|
||||||
user_model "forgejo.org/models/user"
|
user_model "forgejo.org/models/user"
|
||||||
"forgejo.org/modules/container"
|
"forgejo.org/modules/container"
|
||||||
"forgejo.org/modules/log"
|
"forgejo.org/modules/log"
|
||||||
|
"forgejo.org/modules/optional"
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
api "forgejo.org/modules/structs"
|
api "forgejo.org/modules/structs"
|
||||||
"forgejo.org/modules/timeutil"
|
"forgejo.org/modules/timeutil"
|
||||||
|
@ -521,6 +522,45 @@ func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
|
||||||
return issue, nil
|
return issue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isPullToCond(isPull optional.Option[bool]) builder.Cond {
|
||||||
|
if isPull.Has() {
|
||||||
|
return builder.Eq{"is_pull": isPull.Value()}
|
||||||
|
}
|
||||||
|
return builder.NewCond()
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindLatestUpdatedIssues(ctx context.Context, repoID int64, isPull optional.Option[bool], pageSize int) (IssueList, error) {
|
||||||
|
issues := make([]*Issue, 0, pageSize)
|
||||||
|
err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
|
||||||
|
And(isPullToCond(isPull)).
|
||||||
|
OrderBy("updated_unix DESC").
|
||||||
|
Limit(pageSize).
|
||||||
|
Find(&issues)
|
||||||
|
return issues, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindIssuesSuggestionByKeyword(ctx context.Context, repoID int64, keyword string, isPull optional.Option[bool], excludedID int64, pageSize int) (IssueList, error) {
|
||||||
|
cond := builder.NewCond()
|
||||||
|
if excludedID > 0 {
|
||||||
|
cond = cond.And(builder.Neq{"`id`": excludedID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// It seems that GitHub searches both title and content (maybe sorting by the search engine's ranking system?)
|
||||||
|
// The first PR (https://github.com/go-gitea/gitea/pull/32327) uses "search indexer" to search "name(title) + content"
|
||||||
|
// But it seems that searching "content" (especially LIKE by DB engine) generates worse (unusable) results.
|
||||||
|
// So now (https://github.com/go-gitea/gitea/pull/33538) it only searches "name(title)", leave the improvements to the future.
|
||||||
|
cond = cond.And(db.BuildCaseInsensitiveLike("`name`", keyword))
|
||||||
|
|
||||||
|
issues := make([]*Issue, 0, pageSize)
|
||||||
|
err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
|
||||||
|
And(isPullToCond(isPull)).
|
||||||
|
And(cond).
|
||||||
|
OrderBy("updated_unix DESC, `index` DESC").
|
||||||
|
Limit(pageSize).
|
||||||
|
Find(&issues)
|
||||||
|
return issues, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetIssueWithAttrsByIndex returns issue by index in a repository.
|
// GetIssueWithAttrsByIndex returns issue by index in a repository.
|
||||||
func GetIssueWithAttrsByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
|
func GetIssueWithAttrsByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
|
||||||
issue, err := GetIssueByIndex(ctx, repoID, index)
|
issue, err := GetIssueByIndex(ctx, repoID, index)
|
||||||
|
|
7
package-lock.json
generated
7
package-lock.json
generated
|
@ -37,6 +37,7 @@
|
||||||
"monaco-editor": "0.52.2",
|
"monaco-editor": "0.52.2",
|
||||||
"monaco-editor-webpack-plugin": "7.1.0",
|
"monaco-editor-webpack-plugin": "7.1.0",
|
||||||
"pdfobject": "2.3.0",
|
"pdfobject": "2.3.0",
|
||||||
|
"perfect-debounce": "1.0.0",
|
||||||
"postcss": "8.5.2",
|
"postcss": "8.5.2",
|
||||||
"postcss-loader": "8.1.1",
|
"postcss-loader": "8.1.1",
|
||||||
"postcss-nesting": "13.0.1",
|
"postcss-nesting": "13.0.1",
|
||||||
|
@ -11442,6 +11443,12 @@
|
||||||
"integrity": "sha512-w/9pXDXTDs3IDmOri/w8lM/w6LHR0/F4fcBLLzH+4csSoyshQ5su0TE7k0FLHZO7aOjVLDGecqd1M89+PVpVAA==",
|
"integrity": "sha512-w/9pXDXTDs3IDmOri/w8lM/w6LHR0/F4fcBLLzH+4csSoyshQ5su0TE7k0FLHZO7aOjVLDGecqd1M89+PVpVAA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/perfect-debounce": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
"monaco-editor": "0.52.2",
|
"monaco-editor": "0.52.2",
|
||||||
"monaco-editor-webpack-plugin": "7.1.0",
|
"monaco-editor-webpack-plugin": "7.1.0",
|
||||||
"pdfobject": "2.3.0",
|
"pdfobject": "2.3.0",
|
||||||
|
"perfect-debounce": "1.0.0",
|
||||||
"postcss": "8.5.2",
|
"postcss": "8.5.2",
|
||||||
"postcss-loader": "8.1.1",
|
"postcss-loader": "8.1.1",
|
||||||
"postcss-nesting": "13.0.1",
|
"postcss-nesting": "13.0.1",
|
||||||
|
@ -79,8 +80,8 @@
|
||||||
"eslint-plugin-playwright": "2.2.0",
|
"eslint-plugin-playwright": "2.2.0",
|
||||||
"eslint-plugin-regexp": "2.7.0",
|
"eslint-plugin-regexp": "2.7.0",
|
||||||
"eslint-plugin-sonarjs": "3.0.2",
|
"eslint-plugin-sonarjs": "3.0.2",
|
||||||
"eslint-plugin-unicorn": "59.0.0",
|
|
||||||
"eslint-plugin-toml": "0.12.0",
|
"eslint-plugin-toml": "0.12.0",
|
||||||
|
"eslint-plugin-unicorn": "59.0.0",
|
||||||
"eslint-plugin-vitest-globals": "1.5.0",
|
"eslint-plugin-vitest-globals": "1.5.0",
|
||||||
"eslint-plugin-vue": "10.1.0",
|
"eslint-plugin-vue": "10.1.0",
|
||||||
"eslint-plugin-vue-scoped-css": "2.9.0",
|
"eslint-plugin-vue-scoped-css": "2.9.0",
|
||||||
|
|
36
routers/web/repo/issue_suggestions.go
Normal file
36
routers/web/repo/issue_suggestions.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forgejo.org/models/unit"
|
||||||
|
"forgejo.org/modules/optional"
|
||||||
|
"forgejo.org/services/context"
|
||||||
|
issue_service "forgejo.org/services/issue"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IssueSuggestions returns a list of issue suggestions
|
||||||
|
func IssueSuggestions(ctx *context.Context) {
|
||||||
|
keyword := ctx.Req.FormValue("q")
|
||||||
|
|
||||||
|
canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
|
||||||
|
canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
|
||||||
|
|
||||||
|
var isPull optional.Option[bool]
|
||||||
|
if canReadPulls && !canReadIssues {
|
||||||
|
isPull = optional.Some(true)
|
||||||
|
} else if canReadIssues && !canReadPulls {
|
||||||
|
isPull = optional.Some(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestions, err := issue_service.GetSuggestion(ctx, ctx.Repo.Repository, isPull, keyword)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetSuggestion", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, suggestions)
|
||||||
|
}
|
|
@ -1337,6 +1337,7 @@ func registerRoutes(m *web.Route) {
|
||||||
})
|
})
|
||||||
m.Get("/labels", reqRepoIssuesOrPullsReader, repo.RetrieveLabels, repo.Labels)
|
m.Get("/labels", reqRepoIssuesOrPullsReader, repo.RetrieveLabels, repo.Labels)
|
||||||
m.Get("/milestones", reqRepoIssuesOrPullsReader, repo.Milestones)
|
m.Get("/milestones", reqRepoIssuesOrPullsReader, repo.Milestones)
|
||||||
|
m.Get("/issues/suggestions", reqRepoIssuesOrPullsReader, repo.IssueSuggestions)
|
||||||
}, context.RepoRef())
|
}, context.RepoRef())
|
||||||
|
|
||||||
if setting.Packages.Enabled {
|
if setting.Packages.Enabled {
|
||||||
|
|
73
services/issue/suggestion.go
Normal file
73
services/issue/suggestion.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
issues_model "forgejo.org/models/issues"
|
||||||
|
repo_model "forgejo.org/models/repo"
|
||||||
|
"forgejo.org/modules/optional"
|
||||||
|
"forgejo.org/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetSuggestion(ctx context.Context, repo *repo_model.Repository, isPull optional.Option[bool], keyword string) ([]*structs.Issue, error) {
|
||||||
|
var issues issues_model.IssueList
|
||||||
|
var err error
|
||||||
|
pageSize := 5
|
||||||
|
if keyword == "" {
|
||||||
|
issues, err = issues_model.FindLatestUpdatedIssues(ctx, repo.ID, isPull, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
indexKeyword, _ := strconv.ParseInt(keyword, 10, 64)
|
||||||
|
var issueByIndex *issues_model.Issue
|
||||||
|
var excludedID int64
|
||||||
|
if indexKeyword > 0 {
|
||||||
|
issueByIndex, err = issues_model.GetIssueByIndex(ctx, repo.ID, indexKeyword)
|
||||||
|
if err != nil && !issues_model.IsErrIssueNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if issueByIndex != nil {
|
||||||
|
excludedID = issueByIndex.ID
|
||||||
|
pageSize--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
issues, err = issues_model.FindIssuesSuggestionByKeyword(ctx, repo.ID, keyword, isPull, excludedID, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if issueByIndex != nil {
|
||||||
|
issues = append([]*issues_model.Issue{issueByIndex}, issues...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issues.LoadPullRequests(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestions := make([]*structs.Issue, 0, len(issues))
|
||||||
|
for _, issue := range issues {
|
||||||
|
suggestion := &structs.Issue{
|
||||||
|
ID: issue.ID,
|
||||||
|
Index: issue.Index,
|
||||||
|
Title: issue.Title,
|
||||||
|
State: issue.State(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.IsPull && issue.PullRequest != nil {
|
||||||
|
suggestion.PullRequest = &structs.PullRequestMeta{
|
||||||
|
HasMerged: issue.PullRequest.HasMerged,
|
||||||
|
IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suggestions = append(suggestions, suggestion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions, nil
|
||||||
|
}
|
59
services/issue/suggestion_test.go
Normal file
59
services/issue/suggestion_test.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forgejo.org/models/db"
|
||||||
|
repo_model "forgejo.org/models/repo"
|
||||||
|
"forgejo.org/models/unittest"
|
||||||
|
"forgejo.org/modules/optional"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Suggestion(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
keyword string
|
||||||
|
isPull optional.Option[bool]
|
||||||
|
expectedIndexes []int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
keyword: "",
|
||||||
|
expectedIndexes: []int64{5, 1, 4, 2, 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keyword: "1",
|
||||||
|
expectedIndexes: []int64{1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keyword: "issue",
|
||||||
|
expectedIndexes: []int64{4, 1, 2, 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keyword: "pull",
|
||||||
|
expectedIndexes: []int64{5},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.keyword, func(t *testing.T) {
|
||||||
|
issues, err := GetSuggestion(db.DefaultContext, repo1, testCase.isPull, testCase.keyword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
issueIndexes := make([]int64, 0, len(issues))
|
||||||
|
for _, issue := range issues {
|
||||||
|
issueIndexes = append(issueIndexes, issue.Index)
|
||||||
|
}
|
||||||
|
assert.Equal(t, testCase.expectedIndexes, issueIndexes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,7 +53,7 @@ Template Attributes:
|
||||||
</div>
|
</div>
|
||||||
</markdown-toolbar>
|
</markdown-toolbar>
|
||||||
<div class="ui tab active" data-tab-panel="markdown-writer">
|
<div class="ui tab active" data-tab-panel="markdown-writer">
|
||||||
<text-expander keys=": @" suffix="">
|
<text-expander keys=": @ #" multiword="#" suffix="">
|
||||||
<textarea class="markdown-text-editor js-quick-submit"{{if .TextareaName}} name="{{.TextareaName}}"{{end}}{{if .TextareaPlaceholder}} placeholder="{{.TextareaPlaceholder}}"{{end}}{{if .TextareaAriaLabel}} aria-label="{{.TextareaAriaLabel}}"{{end}}{{if .DisableAutosize}} data-disable-autosize="{{.DisableAutosize}}"{{end}}>{{.TextareaContent}}</textarea>
|
<textarea class="markdown-text-editor js-quick-submit"{{if .TextareaName}} name="{{.TextareaName}}"{{end}}{{if .TextareaPlaceholder}} placeholder="{{.TextareaPlaceholder}}"{{end}}{{if .TextareaAriaLabel}} aria-label="{{.TextareaAriaLabel}}"{{end}}{{if .DisableAutosize}} data-disable-autosize="{{.DisableAutosize}}"{{end}}>{{.TextareaContent}}</textarea>
|
||||||
</text-expander>
|
</text-expander>
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -1,5 +1,41 @@
|
||||||
import {matchEmoji, matchMention} from '../../utils/match.js';
|
import {matchEmoji, matchMention, matchIssue} from '../../utils/match.js';
|
||||||
import {emojiString} from '../emoji.js';
|
import {emojiString} from '../emoji.js';
|
||||||
|
import {getIssueIcon, getIssueColor} from '../issue.js'
|
||||||
|
import {parseIssueHref} from '../../utils.js'
|
||||||
|
import {svg} from '../../svg.js'
|
||||||
|
import {createElementFromHTML} from '../../utils/dom.js';
|
||||||
|
import {debounce} from 'perfect-debounce';
|
||||||
|
|
||||||
|
const debouncedSuggestIssues = debounce((key, text) => new Promise(
|
||||||
|
async (resolve, reject) => {
|
||||||
|
const {owner, repo, index} = parseIssueHref(window.location.href);
|
||||||
|
const matches = await matchIssue(owner, repo, index, text);
|
||||||
|
if (!matches.length) return resolve({matched: false});
|
||||||
|
|
||||||
|
const ul = document.createElement('ul');
|
||||||
|
ul.classList.add('suggestions');
|
||||||
|
for (const issue of matches) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.setAttribute('role', 'option');
|
||||||
|
li.setAttribute('data-value', `${key}${issue.id}`);
|
||||||
|
li.classList.add('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);
|
||||||
|
|
||||||
|
ul.append(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({matched: true, fragment: ul});
|
||||||
|
}), 100)
|
||||||
|
|
||||||
export function initTextExpander(expander) {
|
export function initTextExpander(expander) {
|
||||||
expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => {
|
expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => {
|
||||||
|
@ -49,12 +85,14 @@ export function initTextExpander(expander) {
|
||||||
}
|
}
|
||||||
|
|
||||||
provide({matched: true, fragment: ul});
|
provide({matched: true, fragment: ul});
|
||||||
|
} else if (key === '#') {
|
||||||
|
provide(debouncedSuggestIssues(key, text));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expander?.addEventListener('text-expander-value', ({detail}) => {
|
expander?.addEventListener('text-expander-value', ({detail}) => {
|
||||||
if (detail?.item) {
|
if (detail?.item) {
|
||||||
// add a space after @mentions as it's likely the user wants one
|
// add a space after @mentions and #issue as it's likely the user wants one
|
||||||
const suffix = detail.key === '@' ? ' ' : '';
|
const suffix = ['@', '#'].includes(detail.key) ? ' ' : '';
|
||||||
detail.value = `${detail.item.getAttribute('data-value')}${suffix}`;
|
detail.value = `${detail.item.getAttribute('data-value')}${suffix}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
30
web_src/js/features/issue.js
Normal file
30
web_src/js/features/issue.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
export function getIssueIcon(issue) {
|
||||||
|
if (issue.pull_request) {
|
||||||
|
if (issue.state === 'open') {
|
||||||
|
if (issue.pull_request.draft === true) {
|
||||||
|
return 'octicon-git-pull-request-draft'; // WIP PR
|
||||||
|
}
|
||||||
|
return 'octicon-git-pull-request'; // Open PR
|
||||||
|
} else if (issue.pull_request.merged === true) {
|
||||||
|
return 'octicon-git-merge'; // Merged PR
|
||||||
|
}
|
||||||
|
return 'octicon-git-pull-request'; // Closed PR
|
||||||
|
} else if (issue.state === 'open') {
|
||||||
|
return 'octicon-issue-opened'; // Open Issue
|
||||||
|
}
|
||||||
|
return 'octicon-issue-closed'; // Closed Issue
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIssueColor(issue) {
|
||||||
|
if (issue.pull_request) {
|
||||||
|
if (issue.pull_request.draft === true) {
|
||||||
|
return 'grey'; // WIP PR
|
||||||
|
} else if (issue.pull_request.merged === true) {
|
||||||
|
return 'purple'; // Merged PR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (issue.state === 'open') {
|
||||||
|
return 'green'; // Open Issue
|
||||||
|
}
|
||||||
|
return 'red'; // Closed Issue
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
import emojis from '../../../assets/emoji.json';
|
import emojis from '../../../assets/emoji.json';
|
||||||
|
import {GET} from '../modules/fetch.js';
|
||||||
|
|
||||||
|
|
||||||
const maxMatches = 6;
|
const maxMatches = 6;
|
||||||
|
|
||||||
|
@ -41,3 +43,13 @@ export function matchMention(queryText) {
|
||||||
|
|
||||||
return sortAndReduce(results);
|
return sortAndReduce(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function matchIssue(owner, repo, issueIndexStr, query) {
|
||||||
|
const res = await GET(`${window.config.appSubUrl}/${owner}/${repo}/issues/suggestions?q=${encodeURIComponent(query)}`);
|
||||||
|
|
||||||
|
const issues = await res.json();
|
||||||
|
const issueIndex = parseInt(issueIndexStr);
|
||||||
|
|
||||||
|
// filter out issue with same id
|
||||||
|
return issues.filter((i) => i.id !== issueIndex);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue