Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 30 additions & 2 deletions api/internal/handler/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,32 @@ func issueID(c *gin.Context) (uuid.UUID, bool) {
return id, true
}

// ListWorkspaceDrafts returns draft work items for the workspace (all projects).
// GET /api/workspaces/:slug/draft-issues/
func (h *IssueHandler) ListWorkspaceDrafts(c *gin.Context) {
user := middleware.GetUser(c)
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
return
}
slug := c.Param("slug")
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
if limit <= 0 || limit > 100 {
limit = 50
}
list, err := h.Issue.ListDraftsForWorkspace(c.Request.Context(), slug, user.ID, limit, offset)
if err != nil {
if err == service.ErrWorkspaceForbidden {
c.JSON(http.StatusNotFound, gin.H{"error": "Not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list draft issues"})
return
}
c.JSON(http.StatusOK, list)
}

// List returns issues for the project.
// GET /api/workspaces/:slug/projects/:projectId/issues/
func (h *IssueHandler) List(c *gin.Context) {
Expand Down Expand Up @@ -114,6 +140,7 @@ func (h *IssueHandler) Create(c *gin.Context) {
TargetDate *string `json:"target_date"`
AssigneeIDs []uuid.UUID `json:"assignee_ids"`
LabelIDs []uuid.UUID `json:"label_ids"`
IsDraft bool `json:"is_draft"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "detail": err.Error()})
Expand All @@ -137,7 +164,7 @@ func (h *IssueHandler) Create(c *gin.Context) {
}
}

issue, err := h.Issue.Create(c.Request.Context(), slug, projectID, user.ID, body.Name, body.Description, body.Priority, body.StateID, body.AssigneeIDs, body.LabelIDs, startDate, targetDate, body.ParentID)
issue, err := h.Issue.Create(c.Request.Context(), slug, projectID, user.ID, body.Name, body.Description, body.Priority, body.StateID, body.AssigneeIDs, body.LabelIDs, startDate, targetDate, body.ParentID, body.IsDraft)
if err != nil {
if err == service.ErrProjectForbidden || err == service.ErrProjectNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Not found"})
Expand Down Expand Up @@ -177,6 +204,7 @@ func (h *IssueHandler) Update(c *gin.Context) {
TargetDate *string `json:"target_date"`
AssigneeIDs []uuid.UUID `json:"assignee_ids"`
LabelIDs []uuid.UUID `json:"label_ids"`
IsDraft *bool `json:"is_draft"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "detail": err.Error()})
Expand Down Expand Up @@ -222,7 +250,7 @@ func (h *IssueHandler) Update(c *gin.Context) {
}
}

