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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user