diff --git a/modules/structs/issue.go b/modules/structs/issue.go index a67bdcf50e..7df3e50ac9 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -82,6 +82,15 @@ type Issue struct { PinOrder int `json:"pin_order"` } +type IssueSuggestion struct { + Index int64 `json:"number"` + State StateType `json:"state"` + Title string `json:"title"` + IsPr bool `json:"is_pr"` + HasMerged bool `json:"merged"` + IsWorkInProgress bool `json:"draft"` +} + // CreateIssueOption options to create one issue type CreateIssueOption struct { // required:true diff --git a/routers/web/repo/issue_suggestions.go b/routers/web/repo/issue_suggestions.go index 4d1ad33e3b..f1f82441fa 100644 --- a/routers/web/repo/issue_suggestions.go +++ b/routers/web/repo/issue_suggestions.go @@ -24,9 +24,9 @@ func IssueSuggestions(ctx *context.Context) { isPull = optional.Some(false) } - suggestions, err := issue_service.GetSuggestion(ctx, ctx.Repo.Repository, isPull) + suggestions, err := issue_service.GetSuggestions(ctx, ctx.Repo.Repository, isPull) if err != nil { - ctx.ServerError("GetSuggestion", err) + ctx.ServerError("GetSuggestions", err) return } diff --git a/services/issue/suggestion_test.go b/services/issue/suggestion_test.go deleted file mode 100644 index 92468f5c00..0000000000 --- a/services/issue/suggestion_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// 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 { - name string - isPull optional.Option[bool] - expectedIndexes []int64 - }{ - { - name: "All", - expectedIndexes: []int64{5, 1, 4, 2, 3}, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - issues, err := GetSuggestion(db.DefaultContext, repo1, testCase.isPull) - 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) - }) - } -} diff --git a/services/issue/suggestion.go b/services/issue/suggestions.go similarity index 65% rename from services/issue/suggestion.go rename to services/issue/suggestions.go index b0b37b4b3a..1dda204c38 100644 --- a/services/issue/suggestion.go +++ b/services/issue/suggestions.go @@ -12,7 +12,7 @@ import ( "forgejo.org/modules/structs" ) -func GetSuggestion(ctx context.Context, repo *repo_model.Repository, isPull optional.Option[bool]) ([]*structs.Issue, error) { +func GetSuggestions(ctx context.Context, repo *repo_model.Repository, isPull optional.Option[bool]) ([]*structs.IssueSuggestion, error) { var issues issues_model.IssueList var err error pageSize := 1000 @@ -26,20 +26,18 @@ func GetSuggestion(ctx context.Context, repo *repo_model.Repository, isPull opti return nil, err } - suggestions := make([]*structs.Issue, 0, len(issues)) + suggestions := make([]*structs.IssueSuggestion, 0, len(issues)) for _, issue := range issues { - suggestion := &structs.Issue{ - ID: issue.ID, + suggestion := &structs.IssueSuggestion{ 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), - } + suggestion.IsPr = true + suggestion.HasMerged = issue.PullRequest.HasMerged + suggestion.IsWorkInProgress = issue.PullRequest.IsWorkInProgress(ctx) } suggestions = append(suggestions, suggestion) } diff --git a/services/issue/suggestions_test.go b/services/issue/suggestions_test.go new file mode 100644 index 0000000000..a437604acc --- /dev/null +++ b/services/issue/suggestions_test.go @@ -0,0 +1,84 @@ +// 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" + "forgejo.org/modules/structs" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Suggestions(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + testCases := []struct { + name string + isPull optional.Option[bool] + expectedSuggestion []*structs.IssueSuggestion + }{ + { + name: "All", + expectedSuggestion: []*structs.IssueSuggestion{ + { + Index: 5, + State: "open", + Title: "pull5", + IsPr: true, + HasMerged: false, + IsWorkInProgress: false, + }, + { + Index: 1, + State: "open", + Title: "issue1", + IsPr: false, + HasMerged: false, + IsWorkInProgress: false, + }, + { + Index: 4, + State: "closed", + Title: "issue5", + IsPr: false, + HasMerged: false, + IsWorkInProgress: false, + }, + { + Index: 2, + State: "open", + Title: "issue2", + IsPr: true, + HasMerged: true, + IsWorkInProgress: false, + }, + { + Index: 3, + State: "open", + Title: "issue3", + IsPr: true, + HasMerged: false, + IsWorkInProgress: false, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + suggestion, err := GetSuggestions(db.DefaultContext, repo1, testCase.isPull) + require.NoError(t, err) + assert.Equal(t, testCase.expectedSuggestion, suggestion) + }) + } +} diff --git a/tests/e2e/markdown-editor.test.e2e.ts b/tests/e2e/markdown-editor.test.e2e.ts index 2b5f0d80a0..00fb9ff56b 100644 --- a/tests/e2e/markdown-editor.test.e2e.ts +++ b/tests/e2e/markdown-editor.test.e2e.ts @@ -413,8 +413,14 @@ test('text expander has higher prio then prefix continuation', async ({page}) => await textarea.press('Enter'); await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 `); + // Test issue completion await textarea.press('Enter'); - await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 \n* `); + await textarea.pressSequentially('#pull'); + await textarea.press('Enter'); + await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 \n* #5 `); + + await textarea.press('Enter'); + await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 \n* #5 \n* `); }); test('Combo Markdown: preview mode switch', async ({page}) => { @@ -456,3 +462,28 @@ test('Combo Markdown: preview mode switch', async ({page}) => { await expect(previewPanel).toBeHidden(); await save_visual(page); }); + +test('issue suggestions', async ({page}) => { + const response = await page.goto('/user2/repo1/issues/1'); + expect(response?.status()).toBe(200); + + const textarea = page.locator('#comment-form textarea[name=content]'); + + await textarea.focus(); + await textarea.pressSequentially('#'); + + const suggestionList = page.locator('#comment-form .suggestions'); + await expect(suggestionList).toBeVisible(); + + const expectedSuggestions = [ + { number: '5', label: 'pull5' }, + { number: '4', label: 'issue5' }, + { number: '3', label: 'issue3' }, + { number: '2', label: 'issue2' }, + ]; + + for (const suggestion of expectedSuggestions) { + const entry = suggestionList.locator(`li:has-text("${suggestion.number}") >> text="${suggestion.label}"`); + await expect(entry).toBeVisible(); + } +}) diff --git a/web_src/js/features/issue.js b/web_src/js/features/issue.js index 3997a56e0a..d30a1774d3 100644 --- a/web_src/js/features/issue.js +++ b/web_src/js/features/issue.js @@ -2,13 +2,13 @@ import {GET} from '../modules/fetch.js'; import {parseIssueHref, parseRepoOwnerPathInfo} from '../utils.js'; export function getIssueIcon(issue) { - if (issue.pull_request) { + if (issue.is_pr) { if (issue.state === 'open') { - if (issue.pull_request.draft === true) { + if (issue.draft === true) { return 'octicon-git-pull-request-draft'; // WIP PR } return 'octicon-git-pull-request'; // Open PR - } else if (issue.pull_request.merged === true) { + } else if (issue.merged === true) { return 'octicon-git-merge'; // Merged PR } return 'octicon-git-pull-request'; // Closed PR @@ -19,10 +19,10 @@ export function getIssueIcon(issue) { } export function getIssueColor(issue) { - if (issue.pull_request) { - if (issue.pull_request.draft === true) { + if (issue.is_pr) { + if (issue.draft === true) { return 'grey'; // WIP PR - } else if (issue.pull_request.merged === true) { + } else if (issue.merged === true) { return 'purple'; // Merged PR } } @@ -44,9 +44,6 @@ export async function fetchIssueSuggestions() { issuePathInfo.repoName = repoOwnerPathInfo.repoName; // then no issuePathInfo.indexString here, it is only used to exclude the current issue when "matchIssue" } - if (!issuePathInfo.ownerName) { - throw new Error('unexpected'); - } const res = await GET(`${window.config.appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/suggestions`); const issues = await res.json();