Files
go-onlyoffice/cmd/office/model/nav.go
T
eSliderandCursor 40b74eb8ab feat(office): users admin UI, table rendering, and user save fixes
Add Users list/detail with ACL toggles and library people update/status APIs.
Fix project table layout and ANSI-safe cell rendering, and decode array
responses from people/status so saves no longer fail on JSON unmarshal.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 11:07:22 +01:00

254 lines
5.9 KiB
Go

package model
import "strings"
// 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
filter 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", "", false, &ListSpec{Subject: SubjectProjects})
add("tasks", "Tasks", "", false, &ListSpec{Subject: SubjectTasks})
add("projects.dynamic", "By project", "", true, nil)
add("calendar", "Calendar", "", false, &ListSpec{Subject: SubjectCalendar})
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", "", 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) SetFilter(query string) {
t.filter = strings.ToLower(strings.TrimSpace(query))
t.rebuildVisible()
}
func (t *NavTree) ClearFilter() {
t.filter = ""
t.rebuildVisible()
}
func (t *NavTree) FilterQuery() string { return t.filter }
func (t *NavTree) nodeLabelMatches(id string) bool {
if t.filter == "" {
return true
}
n, ok := t.nodes[id]
if !ok {
return false
}
return strings.Contains(strings.ToLower(n.Label), t.filter)
}
func (t *NavTree) hasMatchingDescendant(id string) bool {
for _, child := range t.children[id] {
if t.nodeLabelMatches(child) || t.hasMatchingDescendant(child) {
return true
}
}
return false
}
func (t *NavTree) visibleUnderFilter(id string) bool {
if t.filter == "" {
return true
}
return t.nodeLabelMatches(id) || t.hasMatchingDescendant(id)
}
func (t *NavTree) rebuildVisible() {
t.visible = t.visible[:0]
var walk func(id string)
walk = func(id string) {
if !t.visibleUnderFilter(id) {
return
}
t.visible = append(t.visible, id)
expanded := t.expanded[id]
if t.filter != "" && t.hasMatchingDescendant(id) {
expanded = true
}
if !expanded {
return
}
for _, child := range t.children[id] {
walk(child)
}
}
for _, root := range t.roots {
walk(root)
}
if t.cursor >= len(t.visible) {
t.cursor = len(t.visible) - 1
}
if t.cursor < 0 {
t.cursor = 0
}
}