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:
@@ -0,0 +1,64 @@
|
||||
package model
|
||||
|
||||
// ActionID identifies an operation on a list item.
|
||||
type ActionID string
|
||||
|
||||
const (
|
||||
ActionView ActionID = "view"
|
||||
ActionDelete ActionID = "delete"
|
||||
ActionRefresh ActionID = "refresh"
|
||||
ActionDownload ActionID = "download"
|
||||
)
|
||||
|
||||
// ItemAction is one CRUD-style operation offered for an item.
|
||||
type ItemAction struct {
|
||||
ID ActionID
|
||||
Label string
|
||||
Danger bool
|
||||
}
|
||||
|
||||
// ActionsFor returns available operations for an item kind.
|
||||
func ActionsFor(kind Kind) []ItemAction {
|
||||
switch kind {
|
||||
case KindProject:
|
||||
return []ItemAction{
|
||||
{ID: ActionView, Label: "View details"},
|
||||
{ID: ActionDelete, Label: "Delete project", Danger: true},
|
||||
}
|
||||
case KindTask, KindCRMTask:
|
||||
return []ItemAction{
|
||||
{ID: ActionView, Label: "View details"},
|
||||
{ID: ActionDelete, Label: "Delete task", Danger: true},
|
||||
}
|
||||
case KindContact:
|
||||
return []ItemAction{
|
||||
{ID: ActionView, Label: "View details"},
|
||||
{ID: ActionDelete, Label: "Delete contact", Danger: true},
|
||||
}
|
||||
case KindOpportunity:
|
||||
return []ItemAction{
|
||||
{ID: ActionView, Label: "View details"},
|
||||
{ID: ActionDelete, Label: "Delete deal", Danger: true},
|
||||
}
|
||||
case KindCase:
|
||||
return []ItemAction{
|
||||
{ID: ActionView, Label: "View details"},
|
||||
{ID: ActionDelete, Label: "Delete case", Danger: true},
|
||||
}
|
||||
case KindMail:
|
||||
return []ItemAction{
|
||||
{ID: ActionView, Label: "Read message"},
|
||||
{ID: ActionDelete, Label: "Delete message", Danger: true},
|
||||
}
|
||||
case KindFile:
|
||||
return []ItemAction{
|
||||
{ID: ActionView, Label: "Preview file"},
|
||||
{ID: ActionDownload, Label: "Download"},
|
||||
{ID: ActionDelete, Label: "Delete file", Danger: true},
|
||||
}
|
||||
case KindEvent, KindCalendar, KindUser:
|
||||
return []ItemAction{{ID: ActionView, Label: "View details"}}
|
||||
default:
|
||||
return []ItemAction{{ID: ActionView, Label: "View details"}}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,18 @@ const (
|
||||
FocusPreview
|
||||
)
|
||||
|
||||
// PrevFocusPane cycles preview → list → menu → preview.
|
||||
func PrevFocusPane(p FocusPane) FocusPane {
|
||||
switch p {
|
||||
case FocusMenu:
|
||||
return FocusPreview
|
||||
case FocusList:
|
||||
return FocusMenu
|
||||
default:
|
||||
return FocusList
|
||||
}
|
||||
}
|
||||
|
||||
// NextFocusPane cycles menu → list → preview → menu.
|
||||
func NextFocusPane(p FocusPane) FocusPane {
|
||||
switch p {
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
package model
|
||||
|
||||
// menuNode is one row in the left navigation tree (flat slice with depth).
|
||||
type menuNode struct {
|
||||
label string
|
||||
depth int
|
||||
subject Subject // zero when branch node
|
||||
expandable bool
|
||||
parent int // index of parent branch, -1 for roots
|
||||
}
|
||||
|
||||
// MenuTree is the left-pane OnlyOffice module navigator.
|
||||
type MenuTree struct {
|
||||
nodes []menuNode
|
||||
cursor int
|
||||
expanded map[int]bool
|
||||
visible []int // indices into nodes, recomputed on expand/collapse
|
||||
}
|
||||
|
||||
// DefaultMenuTree returns the standard OnlyOffice Workspace module tree.
|
||||
func DefaultMenuTree() *MenuTree {
|
||||
nodes := []menuNode{
|
||||
{label: "Projects", depth: 0, expandable: true, parent: -1},
|
||||
{label: "All projects", depth: 1, subject: SubjectProjects, parent: 0},
|
||||
{label: "Tasks", depth: 1, subject: SubjectTasks, parent: 0},
|
||||
{label: "Calendar", depth: 0, expandable: true, parent: -1},
|
||||
{label: "Calendars", depth: 1, subject: SubjectCalendars, parent: 3},
|
||||
{label: "Events", depth: 1, subject: SubjectEvents, parent: 3},
|
||||
{label: "CRM", depth: 0, expandable: true, parent: -1},
|
||||
{label: "Contacts", depth: 1, subject: SubjectContacts, parent: 6},
|
||||
{label: "Persons", depth: 1, subject: SubjectPersons, parent: 6},
|
||||
{label: "Companies", depth: 1, subject: SubjectCompanies, parent: 6},
|
||||
{label: "Opportunities", depth: 1, subject: SubjectOpportunities, parent: 6},
|
||||
{label: "Cases", depth: 1, subject: SubjectCases, parent: 6},
|
||||
{label: "CRM Tasks", depth: 1, subject: SubjectCRMTasks, parent: 6},
|
||||
{label: "Mail", depth: 0, expandable: true, parent: -1},
|
||||
{label: "Inbox", depth: 1, subject: SubjectMailInbox, parent: 13},
|
||||
{label: "Sent", depth: 1, subject: SubjectMailSent, parent: 13},
|
||||
{label: "Drafts", depth: 1, subject: SubjectMailDrafts, parent: 13},
|
||||
{label: "Trash", depth: 1, subject: SubjectMailTrash, parent: 13},
|
||||
{label: "Spam", depth: 1, subject: SubjectMailSpam, parent: 13},
|
||||
{label: "Documents", depth: 0, expandable: true, parent: -1},
|
||||
{label: "Project files", depth: 1, subject: SubjectProjectFiles, parent: 19},
|
||||
{label: "Task files", depth: 1, subject: SubjectTaskFiles, parent: 19},
|
||||
{label: "Users", depth: 0, expandable: true, parent: -1},
|
||||
{label: "Directory", depth: 1, subject: SubjectUsers, parent: 22},
|
||||
}
|
||||
m := &MenuTree{
|
||||
nodes: nodes,
|
||||
expanded: map[int]bool{},
|
||||
}
|
||||
m.rebuildVisible()
|
||||
return m
|
||||
}
|
||||
|
||||
// RootLabels returns top-level module names in order.
|
||||
func (m *MenuTree) RootLabels() []string {
|
||||
var out []string
|
||||
for i, n := range m.nodes {
|
||||
if n.depth == 0 {
|
||||
out = append(out, n.label)
|
||||
}
|
||||
_ = i
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Cursor returns the visible-row cursor index.
|
||||
func (m *MenuTree) Cursor() int { return m.cursor }
|
||||
|
||||
// SetCursor sets the visible-row cursor.
|
||||
func (m *MenuTree) SetCursor(c int) {
|
||||
if c < 0 {
|
||||
c = 0
|
||||
}
|
||||
if c >= len(m.visible) {
|
||||
c = len(m.visible) - 1
|
||||
}
|
||||
if c < 0 {
|
||||
c = 0
|
||||
}
|
||||
m.cursor = c
|
||||
}
|
||||
|
||||
// MoveDown advances the menu cursor.
|
||||
func (m *MenuTree) MoveDown() {
|
||||
m.SetCursor(m.cursor + 1)
|
||||
}
|
||||
|
||||
// MoveUp moves the menu cursor up.
|
||||
func (m *MenuTree) MoveUp() {
|
||||
m.SetCursor(m.cursor - 1)
|
||||
}
|
||||
|
||||
// VisibleCount returns the number of visible menu rows.
|
||||
func (m *MenuTree) VisibleCount() int { return len(m.visible) }
|
||||
|
||||
// LabelAt returns the label and depth for visible row i.
|
||||
func (m *MenuTree) LabelAt(i int) (label string, depth int) {
|
||||
if i < 0 || i >= len(m.visible) {
|
||||
return "", 0
|
||||
}
|
||||
n := m.nodes[m.visible[i]]
|
||||
return n.label, n.depth
|
||||
}
|
||||
|
||||
// IsExpandable reports whether visible row i is a branch node.
|
||||
func (m *MenuTree) IsExpandable(visibleIdx int) bool {
|
||||
if visibleIdx < 0 || visibleIdx >= len(m.visible) {
|
||||
return false
|
||||
}
|
||||
return m.nodes[m.visible[visibleIdx]].expandable
|
||||
}
|
||||
|
||||
// IsExpanded reports whether the branch at visible row i is expanded.
|
||||
func (m *MenuTree) IsExpanded(visibleIdx int) bool {
|
||||
if visibleIdx < 0 || visibleIdx >= len(m.visible) {
|
||||
return false
|
||||
}
|
||||
return m.expanded[m.visible[visibleIdx]]
|
||||
}
|
||||
|
||||
// ToggleExpand expands or collapses the branch at visible row i.
|
||||
func (m *MenuTree) ToggleExpand(visibleIdx int) {
|
||||
if !m.IsExpandable(visibleIdx) {
|
||||
return
|
||||
}
|
||||
idx := m.visible[visibleIdx]
|
||||
if m.expanded[idx] {
|
||||
delete(m.expanded, idx)
|
||||
} else {
|
||||
m.expanded[idx] = true
|
||||
}
|
||||
m.rebuildVisible()
|
||||
}
|
||||
|
||||
// SelectIndex returns the subject for visible row i when it is a leaf.
|
||||
func (m *MenuTree) SelectIndex(visibleIdx int) (Subject, bool) {
|
||||
if visibleIdx < 0 || visibleIdx >= len(m.visible) {
|
||||
return "", false
|
||||
}
|
||||
n := m.nodes[m.visible[visibleIdx]]
|
||||
if n.subject == "" {
|
||||
return "", false
|
||||
}
|
||||
return n.subject, true
|
||||
}
|
||||
|
||||
// CurrentSubject returns the subject at the current cursor if it is a leaf.
|
||||
func (m *MenuTree) CurrentSubject() (Subject, bool) {
|
||||
return m.SelectIndex(m.cursor)
|
||||
}
|
||||
|
||||
func (m *MenuTree) rebuildVisible() {
|
||||
m.visible = m.visible[:0]
|
||||
for i, n := range m.nodes {
|
||||
if n.depth == 0 {
|
||||
m.visible = append(m.visible, i)
|
||||
if m.expanded[i] {
|
||||
m.appendChildren(i)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if m.cursor >= len(m.visible) {
|
||||
m.cursor = len(m.visible) - 1
|
||||
}
|
||||
if m.cursor < 0 {
|
||||
m.cursor = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MenuTree) appendChildren(branchIdx int) {
|
||||
for i, n := range m.nodes {
|
||||
if n.parent == branchIdx {
|
||||
m.visible = append(m.visible, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package model_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/eslider/go-onlyoffice/cmd/office/model"
|
||||
)
|
||||
|
||||
func TestMenuTreeHasExpectedRoots(t *testing.T) {
|
||||
tree := model.DefaultMenuTree()
|
||||
roots := tree.RootLabels()
|
||||
want := []string{"Projects", "Calendar", "CRM", "Mail", "Documents", "Users"}
|
||||
if len(roots) != len(want) {
|
||||
t.Fatalf("roots=%v want %v", roots, want)
|
||||
}
|
||||
for i, w := range want {
|
||||
if roots[i] != w {
|
||||
t.Fatalf("root[%d]=%q want %q", i, roots[i], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMenuExpandCollapse(t *testing.T) {
|
||||
tree := model.DefaultMenuTree()
|
||||
if !tree.IsExpandable(0) {
|
||||
t.Fatal("Projects should be expandable")
|
||||
}
|
||||
tree.ToggleExpand(0)
|
||||
if !tree.IsExpanded(0) {
|
||||
t.Fatal("Projects should expand")
|
||||
}
|
||||
tree.ToggleExpand(0)
|
||||
if tree.IsExpanded(0) {
|
||||
t.Fatal("Projects should collapse")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMenuSelectLeafReturnsSubject(t *testing.T) {
|
||||
tree := model.DefaultMenuTree()
|
||||
// Expand Projects and select "All projects" (first child).
|
||||
tree.ToggleExpand(0)
|
||||
subj, ok := tree.SelectIndex(1)
|
||||
if !ok {
|
||||
t.Fatal("expected leaf subject")
|
||||
}
|
||||
if subj != model.SubjectProjects {
|
||||
t.Fatalf("subject=%v want Projects", subj)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package model
|
||||
|
||||
// ListSpec tells the fetch layer what to load when a nav leaf is active.
|
||||
type ListSpec struct {
|
||||
Subject Subject
|
||||
ProjectID string
|
||||
TaskID string
|
||||
}
|
||||
|
||||
// NavNode is one row in the navigation tree.
|
||||
type NavNode struct {
|
||||
ID string
|
||||
Label string
|
||||
Branch bool
|
||||
List *ListSpec // set on leaves that open the center list
|
||||
ParentID string
|
||||
}
|
||||
|
||||
// NavTree is a hierarchical navigator; the center list loads only on leaves.
|
||||
type NavTree struct {
|
||||
nodes map[string]NavNode
|
||||
roots []string
|
||||
children map[string][]string
|
||||
expanded map[string]bool
|
||||
cursor int
|
||||
visible []string
|
||||
}
|
||||
|
||||
// DefaultNavTree returns the OnlyOffice module tree (static skeleton).
|
||||
func DefaultNavTree() *NavTree {
|
||||
t := &NavTree{
|
||||
nodes: make(map[string]NavNode),
|
||||
children: make(map[string][]string),
|
||||
expanded: make(map[string]bool),
|
||||
}
|
||||
add := func(id, label, parent string, branch bool, list *ListSpec) {
|
||||
t.nodes[id] = NavNode{ID: id, Label: label, Branch: branch, List: list, ParentID: parent}
|
||||
if parent == "" {
|
||||
t.roots = append(t.roots, id)
|
||||
} else {
|
||||
t.children[parent] = append(t.children[parent], id)
|
||||
}
|
||||
}
|
||||
|
||||
add("projects", "Projects", "", true, nil)
|
||||
add("projects.browse", "Browse", "projects", true, nil)
|
||||
add("projects.browse.all", "All projects", "projects.browse", false, &ListSpec{Subject: SubjectProjects})
|
||||
add("projects.browse.tasks", "All tasks", "projects.browse", false, &ListSpec{Subject: SubjectTasks})
|
||||
add("projects.dynamic", "By project", "projects", true, nil)
|
||||
|
||||
add("calendar", "Calendar", "", true, nil)
|
||||
add("calendar.cals", "Calendars", "calendar", false, &ListSpec{Subject: SubjectCalendars})
|
||||
add("calendar.events", "Events", "calendar", false, &ListSpec{Subject: SubjectEvents})
|
||||
|
||||
add("crm", "CRM", "", true, nil)
|
||||
add("crm.contacts", "Contacts", "crm", false, &ListSpec{Subject: SubjectContacts})
|
||||
add("crm.persons", "Persons", "crm", false, &ListSpec{Subject: SubjectPersons})
|
||||
add("crm.companies", "Companies", "crm", false, &ListSpec{Subject: SubjectCompanies})
|
||||
add("crm.opportunities", "Opportunities", "crm", false, &ListSpec{Subject: SubjectOpportunities})
|
||||
add("crm.cases", "Cases", "crm", false, &ListSpec{Subject: SubjectCases})
|
||||
add("crm.tasks", "CRM tasks", "crm", false, &ListSpec{Subject: SubjectCRMTasks})
|
||||
|
||||
add("mail", "Mail", "", true, nil)
|
||||
add("mail.inbox", "Inbox", "mail", false, &ListSpec{Subject: SubjectMailInbox})
|
||||
add("mail.sent", "Sent", "mail", false, &ListSpec{Subject: SubjectMailSent})
|
||||
add("mail.drafts", "Drafts", "mail", false, &ListSpec{Subject: SubjectMailDrafts})
|
||||
add("mail.trash", "Trash", "mail", false, &ListSpec{Subject: SubjectMailTrash})
|
||||
add("mail.spam", "Spam", "mail", false, &ListSpec{Subject: SubjectMailSpam})
|
||||
|
||||
add("users", "Users", "", true, nil)
|
||||
add("users.dir", "Directory", "users", false, &ListSpec{Subject: SubjectUsers})
|
||||
|
||||
t.rebuildVisible()
|
||||
return t
|
||||
}
|
||||
|
||||
// RootLabels returns top-level labels (for tests).
|
||||
func (t *NavTree) RootLabels() []string {
|
||||
out := make([]string, len(t.roots))
|
||||
for i, id := range t.roots {
|
||||
out[i] = t.nodes[id].Label
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (t *NavTree) Cursor() int { return t.cursor }
|
||||
|
||||
func (t *NavTree) VisibleCount() int { return len(t.visible) }
|
||||
|
||||
func (t *NavTree) NodeAtVisible(i int) (NavNode, bool) {
|
||||
if i < 0 || i >= len(t.visible) {
|
||||
return NavNode{}, false
|
||||
}
|
||||
n, ok := t.nodes[t.visible[i]]
|
||||
return n, ok
|
||||
}
|
||||
|
||||
func (t *NavTree) DepthAtVisible(i int) int {
|
||||
if i < 0 || i >= len(t.visible) {
|
||||
return 0
|
||||
}
|
||||
depth := 0
|
||||
id := t.visible[i]
|
||||
for {
|
||||
n, ok := t.nodes[id]
|
||||
if !ok || n.ParentID == "" {
|
||||
break
|
||||
}
|
||||
depth++
|
||||
id = n.ParentID
|
||||
}
|
||||
return depth
|
||||
}
|
||||
|
||||
func (t *NavTree) IsExpandable(i int) bool {
|
||||
n, ok := t.NodeAtVisible(i)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return n.Branch && len(t.children[n.ID]) > 0
|
||||
}
|
||||
|
||||
func (t *NavTree) IsExpanded(i int) bool {
|
||||
n, ok := t.NodeAtVisible(i)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return t.expanded[n.ID]
|
||||
}
|
||||
|
||||
func (t *NavTree) ToggleExpand(i int) {
|
||||
n, ok := t.NodeAtVisible(i)
|
||||
if !ok || !n.Branch {
|
||||
return
|
||||
}
|
||||
if t.expanded[n.ID] {
|
||||
delete(t.expanded, n.ID)
|
||||
} else {
|
||||
t.expanded[n.ID] = true
|
||||
}
|
||||
t.rebuildVisible()
|
||||
}
|
||||
|
||||
func (t *NavTree) MoveUp() { t.SetCursor(t.cursor - 1) }
|
||||
func (t *NavTree) MoveDown() { t.SetCursor(t.cursor + 1) }
|
||||
|
||||
func (t *NavTree) SetCursor(c int) {
|
||||
if c < 0 {
|
||||
c = 0
|
||||
}
|
||||
if c >= len(t.visible) {
|
||||
c = len(t.visible) - 1
|
||||
}
|
||||
if c < 0 {
|
||||
c = 0
|
||||
}
|
||||
t.cursor = c
|
||||
}
|
||||
|
||||
// CurrentListSpec returns the list spec when the cursor is on a leaf.
|
||||
func (t *NavTree) CurrentListSpec() (*ListSpec, bool) {
|
||||
n, ok := t.NodeAtVisible(t.cursor)
|
||||
if !ok || n.List == nil {
|
||||
return nil, false
|
||||
}
|
||||
spec := *n.List
|
||||
return &spec, true
|
||||
}
|
||||
|
||||
// Activate expands a branch or returns leaf list spec on Enter.
|
||||
func (t *NavTree) Activate() (*ListSpec, bool) {
|
||||
n, ok := t.NodeAtVisible(t.cursor)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if n.Branch {
|
||||
t.expanded[n.ID] = true
|
||||
t.rebuildVisible()
|
||||
return nil, false
|
||||
}
|
||||
if n.List != nil {
|
||||
spec := *n.List
|
||||
return &spec, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (t *NavTree) rebuildVisible() {
|
||||
t.visible = t.visible[:0]
|
||||
var walk func(id string, depth int)
|
||||
walk = func(id string, depth int) {
|
||||
t.visible = append(t.visible, id)
|
||||
if !t.expanded[id] {
|
||||
return
|
||||
}
|
||||
for _, child := range t.children[id] {
|
||||
walk(child, depth+1)
|
||||
}
|
||||
}
|
||||
for _, root := range t.roots {
|
||||
walk(root, 0)
|
||||
}
|
||||
if t.cursor >= len(t.visible) {
|
||||
t.cursor = len(t.visible) - 1
|
||||
}
|
||||
if t.cursor < 0 {
|
||||
t.cursor = 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
import "strings"
|
||||
|
||||
// InjectProjectNodes adds dynamic project branches under projects.dynamic.
|
||||
func (t *NavTree) InjectProjectNodes(projects []Item) {
|
||||
// Drop prior dynamic project.* nodes.
|
||||
for id := range t.nodes {
|
||||
if strings.HasPrefix(id, "project.") {
|
||||
delete(t.nodes, id)
|
||||
}
|
||||
}
|
||||
t.children["projects.dynamic"] = nil
|
||||
for _, p := range projects {
|
||||
pid := "project." + p.ID
|
||||
t.nodes[pid] = NavNode{ID: pid, Label: p.Title, Branch: true, ParentID: "projects.dynamic"}
|
||||
t.children["projects.dynamic"] = append(t.children["projects.dynamic"], pid)
|
||||
taskID := pid + ".tasks"
|
||||
fileID := pid + ".files"
|
||||
t.nodes[taskID] = NavNode{
|
||||
ID: taskID, Label: "Tasks", Branch: false, ParentID: pid,
|
||||
List: &ListSpec{Subject: SubjectTasks, ProjectID: p.ID},
|
||||
}
|
||||
t.nodes[fileID] = NavNode{
|
||||
ID: fileID, Label: "Files", Branch: false, ParentID: pid,
|
||||
List: &ListSpec{Subject: SubjectProjectFiles, ProjectID: p.ID},
|
||||
}
|
||||
t.children[pid] = []string{taskID, fileID}
|
||||
}
|
||||
t.rebuildVisible()
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package model_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/eslider/go-onlyoffice/cmd/office/model"
|
||||
)
|
||||
|
||||
func TestNavTreeHasExpectedRoots(t *testing.T) {
|
||||
tree := model.DefaultNavTree()
|
||||
roots := tree.RootLabels()
|
||||
want := []string{"Projects", "Calendar", "CRM", "Mail", "Users"}
|
||||
if len(roots) != len(want) {
|
||||
t.Fatalf("roots=%v want %v", roots, want)
|
||||
}
|
||||
for i, w := range want {
|
||||
if roots[i] != w {
|
||||
t.Fatalf("root[%d]=%q want %q", i, roots[i], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNavExpandCollapse(t *testing.T) {
|
||||
tree := model.DefaultNavTree()
|
||||
if !tree.IsExpandable(0) {
|
||||
t.Fatal("Projects should be expandable")
|
||||
}
|
||||
tree.ToggleExpand(0)
|
||||
if !tree.IsExpanded(0) {
|
||||
t.Fatal("Projects should expand")
|
||||
}
|
||||
tree.ToggleExpand(0)
|
||||
if tree.IsExpanded(0) {
|
||||
t.Fatal("Projects should collapse")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNavLeafReturnsListSpec(t *testing.T) {
|
||||
tree := model.DefaultNavTree()
|
||||
tree.ToggleExpand(0) // Projects
|
||||
tree.ToggleExpand(1) // Browse
|
||||
// Find "All projects" leaf cursor
|
||||
var found int = -1
|
||||
for i := 0; i < tree.VisibleCount(); i++ {
|
||||
n, ok := tree.NodeAtVisible(i)
|
||||
if ok && n.List != nil && n.List.Subject == model.SubjectProjects {
|
||||
found = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if found < 0 {
|
||||
t.Fatal("all projects leaf not found")
|
||||
}
|
||||
tree.SetCursor(found)
|
||||
spec, ok := tree.CurrentListSpec()
|
||||
if !ok || spec.Subject != model.SubjectProjects {
|
||||
t.Fatalf("spec=%v ok=%v", spec, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrevFocusPane(t *testing.T) {
|
||||
if got := model.PrevFocusPane(model.FocusList); got != model.FocusMenu {
|
||||
t.Fatalf("got %v", got)
|
||||
}
|
||||
if got := model.PrevFocusPane(model.FocusMenu); got != model.FocusPreview {
|
||||
t.Fatalf("got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsForContact(t *testing.T) {
|
||||
acts := model.ActionsFor(model.KindContact)
|
||||
if len(acts) < 2 {
|
||||
t.Fatalf("expected view+delete, got %d", len(acts))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user