diff --git a/cmd/verstak-gui/bindings_settings.go b/cmd/verstak-gui/bindings_settings.go index 3e691ee..54938d5 100644 --- a/cmd/verstak-gui/bindings_settings.go +++ b/cmd/verstak-gui/bindings_settings.go @@ -200,22 +200,15 @@ func (a *App) OpenFolder(nodeID string) error { if err != nil { return err } - var target string + + var fileRecordPath string if n.Type == nodes.TypeFile && n.FsPath == "" { records, _ := a.files.ListByNode(nodeID) if len(records) > 0 { - target = filepath.Join(a.vault, records[0].Path) - } - if target == "" { - target = a.vault - } - target = filepath.Dir(target) - } else { - target = filepath.Join(a.vault, n.FsPath) - if n.Type == nodes.TypeFile { - target = filepath.Dir(target) + fileRecordPath = records[0].Path } } + target := resolveOpenFolderTarget(a.vault, n, fileRecordPath) if _, err := os.Stat(target); os.IsNotExist(err) { target = a.vault } @@ -223,6 +216,20 @@ func (a *App) OpenFolder(nodeID string) error { return cmd.Run() } +func resolveOpenFolderTarget(vault string, n *nodes.Node, fileRecordPath string) string { + if n.Type == nodes.TypeFile && n.FsPath == "" { + if fileRecordPath == "" { + return vault + } + return filepath.Dir(filepath.Join(vault, fileRecordPath)) + } + target := filepath.Join(vault, n.FsPath) + if n.Type == nodes.TypeFile { + return filepath.Dir(target) + } + return target +} + func (a *App) OpenVaultFolder() error { if !a.IsReady() { return fmt.Errorf("vault not open") diff --git a/cmd/verstak-gui/file_manager_test.go b/cmd/verstak-gui/file_manager_test.go new file mode 100644 index 0000000..a24b0bd --- /dev/null +++ b/cmd/verstak-gui/file_manager_test.go @@ -0,0 +1,122 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "verstak/internal/core/nodes" +) + +func TestResolveOpenFolderTarget_FileNodeUsesRecordPath(t *testing.T) { + vault := t.TempDir() + + got := resolveOpenFolderTarget(vault, &nodes.Node{Type: nodes.TypeFile}, filepath.Join("Parent", "Nested", "file.txt")) + want := filepath.Join(vault, "Parent", "Nested") + if got != want { + t.Fatalf("target = %q, want %q", got, want) + } + + got = resolveOpenFolderTarget(vault, &nodes.Node{Type: nodes.TypeFile}, "") + if got != vault { + t.Fatalf("target without file record = %q, want vault %q", got, vault) + } + + got = resolveOpenFolderTarget(vault, &nodes.Node{Type: nodes.TypeFile, FsPath: filepath.Join("Parent", "file.txt")}, "") + want = filepath.Join(vault, "Parent") + if got != want { + t.Fatalf("target with fs_path = %q, want %q", got, want) + } +} + +func TestFileManagerRecursiveImportListItemsIsFlat(t *testing.T) { + app, _ := setupTestApp(t) + + parent, err := app.CreateNodeFromTemplate("", "Files Parent", "folder.default") + if err != nil { + t.Fatalf("create parent: %v", err) + } + + sourceRoot := filepath.Join(t.TempDir(), "drop") + if err := os.MkdirAll(filepath.Join(sourceRoot, "nested"), 0o750); err != nil { + t.Fatalf("mkdir source: %v", err) + } + if err := os.WriteFile(filepath.Join(sourceRoot, "root.txt"), []byte("root"), 0o640); err != nil { + t.Fatalf("write root file: %v", err) + } + if err := os.WriteFile(filepath.Join(sourceRoot, "nested", "deep.txt"), []byte("deep"), 0o640); err != nil { + t.Fatalf("write nested file: %v", err) + } + + imported, err := app.AddPathCopy(parent.ID, sourceRoot) + if err != nil { + t.Fatalf("AddPathCopy: %v", err) + } + if len(imported) < 4 { + t.Fatalf("imported %d nodes, want folder + nested folder + files", len(imported)) + } + + rootItems, err := app.ListItems(parent.ID) + if err != nil { + t.Fatalf("ListItems(parent): %v", err) + } + if hasItemNamed(rootItems, "deep.txt") { + t.Fatal("parent file view includes nested file deep.txt") + } + drop := findItem(rootItems, "drop", nodes.TypeFolder) + if drop == nil { + t.Fatalf("parent file view missing imported root folder: %#v", rootItems) + } + + dropItems, err := app.ListItems(drop.ID) + if err != nil { + t.Fatalf("ListItems(drop): %v", err) + } + if !hasItemNamed(dropItems, "root.txt") { + t.Fatal("imported root folder missing root.txt") + } + nested := findItem(dropItems, "nested", nodes.TypeFolder) + if nested == nil { + t.Fatalf("imported root folder missing nested folder: %#v", dropItems) + } + + nestedItems, err := app.ListItems(nested.ID) + if err != nil { + t.Fatalf("ListItems(nested): %v", err) + } + if !hasItemNamed(nestedItems, "deep.txt") { + t.Fatal("nested folder missing deep.txt") + } + + seen := map[string]string{} + for level, items := range map[string][]FileTreeItemDTO{ + "parent": rootItems, + "drop": dropItems, + "nested": nestedItems, + } { + for _, item := range items { + if prev, ok := seen[item.ID]; ok { + t.Fatalf("file manager listed ID %s in both %s and %s", item.ID, prev, level) + } + seen[item.ID] = level + } + } +} + +func findItem(items []FileTreeItemDTO, name, typ string) *FileTreeItemDTO { + for i := range items { + if items[i].Name == name && items[i].Type == typ { + return &items[i] + } + } + return nil +} + +func hasItemNamed(items []FileTreeItemDTO, name string) bool { + for _, item := range items { + if item.Name == name { + return true + } + } + return false +}