Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
10977db
feat(api): add comprehensive REST API for Project Boards
SupenBysz Dec 18, 2025
022a77f
fix(api): address review feedback on project board API
Mar 4, 2026
b8a654a
Merge branch 'main' into fix/project-board-api-review-feedback
silverwind Mar 10, 2026
c508fb6
Merge branch 'fix/project-board-api-review-feedback' of github.com:ha…
lunny Mar 18, 2026
8565da9
Fix lint
lunny Mar 19, 2026
84cb665
Merge branch 'fix/project-board-api-review-feedback' of github.com:ha…
lunny Mar 21, 2026
11b9593
improvements
lunny Mar 21, 2026
506236f
Merge branch 'main' into fix/project-board-api-review-feedback
lunny Mar 22, 2026
67c5029
Merge branch 'main' into fix/project-board-api-review-feedback
lunny Mar 23, 2026
2f27c75
Merge branch 'main' into fix/project-board-api-review-feedback
lunny Mar 25, 2026
bca683f
Apply suggestion from @silverwind
silverwind Mar 26, 2026
17cb8e9
Apply suggestion from @silverwind
silverwind Mar 26, 2026
35a4116
Apply suggestion from @silverwind
silverwind Mar 26, 2026
d39f321
Apply suggestion from @silverwind
silverwind Mar 26, 2026
c0cdc8b
Simplify project board API: use state field, extract helpers, remove …
silverwind Mar 26, 2026
e583e56
Merge branch 'fix/project-board-api-review-feedback' of github.com:ha…
lunny Apr 3, 2026
d425b43
refactor
lunny Apr 3, 2026
f08a738
Merge branch 'main' into hanism01-fix/project-board-api-review-feedback
lunny Apr 3, 2026
7512157
fix
lunny Apr 3, 2026
5c141d3
some improvements
lunny Apr 3, 2026
d2dbc96
update swagger
lunny Apr 3, 2026
631e733
Merge branch 'fix/project-board-api-review-feedback' of github.com:ha…
lunny Apr 3, 2026
f7b3810
Fix test
lunny Apr 3, 2026
0fbf248
Merge branch 'main' into hanism01-fix/project-board-api-review-feedback
lunny Apr 3, 2026
3554572
improvement
lunny Apr 3, 2026
d1cfe87
Merge branch 'main' into hanism01-fix/project-board-api-review-feedback
lunny Apr 3, 2026
3fc8d79
Merge branch 'fix/project-board-api-review-feedback' of github.com:ha…
lunny Apr 4, 2026
199162c
improvement
lunny Apr 4, 2026
c4038f1
Fix test
lunny Apr 5, 2026
4fea94c
merge tests
wxiaoguang Apr 5, 2026
7e680f1
fix AI slop
wxiaoguang Apr 5, 2026
bc56ecc
Merge remote-tracking branch 'origin/main' into fix/project-board-api…
silverwind Apr 27, 2026
7b3de22
Address remaining review feedback
silverwind Apr 27, 2026
413603c
Simplify
silverwind Apr 27, 2026
f0b6fa2
Fix review feedback bugs
silverwind Apr 27, 2026
43ca8be
Simplify v332 migration and add cross-DB test
silverwind Apr 27, 2026
9c5cad4
Tighten v332 migration cleanup pass
silverwind Apr 27, 2026
bb52c62
Drop v332 migration test
silverwind Apr 27, 2026
5c2c6e2
Document the SQLite skip in v332
silverwind Apr 27, 2026
0783004
Tighten project board API for GitHub-style consumers
silverwind Apr 27, 2026
6da8027
Fix inconsistent disabled styling on logged-out repo header buttons (…
silverwind Apr 27, 2026
cc1adec
Merge branch 'fix/project-board-api-review-feedback' of github.com:ha…
lunny Apr 27, 2026
b276435
make code simple
lunny Apr 27, 2026
49b0df0
Drop v332 migration, keep project_board.sorting as int8
silverwind Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions models/project/column.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,23 @@ func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
return columns, nil
}

// CountColumns returns the total number of columns for a project
func (p *Project) CountColumns(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Where("project_id=?", p.ID).Count(&Column{})
}

// GetColumnsPaginated fetches a page of columns for a project
func (p *Project) GetColumnsPaginated(ctx context.Context, opts db.ListOptions) (ColumnList, error) {
Comment thread
silverwind marked this conversation as resolved.
Outdated
columns := make([]*Column, 0, opts.PageSize)
if err := db.SetSessionPagination(db.GetEngine(ctx), &opts).
Where("project_id=?", p.ID).
OrderBy("sorting, id").
Find(&columns); err != nil {
return nil, err
}
return columns, nil
}

// getDefaultColumn return default column and ensure only one exists
func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) {
var column Column
Expand Down
37 changes: 37 additions & 0 deletions models/project/column_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"testing"

"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -123,3 +124,39 @@ func Test_NewColumn(t *testing.T) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "maximum number of columns reached")
}

func TestCountColumns(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

project, err := GetProjectByID(t.Context(), 1)
assert.NoError(t, err)

count, err := project.CountColumns(t.Context())
assert.NoError(t, err)
assert.EqualValues(t, 3, count)
}

