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) <noreply@anthropic.com>
This commit is contained in:
@@ -37,9 +37,14 @@ function extractWikilinks(value: FrontmatterValue): string[] {
|
||||
*/
|
||||
export type RelationshipPatch = Record<string, string[] | null>
|
||||
|
||||
/** 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<string, string | number | boolean | null>
|
||||
|
||||
export interface EntryPatchResult {
|
||||
patch: Partial<VaultEntry>
|
||||
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<VaultEntry> {
|
||||
const fm = parseFrontmatter(content)
|
||||
const merged: Partial<VaultEntry> = {}
|
||||
const customProps: Record<string, string | number | boolean | null> = {}
|
||||
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<string, string | number | boolean | null>, propPatch: PropertiesPatch,
|
||||
): Record<string, string | number | boolean | null> {
|
||||
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<string, string[]>, 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)
|
||||
|
||||
@@ -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)', () => {
|
||||
|
||||
Reference in New Issue
Block a user