feat: show property kind icons
This commit is contained in:
@@ -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' },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
<>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user