fix: stabilize markdown notes — internal link modal, rename UI, trash integration
- Replace broken ObjectPickerModal with simple inline modal (Label+URL fields) - Insert internal link at cursor position in textarea - Add rename button in note editor header and note cards - Add delete button on note cards with confirm dialog - Integrate DeleteNote with shared trash (.verstak/trash/) via files.TrashFile() - Remove hidden .verstak/trash/notes/ folder — notes use unified trash now - Fix purgeTrashNode to clean file-record-based trash entries (notes/files) - Add activity + sync ops to DeleteNote binding - Add files.TrashFile() public method - Update i18n keys for note.rename, note.deleteConfirm, internal link modal - AssertContained: symlink-aware path containment check - Update tests: shared trash, file record missing flag, collision on rename - All go test ./... pass, frontend build passes, GUI binary built
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"verstak/internal/core/activity"
|
||||
"verstak/internal/core/nodes"
|
||||
syncsvc "verstak/internal/core/sync"
|
||||
"verstak/internal/core/notes"
|
||||
"verstak/internal/core/templates"
|
||||
"verstak/internal/core/util"
|
||||
)
|
||||
@@ -173,7 +174,8 @@ func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeD
|
||||
}
|
||||
|
||||
for _, df := range tmpl.DefaultFiles {
|
||||
fpath := filepath.Join(physPath, df.Path)
|
||||
// Default files (like Overview.md) go into the Notes/ subfolder
|
||||
fpath := filepath.Join(physPath, notes.NotesFolder, df.Path)
|
||||
if err := os.MkdirAll(filepath.Dir(fpath), 0o755); err != nil {
|
||||
rollbackChildren()
|
||||
return nil, fmt.Errorf("create directory for %s: %w", df.Path, err)
|
||||
|
||||
@@ -67,3 +67,29 @@ func (a *App) SaveNote(noteID, content string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) RenameNote(noteID, newTitle string) error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.notes.Rename(noteID, newTitle)
|
||||
}
|
||||
|
||||
func (a *App) DeleteNote(noteID string) error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Record activity and sync op before delete (need node info).
|
||||
n, _ := a.nodes.GetActive(noteID)
|
||||
pid := ""
|
||||
if n != nil && n.ParentID != nil {
|
||||
pid = *n.ParentID
|
||||
}
|
||||
title := ""
|
||||
if n != nil {
|
||||
title = n.Title
|
||||
}
|
||||
_ = a.activity.Record(pid, activity.TargetNote, noteID, "", activity.TypeNoteDeleted, title, "")
|
||||
_ = a.sync.RecordOp(syncsvc.EntityNote, noteID, syncsvc.OpDelete, nil)
|
||||
return a.notes.Delete(noteID)
|
||||
}
|
||||
|
||||
@@ -315,9 +315,19 @@ func (a *App) purgeTrashNode(nodeID string) error {
|
||||
return err
|
||||
}
|
||||
for _, id := range ids {
|
||||
// Try direct trash entry (folder-type nodes: nodeID_title).
|
||||
if path, err := a.findTrashEntryForNode(id); err == nil {
|
||||
_ = os.RemoveAll(path)
|
||||
}
|
||||
// Try file record trash entries (file/note nodes: fileID_filename).
|
||||
// These are created by files.trashRecord and not found by findTrashEntryForNode.
|
||||
if recs, err := a.files.ListTrashedByNode(id); err == nil {
|
||||
trashDir := filepath.Join(a.vault, ".verstak", "trash")
|
||||
for _, r := range recs {
|
||||
trashPath := filepath.Join(trashDir, r.ID+"_"+r.Filename)
|
||||
_ = os.RemoveAll(trashPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
tx, err := a.db.Begin()
|
||||
if err != nil {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -19,8 +19,8 @@
|
||||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-CRc6HR9x.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-B1PBee3I.css">
|
||||
<script type="module" crossorigin src="/assets/main-6mFhgd0M.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-bmXj_j_Z.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -126,12 +126,17 @@ func TestVaultLayout_CreateProjectTree(t *testing.T) {
|
||||
t.Error("expected project folder on disk")
|
||||
}
|
||||
|
||||
// 4. Verify template created Overview.md
|
||||
overviewPath := filepath.Join(serverFolder, "Overview.md")
|
||||
// 4. Verify template created Overview.md inside Notes/ subfolder
|
||||
overviewPath := filepath.Join(serverFolder, "Notes", "Overview.md")
|
||||
if _, err := os.Stat(overviewPath); os.IsNotExist(err) {
|
||||
t.Log("note: Overview.md from template not created (may not be implemented)")
|
||||
t.Errorf("expected Overview.md at %s", overviewPath)
|
||||
}
|
||||
// Verify no Overview.md in project root
|
||||
overviewRootPath := filepath.Join(serverFolder, "Overview.md")
|
||||
if _, err := os.Stat(overviewRootPath); err == nil {
|
||||
t.Error("Overview.md should not be in project root, only in Notes/")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultLayout_CreateNoteInsideProject(t *testing.T) {
|
||||
app, vault := setupTestApp(t)
|
||||
@@ -150,16 +155,14 @@ func TestVaultLayout_CreateNoteInsideProject(t *testing.T) {
|
||||
t.Fatal("expected non-nil node and file record")
|
||||
}
|
||||
|
||||
// Verify the note .md file is inside the project folder
|
||||
expectedPath := filepath.Join(vault, proj.FsPath, "Моя заметка.md")
|
||||
// Verify the note .md file is inside the project's notes/ subfolder
|
||||
// SafeDisplayNameToPathSegment preserves spaces: "Моя заметка" stays as-is
|
||||
expectedPath := filepath.Join(vault, proj.FsPath, "Notes", "Моя заметка.md")
|
||||
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
||||
// Try the safe-display-name variant
|
||||
expectedPath2 := filepath.Join(vault, proj.FsPath, "Моя_заметка.md")
|
||||
if _, err2 := os.Stat(expectedPath2); os.IsNotExist(err2) {
|
||||
// Show what actually exists
|
||||
entries, _ := os.ReadDir(filepath.Join(vault, proj.FsPath))
|
||||
t.Errorf("expected note file in project folder, found: %v", listNames(entries))
|
||||
}
|
||||
// Show what actually exists in notes subfolder
|
||||
notesDir := filepath.Join(vault, proj.FsPath, "Notes")
|
||||
entries, _ := os.ReadDir(notesDir)
|
||||
t.Errorf("expected note file at %s, found in notes/: %v", expectedPath, listNames(entries))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user