feat: show property kind icons

This commit is contained in:
lucaronin
2026-04-16 07:27:11 +02:00
parent 1b10aa9660
commit f8bd3e1b4a
5 changed files with 81 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
import type { ComponentProps } from 'react'
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'
import { DynamicPropertiesPanel, containsWikilinks } from './DynamicPropertiesPanel'
import type { VaultEntry } from '../types'
import { bindVaultConfigStore, getVaultConfig, resetVaultConfigStore } from '../utils/vaultConfigStore'
@@ -125,6 +125,16 @@ describe('DynamicPropertiesPanel', () => {
expect(screen.getByText('Note')).toBeInTheDocument()
})
it('shows the shared type icon in the type row label', () => {
renderPanel({
content: '# Test\n\nSome words here',
frontmatter: { Status: 'Active' },
onUpdateProperty,
})
expect(screen.getByTestId('type-row-icon')).toBeInTheDocument()
})
it('renders status as colored pill', () => {
renderPanel({ frontmatter: { Status: 'Active' } })
// Status rendered as sentence case
@@ -438,6 +448,15 @@ describe('DynamicPropertiesPanel', () => {
expect(screen.getByText('Icon')).toBeInTheDocument()
})
it('shows a property-kind icon for each suggested slot', () => {
renderPanel({ onAddProperty })
expect(within(findSuggestedSlot('Status')).getByTestId('suggested-property-icon-status')).toBeInTheDocument()
expect(within(findSuggestedSlot('Date')).getByTestId('suggested-property-icon-date')).toBeInTheDocument()
expect(within(findSuggestedSlot('URL')).getByTestId('suggested-property-icon-url')).toBeInTheDocument()
expect(within(findSuggestedSlot('Icon')).getByTestId('suggested-property-icon-text')).toBeInTheDocument()
})
it('hides Status slot when Status property already exists', () => {
renderPanel({
frontmatter: { Status: 'Active' },

View File

@@ -3,7 +3,7 @@ import type { VaultEntry } from '../types'
import type { FrontmatterValue } from './Inspector'
import type { ParsedFrontmatter } from '../utils/frontmatter'
import { usePropertyPanelState } from '../hooks/usePropertyPanelState'
import { getEffectiveDisplayMode, detectPropertyType } from '../utils/propertyTypes'
import { getEffectiveDisplayMode, detectPropertyType, DISPLAY_MODE_ICONS } from '../utils/propertyTypes'
import { SmartPropertyValueCell, DisplayModeSelector } from './PropertyValueCells'
import { TypeSelector } from './TypeSelector'
import { AddPropertyForm } from './AddPropertyForm'
@@ -89,7 +89,13 @@ function getSuggestedDisplayMode(key: string): PropertyDisplayMode {
return SUGGESTED_PROPERTY_MODES[key] ?? 'text'
}
function SuggestedPropertySlot({ label, onAdd }: { label: string; onAdd: () => void }) {
function SuggestedPropertySlot({ label, displayMode, onAdd }: {
label: string
displayMode: PropertyDisplayMode
onAdd: () => void
}) {
const SuggestedIcon = DISPLAY_MODE_ICONS[displayMode]
return (
<button
className={SUGGESTED_PROPERTY_SLOT_CLASS_NAME}
@@ -99,7 +105,10 @@ function SuggestedPropertySlot({ label, onAdd }: { label: string; onAdd: () => v
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onAdd() } }}
data-testid="suggested-property"
>
<span className="min-w-0 max-w-full truncate text-[12px] text-muted-foreground/50">{label}</span>
<span className="flex min-w-0 max-w-full items-center gap-1 text-[12px] text-muted-foreground/50">
<SuggestedIcon className="size-3.5 shrink-0 text-muted-foreground/40" data-testid={`suggested-property-icon-${displayMode}`} />
<span className="min-w-0 truncate">{label}</span>
</span>
<span className="min-w-0 truncate text-[12px] text-muted-foreground/30">{'\u2014'}</span>
</button>
)
@@ -228,7 +237,12 @@ export function DynamicPropertiesPanel({
/>
)}
{missingSuggested.map(({ key, label }) => (
<SuggestedPropertySlot key={key} label={label} onAdd={() => handleSuggestedAdd(key)} />
<SuggestedPropertySlot
key={key}
label={label}
displayMode={getSuggestedDisplayMode(key)}
onAdd={() => handleSuggestedAdd(key)}
/>
))}
</div>
{showAddDialog

View File

@@ -1,5 +1,6 @@
import { useState, useCallback, useRef } from 'react'
import { createPortal } from 'react-dom'
import { ArrowUpRight } from '@phosphor-icons/react'
import type { FrontmatterValue } from './Inspector'
import { EditableValue, TagPillList, UrlValue } from './EditableValue'
import { isUrlValue } from '../utils/url'
@@ -35,6 +36,16 @@ function dateToISO(day: Date): string {
return `${yyyy}-${mm}-${dd}`
}
const RELATIONSHIP_PROPERTY_KEYS = new Set(['belongs_to', 'related_to', 'has'])
function normalizePropertyKey(propKey: string): string {
return propKey.trim().toLowerCase().replace(/[\s-]+/g, '_')
}
function showsRelationshipPropertyIcon(propKey: string): boolean {
return RELATIONSHIP_PROPERTY_KEYS.has(normalizePropertyKey(propKey))
}
function StatusValue({ propKey, value, isEditing, vaultStatuses, onSave, onStartEdit }: {
propKey: string; value: FrontmatterValue; isEditing: boolean; vaultStatuses: string[]
onSave: (key: string, value: string) => void; onStartEdit: (key: string | null) => void
@@ -213,6 +224,7 @@ export function DisplayModeSelector({ propKey, currentMode, autoMode, onSelect }
const [open, setOpen] = useState(false)
const triggerRef = useRef<HTMLButtonElement>(null)
const CurrentIcon = DISPLAY_MODE_ICONS[currentMode]
const showRelationshipIcon = showsRelationshipPropertyIcon(propKey)
const positionMenu = useCallback((node: HTMLDivElement | null) => {
if (!node) return
@@ -246,7 +258,11 @@ export function DisplayModeSelector({ propKey, currentMode, autoMode, onSelect }
aria-label={`Change ${propKey} type`}
data-testid="display-mode-trigger"
>
<CurrentIcon className="size-3.5" data-testid={`display-mode-icon-${currentMode}`} />
{showRelationshipIcon ? (
<ArrowUpRight className="size-3.5" data-testid="display-mode-icon-relationship" />
) : (
<CurrentIcon className="size-3.5" data-testid={`display-mode-icon-${currentMode}`} />
)}
</button>
{open && createPortal(
<>

View File

@@ -1,4 +1,4 @@
import { CaretUpDown, Check } from '@phosphor-icons/react'
import { CaretUpDown, Check, StackSimple } from '@phosphor-icons/react'
import { useEffect, useId, useMemo, useRef, useState, type KeyboardEvent, type PointerEvent } from 'react'
import type { FrontmatterValue } from './Inspector'
import { Button } from '@/components/ui/button'
@@ -92,11 +92,20 @@ function TypeSelectorValue({
)
}
function TypeRowLabel() {
return (
<span className="flex shrink-0 items-center gap-1 text-[12px] text-muted-foreground">
<StackSimple size={14} className="shrink-0" data-testid="type-row-icon" />
<span>Type</span>
</span>
)
}
function ReadOnlyType({ isA, customColorKey, onNavigate }: { isA?: string | null; customColorKey?: string | null; onNavigate?: (target: string) => void }) {
if (!isA) return null
return (
<div className="grid min-w-0 grid-cols-2 items-center gap-2 px-1.5" style={PROPERTY_PANEL_ROW_STYLE}>
<span className="text-[12px] shrink-0 text-muted-foreground">Type</span>
<TypeRowLabel />
<div className="min-w-0">
{onNavigate ? (
<button
@@ -253,7 +262,7 @@ function EditableTypeSelector({ isA, customColorKey, availableTypes, typeColorKe
return (
<div className="grid min-w-0 grid-cols-2 items-center gap-2 px-1.5" style={PROPERTY_PANEL_ROW_STYLE} data-testid="type-selector">
<span className="text-[12px] shrink-0 text-muted-foreground">Type</span>
<TypeRowLabel />
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverAnchor asChild>
<div ref={rootRef} className="min-w-0">

View File

@@ -1,5 +1,5 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { DynamicPropertiesPanel } from './DynamicPropertiesPanel'
import type { VaultEntry } from '../types'
import { initDisplayModeOverrides } from '../utils/propertyTypes'
@@ -101,4 +101,17 @@ describe('property type icon control', () => {
expect(screen.queryByDisplayValue('Weekly')).not.toBeInTheDocument()
})
it('shows the relationship icon for relationship-like property rows', () => {
render(
<DynamicPropertiesPanel
entry={makeEntry()}
frontmatter={{ belongs_to: 'Project Alpha' }}
onUpdateProperty={vi.fn()}
/>
)
const row = screen.getByTestId('editable-property')
expect(within(row).getByTestId('display-mode-icon-relationship')).toBeInTheDocument()
})
})