feat(office): scrollable panes, nav tree drill-down, and item actions

Add viewports to all three columns with cursor sync scrolling. Support
Shift+Tab reverse pane focus. Replace flat menu with hierarchical NavTree
(list loads only on leaf nodes; dynamic project subnodes with Tasks/Files).
Enter or a opens per-item action menu (view, delete, download) wired to
the OnlyOffice API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 16:43:06 +01:00
co-authored by Cursor
parent e969624297
commit fe2ee481b9
14 changed files with 929 additions and 464 deletions
@@ -33,7 +33,7 @@ func liveLoader(t *testing.T) (*fetch.Loader, context.Context) {
func TestIntegrationPreviewProjectFromAPI(t *testing.T) {
loader, ctx := liveLoader(t)
items, err := loader.List(ctx, model.SubjectProjects)
items, err := loader.List(ctx, model.ListSpec{Subject: model.SubjectProjects})
if err != nil {
t.Fatal(err)
}
@@ -62,7 +62,7 @@ func TestIntegrationPreviewProjectFromAPI(t *testing.T) {
func TestIntegrationPreviewContactFromAPI(t *testing.T) {
loader, ctx := liveLoader(t)
items, err := loader.List(ctx, model.SubjectContacts)
items, err := loader.List(ctx, model.ListSpec{Subject: model.SubjectContacts})
if err != nil {
t.Fatal(err)
}
@@ -81,7 +81,7 @@ func TestIntegrationPreviewContactFromAPI(t *testing.T) {
func TestIntegrationPreviewOpportunityFromAPI(t *testing.T) {
loader, ctx := liveLoader(t)
items, err := loader.List(ctx, model.SubjectOpportunities)
items, err := loader.List(ctx, model.ListSpec{Subject: model.SubjectOpportunities})
if err != nil {
t.Fatal(err)
}
@@ -100,7 +100,7 @@ func TestIntegrationPreviewOpportunityFromAPI(t *testing.T) {
func TestIntegrationPreviewMailFromAPI(t *testing.T) {
loader, ctx := liveLoader(t)
items, err := loader.List(ctx, model.SubjectMailInbox)
items, err := loader.List(ctx, model.ListSpec{Subject: model.SubjectMailInbox})
if err != nil {
t.Fatal(err)
}
@@ -112,14 +112,14 @@ func TestIntegrationPreviewMailFromAPI(t *testing.T) {
t.Fatal(err)
}
md := preview.MailMarkdown(raw)
if !strings.Contains(md, items[0].Title) {
t.Fatalf("markdown missing subject %q:\n%s", items[0].Title, md)
if md == "" || !strings.Contains(md, "From:") {
t.Fatalf("unexpected mail markdown:\n%s", md)
}
}
func TestIntegrationPreviewEventFromAPI(t *testing.T) {
loader, ctx := liveLoader(t)
items, err := loader.List(ctx, model.SubjectEvents)
items, err := loader.List(ctx, model.ListSpec{Subject: model.SubjectEvents})
if err != nil {
t.Fatal(err)
}
@@ -134,7 +134,7 @@ func TestIntegrationPreviewEventFromAPI(t *testing.T) {
func TestIntegrationPreviewTaskFromAPI(t *testing.T) {
loader, ctx := liveLoader(t)
items, err := loader.List(ctx, model.SubjectTasks)
items, err := loader.List(ctx, model.ListSpec{Subject: model.SubjectTasks})
if err != nil {
t.Fatal(err)
}
@@ -157,7 +157,7 @@ func TestIntegrationPreviewCSVFromDownloadedFile(t *testing.T) {
t.Skip("ONLYOFFICE_PROJECT_ID not set")
}
loader, ctx := liveLoader(t)
items, err := loader.List(ctx, model.SubjectProjectFiles)
items, err := loader.List(ctx, model.ListSpec{Subject: model.SubjectProjectFiles})
if err != nil {
t.Fatal(err)
}