diff --git a/internal/api/app.go b/internal/api/app.go index daf2fec..1e58423 100644 --- a/internal/api/app.go +++ b/internal/api/app.go @@ -38,6 +38,10 @@ const pluginEventRuntimeName = "verstak:plugin-event" const activityGlobalKey = "events:global" const activityWorkspacePrefix = "events:workspace:" 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. type App struct { @@ -1051,6 +1055,33 @@ func workspaceRootFromRelativePath(relativePath string) string { 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 == "" { + workspaceRoot = strings.TrimSpace(fmt.Sprint(payload["workspaceName"])) + } + if workspaceRoot != "" && workspaceRoot != "" { + 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 { if fileType == corefiles.FileTypeFolder { return syncsvc.EntityFolder @@ -1345,6 +1376,12 @@ func (a *App) CreateWorkspace(name, templateID string) (workspace.Workspace, str if err != nil { return workspace.Workspace{}, err.Error() } + a.publishWorkspaceLifecycleEvent(workspaceCreatedEventName, map[string]interface{}{ + "operation": "create", + "workspaceRootPath": ws.RootPath, + "workspaceName": ws.Name, + "templateId": templateID, + }) return ws, "" } @@ -1356,6 +1393,13 @@ func (a *App) RenameWorkspace(oldName, newName string) string { if err := a.workspace.RenameWorkspace(oldName, newName); err != nil { return err.Error() } + a.publishWorkspaceLifecycleEvent(workspaceRenamedEventName, map[string]interface{}{ + "operation": "rename", + "workspaceRootPath": newName, + "workspaceName": newName, + "previousWorkspaceRootPath": oldName, + "previousWorkspaceName": oldName, + }) return "" } @@ -1368,6 +1412,14 @@ func (a *App) TrashWorkspace(name string) (workspace.TrashResult, string) { if err != nil { 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, "" } @@ -1418,6 +1470,11 @@ func (a *App) SetCurrentWorkspace(name string) string { if err := a.workspace.SetCurrentNode(name); err != nil { return err.Error() } + a.publishWorkspaceLifecycleEvent(workspaceSelectedEventName, map[string]interface{}{ + "operation": "select", + "workspaceRootPath": name, + "workspaceName": name, + }) return "" } diff --git a/internal/api/app_test.go b/internal/api/app_test.go index 4bed333..dba51e4 100644 --- a/internal/api/app_test.go +++ b/internal/api/app_test.go @@ -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) { app, vaultDir := newFilesTestApp(t, []string{"files.read"}) app.workspace = workspace.NewManager(vaultDir)