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:
@@ -295,7 +295,7 @@ func (s *Service) CreateEmptyFile(parentID, filename string) (*nodes.Node, error
|
||||
return nil, fmt.Errorf("invalid filename: %w", err)
|
||||
}
|
||||
filename = s.uniqueTitle(parentID, filename)
|
||||
node, err := s.nodes.Create(parentID, nodes.TypeFile, filename, "")
|
||||
node, err := s.nodes.Create(strPtr(parentID), nodes.TypeFile, filename, 0, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -329,7 +329,7 @@ func (s *Service) Duplicate(nodeID string) (*nodes.Node, error) {
|
||||
parentID = *original.ParentID
|
||||
}
|
||||
newName := s.copyTitle(parentID, original.Title)
|
||||
node, err := s.nodes.Create(parentID, original.Type, newName, original.Section)
|
||||
node, err := s.nodes.Create(strPtr(parentID), original.Type, newName, 0, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -449,7 +449,7 @@ func (s *Service) importPath(parentID, sourcePath string, copyMode bool) ([]node
|
||||
}
|
||||
if !info.IsDir() {
|
||||
title := s.uniqueTitle(parentID, filepath.Base(sourcePath))
|
||||
node, err := s.nodes.Create(parentID, nodes.TypeFile, title, "")
|
||||
node, err := s.nodes.Create(strPtr(parentID), nodes.TypeFile, title, 0, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -468,7 +468,7 @@ func (s *Service) importPath(parentID, sourcePath string, copyMode bool) ([]node
|
||||
|
||||
func (s *Service) importDir(parentID, sourcePath, dirName string, copyMode bool) ([]nodes.Node, error) {
|
||||
dirName = s.uniqueTitle(parentID, dirName)
|
||||
folderNode, err := s.nodes.Create(parentID, nodes.TypeFolder, dirName, "")
|
||||
folderNode, err := s.nodes.Create(strPtr(parentID), nodes.TypeFolder, dirName, 0, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -490,7 +490,7 @@ func (s *Service) importDir(parentID, sourcePath, dirName string, copyMode bool)
|
||||
}
|
||||
all = append(all, children...)
|
||||
} else {
|
||||
childNode, err := s.nodes.Create(folderNode.ID, nodes.TypeFile, entry.Name(), "")
|
||||
childNode, err := s.nodes.Create(strPtr(folderNode.ID), nodes.TypeFile, entry.Name(), 0, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -680,3 +680,10 @@ func scanRecords(rows *sql.Rows) ([]Record, error) {
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func strPtr(s string) *string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ func TestAddPathCopySingleFile(t *testing.T) {
|
||||
nodeRepo := nodes.NewRepository(db)
|
||||
svc := NewService(db, vaultRoot, nodeRepo)
|
||||
|
||||
parent, _ := nodeRepo.Create("", "case", "Test Case", "")
|
||||
parent, _ := nodeRepo.Create(nil, "case", "Test Case", 0, "", "")
|
||||
src := filepath.Join(t.TempDir(), "doc.pdf")
|
||||
os.WriteFile(src, []byte("file content"), 0o640)
|
||||
|
||||
@@ -178,7 +178,7 @@ func TestAddPathLinkSingleFile(t *testing.T) {
|
||||
nodeRepo := nodes.NewRepository(db)
|
||||
svc := NewService(db, vaultRoot, nodeRepo)
|
||||
|
||||
parent, _ := nodeRepo.Create("", "case", "Test Case", "")
|
||||
parent, _ := nodeRepo.Create(nil, "case", "Test Case", 0, "", "")
|
||||
src := filepath.Join(t.TempDir(), "linked.pdf")
|
||||
os.WriteFile(src, []byte("linked"), 0o640)
|
||||
|
||||
@@ -205,7 +205,7 @@ func TestAddPathCopyDirectory(t *testing.T) {
|
||||
nodeRepo := nodes.NewRepository(db)
|
||||
svc := NewService(db, vaultRoot, nodeRepo)
|
||||
|
||||
parent, _ := nodeRepo.Create("", "case", "Test Case", "")
|
||||
parent, _ := nodeRepo.Create(nil, "case", "Test Case", 0, "", "")
|
||||
srcDir := t.TempDir()
|
||||
os.MkdirAll(filepath.Join(srcDir, "sub"), 0o750)
|
||||
os.WriteFile(filepath.Join(srcDir, "a.txt"), []byte("a"), 0o640)
|
||||
@@ -242,8 +242,8 @@ func TestDeleteNodeAndChildren(t *testing.T) {
|
||||
nodeRepo := nodes.NewRepository(db)
|
||||
svc := NewService(db, vaultRoot, nodeRepo)
|
||||
|
||||
parent, _ := nodeRepo.Create("", "case", "To Delete", "")
|
||||
child, _ := nodeRepo.Create(parent.ID, "file", "child.txt", "")
|
||||
parent, _ := nodeRepo.Create(nil, "case", "To Delete", 0, "", "")
|
||||
child, _ := nodeRepo.Create(&parent.ID, "file", "child.txt", 0, "", "")
|
||||
// Add file record to child.
|
||||
src := filepath.Join(t.TempDir(), "child.txt")
|
||||
os.WriteFile(src, []byte("data"), 0o640)
|
||||
@@ -268,7 +268,7 @@ func TestNameConflict(t *testing.T) {
|
||||
nodeRepo := nodes.NewRepository(db)
|
||||
svc := NewService(db, vaultRoot, nodeRepo)
|
||||
|
||||
parent, _ := nodeRepo.Create("", "case", "Test", "")
|
||||
parent, _ := nodeRepo.Create(nil, "case", "Test", 0, "", "")
|
||||
src := filepath.Join(t.TempDir(), "conflict.pdf")
|
||||
os.WriteFile(src, []byte("data"), 0o640)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user