fix: canonicalize hidden system metadata

This commit is contained in:
lucaronin
2026-04-16 06:21:34 +02:00
parent e8e2004aa8
commit e9c1ae2b8a
11 changed files with 700 additions and 750 deletions

View File

@@ -0,0 +1,127 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { DynamicPropertiesPanel } from './DynamicPropertiesPanel'
import { FOCUS_NOTE_ICON_PROPERTY_EVENT } from './noteIconPropertyEvents'
import type { VaultEntry } from '../types'
beforeAll(() => {
global.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} }
Element.prototype.scrollIntoView = vi.fn()
Element.prototype.hasPointerCapture = () => false
Element.prototype.setPointerCapture = vi.fn()
Element.prototype.releasePointerCapture = vi.fn()
if (!window.getComputedStyle) window.getComputedStyle = vi.fn().mockReturnValue({}) as never
})
const makeEntry = (overrides: Partial<VaultEntry> = {}): VaultEntry => ({
path: '/vault/note.md',
filename: 'note.md',
title: 'Note',
isA: 'Note',
aliases: [],
belongsTo: [],
relatedTo: [],
status: null,
archived: false,
modifiedAt: 0,
createdAt: 0,
fileSize: 0,
snippet: '',
wordCount: 0,
relationships: {},
icon: null,
color: null,
order: null,
sidebarLabel: null,
template: null,
sort: null,
view: null,
visible: null,
organized: false,
favorite: false,
favoriteIndex: null,
listPropertiesDisplay: [],
outgoingLinks: [],
properties: {},
hasH1: true,
fileKind: 'markdown',
...overrides,
})
function hasSuggestedSlot(label: string): boolean {
return screen
.queryAllByTestId('suggested-property')
.some((node) => node.textContent?.includes(label))
}
describe('DynamicPropertiesPanel system metadata', () => {
const onAddProperty = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it('hides underscored and legacy system metadata while keeping user properties visible', () => {
render(
<DynamicPropertiesPanel
entry={makeEntry()}
content=""
frontmatter={{
_list_properties_display: ['Owner'],
_icon: 'rocket',
icon: 'legacy',
order: 4,
sort: 'title:asc',
'_sidebar_label': 'Projects',
Owner: 'Luca',
}}
/>,
)
expect(screen.getByText('Owner')).toBeInTheDocument()
expect(screen.getByText('Luca')).toBeInTheDocument()
expect(screen.queryByText('Icon')).not.toBeInTheDocument()
expect(screen.queryByText('Order')).not.toBeInTheDocument()
expect(screen.queryByText('Sort')).not.toBeInTheDocument()
expect(screen.queryByText('Sidebar label')).not.toBeInTheDocument()
})
it('treats _icon as satisfying the suggested icon slot', () => {
render(
<DynamicPropertiesPanel
entry={makeEntry()}
content=""
frontmatter={{ _icon: 'rocket' }}
onAddProperty={onAddProperty}
/>,
)
expect(hasSuggestedSlot('Icon')).toBe(false)
})
it('opens the icon editor without writing metadata until the user saves', async () => {
render(
<DynamicPropertiesPanel
entry={makeEntry()}
content=""
frontmatter={{}}
onAddProperty={onAddProperty}
/>,
)
act(() => {
window.dispatchEvent(new CustomEvent(FOCUS_NOTE_ICON_PROPERTY_EVENT))
})
await waitFor(() => {
expect(screen.getByTestId('icon-editable-input')).toBeInTheDocument()
})
expect(onAddProperty).not.toHaveBeenCalled()
const input = screen.getByTestId('icon-editable-input')
fireEvent.change(input, { target: { value: 'rocket' } })
fireEvent.keyDown(input, { key: 'Enter' })
expect(onAddProperty).toHaveBeenCalledWith('_icon', 'rocket')
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ import type { PropertyDisplayMode } from '../utils/propertyTypes'
import { FOCUS_NOTE_ICON_PROPERTY_EVENT } from './noteIconPropertyEvents'
import { PROPERTY_PANEL_GRID_STYLE, PROPERTY_PANEL_ROW_STYLE } from './propertyPanelLayout'
import { humanizePropertyKey } from '../utils/propertyLabels'
import { canonicalSystemMetadataKey, hasSystemMetadataKey } from '../utils/systemMetadata'
// eslint-disable-next-line react-refresh/only-export-components -- utility co-located with component
export function containsWikilinks(value: FrontmatterValue): boolean {
@@ -107,6 +108,7 @@ function SuggestedPropertySlot({ label, onAdd }: { label: string; onAdd: () => v
function getExistingPropertyKeys(propertyEntries: [string, FrontmatterValue][], frontmatter: ParsedFrontmatter): Set<string> {
const keys = new Set(propertyEntries.map(([key]) => key.toLowerCase()))
for (const key of Object.keys(frontmatter)) keys.add(key.toLowerCase())
if (hasSystemMetadataKey(keys, '_icon')) keys.add('icon')
return keys
}
@@ -118,34 +120,25 @@ function getMissingSuggestedProperties(canAddProperty: boolean, existingKeys: Se
)
}
function getIconPropertyKey(propertyEntries: [string, FrontmatterValue][]) {
return propertyEntries.find(([key]) => key.toLowerCase() === 'icon')?.[0]
}
function useFocusNoteIconProperty({
onAddProperty,
propertyEntries,
setEditingKey,
setPendingSuggestedKey,
}: {
onAddProperty?: (key: string, value: FrontmatterValue) => void
propertyEntries: [string, FrontmatterValue][]
setEditingKey: (key: string | null) => void
setPendingSuggestedKey: (key: string | null) => void
}) {
useEffect(() => {
const handleFocusNoteIcon = () => {
const existingIconKey = getIconPropertyKey(propertyEntries)
if (!existingIconKey) {
if (!onAddProperty) return
onAddProperty('icon', '')
}
setEditingKey(existingIconKey ?? 'icon')
if (!onAddProperty) return
setPendingSuggestedKey('icon')
setEditingKey('icon')
}
window.addEventListener(FOCUS_NOTE_ICON_PROPERTY_EVENT, handleFocusNoteIcon)
return () => window.removeEventListener(FOCUS_NOTE_ICON_PROPERTY_EVENT, handleFocusNoteIcon)
}, [onAddProperty, propertyEntries, setEditingKey])
}, [onAddProperty, setEditingKey, setPendingSuggestedKey])
}
export function DynamicPropertiesPanel({
@@ -195,10 +188,10 @@ export function DynamicPropertiesPanel({
if (!trimmed) {
return
}
onAddProperty(key, trimmed)
onAddProperty(key === 'icon' ? canonicalSystemMetadataKey(key) : key, trimmed)
}, [onAddProperty, setEditingKey])
useFocusNoteIconProperty({ onAddProperty, propertyEntries, setEditingKey })
useFocusNoteIconProperty({ onAddProperty, setEditingKey, setPendingSuggestedKey })
return (
<div className="flex flex-col gap-3">