Allow multiple projects per issue and pull requests (#36784)
Add ability to add and remove multiple projects per issue and pull request. Resolve #12974 --------- Signed-off-by: Icy Avocado <avocado@ovacoda.com> Co-authored-by: Tyrone Yeh <siryeh@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: OpenCode (gpt-5.2-codex) <opencode@openai.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
{{$queryLink := QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" $.State "labels" $.SelectLabels "milestone" $.MilestoneID "project" $.ProjectID "assignee" $.AssigneeID "poster" $.PosterUsername "archived_labels" (Iif $.ShowArchivedLabels "true")}}
|
||||
{{$projectIDs := $.ProjectIDs}}
|
||||
{{$projectIDsQuery := SliceUtils.JoinInt64 $projectIDs}}
|
||||
{{$queryLink := QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" $.State "labels" $.SelectLabels "milestone" $.MilestoneID "project" $projectIDsQuery "assignee" $.AssigneeID "poster" $.PosterUsername "archived_labels" (Iif $.ShowArchivedLabels "true")}}
|
||||
{{$showAllProjects := not $projectIDs}}
|
||||
{{$showNoProjectSelected := and (eq (len $projectIDs) 1) (eq (index $projectIDs 0) -1)}}
|
||||
|
||||
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
|
||||
|
||||
@@ -12,26 +16,28 @@
|
||||
{{end}}
|
||||
|
||||
<!-- Project -->
|
||||
<div class="item ui dropdown jump {{if not (or .OpenProjects .ClosedProjects)}}disabled{{end}}">
|
||||
<div class="item ui dropdown jump project-filter {{if not (or .OpenProjects .ClosedProjects)}}disabled{{end}}">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_project"}}
|
||||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<div class="menu flex-items-menu">
|
||||
<div class="ui icon search input">
|
||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_project"}}">
|
||||
</div>
|
||||
<a class="{{if not .ProjectID}}active selected {{end}}item" href="{{QueryBuild $queryLink "project" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_project_all"}}</a>
|
||||
<a class="{{if eq .ProjectID -1}}active selected {{end}}item" href="{{QueryBuild $queryLink "project" -1}}">{{ctx.Locale.Tr "repo.issues.filter_project_none"}}</a>
|
||||
<a class="item {{if $showAllProjects}}selected{{end}}" href="{{QueryBuild $queryLink "project" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_project_all"}}</a>
|
||||
<a class="item {{if $showNoProjectSelected}}selected{{end}}" href="{{QueryBuild $queryLink "project" -1}}">{{ctx.Locale.Tr "repo.issues.filter_project_none"}}</a>
|
||||
{{if .OpenProjects}}
|
||||
<div class="divider"></div>
|
||||
<div class="header">
|
||||
{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
|
||||
</div>
|
||||
{{range .OpenProjects}}
|
||||
<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item tw-flex" href="{{QueryBuild $queryLink "project" .ID}}">
|
||||
{{svg .IconName 18 "tw-mr-2 tw-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span>
|
||||
{{range $project := .OpenProjects}}
|
||||
{{$toggle := SliceUtils.JoinToggleIDs $projectIDs $project.ID}}
|
||||
{{/* FIXME: ISSUE-MULTIPLE-PROJECTS-FILTER: no multiple project filter support yet. If the support comes, here it should use "&project=${toggle.ToggledIDs}" */}}
|
||||
<a class="item {{if $toggle.IsIncluded}}selected{{end}}" href="{{QueryBuild $queryLink "project" $project.ID}}">
|
||||
{{svg $project.IconName}}<span class="gt-ellipsis">{{$project.Title}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -40,9 +46,11 @@
|
||||
<div class="header">
|
||||
{{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
|
||||
</div>
|
||||
{{range .ClosedProjects}}
|
||||
<a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item" href="{{QueryBuild $queryLink "project" .ID}}">
|
||||
{{svg .IconName 18 "tw-mr-2"}}{{.Title}}
|
||||
{{range $project := .ClosedProjects}}
|
||||
{{$toggle := SliceUtils.JoinToggleIDs $projectIDs $project.ID}}
|
||||
{{/* FIXME: ISSUE-MULTIPLE-PROJECTS-FILTER: no multiple project filter support yet. If the support comes, here it should use "&project=${toggle.ToggledIDs}" */}}
|
||||
<a class="item {{if $toggle.IsIncluded}}selected{{end}}" href="{{QueryBuild $queryLink "project" $project.ID}}">
|
||||
{{svg $project.IconName}}<span class="gt-ellipsis">{{$project.Title}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{{/* this tmpl is quite dirty, it should not mix unrelated things together .... need to split it in the future*/}}
|
||||
{{$allStatesLink := ""}}{{$openLink := ""}}{{$closedLink := ""}}
|
||||
{{$projectIDsQuery := SliceUtils.JoinInt64 $.ProjectIDs}}
|
||||
{{if .PageIsMilestones}}
|
||||
{{$allStatesLink = QueryBuild "?" "q" $.Keyword "sort" $.SortType "state" "all"}}
|
||||
{{else}}
|
||||
{{$allStatesLink = QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" "all" "labels" $.SelectLabels "milestone" $.MilestoneID "project" $.ProjectID "assignee" $.AssigneeID "poster" $.PosterUsername "archived_labels" (Iif $.ShowArchivedLabels "true")}}
|
||||
{{$allStatesLink = QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" "all" "labels" $.SelectLabels "milestone" $.MilestoneID "project" $projectIDsQuery "assignee" $.AssigneeID "poster" $.PosterUsername "archived_labels" (Iif $.ShowArchivedLabels "true")}}
|
||||
{{end}}
|
||||
{{$openLink = QueryBuild $allStatesLink "state" "open"}}
|
||||
{{$closedLink = QueryBuild $allStatesLink "state" "closed"}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<input type="hidden" name="type" value="{{$.ViewType}}">
|
||||
<input type="hidden" name="labels" value="{{$.SelectLabels}}">
|
||||
<input type="hidden" name="milestone" value="{{$.MilestoneID}}">
|
||||
<input type="hidden" name="project" value="{{$.ProjectID}}">
|
||||
<input type="hidden" name="project" value="{{SliceUtils.JoinInt64 $.ProjectIDs}}">
|
||||
<input type="hidden" name="assignee" value="{{$.AssigneeID}}">
|
||||
<input type="hidden" name="poster" value="{{$.PosterUsername}}">
|
||||
<input type="hidden" name="sort" value="{{$.SortType}}">
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<div class="divider"></div>
|
||||
|
||||
{{/* project selector */}}
|
||||
<div class="issue-sidebar-combo sidebar-project-combo" data-selection-mode="single" data-update-algo="all"
|
||||
<div class="issue-sidebar-combo sidebar-project-combo" data-selection-mode="multiple" data-update-algo="all"
|
||||
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/projects?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
|
||||
>
|
||||
<input class="combo-value" name="project_id" type="hidden" value="{{if and $pageMeta.CanModifyIssueOrPull $data.SelectedProjectIDs}}{{index $data.SelectedProjectIDs 0}}{{end}}">
|
||||
<input class="combo-value" name="project_ids" type="hidden" value="{{SliceUtils.JoinInt64 $data.SelectedProjectIDs}}">
|
||||
<div class="ui dropdown full-width {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
|
||||
<a class="fixed-text muted">
|
||||
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
|
||||
@@ -26,7 +26,7 @@
|
||||
{{range $data.OpenProjects}}
|
||||
<a class="item muted" data-value="{{.ID}}" href="{{.Link ctx}}">
|
||||
<span class="item-check-mark">{{svg "octicon-check"}}</span>
|
||||
{{svg .IconName 18}}<span class="tw-flex-1 tw-break-anywhere">{{.Title}}</span>
|
||||
{{svg .IconName 18}}<span class="tw-flex-1 gt-ellipsis">{{.Title}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -36,44 +36,37 @@
|
||||
{{range $data.ClosedProjects}}
|
||||
<a class="item muted" data-value="{{.ID}}" href="{{.Link ctx}}">
|
||||
<span class="item-check-mark">{{svg "octicon-check"}}</span>
|
||||
{{svg .IconName 18}}<span class="tw-flex-1 tw-break-anywhere">{{.Title}}</span>
|
||||
{{svg .IconName 18}}<span class="tw-flex-1 gt-ellipsis">{{.Title}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* project cards (column selectors) */}}
|
||||
{{if not $data.ProjectCards}}
|
||||
<div class="ui list">
|
||||
<div class="item empty-list">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="flex-relaxed-list">
|
||||
{{/* project cards (column selectors) */}}
|
||||
<div class="ui list tw-my-2 flex-relaxed-list issue-sidebar-project-cards" data-combo-list-inited="true">
|
||||
<div class="item empty-list {{if $data.ProjectCards}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</div>
|
||||
{{range $card := $data.ProjectCards}}
|
||||
{{$selectedColumn := $card.SelectedColumn}}
|
||||
<div class="item sidebar-project-card">
|
||||
<div class="tw-mb-1">
|
||||
<a class="suppressed flex-text-block" href="{{$card.Project.Link ctx}}">
|
||||
{{svg $card.Project.IconName 16}}
|
||||
<span class="gt-ellipsis">{{$card.Project.Title}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{if $pageMeta.CanModifyIssueOrPull}}
|
||||
{{/* only show a "project column card" if the selected column exists, otherwise only show the project title */}}
|
||||
<div class="item {{if $selectedColumn}}sidebar-project-card{{end}}">
|
||||
<a class="suppressed flex-text-block" href="{{$card.Project.Link ctx}}">
|
||||
{{svg $card.Project.IconName 16}} <span class="gt-ellipsis">{{$card.Project.Title}}</span>
|
||||
</a>
|
||||
{{if and $selectedColumn $pageMeta.CanModifyIssueOrPull}}
|
||||
<div class="issue-sidebar-combo sidebar-project-column-combo" data-selection-mode="single" data-update-algo="all"
|
||||
data-update-url="{{$pageMeta.RepoLink}}/issues/projects/column?issue_id={{$pageMeta.Issue.ID}}"
|
||||
>
|
||||
<input class="combo-value" name="column_id" type="hidden" value="{{if $selectedColumn}}{{$selectedColumn.ID}}{{end}}">
|
||||
<div class="ui dropdown full-width">
|
||||
<div class="flex-text-block tw-ml-[16px]">{{/* align with the "project" icon */}}
|
||||
<div class="interact-bg tw-px-2 tw-py-1 tw-rounded flex-text-block">
|
||||
<div class="interact-bg tw-px-2 tw-py-1 tw-rounded flex-text-block fixed-text">
|
||||
{{if $selectedColumn}}
|
||||
{{if $card.SelectedColumn.Color}}<span class="color-icon icon-size-8" style="background-color: {{$card.SelectedColumn.Color}}"></span>{{end}}
|
||||
<div class="gt-ellipsis" data-testid="sidebar-project-column-text">{{$card.SelectedColumn.Title}}</div>
|
||||
<div class="gt-ellipsis">{{$card.SelectedColumn.Title}}</div>
|
||||
{{else}}
|
||||
<div class="gt-ellipsis" data-testid="sidebar-project-column-text">{{ctx.Locale.Tr "repo.issues.new.no_column"}}</div>
|
||||
<div class="gt-ellipsis">{{ctx.Locale.Tr "repo.issues.new.no_column"}}</div>
|
||||
{{end}}
|
||||
{{svg "octicon-triangle-down" 14}}
|
||||
</div>
|
||||
@@ -98,4 +91,4 @@
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -76,10 +76,10 @@
|
||||
<span class="gt-ellipsis">{{.Milestone.Name}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Project}}
|
||||
<a class="project flex-text-inline tw-max-w-[300px]" href="{{.Project.Link ctx}}">
|
||||
{{svg .Project.IconName 14}}
|
||||
<span class="gt-ellipsis">{{.Project.Title}}</span>
|
||||
{{range $project := .Projects}}
|
||||
<a class="project flex-text-inline tw-max-w-[300px]" href="{{$project.Link ctx}}">
|
||||
{{svg $project.IconName 14}}
|
||||
<span class="gt-ellipsis">{{$project.Title}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Ref}}{{/* TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl" */}}
|
||||
|
||||
86
templates/swagger/v1_json.tmpl
generated
86
templates/swagger/v1_json.tmpl
generated
@@ -23841,6 +23841,15 @@
|
||||
"format": "int64",
|
||||
"x-go-name": "Milestone"
|
||||
},
|
||||
"projects": {
|
||||
"description": "list of project ids",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"x-go-name": "Projects"
|
||||
},
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"x-go-name": "Ref"
|
||||
@@ -25098,6 +25107,15 @@
|
||||
"format": "int64",
|
||||
"x-go-name": "Milestone"
|
||||
},
|
||||
"projects": {
|
||||
"description": "list of project ids to set (replaces existing projects)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"x-go-name": "Projects"
|
||||
},
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"x-go-name": "Ref"
|
||||
@@ -26622,6 +26640,13 @@
|
||||
"format": "int64",
|
||||
"x-go-name": "PinOrder"
|
||||
},
|
||||
"projects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Project"
|
||||
},
|
||||
"x-go-name": "Projects"
|
||||
},
|
||||
"pull_request": {
|
||||
"$ref": "#/definitions/PullRequestMeta"
|
||||
},
|
||||
@@ -27974,6 +27999,67 @@
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"Project": {
|
||||
"description": "Project represents a project",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"closed_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Closed"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Created"
|
||||
},
|
||||
"creator_id": {
|
||||
"description": "CreatorID is the user who created the project",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "CreatorID"
|
||||
},
|
||||
"description": {
|
||||
"description": "Description provides details about the project",
|
||||
"type": "string",
|
||||
"x-go-name": "Description"
|
||||
},
|
||||
"id": {
|
||||
"description": "ID is the unique identifier for the project",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "ID"
|
||||
},
|
||||
"is_closed": {
|
||||
"description": "IsClosed indicates if the project is closed",
|
||||
"type": "boolean",
|
||||
"x-go-name": "IsClosed"
|
||||
},
|
||||
"owner_id": {
|
||||
"description": "OwnerID is the owner of the project (for org-level projects)",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "OwnerID"
|
||||
},
|
||||
"repo_id": {
|
||||
"description": "RepoID is the repository this project belongs to (for repo-level projects)",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "RepoID"
|
||||
},
|
||||
"title": {
|
||||
"description": "Title is the title of the project",
|
||||
"type": "string",
|
||||
"x-go-name": "Title"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Updated"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"PublicKey": {
|
||||
"description": "PublicKey publickey is a user key to push code to repository",
|
||||
"type": "object",
|
||||
|
||||
@@ -4102,6 +4102,15 @@
|
||||
"type": "integer",
|
||||
"x-go-name": "Milestone"
|
||||
},
|
||||
"projects": {
|
||||
"description": "list of project ids",
|
||||
"items": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "array",
|
||||
"x-go-name": "Projects"
|
||||
},
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"x-go-name": "Ref"
|
||||
@@ -5330,6 +5339,15 @@
|
||||
"type": "integer",
|
||||
"x-go-name": "Milestone"
|
||||
},
|
||||
"projects": {
|
||||
"description": "list of project ids to set (replaces existing projects)",
|
||||
"items": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "array",
|
||||
"x-go-name": "Projects"
|
||||
},
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"x-go-name": "Ref"
|
||||
@@ -6849,6 +6867,13 @@
|
||||
"type": "integer",
|
||||
"x-go-name": "PinOrder"
|
||||
},
|
||||
"projects": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Project"
|
||||
},
|
||||
"type": "array",
|
||||
"x-go-name": "Projects"
|
||||
},
|
||||
"pull_request": {
|
||||
"$ref": "#/components/schemas/PullRequestMeta"
|
||||
},
|
||||
@@ -8215,6 +8240,67 @@
|
||||
"type": "object",
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"Project": {
|
||||
"description": "Project represents a project",
|
||||
"properties": {
|
||||
"closed_at": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"x-go-name": "Closed"
|
||||
},
|
||||
"created_at": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"x-go-name": "Created"
|
||||
},
|
||||
"creator_id": {
|
||||
"description": "CreatorID is the user who created the project",
|
||||
"format": "int64",
|
||||
"type": "integer",
|
||||
"x-go-name": "CreatorID"
|
||||
},
|
||||
"description": {
|
||||
"description": "Description provides details about the project",
|
||||
"type": "string",
|
||||
"x-go-name": "Description"
|
||||
},
|
||||
"id": {
|
||||
"description": "ID is the unique identifier for the project",
|
||||
"format": "int64",
|
||||
"type": "integer",
|
||||
"x-go-name": "ID"
|
||||
},
|
||||
"is_closed": {
|
||||
"description": "IsClosed indicates if the project is closed",
|
||||
"type": "boolean",
|
||||
"x-go-name": "IsClosed"
|
||||
},
|
||||
"owner_id": {
|
||||
"description": "OwnerID is the owner of the project (for org-level projects)",
|
||||
"format": "int64",
|
||||
"type": "integer",
|
||||
"x-go-name": "OwnerID"
|
||||
},
|
||||
"repo_id": {
|
||||
"description": "RepoID is the repository this project belongs to (for repo-level projects)",
|
||||
"format": "int64",
|
||||
"type": "integer",
|
||||
"x-go-name": "RepoID"
|
||||
},
|
||||
"title": {
|
||||
"description": "Title is the title of the project",
|
||||
"type": "string",
|
||||
"x-go-name": "Title"
|
||||
},
|
||||
"updated_at": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"x-go-name": "Updated"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"PublicKey": {
|
||||
"description": "PublicKey publickey is a user key to push code to repository",
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user