fix: flat vault type resolution — remove type/ prefix from links and protected folders
- Type relationship links: [[type/essay]] → [[essay]] (matches flat vault structure) - PROTECTED_FOLDERS / KEEP_FOLDERS: only attachments, _themes, assets - TypeSelector navigation: type/slug → slug - Updated all 14 affected test files - All 612 Rust tests + 2151 frontend tests pass
This commit is contained in:
21
demo-vault-v2/default-theme.md
Normal file
21
demo-vault-v2/default-theme.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
type: Theme
|
||||
title: Default Theme
|
||||
primary: "#155DFF"
|
||||
background: "#1a1a2e"
|
||||
foreground: "#37352F"
|
||||
sidebar: "#2a2a3e"
|
||||
border: "#E9E9E7"
|
||||
muted: "#F0F0EF"
|
||||
muted-foreground: "#9B9A97"
|
||||
accent: "#F0F7FF"
|
||||
accent-foreground: "#0A3B8F"
|
||||
font-family: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif"
|
||||
font-size-base: 14
|
||||
line-height-base: 1.6
|
||||
---
|
||||
# Default Theme
|
||||
|
||||
Light theme with warm, paper-like tones.
|
||||
|
||||
[[Ma
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
Is A: Theme
|
||||
Description: Light theme with warm, paper-like tones
|
||||
background: "#FFFFFF"
|
||||
foreground: "#37352F"
|
||||
card: "#FFFFFF"
|
||||
popover: "#FFFFFF"
|
||||
primary: "#155DFF"
|
||||
primary-foreground: "#FFFFFF"
|
||||
secondary: "#EBEBEA"
|
||||
secondary-foreground: "#37352F"
|
||||
muted: "#F0F0EF"
|
||||
muted-foreground: "#787774"
|
||||
accent: "#EBEBEA"
|
||||
accent-foreground: "#37352F"
|
||||
destructive: "#E03E3E"
|
||||
border: "#E9E9E7"
|
||||
input: "#E9E9E7"
|
||||
ring: "#155DFF"
|
||||
sidebar: "#F7F6F3"
|
||||
sidebar-foreground: "#37352F"
|
||||
sidebar-border: "#E9E9E7"
|
||||
sidebar-accent: "#EBEBEA"
|
||||
text-primary: "#37352F"
|
||||
text-secondary: "#787774"
|
||||
text-muted: "#B4B4B4"
|
||||
text-heading: "#37352F"
|
||||
bg-primary: "#FFFFFF"
|
||||
bg-sidebar: "#F7F6F3"
|
||||
bg-hover: "#EBEBEA"
|
||||
bg-hover-subtle: "#F0F0EF"
|
||||
bg-selected: "#E8F4FE"
|
||||
border-primary: "#E9E9E7"
|
||||
accent-blue: "#155DFF"
|
||||
accent-green: "#00B38B"
|
||||
accent-orange: "#D9730D"
|
||||
accent-red: "#E03E3E"
|
||||
accent-purple: "#A932FF"
|
||||
accent-yellow: "#F0B100"
|
||||
accent-blue-light: "#155DFF14"
|
||||
accent-green-light: "#00B38B14"
|
||||
accent-purple-light: "#A932FF14"
|
||||
accent-red-light: "#E03E3E14"
|
||||
accent-yellow-light: "#F0B10014"
|
||||
font-family: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif"
|
||||
font-size-base: 14px
|
||||
editor-font-size: 16
|
||||
editor-line-height: 1.5
|
||||
editor-max-width: 720
|
||||
---
|
||||
|
||||
# Default Theme
|
||||
|
||||
The default light theme for Laputa. Clean and warm, inspired by Notion.
|
||||
8
demo-vault-v2/enter-rename-test.md
Normal file
8
demo-vault-v2/enter-rename-test.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Untitled note 4
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
|
||||
# Enter Rename Test
|
||||
|
||||
10
demo-vault-v2/final-renamed.md
Normal file
10
demo-vault-v2/final-renamed.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Final Renamed
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Final Renamed
|
||||
|
||||
Title
|
||||
|
||||
Snippet test content 1773604558993
|
||||
12
demo-vault-v2/final-title-r-renamed.md
Normal file
12
demo-vault-v2/final-title-r-renamed.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Final Title R Renamed
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Final Title R Renamed
|
||||
|
||||
Snippet test content 1773607529878enamed
|
||||
|
||||
Snippet test content 1773607534349
|
||||
|
||||
[[Ma
|
||||
8
demo-vault-v2/final-title-r.md
Normal file
8
demo-vault-v2/final-title-r.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Final Title R
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Final Title R
|
||||
|
||||
[[Maenamed
|
||||
12
demo-vault-v2/final-title-renamed-renamed.md
Normal file
12
demo-vault-v2/final-title-renamed-renamed.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Final Title Renamed Renamed
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Final Title Renamed Renamed
|
||||
|
||||
[[MaSnippet test content 1773614740678
|
||||
|
||||
Snippet test content 1773614745082
|
||||
|
||||
[[Ma
|
||||
8
demo-vault-v2/my-renamed-note.md
Normal file
8
demo-vault-v2/my-renamed-note.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Untitled note
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
|
||||
# My Renamed Note
|
||||
|
||||
10
demo-vault-v2/note/final-title-r-renamed.md
Normal file
10
demo-vault-v2/note/final-title-r-renamed.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Final Title R Renamed
|
||||
type: Note
|
||||
status: Active
|
||||
Trashed: true
|
||||
Trashed at: '2026-03-13'
|
||||
---
|
||||
# Final Title R Renamed
|
||||
|
||||
[[Maenamed
|
||||
8
demo-vault-v2/note/final-title-r.md
Normal file
8
demo-vault-v2/note/final-title-r.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Final Title R
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Final Title R
|
||||
|
||||
[[Maenamed
|
||||
10
demo-vault-v2/note/final-title-renamed-renamed.md
Normal file
10
demo-vault-v2/note/final-title-renamed-renamed.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Final Title Renamed
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Final Title Renamed Renamed
|
||||
|
||||
Snippet test content 1773606979013
|
||||
|
||||
Snippet test content 1773607012674
|
||||
8
demo-vault-v2/note/untitled-save-no-rename-te.md
Normal file
8
demo-vault-v2/note/untitled-save-no-rename-te.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Untitled Save No Rename Te
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Untitled Save No Rename Te
|
||||
|
||||
[[Mast 2
|
||||
6
demo-vault-v2/note/untitled-save-no-rename-test-2.md
Normal file
6
demo-vault-v2/note/untitled-save-no-rename-test-2.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Untitled Save No Rename Test 2
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Untitled Save No Rename Test 2
|
||||
6
demo-vault-v2/note/untitled-save-no-rename-test.md
Normal file
6
demo-vault-v2/note/untitled-save-no-rename-test.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Untitled Save No Rename Test
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Untitled Save No Rename Test
|
||||
10
demo-vault-v2/note/untitled-tes-renamed.md
Normal file
10
demo-vault-v2/note/untitled-tes-renamed.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Untitled Tes Renamed
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Untitled Tes Renamed
|
||||
|
||||
[[Mat Note ABC
|
||||
|
||||
[[Ma
|
||||
6
demo-vault-v2/note/untitled-test-note-abc-2.md
Normal file
6
demo-vault-v2/note/untitled-test-note-abc-2.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Untitled Test Note ABC 2
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Untitled Test Note ABC 2
|
||||
6
demo-vault-v2/note/untitled-test-note-abc-3.md
Normal file
6
demo-vault-v2/note/untitled-test-note-abc-3.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Untitled Test Note ABC 3
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Untitled Test Note ABC 3
|
||||
10
demo-vault-v2/note/untitled-test-note-abc-renamed.md
Normal file
10
demo-vault-v2/note/untitled-test-note-abc-renamed.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Untitled Test Note ABC Renamed
|
||||
type: Note
|
||||
status: Active
|
||||
Trashed: true
|
||||
Trashed at: '2026-03-13'
|
||||
---
|
||||
# Untitled Test Note ABC Renamed
|
||||
|
||||
[[Ma
|
||||
6
demo-vault-v2/save-no-rename-test.md
Normal file
6
demo-vault-v2/save-no-rename-test.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Save No Rename Test
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Save No Rename Test
|
||||
8
demo-vault-v2/should-be-r-renamed.md
Normal file
8
demo-vault-v2/should-be-r-renamed.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Should Be R
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Should Be R Renamed
|
||||
|
||||
Snippet test content 1773606307602everted
|
||||
8
demo-vault-v2/test-note-a.md
Normal file
8
demo-vault-v2/test-note-a.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Test Note A
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Test Note A
|
||||
|
||||
[[MaBC
|
||||
6
demo-vault-v2/test-note-abc.md
Normal file
6
demo-vault-v2/test-note-abc.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Test Note ABC
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Test Note ABC
|
||||
20
demo-vault-v2/theme/default-theme.md
Normal file
20
demo-vault-v2/theme/default-theme.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
type: Theme
|
||||
title: Default
|
||||
primary: "#155DFF"
|
||||
background: "#1a1a2e"
|
||||
foreground: "#37352F"
|
||||
sidebar: "#2a2a3e"
|
||||
border: "#E9E9E7"
|
||||
muted: "#F0F0EF"
|
||||
muted-foreground: "#9B9A97"
|
||||
accent: "#F0F7FF"
|
||||
accent-foreground: "#0A3B8F"
|
||||
font-family: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif"
|
||||
font-size-base: 14
|
||||
line-height-base: 1.6
|
||||
---
|
||||
|
||||
# Default Theme
|
||||
|
||||
Light theme with warm, paper-like tones.
|
||||
22
demo-vault-v2/untitled-experiment-2.md
Normal file
22
demo-vault-v2/untitled-experiment-2.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: Untitled experiment 4
|
||||
type: Experiment
|
||||
status: Active
|
||||
---
|
||||
|
||||
# Untitled experiment 2
|
||||
|
||||
## Hypothesis
|
||||
|
||||
|
||||
|
||||
## Method
|
||||
|
||||
|
||||
|
||||
## Results
|
||||
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
16
demo-vault-v2/untitled-experiment.md
Normal file
16
demo-vault-v2/untitled-experiment.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: Untitled experiment
|
||||
type: Experiment
|
||||
status: Active
|
||||
---
|
||||
# Untitled experiment
|
||||
|
||||
## Hypothesis
|
||||
|
||||
## Method
|
||||
|
||||
[[Ma
|
||||
|
||||
## Results
|
||||
|
||||
## Conclusion
|
||||
8
demo-vault-v2/untitled-not.md
Normal file
8
demo-vault-v2/untitled-not.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Untitled not
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Untitled not
|
||||
|
||||
[[Mae
|
||||
6
demo-vault-v2/untitled-save-no-rename-test.md
Normal file
6
demo-vault-v2/untitled-save-no-rename-test.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Untitled Save No Rename Test
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Untitled Save No Rename Test
|
||||
6
demo-vault-v2/untitled-test-note-abc-2-renamed.md
Normal file
6
demo-vault-v2/untitled-test-note-abc-2-renamed.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Untitled Test Note ABC 2
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Untitled Test Note ABC 2 Renamed
|
||||
6
demo-vault-v2/untitled-test-note-abc-2.md
Normal file
6
demo-vault-v2/untitled-test-note-abc-2.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Untitled Test Note ABC 2
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Untitled Test Note ABC 2
|
||||
6
demo-vault-v2/untitled-test-note-abc.md
Normal file
6
demo-vault-v2/untitled-test-note-abc.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Untitled Test Note ABC
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
# Untitled Test Note ABC
|
||||
@@ -554,7 +554,7 @@ pub fn repair_vault(vault_path: String) -> Result<String, String> {
|
||||
let vault_path = expand_tilde(&vault_path);
|
||||
// Repair themes
|
||||
theme::restore_default_themes(&vault_path)?;
|
||||
// Repair config files (config/agents.md, type/config.md, AGENTS.md stub)
|
||||
// Repair config files (config/agents.md, config.md type def, AGENTS.md stub)
|
||||
vault::repair_config_files(&vault_path)?;
|
||||
// Ensure .gitignore with sensible defaults exists
|
||||
git::ensure_gitignore(&vault_path)?;
|
||||
|
||||
@@ -53,7 +53,7 @@ fn run_startup_tasks() {
|
||||
theme::seed_default_themes(vp_str);
|
||||
// Seed theme/ with built-in vault theme notes if missing
|
||||
theme::seed_vault_themes(vp_str);
|
||||
// Seed type/theme.md so the Theme type has an icon in the sidebar
|
||||
// Seed theme.md type definition so the Theme type has an icon in the sidebar
|
||||
let _ = theme::ensure_theme_type_definition(vp_str);
|
||||
|
||||
// Migrate root AGENTS.md → config/agents.md (one-time, idempotent)
|
||||
|
||||
@@ -107,17 +107,16 @@ pub fn restore_default_themes(vault_path: &str) -> Result<String, String> {
|
||||
// Seed theme/ markdown notes (reuses ensure_vault_themes for consistency)
|
||||
ensure_vault_themes(vault_path)?;
|
||||
|
||||
// Seed type/theme.md so the Theme type has an icon and label in the sidebar
|
||||
// Seed theme.md type definition so the Theme type has an icon and label in the sidebar
|
||||
ensure_theme_type_definition(vault_path)?;
|
||||
|
||||
Ok("Default themes restored".to_string())
|
||||
}
|
||||
|
||||
/// Create `type/theme.md` if it doesn't exist (gives the Theme type a sidebar icon/color).
|
||||
/// Create `theme.md` at vault root if it doesn't exist (gives the Theme type a sidebar icon/color).
|
||||
pub fn ensure_theme_type_definition(vault_path: &str) -> Result<(), String> {
|
||||
let type_dir = Path::new(vault_path).join("type");
|
||||
fs::create_dir_all(&type_dir).map_err(|e| format!("Failed to create type directory: {e}"))?;
|
||||
write_if_missing(&type_dir.join("theme.md"), THEME_TYPE_DEFINITION)?;
|
||||
let vault = Path::new(vault_path);
|
||||
write_if_missing(&vault.join("theme.md"), THEME_TYPE_DEFINITION)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -259,10 +258,10 @@ mod tests {
|
||||
assert!(vault.join("theme").join("dark.md").exists());
|
||||
assert!(vault.join("theme").join("minimal.md").exists());
|
||||
assert!(
|
||||
vault.join("type").join("theme.md").exists(),
|
||||
"restore must create type/theme.md"
|
||||
vault.join("theme.md").exists(),
|
||||
"restore must create theme.md"
|
||||
);
|
||||
let type_content = fs::read_to_string(vault.join("type").join("theme.md")).unwrap();
|
||||
let type_content = fs::read_to_string(vault.join("theme.md")).unwrap();
|
||||
assert!(type_content.contains("type: Type"));
|
||||
assert!(type_content.contains("icon: palette"));
|
||||
}
|
||||
@@ -275,7 +274,7 @@ mod tests {
|
||||
let vp = vault.to_str().unwrap();
|
||||
|
||||
ensure_theme_type_definition(vp).unwrap();
|
||||
let path = vault.join("type").join("theme.md");
|
||||
let path = vault.join("theme.md");
|
||||
assert!(path.exists());
|
||||
let content = fs::read_to_string(&path).unwrap();
|
||||
assert!(content.contains("type: Type"));
|
||||
@@ -286,14 +285,13 @@ mod tests {
|
||||
fn test_ensure_theme_type_definition_is_idempotent() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let vault = dir.path().join("vault");
|
||||
let type_dir = vault.join("type");
|
||||
fs::create_dir_all(&type_dir).unwrap();
|
||||
fs::create_dir_all(&vault).unwrap();
|
||||
let custom = "---\ntype: Type\nicon: swatches\ncolor: green\n---\n# Theme\n";
|
||||
fs::write(type_dir.join("theme.md"), custom).unwrap();
|
||||
fs::write(vault.join("theme.md"), custom).unwrap();
|
||||
let vp = vault.to_str().unwrap();
|
||||
|
||||
ensure_theme_type_definition(vp).unwrap();
|
||||
let content = fs::read_to_string(type_dir.join("theme.md")).unwrap();
|
||||
let content = fs::read_to_string(vault.join("theme.md")).unwrap();
|
||||
assert!(
|
||||
content.contains("swatches"),
|
||||
"existing content must be preserved"
|
||||
|
||||
@@ -623,7 +623,7 @@ mod tests {
|
||||
let vault = dir.path();
|
||||
|
||||
// Commit a type note without sidebar label
|
||||
create_test_file(vault, "type/news.md", "---\ntype: Type\n---\n# News\n");
|
||||
create_test_file(vault, "news.md", "---\ntype: Type\n---\n# News\n");
|
||||
git_add_commit(vault, "init");
|
||||
|
||||
// Prime the cache (same commit hash)
|
||||
@@ -634,7 +634,7 @@ mod tests {
|
||||
// User edits the type note to add sidebar label (uncommitted)
|
||||
create_test_file(
|
||||
vault,
|
||||
"type/news.md",
|
||||
"news.md",
|
||||
"---\ntype: Type\nsidebar label: News\n---\n# News\n",
|
||||
);
|
||||
|
||||
@@ -683,15 +683,15 @@ mod tests {
|
||||
let entries = scan_vault_cached(vault).unwrap();
|
||||
assert_eq!(entries.len(), 1);
|
||||
|
||||
// Create files in a new subdirectory (simulates restore_default_themes)
|
||||
// Create files in a new protected subdirectory (simulates asset creation)
|
||||
create_test_file(
|
||||
vault,
|
||||
"theme/default.md",
|
||||
"assets/default.md",
|
||||
"---\nIs A: Theme\n---\n# Default Theme\n",
|
||||
);
|
||||
create_test_file(
|
||||
vault,
|
||||
"theme/dark.md",
|
||||
"assets/dark.md",
|
||||
"---\nIs A: Theme\n---\n# Dark Theme\n",
|
||||
);
|
||||
|
||||
@@ -716,7 +716,7 @@ mod tests {
|
||||
// Commit a type note with visible: false
|
||||
create_test_file(
|
||||
vault,
|
||||
"type/topic.md",
|
||||
"topic.md",
|
||||
"---\ntype: Type\nvisible: false\n---\n# Topic\n",
|
||||
);
|
||||
git_add_commit(vault, "init");
|
||||
@@ -731,7 +731,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// User removes visible field (uncommitted edit)
|
||||
create_test_file(vault, "type/topic.md", "---\ntype: Type\n---\n# Topic\n");
|
||||
create_test_file(vault, "topic.md", "---\ntype: Type\n---\n# Topic\n");
|
||||
|
||||
// Reload — must reflect the removal (visible defaults to None)
|
||||
let entries2 = scan_vault_cached(vault).unwrap();
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::path::Path;
|
||||
|
||||
use super::getting_started::AGENTS_MD;
|
||||
|
||||
/// Content for `type/config.md` — gives the Config type a sidebar icon and label.
|
||||
/// Content for `config.md` — gives the Config type a sidebar icon and label.
|
||||
const CONFIG_TYPE_DEFINITION: &str = "\
|
||||
---
|
||||
Is A: Type
|
||||
@@ -26,7 +26,7 @@ See config/agents.md for vault instructions.
|
||||
";
|
||||
|
||||
/// Seed `config/agents.md` if missing or empty (idempotent, per-file).
|
||||
/// Also seeds `type/config.md` for sidebar visibility.
|
||||
/// Also seeds `config.md` type definition for sidebar visibility.
|
||||
pub fn seed_config_files(vault_path: &str) {
|
||||
let vault = Path::new(vault_path);
|
||||
let config_dir = vault.join("config");
|
||||
@@ -45,13 +45,9 @@ pub fn seed_config_files(vault_path: &str) {
|
||||
ensure_config_type_definition(vault_path);
|
||||
}
|
||||
|
||||
/// Ensure `type/config.md` exists (gives Config type a sidebar icon/color).
|
||||
/// Ensure `config.md` exists at vault root (gives Config type a sidebar icon/color).
|
||||
fn ensure_config_type_definition(vault_path: &str) {
|
||||
let type_dir = Path::new(vault_path).join("type");
|
||||
if fs::create_dir_all(&type_dir).is_err() {
|
||||
return;
|
||||
}
|
||||
let path = type_dir.join("config.md");
|
||||
let path = Path::new(vault_path).join("config.md");
|
||||
let needs_write = !path.exists() || fs::metadata(&path).map_or(true, |m| m.len() == 0);
|
||||
if needs_write {
|
||||
let _ = fs::write(&path, CONFIG_TYPE_DEFINITION);
|
||||
@@ -99,7 +95,7 @@ pub fn migrate_agents_md(vault_path: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Repair config files: re-create missing `config/agents.md` and `type/config.md`.
|
||||
/// Repair config files: re-create missing `config/agents.md` and `config.md` type definition.
|
||||
/// Called by the "Repair Vault" command. Returns a status message.
|
||||
pub fn repair_config_files(vault_path: &str) -> Result<String, String> {
|
||||
let vault = Path::new(vault_path);
|
||||
@@ -136,15 +132,13 @@ pub fn repair_config_files(vault_path: &str) -> Result<String, String> {
|
||||
.map_err(|e| format!("Failed to write config/agents.md: {e}"))?;
|
||||
}
|
||||
|
||||
// Step 3: Ensure type/config.md
|
||||
let type_dir = vault.join("type");
|
||||
fs::create_dir_all(&type_dir).map_err(|e| format!("Failed to create type directory: {e}"))?;
|
||||
let config_type_path = type_dir.join("config.md");
|
||||
// Step 3: Ensure config.md type definition at vault root
|
||||
let config_type_path = vault.join("config.md");
|
||||
let type_needs_write = !config_type_path.exists()
|
||||
|| fs::metadata(&config_type_path).map_or(true, |m| m.len() == 0);
|
||||
if type_needs_write {
|
||||
fs::write(&config_type_path, CONFIG_TYPE_DEFINITION)
|
||||
.map_err(|e| format!("Failed to write type/config.md: {e}"))?;
|
||||
.map_err(|e| format!("Failed to write config.md: {e}"))?;
|
||||
}
|
||||
|
||||
// Step 4: Ensure root AGENTS.md stub exists
|
||||
@@ -186,8 +180,8 @@ mod tests {
|
||||
|
||||
seed_config_files(vault.to_str().unwrap());
|
||||
|
||||
assert!(vault.join("type/config.md").exists());
|
||||
let content = fs::read_to_string(vault.join("type/config.md")).unwrap();
|
||||
assert!(vault.join("config.md").exists());
|
||||
let content = fs::read_to_string(vault.join("config.md")).unwrap();
|
||||
assert!(content.contains("Is A: Type"));
|
||||
assert!(content.contains("icon: gear-six"));
|
||||
}
|
||||
@@ -301,7 +295,7 @@ mod tests {
|
||||
assert_eq!(msg, "Config files repaired");
|
||||
|
||||
assert!(vault.join("config/agents.md").exists());
|
||||
assert!(vault.join("type/config.md").exists());
|
||||
assert!(vault.join("config.md").exists());
|
||||
assert!(vault.join("AGENTS.md").exists());
|
||||
|
||||
let agents = fs::read_to_string(vault.join("config/agents.md")).unwrap();
|
||||
|
||||
@@ -118,27 +118,27 @@ Available colors: red, purple, blue, green, yellow, orange. Icons are Phosphor n
|
||||
|
||||
const SAMPLE_FILES: &[SampleFile] = &[
|
||||
SampleFile {
|
||||
rel_path: "type/project.md",
|
||||
rel_path: "project.md",
|
||||
content: "---\ntype: Type\nicon: rocket-launch\ncolor: purple\norder: 1\n---\n\n# Project\n\nA Project is a time-bounded effort with a clear goal and an eventual completion date. Projects belong to a quarter or area and advance specific goals.\n",
|
||||
},
|
||||
SampleFile {
|
||||
rel_path: "type/note.md",
|
||||
rel_path: "note.md",
|
||||
content: "---\ntype: Type\nicon: note\ncolor: blue\norder: 2\n---\n\n# Note\n\nA Note is a general-purpose document — research notes, meeting notes, strategy docs, or anything that doesn't fit a more specific type.\n",
|
||||
},
|
||||
SampleFile {
|
||||
rel_path: "type/person.md",
|
||||
rel_path: "person.md",
|
||||
content: "---\ntype: Type\nicon: user\ncolor: green\norder: 3\n---\n\n# Person\n\nA Person represents someone you interact with — a colleague, friend, mentor, or collaborator.\n",
|
||||
},
|
||||
SampleFile {
|
||||
rel_path: "type/topic.md",
|
||||
rel_path: "topic.md",
|
||||
content: "---\ntype: Type\nicon: tag\ncolor: yellow\norder: 4\n---\n\n# Topic\n\nA Topic is a subject area or interest category that groups related notes, projects, and people.\n",
|
||||
},
|
||||
SampleFile {
|
||||
rel_path: "type/theme.md",
|
||||
rel_path: "theme.md",
|
||||
content: "---\ntype: Type\nicon: palette\ncolor: purple\norder: 50\n---\n\n# Theme\n\nA visual theme for Laputa. Each theme defines CSS custom properties that control colors, typography, and spacing.\n",
|
||||
},
|
||||
SampleFile {
|
||||
rel_path: "type/config.md",
|
||||
rel_path: "config.md",
|
||||
content: "---\ntype: Type\nicon: gear-six\ncolor: gray\norder: 90\nsidebar label: Config\n---\n\n# Config\n\nVault configuration files. These control how AI agents, tools, and other integrations interact with this vault.\n",
|
||||
},
|
||||
SampleFile {
|
||||
@@ -485,11 +485,11 @@ mod tests {
|
||||
assert!(vault_path.join("sample-project.md").exists());
|
||||
assert!(vault_path.join("sample-collaborator.md").exists());
|
||||
assert!(vault_path.join("getting-started.md").exists());
|
||||
assert!(vault_path.join("type/project.md").exists());
|
||||
assert!(vault_path.join("type/note.md").exists());
|
||||
assert!(vault_path.join("type/person.md").exists());
|
||||
assert!(vault_path.join("type/topic.md").exists());
|
||||
assert!(vault_path.join("type/config.md").exists());
|
||||
assert!(vault_path.join("project.md").exists());
|
||||
assert!(vault_path.join("note.md").exists());
|
||||
assert!(vault_path.join("person.md").exists());
|
||||
assert!(vault_path.join("topic.md").exists());
|
||||
assert!(vault_path.join("config.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -544,8 +544,8 @@ mod tests {
|
||||
create_getting_started_vault(vault_path.to_str().unwrap()).unwrap();
|
||||
|
||||
let entries = crate::vault::scan_vault(&vault_path).unwrap();
|
||||
// SAMPLE_FILES + config/agents.md + AGENTS.md stub + 3 vault theme notes
|
||||
assert_eq!(entries.len(), SAMPLE_FILES.len() + 2 + 3);
|
||||
// SAMPLE_FILES (all at root) + AGENTS.md stub
|
||||
assert_eq!(entries.len(), SAMPLE_FILES.len() + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -641,7 +641,7 @@ mod tests {
|
||||
assert!(vault_path.join("theme/minimal.md").exists());
|
||||
|
||||
// Theme type definition
|
||||
assert!(vault_path.join("type/theme.md").exists());
|
||||
assert!(vault_path.join("theme.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -118,8 +118,8 @@ pub fn migrate_is_a_to_type(vault_path: &str) -> Result<usize, String> {
|
||||
Ok(migrated)
|
||||
}
|
||||
|
||||
/// Folders that are NOT flattened — they contain definitions/config, not notes.
|
||||
const KEEP_FOLDERS: &[&str] = &["type", "config", "attachments", "_themes", "theme"];
|
||||
/// Folders that are NOT flattened — they contain assets/themes, not notes.
|
||||
const KEEP_FOLDERS: &[&str] = &["attachments", "_themes", "assets"];
|
||||
|
||||
/// Determine a unique filename at `dest_dir`, appending -2, -3, etc. on collision.
|
||||
fn unique_filename(dest_dir: &Path, filename: &str, taken: &HashSet<String>) -> String {
|
||||
@@ -589,26 +589,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flatten_vault_skips_type_and_config() {
|
||||
fn test_flatten_vault_skips_protected_folders() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let vault = tmp.path();
|
||||
write_nested_file(
|
||||
vault,
|
||||
"type/project.md",
|
||||
"---\ntype: Type\n---\n# Project\n",
|
||||
);
|
||||
write_nested_file(
|
||||
vault,
|
||||
"config/agents.md",
|
||||
"---\ntype: Config\n---\n# Agents\n",
|
||||
);
|
||||
write_nested_file(vault, "attachments/image.md", "# Image note\n");
|
||||
write_nested_file(vault, "_themes/legacy.md", "---\n---\n# Legacy\n");
|
||||
write_nested_file(vault, "note/hello.md", "---\ntype: Note\n---\n# Hello\n");
|
||||
|
||||
let count = flatten_vault(vault.to_str().unwrap()).unwrap();
|
||||
assert_eq!(count, 1);
|
||||
assert!(vault.join("hello.md").exists());
|
||||
assert!(vault.join("type/project.md").exists());
|
||||
assert!(vault.join("config/agents.md").exists());
|
||||
assert!(vault.join("attachments/image.md").exists());
|
||||
assert!(vault.join("_themes/legacy.md").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -721,16 +713,12 @@ mod tests {
|
||||
let tmp = tempdir().unwrap();
|
||||
let vault = tmp.path();
|
||||
write_file(vault, "root-note.md", "---\ntype: Note\n---\n# Root Note\n");
|
||||
write_file(vault, "project.md", "---\ntype: Type\n---\n# Project\n");
|
||||
write_nested_file(
|
||||
vault,
|
||||
"old-folder/stray.md",
|
||||
"---\ntype: Note\n---\n# Stray\n",
|
||||
);
|
||||
write_nested_file(
|
||||
vault,
|
||||
"type/project.md",
|
||||
"---\ntype: Type\n---\n# Project\n",
|
||||
);
|
||||
|
||||
let report = vault_health_check(vault.to_str().unwrap()).unwrap();
|
||||
assert_eq!(report.stray_files.len(), 1);
|
||||
@@ -742,11 +730,7 @@ mod tests {
|
||||
let tmp = tempdir().unwrap();
|
||||
let vault = tmp.path();
|
||||
write_file(vault, "my-note.md", "# My Note\n");
|
||||
write_nested_file(
|
||||
vault,
|
||||
"type/project.md",
|
||||
"---\ntype: Type\n---\n# Project\n",
|
||||
);
|
||||
write_file(vault, "project.md", "---\ntype: Type\n---\n# Project\n");
|
||||
|
||||
let report = vault_health_check(vault.to_str().unwrap()).unwrap();
|
||||
assert!(report.stray_files.is_empty());
|
||||
|
||||
@@ -394,11 +394,11 @@ pub fn parse_md_file(path: &Path) -> Result<VaultEntry, String> {
|
||||
// Skip for type documents themselves (isA == "Type") to avoid self-referential links.
|
||||
if let Some(ref type_name) = is_a {
|
||||
if type_name != "Type" {
|
||||
// If isA is already a wikilink (e.g. "[[type/project]]"), use it directly
|
||||
// If isA is already a wikilink (e.g. "[[project]]"), use it directly
|
||||
let type_link = if type_name.starts_with("[[") && type_name.ends_with("]]") {
|
||||
type_name.clone()
|
||||
} else {
|
||||
format!("[[type/{}]]", type_name.to_lowercase())
|
||||
format!("[[{}]]", type_name.to_lowercase())
|
||||
};
|
||||
relationships.insert("Type".to_string(), vec![type_link]);
|
||||
}
|
||||
@@ -519,9 +519,9 @@ pub fn save_note_content(path: &str, content: &str) -> Result<(), String> {
|
||||
}
|
||||
|
||||
/// Scan a directory recursively for .md files and return VaultEntry for each.
|
||||
/// Folders that are scanned recursively (type definitions, config, themes, attachments).
|
||||
/// All other subfolders are ignored — notes live flat at the vault root.
|
||||
const PROTECTED_FOLDERS: &[&str] = &["type", "config", "attachments", "_themes", "theme"];
|
||||
/// Folders that are scanned recursively (themes, attachments, assets).
|
||||
/// All other subfolders are ignored — notes and type definitions live flat at the vault root.
|
||||
const PROTECTED_FOLDERS: &[&str] = &["attachments", "_themes", "assets"];
|
||||
|
||||
pub fn scan_vault(vault_path: &Path) -> Result<Vec<VaultEntry>, String> {
|
||||
if !vault_path.exists() {
|
||||
@@ -700,14 +700,10 @@ mod tests {
|
||||
create_test_file(dir.path(), "root.md", "# Root Note\n");
|
||||
create_test_file(
|
||||
dir.path(),
|
||||
"type/project.md",
|
||||
"project.md",
|
||||
"---\ntype: Type\n---\n# Project\n",
|
||||
);
|
||||
create_test_file(
|
||||
dir.path(),
|
||||
"config/agents.md",
|
||||
"---\ntype: Config\n---\n# Agents\n",
|
||||
);
|
||||
create_test_file(dir.path(), "attachments/notes.md", "# Attachment note\n");
|
||||
create_test_file(dir.path(), "not-markdown.txt", "This should be ignored");
|
||||
|
||||
let entries = scan_vault(dir.path()).unwrap();
|
||||
@@ -716,7 +712,7 @@ mod tests {
|
||||
let filenames: Vec<&str> = entries.iter().map(|e| e.filename.as_str()).collect();
|
||||
assert!(filenames.contains(&"root.md"));
|
||||
assert!(filenames.contains(&"project.md"));
|
||||
assert!(filenames.contains(&"agents.md"));
|
||||
assert!(filenames.contains(&"notes.md"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -743,22 +739,12 @@ mod tests {
|
||||
fn test_scan_vault_includes_all_protected_folders() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
create_test_file(dir.path(), "root.md", "# Root\n");
|
||||
create_test_file(
|
||||
dir.path(),
|
||||
"type/event.md",
|
||||
"---\ntype: Type\n---\n# Event\n",
|
||||
);
|
||||
create_test_file(dir.path(), "config/ui.config.md", "---\n---\n# Config\n");
|
||||
create_test_file(
|
||||
dir.path(),
|
||||
"theme/default.md",
|
||||
"---\ntype: Theme\n---\n# Default\n",
|
||||
);
|
||||
create_test_file(dir.path(), "_themes/legacy.md", "---\n---\n# Legacy\n");
|
||||
create_test_file(dir.path(), "attachments/notes.md", "# Attachment note\n");
|
||||
create_test_file(dir.path(), "assets/image.md", "# Asset\n");
|
||||
|
||||
let entries = scan_vault(dir.path()).unwrap();
|
||||
assert_eq!(entries.len(), 6);
|
||||
assert_eq!(entries.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -872,7 +858,7 @@ Status: Active
|
||||
);
|
||||
assert_eq!(
|
||||
entry.relationships.get("Type").unwrap(),
|
||||
&vec!["[[type/responsibility]]".to_string()]
|
||||
&vec!["[[responsibility]]".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -924,7 +910,7 @@ Custom Field: just a plain string
|
||||
assert_eq!(entry.relationships.len(), 1);
|
||||
assert_eq!(
|
||||
entry.relationships.get("Type").unwrap(),
|
||||
&vec!["[[type/note]]".to_string()]
|
||||
&vec!["[[note]]".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1005,7 +991,7 @@ Context: "[[area/research]]"
|
||||
);
|
||||
}
|
||||
|
||||
const SKIP_KEYS_CONTENT: &str = "---\nIs A: \"[[type/project]]\"\nAliases:\n - \"[[alias/foo]]\"\nStatus: \"[[status/active]]\"\nCadence: \"[[cadence/weekly]]\"\nCreated at: \"[[time/2024-01-01]]\"\nCreated time: \"[[time/noon]]\"\nReal Relation: \"[[note/important]]\"\n---\n# Skip Keys Test\n";
|
||||
const SKIP_KEYS_CONTENT: &str = "---\nIs A: \"[[project]]\"\nAliases:\n - \"[[alias/foo]]\"\nStatus: \"[[status/active]]\"\nCadence: \"[[cadence/weekly]]\"\nCreated at: \"[[time/2024-01-01]]\"\nCreated time: \"[[time/noon]]\"\nReal Relation: \"[[note/important]]\"\n---\n# Skip Keys Test\n";
|
||||
|
||||
fn parse_skip_keys_rels() -> (HashMap<String, Vec<String>>, usize) {
|
||||
let dir = TempDir::new().unwrap();
|
||||
@@ -1037,12 +1023,9 @@ Context: "[[area/research]]"
|
||||
rels.get("Real Relation").unwrap(),
|
||||
&vec!["[[note/important]]".to_string()]
|
||||
);
|
||||
// "Real Relation" + auto-generated "Type" (from is_a: "[[type/project]]")
|
||||
// "Real Relation" + auto-generated "Type" (from is_a: "[[project]]")
|
||||
assert_eq!(len, 2);
|
||||
assert_eq!(
|
||||
rels.get("Type").unwrap(),
|
||||
&vec!["[[type/project]]".to_string()]
|
||||
);
|
||||
assert_eq!(rels.get("Type").unwrap(), &vec!["[[project]]".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1121,7 +1104,7 @@ References:
|
||||
let entry = parse_test_entry(&dir, "project/my-project.md", content);
|
||||
assert_eq!(
|
||||
entry.relationships.get("Type").unwrap(),
|
||||
&vec!["[[type/project]]".to_string()]
|
||||
&vec!["[[project]]".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1129,7 +1112,7 @@ References:
|
||||
fn test_type_relationship_skipped_for_type_documents() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\nIs A: Type\n---\n# Project\n";
|
||||
let entry = parse_test_entry(&dir, "type/project.md", content);
|
||||
let entry = parse_test_entry(&dir, "project.md", content);
|
||||
assert!(entry.relationships.get("Type").is_none());
|
||||
}
|
||||
|
||||
@@ -1145,11 +1128,11 @@ References:
|
||||
#[test]
|
||||
fn test_type_relationship_handles_wikilink_is_a() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\nIs A: \"[[type/experiment]]\"\n---\n# Test\n";
|
||||
let content = "---\nIs A: \"[[experiment]]\"\n---\n# Test\n";
|
||||
let entry = parse_test_entry(&dir, "test.md", content);
|
||||
assert_eq!(
|
||||
entry.relationships.get("Type").unwrap(),
|
||||
&vec!["[[type/experiment]]".to_string()]
|
||||
&vec!["[[experiment]]".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1157,7 +1140,7 @@ References:
|
||||
fn test_type_from_frontmatter_not_folder() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\n---\n# Some Type\n";
|
||||
let entry = parse_test_entry(&dir, "type/some-type.md", content);
|
||||
let entry = parse_test_entry(&dir, "some-type.md", content);
|
||||
assert_eq!(entry.is_a, Some("Type".to_string()));
|
||||
}
|
||||
|
||||
@@ -1178,7 +1161,7 @@ References:
|
||||
let entry = parse_test_entry(&dir, "person/alice.md", content);
|
||||
assert_eq!(
|
||||
entry.relationships.get("Type").unwrap(),
|
||||
&vec!["[[type/person]]".to_string()]
|
||||
&vec!["[[person]]".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1264,7 +1247,7 @@ References:
|
||||
fn test_parse_sidebar_label_from_type_entry() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\nsidebar label: News\n---\n# News\n";
|
||||
let entry = parse_test_entry(&dir, "type/news.md", content);
|
||||
let entry = parse_test_entry(&dir, "news.md", content);
|
||||
assert_eq!(entry.sidebar_label, Some("News".to_string()));
|
||||
}
|
||||
|
||||
@@ -1272,7 +1255,7 @@ References:
|
||||
fn test_parse_sidebar_label_missing_defaults_to_none() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\n---\n# Project\n";
|
||||
let entry = parse_test_entry(&dir, "type/project.md", content);
|
||||
let entry = parse_test_entry(&dir, "project.md", content);
|
||||
assert_eq!(entry.sidebar_label, None);
|
||||
}
|
||||
|
||||
@@ -1280,7 +1263,7 @@ References:
|
||||
fn test_sidebar_label_not_in_relationships() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\nsidebar label: My Series\n---\n# Series\n";
|
||||
let entry = parse_test_entry(&dir, "type/series.md", content);
|
||||
let entry = parse_test_entry(&dir, "series.md", content);
|
||||
assert!(entry.relationships.get("sidebar label").is_none());
|
||||
}
|
||||
|
||||
@@ -1291,7 +1274,7 @@ References:
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content =
|
||||
"---\ntype: Type\ntemplate: \"## Objective\\n\\n## Timeline\"\n---\n# Project\n";
|
||||
let entry = parse_test_entry(&dir, "type/project.md", content);
|
||||
let entry = parse_test_entry(&dir, "project.md", content);
|
||||
assert!(entry.template.is_some());
|
||||
}
|
||||
|
||||
@@ -1300,7 +1283,7 @@ References:
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content =
|
||||
"---\ntype: Type\ntemplate: |\n ## Objective\n \n ## Timeline\n---\n# Project\n";
|
||||
let entry = parse_test_entry(&dir, "type/project.md", content);
|
||||
let entry = parse_test_entry(&dir, "project.md", content);
|
||||
assert!(entry.template.is_some());
|
||||
let tmpl = entry.template.unwrap();
|
||||
assert!(tmpl.contains("## Objective"));
|
||||
@@ -1311,7 +1294,7 @@ References:
|
||||
fn test_parse_template_missing_defaults_to_none() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\n---\n# Note\n";
|
||||
let entry = parse_test_entry(&dir, "type/note.md", content);
|
||||
let entry = parse_test_entry(&dir, "note.md", content);
|
||||
assert_eq!(entry.template, None);
|
||||
}
|
||||
|
||||
@@ -1319,7 +1302,7 @@ References:
|
||||
fn test_template_not_in_relationships() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\ntemplate: \"## Heading\"\n---\n# Project\n";
|
||||
let entry = parse_test_entry(&dir, "type/project.md", content);
|
||||
let entry = parse_test_entry(&dir, "project.md", content);
|
||||
assert!(entry.relationships.get("template").is_none());
|
||||
}
|
||||
|
||||
@@ -1329,7 +1312,7 @@ References:
|
||||
fn test_parse_sort_from_type_entry() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\nsort: \"modified:desc\"\n---\n# Project\n";
|
||||
let entry = parse_test_entry(&dir, "type/project.md", content);
|
||||
let entry = parse_test_entry(&dir, "project.md", content);
|
||||
assert_eq!(entry.sort, Some("modified:desc".to_string()));
|
||||
}
|
||||
|
||||
@@ -1337,7 +1320,7 @@ References:
|
||||
fn test_parse_sort_missing_defaults_to_none() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\n---\n# Project\n";
|
||||
let entry = parse_test_entry(&dir, "type/project.md", content);
|
||||
let entry = parse_test_entry(&dir, "project.md", content);
|
||||
assert_eq!(entry.sort, None);
|
||||
}
|
||||
|
||||
@@ -1345,7 +1328,7 @@ References:
|
||||
fn test_sort_not_in_relationships() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\nsort: \"title:asc\"\n---\n# Project\n";
|
||||
let entry = parse_test_entry(&dir, "type/project.md", content);
|
||||
let entry = parse_test_entry(&dir, "project.md", content);
|
||||
assert!(entry.relationships.get("sort").is_none());
|
||||
}
|
||||
|
||||
@@ -1353,7 +1336,7 @@ References:
|
||||
fn test_sort_not_in_properties() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\nsort: \"title:asc\"\n---\n# Project\n";
|
||||
let entry = parse_test_entry(&dir, "type/project.md", content);
|
||||
let entry = parse_test_entry(&dir, "project.md", content);
|
||||
assert!(entry.properties.get("sort").is_none());
|
||||
}
|
||||
|
||||
@@ -1588,7 +1571,7 @@ Company: Acme Corp
|
||||
fn test_parse_visible_false_from_type_entry() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\nvisible: false\n---\n# Journal\n";
|
||||
let entry = parse_test_entry(&dir, "type/journal.md", content);
|
||||
let entry = parse_test_entry(&dir, "journal.md", content);
|
||||
assert_eq!(entry.visible, Some(false));
|
||||
}
|
||||
|
||||
@@ -1596,7 +1579,7 @@ Company: Acme Corp
|
||||
fn test_parse_visible_true_from_type_entry() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\nvisible: true\n---\n# Project\n";
|
||||
let entry = parse_test_entry(&dir, "type/project.md", content);
|
||||
let entry = parse_test_entry(&dir, "project.md", content);
|
||||
assert_eq!(entry.visible, Some(true));
|
||||
}
|
||||
|
||||
@@ -1604,7 +1587,7 @@ Company: Acme Corp
|
||||
fn test_parse_visible_missing_defaults_to_none() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\n---\n# Project\n";
|
||||
let entry = parse_test_entry(&dir, "type/project.md", content);
|
||||
let entry = parse_test_entry(&dir, "project.md", content);
|
||||
assert_eq!(entry.visible, None);
|
||||
}
|
||||
|
||||
@@ -1612,7 +1595,7 @@ Company: Acme Corp
|
||||
fn test_visible_not_in_relationships() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\nvisible: false\n---\n# Journal\n";
|
||||
let entry = parse_test_entry(&dir, "type/journal.md", content);
|
||||
let entry = parse_test_entry(&dir, "journal.md", content);
|
||||
assert!(entry.relationships.get("visible").is_none());
|
||||
}
|
||||
|
||||
@@ -1620,7 +1603,7 @@ Company: Acme Corp
|
||||
fn test_visible_not_in_properties() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = "---\ntype: Type\nvisible: false\n---\n# Journal\n";
|
||||
let entry = parse_test_entry(&dir, "type/journal.md", content);
|
||||
let entry = parse_test_entry(&dir, "journal.md", content);
|
||||
assert!(entry.properties.get("visible").is_none());
|
||||
}
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ fn yaml_safe_value(value: &str) -> String {
|
||||
/// on Type notes. Returns the number of Type notes updated.
|
||||
///
|
||||
/// For each type name in `hidden_sections`:
|
||||
/// - If `type/<slug>.md` exists, adds `visible: false` to its frontmatter
|
||||
/// - If `<slug>.md` exists at vault root, adds `visible: false` to its frontmatter
|
||||
/// - If it doesn't exist, creates it with `type: Type`, `title: <name>`, `visible: false`
|
||||
/// - Re-saves the config without `hidden_sections`
|
||||
pub fn migrate_hidden_sections_to_visible(vault_path: &str) -> Result<usize, String> {
|
||||
@@ -159,16 +159,12 @@ pub fn migrate_hidden_sections_to_visible(vault_path: &str) -> Result<usize, Str
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let type_dir = Path::new(vault_path).join("type");
|
||||
if !type_dir.exists() {
|
||||
std::fs::create_dir_all(&type_dir)
|
||||
.map_err(|e| format!("Failed to create type dir: {e}"))?;
|
||||
}
|
||||
let vault = Path::new(vault_path);
|
||||
|
||||
let mut migrated = 0;
|
||||
for type_name in &hidden {
|
||||
let slug = type_name_to_slug(type_name);
|
||||
let type_path = type_dir.join(format!("{slug}.md"));
|
||||
let type_path = vault.join(format!("{slug}.md"));
|
||||
|
||||
if type_path.exists() {
|
||||
let type_content = std::fs::read_to_string(&type_path)
|
||||
@@ -365,11 +361,11 @@ property_display_modes:
|
||||
assert_eq!(count, 2);
|
||||
|
||||
// Check type notes were created
|
||||
let bookmark = std::fs::read_to_string(dir.path().join("type/bookmark.md")).unwrap();
|
||||
let bookmark = std::fs::read_to_string(dir.path().join("bookmark.md")).unwrap();
|
||||
assert!(bookmark.contains("visible: false"));
|
||||
assert!(bookmark.contains("title: Bookmark"));
|
||||
|
||||
let recipe = std::fs::read_to_string(dir.path().join("type/recipe.md")).unwrap();
|
||||
let recipe = std::fs::read_to_string(dir.path().join("recipe.md")).unwrap();
|
||||
assert!(recipe.contains("visible: false"));
|
||||
|
||||
// Config should no longer have hidden_sections
|
||||
@@ -391,11 +387,9 @@ property_display_modes:
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Create existing type note without visible
|
||||
let type_dir = dir.path().join("type");
|
||||
std::fs::create_dir_all(&type_dir).unwrap();
|
||||
// Create existing type note at vault root without visible
|
||||
std::fs::write(
|
||||
type_dir.join("project.md"),
|
||||
dir.path().join("project.md"),
|
||||
"---\ntype: Type\ntitle: Project\nicon: briefcase\n---\n\n# Project\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -403,7 +397,7 @@ property_display_modes:
|
||||
let count = migrate_hidden_sections_to_visible(vault_path).unwrap();
|
||||
assert_eq!(count, 1);
|
||||
|
||||
let content = std::fs::read_to_string(type_dir.join("project.md")).unwrap();
|
||||
let content = std::fs::read_to_string(dir.path().join("project.md")).unwrap();
|
||||
assert!(content.contains("visible: false"));
|
||||
assert!(
|
||||
content.contains("icon: briefcase"),
|
||||
|
||||
@@ -306,14 +306,14 @@ describe('DynamicPropertiesPanel', () => {
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('Project'))
|
||||
expect(onNavigate).toHaveBeenCalledWith('type/project')
|
||||
expect(onNavigate).toHaveBeenCalledWith('project')
|
||||
})
|
||||
|
||||
describe('TypeSelector', () => {
|
||||
const typeEntries = [
|
||||
makeEntry({ path: '/vault/type/project.md', title: 'Project', isA: 'Type' }),
|
||||
makeEntry({ path: '/vault/type/person.md', title: 'Person', isA: 'Type' }),
|
||||
makeEntry({ path: '/vault/type/topic.md', title: 'Topic', isA: 'Type' }),
|
||||
makeEntry({ path: '/vault/project.md', title: 'Project', isA: 'Type' }),
|
||||
makeEntry({ path: '/vault/person.md', title: 'Person', isA: 'Type' }),
|
||||
makeEntry({ path: '/vault/topic.md', title: 'Topic', isA: 'Type' }),
|
||||
]
|
||||
|
||||
it('renders as dropdown when editable', () => {
|
||||
|
||||
@@ -372,7 +372,7 @@ This is a test note with some words to count.
|
||||
fileSize: 500,
|
||||
snippet: '',
|
||||
wordCount: 0,
|
||||
relationships: { 'Type': ['[[type/responsibility]]'] },
|
||||
relationships: { 'Type': ['[[responsibility]]'] },
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
@@ -399,7 +399,7 @@ This is a test note with some words to count.
|
||||
fileSize: 300,
|
||||
snippet: '',
|
||||
wordCount: 0,
|
||||
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[type/essay]]'] },
|
||||
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[essay]]'] },
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
@@ -426,7 +426,7 @@ This is a test note with some words to count.
|
||||
fileSize: 400,
|
||||
snippet: '',
|
||||
wordCount: 0,
|
||||
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[type/procedure]]'] },
|
||||
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[procedure]]'] },
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
@@ -453,7 +453,7 @@ This is a test note with some words to count.
|
||||
fileSize: 200,
|
||||
snippet: '',
|
||||
wordCount: 0,
|
||||
relationships: { 'Related to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[type/experiment]]'] },
|
||||
relationships: { 'Related to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[experiment]]'] },
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
@@ -532,13 +532,13 @@ Status: Active
|
||||
it('skips Type relationships in referenced-by computation', () => {
|
||||
const typeEntry: VaultEntry = {
|
||||
...targetEntry,
|
||||
path: '/Users/luca/Laputa/type/responsibility.md',
|
||||
path: '/Users/luca/Laputa/responsibility.md',
|
||||
filename: 'responsibility.md',
|
||||
title: 'Responsibility',
|
||||
isA: 'Type',
|
||||
relationships: {},
|
||||
}
|
||||
// essayEntry has Type: [[type/responsibility]] — should NOT show as referenced-by
|
||||
// essayEntry has Type: [[responsibility]] — should NOT show as referenced-by
|
||||
render(
|
||||
<Inspector
|
||||
{...defaultProps}
|
||||
@@ -549,7 +549,7 @@ Status: Active
|
||||
/>
|
||||
)
|
||||
// On Writing Well references responsibility via "Belongs to" (path match), not via "Type"
|
||||
// But the Type entry is at type/responsibility.md, so wikilinks to
|
||||
// But the Type entry is at responsibility.md, so wikilinks to
|
||||
// responsibility/grow-newsletter won't match. Section should be hidden
|
||||
expect(screen.queryByText('Referenced by')).not.toBeInTheDocument()
|
||||
})
|
||||
@@ -561,7 +561,7 @@ Status: Active
|
||||
}
|
||||
const referrer: VaultEntry = {
|
||||
...essayEntry,
|
||||
relationships: { 'Topics': ['[[Newsletter]]'], 'Type': ['[[type/essay]]'] },
|
||||
relationships: { 'Topics': ['[[Newsletter]]'], 'Type': ['[[essay]]'] },
|
||||
}
|
||||
render(
|
||||
<Inspector
|
||||
@@ -596,7 +596,7 @@ Status: Active
|
||||
fileSize: 300,
|
||||
snippet: '',
|
||||
wordCount: 0,
|
||||
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[type/essay]]'] },
|
||||
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[essay]]'] },
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('DynamicRelationshipsPanel', () => {
|
||||
const onNavigate = vi.fn()
|
||||
const onAddProperty = vi.fn()
|
||||
const personTypeEntry = makeEntry({
|
||||
path: '/vault/type/person.md', filename: 'person.md', title: 'Person',
|
||||
path: '/vault/person.md', filename: 'person.md', title: 'Person',
|
||||
isA: 'Type', color: 'yellow', icon: 'user',
|
||||
})
|
||||
const entries = [
|
||||
@@ -757,7 +757,7 @@ describe('GitHistoryPanel', () => {
|
||||
describe('InstancesPanel', () => {
|
||||
const onNavigate = vi.fn()
|
||||
const quarterType = makeEntry({
|
||||
path: '/vault/type/quarter.md', filename: 'quarter.md', title: 'Quarter',
|
||||
path: '/vault/quarter.md', filename: 'quarter.md', title: 'Quarter',
|
||||
isA: 'Type', color: 'blue', icon: 'calendar',
|
||||
})
|
||||
const typeEntryMap: Record<string, VaultEntry> = { Quarter: quarterType }
|
||||
|
||||
@@ -38,7 +38,7 @@ const entries = [
|
||||
]
|
||||
|
||||
const personTypeEntry = makeEntry({
|
||||
path: '/vault/type/person.md', filename: 'person.md', title: 'Person',
|
||||
path: '/vault/person.md', filename: 'person.md', title: 'Person',
|
||||
isA: 'Type', color: 'yellow', icon: 'user',
|
||||
})
|
||||
const typeEntryMap: Record<string, VaultEntry> = { Person: personTypeEntry }
|
||||
|
||||
@@ -422,7 +422,7 @@ describe('Sidebar', () => {
|
||||
const entriesWithCustomTypes: VaultEntry[] = [
|
||||
...mockEntries,
|
||||
{
|
||||
path: '/vault/type/recipe.md',
|
||||
path: '/vault/recipe.md',
|
||||
filename: 'recipe.md',
|
||||
title: 'Recipe',
|
||||
isA: 'Type',
|
||||
@@ -450,7 +450,7 @@ describe('Sidebar', () => {
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/vault/type/book.md',
|
||||
path: '/vault/book.md',
|
||||
filename: 'book.md',
|
||||
title: 'Book',
|
||||
isA: 'Type',
|
||||
@@ -592,7 +592,7 @@ describe('Sidebar', () => {
|
||||
|
||||
it('does not show built-in types as custom sections', () => {
|
||||
const projectTypeEntry: VaultEntry = {
|
||||
path: '/vault/type/project.md',
|
||||
path: '/vault/project.md',
|
||||
filename: 'project.md',
|
||||
title: 'Project',
|
||||
isA: 'Type',
|
||||
@@ -629,7 +629,7 @@ describe('Sidebar', () => {
|
||||
const entriesWithLabel: VaultEntry[] = [
|
||||
...mockEntries,
|
||||
{
|
||||
path: '/vault/type/news.md', filename: 'news.md', title: 'News', isA: 'Type',
|
||||
path: '/vault/news.md', filename: 'news.md', title: 'News', isA: 'Type',
|
||||
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
|
||||
archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null,
|
||||
fileSize: 200, snippet: '', wordCount: 0, relationships: {},
|
||||
@@ -655,7 +655,7 @@ describe('Sidebar', () => {
|
||||
const entriesWithBuiltInOverride: VaultEntry[] = [
|
||||
...mockEntries,
|
||||
{
|
||||
path: '/vault/type/person.md', filename: 'person.md', title: 'Person', isA: 'Type',
|
||||
path: '/vault/person.md', filename: 'person.md', title: 'Person', isA: 'Type',
|
||||
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
|
||||
archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null,
|
||||
fileSize: 200, snippet: '', wordCount: 0, relationships: {},
|
||||
@@ -677,7 +677,7 @@ describe('Sidebar', () => {
|
||||
|
||||
describe('type visibility via visible property', () => {
|
||||
const makeTypeEntry = (title: string, visible: boolean | null): VaultEntry => ({
|
||||
path: `/vault/type/${title.toLowerCase()}.md`,
|
||||
path: `/vault/${title.toLowerCase()}.md`,
|
||||
filename: `${title.toLowerCase()}.md`,
|
||||
title,
|
||||
isA: 'Type',
|
||||
@@ -809,7 +809,7 @@ describe('Sidebar', () => {
|
||||
...mockEntries,
|
||||
// Type entries with order values — reversed from default
|
||||
{
|
||||
path: '/vault/type/project.md', filename: 'project.md', title: 'Project', isA: 'Type',
|
||||
path: '/vault/project.md', filename: 'project.md', title: 'Project', isA: 'Type',
|
||||
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
|
||||
archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '',
|
||||
wordCount: 0,
|
||||
@@ -817,7 +817,7 @@ describe('Sidebar', () => {
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/vault/type/topic.md', filename: 'topic.md', title: 'Topic', isA: 'Type',
|
||||
path: '/vault/topic.md', filename: 'topic.md', title: 'Topic', isA: 'Type',
|
||||
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
|
||||
archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '',
|
||||
wordCount: 0,
|
||||
@@ -825,7 +825,7 @@ describe('Sidebar', () => {
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/vault/type/person.md', filename: 'person.md', title: 'Person', isA: 'Type',
|
||||
path: '/vault/person.md', filename: 'person.md', title: 'Person', isA: 'Type',
|
||||
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
|
||||
archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '',
|
||||
wordCount: 0,
|
||||
|
||||
@@ -28,7 +28,7 @@ function ReadOnlyType({ isA, customColorKey, onNavigate }: { isA?: string | null
|
||||
<button
|
||||
className="min-w-0 truncate border-none text-right cursor-pointer hover:opacity-80"
|
||||
style={{ background: getTypeLightColor(isA, customColorKey), color: getTypeColor(isA, customColorKey), borderRadius: 6, padding: '2px 8px', fontSize: 12, fontWeight: 500 }}
|
||||
onClick={() => onNavigate(`type/${isA.toLowerCase()}`)} title={isA}
|
||||
onClick={() => onNavigate(isA.toLowerCase())} title={isA}
|
||||
>{isA}</button>
|
||||
) : (
|
||||
<span className="text-right text-[12px] text-secondary-foreground">{isA}</span>
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('useNoteListSort (via NoteList)', () => {
|
||||
})
|
||||
|
||||
it('reads sort from type document for sectionGroup selection', () => {
|
||||
const typeDoc = makeEntry({ path: '/type/note.md', title: 'Note', isA: 'Type', sort: 'title:asc' })
|
||||
const typeDoc = makeEntry({ path: '/note.md', title: 'Note', isA: 'Type', sort: 'title:asc' })
|
||||
const entries = [
|
||||
typeDoc,
|
||||
makeEntry({ path: '/c.md', title: 'Charlie', modifiedAt: 3000 }),
|
||||
@@ -82,7 +82,7 @@ describe('useNoteListSort (via NoteList)', () => {
|
||||
})
|
||||
|
||||
it('shows type title as header for sectionGroup selection', () => {
|
||||
const typeDoc = makeEntry({ path: '/type/project.md', title: 'Project', isA: 'Type' })
|
||||
const typeDoc = makeEntry({ path: '/project.md', title: 'Project', isA: 'Type' })
|
||||
renderNoteList({ entries: [typeDoc], selection: { kind: 'sectionGroup', type: 'Project', label: 'Projects' } })
|
||||
expect(screen.getByText('Project')).toBeInTheDocument()
|
||||
})
|
||||
@@ -91,7 +91,7 @@ describe('useNoteListSort (via NoteList)', () => {
|
||||
localStorageMock.setItem('laputa-sort-preferences', JSON.stringify({ '__list__': { option: 'title', direction: 'asc' } }))
|
||||
const onUpdateTypeSort = vi.fn()
|
||||
const updateEntry = vi.fn()
|
||||
const typeDoc = makeEntry({ path: '/type/project.md', title: 'Project', isA: 'Type', sort: null })
|
||||
const typeDoc = makeEntry({ path: '/project.md', title: 'Project', isA: 'Type', sort: null })
|
||||
const entries = [typeDoc, makeEntry()]
|
||||
|
||||
renderNoteList({
|
||||
@@ -101,15 +101,15 @@ describe('useNoteListSort (via NoteList)', () => {
|
||||
updateEntry,
|
||||
})
|
||||
|
||||
expect(onUpdateTypeSort).toHaveBeenCalledWith('/type/project.md', 'sort', 'title:asc')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/type/project.md', { sort: 'title:asc' })
|
||||
expect(onUpdateTypeSort).toHaveBeenCalledWith('/project.md', 'sort', 'title:asc')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/project.md', { sort: 'title:asc' })
|
||||
})
|
||||
|
||||
it('does not migrate if type already has sort', () => {
|
||||
localStorageMock.setItem('laputa-sort-preferences', JSON.stringify({ '__list__': { option: 'title', direction: 'asc' } }))
|
||||
const onUpdateTypeSort = vi.fn()
|
||||
const updateEntry = vi.fn()
|
||||
const typeDoc = makeEntry({ path: '/type/project.md', title: 'Project', isA: 'Type', sort: 'modified:desc' })
|
||||
const typeDoc = makeEntry({ path: '/project.md', title: 'Project', isA: 'Type', sort: 'modified:desc' })
|
||||
const entries = [typeDoc, makeEntry()]
|
||||
|
||||
renderNoteList({
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('useEntryActions', () => {
|
||||
const handleDeleteProperty = vi.fn().mockResolvedValue(undefined)
|
||||
const setToastMessage = vi.fn()
|
||||
const createTypeEntry = vi.fn().mockImplementation((typeName: string) =>
|
||||
Promise.resolve(makeEntry({ isA: 'Type', title: typeName, path: `/vault/type/${typeName.toLowerCase()}.md` })),
|
||||
Promise.resolve(makeEntry({ isA: 'Type', title: typeName, path: `/vault/${typeName.toLowerCase()}.md` })),
|
||||
)
|
||||
|
||||
function setup(entries: VaultEntry[] = []) {
|
||||
@@ -131,16 +131,16 @@ describe('useEntryActions', () => {
|
||||
|
||||
describe('handleCustomizeType', () => {
|
||||
it('updates icon and color on the type entry', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/type/recipe.md' })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/recipe.md' })
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleCustomizeType('Recipe', 'cooking-pot', 'green')
|
||||
})
|
||||
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/recipe.md', 'icon', 'cooking-pot')
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/recipe.md', 'color', 'green')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/recipe.md', { icon: 'cooking-pot', color: 'green' })
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/recipe.md', 'icon', 'cooking-pot')
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/recipe.md', 'color', 'green')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/recipe.md', { icon: 'cooking-pot', color: 'green' })
|
||||
expect(onFrontmatterPersisted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -152,9 +152,9 @@ describe('useEntryActions', () => {
|
||||
})
|
||||
|
||||
expect(createTypeEntry).toHaveBeenCalledWith('Recipe')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/recipe.md', { icon: 'star', color: 'red' })
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/recipe.md', 'icon', 'star')
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/recipe.md', 'color', 'red')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/recipe.md', { icon: 'star', color: 'red' })
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/recipe.md', 'icon', 'star')
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/recipe.md', 'color', 'red')
|
||||
})
|
||||
|
||||
it('serializes frontmatter writes (icon before color)', async () => {
|
||||
@@ -163,7 +163,7 @@ describe('useEntryActions', () => {
|
||||
callOrder.push(key)
|
||||
return Promise.resolve()
|
||||
})
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Project', path: '/vault/type/project.md' })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Project', path: '/vault/project.md' })
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
await act(async () => {
|
||||
@@ -176,28 +176,28 @@ describe('useEntryActions', () => {
|
||||
|
||||
describe('handleUpdateTypeTemplate', () => {
|
||||
it('updates template on the type entry', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Project', path: '/vault/type/project.md' })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Project', path: '/vault/project.md' })
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleUpdateTypeTemplate('Project', '## Objective\n\n## Notes')
|
||||
})
|
||||
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/project.md', 'template', '## Objective\n\n## Notes')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/project.md', { template: '## Objective\n\n## Notes' })
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/project.md', 'template', '## Objective\n\n## Notes')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/project.md', { template: '## Objective\n\n## Notes' })
|
||||
expect(onFrontmatterPersisted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sets template to null when empty string', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Project', path: '/vault/type/project.md' })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Project', path: '/vault/project.md' })
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleUpdateTypeTemplate('Project', '')
|
||||
})
|
||||
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/project.md', 'template', '')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/project.md', { template: null })
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/project.md', 'template', '')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/project.md', { template: null })
|
||||
})
|
||||
|
||||
it('auto-creates type entry when not found', async () => {
|
||||
@@ -208,15 +208,15 @@ describe('useEntryActions', () => {
|
||||
})
|
||||
|
||||
expect(createTypeEntry).toHaveBeenCalledWith('NonExistent')
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/nonexistent.md', 'template', '## Template')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/nonexistent.md', { template: '## Template' })
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/nonexistent.md', 'template', '## Template')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/nonexistent.md', { template: '## Template' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleReorderSections', () => {
|
||||
it('updates order on multiple type entries', async () => {
|
||||
const typeA = makeEntry({ isA: 'Type', title: 'Note', path: '/vault/type/note.md' })
|
||||
const typeB = makeEntry({ isA: 'Type', title: 'Project', path: '/vault/type/project.md' })
|
||||
const typeA = makeEntry({ isA: 'Type', title: 'Note', path: '/vault/note.md' })
|
||||
const typeB = makeEntry({ isA: 'Type', title: 'Project', path: '/vault/project.md' })
|
||||
const { result } = setup([typeA, typeB])
|
||||
|
||||
await act(async () => {
|
||||
@@ -226,15 +226,15 @@ describe('useEntryActions', () => {
|
||||
])
|
||||
})
|
||||
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/note.md', 'order', 0)
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/project.md', 'order', 1)
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/note.md', { order: 0 })
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/project.md', { order: 1 })
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note.md', 'order', 0)
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/project.md', 'order', 1)
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/note.md', { order: 0 })
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/project.md', { order: 1 })
|
||||
expect(onFrontmatterPersisted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('auto-creates type entries when not found', async () => {
|
||||
const typeA = makeEntry({ isA: 'Type', title: 'Note', path: '/vault/type/note.md' })
|
||||
const typeA = makeEntry({ isA: 'Type', title: 'Note', path: '/vault/note.md' })
|
||||
const { result } = setup([typeA])
|
||||
|
||||
await act(async () => {
|
||||
@@ -246,47 +246,47 @@ describe('useEntryActions', () => {
|
||||
|
||||
expect(createTypeEntry).toHaveBeenCalledWith('Missing')
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledTimes(2)
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/note.md', 'order', 0)
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/missing.md', 'order', 1)
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note.md', 'order', 0)
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/missing.md', 'order', 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleRenameSection', () => {
|
||||
it('writes sidebar label frontmatter and updates entry in memory', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/type/recipe.md', sidebarLabel: null })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/recipe.md', sidebarLabel: null })
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleRenameSection('Recipe', 'Recipes')
|
||||
})
|
||||
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/recipe.md', 'sidebar label', 'Recipes')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/recipe.md', { sidebarLabel: 'Recipes' })
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/recipe.md', 'sidebar label', 'Recipes')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/recipe.md', { sidebarLabel: 'Recipes' })
|
||||
expect(onFrontmatterPersisted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('trims whitespace before saving', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/type/recipe.md', sidebarLabel: null })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/recipe.md', sidebarLabel: null })
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleRenameSection('Recipe', ' Dishes ')
|
||||
})
|
||||
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/recipe.md', 'sidebar label', 'Dishes')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/recipe.md', { sidebarLabel: 'Dishes' })
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/recipe.md', 'sidebar label', 'Dishes')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/recipe.md', { sidebarLabel: 'Dishes' })
|
||||
})
|
||||
|
||||
it('deletes sidebar label when label is empty', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/type/recipe.md', sidebarLabel: 'Dishes' })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/recipe.md', sidebarLabel: 'Dishes' })
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleRenameSection('Recipe', '')
|
||||
})
|
||||
|
||||
expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/type/recipe.md', 'sidebar label')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/recipe.md', { sidebarLabel: null })
|
||||
expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/recipe.md', 'sidebar label')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/recipe.md', { sidebarLabel: null })
|
||||
expect(handleUpdateFrontmatter).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -298,35 +298,35 @@ describe('useEntryActions', () => {
|
||||
})
|
||||
|
||||
expect(createTypeEntry).toHaveBeenCalledWith('NonExistent')
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/nonexistent.md', 'sidebar label', 'Label')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/nonexistent.md', { sidebarLabel: 'Label' })
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/nonexistent.md', 'sidebar label', 'Label')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/nonexistent.md', { sidebarLabel: 'Label' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleToggleTypeVisibility', () => {
|
||||
it('sets visible to false when currently visible (null/default)', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Journal', path: '/vault/type/journal.md', visible: null })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Journal', path: '/vault/journal.md', visible: null })
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleToggleTypeVisibility('Journal')
|
||||
})
|
||||
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/journal.md', 'visible', false)
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/journal.md', { visible: false })
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/journal.md', 'visible', false)
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/journal.md', { visible: false })
|
||||
expect(onFrontmatterPersisted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sets visible to true (deletes property) when currently hidden', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Journal', path: '/vault/type/journal.md', visible: false })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Journal', path: '/vault/journal.md', visible: false })
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleToggleTypeVisibility('Journal')
|
||||
})
|
||||
|
||||
expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/type/journal.md', 'visible')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/journal.md', { visible: null })
|
||||
expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/journal.md', 'visible')
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/journal.md', { visible: null })
|
||||
expect(onFrontmatterPersisted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -338,14 +338,14 @@ describe('useEntryActions', () => {
|
||||
})
|
||||
|
||||
expect(createTypeEntry).toHaveBeenCalledWith('Journal')
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/type/journal.md', 'visible', false)
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/type/journal.md', { visible: false })
|
||||
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/journal.md', 'visible', false)
|
||||
expect(updateEntry).toHaveBeenCalledWith('/vault/journal.md', { visible: false })
|
||||
})
|
||||
})
|
||||
|
||||
describe('failed disk writes do not update React state', () => {
|
||||
it('handleCustomizeType does not update entry when frontmatter write fails', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/type/recipe.md' })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/recipe.md' })
|
||||
handleUpdateFrontmatter.mockRejectedValueOnce(new Error('disk full'))
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
@@ -357,7 +357,7 @@ describe('useEntryActions', () => {
|
||||
})
|
||||
|
||||
it('handleRenameSection does not update entry when frontmatter write fails', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/type/recipe.md' })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/recipe.md' })
|
||||
handleUpdateFrontmatter.mockRejectedValueOnce(new Error('disk full'))
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
@@ -369,7 +369,7 @@ describe('useEntryActions', () => {
|
||||
})
|
||||
|
||||
it('handleRenameSection does not update entry when delete property fails', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/type/recipe.md', sidebarLabel: 'Dishes' })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', path: '/vault/recipe.md', sidebarLabel: 'Dishes' })
|
||||
handleDeleteProperty.mockRejectedValueOnce(new Error('disk full'))
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
@@ -381,7 +381,7 @@ describe('useEntryActions', () => {
|
||||
})
|
||||
|
||||
it('handleToggleTypeVisibility does not update entry when frontmatter write fails (hide)', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Journal', path: '/vault/type/journal.md', visible: null })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Journal', path: '/vault/journal.md', visible: null })
|
||||
handleUpdateFrontmatter.mockRejectedValueOnce(new Error('disk full'))
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
@@ -393,7 +393,7 @@ describe('useEntryActions', () => {
|
||||
})
|
||||
|
||||
it('handleToggleTypeVisibility does not update entry when delete property fails (show)', async () => {
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Journal', path: '/vault/type/journal.md', visible: false })
|
||||
const typeEntry = makeEntry({ isA: 'Type', title: 'Journal', path: '/vault/journal.md', visible: false })
|
||||
handleDeleteProperty.mockRejectedValueOnce(new Error('disk full'))
|
||||
const { result } = setup([typeEntry])
|
||||
|
||||
|
||||
@@ -315,9 +315,9 @@ describe('resolveNewNote', () => {
|
||||
})
|
||||
|
||||
describe('resolveNewType', () => {
|
||||
it('creates a type entry in the type folder', () => {
|
||||
it('creates a type entry at vault root', () => {
|
||||
const { entry, content } = resolveNewType('Recipe', '/my/vault')
|
||||
expect(entry.path).toBe('/my/vault/type/recipe.md')
|
||||
expect(entry.path).toBe('/my/vault/recipe.md')
|
||||
expect(entry.isA).toBe('Type')
|
||||
expect(entry.status).toBeNull()
|
||||
expect(content).toContain('type: Type')
|
||||
@@ -326,7 +326,7 @@ describe('resolveNewType', () => {
|
||||
|
||||
it('uses provided vault path instead of hardcoded path', () => {
|
||||
const { entry } = resolveNewType('Responsibility', '/other/vault')
|
||||
expect(entry.path).toBe('/other/vault/type/responsibility.md')
|
||||
expect(entry.path).toBe('/other/vault/responsibility.md')
|
||||
expect(entry.path).not.toContain('/Users/luca/Laputa')
|
||||
})
|
||||
})
|
||||
@@ -846,7 +846,7 @@ describe('useNoteActions hook', () => {
|
||||
|
||||
it.each([
|
||||
['handleCreateNote', 'Failing Note', 'Note', 'failing-note.md'],
|
||||
['handleCreateType', 'Recipe', 'Type', 'type/recipe.md'],
|
||||
['handleCreateType', 'Recipe', 'Type', 'recipe.md'],
|
||||
])('reverts optimistic creation via %s when disk write fails', async (method, title, type, pathFragment) => {
|
||||
vi.mocked(invoke).mockRejectedValueOnce(new Error('disk full'))
|
||||
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
||||
|
||||
@@ -199,7 +199,7 @@ export function resolveNewNote(title: string, type: string, vaultPath: string, t
|
||||
|
||||
export function resolveNewType(typeName: string, vaultPath: string): { entry: VaultEntry; content: string } {
|
||||
const slug = slugify(typeName)
|
||||
const entry = buildNewEntry({ path: `${vaultPath}/type/${slug}.md`, slug, title: typeName, type: 'Type', status: null })
|
||||
const entry = buildNewEntry({ path: `${vaultPath}/${slug}.md`, slug, title: typeName, type: 'Type', status: null })
|
||||
return { entry, content: `---\ntype: Type\n---\n\n# ${typeName}\n\n` }
|
||||
}
|
||||
|
||||
|
||||
@@ -479,21 +479,21 @@ aliases:
|
||||
## Role
|
||||
Content strategist — plans newsletter topics and manages the editorial calendar.
|
||||
`,
|
||||
'/Users/luca/Laputa/type/project.md': `---
|
||||
'/Users/luca/Laputa/project.md': `---
|
||||
type: Type
|
||||
order: 0
|
||||
---
|
||||
|
||||
# Project
|
||||
|
||||
A **time-bound initiative** that advances a [[type/responsibility|Responsibility]]. Projects have a clear start, end, and deliverables.
|
||||
A **time-bound initiative** that advances a [[responsibility|Responsibility]]. Projects have a clear start, end, and deliverables.
|
||||
|
||||
## Properties
|
||||
- **Status**: Active, Paused, Done, Dropped
|
||||
- **Owner**: The person accountable
|
||||
- **Belongs to**: Usually a Quarter or Responsibility
|
||||
`,
|
||||
'/Users/luca/Laputa/type/responsibility.md': `---
|
||||
'/Users/luca/Laputa/responsibility.md': `---
|
||||
type: Type
|
||||
order: 1
|
||||
---
|
||||
@@ -506,14 +506,14 @@ An **ongoing area of ownership** — something you're accountable for indefinite
|
||||
- **Status**: Active, Paused, Archived
|
||||
- **Owner**: The person accountable
|
||||
`,
|
||||
'/Users/luca/Laputa/type/procedure.md': `---
|
||||
'/Users/luca/Laputa/procedure.md': `---
|
||||
type: Type
|
||||
order: 2
|
||||
---
|
||||
|
||||
# Procedure
|
||||
|
||||
A **recurring process** tied to a [[type/responsibility|Responsibility]]. Procedures have a cadence (weekly, monthly) and describe how to do something.
|
||||
A **recurring process** tied to a [[responsibility|Responsibility]]. Procedures have a cadence (weekly, monthly) and describe how to do something.
|
||||
|
||||
## Properties
|
||||
- **Status**: Active, Paused
|
||||
@@ -521,7 +521,7 @@ A **recurring process** tied to a [[type/responsibility|Responsibility]]. Proced
|
||||
- **Cadence**: Weekly, Monthly, Quarterly
|
||||
- **Belongs to**: A Responsibility
|
||||
`,
|
||||
'/Users/luca/Laputa/type/experiment.md': `---
|
||||
'/Users/luca/Laputa/experiment.md': `---
|
||||
type: Type
|
||||
order: 3
|
||||
---
|
||||
@@ -534,7 +534,7 @@ A **hypothesis-driven investigation** with a clear test and measurable outcome.
|
||||
- **Status**: Active, Done, Dropped
|
||||
- **Owner**: The person running the experiment
|
||||
`,
|
||||
'/Users/luca/Laputa/type/person.md': `---
|
||||
'/Users/luca/Laputa/person.md': `---
|
||||
type: Type
|
||||
order: 4
|
||||
---
|
||||
@@ -546,7 +546,7 @@ A **person** you interact with — team members, collaborators, contacts. People
|
||||
## Properties
|
||||
- **Aliases**: Alternative names for wikilink resolution
|
||||
`,
|
||||
'/Users/luca/Laputa/type/event.md': `---
|
||||
'/Users/luca/Laputa/event.md': `---
|
||||
type: Type
|
||||
order: 5
|
||||
---
|
||||
@@ -558,7 +558,7 @@ A **point-in-time occurrence** — meetings, launches, milestones. Events are li
|
||||
## Properties
|
||||
- **Related to**: Entities this event is about
|
||||
`,
|
||||
'/Users/luca/Laputa/type/topic.md': `---
|
||||
'/Users/luca/Laputa/topic.md': `---
|
||||
type: Type
|
||||
order: 6
|
||||
---
|
||||
@@ -570,7 +570,7 @@ A **subject area** for categorization. Topics group related notes, projects, and
|
||||
## Properties
|
||||
- **Aliases**: Alternative names
|
||||
`,
|
||||
'/Users/luca/Laputa/type/essay.md': `---
|
||||
'/Users/luca/Laputa/essay.md': `---
|
||||
type: Type
|
||||
order: 7
|
||||
---
|
||||
@@ -582,7 +582,7 @@ A **published piece of writing** — newsletter essays, blog posts, articles. Es
|
||||
## Properties
|
||||
- **Belongs to**: Usually a Responsibility
|
||||
`,
|
||||
'/Users/luca/Laputa/type/note.md': `---
|
||||
'/Users/luca/Laputa/note.md': `---
|
||||
type: Type
|
||||
order: 8
|
||||
---
|
||||
@@ -594,7 +594,7 @@ A **general-purpose document** — research notes, meeting notes, strategy docs.
|
||||
## Properties
|
||||
- **Belongs to**: A Project, Responsibility, or other parent
|
||||
`,
|
||||
'/Users/luca/Laputa/type/recipe.md': `---
|
||||
'/Users/luca/Laputa/recipe.md': `---
|
||||
type: Type
|
||||
icon: cooking-pot
|
||||
color: orange
|
||||
@@ -609,7 +609,7 @@ A **recipe** for cooking or baking. Recipes have ingredients, steps, and serving
|
||||
- **Prep Time**: Time to prepare
|
||||
- **Cook Time**: Time to cook
|
||||
`,
|
||||
'/Users/luca/Laputa/type/book.md': `---
|
||||
'/Users/luca/Laputa/book.md': `---
|
||||
type: Type
|
||||
icon: book-open
|
||||
color: green
|
||||
|
||||
@@ -30,7 +30,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {
|
||||
'Belongs to': ['[[q1-2026]]'],
|
||||
'Related to': ['[[software-development]]'],
|
||||
'Type': ['[[type/project]]'],
|
||||
'Type': ['[[project]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -67,7 +67,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
],
|
||||
'Topics': ['[[growth]]', '[[writing]]'],
|
||||
'Related to': ['[[growth]]'],
|
||||
'Type': ['[[type/responsibility]]'],
|
||||
'Type': ['[[responsibility]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -98,7 +98,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 180,
|
||||
relationships: {
|
||||
'Owner': ['[[matteo-cellini|Matteo Cellini]]'],
|
||||
'Type': ['[[type/responsibility]]'],
|
||||
'Type': ['[[responsibility]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -129,7 +129,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 95,
|
||||
relationships: {
|
||||
'Belongs to': ['[[grow-newsletter]]'],
|
||||
'Type': ['[[type/procedure]]'],
|
||||
'Type': ['[[procedure]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -160,7 +160,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 128,
|
||||
relationships: {
|
||||
'Belongs to': ['[[manage-sponsorships]]'],
|
||||
'Type': ['[[type/procedure]]'],
|
||||
'Type': ['[[procedure]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -192,7 +192,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {
|
||||
'Related to': ['[[trading]]', '[[algorithmic-trading]]'],
|
||||
'Has Data': ['[[ema200-backtest-results]]'],
|
||||
'Type': ['[[type/experiment]]'],
|
||||
'Type': ['[[experiment]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -224,7 +224,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {
|
||||
'Belongs to': ['[[26q1-laputa-app]]'],
|
||||
'Related to': ['[[growth]]', '[[ads]]'],
|
||||
'Type': ['[[type/note]]'],
|
||||
'Type': ['[[note]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -255,7 +255,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 150,
|
||||
relationships: {
|
||||
'Belongs to': ['[[26q1-laputa-app]]'],
|
||||
'Type': ['[[type/note]]'],
|
||||
'Type': ['[[note]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -285,7 +285,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
snippet: 'Sponsorship manager — handles all sponsor relationships, proposals, and reporting.',
|
||||
wordCount: 88,
|
||||
relationships: {
|
||||
'Type': ['[[type/person]]'],
|
||||
'Type': ['[[person]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -315,7 +315,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
snippet: 'Product designer — leads UX research and design sprints for the app.',
|
||||
wordCount: 120,
|
||||
relationships: {
|
||||
'Type': ['[[type/person]]'],
|
||||
'Type': ['[[person]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -345,7 +345,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
snippet: 'Frontend engineer — focuses on React performance and accessibility.',
|
||||
wordCount: 95,
|
||||
relationships: {
|
||||
'Type': ['[[type/person]]'],
|
||||
'Type': ['[[person]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -375,7 +375,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
snippet: 'Content strategist — plans newsletter topics and manages the editorial calendar.',
|
||||
wordCount: 75,
|
||||
relationships: {
|
||||
'Type': ['[[type/person]]'],
|
||||
'Type': ['[[person]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -406,7 +406,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 310,
|
||||
relationships: {
|
||||
'Related to': ['[[26q1-laputa-app]]', '[[matteo-cellini]]'],
|
||||
'Type': ['[[type/event]]'],
|
||||
'Type': ['[[event]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -437,7 +437,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 45,
|
||||
relationships: {
|
||||
'Notes': ['[[facebook-ads-strategy]]', '[[budget-allocation]]'],
|
||||
'Type': ['[[type/topic]]'],
|
||||
'Type': ['[[topic]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -468,7 +468,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 60,
|
||||
relationships: {
|
||||
'Notes': ['[[stock-screener]]'],
|
||||
'Type': ['[[type/topic]]'],
|
||||
'Type': ['[[topic]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -499,7 +499,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 180,
|
||||
relationships: {
|
||||
'Belongs to': ['[[grow-newsletter]]'],
|
||||
'Type': ['[[type/essay]]'],
|
||||
'Type': ['[[essay]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -531,7 +531,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {
|
||||
'Belongs to': ['[[grow-newsletter]]'],
|
||||
'Related to': ['[[software-development]]'],
|
||||
'Type': ['[[type/essay]]'],
|
||||
'Type': ['[[essay]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -562,7 +562,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 410,
|
||||
relationships: {
|
||||
'Belongs to': ['[[grow-newsletter]]'],
|
||||
'Type': ['[[type/essay]]'],
|
||||
'Type': ['[[essay]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -574,7 +574,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
// --- Type documents ---
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/project.md',
|
||||
path: '/Users/luca/Laputa/project.md',
|
||||
filename: 'project.md',
|
||||
title: 'Project',
|
||||
isA: 'Type',
|
||||
@@ -602,7 +602,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/responsibility.md',
|
||||
path: '/Users/luca/Laputa/responsibility.md',
|
||||
filename: 'responsibility.md',
|
||||
title: 'Responsibility',
|
||||
isA: 'Type',
|
||||
@@ -630,7 +630,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/procedure.md',
|
||||
path: '/Users/luca/Laputa/procedure.md',
|
||||
filename: 'procedure.md',
|
||||
title: 'Procedure',
|
||||
isA: 'Type',
|
||||
@@ -658,7 +658,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/experiment.md',
|
||||
path: '/Users/luca/Laputa/experiment.md',
|
||||
filename: 'experiment.md',
|
||||
title: 'Experiment',
|
||||
isA: 'Type',
|
||||
@@ -686,7 +686,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/person.md',
|
||||
path: '/Users/luca/Laputa/person.md',
|
||||
filename: 'person.md',
|
||||
title: 'Person',
|
||||
isA: 'Type',
|
||||
@@ -714,7 +714,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/event.md',
|
||||
path: '/Users/luca/Laputa/event.md',
|
||||
filename: 'event.md',
|
||||
title: 'Event',
|
||||
isA: 'Type',
|
||||
@@ -742,7 +742,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/topic.md',
|
||||
path: '/Users/luca/Laputa/topic.md',
|
||||
filename: 'topic.md',
|
||||
title: 'Topic',
|
||||
isA: 'Type',
|
||||
@@ -770,7 +770,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/essay.md',
|
||||
path: '/Users/luca/Laputa/essay.md',
|
||||
filename: 'essay.md',
|
||||
title: 'Essay',
|
||||
isA: 'Type',
|
||||
@@ -798,7 +798,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/note.md',
|
||||
path: '/Users/luca/Laputa/note.md',
|
||||
filename: 'note.md',
|
||||
title: 'Note',
|
||||
isA: 'Type',
|
||||
@@ -827,7 +827,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
// --- Custom type documents ---
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/config.md',
|
||||
path: '/Users/luca/Laputa/config.md',
|
||||
filename: 'config.md',
|
||||
title: 'Config',
|
||||
isA: 'Type',
|
||||
@@ -855,7 +855,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/recipe.md',
|
||||
path: '/Users/luca/Laputa/recipe.md',
|
||||
filename: 'recipe.md',
|
||||
title: 'Recipe',
|
||||
isA: 'Type',
|
||||
@@ -883,7 +883,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/book.md',
|
||||
path: '/Users/luca/Laputa/book.md',
|
||||
filename: 'book.md',
|
||||
title: 'Book',
|
||||
isA: 'Type',
|
||||
@@ -931,7 +931,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
snippet: 'Vault instructions for AI agents. Defines how tools and integrations interact with this vault.',
|
||||
wordCount: 200,
|
||||
relationships: {
|
||||
'Type': ['[[type/config]]'],
|
||||
'Type': ['[[config]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -961,7 +961,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
snippet: 'Classic Roman pasta dish with eggs, pecorino, guanciale, and black pepper.',
|
||||
wordCount: 310,
|
||||
relationships: {
|
||||
'Type': ['[[type/recipe]]'],
|
||||
'Type': ['[[recipe]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -991,7 +991,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
snippet: 'Essential reading for anyone building distributed systems. Covers replication, partitioning, transactions.',
|
||||
wordCount: 100,
|
||||
relationships: {
|
||||
'Type': ['[[type/book]]'],
|
||||
'Type': ['[[book]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -1023,7 +1023,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 100,
|
||||
relationships: {
|
||||
'Belongs to': ['[[26q1-laputa-app]]'],
|
||||
'Type': ['[[type/note]]'],
|
||||
'Type': ['[[note]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -1053,7 +1053,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
snippet: 'Old API documentation for the v1 endpoint. Replaced by v2 docs.',
|
||||
wordCount: 85,
|
||||
relationships: {
|
||||
'Type': ['[[type/note]]'],
|
||||
'Type': ['[[note]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -1084,7 +1084,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 120,
|
||||
relationships: {
|
||||
'Related to': ['[[grow-newsletter]]'],
|
||||
'Type': ['[[type/experiment]]'],
|
||||
'Type': ['[[experiment]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
@@ -1123,7 +1123,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 342,
|
||||
relationships: {
|
||||
'Belongs to': ['[[q3-2025]]'],
|
||||
'Type': ['[[type/project]]'],
|
||||
'Type': ['[[project]]'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1154,7 +1154,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
|
||||
wordCount: 215,
|
||||
relationships: {
|
||||
'Related to': ['[[grow-newsletter]]'],
|
||||
'Type': ['[[type/experiment]]'],
|
||||
'Type': ['[[experiment]]'],
|
||||
},
|
||||
},
|
||||
// --- Refactoring entries for exact-match search testing ---
|
||||
|
||||
@@ -239,7 +239,7 @@ describe('buildRelationshipGroups', () => {
|
||||
it('excludes Type key from relationship groups', () => {
|
||||
const entity = makeEntry({
|
||||
path: '/Laputa/project/alpha.md', filename: 'alpha.md', title: 'Alpha',
|
||||
relationships: { Type: ['[[type/project]]'] },
|
||||
relationships: { Type: ['[[project]]'] },
|
||||
})
|
||||
const groups = buildRelationshipGroups(entity, [entity])
|
||||
const labels = groups.map((g) => g.label)
|
||||
@@ -272,7 +272,7 @@ describe('buildRelationshipGroups', () => {
|
||||
const instance1 = makeEntry({ path: '/Laputa/project/a.md', filename: 'a.md', title: 'Project A', isA: 'Project', modifiedAt: 1700000000 })
|
||||
const instance2 = makeEntry({ path: '/Laputa/project/b.md', filename: 'b.md', title: 'Project B', isA: 'Project', modifiedAt: 1700000000 })
|
||||
const typeEntity = makeEntry({
|
||||
path: '/Laputa/type/project.md', filename: 'project.md', title: 'Project',
|
||||
path: '/Laputa/project.md', filename: 'project.md', title: 'Project',
|
||||
isA: 'Type', relationships: {},
|
||||
})
|
||||
const groups = buildRelationshipGroups(typeEntity, [typeEntity, instance1, instance2])
|
||||
|
||||
@@ -32,12 +32,12 @@ function makeEntry(overrides: Partial<VaultEntry>): VaultEntry {
|
||||
}
|
||||
}
|
||||
|
||||
const typeProject = makeEntry({ path: '/vault/type/project.md', filename: 'project.md', title: 'Project', isA: 'Type', color: 'red' })
|
||||
const typePerson = makeEntry({ path: '/vault/type/person.md', filename: 'person.md', title: 'Person', isA: 'Type', color: 'yellow' })
|
||||
const typeEvent = makeEntry({ path: '/vault/type/event.md', filename: 'event.md', title: 'Event', isA: 'Type', color: 'yellow' })
|
||||
const typeTopic = makeEntry({ path: '/vault/type/topic.md', filename: 'topic.md', title: 'Topic', isA: 'Type', color: 'green' })
|
||||
const typeRecipe = makeEntry({ path: '/vault/type/recipe.md', filename: 'recipe.md', title: 'Recipe', isA: 'Type', color: 'orange', icon: 'cooking-pot' })
|
||||
const typeNote = makeEntry({ path: '/vault/type/note.md', filename: 'note.md', title: 'Note', isA: 'Type', color: 'blue' })
|
||||
const typeProject = makeEntry({ path: '/vault/project.md', filename: 'project.md', title: 'Project', isA: 'Type', color: 'red' })
|
||||
const typePerson = makeEntry({ path: '/vault/person.md', filename: 'person.md', title: 'Person', isA: 'Type', color: 'yellow' })
|
||||
const typeEvent = makeEntry({ path: '/vault/event.md', filename: 'event.md', title: 'Event', isA: 'Type', color: 'yellow' })
|
||||
const typeTopic = makeEntry({ path: '/vault/topic.md', filename: 'topic.md', title: 'Topic', isA: 'Type', color: 'green' })
|
||||
const typeRecipe = makeEntry({ path: '/vault/recipe.md', filename: 'recipe.md', title: 'Recipe', isA: 'Type', color: 'orange', icon: 'cooking-pot' })
|
||||
const typeNote = makeEntry({ path: '/vault/note.md', filename: 'note.md', title: 'Note', isA: 'Type', color: 'blue' })
|
||||
|
||||
const projectEntry = makeEntry({ path: '/vault/project/app.md', filename: 'app.md', title: 'Build App', isA: 'Project' })
|
||||
const personEntry = makeEntry({ path: '/vault/person/alice.md', filename: 'alice.md', title: 'Alice', isA: 'Person', aliases: ['Alice Smith'] })
|
||||
|
||||
Reference in New Issue
Block a user