issue, err := h.Issue.Update(c.Request.Context(), slug, projectID, issueID, user.ID, name, priority, description, body.StateID, assigneeIDs, labelIDs, startDate, targetDate, body.ParentID)
issue, err := h.Issue.Update(c.Request.Context(), slug, projectID, issueID, user.ID, name, priority, description, body.StateID, assigneeIDs, labelIDs, startDate, targetDate, body.ParentID, body.IsDraft)
if err != nil {
if err == service.ErrIssueNotFound || err == service.ErrProjectForbidden {
c.JSON(http.StatusNotFound, gin.H{"error": "Issue not found"})
Expand Down
2 changes: 2 additions & 0 deletions api/internal/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ func New(cfg Config) *gin.Engine {

api.GET("/users/me/workspaces/:slug/projects/invitations/", projectHandler.ListUserProjectInvitations)
api.POST("/workspaces/:slug/projects/join/", projectHandler.JoinByToken)
api.GET("/workspaces/:slug/draft-issues/", issueHandler.ListWorkspaceDrafts)

api.GET("/workspaces/:slug/projects/", projectHandler.List)
api.POST("/workspaces/:slug/projects/", projectHandler.Create)
api.GET("/workspaces/:slug/projects/:projectId/", projectHandler.Get)
Expand Down
48 changes: 46 additions & 2 deletions api/internal/service/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,46 @@ func (s *IssueService) ensureProjectAccess(ctx context.Context, workspaceSlug st
return nil
}

func (s *IssueService) ensureWorkspaceAccess(ctx context.Context, workspaceSlug string, userID uuid.UUID) (*model.Workspace, error) {
wrk, err := s.ws.GetBySlug(ctx, workspaceSlug)
if err != nil {
return nil, ErrWorkspaceForbidden
}
ok, _ := s.ws.IsMember(ctx, wrk.ID, userID)
if !ok {
return nil, ErrWorkspaceForbidden
}
return wrk, nil
}

// ListDraftsForWorkspace returns draft issues for all projects in the workspace the user can access.
func (s *IssueService) ListDraftsForWorkspace(ctx context.Context, workspaceSlug string, userID uuid.UUID, limit, offset int) ([]model.Issue, error) {
wrk, err := s.ensureWorkspaceAccess(ctx, workspaceSlug, userID)
if err != nil {
return nil, err
}
list, err := s.is.ListDraftsByWorkspaceID(ctx, wrk.ID, limit, offset)
if err != nil {
return nil, err
}
for i := range list {
issueID := list[i].ID
if ids, err := s.is.ListAssigneesForIssue(ctx, issueID); err == nil {
list[i].AssigneeIDs = ids
}
if ids, err := s.is.ListLabelsForIssue(ctx, issueID); err == nil {
list[i].LabelIDs = ids
}
if ids, err := s.is.ListCycleIDsForIssue(ctx, issueID); err == nil {
list[i].CycleIDs = ids
}
if ids, err := s.is.ListModuleIDsForIssue(ctx, issueID); err == nil {
list[i].ModuleIDs = ids
}
}
return list, nil
}

func (s *IssueService) List(ctx context.Context, workspaceSlug string, projectID uuid.UUID, userID uuid.UUID, limit, offset int) ([]model.Issue, error) {
if err := s.ensureProjectAccess(ctx, workspaceSlug, projectID, userID); err != nil {
return nil, err
Expand Down Expand Up @@ -97,7 +137,7 @@ func (s *IssueService) GetByID(ctx context.Context, workspaceSlug string, projec
return issue, nil
}

func (s *IssueService) Create(ctx context.Context, workspaceSlug string, projectID uuid.UUID, userID uuid.UUID, name, description, priority string, stateID *uuid.UUID, assigneeIDs []uuid.UUID, labelIDs []uuid.UUID, startDate, targetDate *time.Time, parentID *uuid.UUID) (*model.Issue, error) {
func (s *IssueService) Create(ctx context.Context, workspaceSlug string, projectID uuid.UUID, userID uuid.UUID, name, description, priority string, stateID *uuid.UUID, assigneeIDs []uuid.UUID, labelIDs []uuid.UUID, startDate, targetDate *time.Time, parentID *uuid.UUID, isDraft bool) (*model.Issue, error) {
if err := s.ensureProjectAccess(ctx, workspaceSlug, projectID, userID); err != nil {
return nil, err
}
Expand All @@ -107,6 +147,7 @@ func (s *IssueService) Create(ctx context.Context, workspaceSlug string, project
ProjectID: projectID,
WorkspaceID: wrk.ID,
CreatedByID: &userID,
IsDraft: isDraft,
}
if description != "" {
issue.DescriptionHTML = description
Expand Down Expand Up @@ -145,7 +186,7 @@ func (s *IssueService) Create(ctx context.Context, workspaceSlug string, project
return issue, nil
}

func (s *IssueService) Update(ctx context.Context, workspaceSlug string, projectID, issueID uuid.UUID, userID uuid.UUID, name, priority, description *string, stateID *uuid.UUID, assigneeIDs, labelIDs *[]uuid.UUID, startDate, targetDate *time.Time, parentID *uuid.UUID) (*model.Issue, error) {
func (s *IssueService) Update(ctx context.Context, workspaceSlug string, projectID, issueID uuid.UUID, userID uuid.UUID, name, priority, description *string, stateID *uuid.UUID, assigneeIDs, labelIDs *[]uuid.UUID, startDate, targetDate *time.Time, parentID *uuid.UUID, isDraft *bool) (*model.Issue, error) {
issue, err := s.GetByID(ctx, workspaceSlug, projectID, issueID, userID)
if err != nil {
return nil, err
Expand All @@ -171,6 +212,9 @@ func (s *IssueService) Update(ctx context.Context, workspaceSlug string, project
if parentID != nil {
issue.ParentID = parentID
}
if isDraft != nil {
issue.IsDraft = *isDraft
}
issue.UpdatedByID = &userID
if err := s.is.Update(ctx, issue); err != nil {
return nil, err
Expand Down
16 changes: 16 additions & 0 deletions api/internal/store/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ func (s *IssueStore) ListByProjectID(ctx context.Context, projectID uuid.UUID, l
return list, err
}

func (s *IssueStore) ListDraftsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID, limit, offset int) ([]model.Issue, error) {
var list []model.Issue
q := s.db.WithContext(ctx).Where(
"workspace_id = ? AND is_draft = ? AND deleted_at IS NULL",
workspaceID, true,
).Order("updated_at DESC")
if limit > 0 {
q = q.Limit(limit)
}
if offset > 0 {
q = q.Offset(offset)
}
err := q.Find(&list).Error
return list, err
}

func (s *IssueStore) Update(ctx context.Context, i *model.Issue) error {
return s.db.WithContext(ctx).Save(i).Error
}
Expand Down
Loading
Loading