verstak/cmd/verstak-gui/bindings_nodes.go

337 lines
8.1 KiB
Go

package main
import (
"fmt"
"os"
"path/filepath"
"time"
"verstak/internal/core/activity"
"verstak/internal/core/nodes"
"verstak/internal/core/templates"
syncsvc "verstak/internal/core/sync"
)
func (a *App) ListWorkspaceTree() ([]NodeDTO, error) {
list, err := a.nodes.ListRoots(false)
if err != nil {
return nil, err
}
return toNodeDTOs(list), nil
}
func (a *App) ListChildren(parentID string) ([]NodeDTO, error) {
list, err := a.nodes.ListChildren(parentID, false)
if err != nil {
return nil, err
}
return toNodeDTOs(list), nil
}
func (a *App) GetNodeDetail(nodeID string) (*NodeDTO, error) {
n, err := a.nodes.GetActive(nodeID)
if err != nil {
return nil, err
}
dto := toNodeDTO(n)
return &dto, nil
}
func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeDTO, error) {
tmpl, ok := a.templates.Get(templateID)
if !ok {
return nil, fmt.Errorf("template %q not found", templateID)
}
seg := templates.SafeDisplayNameToPathSegment(title)
if seg == "" {
seg = title
}
var parent *nodes.Node
var parentFsPath string
if parentID != "" {
var err error
parent, err = a.nodes.GetActive(parentID)
if err != nil {
return nil, fmt.Errorf("parent not found: %w", err)
}
parentFsPath = parent.FsPath
}
fsPath := seg
if parentFsPath != "" {
fsPath = filepath.Join(parentFsPath, seg)
}
physPath := filepath.Join(a.vault, fsPath)
physPath = templates.UniquePath(physPath)
var pID *string
if parentID != "" {
pID = &parentID
}
sortOrder := 0
n, err := a.nodes.Create(pID, tmpl.Type, title, sortOrder, tmpl.ID, fsPath)
if err != nil {
return nil, fmt.Errorf("create node: %w", err)
}
if err := os.MkdirAll(physPath, 0o755); err != nil {
return nil, fmt.Errorf("create folder: %w", err)
}
for _, df := range tmpl.DefaultFiles {
fpath := filepath.Join(physPath, df.Path)
if err := os.MkdirAll(filepath.Dir(fpath), 0o755); err != nil {
continue
}
content := fmt.Sprintf("# %s\n\n", title)
_ = os.WriteFile(fpath, []byte(content), 0o640)
}
for _, folder := range tmpl.DefaultFolders {
fpath := filepath.Join(physPath, folder)
_ = os.MkdirAll(fpath, 0o755)
}
pid := ""
if parentID != "" {
pid = parentID
}
_ = a.activity.Record(pid, activity.TargetNode, n.ID, "", activity.TypeNodeCreated, title, `{"template":"`+templateID+`"}`)
_ = a.sync.RecordOp(syncsvc.EntityNode, n.ID, syncsvc.OpCreate, nodePayload(n))
dto := toNodeDTO(n)
return &dto, nil
}
func (a *App) DeleteNode(id string) error {
n, err := a.nodes.GetActive(id)
if err != nil {
return a.nodes.SoftDelete(id)
}
pid := ""
if n.ParentID != nil {
pid = *n.ParentID
}
var entity string
var targetType string
var evType string
switch n.Type {
case nodes.TypeNote:
entity = syncsvc.EntityNote
targetType = activity.TargetNote
evType = activity.TypeNoteDeleted
case nodes.TypeFolder:
entity = syncsvc.EntityFolder
targetType = activity.TargetFolder
evType = activity.TypeFolderDeleted
case nodes.TypeFile:
entity = syncsvc.EntityFile
targetType = activity.TargetFile
evType = activity.TypeFileDeleted
default:
entity = syncsvc.EntityNode
targetType = activity.TargetNode
evType = activity.TypeNodeDeleted
}
_ = a.activity.Record(pid, targetType, id, "", evType, n.Title, "")
_ = a.sync.RecordOp(entity, id, syncsvc.OpDelete, nil)
return a.nodes.SoftDelete(id)
}
func (a *App) RenameNode(nodeID, newTitle string) error {
n, err := a.nodes.GetActive(nodeID)
if err != nil {
return err
}
seg := templates.SafeDisplayNameToPathSegment(newTitle)
if seg == "" {
seg = newTitle
}
oldFsPath := n.FsPath
oldPhysPath := filepath.Join(a.vault, oldFsPath)
parentFsPath := ""
if n.ParentID != nil {
p, err := a.nodes.GetActive(*n.ParentID)
if err == nil {
parentFsPath = p.FsPath
}
}
newFsPath := seg
if parentFsPath != "" {
newFsPath = filepath.Join(parentFsPath, seg)
}
newPhysPath := filepath.Join(a.vault, newFsPath)
newPhysPath = templates.UniquePath(newPhysPath)
rel, _ := filepath.Rel(a.vault, newPhysPath)
newFsPath = rel
oldTitle := n.Title
if err := a.nodes.UpdateTitle(nodeID, newTitle); err != nil {
return err
}
if err := a.nodes.UpdateFsPath(nodeID, newFsPath); err != nil {
return err
}
if err := a.nodes.UpdateFsPathRecursive(nodeID, newFsPath); err != nil {
return err
}
if _, err := os.Stat(oldPhysPath); err == nil {
if err := os.Rename(oldPhysPath, newPhysPath); err != nil {
return fmt.Errorf("rename folder: %w", err)
}
}
pid := ""
if n.ParentID != nil {
pid = *n.ParentID
}
var evType string
var targetType string
var syncEntity string
switch n.Type {
case nodes.TypeNote:
evType = activity.TypeNoteUpdated
targetType = activity.TargetNote
syncEntity = syncsvc.EntityNote
case nodes.TypeFile:
evType = activity.TypeFileRenamed
targetType = activity.TargetFile
syncEntity = syncsvc.EntityFile
case nodes.TypeFolder:
evType = activity.TypeFolderRenamed
targetType = activity.TargetFolder
syncEntity = syncsvc.EntityFolder
default:
evType = activity.TypeNodeUpdated
targetType = activity.TargetNode
syncEntity = syncsvc.EntityNode
}
_ = a.activity.Record(pid, targetType, nodeID, "", evType, newTitle, `{"from":"`+oldTitle+`","to":"`+newTitle+`"}`)
_ = a.sync.RecordOp(syncEntity, nodeID, syncsvc.OpUpdate, map[string]interface{}{
"title": newTitle,
"fs_path": newFsPath,
"updated_at": time.Now().UTC().Format(time.RFC3339),
})
return nil
}
func (a *App) MoveNode(nodeID, newParentID string) error {
destChildren, err := a.nodes.ListChildren(newParentID, false)
if err != nil {
return err
}
node, err := a.nodes.GetActive(nodeID)
if err != nil {
return err
}
for i := range destChildren {
if destChildren[i].Title == node.Title {
newName := fmt.Sprintf("%s (%d)", node.Title, 2)
_ = a.nodes.UpdateTitle(nodeID, newName)
break
}
}
var parent *nodes.Node
if newParentID != "" {
parent, err = a.nodes.GetActive(newParentID)
if err != nil {
return fmt.Errorf("new parent not found: %w", err)
}
}
seg := templates.SafeDisplayNameToPathSegment(node.Title)
newFsPath := seg
if parent != nil && parent.FsPath != "" {
newFsPath = filepath.Join(parent.FsPath, seg)
}
newPhysPath := filepath.Join(a.vault, newFsPath)
newPhysPath = templates.UniquePath(newPhysPath)
rel, _ := filepath.Rel(a.vault, newPhysPath)
newFsPath = rel
oldPhysPath := filepath.Join(a.vault, node.FsPath)
if err := a.nodes.Move(nodeID, &newParentID, 0); err != nil {
return err
}
if err := a.nodes.UpdateFsPath(nodeID, newFsPath); err != nil {
return err
}
if err := a.nodes.UpdateFsPathRecursive(nodeID, newFsPath); err != nil {
return err
}
if _, err := os.Stat(oldPhysPath); err == nil {
if err := os.Rename(oldPhysPath, newPhysPath); err != nil {
return fmt.Errorf("move folder: %w", err)
}
}
pid := ""
if node.ParentID != nil {
pid = *node.ParentID
}
var targetType string
var evType string
var syncEntity string
switch node.Type {
case nodes.TypeNote:
targetType = activity.TargetNote
evType = activity.TypeNoteUpdated
syncEntity = syncsvc.EntityNote
case nodes.TypeFile:
targetType = activity.TargetFile
evType = activity.TypeFileMoved
syncEntity = syncsvc.EntityFile
case nodes.TypeFolder:
targetType = activity.TargetFolder
evType = activity.TypeFolderMoved
syncEntity = syncsvc.EntityFolder
default:
targetType = activity.TargetNode
evType = activity.TypeNodeUpdated
syncEntity = syncsvc.EntityNode
}
_ = a.activity.Record(pid, targetType, nodeID, "", evType, node.Title, `{"to":"`+newParentID+`"}`)
_ = a.sync.RecordOp(syncEntity, nodeID, syncsvc.OpMove, map[string]interface{}{
"parent_id": newParentID,
"fs_path": newFsPath,
"updated_at": time.Now().UTC().Format(time.RFC3339),
})
return nil
}
func (a *App) ListEnabledTemplates() ([]TemplateDTO, error) {
list := a.templates.Enabled()
result := make([]TemplateDTO, len(list))
for i, t := range list {
result[i] = TemplateDTO{
ID: t.ID,
Title: t.Title,
Type: t.Type,
Icon: t.Icon,
}
}
return result, nil
}
func (a *App) OpenNodeFolder(nodeID string) (string, error) {
n, err := a.nodes.GetActive(nodeID)
if err != nil {
return "", err
}
physPath := filepath.Join(a.vault, n.FsPath)
return physPath, nil
}