From 2f5b5a9e9c32e6042f1f06f1b112a795267d6955 Mon Sep 17 00:00:00 2001 From: Myers Carpenter Date: Sun, 19 Apr 2026 08:53:02 -0400 Subject: [PATCH] Add project column picker to issue and pull request sidebar (#37037) Why? You are working on a ticket, it's ready to be moved to the QA column in your project. Currently you have to go to the project, find the issue card, then move it. With this change you can move the issue's column on the issue page. When an issue or pull request belongs to a project board, a dropdown appears in the sidebar to move it between columns without opening the board view. Read-only users see the current column name instead. * Fix #13520 * Replace #30617 This was written using Claude Code and Opus. Closed: image Open: image --------- Signed-off-by: silverwind Co-authored-by: silverwind Co-authored-by: Claude (Opus 4.7) Co-authored-by: wxiaoguang Co-authored-by: Nicolas Co-authored-by: Cursor --- AGENTS.md | 1 + models/issues/issue.go | 11 ++ models/issues/issue_project.go | 11 +- models/project/column.go | 2 +- models/project/column_test.go | 2 +- models/project/issue.go | 47 ++++---- options/locale/locale_en-US.json | 5 +- routers/web/repo/issue_page_meta.go | 30 +++-- routers/web/repo/projects.go | 48 ++++++++ routers/web/web.go | 1 + templates/repo/issue/sidebar/label_list.tmpl | 2 +- .../repo/issue/sidebar/project_list.tmpl | 74 ++++++++++-- tests/e2e/issue-project.test.ts | 67 +++++++++++ tests/e2e/utils.ts | 7 ++ tests/integration/project_test.go | 105 ++++++++++++++++++ web_src/css/base.css | 6 + web_src/css/modules/svg.css | 10 +- web_src/css/repo.css | 11 ++ 18 files changed, 380 insertions(+), 60 deletions(-) create mode 100644 tests/e2e/issue-project.test.ts diff --git a/AGENTS.md b/AGENTS.md index 589dea7865f..6c7e50fea4f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,5 +7,6 @@ - Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates - Preserve existing code comments, do not remove or rewrite comments that are still relevant - In TypeScript, use `!` (non-null assertion) instead of `?.`/`??` when a value is known to always exist +- For CSS layout, prefer `flex-*` helpers over per-child `tw-ml-*` / `tw-mr-*` margins; fall back to `tw-*` utilities when specificity requires `!important` - Include authorship attribution in issue and pull request comments - Add `Co-Authored-By` lines to all commits, indicating name and model used diff --git a/models/issues/issue.go b/models/issues/issue.go index 655cdebdfc6..838d41a3005 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -592,6 +592,17 @@ func GetIssueByID(ctx context.Context, id int64) (*Issue, error) { return issue, nil } +func GetIssueByRepoID(ctx context.Context, repoID, issueID int64) (*Issue, error) { + issue := new(Issue) + has, err := db.GetEngine(ctx).ID(issueID).Where("repo_id=?", repoID).Get(issue) + if err != nil { + return nil, err + } else if !has { + return nil, ErrIssueNotExist{issueID, repoID, 0} + } + return issue, nil +} + // GetIssuesByIDs return issues with the given IDs. // If keepOrder is true, the order of the returned issues will be the same as the given IDs. func GetIssuesByIDs(ctx context.Context, issueIDs []int64, keepOrder ...bool) (IssueList, error) { diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 3bb09363019..f78daf77f88 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -115,17 +115,10 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo panic("newColumnID must not be zero") // shouldn't happen } - res := struct { - MaxSorting int64 - IssueCount int64 - }{} - if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue"). - Where("project_id=?", newProjectID). - And("project_board_id=?", newColumnID). - Get(&res); err != nil { + newSorting, err := project_model.GetColumnIssueNextSorting(ctx, newProjectID, newColumnID) + if err != nil { return err } - newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) return db.Insert(ctx, &project_model.ProjectIssue{ IssueID: issue.ID, ProjectID: newProjectID, diff --git a/models/project/column.go b/models/project/column.go index 7365204f18e..997e82ddf90 100644 --- a/models/project/column.go +++ b/models/project/column.go @@ -185,7 +185,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error { return err } - if err = column.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil { + if err = moveIssuesToAnotherColumn(ctx, column, defaultColumn); err != nil { return err } diff --git a/models/project/column_test.go b/models/project/column_test.go index 948e012c62d..6437a764ed3 100644 --- a/models/project/column_test.go +++ b/models/project/column_test.go @@ -59,7 +59,7 @@ func Test_moveIssuesToAnotherColumn(t *testing.T) { assert.Len(t, issues, 1) assert.EqualValues(t, 3, issues[0].ID) - err = column1.moveIssuesToAnotherColumn(t.Context(), column2) + err = moveIssuesToAnotherColumn(t.Context(), column1, column2) assert.NoError(t, err) issues, err = column1.GetIssues(t.Context()) diff --git a/models/project/issue.go b/models/project/issue.go index 47d1537ec73..c89f5243054 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -33,38 +33,45 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error return err } -func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error { - if c.ProjectID != newColumn.ProjectID { - return errors.New("columns have to be in the same project") - } - - if c.ID == newColumn.ID { - return nil - } - +// GetColumnIssueNextSorting returns the sorting value to append an issue at the end of the column. +func GetColumnIssueNextSorting(ctx context.Context, projectID, columnID int64) (int64, error) { res := struct { MaxSorting int64 IssueCount int64 }{} - if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count"). + if _, err := db.GetEngine(ctx).Select("max(sorting) AS max_sorting, count(*) AS issue_count"). Table("project_issue"). - Where("project_id=?", newColumn.ProjectID). - And("project_board_id=?", newColumn.ID). + Where("project_id=?", projectID). + And("project_board_id=?", columnID). Get(&res); err != nil { - return err + return 0, err + } + return util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0), nil +} + +func moveIssuesToAnotherColumn(ctx context.Context, oldColumn, newColumn *Column) error { + if oldColumn.ProjectID != newColumn.ProjectID { + return errors.New("columns have to be in the same project") } - issues, err := c.GetIssues(ctx) - if err != nil { - return err - } - if len(issues) == 0 { + if oldColumn.ID == newColumn.ID { return nil } - nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) + movedIssues, err := oldColumn.GetIssues(ctx) + if err != nil { + return err + } + if len(movedIssues) == 0 { + return nil + } + + nextSorting, err := GetColumnIssueNextSorting(ctx, newColumn.ProjectID, newColumn.ID) + if err != nil { + return err + } return db.WithTx(ctx, func(ctx context.Context) error { - for i, issue := range issues { + for i, issue := range movedIssues { issue.ProjectColumnID = newColumn.ID issue.Sorting = nextSorting + int64(i) if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil { diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 8efafd5c4b7..819195efe2d 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -1407,11 +1407,12 @@ "repo.issues.new": "New Issue", "repo.issues.new.title_empty": "Title cannot be empty", "repo.issues.new.labels": "Labels", - "repo.issues.new.no_label": "No Label", + "repo.issues.new.no_labels": "No labels", "repo.issues.new.clear_labels": "Clear labels", "repo.issues.new.projects": "Projects", "repo.issues.new.clear_projects": "Clear projects", - "repo.issues.new.no_projects": "No project", + "repo.issues.new.no_projects": "No projects", + "repo.issues.new.no_column": "No column", "repo.issues.new.open_projects": "Open Projects", "repo.issues.new.closed_projects": "Closed Projects", "repo.issues.new.no_items": "No items", diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index 639333ab425..be609d8cdfb 100644 --- a/routers/web/repo/issue_page_meta.go +++ b/routers/web/repo/issue_page_meta.go @@ -33,12 +33,15 @@ type issueSidebarAssigneesData struct { CandidateAssignees []*user_model.User } +type issueSidebarProjectCardData struct { + Project *project_model.Project + Columns []*project_model.Column + SelectedColumn *project_model.Column +} + type issueSidebarProjectsData struct { SelectedProjectIDs []int64 // TODO: support multiple projects in the future - - // the "selected" fields are only valid when len(SelectedProjectIDs)==1 - SelectedProjectColumns []*project_model.Column - SelectedProjectColumn *project_model.Column + ProjectCards []*issueSidebarProjectCardData OpenProjects []*project_model.Project ClosedProjects []*project_model.Project @@ -172,30 +175,37 @@ func (d *IssuePageMetaData) retrieveProjectData(ctx *context.Context) { if d.Issue == nil || d.Issue.Project == nil { return } - d.ProjectsData.SelectedProjectIDs = []int64{d.Issue.Project.ID} columns, err := d.Issue.Project.GetColumns(ctx) if err != nil { ctx.ServerError("GetProjectColumns", err) return } - d.ProjectsData.SelectedProjectColumns = columns columnID, err := d.Issue.ProjectColumnID(ctx) if err != nil { ctx.ServerError("ProjectColumnID", err) return } + var selectedColumn *project_model.Column for _, col := range columns { if col.ID == columnID { - d.ProjectsData.SelectedProjectColumn = col + selectedColumn = col break } } + d.ProjectsData.ProjectCards = []*issueSidebarProjectCardData{ + { + Project: d.Issue.Project, + Columns: columns, + SelectedColumn: selectedColumn, + }, + } + d.ProjectsData.SelectedProjectIDs = make([]int64, 0, len(d.ProjectsData.ProjectCards)) + for _, card := range d.ProjectsData.ProjectCards { + d.ProjectsData.SelectedProjectIDs = append(d.ProjectsData.SelectedProjectIDs, card.Project.ID) + } } func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) { - if d.Issue != nil && d.Issue.Project != nil { - d.ProjectsData.SelectedProjectIDs = []int64{d.Issue.Project.ID} - } d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository) } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index c9bdc5be76e..be12674223e 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -464,6 +464,54 @@ func UpdateIssueProject(ctx *context.Context) { ctx.JSONOK() } +// UpdateIssueProjectColumn moves an issue to a different column within its project +func UpdateIssueProjectColumn(ctx *context.Context) { + issue, err := issues_model.GetIssueByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("issue_id")) + if err != nil { + ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err) + return + } + column, err := project_model.GetColumn(ctx, ctx.FormInt64("id")) + if err != nil { + ctx.NotFoundOrServerError("GetColumn", project_model.IsErrProjectColumnNotExist, err) + return + } + + if err := issue.LoadProject(ctx); err != nil { + ctx.ServerError("LoadProject", err) + return + } + + issueProjects := []*project_model.Project{issue.Project} // TODO: this is for the multiple project support in the future + + // it must make sure the requested column is in this issue's projects + var columnProject *project_model.Project + for _, project := range issueProjects { + if column.ProjectID == project.ID { + columnProject = project + break + } + } + if columnProject == nil { + ctx.NotFound(nil) + return + } + + // append to the end of the target column so we don't collide with existing sorting values + newSorting, err := project_model.GetColumnIssueNextSorting(ctx, columnProject.ID, column.ID) + if err != nil { + ctx.ServerError("GetColumnIssueNextSorting", err) + return + } + + if err := project_service.MoveIssuesOnProjectColumn(ctx, ctx.Doer, column, map[int64]int64{newSorting: issue.ID}); err != nil { + ctx.ServerError("MoveIssuesOnProjectColumn", err) + return + } + + ctx.JSONOK() +} + // DeleteProjectColumn allows for the deletion of a project column func DeleteProjectColumn(ctx *context.Context) { if ctx.Doer == nil { diff --git a/routers/web/web.go b/routers/web/web.go index 1dff6cbc04e..e0ff54fcff5 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1355,6 +1355,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel) m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone) m.Post("/projects", reqRepoIssuesOrPullsWriter, reqRepoProjectsReader, repo.UpdateIssueProject) + m.Post("/projects/column", reqRepoIssuesOrPullsWriter, reqRepoProjectsWriter, repo.UpdateIssueProjectColumn) m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee) m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus) m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues) diff --git a/templates/repo/issue/sidebar/label_list.tmpl b/templates/repo/issue/sidebar/label_list.tmpl index d30196326b3..3eb7ba8d7a5 100644 --- a/templates/repo/issue/sidebar/label_list.tmpl +++ b/templates/repo/issue/sidebar/label_list.tmpl @@ -45,7 +45,7 @@ - + +{{/* project cards (column selectors) */}} +{{if not $data.ProjectCards}} +
+
{{ctx.Locale.Tr "repo.issues.new.no_projects"}}
+
+{{else}} +
+ {{range $card := $data.ProjectCards}} + {{$selectedColumn := $card.SelectedColumn}} + + {{end}} +
+{{end}} diff --git a/tests/e2e/issue-project.test.ts b/tests/e2e/issue-project.test.ts new file mode 100644 index 00000000000..451006c3f02 --- /dev/null +++ b/tests/e2e/issue-project.test.ts @@ -0,0 +1,67 @@ +import {env} from 'node:process'; +import {test, expect} from '@playwright/test'; +import {login, apiCreateRepo, apiCreateIssue, apiDeleteRepo, createProjectColumn, randomString, timeoutFactor} from './utils.ts'; + +test('assign issue to project and change column', async ({page}) => { + const repoName = `e2e-issue-project-${randomString(8)}`; + const user = env.GITEA_TEST_E2E_USER; + await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]); + + await page.goto(`/${user}/${repoName}/projects/new`); + await page.locator('input[name="title"]').fill('Kanban Board'); + await page.getByRole('button', {name: 'Create Project'}).click(); + + const projectLink = page.locator('.milestone-list a', {hasText: 'Kanban Board'}).first(); + await expect(projectLink).toBeVisible(); + const href = await projectLink.getAttribute('href'); + const projectID = href!.split('/').pop(); + + // columns created via POST because the web UI uses modals that are hard to drive + await Promise.all(['Backlog', 'In Progress', 'Done'].map((title) => + createProjectColumn(page.request, user, repoName, projectID!, title), + )); + + await apiCreateIssue(page.request, user, repoName, {title: 'Column picker test'}); + + // Same ceiling as tests/e2e/events.test.ts; Playwright defaults are 5000*factor (see playwright.config.ts). + const slowTimeout = 15_000 * timeoutFactor; + + await page.goto(`/${user}/${repoName}/issues/1`); + await page.locator('.sidebar-project-combo .ui.dropdown').click(); + await Promise.all([ + page.waitForResponse( + (resp) => resp.url().includes('/issues/projects') && resp.status() === 200, + {timeout: slowTimeout}, + ), + page.locator('.sidebar-project-combo .menu .item', {hasText: 'Kanban Board'}).click(), + ]); + + const columnCombo = page.locator('.sidebar-project-column-combo'); + await expect(columnCombo).toBeVisible({timeout: slowTimeout}); + await columnCombo.locator('.ui.dropdown').click(); + await columnCombo.locator('.menu').waitFor({state: 'visible', timeout: slowTimeout}); + + const inProgressItem = columnCombo.locator('a.item', {hasText: 'In Progress'}); + await expect(inProgressItem).toBeVisible({timeout: slowTimeout}); + await inProgressItem.scrollIntoViewIfNeeded(); + + await Promise.all([ + page.waitForResponse( + (resp) => + resp.request().method() === 'POST' && + resp.url().includes('/issues/projects/column') && + resp.ok(), + {timeout: slowTimeout}, + ), + inProgressItem.click(), + ]); + + await expect(columnCombo.getByTestId('sidebar-project-column-text')).toContainText('In Progress', { + timeout: slowTimeout, + }); + await expect(page.locator('.timeline-item', {hasText: 'moved this to In Progress'})).toBeVisible({ + timeout: slowTimeout, + }); + + await apiDeleteRepo(page.request, user, repoName); +}); diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts index 08de0241268..0b00ab129a6 100644 --- a/tests/e2e/utils.ts +++ b/tests/e2e/utils.ts @@ -74,6 +74,13 @@ export async function apiCreateBranch(requestContext: APIRequestContext, owner: }), 'apiCreateBranch'); } +export async function createProjectColumn(requestContext: APIRequestContext, owner: string, repo: string, projectID: string, title: string) { + await apiRetry(() => requestContext.post(`${baseUrl()}/${owner}/${repo}/projects/${projectID}/columns/new`, { + headers: apiHeaders(), + form: {title}, + }), 'createProjectColumn'); +} + export async function apiDeleteRepo(requestContext: APIRequestContext, owner: string, name: string) { await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/repos/${owner}/${name}`, { headers: apiHeaders(), diff --git a/tests/integration/project_test.go b/tests/integration/project_test.go index 1e38322dbf4..ddf0743040d 100644 --- a/tests/integration/project_test.go +++ b/tests/integration/project_test.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "testing" issues_model "code.gitea.io/gitea/models/issues" @@ -89,6 +90,110 @@ func TestMoveRepoProjectColumns(t *testing.T) { assert.NoError(t, project_model.DeleteProjectByID(t.Context(), project1.ID)) } +func TestUpdateIssueProjectColumn(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // fixture: issue 3 is in project 1 of repo user2/repo1, column "In Progress" (id=2) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) + assert.EqualValues(t, 1, issue.RepoID) + + sess := loginUser(t, "user2") + + t.Run("MoveColumn", func(t *testing.T) { + req := NewRequestWithValues(t, "POST", "/user2/repo1/issues/projects/column", map[string]string{ + "issue_id": "3", + "id": "3", + }) + sess.MakeRequest(t, req, http.StatusOK) + + pi := unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{IssueID: 3}) + assert.EqualValues(t, 3, pi.ProjectColumnID) + }) + + t.Run("InvalidIssueID", func(t *testing.T) { + req := NewRequestWithValues(t, "POST", "/user2/repo1/issues/projects/column", map[string]string{ + "issue_id": "0", + "id": "3", + }) + sess.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("WrongRepo", func(t *testing.T) { + req := NewRequestWithValues(t, "POST", "/user2/repo1/issues/projects/column", map[string]string{ + "issue_id": "6", + "id": "3", + }) + sess.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("WrongProject", func(t *testing.T) { + project2 := project_model.Project{ + Title: "second project on repo1", + RepoID: 1, + Type: project_model.TypeRepository, + TemplateType: project_model.TemplateTypeNone, + } + require.NoError(t, project_model.NewProject(t.Context(), &project2)) + require.NoError(t, project_model.NewColumn(t.Context(), &project_model.Column{ + Title: "other column", + ProjectID: project2.ID, + })) + columns, err := project2.GetColumns(t.Context()) + require.NoError(t, err) + require.NotEmpty(t, columns) + + req := NewRequestWithValues(t, "POST", "/user2/repo1/issues/projects/column", map[string]string{ + "issue_id": "1", + "id": strconv.FormatInt(columns[0].ID, 10), + }) + sess.MakeRequest(t, req, http.StatusNotFound) + }) +} + +func TestIssueSidebarProjectColumn(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // fixture: issue 5 (index=4) is in project 1 of repo user2/repo1, column "Done" (id=3) + sess := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user2/repo1/issues/4") + resp := sess.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + cards := htmlDoc.Find(".sidebar-project-card") + assert.Equal(t, 1, cards.Length()) + + title := cards.Find(".sidebar-project-card a.suppressed .gt-ellipsis") + assert.Contains(t, strings.TrimSpace(title.Text()), "First project") + + columnCombo := cards.Find(".sidebar-project-column-combo") + assert.Equal(t, 1, columnCombo.Length()) + + defaultItem := columnCombo.Find(`.menu .item[data-value="1"]`) + assert.Equal(t, 1, defaultItem.Length()) + + inProgressItem := columnCombo.Find(`.menu .item[data-value="2"]`) + assert.Equal(t, 1, inProgressItem.Length()) + doneItem := columnCombo.Find(`.menu .item[data-value="3"]`) + assert.Equal(t, 1, doneItem.Length()) + + comboVal, exists := columnCombo.Find("input.combo-value").Attr("value") + assert.True(t, exists) + assert.Equal(t, "3", comboVal) + + req = NewRequestWithValues(t, "POST", "/user2/repo1/issues/projects?issue_ids=5", map[string]string{ + "id": "0", + }) + sess.MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", "/user2/repo1/issues/4") + resp = sess.MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + + cards = htmlDoc.Find(".sidebar-project-card") + assert.Equal(t, 0, cards.Length()) +} + // getProjectIssueIDs returns the set of issue IDs rendered as cards on the project board page. func getProjectIssueIDs(t *testing.T, htmlDoc *HTMLDoc) map[int64]struct{} { t.Helper() diff --git a/web_src/css/base.css b/web_src/css/base.css index 08033e4a6f0..d9207a18bf4 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -782,11 +782,17 @@ tr.top-line-blame:first-of-type { .color-icon { display: inline-block; + flex-shrink: 0; border-radius: var(--border-radius-full); height: 14px; width: 14px; } +.icon-size-8 { + width: 8px; + height: 8px; +} + .rss-icon { display: inline-flex; color: var(--color-text-light-1); diff --git a/web_src/css/modules/svg.css b/web_src/css/modules/svg.css index e32fa0911f1..7adfc77a818 100644 --- a/web_src/css/modules/svg.css +++ b/web_src/css/modules/svg.css @@ -6,10 +6,6 @@ fill: currentcolor; } -.middle .svg { - vertical-align: middle; -} - /* some browsers like Chrome have a bug: when a SVG is in a "display: none" container and referenced somewhere else by ``, it won't be rendered correctly. e.g.: ".kts -> kotlin" */ .svg-icon-container { @@ -50,3 +46,9 @@ .svg[width="36"] { min-width: 36px; } .svg[width="48"] { min-width: 48px; } .svg[width="56"] { min-width: 56px; } + +/* when the svg is used in menu or item, it's certain that we don't want it to be shrunk */ +.menu .svg, +.item .svg { + flex-shrink: 0; +} diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 51745b4adac..428d2e0714e 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -62,12 +62,23 @@ visibility: hidden; } +.sidebar-project-card { + border: 1px solid var(--color-secondary); + border-radius: var(--border-radius); + margin-top: var(--gap-block); + padding: 8px; +} + .issue-content-right .ui.list.labels-list { display: flex; gap: var(--gap-inline); flex-wrap: wrap; } +.issue-content-right .empty-list { + font-size: 12px; +} + @media (max-width: 767.98px) { .issue-content-left, .issue-content-right {