feat: complete project workspace tags and notes UI
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"senlinai-agent/backend/internal/models"
|
||||
)
|
||||
|
||||
@@ -23,6 +24,9 @@ type Dashboard struct {
|
||||
type WorkbenchProject struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Identifier string `json:"identifier"`
|
||||
Icon string `json:"icon"`
|
||||
Background string `json:"background"`
|
||||
Description string `json:"description"`
|
||||
Initials string `json:"initials"`
|
||||
UnreadCount int64 `json:"unreadCount"`
|
||||
@@ -99,11 +103,29 @@ type ProjectWorkspace struct {
|
||||
CronPlans []CronPlan `json:"cronPlans"`
|
||||
}
|
||||
|
||||
type CreateProjectInput struct {
|
||||
Name string
|
||||
Identifier string
|
||||
Icon string
|
||||
Background string
|
||||
Description string
|
||||
}
|
||||
|
||||
type CreateTaskInput struct {
|
||||
Title string
|
||||
Description string
|
||||
Status string
|
||||
DueAt *time.Time
|
||||
Tag string
|
||||
}
|
||||
|
||||
type UpdateTaskInput struct {
|
||||
Title string
|
||||
Description string
|
||||
Status string
|
||||
Completed bool
|
||||
NextProjectID uint
|
||||
Tag string
|
||||
}
|
||||
|
||||
type CreateFileSourceInput struct {
|
||||
@@ -123,11 +145,26 @@ func NewService() *Service {
|
||||
}
|
||||
|
||||
func (s *Service) CreateProject(ownerID uint, name string, description string) (*models.SenlinAgentProject, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
return s.CreateProjectWithInput(ownerID, CreateProjectInput{Name: name, Description: description})
|
||||
}
|
||||
|
||||
func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectInput) (*models.SenlinAgentProject, error) {
|
||||
name := strings.TrimSpace(input.Name)
|
||||
if name == "" {
|
||||
return nil, errors.New("project name is required")
|
||||
}
|
||||
project := &models.SenlinAgentProject{OwnerID: ownerID, Name: name, Description: description}
|
||||
identifier := strings.TrimSpace(input.Identifier)
|
||||
if identifier == "" {
|
||||
identifier = projectInitials(name)
|
||||
}
|
||||
project := &models.SenlinAgentProject{
|
||||
OwnerID: ownerID,
|
||||
Name: name,
|
||||
Identifier: identifier,
|
||||
Icon: strings.TrimSpace(input.Icon),
|
||||
Background: strings.TrimSpace(input.Background),
|
||||
Description: strings.TrimSpace(input.Description),
|
||||
}
|
||||
return project, models.DBService.Create(project).Error
|
||||
}
|
||||
|
||||
@@ -149,9 +186,14 @@ func (s *Service) CreateTask(ownerID uint, projectID uint, input CreateTaskInput
|
||||
if status == "" {
|
||||
status = "open"
|
||||
}
|
||||
tagID, err := s.findOrCreateTagID(projectID, input.Tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
task := &models.SenlinAgentTask{
|
||||
ProjectID: projectID,
|
||||
CreatedBy: ownerID,
|
||||
TagID: tagID,
|
||||
Title: title,
|
||||
Description: strings.TrimSpace(input.Description),
|
||||
Status: status,
|
||||
@@ -160,6 +202,58 @@ func (s *Service) CreateTask(ownerID uint, projectID uint, input CreateTaskInput
|
||||
return task, models.DBService.Create(task).Error
|
||||
}
|
||||
|
||||
func (s *Service) UpdateTask(ownerID uint, projectID uint, taskID uint, input UpdateTaskInput) (*models.SenlinAgentTask, error) {
|
||||
if err := ensureProjectOwner(ownerID, projectID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var task models.SenlinAgentTask
|
||||
if err := models.DBService.Where("id = ? AND project_id = ?", taskID, projectID).First(&task).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nextProjectID := input.NextProjectID
|
||||
if nextProjectID == 0 {
|
||||
nextProjectID = task.ProjectID
|
||||
}
|
||||
if err := ensureProjectOwner(ownerID, nextProjectID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
title := strings.TrimSpace(input.Title)
|
||||
if title == "" {
|
||||
return nil, errors.New("task title is required")
|
||||
}
|
||||
tagID, err := s.findOrCreateTagID(nextProjectID, input.Tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projectIdentity, err := projectIdentity(nextProjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagIdentity, err := optionalTagIdentity(tagID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := strings.TrimSpace(input.Status)
|
||||
if status == "" {
|
||||
if input.Completed {
|
||||
status = "done"
|
||||
} else {
|
||||
status = "open"
|
||||
}
|
||||
}
|
||||
task.ProjectID = nextProjectID
|
||||
task.ProjectIdentity = projectIdentity
|
||||
task.Title = title
|
||||
task.Description = strings.TrimSpace(input.Description)
|
||||
task.Status = status
|
||||
task.TagID = tagID
|
||||
task.TagIdentity = tagIdentity
|
||||
if err := models.DBService.Save(&task).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateFileSource(ownerID uint, projectID uint, input CreateFileSourceInput) (*models.SenlinAgentSource, error) {
|
||||
if err := ensureProjectOwner(ownerID, projectID); err != nil {
|
||||
return nil, err
|
||||
@@ -206,21 +300,55 @@ func (s *Service) CreateCronPlan(ownerID uint, projectID uint, input CreateCronP
|
||||
return plan, models.DBService.Create(plan).Error
|
||||
}
|
||||
|
||||
func (s *Service) CreateProjectTag(ownerID uint, projectID uint, name string) (*models.SenlinAgentTag, error) {
|
||||
if err := ensureProjectOwner(ownerID, projectID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.CreateTag(projectID, name)
|
||||
}
|
||||
|
||||
func (s *Service) CreateTag(projectID uint, name string) (*models.SenlinAgentTag, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, errors.New("tag name is required")
|
||||
}
|
||||
var existing models.SenlinAgentTag
|
||||
err := models.DBService.Where("project_id = ? AND name = ?", projectID, name).First(&existing).Error
|
||||
if err == nil {
|
||||
return &existing, nil
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
tag := &models.SenlinAgentTag{ProjectID: projectID, Name: name}
|
||||
return tag, models.DBService.Create(tag).Error
|
||||
}
|
||||
|
||||
func (s *Service) ListProjectTags(ownerID uint, projectID uint) ([]models.SenlinAgentTag, error) {
|
||||
if err := ensureProjectOwner(ownerID, projectID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.ListTags(projectID)
|
||||
}
|
||||
|
||||
func (s *Service) ListTags(projectID uint) ([]models.SenlinAgentTag, error) {
|
||||
var tags []models.SenlinAgentTag
|
||||
err := models.DBService.Where("project_id = ?", projectID).Order("name asc").Find(&tags).Error
|
||||
return tags, err
|
||||
}
|
||||
|
||||
func (s *Service) findOrCreateTagID(projectID uint, name string) (*uint, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
tag, err := s.CreateTag(projectID, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tag.ID, nil
|
||||
}
|
||||
|
||||
func (s *Service) Dashboard(ownerID uint, projectID uint) (Dashboard, error) {
|
||||
if err := ensureProjectOwner(ownerID, projectID); err != nil {
|
||||
return Dashboard{}, err
|
||||
@@ -284,8 +412,11 @@ func (s *Service) Workspace(ownerID uint, projectID uint) (ProjectWorkspace, err
|
||||
Project: WorkbenchProject{
|
||||
ID: project.ID,
|
||||
Name: project.Name,
|
||||
Identifier: project.Identifier,
|
||||
Icon: project.Icon,
|
||||
Background: project.Background,
|
||||
Description: project.Description,
|
||||
Initials: projectInitials(project.Name),
|
||||
Initials: defaultString(project.Identifier, projectInitials(project.Name)),
|
||||
UnreadCount: counts.inbox,
|
||||
},
|
||||
Channels: channels,
|
||||
@@ -409,6 +540,10 @@ func (s *Service) workspaceTasks(projectID uint) ([]WorkTask, error) {
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("CASE WHEN status = 'done' THEN 1 ELSE 0 END asc").Order("sort_order asc, updated_at desc, id desc").Limit(50).Find(&tasks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagNames, err := taskTagNames(tasks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]WorkTask, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
owner, err := userDisplayName(task.AssigneeID, task.CreatedBy)
|
||||
@@ -423,12 +558,58 @@ func (s *Service) workspaceTasks(projectID uint) ([]WorkTask, error) {
|
||||
Owner: owner,
|
||||
Due: displayOptionalTime(task.DueAt),
|
||||
CreatedAt: displayTime(task.CreatedAt),
|
||||
Tag: "",
|
||||
Tag: tagNames[task.ID],
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func taskTagNames(tasks []models.SenlinAgentTask) (map[uint]string, error) {
|
||||
names := make(map[uint]string)
|
||||
tagIDs := make([]uint, 0)
|
||||
for _, task := range tasks {
|
||||
if task.TagID != nil {
|
||||
tagIDs = append(tagIDs, *task.TagID)
|
||||
}
|
||||
}
|
||||
if len(tagIDs) == 0 {
|
||||
return names, nil
|
||||
}
|
||||
var tags []models.SenlinAgentTag
|
||||
if err := models.DBService.Where("id IN ?", tagIDs).Find(&tags).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagByID := make(map[uint]string, len(tags))
|
||||
for _, tag := range tags {
|
||||
tagByID[tag.ID] = tag.Name
|
||||
}
|
||||
for _, task := range tasks {
|
||||
if task.TagID != nil {
|
||||
names[task.ID] = tagByID[*task.TagID]
|
||||
}
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func projectIdentity(projectID uint) (string, error) {
|
||||
var project models.SenlinAgentProject
|
||||
if err := models.DBService.Select("identity").First(&project, projectID).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
return project.Identity, nil
|
||||
}
|
||||
|
||||
func optionalTagIdentity(tagID *uint) (*string, error) {
|
||||
if tagID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var tag models.SenlinAgentTag
|
||||
if err := models.DBService.Select("identity").First(&tag, *tagID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tag.Identity, nil
|
||||
}
|
||||
|
||||
func (s *Service) workspaceAISessions(projectID uint) ([]AISessionItem, error) {
|
||||
var sessions []models.SenlinAgentAISession
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(&sessions).Error; err != nil {
|
||||
|
||||
Reference in New Issue
Block a user