feat(office): table detail panes and Alt+1/2/3 layout toggles
Add spreadsheet-style list, split detail pane with forms and document preview, and Alt+1/2/3 to show/hide panes with even width distribution. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -244,13 +244,24 @@ func (l *Loader) listProjects(ctx context.Context) ([]model.Item, error) {
|
||||
if p.Title != nil {
|
||||
title = *p.Title
|
||||
}
|
||||
status := ""
|
||||
if p.Status != nil {
|
||||
status = fmt.Sprint(*p.Status)
|
||||
raw := map[string]any{
|
||||
"id": id,
|
||||
"title": title,
|
||||
}
|
||||
if p.TaskCount != nil {
|
||||
raw["taskCount"] = *p.TaskCount
|
||||
}
|
||||
if p.TaskCountTotal != nil {
|
||||
raw["taskCountTotal"] = *p.TaskCountTotal
|
||||
}
|
||||
if p.DocumentsCount != nil {
|
||||
raw["documentsCount"] = *p.DocumentsCount
|
||||
}
|
||||
if p.ParticipantCount != nil {
|
||||
raw["participantCount"] = *p.ParticipantCount
|
||||
}
|
||||
items[i] = model.Item{
|
||||
ID: id, Title: title, Subtitle: status, Kind: model.KindProject,
|
||||
Raw: map[string]any{"id": id, "title": title, "status": status},
|
||||
ID: id, Title: title, Kind: model.KindProject, Raw: raw,
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
onlyoffice "github.com/eslider/go-onlyoffice"
|
||||
"github.com/eslider/go-onlyoffice/cmd/office/model"
|
||||
"github.com/eslider/go-onlyoffice/cmd/office/preview"
|
||||
)
|
||||
|
||||
// PreviewMarkdown loads item data and returns markdown for the preview pane.
|
||||
func (l *Loader) PreviewMarkdown(ctx context.Context, item model.Item) (string, error) {
|
||||
if l == nil || l.Client == nil {
|
||||
return "", fmt.Errorf("fetch: client is nil")
|
||||
}
|
||||
if item.Kind == model.KindFile {
|
||||
return l.filePreviewMarkdown(ctx, item)
|
||||
}
|
||||
raw, err := l.Detail(ctx, item)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return preview.EntityMarkdown(string(item.Kind), raw), nil
|
||||
}
|
||||
|
||||
func (l *Loader) filePreviewMarkdown(ctx context.Context, item model.Item) (string, error) {
|
||||
if item.ID == "" {
|
||||
return "", fmt.Errorf("file id missing")
|
||||
}
|
||||
name := item.Title
|
||||
if meta, err := l.Client.GetFile(ctx, item.ID); err == nil && meta != nil {
|
||||
if t := onlyoffice.FileEntryTitle(meta); t != "" {
|
||||
name = t
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if _, err := l.Client.DownloadFile(ctx, item.ID, &buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return preview.FileBytesToMarkdown(name, buf.Bytes())
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
onlyoffice "github.com/eslider/go-onlyoffice"
|
||||
"github.com/eslider/go-onlyoffice/cmd/office/model"
|
||||
)
|
||||
|
||||
// SaveItem persists editable form fields for supported entity kinds.
|
||||
func (l *Loader) SaveItem(ctx context.Context, item model.Item, title, description string) error {
|
||||
if l == nil || l.Client == nil {
|
||||
return fmt.Errorf("fetch: client is nil")
|
||||
}
|
||||
switch item.Kind {
|
||||
case model.KindTask:
|
||||
return l.UpdateTask(ctx, item.ID, title, description)
|
||||
case model.KindProject:
|
||||
id, err := strconv.Atoi(item.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = l.Client.UpdateProject(onlyoffice.ProjectUpdateRequest{
|
||||
ID: id,
|
||||
Title: title,
|
||||
Description: description,
|
||||
})
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("save not supported for %s", item.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
// DetailForm loads form field values for the detail pane.
|
||||
func (l *Loader) DetailForm(ctx context.Context, item model.Item) (model.FormFields, error) {
|
||||
raw, err := l.Detail(ctx, item)
|
||||
if err != nil {
|
||||
return model.FormFields{}, err
|
||||
}
|
||||
return model.FormFieldsFromRaw(item.Kind, raw), nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
onlyoffice "github.com/eslider/go-onlyoffice"
|
||||
"github.com/eslider/go-onlyoffice/cmd/office/model"
|
||||
)
|
||||
|
||||
// UpdateTask saves title and description for a project task.
|
||||
func (l *Loader) UpdateTask(ctx context.Context, taskID, title, description string) error {
|
||||
if l == nil || l.Client == nil {
|
||||
return fmt.Errorf("fetch: client is nil")
|
||||
}
|
||||
id, err := strconv.Atoi(taskID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("task id %q: %w", taskID, err)
|
||||
}
|
||||
_, err = l.Client.UpdateProjectTask(onlyoffice.ProjectTaskUpdateRequest{
|
||||
ID: id,
|
||||
Title: title,
|
||||
Description: description,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// TaskFields loads title and description for a project task item.
|
||||
func (l *Loader) TaskFields(ctx context.Context, item model.Item) (title, description string, err error) {
|
||||
fields, err := l.DetailForm(ctx, item)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return fields.Primary, fields.Secondary, nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//go:build integration
|
||||
|
||||
package fetch_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/eslider/go-onlyoffice/cmd/office/model"
|
||||
)
|
||||
|
||||
func TestIntegrationUpdateTaskTitleDescription(t *testing.T) {
|
||||
loader, ctx := liveLoader(t)
|
||||
items, err := loader.List(ctx, model.ListSpec{Subject: model.SubjectTasks})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) == 0 {
|
||||
t.Skip("no tasks")
|
||||
}
|
||||
item := items[0]
|
||||
title, desc, err := loader.TaskFields(ctx, item)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
restoreTitle, restoreDesc := title, desc
|
||||
t.Cleanup(func() {
|
||||
_ = loader.UpdateTask(context.Background(), item.ID, restoreTitle, restoreDesc)
|
||||
})
|
||||
newTitle := title + " (office TUI test)"
|
||||
newDesc := desc + "\n\n_edited by office integration test_"
|
||||
if err := loader.UpdateTask(ctx, item.ID, newTitle, newDesc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotTitle, gotDesc, err := loader.TaskFields(ctx, item)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotTitle != newTitle {
|
||||
t.Fatalf("title: got %q want %q", gotTitle, newTitle)
|
||||
}
|
||||
if gotDesc != newDesc {
|
||||
t.Fatalf("description mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationTaskFieldsFromLiveAPI(t *testing.T) {
|
||||
if os.Getenv("ONLYOFFICE_URL") == "" && os.Getenv("ONLYOFFICE_HOST") == "" {
|
||||
t.Skip("ONLYOFFICE_URL not set")
|
||||
}
|
||||
loader, ctx := liveLoader(t)
|
||||
items, err := loader.List(ctx, model.ListSpec{Subject: model.SubjectTasks})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) == 0 {
|
||||
t.Skip("no tasks")
|
||||
}
|
||||
title, desc, err := loader.TaskFields(ctx, items[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if title == "" {
|
||||
t.Fatal("empty title")
|
||||
}
|
||||
_ = desc
|
||||
}
|
||||
Reference in New Issue
Block a user