From f8bd3e1b4ae14ffcbfe0ce6abfd155f6e77df9b9 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Thu, 16 Apr 2026 07:27:11 +0200 Subject: [PATCH] feat: show property kind icons --- .../DynamicPropertiesPanel.test.tsx | 21 +++++++++++++++++- src/components/DynamicPropertiesPanel.tsx | 22 +++++++++++++++---- src/components/PropertyValueCells.tsx | 18 ++++++++++++++- src/components/TypeSelector.tsx | 15 ++++++++++--- .../propertyTypeIconControl.test.tsx | 15 ++++++++++++- 5 files changed, 81 insertions(+), 10 deletions(-) diff --git a/src/components/DynamicPropertiesPanel.test.tsx b/src/components/DynamicPropertiesPanel.test.tsx index 6d788963..ffacba34 100644 --- a/src/components/DynamicPropertiesPanel.test.tsx +++ b/src/components/DynamicPropertiesPanel.test.tsx @@ -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' }, diff --git a/src/components/DynamicPropertiesPanel.tsx b/src/components/DynamicPropertiesPanel.tsx index 374eadc6..40a4b523 100644 --- a/src/components/DynamicPropertiesPanel.tsx +++ b/src/components/DynamicPropertiesPanel.tsx @@ -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 ( ) @@ -228,7 +237,12 @@ export function DynamicPropertiesPanel({ /> )} {missingSuggested.map(({ key, label }) => ( - handleSuggestedAdd(key)} /> + handleSuggestedAdd(key)} + /> ))} {showAddDialog diff --git a/src/components/PropertyValueCells.tsx b/src/components/PropertyValueCells.tsx index ef4f2ec5..c8e183d5 100644 --- a/src/components/PropertyValueCells.tsx +++ b/src/components/PropertyValueCells.tsx @@ -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(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" > - + {showRelationshipIcon ? ( + + ) : ( + + )} {open && createPortal( <> diff --git a/src/components/TypeSelector.tsx b/src/components/TypeSelector.tsx index 9f4c5e72..1db3db55 100644 --- a/src/components/TypeSelector.tsx +++ b/src/components/TypeSelector.tsx @@ -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 ( + + + Type + + ) +} + function ReadOnlyType({ isA, customColorKey, onNavigate }: { isA?: string | null; customColorKey?: string | null; onNavigate?: (target: string) => void }) { if (!isA) return null return (
- Type +
{onNavigate ? (