Files tab: .md → note editor via CheckFileAction + frontend

Backend:
- FindByFileID: notes+files JOIN query
- LinkFile: INSERT OR IGNORE notes record
- CheckFileAction binding: note/preview/external/auto-link

Frontend (App.svelte):
- Import isMarkdownFile from fileUtils
- openPreview now calls CheckFileAction for .md files
- .md+note → switch to Notes tab + note editor
- .md outside Notes/ → inline preview
- non-.md → unchanged

Tests: 7 new (FindByFileID×3, CheckFileAction×4), all PASS
This commit is contained in:
2026-06-15 18:42:37 +08:00
parent fec35f55b8
commit bfe57ac0ac
8 changed files with 433 additions and 23 deletions
+36
View File
@@ -122,6 +122,11 @@ func NewService(db *storage.DB, vaultRoot string, nodeRepo *nodes.Repository, fi
return &Service{db: db, vaultRoot: vaultRoot, nodes: nodeRepo, files: fileSvc}
}
// DB returns the underlying storage.DB for direct queries (used in tests).
func (s *Service) DB() *storage.DB {
return s.db
}
// FindNotesFolder returns the TypeFolder "Notes" node under parentID, or nil.
func (s *Service) FindNotesFolder(parentID string) *nodes.Node {
children, err := s.nodes.ListChildren(parentID, false)
@@ -683,6 +688,37 @@ func (s *Service) repairNoteFilePath(noteID, caseID string, res *RepairResult) e
return nil
}
// FindByFileID looks up the note record associated with the given file ID.
// LinkFile creates a notes record linking an existing node to an existing file record.
// It is a no-op if a record for node_id already exists (idempotent).
func (s *Service) LinkFile(nodeID, fileID, format string) error {
if format == "" {
format = "markdown"
}
_, err := s.db.Exec(
`INSERT OR IGNORE INTO notes (node_id, file_id, format) VALUES (?,?,?)`,
nodeID, fileID, format,
)
return err
}
func (s *Service) FindByFileID(fileID string) (*Record, error) {
row := s.db.QueryRow(
`SELECT node_id, file_id, format, encrypted FROM notes WHERE file_id=?`, fileID,
)
var rec Record
var enc int
if err := row.Scan(&rec.NodeID, &rec.FileID, &rec.Format, &enc); err != nil {
return nil, err
}
rec.Encrypted = enc != 0
return &rec, nil
}
// fileExists returns true if path refers to an existing file or directory.
func fileExists(path string) bool {
_, err := os.Stat(path)
+92
View File
@@ -1431,3 +1431,95 @@ func TestFilesTabManualMoveRepair(t *testing.T) {
t.Error("Files tab should show Overview note inside Notes folder after repair")
}
}
func TestFindByFileID_Success(t *testing.T) {
svc, nodeRepo, _, vaultDir := setupRepairTest(t)
caseNode := createCaseNode(t, nodeRepo, "TestCase", "test_case")
// Create a note via the full Create path...
noteNode, fileRec, err := svc.Create(caseNode.ID, "TestNote", "")
if err != nil {
t.Fatalf("create note: %v", err)
}
// Verify FindByFileID works
rec, err := svc.FindByFileID(fileRec.ID)
if err != nil {
t.Fatalf("FindByFileID: %v", err)
}
if rec == nil {
t.Fatal("expected non-nil record")
}
if rec.NodeID != noteNode.ID {
t.Errorf("expected NodeID=%s, got %s", noteNode.ID, rec.NodeID)
}
if rec.FileID != fileRec.ID {
t.Errorf("expected FileID=%s, got %s", fileRec.ID, rec.FileID)
}
if rec.Format != "markdown" {
t.Errorf("expected Format=markdown, got %s", rec.Format)
}
_ = vaultDir
}
func TestFindByFileID_NotFound(t *testing.T) {
svc, _, _, _ := setupRepairTest(t)
rec, err := svc.FindByFileID("nonexistent-file-id")
if err == nil {
t.Error("expected error for nonexistent file ID")
}
if rec != nil {
t.Error("expected nil record")
}
}
func TestFindByFileID_AfterLinkFile(t *testing.T) {
svc, nodeRepo, _, vaultDir := setupRepairTest(t)
caseNode, err := nodeRepo.Create(nil, nodes.TypeCase, "TestCase", 0, "", "test_case_link")
if err != nil {
t.Fatalf("create case node: %v", err)
}
// Create directory on disk
caseDir := filepath.Join(vaultDir, "test_case_link")
if err := os.MkdirAll(caseDir, 0o750); err != nil {
t.Fatalf("mkdir: %v", err)
}
// Write a physical .md file
mdPath := filepath.Join(caseDir, "mynote.md")
if err := os.WriteFile(mdPath, []byte("# mynote"), 0o640); err != nil {
t.Fatalf("write file: %v", err)
}
// Create a file node
fileNode, err := nodeRepo.Create(&caseNode.ID, nodes.TypeFile, "mynote.md", 0, "", "test_case_link/mynote.md")
if err != nil {
t.Fatalf("create file node: %v", err)
}
// Insert file record directly (insertRecord is private, so we use DB())
db := svc.DB()
if db == nil {
t.Fatal("DB() returned nil")
}
_, err = db.Exec(
`INSERT INTO files (id,node_id,filename,path,storage_mode,size,sha256,mime,created_at,updated_at,missing)
VALUES (?,?,?,?,'vault',0,'','text/markdown','2024-01-01T00:00:00Z','2024-01-01T00:00:00Z',0)`,
"file-"+fileNode.ID, fileNode.ID, "mynote.md", "test_case_link/mynote.md")
if err != nil {
t.Fatalf("insert file record: %v", err)
}
// Link note record via LinkFile
if err := svc.LinkFile(fileNode.ID, "file-"+fileNode.ID, "markdown"); err != nil {
t.Fatalf("LinkFile: %v", err)
}
// Verify
rec, err := svc.FindByFileID("file-" + fileNode.ID)
if err != nil {
t.Fatalf("FindByFileID: %v", err)
}
if rec == nil {
t.Fatal("expected non-nil record")
}
if rec.NodeID != fileNode.ID {
t.Errorf("expected NodeID=%s, got %s", fileNode.ID, rec.NodeID)
}
_ = vaultDir
}