func TestGetColumnsPaginated(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

project, err := GetProjectByID(t.Context(), 1)
assert.NoError(t, err)

// Page 1, limit 2 — returns first 2 columns
page1, err := project.GetColumnsPaginated(t.Context(), db.ListOptions{Page: 1, PageSize: 2})
assert.NoError(t, err)
assert.Len(t, page1, 2)

// Page 2, limit 2 — returns remaining column
page2, err := project.GetColumnsPaginated(t.Context(), db.ListOptions{Page: 2, PageSize: 2})
assert.NoError(t, err)
assert.Len(t, page2, 1)

// Page 1 and page 2 together cover all columns with no overlap
allIDs := make(map[int64]bool)
for _, c := range append(page1, page2...) {
assert.False(t, allIDs[c.ID], "duplicate column ID %d across pages", c.ID)
allIDs[c.ID] = true
}
assert.Len(t, allIDs, 3)
}
139 changes: 139 additions & 0 deletions modules/structs/project.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
Comment thread
silverwind marked this conversation as resolved.
Outdated
// SPDX-License-Identifier: MIT

package structs

import (
"time"
)

// Project represents a project
// swagger:model
type Project struct {
// Unique identifier of the project
ID int64 `json:"id"`
// Project title
Title string `json:"title"`
// Project description
Description string `json:"description"`
// Owner ID (for organization or user projects)
OwnerID int64 `json:"owner_id,omitempty"`
// Repository ID (for repository projects)
RepoID int64 `json:"repo_id,omitempty"`
// Creator ID
CreatorID int64 `json:"creator_id"`
// Whether the project is closed
IsClosed bool `json:"is_closed"`
// Template type: 0=none, 1=basic_kanban, 2=bug_triage
TemplateType int `json:"template_type"`
// Card type: 0=text_only, 1=images_and_text
CardType int `json:"card_type"`
// Project type: 1=individual, 2=repository, 3=organization
Type int `json:"type"`
// Number of open issues
NumOpenIssues int64 `json:"num_open_issues,omitempty"`
// Number of closed issues
NumClosedIssues int64 `json:"num_closed_issues,omitempty"`
// Total number of issues
NumIssues int64 `json:"num_issues,omitempty"`
// Created time
// swagger:strfmt date-time
Created time.Time `json:"created"`
// Updated time
// swagger:strfmt date-time
Updated time.Time `json:"updated"`
// Closed time
// swagger:strfmt date-time
ClosedDate *time.Time `json:"closed_date,omitempty"`
// Project URL
URL string `json:"url,omitempty"`
}

// CreateProjectOption represents options for creating a project
// swagger:model
type CreateProjectOption struct {
// required: true
Title string `json:"title" binding:"Required"`
// Project description
Description string `json:"description"`
// Template type: 0=none, 1=basic_kanban, 2=bug_triage
TemplateType int `json:"template_type"`
// Card type: 0=text_only, 1=images_and_text
CardType int `json:"card_type"`
}

// EditProjectOption represents options for editing a project
// swagger:model
type EditProjectOption struct {
// Project title
Title *string `json:"title,omitempty"`
// Project description
Description *string `json:"description,omitempty"`
// Card type: 0=text_only, 1=images_and_text
CardType *int `json:"card_type,omitempty"`
// Whether the project is closed
IsClosed *bool `json:"is_closed,omitempty"`
}

// ProjectColumn represents a project column (board)
// swagger:model
type ProjectColumn struct {
// Unique identifier of the column
ID int64 `json:"id"`
// Column title
Title string `json:"title"`
// Whether this is the default column
Default bool `json:"default"`
// Sorting order
Sorting int `json:"sorting"`
// Column color (hex format)
Color string `json:"color,omitempty"`
// Project ID
ProjectID int64 `json:"project_id"`
// Creator ID
CreatorID int64 `json:"creator_id"`
// Number of issues in this column
NumIssues int64 `json:"num_issues,omitempty"`
// Created time
// swagger:strfmt date-time
Created time.Time `json:"created"`
// Updated time
// swagger:strfmt date-time
Updated time.Time `json:"updated"`
}

// CreateProjectColumnOption represents options for creating a project column
// swagger:model
type CreateProjectColumnOption struct {
// required: true
Title string `json:"title" binding:"Required"`
// Column color (hex format, e.g., #FF0000)
Color string `json:"color,omitempty"`
}

// EditProjectColumnOption represents options for editing a project column
// swagger:model
type EditProjectColumnOption struct {
// Column title
Title *string `json:"title,omitempty"`
// Column color (hex format)
Color *string `json:"color,omitempty"`
// Sorting order
Sorting *int `json:"sorting,omitempty"`
}

// MoveProjectColumnOption represents options for moving a project column
// swagger:model
type MoveProjectColumnOption struct {
// Position to move the column to (0-based index)
// required: true
Position int `json:"position" binding:"Required"`
}

// AddIssueToProjectColumnOption represents options for adding an issue to a project column
// swagger:model
type AddIssueToProjectColumnOption struct {
// Issue ID to add to the column
// required: true
IssueID int64 `json:"issue_id" binding:"Required"`
}
17 changes: 17 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,23 @@ func Routes() *web.Router {
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
})
m.Group("/projects", func() {
m.Combo("").Get(repo.ListProjects).
Post(reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived, bind(api.CreateProjectOption{}), repo.CreateProject)
m.Group("/{id}", func() {
m.Combo("").Get(repo.GetProject).
Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
m.Combo("/columns").Get(repo.ListProjectColumns).
Post(reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived, bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
})
m.Group("/columns/{id}", func() {
m.Combo("").
Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectColumnOption{}), repo.EditProjectColumn).
Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProjectColumn)
m.Post("/issues", reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived, bind(api.AddIssueToProjectColumnOption{}), repo.AddIssueToProjectColumn)
Comment thread
silverwind marked this conversation as resolved.
Outdated
})
}, reqRepoReader(unit.TypeProjects))
Comment thread
wxiaoguang marked this conversation as resolved.
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))

Expand Down
Loading
Loading