feat: publish workspace lifecycle events

This commit is contained in:
mirivlad 2026-06-28 16:34:03 +08:00
parent 1f1001108b
commit d46bc9436d
2 changed files with 113 additions and 0 deletions

View File

@ -38,6 +38,10 @@ const pluginEventRuntimeName = "verstak:plugin-event"
const activityGlobalKey = "events:global" const activityGlobalKey = "events:global"
const activityWorkspacePrefix = "events:workspace:" const activityWorkspacePrefix = "events:workspace:"
const maxActivityEvents = 250 const maxActivityEvents = 250
const workspaceCreatedEventName = "workspace.created"
const workspaceRenamedEventName = "workspace.renamed"
const workspaceTrashedEventName = "workspace.trashed"
const workspaceSelectedEventName = "workspace.selected"
// App is the main application struct exposed to the Wails frontend. // App is the main application struct exposed to the Wails frontend.
type App struct { type App struct {
@ -1051,6 +1055,33 @@ func workspaceRootFromRelativePath(relativePath string) string {
return path return path
} }
func (a *App) publishWorkspaceLifecycleEvent(eventName string, payload map[string]interface{}) {
if a.eventBus == nil {
return
}
if payload == nil {
payload = map[string]interface{}{}
}
workspaceRoot := strings.TrimSpace(fmt.Sprint(payload["workspaceRootPath"]))
if workspaceRoot == "" || workspaceRoot == "<nil>" {
workspaceRoot = strings.TrimSpace(fmt.Sprint(payload["workspaceName"]))
}
if workspaceRoot != "" && workspaceRoot != "<nil>" {
payload["workspaceRootPath"] = workspaceRoot
if _, ok := payload["workspaceName"]; !ok {
payload["workspaceName"] = workspaceRoot
}
if _, ok := payload["title"]; !ok {
payload["title"] = workspaceRoot
}
}
a.eventBus.Publish(events.Event{
Name: eventName,
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
Payload: payload,
})
}
func syncEntityTypeForFileType(fileType corefiles.FileType) string { func syncEntityTypeForFileType(fileType corefiles.FileType) string {
if fileType == corefiles.FileTypeFolder { if fileType == corefiles.FileTypeFolder {
return syncsvc.EntityFolder return syncsvc.EntityFolder
@ -1345,6 +1376,12 @@ func (a *App) CreateWorkspace(name, templateID string) (workspace.Workspace, str
if err != nil { if err != nil {
return workspace.Workspace{}, err.Error() return workspace.Workspace{}, err.Error()
} }
a.publishWorkspaceLifecycleEvent(workspaceCreatedEventName, map[string]interface{}{
"operation": "create",
"workspaceRootPath": ws.RootPath,
"workspaceName": ws.Name,
"templateId": templateID,
})
return ws, "" return ws, ""
} }
@ -1356,6 +1393,13 @@ func (a *App) RenameWorkspace(oldName, newName string) string {
if err := a.workspace.RenameWorkspace(oldName, newName); err != nil { if err := a.workspace.RenameWorkspace(oldName, newName); err != nil {
return err.Error() return err.Error()
} }
a.publishWorkspaceLifecycleEvent(workspaceRenamedEventName, map[string]interface{}{
"operation": "rename",
"workspaceRootPath": newName,
"workspaceName": newName,
"previousWorkspaceRootPath": oldName,
"previousWorkspaceName": oldName,
})
return "" return ""
} }
@ -1368,6 +1412,14 @@ func (a *App) TrashWorkspace(name string) (workspace.TrashResult, string) {
if err != nil { if err != nil {
return workspace.TrashResult{}, err.Error() return workspace.TrashResult{}, err.Error()
} }
a.publishWorkspaceLifecycleEvent(workspaceTrashedEventName, map[string]interface{}{
"operation": "trash",
"workspaceRootPath": name,
"workspaceName": name,
"trashId": result.TrashID,
"trashPath": result.TrashPath,
"deletedAt": result.DeletedAt,
})
return result, "" return result, ""
} }
@ -1418,6 +1470,11 @@ func (a *App) SetCurrentWorkspace(name string) string {
if err := a.workspace.SetCurrentNode(name); err != nil { if err := a.workspace.SetCurrentNode(name); err != nil {
return err.Error() return err.Error()
} }
a.publishWorkspaceLifecycleEvent(workspaceSelectedEventName, map[string]interface{}{
"operation": "select",
"workspaceRootPath": name,
"workspaceName": name,
})
return "" return ""
} }

View File

@ -1079,6 +1079,62 @@ func TestWorkspaceAPIUsesTopLevelFoldersAndMetadataSnapshot(t *testing.T) {
} }
} }
func TestWorkspaceAPIPublishesLifecycleEvents(t *testing.T) {
app, vaultDir := newFilesTestApp(t, []string{"files.read"})
app.workspace = workspace.NewManager(vaultDir)
app.eventBus = events.NewBus()
if err := app.workspace.Load(); err != nil {
t.Fatalf("workspace Load: %v", err)
}
received := map[string]map[string]interface{}{}
for _, eventName := range []string{"workspace.created", "workspace.selected", "workspace.renamed", "workspace.trashed"} {
name := eventName
app.eventBus.Subscribe(name, func(event events.Event) {
payload, ok := event.Payload.(map[string]interface{})
if !ok {
t.Fatalf("%s payload type = %T", name, event.Payload)
}
received[name] = payload
})
}
if _, errStr := app.CreateWorkspace("Project", "client-project"); errStr != "" {
t.Fatalf("CreateWorkspace: %s", errStr)
}
if errStr := app.SetCurrentWorkspace("Project"); errStr != "" {
t.Fatalf("SetCurrentWorkspace: %s", errStr)
}
if errStr := app.RenameWorkspace("Project", "Renamed"); errStr != "" {
t.Fatalf("RenameWorkspace: %s", errStr)
}
if _, errStr := app.TrashWorkspace("Renamed"); errStr != "" {
t.Fatalf("TrashWorkspace: %s", errStr)
}
if got := received["workspace.created"]["workspaceRootPath"]; got != "Project" {
t.Fatalf("workspace.created workspaceRootPath = %#v, want Project", got)
}
if got := received["workspace.created"]["templateId"]; got != "client-project" {
t.Fatalf("workspace.created templateId = %#v, want client-project", got)
}
if got := received["workspace.selected"]["workspaceRootPath"]; got != "Project" {
t.Fatalf("workspace.selected workspaceRootPath = %#v, want Project", got)
}
if got := received["workspace.renamed"]["workspaceRootPath"]; got != "Renamed" {
t.Fatalf("workspace.renamed workspaceRootPath = %#v, want Renamed", got)
}
if got := received["workspace.renamed"]["previousWorkspaceRootPath"]; got != "Project" {
t.Fatalf("workspace.renamed previousWorkspaceRootPath = %#v, want Project", got)
}
if got := received["workspace.trashed"]["workspaceRootPath"]; got != "Renamed" {
t.Fatalf("workspace.trashed workspaceRootPath = %#v, want Renamed", got)
}
if got := received["workspace.trashed"]["trashPath"]; got == "" {
t.Fatalf("workspace.trashed trashPath = %#v, want non-empty", got)
}
}
func TestMoveWorkspaceNodeCompatibilityIsUnsupported(t *testing.T) { func TestMoveWorkspaceNodeCompatibilityIsUnsupported(t *testing.T) {
app, vaultDir := newFilesTestApp(t, []string{"files.read"}) app, vaultDir := newFilesTestApp(t, []string{"files.read"})
app.workspace = workspace.NewManager(vaultDir) app.workspace = workspace.NewManager(vaultDir)