Files
gitea/services/projects/column.go
T
Oleks 3fd0aa751d feat(project): publish board events from service+model choke points
Wrap the model-layer column/project/issue mutation funcs in service-layer
helpers (CreateColumn, EditColumn, DeleteColumn, ReorderColumns,
DeleteProject, AssignOrRemoveProjects) that publish the matching SSE
event after the underlying call succeeds.

Routers (web + REST) are migrated to call these service helpers so the
publish side-effects fire uniformly across repo, user, and org scopes.

DeleteColumn snapshots the column's issues before deletion and emits
one CardMoved per affected issue (alongside the ColumnDeleted event)
so the receiving tab can patch the DOM without a full reload.

Move-issue publishing fires after the txn commits so we never emit
events for moves that get rolled back.
2026-05-15 21:53:35 +03:00

152 lines
4.7 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package project
import (
"context"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/project_events"
)
// CreateColumn inserts a new column into a project and publishes a
// ColumnCreated event. Routers should call this instead of
// project_model.NewColumn so the SSE side-effect fires uniformly across
// repo, user, and org scopes.
func CreateColumn(ctx context.Context, column *project_model.Column) error {
if err := project_model.NewColumn(ctx, column); err != nil {
return err
}
project_events.PublishColumnCreated(ctx, project_events.ColumnCreated{
ProjectID: column.ProjectID,
ColumnID: column.ID,
Title: column.Title,
Color: column.Color,
Sorting: int64(column.Sorting),
IsDefault: column.Default,
})
return nil
}
// EditColumn updates a column and publishes a ColumnUpdated event.
func EditColumn(ctx context.Context, column *project_model.Column) error {
if err := project_model.UpdateColumn(ctx, column); err != nil {
return err
}
project_events.PublishColumnUpdated(ctx, project_events.ColumnUpdated{
ProjectID: column.ProjectID,
ColumnID: column.ID,
Title: column.Title,
Color: column.Color,
Sorting: int64(column.Sorting),
})
return nil
}
// DeleteColumn removes a column from a project and publishes the
// matching ColumnDeleted event. The model layer also moves the
// column's issues to the project's default column; we publish those
// individual moves so receiving tabs can patch the DOM without a full
// reload. We snapshot affected issues *before* the delete so we have
// their ids; the destination column id is resolved after.
func DeleteColumn(ctx context.Context, columnID int64) error {
// Snapshot the column + its issues so we know what to publish
// after the delete commits. Errors here are non-fatal: we still
// run the delete, and just skip per-issue events.
col, snapErr := project_model.GetColumn(ctx, columnID)
var (
projectID int64
movedIssues []int64
)
if snapErr == nil {
projectID = col.ProjectID
issues, err := col.GetIssues(ctx)
if err == nil {
movedIssues = make([]int64, 0, len(issues))
for _, pi := range issues {
movedIssues = append(movedIssues, pi.IssueID)
}
}
}
if err := project_model.DeleteColumnByID(ctx, columnID); err != nil {
return err
}
if snapErr != nil || projectID == 0 {
return nil
}
project_events.PublishColumnDeleted(ctx, project_events.ColumnDeleted{
ProjectID: projectID,
ColumnID: columnID,
})
// Resolve the new (default) column to attach to the per-issue
// CardMoved events. Failures here are tolerated; the frontend
// already knows the column is gone and will simply render its
// next refresh as authoritative.
project, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
return nil
}
defaultCol, err := project.MustDefaultColumn(ctx)
if err != nil {
return nil
}
for _, issueID := range movedIssues {
project_events.PublishCardMoved(ctx, project_events.CardMoved{
ProjectID: projectID,
IssueID: issueID,
FromColumnID: columnID,
ToColumnID: defaultCol.ID,
})
}
return nil
}
// ReorderColumns persists a new sort order for project columns and
// publishes a ColumnReordered batch event.
func ReorderColumns(ctx context.Context, project *project_model.Project, sortedColumnIDs map[int64]int64) error {
if err := project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil {
return err
}
cols := make([]project_events.ColumnSort, 0, len(sortedColumnIDs))
for sorting, columnID := range sortedColumnIDs {
cols = append(cols, project_events.ColumnSort{
ColumnID: columnID,
Sorting: sorting,
})
}
project_events.PublishColumnReordered(ctx, project_events.ColumnReordered{
ProjectID: project.ID,
Columns: cols,
})
return nil
}
// DeleteProject deletes a project and publishes a ProjectDeleted event.
func DeleteProject(ctx context.Context, projectID int64) error {
if err := project_model.DeleteProjectByID(ctx, projectID); err != nil {
return err
}
project_events.PublishProjectDeleted(ctx, project_events.ProjectDeleted{
ProjectID: projectID,
})
return nil
}
// publishProjectUpdated emits a ProjectUpdated event for the current
// state of the given project. It is exported via UpdateProject in
// project.go after the txn commits.
func publishProjectUpdated(ctx context.Context, project *project_model.Project) {
project_events.PublishProjectUpdated(ctx, project_events.ProjectUpdated{
ProjectID: project.ID,
Title: project.Title,
Description: project.Description,
CardType: convert.ProjectCardTypeToString(project.CardType),
IsClosed: project.IsClosed,
})
}