Introduce the office Bubble Tea browser (module tree, multi-select lists, markdown preview) alongside oo, with cmd/internal/bootstrap for shared auth, unit tests across preview/model/ui/fetch, CI build for both binaries, and GoReleaser archives for office. Co-authored-by: Cursor <cursoragent@cursor.com>
180 lines
5.0 KiB
Go
180 lines
5.0 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|