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)', () => {