refactor: implement template-driven node tree and human-readable vault layout

Unified Node model: added template_id, fs_path, archived, sort_order fields.
Template registry: system templates embedded as JSON (folder/project/client/
document/recipe), with Registry for enabled/disabled/filtered access.
SafeDisplayNameToPathSegment: human-readable path segments with Cyrillic
support, illegal char replacement, uniqueness via numeric suffixes.
Sidebar refactored: system views (Today/Inbox/Activity) separate from
workspace tree. Creation menu built dynamically from enabled templates.
Create/Rename/Move: physical folder operations with fs_path update,
recursive descendant path updates.
DB migration 012: adds template_id, fs_path, archived columns.
Vault migration command: rebuilds fs_path for existing nodes.
Tests: safename, registry, node model, repository integration.
Docs: VAULT_LAYOUT.md, TEMPLATES.md, PLAN.md updated.
i18n: nav.system, nav.workspace, template.*, common.rename/archive,
migrate.* keys added to ru.json and en.json.
This commit is contained in:
2026-06-02 12:47:06 +08:00
parent 12f2916a24
commit 0b26f7e5b3
37 changed files with 1479 additions and 338 deletions
+14 -8
View File
@@ -107,6 +107,13 @@ func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) {
t.Execute(w, nil)
}
func strPtr(s string) *string {
if s == "" {
return nil
}
return &s
}
func jsonOK(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
@@ -118,16 +125,15 @@ func jsonErr(w http.ResponseWriter, code int, msg string) {
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}
// GET /api/nodes[?parent=ID&section=X] POST /api/nodes
// GET /api/nodes[?parent=ID] POST /api/nodes
func (s *Server) handleNodes(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
parent := r.URL.Query().Get("parent")
section := r.URL.Query().Get("section")
var list interface{}
var err error
if parent == "" {
list, err = s.nodes.ListRoots(false, section)
list, err = s.nodes.ListRoots(false)
} else {
list, err = s.nodes.ListChildren(parent, false)
}
@@ -147,7 +153,7 @@ func (s *Server) handleNodes(w http.ResponseWriter, r *http.Request) {
jsonErr(w, 400, "bad json")
return
}
n, err := s.nodes.Create(req.ParentID, req.Type, req.Title, req.Section)
n, err := s.nodes.Create(strPtr(req.ParentID), req.Type, req.Title, 0, "", "")
if err != nil {
jsonErr(w, 500, err.Error())
return
@@ -187,7 +193,7 @@ func (s *Server) handleNodeFromTemplate(w http.ResponseWriter, r *http.Request)
}
// Create root node.
root, err := s.nodes.Create(req.ParentID, tmpl.RootType, req.Title, req.Section)
root, err := s.nodes.Create(strPtr(req.ParentID), tmpl.RootType, req.Title, 0, "", "")
if err != nil {
jsonErr(w, 500, err.Error())
return
@@ -197,7 +203,7 @@ func (s *Server) handleNodeFromTemplate(w http.ResponseWriter, r *http.Request)
var createTree func(parentID string, nodes []plugins.TreeNode) error
createTree = func(parentID string, nodes []plugins.TreeNode) error {
for _, tn := range nodes {
child, err := s.nodes.Create(parentID, tn.Type, tn.Title, "")
child, err := s.nodes.Create(strPtr(parentID), tn.Type, tn.Title, 0, "", "")
if err != nil {
return err
}
@@ -244,7 +250,7 @@ func (s *Server) handleNodeDetail(w http.ResponseWriter, r *http.Request) {
s.nodes.UpdateTitle(id, req.Title)
}
if req.ParentID != "" {
s.nodes.Move(id, req.ParentID, req.Sort)
s.nodes.Move(id, &req.ParentID, req.Sort)
}
jsonOK(w, map[string]string{"status": "ok"})
case "DELETE":
@@ -466,7 +472,7 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
results, err := s.search.Search(q)
if err != nil || len(results) == 0 {
// Fallback: search node titles directly.
roots, _ := s.nodes.ListRoots(false, "")
roots, _ := s.nodes.ListRoots(false)
ql := strings.ToLower(q)
for _, n := range roots {
if strings.Contains(strings.ToLower(n.Title), ql) {