refactor: replace relationship-based inbox logic with _organized property

Inbox now uses an explicit _organized frontmatter property instead of
inferring organization from relationships. This is simpler and gives
users direct control over inbox membership.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-04-04 20:45:25 +02:00
parent bfe06ae5c4
commit 49d7353f2d
9 changed files with 97 additions and 178 deletions

View File

@@ -59,6 +59,8 @@ pub struct VaultEntry {
pub view: Option<String>,
/// Whether this Type is visible in the sidebar. Defaults to true when absent.
pub visible: Option<bool>,
/// Whether this note has been explicitly organized (removed from Inbox).
pub organized: bool,
/// Whether this note is a user favorite (shown in FAVORITES sidebar section).
pub favorite: bool,
/// Display order within the FAVORITES section (lower = higher).

View File

@@ -47,6 +47,12 @@ pub(crate) struct Frontmatter {
pub view: Option<StringOrList>,
#[serde(default)]
pub visible: Option<bool>,
#[serde(
rename = "_organized",
default,
deserialize_with = "deserialize_bool_or_string"
)]
pub organized: Option<bool>,
#[serde(
rename = "_favorite",
default,
@@ -169,6 +175,7 @@ fn parse_frontmatter(data: &HashMap<String, serde_json::Value>) -> Frontmatter {
"notion_id",
"Status",
"status",
"_organized",
"_favorite",
"_favorite_index",
"_list_properties_display",
@@ -206,6 +213,7 @@ const SKIP_KEYS: &[&str] = &[
"view",
"visible",
"status",
"_organized",
"_favorite",
"_favorite_index",
"_list_properties_display",

View File

@@ -115,6 +115,7 @@ pub fn parse_md_file(path: &Path, git_dates: Option<(u64, u64)>) -> Result<Vault
sort: frontmatter.sort.and_then(|v| v.into_scalar()),
view: frontmatter.view.and_then(|v| v.into_scalar()),
visible: frontmatter.visible,
organized: frontmatter.organized.unwrap_or(false),
favorite: frontmatter.favorite.unwrap_or(false),
favorite_index: frontmatter.favorite_index,
list_properties_display: frontmatter.list_properties_display.unwrap_or_default(),

View File

@@ -14,6 +14,7 @@ const ENTRY_DELETE_MAP: Record<string, Partial<VaultEntry>> = {
_trashed: { trashed: false }, trashed: { trashed: false },
order: { order: null },
template: { template: null }, sort: { sort: null }, visible: { visible: null },
_organized: { organized: false },
_favorite: { favorite: false }, _favorite_index: { favoriteIndex: null },
_list_properties_display: { listPropertiesDisplay: [] },
}
@@ -63,6 +64,7 @@ export function frontmatterToEntryPatch(
sort: { sort: str },
view: { view: str },
visible: { visible: value === false ? false : null },
_organized: { organized: Boolean(value) },
_favorite: { favorite: Boolean(value) },
_favorite_index: { favoriteIndex: typeof value === 'number' ? value : null },
_list_properties_display: { listPropertiesDisplay: Array.isArray(value) ? value.map(String) : [] },

View File

@@ -20,7 +20,7 @@ export function buildNewEntry({ path, slug, title, type, status }: NewEntryParam
aliases: [], belongsTo: [], relatedTo: [],
status, archived: false, trashed: false, trashedAt: null,
modifiedAt: now, createdAt: now, fileSize: 0,
snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, outgoingLinks: [], sidebarLabel: null, template: null, sort: null, view: null, visible: null, properties: {}, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, outgoingLinks: [], sidebarLabel: null, template: null, sort: null, view: null, visible: null, properties: {}, organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
}
}

View File

@@ -37,7 +37,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: ['q1-2026', 'software-development', 'matteo-cellini', 'maria-bianchi', 'marco-verdi'],
properties: { Priority: 'High', 'Due date': '2026-06-15', Owner: 'Luca Rossi' },
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/grow-newsletter.md',
@@ -73,7 +73,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: ['on-writing-well', 'engineering-leadership-101', 'ai-agents-primer', 'growth', 'writing'],
properties: { Priority: 'High', Rating: 5, Cadence: 'Weekly', Owner: 'Luca Rossi' },
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/manage-sponsorships.md',
@@ -103,7 +103,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: ['matteo-cellini'],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/write-weekly-essays.md',
@@ -133,7 +133,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: ['grow-newsletter'],
properties: { Owner: 'Luca Rossi', Cadence: 'Weekly' },
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/run-sponsorships.md',
@@ -163,7 +163,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: ['manage-sponsorships'],
properties: { Owner: 'Matteo Cellini', Cadence: 'Weekly' },
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/stock-screener.md',
@@ -194,7 +194,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: ['trading', 'algorithmic-trading', 'ema200-backtest-results'],
properties: { Priority: 'Low', 'Due date': '2026-03-01', Owner: 'Luca Rossi' },
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/facebook-ads-strategy.md',
@@ -225,7 +225,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: ['26q1-laputa-app', 'growth', 'ads'],
properties: { Priority: 'Medium', Rating: 4 },
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/budget-allocation.md',
@@ -255,7 +255,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: ['26q1-laputa-app'],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/matteo-cellini.md',
@@ -284,7 +284,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: { Company: 'Acme Corp', Role: 'Engineering Lead' },
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/maria-bianchi.md',
@@ -313,7 +313,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: { Company: 'TechStart', Role: 'Product Manager' },
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/marco-verdi.md',
@@ -342,7 +342,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/elena-russo.md',
@@ -371,7 +371,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/2026-02-14-laputa-app-kickoff.md',
@@ -401,7 +401,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: ['26q1-laputa-app', 'matteo-cellini'],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/software-development.md',
@@ -431,7 +431,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/trading.md',
@@ -461,7 +461,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/on-writing-well.md',
@@ -491,7 +491,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: ['grow-newsletter'],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/engineering-leadership-101.md',
@@ -522,7 +522,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: ['grow-newsletter', 'software-development'],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/ai-agents-primer.md',
@@ -552,7 +552,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: ['grow-newsletter'],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
// --- Type documents ---
{
@@ -580,7 +580,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/responsibility.md',
@@ -607,7 +607,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/procedure.md',
@@ -634,7 +634,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/experiment.md',
@@ -661,7 +661,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: false,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/person.md',
@@ -688,7 +688,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/event.md',
@@ -715,7 +715,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/topic.md',
@@ -742,7 +742,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/essay.md',
@@ -769,7 +769,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/note.md',
@@ -796,7 +796,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
// --- Custom type documents ---
{
@@ -824,7 +824,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/recipe.md',
@@ -851,7 +851,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/book.md',
@@ -878,7 +878,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
// --- Instances of custom types ---
{
@@ -908,7 +908,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/pasta-carbonara.md',
@@ -937,7 +937,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: { Difficulty: 'Easy', 'Prep time': '30 min', Servings: 4 },
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/designing-data-intensive-applications.md',
@@ -966,7 +966,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: { Author: 'Martin Kleppmann', Rating: 5, 'Year published': 2017 },
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
// --- Trashed entries ---
{
@@ -997,7 +997,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/deprecated-api-notes.md',
@@ -1026,7 +1026,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/failed-seo-experiment.md',
@@ -1056,7 +1056,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: { Owner: 'Luca Rossi' },
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
// --- Archived entries ---
{
@@ -1087,7 +1087,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
'Belongs to': ['[[q3-2025]]'],
'Type': ['[[project]]'],
},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/twitter-thread-experiment.md',
@@ -1117,7 +1117,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
'Related to': ['[[grow-newsletter]]'],
'Type': ['[[experiment]]'],
},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
// --- Refactoring entries for exact-match search testing ---
{
@@ -1145,7 +1145,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/refactoring-ideas.md',
@@ -1172,7 +1172,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/refactoring-key-ideas.md',
@@ -1199,7 +1199,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
{
path: '/Users/luca/Laputa/refactoring-patterns.md',
@@ -1226,7 +1226,7 @@ export const MOCK_ENTRIES: VaultEntry[] = [
template: null, sort: null, view: null, visible: null,
outgoingLinks: [],
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
},
]
@@ -1279,7 +1279,7 @@ function generateBulkEntries(count: number): VaultEntry[] {
sidebarLabel: null,
template: null, sort: null, view: null, visible: null,
properties: {},
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
})
}
return entries

View File

@@ -35,6 +35,8 @@ export interface VaultEntry {
view: string | null
/** Whether this Type is visible in the sidebar. Defaults to true when absent. */
visible: boolean | null
/** Whether this note has been explicitly organized (removed from Inbox). */
organized: boolean
/** Whether this note is a user favorite (shown in FAVORITES sidebar section). */
favorite: boolean
/** Display order within the FAVORITES section (lower = higher). */

View File

@@ -1,12 +1,12 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { formatSubtitle, formatSearchSubtitle, relativeDate, buildRelationshipGroups, getSortComparator, extractSortableProperties, getSortOptionLabel, getDefaultDirection, parseSortConfig, serializeSortConfig, buildValidLinkTargets, isInboxEntry, filterInboxEntries, filterEntries } from './noteListHelpers'
import { formatSubtitle, formatSearchSubtitle, relativeDate, buildRelationshipGroups, getSortComparator, extractSortableProperties, getSortOptionLabel, getDefaultDirection, parseSortConfig, serializeSortConfig, isInboxEntry, filterInboxEntries, filterEntries } from './noteListHelpers'
import type { VaultEntry } from '../types'
function makeEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {
return {
path: '/vault/note/test.md', filename: 'test.md', title: 'Test',
isA: 'Note', aliases: [], belongsTo: [], relatedTo: [],
status: null, archived: false,
status: null, organized: false, archived: false,
trashed: false, trashedAt: null,
modifiedAt: null, createdAt: null, fileSize: 0,
snippet: '', wordCount: 0, relationships: {},
@@ -566,79 +566,35 @@ describe('parseSortConfig', () => {
// --- Inbox ---
describe('buildValidLinkTargets', () => {
it('builds a set of titles, filename stems, and path stems', () => {
const entries = [
makeEntry({ path: '/vault/project/my-project.md', filename: 'my-project.md', title: 'My Project', aliases: ['MP'] }),
makeEntry({ path: '/vault/note/test.md', filename: 'test.md', title: 'Test Note' }),
]
const targets = buildValidLinkTargets(entries)
expect(targets.has('My Project')).toBe(true)
expect(targets.has('Test Note')).toBe(true)
expect(targets.has('my-project')).toBe(true)
expect(targets.has('test')).toBe(true)
expect(targets.has('MP')).toBe(true)
// path stems (last 2 segments without .md)
expect(targets.has('project/my-project')).toBe(true)
expect(targets.has('note/test')).toBe(true)
})
})
describe('isInboxEntry', () => {
const allEntries = [
makeEntry({ path: '/vault/topic/ai.md', filename: 'ai.md', title: 'AI', aliases: [] }),
makeEntry({ path: '/vault/project/laputa.md', filename: 'laputa.md', title: 'Laputa' }),
]
const validTargets = buildValidLinkTargets(allEntries)
it('returns true for a note that is not organized', () => {
const note = makeEntry({ organized: false })
expect(isInboxEntry(note)).toBe(true)
})
it('returns true for a note with no outgoing links and no relationships', () => {
const note = makeEntry({ outgoingLinks: [], relationships: {}, belongsTo: [], relatedTo: [] })
expect(isInboxEntry(note, validTargets)).toBe(true)
it('returns false for an organized note', () => {
const note = makeEntry({ organized: true })
expect(isInboxEntry(note)).toBe(false)
})
it('returns false for a trashed note', () => {
const note = makeEntry({ trashed: true, outgoingLinks: [], relationships: {} })
expect(isInboxEntry(note, validTargets)).toBe(false)
const note = makeEntry({ trashed: true })
expect(isInboxEntry(note)).toBe(false)
})
it('returns false for an archived note', () => {
const note = makeEntry({ archived: true, outgoingLinks: [], relationships: {} })
expect(isInboxEntry(note, validTargets)).toBe(false)
})
it('returns false for a note with valid outgoing links', () => {
const note = makeEntry({ outgoingLinks: ['AI'] })
expect(isInboxEntry(note, validTargets)).toBe(false)
})
it('returns true for a note with only broken outgoing links (non-existent targets)', () => {
const note = makeEntry({ outgoingLinks: ['NonExistent Page', 'Another Missing'] })
expect(isInboxEntry(note, validTargets)).toBe(true)
})
it('returns false for a note with valid frontmatter relationships', () => {
const note = makeEntry({ outgoingLinks: [], relationships: { 'Related to': ['[[AI]]'] } })
expect(isInboxEntry(note, validTargets)).toBe(false)
})
it('returns false for a note with belongsTo pointing to real note', () => {
const note = makeEntry({ outgoingLinks: [], belongsTo: ['[[Laputa]]'] })
expect(isInboxEntry(note, validTargets)).toBe(false)
})
it('returns false for a note with relatedTo pointing to real note', () => {
const note = makeEntry({ outgoingLinks: [], relatedTo: ['[[AI]]'] })
expect(isInboxEntry(note, validTargets)).toBe(false)
})
it('returns true for a note with only broken relationship refs', () => {
const note = makeEntry({ outgoingLinks: [], relationships: { 'Relates': ['[[Ghost]]'] }, belongsTo: ['[[Missing]]'] })
expect(isInboxEntry(note, validTargets)).toBe(true)
const note = makeEntry({ archived: true })
expect(isInboxEntry(note)).toBe(false)
})
it('excludes Type entries from inbox', () => {
const note = makeEntry({ isA: 'Type', outgoingLinks: [], relationships: {} })
expect(isInboxEntry(note, validTargets)).toBe(false)
const note = makeEntry({ isA: 'Type' })
expect(isInboxEntry(note)).toBe(false)
})
it('returns true for a note without _organized field (defaults to false)', () => {
const note = makeEntry({})
expect(isInboxEntry(note)).toBe(true)
})
})
@@ -647,11 +603,11 @@ describe('filterInboxEntries', () => {
const DAY = 86400
const allEntries = [
makeEntry({ path: '/vault/a.md', filename: 'a.md', title: 'A', createdAt: now - 2 * DAY, outgoingLinks: [] }),
makeEntry({ path: '/vault/b.md', filename: 'b.md', title: 'B', createdAt: now - 15 * DAY, outgoingLinks: [] }),
makeEntry({ path: '/vault/c.md', filename: 'c.md', title: 'C', createdAt: now - 60 * DAY, outgoingLinks: [] }),
makeEntry({ path: '/vault/d.md', filename: 'd.md', title: 'D', createdAt: now - 120 * DAY, outgoingLinks: [] }),
makeEntry({ path: '/vault/linked.md', filename: 'linked.md', title: 'Linked', createdAt: now - 1 * DAY, outgoingLinks: ['A'] }),
makeEntry({ path: '/vault/a.md', filename: 'a.md', title: 'A', createdAt: now - 2 * DAY }),
makeEntry({ path: '/vault/b.md', filename: 'b.md', title: 'B', createdAt: now - 15 * DAY }),
makeEntry({ path: '/vault/c.md', filename: 'c.md', title: 'C', createdAt: now - 60 * DAY }),
makeEntry({ path: '/vault/d.md', filename: 'd.md', title: 'D', createdAt: now - 120 * DAY }),
makeEntry({ path: '/vault/org.md', filename: 'org.md', title: 'Organized', createdAt: now - 1 * DAY, organized: true }),
]
it('filters by "week" period (last 7 days)', () => {
@@ -681,17 +637,17 @@ describe('filterInboxEntries', () => {
}
})
it('excludes linked notes', () => {
it('excludes organized notes', () => {
const result = filterInboxEntries(allEntries, 'all')
expect(result.find(e => e.title === 'Linked')).toBeUndefined()
expect(result.find(e => e.title === 'Organized')).toBeUndefined()
})
it('returns empty array when all notes have valid outgoing links', () => {
const linked = [
makeEntry({ path: '/vault/x.md', title: 'X', outgoingLinks: ['Y'] }),
makeEntry({ path: '/vault/y.md', title: 'Y', outgoingLinks: ['X'] }),
it('returns empty array when all notes are organized', () => {
const organized = [
makeEntry({ path: '/vault/x.md', title: 'X', organized: true }),
makeEntry({ path: '/vault/y.md', title: 'Y', organized: true }),
]
const result = filterInboxEntries(linked, 'all')
const result = filterInboxEntries(organized, 'all')
expect(result).toEqual([])
})
})

View File

@@ -381,82 +381,30 @@ export function countAllByFilter(entries: VaultEntry[]): Record<NoteListFilter,
// --- Inbox ---
/** Build a set of all valid link targets (titles, aliases, filename stems, path stems). */
export function buildValidLinkTargets(entries: VaultEntry[]): Set<string> {
const targets = new Set<string>()
for (const e of entries) {
targets.add(e.title)
const fileStem = e.filename.replace(/\.md$/, '')
targets.add(fileStem)
// path stem: everything after vault root, minus .md
// E.g. /Users/luca/Laputa/project/foo.md → project/foo
const parts = e.path.replace(/\.md$/, '').split('/')
// Try from index that gives "folder/name" pattern — skip first segments
if (parts.length >= 2) {
const last2 = parts.slice(-2).join('/')
if (last2 !== fileStem) targets.add(last2)
}
for (const alias of e.aliases) targets.add(alias)
}
return targets
}
function extractRef(raw: string): string {
return raw.replace(/^\[\[/, '').replace(/\]\]$/, '').split('|')[0]
}
function hasValidRef(refs: string[], validTargets: Set<string>): boolean {
return refs.some((raw) => {
const inner = extractRef(raw)
return validTargets.has(inner) || validTargets.has(inner.split('/').pop() ?? '')
})
}
/** Check if entry has any valid outgoing link (body or frontmatter) that resolves to a real note. */
export function isInboxEntry(entry: VaultEntry, validTargets: Set<string>): boolean {
/** Check if entry belongs in the Inbox (not organized, not trashed/archived, not a Type). */
export function isInboxEntry(entry: VaultEntry): boolean {
if (entry.trashed || entry.archived) return false
if (entry.isA === 'Type') return false
return !hasAnyValidLinks(entry, validTargets)
}
function hasAnyValidLinks(entry: VaultEntry, validTargets: Set<string>): boolean {
return hasValidBodyLinks(entry.outgoingLinks, validTargets) || hasValidFrontmatterLinks(entry, validTargets)
}
function hasValidBodyLinks(outgoingLinks: string[], validTargets: Set<string>): boolean {
return outgoingLinks.some((link) => validTargets.has(link) || validTargets.has(link.split('/').pop() ?? ''))
}
function hasValidFrontmatterLinks(entry: VaultEntry, validTargets: Set<string>): boolean {
if (entry.belongsTo?.length && hasValidRef(entry.belongsTo, validTargets)) return true
if (entry.relatedTo?.length && hasValidRef(entry.relatedTo, validTargets)) return true
if (entry.relationships) {
return Object.values(entry.relationships).some((refs) => hasValidRef(refs, validTargets))
}
return false
return !entry.organized
}
const INBOX_PERIOD_DAYS: Record<InboxPeriod, number> = {
week: 7, month: 30, quarter: 90, all: Infinity,
}
/** Filter entries for the Inbox view: no valid relationships, within the given time period, sorted by createdAt desc. */
/** Filter entries for the Inbox view: not organized, within the given time period, sorted by createdAt desc. */
export function filterInboxEntries(entries: VaultEntry[], period: InboxPeriod): VaultEntry[] {
const validTargets = buildValidLinkTargets(entries)
const now = Math.floor(Date.now() / 1000)
const cutoff = period === 'all' ? 0 : now - INBOX_PERIOD_DAYS[period] * 86400
return entries
.filter((e) => isInboxEntry(e, validTargets) && (e.createdAt ?? 0) >= cutoff)
.filter((e) => isInboxEntry(e) && (e.createdAt ?? 0) >= cutoff)
.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0))
}
/** Count inbox entries per period. */
export function countInboxByPeriod(entries: VaultEntry[]): Record<InboxPeriod, number> {
const validTargets = buildValidLinkTargets(entries)
const inbox = entries.filter((e) => isInboxEntry(e, validTargets))
const inbox = entries.filter((e) => isInboxEntry(e))
const now = Math.floor(Date.now() / 1000)
let week = 0, month = 0, quarter = 0