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>
383 lines
11 KiB
Go
383 lines
11 KiB
Go
package fetch
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
|
|
onlyoffice "github.com/eslider/go-onlyoffice"
|
|
"github.com/eslider/go-onlyoffice/cmd/office/model"
|
|
)
|
|
|
|
// Loader fetches list items for a menu subject using the OnlyOffice client.
|
|
type Loader struct {
|
|
Client *onlyoffice.Client
|
|
}
|
|
|
|
// List returns items for the given list spec (nav leaf).
|
|
func (l *Loader) List(ctx context.Context, spec model.ListSpec) ([]model.Item, error) {
|
|
if l == nil || l.Client == nil {
|
|
return nil, fmt.Errorf("fetch: client is nil")
|
|
}
|
|
switch spec.Subject {
|
|
case model.SubjectProjects:
|
|
return l.listProjects(ctx)
|
|
case model.SubjectTasks:
|
|
if spec.ProjectID != "" {
|
|
return l.listTasksForProject(ctx, spec.ProjectID)
|
|
}
|
|
return l.listTasks(ctx)
|
|
case model.SubjectCalendars:
|
|
return l.listCalendars(ctx)
|
|
case model.SubjectEvents:
|
|
return l.listEvents(ctx)
|
|
case model.SubjectContacts:
|
|
return l.listContacts(ctx, nil)
|
|
case model.SubjectPersons:
|
|
falseVal := false
|
|
return l.listContacts(ctx, &falseVal)
|
|
case model.SubjectCompanies:
|
|
trueVal := true
|
|
return l.listContacts(ctx, &trueVal)
|
|
case model.SubjectOpportunities:
|
|
return l.listOpportunities(ctx)
|
|
case model.SubjectCases:
|
|
return l.listCases(ctx)
|
|
case model.SubjectCRMTasks:
|
|
return l.listCRMTasks(ctx)
|
|
case model.SubjectMailInbox:
|
|
return l.listMail(ctx, onlyoffice.MailFolderInbox)
|
|
case model.SubjectMailSent:
|
|
return l.listMail(ctx, onlyoffice.MailFolderSent)
|
|
case model.SubjectMailDrafts:
|
|
return l.listMail(ctx, onlyoffice.MailFolderDrafts)
|
|
case model.SubjectMailTrash:
|
|
return l.listMail(ctx, onlyoffice.MailFolderTrash)
|
|
case model.SubjectMailSpam:
|
|
return l.listMail(ctx, onlyoffice.MailFolderSpam)
|
|
case model.SubjectUsers:
|
|
return l.listUsers(ctx)
|
|
case model.SubjectProjectFiles:
|
|
pid := spec.ProjectID
|
|
if pid == "" {
|
|
pid = onlyoffice.GetEnvironmentDefaults().ProjectID
|
|
}
|
|
if pid == "" {
|
|
return nil, fmt.Errorf("set ONLYOFFICE_PROJECT_ID or pick a project in the tree")
|
|
}
|
|
return l.listProjectFiles(ctx, pid)
|
|
case model.SubjectTaskFiles:
|
|
if spec.TaskID == "" {
|
|
return nil, fmt.Errorf("pick a task under Projects in the tree")
|
|
}
|
|
return l.listTaskFiles(ctx, spec.TaskID)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported subject %q", spec.Subject)
|
|
}
|
|
}
|
|
|
|
// LoadProjectsForNav returns projects to inject as dynamic tree nodes.
|
|
func (l *Loader) LoadProjectsForNav(ctx context.Context) ([]model.Item, error) {
|
|
return l.listProjects(ctx)
|
|
}
|
|
|
|
// Detail fetches full record data for preview when list row is insufficient.
|
|
func (l *Loader) Detail(ctx context.Context, item model.Item) (map[string]any, error) {
|
|
switch item.Kind {
|
|
case model.KindOpportunity:
|
|
return l.Client.GetOpportunity(ctx, item.ID)
|
|
case model.KindContact:
|
|
return l.Client.GetContact(ctx, item.ID)
|
|
case model.KindMail:
|
|
return l.Client.GetMailMessage(ctx, item.ID)
|
|
case model.KindTask, model.KindCRMTask:
|
|
return l.Client.GetTaskByID(ctx, item.ID)
|
|
case model.KindProject:
|
|
return l.Client.GetProjectByID(ctx, item.ID)
|
|
default:
|
|
if item.Raw != nil {
|
|
return item.Raw, nil
|
|
}
|
|
return map[string]any{"id": item.ID, "title": item.Title}, nil
|
|
}
|
|
}
|
|
|
|
// Execute runs a user-selected action on an item.
|
|
func (l *Loader) Execute(ctx context.Context, action model.ActionID, item model.Item, destPath string) (string, error) {
|
|
switch action {
|
|
case model.ActionView:
|
|
return "view", nil
|
|
case model.ActionDelete:
|
|
return l.executeDelete(ctx, item)
|
|
case model.ActionDownload:
|
|
return l.executeDownload(ctx, item, destPath)
|
|
default:
|
|
return "", fmt.Errorf("unsupported action %q", action)
|
|
}
|
|
}
|
|
|
|
func (l *Loader) executeDelete(ctx context.Context, item model.Item) (string, error) {
|
|
switch item.Kind {
|
|
case model.KindProject:
|
|
id, err := strconv.Atoi(item.ID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := l.Client.DeleteProject(id); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Deleted project %s", item.Title), nil
|
|
case model.KindTask:
|
|
if _, err := l.Client.DeleteTask(ctx, item.ID); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Deleted task %s", item.Title), nil
|
|
case model.KindContact:
|
|
if _, err := l.Client.DeleteContact(ctx, item.ID); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Deleted contact %s", item.Title), nil
|
|
case model.KindOpportunity:
|
|
if _, err := l.Client.DeleteOpportunity(ctx, item.ID); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Deleted deal %s", item.Title), nil
|
|
case model.KindCase:
|
|
if _, err := l.Client.DeleteCase(ctx, item.ID); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Deleted case %s", item.Title), nil
|
|
case model.KindCRMTask:
|
|
if _, err := l.Client.DeleteCRMTask(ctx, item.ID); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Deleted CRM task %s", item.Title), nil
|
|
case model.KindMail:
|
|
id, err := strconv.Atoi(item.ID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := l.Client.RemoveMailMessages(ctx, id); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Deleted message %s", item.Title), nil
|
|
case model.KindFile:
|
|
id, err := strconv.Atoi(item.ID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := l.Client.DeleteFiles(ctx, []int{id}); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Deleted file %s", item.Title), nil
|
|
default:
|
|
return "", fmt.Errorf("delete not supported for %s", item.Kind)
|
|
}
|
|
}
|
|
|
|
func (l *Loader) executeDownload(ctx context.Context, item model.Item, destPath string) (string, error) {
|
|
if item.Kind != model.KindFile {
|
|
return "", fmt.Errorf("download only for files")
|
|
}
|
|
if destPath == "" {
|
|
destPath = filepath.Join(os.TempDir(), "office", item.Title)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
f, err := os.Create(destPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer f.Close()
|
|
if _, err := l.Client.DownloadFile(ctx, item.ID, f); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("Downloaded to %s", destPath), nil
|
|
}
|
|
|
|
func (l *Loader) listTasksForProject(ctx context.Context, projectID string) ([]model.Item, error) {
|
|
rows, err := l.Client.ListTasks(ctx, projectID, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ItemsFromMaps(rows, model.KindTask, TaskItemFields), nil
|
|
}
|
|
|
|
func (l *Loader) listTaskFiles(ctx context.Context, taskID string) ([]model.Item, error) {
|
|
files, err := l.Client.GetTaskFiles(ctx, taskID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var items []model.Item
|
|
for _, f := range files {
|
|
id, title := "", ""
|
|
if f.ID != nil {
|
|
id = f.ID.String()
|
|
}
|
|
if f.Title != nil {
|
|
title = *f.Title
|
|
}
|
|
items = append(items, model.Item{
|
|
ID: id, Title: title, Kind: model.KindFile,
|
|
Raw: map[string]any{"id": id, "title": title},
|
|
})
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (l *Loader) listProjects(ctx context.Context) ([]model.Item, error) {
|
|
projects, err := l.Client.GetProjects()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]model.Item, len(projects))
|
|
for i, p := range projects {
|
|
id := ""
|
|
if p.ID != nil {
|
|
id = fmt.Sprint(*p.ID)
|
|
}
|
|
title := ""
|
|
if p.Title != nil {
|
|
title = *p.Title
|
|
}
|
|
status := ""
|
|
if p.Status != nil {
|
|
status = fmt.Sprint(*p.Status)
|
|
}
|
|
items[i] = model.Item{
|
|
ID: id, Title: title, Subtitle: status, Kind: model.KindProject,
|
|
Raw: map[string]any{"id": id, "title": title, "status": status},
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (l *Loader) listTasks(ctx context.Context) ([]model.Item, error) {
|
|
rows, err := l.Client.ListAllTasks(ctx, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ItemsFromMaps(rows, model.KindTask, TaskItemFields), nil
|
|
}
|
|
|
|
func (l *Loader) listCalendars(ctx context.Context) ([]model.Item, error) {
|
|
rows, err := l.Client.ListCalendars(ctx, "", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ItemsFromMaps(rows, model.KindCalendar, FieldMap{IDKey: "objectId", TitleKey: "title"}), nil
|
|
}
|
|
|
|
func (l *Loader) listEvents(ctx context.Context) ([]model.Item, error) {
|
|
start := time.Now().Format("2006-01-02")
|
|
end := time.Now().AddDate(0, 0, 7).Format("2006-01-02")
|
|
rows, err := l.Client.ListEvents(ctx, start, end)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ItemsFromMaps(rows, model.KindEvent, FieldMap{IDKey: "objectId", TitleKey: "title", SubtitleKey: "start"}), nil
|
|
}
|
|
|
|
func (l *Loader) listContacts(ctx context.Context, companyOnly *bool) ([]model.Item, error) {
|
|
rows, err := l.Client.ListAllContacts(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if companyOnly != nil {
|
|
filtered := make([]map[string]any, 0, len(rows))
|
|
for _, r := range rows {
|
|
isCo, _ := r["isCompany"].(bool)
|
|
if isCo == *companyOnly {
|
|
filtered = append(filtered, r)
|
|
}
|
|
}
|
|
rows = filtered
|
|
}
|
|
return ItemsFromMaps(rows, model.KindContact, ContactItemFields), nil
|
|
}
|
|
|
|
func (l *Loader) listOpportunities(ctx context.Context) ([]model.Item, error) {
|
|
rows, err := l.Client.ListAllOpportunities(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ItemsFromMaps(rows, model.KindOpportunity, FieldMap{IDKey: "id", TitleKey: "title", SubtitleKey: "stage"}), nil
|
|
}
|
|
|
|
func (l *Loader) listCases(ctx context.Context) ([]model.Item, error) {
|
|
cases, _, err := l.Client.ListCases(ctx, 100, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ItemsFromMaps(cases, model.KindCase, FieldMap{IDKey: "id", TitleKey: "title"}), nil
|
|
}
|
|
|
|
func (l *Loader) listCRMTasks(ctx context.Context) ([]model.Item, error) {
|
|
rows, _, err := l.Client.ListCRMTasks(ctx, 100, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ItemsFromMaps(rows, model.KindCRMTask, TaskItemFields), nil
|
|
}
|
|
|
|
func (l *Loader) listMail(ctx context.Context, folder int) ([]model.Item, error) {
|
|
rows, err := l.Client.ListMailMessages(ctx, onlyoffice.MailMessagesFilter{Folder: folder, Count: 50})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ItemsFromMaps(rows, model.KindMail, MailItemFields), nil
|
|
}
|
|
|
|
func (l *Loader) listUsers(ctx context.Context) ([]model.Item, error) {
|
|
users, err := l.Client.GetUsers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]model.Item, len(users))
|
|
for i, u := range users {
|
|
id := ""
|
|
if u.ID != nil {
|
|
id = fmt.Sprint(*u.ID)
|
|
}
|
|
title := ""
|
|
if u.DisplayName != nil {
|
|
title = *u.DisplayName
|
|
}
|
|
email := ""
|
|
if u.Email != nil {
|
|
email = *u.Email
|
|
}
|
|
items[i] = model.Item{
|
|
ID: id, Title: title, Subtitle: email, Kind: model.KindUser,
|
|
Raw: map[string]any{"id": id, "displayName": title, "email": email},
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (l *Loader) listProjectFiles(ctx context.Context, projectID string) ([]model.Item, error) {
|
|
resp, err := l.Client.GetProjectFiles(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var items []model.Item
|
|
for _, f := range resp.Files {
|
|
id, title := "", ""
|
|
if f.ID != nil {
|
|
id = f.ID.String()
|
|
}
|
|
if f.Title != nil {
|
|
title = *f.Title
|
|
}
|
|
items = append(items, model.Item{
|
|
ID: id, Title: title, Kind: model.KindFile,
|
|
Raw: map[string]any{"id": id, "title": title},
|
|
})
|
|
}
|
|
return items, nil
|
|
}
|