From c0df2124ff8749592127d0f7fe522f8934df2816 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 6 Apr 2026 16:10:02 +0200 Subject: [PATCH] fix: sync custom properties to VaultEntry on frontmatter changes View filters that reference custom properties (e.g. "Trashed", "Priority") were not reactively updating because contentToEntryPatch and frontmatterToEntryPatch only synced known fields (type, status, etc.) to the VaultEntry. Custom frontmatter keys were silently dropped. Now frontmatterToEntryPatch produces a propertiesPatch for unknown keys, and contentToEntryPatch includes them in the properties field. This ensures evaluateView sees up-to-date custom property values when re-filtering the note list. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hooks/frontmatterOps.ts | 42 +++++++++++++++++++++++++++----- src/hooks/useNoteActions.test.ts | 10 +++++--- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/hooks/frontmatterOps.ts b/src/hooks/frontmatterOps.ts index 3f525a4f..a016c5f2 100644 --- a/src/hooks/frontmatterOps.ts +++ b/src/hooks/frontmatterOps.ts @@ -37,9 +37,14 @@ function extractWikilinks(value: FrontmatterValue): string[] { */ export type RelationshipPatch = Record +/** Properties patch: a partial update to merge into `entry.properties`. + * Keys map to their new scalar values. A `null` value means "remove this key". */ +export type PropertiesPatch = Record + export interface EntryPatchResult { patch: Partial relationshipPatch: RelationshipPatch | null + propertiesPatch: PropertiesPatch | null } /** Map a frontmatter key+value to the corresponding VaultEntry field(s). */ @@ -49,7 +54,8 @@ export function frontmatterToEntryPatch( const k = key.toLowerCase().replace(/\s+/g, '_') if (op === 'delete') { const relPatch: RelationshipPatch = { [key]: null } - return { patch: ENTRY_DELETE_MAP[k] ?? {}, relationshipPatch: relPatch } + const propPatch: PropertiesPatch | null = !(k in ENTRY_DELETE_MAP) ? { [key]: null } : null + return { patch: ENTRY_DELETE_MAP[k] ?? {}, relationshipPatch: relPatch, propertiesPatch: propPatch } } const str = value != null ? String(value) : null const arr = Array.isArray(value) ? value.map(String) : [] @@ -73,17 +79,24 @@ export function frontmatterToEntryPatch( const wikilinks = value != null ? extractWikilinks(value) : [] const relationshipPatch: RelationshipPatch | null = wikilinks.length > 0 ? { [key]: wikilinks } : null - return { patch: updates[k] ?? {}, relationshipPatch } + // For unknown keys (custom properties), produce a propertiesPatch + const isKnownKey = k in updates + const propertiesPatch: PropertiesPatch | null = + !isKnownKey && value != null ? { [key]: typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? value : String(value) } : null + return { patch: updates[k] ?? {}, relationshipPatch, propertiesPatch } } /** Parse frontmatter from full content and return a merged VaultEntry patch for all known fields. */ export function contentToEntryPatch(content: string): Partial { const fm = parseFrontmatter(content) const merged: Partial = {} + const customProps: Record = {} for (const [key, value] of Object.entries(fm)) { - const { patch } = frontmatterToEntryPatch('update', key, value) + const { patch, propertiesPatch } = frontmatterToEntryPatch('update', key, value) Object.assign(merged, patch) + if (propertiesPatch) Object.assign(customProps, propertiesPatch) } + if (Object.keys(customProps).length > 0) merged.properties = customProps return merged } @@ -117,6 +130,18 @@ export interface FrontmatterOpOptions { silent?: boolean } +/** Apply a properties patch by merging into the existing properties map. */ +export function applyPropertiesPatch( + existing: Record, propPatch: PropertiesPatch, +): Record { + const merged = { ...existing } + for (const [k, v] of Object.entries(propPatch)) { + if (v === null) delete merged[k] + else merged[k] = v + } + return merged +} + /** Apply a relationship patch by merging into the existing relationships map. */ export function applyRelationshipPatch( existing: Record, relPatch: RelationshipPatch, @@ -144,12 +169,17 @@ export async function runFrontmatterAndApply( try { const newContent = await executeFrontmatterOp(op, path, key, value) callbacks.updateTab(path, newContent) - const { patch, relationshipPatch } = frontmatterToEntryPatch(op, key, value) + const { patch, relationshipPatch, propertiesPatch } = frontmatterToEntryPatch(op, key, value) const fullPatch = { ...patch } - if (relationshipPatch && callbacks.getEntry) { + if ((relationshipPatch || propertiesPatch) && callbacks.getEntry) { const current = callbacks.getEntry(path) if (current) { - fullPatch.relationships = applyRelationshipPatch(current.relationships, relationshipPatch) + if (relationshipPatch) { + fullPatch.relationships = applyRelationshipPatch(current.relationships, relationshipPatch) + } + if (propertiesPatch) { + fullPatch.properties = applyPropertiesPatch(current.properties, propertiesPatch) + } } } if (Object.keys(fullPatch).length > 0) callbacks.updateEntry(path, fullPatch) diff --git a/src/hooks/useNoteActions.test.ts b/src/hooks/useNoteActions.test.ts index 414d564d..cc5dcc67 100644 --- a/src/hooks/useNoteActions.test.ts +++ b/src/hooks/useNoteActions.test.ts @@ -373,9 +373,10 @@ describe('frontmatterToEntryPatch', () => { expect(result.relationshipPatch).toEqual({ 'Belongs to': ['[[x]]'] }) }) - it('returns empty patch for unknown non-wikilink keys', () => { + it('returns propertiesPatch for unknown non-wikilink keys', () => { const result = frontmatterToEntryPatch('update', 'custom_field', 'value') expect(result.patch).toEqual({}) + expect(result.propertiesPatch).toEqual({ custom_field: 'value' }) // Non-wikilink value → no relationship change expect(result.relationshipPatch).toBeNull() }) @@ -417,10 +418,11 @@ describe('frontmatterToEntryPatch', () => { expect(result.patch).toEqual({ listPropertiesDisplay: [] }) }) - it('returns empty patch for unknown key on delete, with relationship removal', () => { + it('returns empty patch for unknown key on delete, with relationship and properties removal', () => { const result = frontmatterToEntryPatch('delete', 'unknown_key') expect(result.patch).toEqual({}) expect(result.relationshipPatch).toEqual({ unknown_key: null }) + expect(result.propertiesPatch).toEqual({ unknown_key: null }) }) it('delete of known key also produces relationship removal', () => { @@ -490,9 +492,9 @@ describe('contentToEntryPatch', () => { expect(contentToEntryPatch(content)).toEqual({ isA: 'Type', sidebarLabel: 'Projects' }) }) - it('ignores unknown frontmatter keys', () => { + it('includes custom frontmatter keys in properties patch', () => { const content = '---\ntype: Note\ncustom: value\n---\n' - expect(contentToEntryPatch(content)).toEqual({ isA: 'Note' }) + expect(contentToEntryPatch(content)).toEqual({ isA: 'Note', properties: { custom: 'value' } }) }) it('preserves _favorite_index as a number (not null)', () => {