Files
goclaw/internal/http/vault_handler_tree.go
T
viettranx 35ce8cc2c5 feat(vault): tree sidebar with lazy-load folder hierarchy
Replace flat paginated document list with collapsible tree view.
Backend: two-query-per-level approach (files + DISTINCT deeper paths
for virtual folder derivation), path parsing in Go for PG+SQLite
compatibility, text_pattern_ops index for prefix range scans.
Frontend: VaultTree component with doc_type colored icons, compact
single-line nodes, truncate-middle filenames, scope dots, hover
tooltip for date. Sidebar expanded to 384px on large screens.

Also fixes pre-existing SQLite scanVaultDocRow missing path_basename.
2026-04-12 12:37:10 +07:00

57 lines
1.6 KiB
Go

package http
import (
"log/slog"
"net/http"
"strings"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// handleVaultTree returns immediate children (files + virtual folders) under
// a given path prefix for lazy-loading the vault sidebar tree.
func (h *VaultHandler) handleVaultTree(w http.ResponseWriter, r *http.Request) {
tenantID := store.TenantIDFromContext(r.Context())
path := r.URL.Query().Get("path")
if strings.Contains(path, "..") || strings.HasPrefix(path, "/") {
slog.Warn("security.vault_tree_traversal", "path", path)
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid path"})
return
}
opts := store.VaultTreeOptions{
Path: path,
Scope: r.URL.Query().Get("scope"),
DocTypes: splitCSV(r.URL.Query().Get("doc_type")),
}
if teamID := r.URL.Query().Get("team_id"); teamID != "" {
opts.TeamID = &teamID
}
if opts.TeamID == nil && !store.IsOwnerRole(r.Context()) {
if ids := h.userAccessibleTeamIDs(r.Context()); len(ids) > 0 {
opts.TeamIDs = ids
} else {
empty := ""
opts.TeamID = &empty
}
}
if opts.TeamID != nil && *opts.TeamID != "" {
if !h.validateTeamMembership(r.Context(), w, *opts.TeamID) {
return
}
}
entries, err := h.store.ListTreeEntries(r.Context(), tenantID.String(), opts)
if err != nil {
slog.Warn("vault.tree failed", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to load tree"})
return
}
if entries == nil {
entries = []store.VaultTreeEntry{}
}
writeJSON(w, http.StatusOK, map[string]any{"entries": entries})
}