From 87f847b71036cc18dabeb10863e2de500c15ba22 Mon Sep 17 00:00:00 2001 From: Luca Rossi Date: Fri, 27 Feb 2026 12:59:23 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20type-aware=20property=20value=20inputs?= =?UTF-8?q?=20=E2=80=94=20date=20picker,=20boolean=20toggle,=20status=20dr?= =?UTF-8?q?opdown=20(#110)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Property value inputs now adapt to their detected or overridden display mode: - Date properties show a calendar date picker (inline and add form) - Boolean properties show a yes/no toggle (handles string "true"/"false") - Status properties show a dropdown with vault + suggested statuses - Null/empty values are now routed through display mode instead of defaulting to text Refactored SmartPropertyValueCell and usePropertyPanelState for code health 10.0. Co-authored-by: Claude Opus 4.6 --- design/property-value-input.pen | 1 + .../DynamicPropertiesPanel.test.tsx | 172 ++++++++++ src/components/DynamicPropertiesPanel.tsx | 300 ++++++++++++------ 3 files changed, 376 insertions(+), 97 deletions(-) create mode 100644 design/property-value-input.pen diff --git a/design/property-value-input.pen b/design/property-value-input.pen new file mode 100644 index 00000000..3f78a1ea --- /dev/null +++ b/design/property-value-input.pen @@ -0,0 +1 @@ +{"children":[{"children":[{"content":"Text Input (default editing mode)","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"gfFsr","name":"lbl1","type":"text"},{"alignItems":"center","children":[{"content":"OWNER","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"U7qrk","letterSpacing":1.2,"name":"k1","type":"text"},{"content":"Luca","fill":"$--secondary-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"xus3c","name":"v1","type":"text"}],"cornerRadius":4,"fill":"$--bg-hover-subtle","id":"y19Vl","name":"Property Row — owner","padding":[4,6],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"content":"CADENCE","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"iXGSk","letterSpacing":1.2,"name":"k2","type":"text"},{"children":[{"content":"Weekly","fill":"$--foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"Dp7NE","name":"inpTxt","type":"text"}],"cornerRadius":4,"fill":"$--muted","id":"jDfek","name":"Text Input Active","padding":[4,8],"stroke":{"align":"inside","fill":"$--ring","thickness":1},"type":"frame","width":"fill_container"}],"cornerRadius":4,"id":"lBJ6j","name":"Property Row — cadence (editing)","padding":[4,6],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"content":"DESCRIPTION","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"wNhQR","letterSpacing":1.2,"name":"k3","type":"text"},{"content":"My project notes","fill":"$--secondary-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"TDgEG","name":"v3","type":"text"}],"cornerRadius":4,"id":"rQySL","name":"Property Row — description","padding":[4,6],"type":"frame","width":"fill_container"}],"fill":"$--background","gap":8,"id":"ue2qN","layout":"vertical","name":"Property Value — Text Input (default)","padding":12,"type":"frame","width":300,"x":0,"y":0},{"children":[{"content":"Date Picker (calendar popover on click)","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"U8pRy","name":"lbl2","type":"text"},{"alignItems":"center","children":[{"content":"DEADLINE","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"jVJZw","letterSpacing":1.2,"name":"kD1","type":"text"},{"alignItems":"center","children":[{"fill":"$--muted-foreground","height":12,"iconFontFamily":"lucide","iconFontName":"calendar","id":"9LGQq","name":"calIco","type":"icon_font","width":12},{"content":"Mar 31, 2026","fill":"$--secondary-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"huIJ2","name":"dateTxt","type":"text"}],"cornerRadius":4,"gap":4,"id":"2Nx8W","name":"Date Button","padding":[2,4],"type":"frame"}],"cornerRadius":4,"fill":"$--bg-hover-subtle","id":"gAwzs","name":"Property Row — deadline (date display)","padding":[4,6],"type":"frame","width":"fill_container"},{"children":[{"alignItems":"center","children":[{"fill":"$--muted-foreground","height":16,"iconFontFamily":"lucide","iconFontName":"chevron-left","id":"jGsuq","name":"calNav1","type":"icon_font","width":16},{"content":"March 2026","fill":"$--foreground","fontFamily":"Inter","fontSize":13,"fontWeight":"600","id":"Q0Hbu","name":"calMo","type":"text"},{"fill":"$--muted-foreground","height":16,"iconFontFamily":"lucide","iconFontName":"chevron-right","id":"fo6jZ","name":"calNav2","type":"icon_font","width":16}],"id":"NTfdy","name":"Calendar Header","type":"frame","width":"fill_container"},{"content":"Su Mo Tu We Th Fr Sa","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"normal","id":"5XDoM","letterSpacing":0.5,"name":"calDays","type":"text"},{"children":[{"content":" 1 2 3 4 5 6 7","fill":"$--foreground","fontFamily":"IBM Plex Mono","fontSize":11,"fontWeight":"normal","id":"phwKq","name":"calW1","type":"text"},{"content":" 8 9 10 11 12 13 14","fill":"$--foreground","fontFamily":"IBM Plex Mono","fontSize":11,"fontWeight":"normal","id":"hIyef","name":"calW2","type":"text"},{"content":"15 16 17 18 19 20 21","fill":"$--foreground","fontFamily":"IBM Plex Mono","fontSize":11,"fontWeight":"normal","id":"N9zUH","name":"calW3","type":"text"},{"content":"22 23 24 25 26 27 28","fill":"$--foreground","fontFamily":"IBM Plex Mono","fontSize":11,"fontWeight":"normal","id":"02wVP","name":"calW4","type":"text"},{"content":"29 30 [31]","fill":"$--primary","fontFamily":"IBM Plex Mono","fontSize":11,"fontWeight":"normal","id":"pm7vP","name":"calW5","type":"text"}],"gap":2,"id":"obkXY","layout":"vertical","name":"Day Grid (representation)","type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"fill":"$--muted-foreground","height":12,"iconFontFamily":"lucide","iconFontName":"x","id":"cPTHQ","name":"clearIco","type":"icon_font","width":12},{"content":"Clear date","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"z5fZw","name":"clearTxt","type":"text"}],"gap":4,"id":"T2eel","name":"Clear Date Row","padding":[8,0,0,0],"stroke":{"align":"inside","fill":"$--border","thickness":{"top":1}},"type":"frame","width":"fill_container"}],"cornerRadius":8,"effect":{"blur":12,"color":"#00000015","offset":{"x":0,"y":4},"shadowType":"outer","type":"shadow"},"fill":"$--popover","gap":8,"id":"Ft17I","layout":"vertical","name":"Calendar Popover (representation)","padding":12,"stroke":{"align":"inside","fill":"$--border","thickness":1},"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"content":"START DATE","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"HNo7P","letterSpacing":1.2,"name":"kD2","type":"text"},{"alignItems":"center","children":[{"fill":"$--muted-foreground","height":12,"iconFontFamily":"lucide","iconFontName":"calendar","id":"kKyym","name":"calIco2","type":"icon_font","width":12},{"content":"Pick a date...","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"w5Yim","name":"dateTxt2","type":"text"}],"cornerRadius":4,"gap":4,"id":"FShL0","name":"Date Button — empty","padding":[2,4],"type":"frame"}],"cornerRadius":4,"id":"6y8Ld","name":"Property Row — date (empty value with picker)","padding":[4,6],"type":"frame","width":"fill_container"},{"content":"Calendar opens for any value when display mode is date","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":10,"fontWeight":"normal","id":"AOnAh","name":"noteD","textGrowth":"fixed-width","type":"text","width":"fill_container"}],"fill":"$--background","gap":8,"id":"3kOd1","layout":"vertical","name":"Property Value — Date Picker (calendar open)","padding":12,"type":"frame","width":300,"x":400,"y":0},{"children":[{"content":"Boolean Toggle (yes/no click-to-flip)","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"Vi8uD","name":"lbl3","type":"text"},{"alignItems":"center","children":[{"content":"ARCHIVED","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"5zOMO","letterSpacing":1.2,"name":"kB1","type":"text"},{"children":[{"content":"✗ No","fill":"$--secondary-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"2jdhQ","name":"vB1t","type":"text"}],"cornerRadius":4,"fill":"transparent","id":"8aj9u","name":"Boolean Button — No","padding":[2,8],"stroke":{"align":"inside","fill":"$--border","thickness":1},"type":"frame"}],"cornerRadius":4,"id":"EPVN2","name":"Property Row — archived (false)","padding":[4,6],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"content":"PUBLISHED","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"J4kqn","letterSpacing":1.2,"name":"kB2","type":"text"},{"children":[{"content":"✓ Yes","fill":"$--accent-green","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"PTt7N","name":"vB2t","type":"text"}],"cornerRadius":4,"fill":"transparent","id":"2Kc2p","name":"Boolean Button — Yes","padding":[2,8],"stroke":{"align":"inside","fill":"$--border","thickness":1},"type":"frame"}],"cornerRadius":4,"fill":"$--bg-hover-subtle","id":"Mx1EU","name":"Property Row — published (true)","padding":[4,6],"type":"frame","width":"fill_container"},{"alignItems":"center","children":[{"content":"DRAFT","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"y9lFO","letterSpacing":1.2,"name":"kB3","type":"text"},{"children":[{"content":"✓ Yes","fill":"$--accent-green","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"MMOH6","name":"vB3t","type":"text"}],"cornerRadius":4,"fill":"transparent","id":"KEvKa","name":"Boolean Button — Yes (from string)","padding":[2,8],"stroke":{"align":"inside","fill":"$--border","thickness":1},"type":"frame"}],"cornerRadius":4,"id":"O2K4k","name":"Property Row — draft (true, string)","padding":[4,6],"type":"frame","width":"fill_container"},{"content":"Handles both native boolean and string true/false values","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":10,"fontWeight":"normal","id":"9hRyB","name":"note3","textGrowth":"fixed-width","type":"text","width":"fill_container"}],"fill":"$--background","gap":8,"id":"MYYnw","layout":"vertical","name":"Property Value — Boolean Toggle","padding":12,"type":"frame","width":300,"x":0,"y":420},{"children":[{"content":"Status Dropdown (searchable with vault + suggested)","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":11,"fontWeight":"normal","id":"4wWho","name":"lbl4","type":"text"},{"alignItems":"center","children":[{"content":"STATUS","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"500","id":"G0vke","letterSpacing":1.2,"name":"kS1","type":"text"},{"children":[{"content":"ACTIVE","fill":"$--accent-green","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"wZ0aD","letterSpacing":1.2,"name":"statusTxt","type":"text"}],"cornerRadius":16,"fill":"$--accent-green-light","id":"lmfgA","name":"Status Pill — Active","padding":[1,6],"type":"frame"}],"cornerRadius":4,"fill":"$--bg-hover-subtle","id":"EkmCc","name":"Property Row — status","padding":[4,6],"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"Type a status...","fill":"$--muted-foreground","fontFamily":"Inter","fontSize":12,"fontWeight":"normal","id":"EBbCZ","name":"searchTxt","type":"text"}],"id":"lo69T","name":"Search Input","padding":[6,8],"stroke":{"align":"inside","fill":"$--border","thickness":{"bottom":1}},"type":"frame","width":"fill_container"},{"children":[{"content":"FROM VAULT","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":9,"fontWeight":"500","id":"WD9o4","letterSpacing":1.2,"name":"vaultTxt","type":"text"}],"id":"aU6V8","name":"Vault Section","padding":[4,8],"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"ACTIVE","fill":"$--accent-green","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"ORffY","letterSpacing":1.2,"name":"pill1t","type":"text"}],"cornerRadius":16,"fill":"$--accent-green-light","id":"6X6T5","name":"pill1","padding":[1,6],"type":"frame"}],"cornerRadius":4,"fill":"$--muted","id":"aCzSa","name":"Option — Active (highlighted)","padding":[4,8],"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"DONE","fill":"$--accent-blue","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"BNI2j","letterSpacing":1.2,"name":"pill2t","type":"text"}],"cornerRadius":16,"fill":"$--accent-blue-light","id":"dCU52","name":"pill2","padding":[1,6],"type":"frame"}],"id":"RMOKs","name":"Option — Done","padding":[4,8],"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"PAUSED","fill":"$--accent-orange","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"JCZHo","letterSpacing":1.2,"name":"pill3t","type":"text"}],"cornerRadius":16,"fill":"$--accent-orange-light","id":"t820y","name":"pill3","padding":[1,6],"type":"frame"}],"id":"xGNr9","name":"Option — Paused","padding":[4,8],"type":"frame","width":"fill_container"},{"fill":"$--border","height":1,"id":"tvMv5","name":"sep1","type":"frame","width":"fill_container"},{"children":[{"content":"SUGGESTED","fill":"$--muted-foreground","fontFamily":"IBM Plex Mono","fontSize":9,"fontWeight":"500","id":"nG95l","letterSpacing":1.2,"name":"sugTxt","type":"text"}],"id":"taNZ2","name":"Suggested Section","padding":[4,8],"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"IN PROGRESS","fill":"$--accent-yellow","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"cNzSe","letterSpacing":1.2,"name":"pill4t","type":"text"}],"cornerRadius":16,"fill":"$--accent-yellow-light","id":"LPqKJ","name":"pill4","padding":[1,6],"type":"frame"}],"id":"YZUM8","name":"Option — In Progress","padding":[4,8],"type":"frame","width":"fill_container"},{"children":[{"children":[{"content":"BLOCKED","fill":"$--accent-red","fontFamily":"IBM Plex Mono","fontSize":10,"fontWeight":"600","id":"9drsD","letterSpacing":1.2,"name":"pill5t","type":"text"}],"cornerRadius":16,"fill":"$--accent-red-light","id":"ixyZV","name":"pill5","padding":[1,6],"type":"frame"}],"id":"snwbA","name":"Option — Blocked","padding":[4,8],"type":"frame","width":"fill_container"},{"height":4,"id":"nKzOO","name":"pad","type":"frame","width":"fill_container"}],"clip":true,"cornerRadius":8,"effect":{"blur":12,"color":"#00000015","offset":{"x":0,"y":4},"shadowType":"outer","type":"shadow"},"fill":"$--popover","id":"oHRUi","layout":"vertical","name":"Status Dropdown Popover","stroke":{"align":"inside","fill":"$--border","thickness":1},"type":"frame","width":"fill_container"}],"fill":"$--background","gap":8,"id":"aKgqZ","layout":"vertical","name":"Property Value — Status Dropdown (open)","padding":12,"type":"frame","width":300,"x":400,"y":420}],"variables":{"--accent-blue":{"type":"color","value":"#2383E2"},"--accent-blue-light":{"type":"color","value":"#D3E5EF"},"--accent-green":{"type":"color","value":"#0F7B6C"},"--accent-green-light":{"type":"color","value":"#DBEDDB"},"--accent-orange":{"type":"color","value":"#D9730D"},"--accent-orange-light":{"type":"color","value":"#FADEC9"},"--accent-purple":{"type":"color","value":"#9065B0"},"--accent-purple-light":{"type":"color","value":"#E8DEEE"},"--accent-red":{"type":"color","value":"#E03E3E"},"--accent-red-light":{"type":"color","value":"#FFE2DD"},"--accent-yellow":{"type":"color","value":"#DFAB01"},"--accent-yellow-light":{"type":"color","value":"#FDE68A"},"--background":{"type":"color","value":"#FFFFFF"},"--bg-hover-subtle":{"type":"color","value":"#F5F5F4"},"--border":{"type":"color","value":"#E5E5E2"},"--foreground":{"type":"color","value":"#37352F"},"--muted":{"type":"color","value":"#F5F5F4"},"--muted-foreground":{"type":"color","value":"#91918E"},"--popover":{"type":"color","value":"#FFFFFF"},"--popover-foreground":{"type":"color","value":"#37352F"},"--primary":{"type":"color","value":"#2383E2"},"--ring":{"type":"color","value":"#2383E2"},"--secondary-foreground":{"type":"color","value":"#37352F"}}} \ No newline at end of file diff --git a/src/components/DynamicPropertiesPanel.test.tsx b/src/components/DynamicPropertiesPanel.test.tsx index 344f41b5..20ed4213 100644 --- a/src/components/DynamicPropertiesPanel.test.tsx +++ b/src/components/DynamicPropertiesPanel.test.tsx @@ -906,5 +906,177 @@ describe('DynamicPropertiesPanel', () => { ) expect(screen.getByTestId('status-badge')).toBeInTheDocument() }) + + it('renders boolean toggle for string "true" when boolean mode overridden', () => { + localStorageMock.setItem('laputa:display-mode-overrides', JSON.stringify({ draft: 'boolean' })) + render( + + ) + expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument() + expect(screen.getByText('\u2713 Yes')).toBeInTheDocument() + }) + + it('renders boolean toggle for string "false" when boolean mode overridden', () => { + localStorageMock.setItem('laputa:display-mode-overrides', JSON.stringify({ draft: 'boolean' })) + render( + + ) + expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument() + expect(screen.getByText('\u2717 No')).toBeInTheDocument() + }) + + it('toggles string boolean from false to true', () => { + localStorageMock.setItem('laputa:display-mode-overrides', JSON.stringify({ draft: 'boolean' })) + render( + + ) + fireEvent.click(screen.getByTestId('boolean-toggle')) + expect(onUpdateProperty).toHaveBeenCalledWith('draft', true) + }) + + it('renders date picker for empty value when date mode overridden', () => { + localStorageMock.setItem('laputa:display-mode-overrides', JSON.stringify({ due: 'date' })) + render( + + ) + expect(screen.getByTestId('date-display')).toBeInTheDocument() + expect(screen.getByText('Pick a date\u2026')).toBeInTheDocument() + }) + + it('renders date picker for non-date string when date mode overridden', () => { + localStorageMock.setItem('laputa:display-mode-overrides', JSON.stringify({ deadline: 'date' })) + render( + + ) + expect(screen.getByTestId('date-display')).toBeInTheDocument() + }) + }) + + describe('type-aware add property form', () => { + it('shows boolean toggle when boolean type selected', () => { + render( + + ) + fireEvent.click(screen.getByText('+ Add property')) + // Switch type to boolean + fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' }) + fireEvent.click(screen.getByRole('option', { name: /Boolean/ })) + expect(screen.getByTestId('add-property-boolean-toggle')).toBeInTheDocument() + expect(screen.getByText('\u2717 No')).toBeInTheDocument() + }) + + it('toggles boolean value in add form', () => { + render( + + ) + fireEvent.click(screen.getByText('+ Add property')) + fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' }) + fireEvent.click(screen.getByRole('option', { name: /Boolean/ })) + // Toggle from No to Yes + fireEvent.click(screen.getByTestId('add-property-boolean-toggle')) + expect(screen.getByText('\u2713 Yes')).toBeInTheDocument() + }) + + it('stores actual boolean value when adding boolean property', () => { + render( + + ) + fireEvent.click(screen.getByText('+ Add property')) + const keyInput = screen.getByPlaceholderText('Property name') + fireEvent.change(keyInput, { target: { value: 'published' } }) + // Switch to boolean type + fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' }) + fireEvent.click(screen.getByRole('option', { name: /Boolean/ })) + // Default is false, toggle to true + fireEvent.click(screen.getByTestId('add-property-boolean-toggle')) + // Submit + fireEvent.click(screen.getByTestId('add-property-confirm')) + expect(onAddProperty).toHaveBeenCalledWith('published', true) + }) + + it('shows date picker trigger when date type selected', () => { + render( + + ) + fireEvent.click(screen.getByText('+ Add property')) + fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' }) + fireEvent.click(screen.getByRole('option', { name: /Date/ })) + expect(screen.getByTestId('add-property-date-trigger')).toBeInTheDocument() + expect(screen.getByText('Pick a date\u2026')).toBeInTheDocument() + }) + + it('shows status dropdown when status type selected', () => { + render( + + ) + fireEvent.click(screen.getByText('+ Add property')) + fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' }) + fireEvent.click(screen.getByRole('option', { name: /Status/ })) + expect(screen.getByTestId('add-property-status-trigger')).toBeInTheDocument() + }) + + it('shows text input for text and url types', () => { + render( + + ) + fireEvent.click(screen.getByText('+ Add property')) + // Default mode is text + expect(screen.getByPlaceholderText('Value')).toBeInTheDocument() + }) }) }) diff --git a/src/components/DynamicPropertiesPanel.tsx b/src/components/DynamicPropertiesPanel.tsx index 50054e93..88ca3f0e 100644 --- a/src/components/DynamicPropertiesPanel.tsx +++ b/src/components/DynamicPropertiesPanel.tsx @@ -22,6 +22,7 @@ import { detectPropertyType, } from '../utils/propertyTypes' import { StatusPill, StatusDropdown } from './StatusDropdown' +import { SUGGESTED_STATUSES } from '../utils/statusStyles' // Keys that are relationships (contain wikilinks) export const RELATIONSHIP_KEYS = new Set([ @@ -111,6 +112,13 @@ function parseDateValue(value: string): Date | undefined { return isNaN(d.getTime()) ? undefined : d } +function dateToISO(day: Date): string { + const yyyy = day.getFullYear() + const mm = String(day.getMonth() + 1).padStart(2, '0') + const dd = String(day.getDate()).padStart(2, '0') + return `${yyyy}-${mm}-${dd}` +} + function DateValue({ value, onSave }: { value: string; onSave: (newValue: string) => void }) { @@ -119,12 +127,7 @@ function DateValue({ value, onSave }: { const selectedDate = parseDateValue(value) const handleSelect = (day: Date | undefined) => { - if (day) { - const yyyy = day.getFullYear() - const mm = String(day.getMonth() + 1).padStart(2, '0') - const dd = String(day.getDate()).padStart(2, '0') - onSave(`${yyyy}-${mm}-${dd}`) - } + if (day) onSave(dateToISO(day)) setOpen(false) } @@ -143,7 +146,7 @@ function DateValue({ value, onSave }: { data-testid="date-display" > - {formatted} + {formatted || 'Pick a date\u2026'} @@ -239,13 +242,100 @@ const DISPLAY_MODE_ICONS: Record = { text: Type, date: CalendarIcon, boolean: ToggleLeft, status: Circle, url: Link, } -function AddPropertyForm({ onAdd, onCancel }: { +const ADD_INPUT_CLASS = "h-[26px] min-w-0 flex-1 rounded border border-border bg-muted px-1.5 text-[12px] text-foreground outline-none focus:border-primary" + +function AddBooleanInput({ value, onChange }: { value: string; onChange: (v: string) => void }) { + const boolVal = value.toLowerCase() === 'true' + return ( + + ) +} + +function AddDateInput({ value, onChange }: { value: string; onChange: (v: string) => void }) { + const selectedDate = value ? parseDateValue(value) : undefined + const formatted = value ? formatDateValue(value) : '' + return ( + + + + + + { if (day) onChange(dateToISO(day)) }} + defaultMonth={selectedDate} + /> + + + ) +} + +function AddStatusInput({ value, onChange, vaultStatuses }: { value: string; onChange: (v: string) => void; vaultStatuses: string[] }) { + const allStatuses = [...new Set([...vaultStatuses, ...SUGGESTED_STATUSES])] + return ( + + ) +} + +function AddPropertyValueInput({ displayMode, value, onChange, onKeyDown, vaultStatuses }: { + displayMode: PropertyDisplayMode; value: string; onChange: (v: string) => void + onKeyDown: (e: React.KeyboardEvent) => void; vaultStatuses: string[] +}) { + switch (displayMode) { + case 'boolean': return + case 'date': return + case 'status': return + default: return ( + onChange(e.target.value)} onKeyDown={onKeyDown} + /> + ) + } +} + +function AddPropertyForm({ onAdd, onCancel, vaultStatuses }: { onAdd: (key: string, value: string, displayMode: PropertyDisplayMode) => void; onCancel: () => void + vaultStatuses: string[] }) { const [newKey, setNewKey] = useState('') const [newValue, setNewValue] = useState('') const [displayMode, setDisplayMode] = useState('text') + const handleModeChange = (mode: PropertyDisplayMode) => { + setDisplayMode(mode) + if (mode === 'boolean') setNewValue('false') + else if (mode !== displayMode) setNewValue('') + } + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && newKey.trim()) onAdd(newKey, newValue, displayMode) else if (e.key === 'Escape') onCancel() @@ -258,7 +348,7 @@ function AddPropertyForm({ onAdd, onCancel }: { type="text" placeholder="Property name" value={newKey} onChange={(e) => setNewKey(e.target.value)} onKeyDown={handleKeyDown} autoFocus /> - handleModeChange(v as PropertyDisplayMode)}> - setNewValue(e.target.value)} onKeyDown={handleKeyDown} - /> +