feat(documents): rebuild project document workbench
This commit is contained in:
@@ -50,7 +50,7 @@ type WorkspaceDTO struct {
|
||||
Inbox []WorkspaceInboxDTO `json:"inbox"`
|
||||
Tasks []WorkspaceTaskDTO `json:"tasks"`
|
||||
AISessions []WorkspaceAISessionDTO `json:"aiSessions"`
|
||||
NotesSources []WorkspaceNoteSourceDTO `json:"notesSources"`
|
||||
Documents []WorkspaceDocumentDTO `json:"documents"`
|
||||
CronPlans []WorkspaceCronPlanDTO `json:"cronPlans"`
|
||||
}
|
||||
|
||||
@@ -121,15 +121,15 @@ type WorkspaceAISessionDTO struct {
|
||||
References []string `json:"references"`
|
||||
}
|
||||
|
||||
// WorkspaceNoteSourceDTO 将笔记和资料合并为统一的工作区列表项。
|
||||
type WorkspaceNoteSourceDTO struct {
|
||||
// WorkspaceDocumentDTO is the recent document summary used by the workspace.
|
||||
type WorkspaceDocumentDTO struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"projectId"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Name string `json:"name"`
|
||||
Extension string `json:"extension"`
|
||||
MimeType string `json:"mimeType"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Tag string `json:"tag"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// WorkspaceCronPlanDTO 是只展示元数据、不执行自主 Agent 的计划任务摘要。
|
||||
|
||||
@@ -127,8 +127,8 @@ func TestWorkspaceMatchesFrontendContract(t *testing.T) {
|
||||
require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: 1, Title: "Confirm IA", Description: "Review channel layout.", Status: "open", DueAt: &nextRun}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: 1, Title: "Archive notes", Description: "Move outdated notes.", Status: "done"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI prototype critique", Context: "Discuss channel behavior."}).Error)
|
||||
require.NoError(t, database.Create(&models.SaNote{ProjectID: project.ID, CreatedBy: 1, Title: "Workbench principles", Markdown: "# Notes"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaSource{ProjectID: project.ID, CreatedBy: 1, Kind: "link", Title: "Reference board", URL: "https://example.com"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaDocument{ProjectID: project.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Workbench principles.md", NormalizedName: "workbench principles.md", Extension: ".md", Revision: 1}).Error)
|
||||
require.NoError(t, database.Create(&models.SaDocument{ProjectID: project.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Reference board.pdf", NormalizedName: "reference board.pdf", Extension: ".pdf", Revision: 1}).Error)
|
||||
require.NoError(t, database.Create(&models.SaProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap-a1", SortOrder: 7}).Error)
|
||||
require.NoError(t, database.Create(&models.SaProjectChannel{ProjectID: other.ID, Title: "Other Board", Icon: "link", URL: "https://example.com/other", SortOrder: 7}).Error)
|
||||
require.NoError(t, database.Create(&models.SaCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly inbox review reminder", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, LastResult: "Not run yet"}).Error)
|
||||
@@ -158,9 +158,9 @@ func TestWorkspaceMatchesFrontendContract(t *testing.T) {
|
||||
require.NotEmpty(t, workspace.Tasks[0].CreatedAt)
|
||||
require.Len(t, workspace.AISessions, 1)
|
||||
require.Len(t, workspace.RecentSessions, 1)
|
||||
require.Len(t, workspace.NotesSources, 2)
|
||||
require.Equal(t, "note", workspace.NotesSources[0].Kind)
|
||||
require.Equal(t, "link", workspace.NotesSources[1].Kind)
|
||||
require.Len(t, workspace.Documents, 2)
|
||||
require.Equal(t, models.DocumentKindFile, workspace.Documents[0].Kind)
|
||||
require.Equal(t, models.DocumentKindFile, workspace.Documents[1].Kind)
|
||||
require.Len(t, workspace.CronPlans, 1)
|
||||
require.True(t, workspace.CronPlans[0].Enabled)
|
||||
require.Equal(t, "David", workspace.CronPlans[0].Owner)
|
||||
|
||||
@@ -13,7 +13,7 @@ type workspaceCounts struct {
|
||||
inbox int64
|
||||
tasks int64
|
||||
aiSessions int64
|
||||
notesSources int64
|
||||
documents int64
|
||||
cronPlans int64
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func (s *Service) Workspace(ownerID uint, projectIdentity string) (WorkspaceDTO,
|
||||
if err != nil {
|
||||
return WorkspaceDTO{}, err
|
||||
}
|
||||
notesSources, err := s.workspaceNotesSources(project.ID, project.Identity)
|
||||
documents, err := s.workspaceDocuments(project.ID, project.Identity)
|
||||
if err != nil {
|
||||
return WorkspaceDTO{}, err
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func (s *Service) Workspace(ownerID uint, projectIdentity string) (WorkspaceDTO,
|
||||
Inbox: inboxItems,
|
||||
Tasks: tasks,
|
||||
AISessions: aiSessions,
|
||||
NotesSources: notesSources,
|
||||
Documents: documents,
|
||||
CronPlans: cronPlans,
|
||||
}, nil
|
||||
}
|
||||
@@ -90,15 +90,9 @@ func (s *Service) workspaceCounts(projectID uint) (workspaceCounts, error) {
|
||||
if err := models.DBService.Model(&models.SaAISession{}).Where("project_id = ?", projectID).Count(&counts.aiSessions).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
var noteCount int64
|
||||
if err := models.DBService.Model(&models.SaNote{}).Where("project_id = ?", projectID).Count(¬eCount).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaDocument{}).Where("project_id = ?", projectID).Count(&counts.documents).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
var sourceCount int64
|
||||
if err := models.DBService.Model(&models.SaSource{}).Where("project_id = ?", projectID).Count(&sourceCount).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
counts.notesSources = noteCount + sourceCount
|
||||
if err := models.DBService.Model(&models.SaCronPlan{}).Where("project_id = ?", projectID).Count(&counts.cronPlans).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
@@ -118,13 +112,13 @@ func (s *Service) workspaceTags(projectID uint) ([]WorkspaceTagDTO, error) {
|
||||
}
|
||||
|
||||
func (s *Service) workspaceChannels(projectID uint, projectIdentity string, counts workspaceCounts) ([]WorkspaceChannelDTO, error) {
|
||||
total := counts.inbox + counts.tasks + counts.aiSessions + counts.notesSources + counts.cronPlans
|
||||
total := counts.inbox + counts.tasks + counts.aiSessions + counts.documents + counts.cronPlans
|
||||
channels := []WorkspaceChannelDTO{
|
||||
{ID: projectIdentity + ":overview", ProjectID: projectIdentity, Type: "overview", Title: "Overview", Icon: "home", Count: total, URL: "", SortOrder: 1},
|
||||
{ID: projectIdentity + ":inbox", ProjectID: projectIdentity, Type: "inbox", Title: "Message Flow", Icon: "mail", Count: counts.inbox, URL: "", SortOrder: 2},
|
||||
{ID: projectIdentity + ":tasks", ProjectID: projectIdentity, Type: "tasks", Title: "Work Plan", Icon: "list", Count: counts.tasks, URL: "", SortOrder: 3},
|
||||
{ID: projectIdentity + ":ai", ProjectID: projectIdentity, Type: "ai_sessions", Title: "AI Sessions", Icon: "sparkles", Count: counts.aiSessions, URL: "", SortOrder: 4},
|
||||
{ID: projectIdentity + ":notes", ProjectID: projectIdentity, Type: "notes_sources", Title: "Notes & Sources", Icon: "file", Count: counts.notesSources, URL: "", SortOrder: 5},
|
||||
{ID: projectIdentity + ":documents", ProjectID: projectIdentity, Type: "documents", Title: "Documents", Icon: "file", Count: counts.documents, URL: "", SortOrder: 5},
|
||||
{ID: projectIdentity + ":cron", ProjectID: projectIdentity, Type: "cron", Title: "Cron Plans", Icon: "clock", Count: counts.cronPlans, URL: "", SortOrder: 6},
|
||||
}
|
||||
var customChannels []models.SaProjectChannel
|
||||
@@ -229,27 +223,16 @@ func (s *Service) workspaceAISessions(projectID uint, projectIdentity string) ([
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Service) workspaceNotesSources(projectID uint, projectIdentity string) ([]WorkspaceNoteSourceDTO, error) {
|
||||
var notes []models.SaNote
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(¬es).Error; err != nil {
|
||||
func (s *Service) workspaceDocuments(projectID uint, projectIdentity string) ([]WorkspaceDocumentDTO, error) {
|
||||
var documents []models.SaDocument
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(&documents).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]WorkspaceNoteSourceDTO, 0, len(notes))
|
||||
for _, note := range notes {
|
||||
items = append(items, WorkspaceNoteSourceDTO{
|
||||
ID: note.Identity, ProjectID: projectIdentity, Kind: "note", Title: note.Title,
|
||||
UpdatedAt: note.UpdatedAt.UTC(), Source: "Markdown",
|
||||
})
|
||||
}
|
||||
|
||||
var sources []models.SaSource
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(&sources).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, source := range sources {
|
||||
items = append(items, WorkspaceNoteSourceDTO{
|
||||
ID: source.Identity, ProjectID: projectIdentity, Kind: source.Kind, Title: source.Title,
|
||||
UpdatedAt: source.UpdatedAt.UTC(), Source: sourceDisplayName(source),
|
||||
items := make([]WorkspaceDocumentDTO, 0, len(documents))
|
||||
for _, document := range documents {
|
||||
items = append(items, WorkspaceDocumentDTO{
|
||||
ID: document.Identity, ProjectID: projectIdentity, Kind: document.Kind, Name: document.Name,
|
||||
Extension: document.Extension, MimeType: document.MimeType, UpdatedAt: document.UpdatedAt.UTC(),
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
@@ -283,17 +266,6 @@ func utcOptionalTime(value *time.Time) *time.Time {
|
||||
return &utc
|
||||
}
|
||||
|
||||
func sourceDisplayName(source models.SaSource) string {
|
||||
switch source.Kind {
|
||||
case "file":
|
||||
return "Attachment"
|
||||
case "link":
|
||||
return "URL"
|
||||
default:
|
||||
return source.Kind
|
||||
}
|
||||
}
|
||||
|
||||
func userDisplayName(assigneeID *uint, createdBy uint) (string, error) {
|
||||
userID := createdBy
|
||||
if assigneeID != nil && *assigneeID != 0 {
|
||||
|
||||
@@ -33,11 +33,11 @@ func TestWorkspaceUsesIdentitiesUTCTimesAndProjectScopedTags(t *testing.T) {
|
||||
inbox := models.SaInboxItem{ProjectID: project.ID, CreatedBy: 1, SourceType: "Manual", Title: "Collect notes", Body: "Turn research into tasks.", Status: "open", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
task := models.SaTask{ProjectID: project.ID, CreatedBy: 1, TagID: &projectTag.ID, Title: "Confirm IA", Description: "Review channel layout.", Status: "done", DueAt: &nextRun, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
session := models.SaAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI critique", Context: "Discuss channel behavior.", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
note := models.SaNote{ProjectID: project.ID, CreatedBy: 1, Title: "Workbench principles", Markdown: "# Notes", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
source := models.SaSource{ProjectID: project.ID, CreatedBy: 1, Kind: "link", Title: "Reference board", URL: "https://example.com", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
firstDocument := models.SaDocument{ProjectID: project.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Workbench principles.md", NormalizedName: "workbench principles.md", Extension: ".md", MimeType: "text/markdown", Revision: 1, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
secondDocument := models.SaDocument{ProjectID: project.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Reference board.pdf", NormalizedName: "reference board.pdf", Extension: ".pdf", MimeType: "application/pdf", Revision: 1, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
channel := models.SaProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap", SortOrder: 7, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
plan := models.SaCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly review", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
for _, record := range []any{&inbox, &task, &session, ¬e, &source, &channel, &plan} {
|
||||
for _, record := range []any{&inbox, &task, &session, &firstDocument, &secondDocument, &channel, &plan} {
|
||||
require.NoError(t, database.Create(record).Error)
|
||||
}
|
||||
|
||||
@@ -71,10 +71,10 @@ func TestWorkspaceUsesIdentitiesUTCTimesAndProjectScopedTags(t *testing.T) {
|
||||
require.Equal(t, updatedAt.UTC(), workspace.AISessions[0].UpdatedAt)
|
||||
require.Equal(t, session.Identity, workspace.RecentSessions[0].ID)
|
||||
|
||||
require.Len(t, workspace.NotesSources, 2)
|
||||
require.Equal(t, note.Identity, workspace.NotesSources[0].ID)
|
||||
require.Equal(t, source.Identity, workspace.NotesSources[1].ID)
|
||||
for _, item := range workspace.NotesSources {
|
||||
require.Len(t, workspace.Documents, 2)
|
||||
require.Equal(t, secondDocument.Identity, workspace.Documents[0].ID)
|
||||
require.Equal(t, firstDocument.Identity, workspace.Documents[1].ID)
|
||||
for _, item := range workspace.Documents {
|
||||
require.Equal(t, project.Identity, item.ProjectID)
|
||||
require.Equal(t, updatedAt.UTC(), item.UpdatedAt)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user