feat(documents): rebuild project document workbench

This commit is contained in:
2026-07-24 12:16:02 +08:00
parent 366136cee9
commit 77beaebb30
73 changed files with 5867 additions and 1264 deletions

View File

@@ -29,13 +29,13 @@ func TestPostgresMovePreventsOldProjectShareFromBeingInsertedConcurrently(t *tes
require.NoError(t, database.Create(&first).Error)
require.NoError(t, database.Create(&second).Error)
task := models.SaTask{ProjectID: first.ID, CreatedBy: owner.ID, Title: "Move", Status: "open"}
note := models.SaNote{ProjectID: first.ID, CreatedBy: owner.ID, Title: "Old context", Markdown: "private"}
document := models.SaDocument{ProjectID: first.ID, CreatedBy: owner.ID, Kind: models.DocumentKindFile, Name: "Old context.md", NormalizedName: "old context.md", Extension: ".md", Revision: 1}
require.NoError(t, database.Create(&task).Error)
require.NoError(t, database.Create(&note).Error)
require.NoError(t, database.Create(&document).Error)
t.Cleanup(func() {
database.Where("task_id = ?", task.ID).Delete(&models.SaTaskShare{})
database.Where("entity_type = ? AND entity_id = ?", "task", task.ID).Delete(&models.SaProjectEvent{})
database.Delete(&note)
database.Delete(&document)
database.Delete(&task)
database.Delete(&first)
database.Delete(&second)
@@ -78,7 +78,7 @@ func TestPostgresMovePreventsOldProjectShareFromBeingInsertedConcurrently(t *tes
}
shareResult := make(chan error, 1)
go func() { shareResult <- service.ShareObject(task.ID, "note", note.ID) }()
go func() { shareResult <- service.ShareObject(task.ID, "document", document.ID) }()
select {
case err := <-shareResult:
t.Fatalf("ShareObject returned before the moving transaction released its task lock: %v", err)

View File

@@ -50,9 +50,9 @@ func TestTaskRegistrarMovesTaskByIdentityAndClearsForeignProjectTag(t *testing.T
require.NoError(t, database.Create(&tag).Error)
task := models.SaTask{ProjectID: first.ID, CreatedBy: 1, TagID: &tag.ID, Title: "迁移任务", Status: "open"}
require.NoError(t, database.Create(&task).Error)
note := models.SaNote{ProjectID: first.ID, CreatedBy: 1, Title: "旧项目资料", Markdown: "仅可在 Alpha 分享"}
require.NoError(t, database.Create(&note).Error)
require.NoError(t, NewService(database).ShareObject(task.ID, "note", note.ID))
document := models.SaDocument{ProjectID: first.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "旧项目资料.md", NormalizedName: "旧项目资料.md", Extension: ".md", Revision: 1}
require.NoError(t, database.Create(&document).Error)
require.NoError(t, NewService(database).ShareObject(task.ID, "document", document.ID))
body := bytes.NewBufferString(fmt.Sprintf(`{"title":"迁移任务","description":"已移动","completed":false,"nextProjectId":%q}`, strings.ToUpper(second.Identity)))
req := httptest.NewRequest(http.MethodPatch, "/api/v1/projects/"+first.Identity+"/tasks/"+task.Identity, body)
req.Header.Set("Content-Type", "application/json")

View File

@@ -63,9 +63,9 @@ func (s *Service) Assign(taskID uint, assigneeIdentity string) error {
})
}
// ShareObject 只允许显式分享 note/source并在同一事务中验证关联对象属于任务所在项目。
// ShareObject only allows an explicitly shared document from the task project.
func (s *Service) ShareObject(taskID uint, objectType string, objectID uint) error {
if objectType != "note" && objectType != "source" {
if objectType != "document" {
return errors.New("unsupported shared object type")
}
return s.database().Transaction(func(tx *gorm.DB) error {
@@ -73,7 +73,7 @@ func (s *Service) ShareObject(taskID uint, objectType string, objectID uint) err
if err != nil {
return err
}
if err := ensureSharedObjectInProject(tx, task.ProjectID, objectType, objectID); err != nil {
if err := ensureSharedObjectInProject(tx, task.ProjectID, objectID); err != nil {
return err
}
if err := tx.Create(&models.SaTaskShare{TaskID: taskID, ObjectType: objectType, ObjectID: objectID}).Error; err != nil {
@@ -193,7 +193,7 @@ func (s *Service) Update(ownerID uint, projectIdentity, taskIdentity string, inp
task.TagID = tagID
task.TagIdentity = tagIdentity
if targetProject.ID != currentProject.ID {
// 现有分享都在原项目边界内;移动后必须清空,避免旧项目 note/source 继续对被指派人可见。
// 现有分享都在原项目边界内;移动后必须清空,避免旧项目 document 继续对被指派人可见。
if err := tx.Where("task_id = ?", task.ID).Delete(&models.SaTaskShare{}).Error; err != nil {
return err
}
@@ -267,24 +267,13 @@ func utcOptionalTime(value *time.Time) *time.Time {
return &result
}
func ensureSharedObjectInProject(tx *gorm.DB, projectID uint, objectType string, objectID uint) error {
switch objectType {
case "note":
var count int64
if err := tx.Model(&models.SaNote{}).Where("id = ? AND project_id = ?", objectID, projectID).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return errors.New("shared object not found in task project")
}
case "source":
var count int64
if err := tx.Model(&models.SaSource{}).Where("id = ? AND project_id = ?", objectID, projectID).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return errors.New("shared object not found in task project")
}
func ensureSharedObjectInProject(tx *gorm.DB, projectID uint, objectID uint) error {
var count int64
if err := tx.Model(&models.SaDocument{}).Where("id = ? AND project_id = ?", objectID, projectID).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return errors.New("shared object not found in task project")
}
return nil
}

View File

@@ -24,11 +24,11 @@ func TestUpdateLocksTaskBeforeProjectValidation(t *testing.T) {
func TestShareObjectLocksTaskBeforeObjectValidation(t *testing.T) {
database, _, project, task := newTaskLockFixture(t)
note := models.SaNote{ProjectID: project.ID, CreatedBy: task.CreatedBy, Title: "Context", Markdown: "Private"}
require.NoError(t, database.Create(&note).Error)
document := models.SaDocument{ProjectID: project.ID, CreatedBy: task.CreatedBy, Kind: models.DocumentKindFile, Name: "Context.md", NormalizedName: "context.md", Extension: ".md", Revision: 1}
require.NoError(t, database.Create(&document).Error)
queries := captureTaskQueryOrder(t, database)
err := NewService(database).ShareObject(task.ID, "note", note.ID)
err := NewService(database).ShareObject(task.ID, "document", document.ID)
require.NoError(t, err)
require.NotEmpty(t, *queries)
@@ -39,21 +39,21 @@ func TestAssigneeOnlySeesExplicitlySharedObjects(t *testing.T) {
database := newTestDB(t)
assigneeID := uint(2)
task := models.SaTask{ProjectID: 1, CreatedBy: 1, AssigneeID: &assigneeID, Title: "处理合同"}
note := models.SaNote{ProjectID: 1, CreatedBy: 1, Title: "合同背景", Markdown: "只在共享后可见"}
document := models.SaDocument{ProjectID: 1, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "合同背景.md", NormalizedName: "合同背景.md", Extension: ".md", Revision: 1}
require.NoError(t, database.Create(&task).Error)
require.NoError(t, database.Create(&note).Error)
require.NoError(t, database.Create(&document).Error)
service := NewService()
before, err := service.VisibleLinkedObjects(task.ID, assigneeID)
require.NoError(t, err)
require.Empty(t, before)
require.NoError(t, service.ShareObject(task.ID, "note", note.ID))
require.NoError(t, service.ShareObject(task.ID, "document", document.ID))
after, err := service.VisibleLinkedObjects(task.ID, assigneeID)
require.NoError(t, err)
require.Len(t, after, 1)
require.Equal(t, "note", after[0].ObjectType)
require.Equal(t, note.Identity, after[0].ObjectID)
require.Equal(t, "document", after[0].ObjectType)
require.Equal(t, document.Identity, after[0].ObjectID)
}
func TestShareObjectRejectsUnsupportedType(t *testing.T) {
@@ -68,12 +68,12 @@ func TestShareObjectRejectsUnsupportedType(t *testing.T) {
func TestShareObjectRejectsObjectFromAnotherProject(t *testing.T) {
database := newTestDB(t)
task := models.SaTask{ProjectID: 1, CreatedBy: 1, Title: "Review"}
note := models.SaNote{ProjectID: 2, CreatedBy: 1, Title: "Other project", Markdown: "Private context"}
document := models.SaDocument{ProjectID: 2, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Other project.md", NormalizedName: "other project.md", Extension: ".md", Revision: 1}
require.NoError(t, database.Create(&task).Error)
require.NoError(t, database.Create(&note).Error)
require.NoError(t, database.Create(&document).Error)
service := NewService()
err := service.ShareObject(task.ID, "note", note.ID)
err := service.ShareObject(task.ID, "document", document.ID)
require.ErrorContains(t, err, "shared object not found in task project")
}