diff --git a/src/App.tsx b/src/App.tsx
index 107546c0..d1184167 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -336,15 +336,23 @@ function App() {
setToastMessage(`Type "${name}" created`)
}, [notes])
- const handleCreateView = useCallback(async (definition: import('./types').ViewDefinition) => {
- const filename = definition.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') + '.yml'
+ const handleCreateOrUpdateView = useCallback(async (definition: import('./types').ViewDefinition) => {
+ const editing = dialogs.editingView
+ const filename = editing
+ ? editing.filename
+ : definition.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') + '.yml'
const target = isTauri() ? invoke : mockInvoke
await target('save_view_cmd', { vaultPath: resolvedPath, filename, definition })
- trackEvent('view_created')
+ trackEvent(editing ? 'view_updated' : 'view_created')
await vault.reloadViews()
- setToastMessage(`View "${definition.name}" created`)
+ setToastMessage(editing ? `View "${definition.name}" updated` : `View "${definition.name}" created`)
handleSetSelection({ kind: 'view', filename })
- }, [resolvedPath, vault, handleSetSelection])
+ }, [resolvedPath, vault, handleSetSelection, dialogs.editingView])
+
+ const handleEditView = useCallback((filename: string) => {
+ const view = vault.views.find((v) => v.filename === filename)
+ if (view) dialogs.openEditView(filename, view.definition)
+ }, [vault.views, dialogs])
const handleDeleteView = useCallback(async (filename: string) => {
const target = isTauri() ? invoke : mockInvoke
@@ -545,7 +553,7 @@ function App() {
{sidebarVisible && (
<>
-
+
>
@@ -624,7 +632,7 @@ function App() {
-
+
{
+ const defaultProps = {
+ open: true,
+ onClose: vi.fn(),
+ onCreate: vi.fn(),
+ availableFields: ['type', 'status', 'title'],
+ }
+
+ it('shows "Create View" title in create mode', () => {
+ render()
+ expect(screen.getByText('Create View')).toBeInTheDocument()
+ expect(screen.getByText('Create')).toBeInTheDocument()
+ })
+
+ it('shows "Edit View" title when editingView is provided', () => {
+ const editingView: ViewDefinition = {
+ name: 'Active Projects',
+ icon: '🚀',
+ color: null,
+ sort: null,
+ filters: { all: [{ field: 'type', op: 'equals', value: 'Project' }] },
+ }
+ render()
+ expect(screen.getByText('Edit View')).toBeInTheDocument()
+ expect(screen.getByText('Save')).toBeInTheDocument()
+ })
+
+ it('pre-populates name field in edit mode', () => {
+ const editingView: ViewDefinition = {
+ name: 'Active Projects',
+ icon: '🚀',
+ color: null,
+ sort: null,
+ filters: { all: [{ field: 'type', op: 'equals', value: 'Project' }] },
+ }
+ render()
+ const input = screen.getByPlaceholderText(/Active Projects|Reading List/i)
+ expect(input).toHaveValue('Active Projects')
+ })
+})
diff --git a/src/components/CreateViewDialog.tsx b/src/components/CreateViewDialog.tsx
index 4913c20d..f7f33ca2 100644
--- a/src/components/CreateViewDialog.tsx
+++ b/src/components/CreateViewDialog.tsx
@@ -13,9 +13,11 @@ interface CreateViewDialogProps {
availableFields: string[]
/** Returns known values for a given field (for autocomplete). */
valueSuggestions?: (field: string) => string[]
+ /** When provided, the dialog operates in edit mode with pre-populated fields. */
+ editingView?: ViewDefinition | null
}
-export function CreateViewDialog({ open, onClose, onCreate, availableFields, valueSuggestions }: CreateViewDialogProps) {
+export function CreateViewDialog({ open, onClose, onCreate, availableFields, valueSuggestions, editingView }: CreateViewDialogProps) {
const [name, setName] = useState('')
const [icon, setIcon] = useState('')
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
@@ -23,16 +25,23 @@ export function CreateViewDialog({ open, onClose, onCreate, availableFields, val
all: [{ field: 'type', op: 'equals', value: '' }],
})
const inputRef = useRef(null)
+ const isEditing = !!editingView
useEffect(() => {
if (open) {
- setName('') // eslint-disable-line react-hooks/set-state-in-effect -- reset on dialog open
- setIcon('') // eslint-disable-line react-hooks/set-state-in-effect -- reset on dialog open
+ if (editingView) {
+ setName(editingView.name) // eslint-disable-line react-hooks/set-state-in-effect -- populate on dialog open
+ setIcon(editingView.icon ?? '') // eslint-disable-line react-hooks/set-state-in-effect -- populate on dialog open
+ setFilters(editingView.filters) // eslint-disable-line react-hooks/set-state-in-effect -- populate on dialog open
+ } else {
+ setName('') // eslint-disable-line react-hooks/set-state-in-effect -- reset on dialog open
+ setIcon('') // eslint-disable-line react-hooks/set-state-in-effect -- reset on dialog open
+ setFilters({ all: [{ field: availableFields[0] ?? 'type', op: 'equals', value: '' }] }) // eslint-disable-line react-hooks/set-state-in-effect -- reset on dialog open
+ }
setShowEmojiPicker(false) // eslint-disable-line react-hooks/set-state-in-effect -- reset on dialog open
- setFilters({ all: [{ field: availableFields[0] ?? 'type', op: 'equals', value: '' }] }) // eslint-disable-line react-hooks/set-state-in-effect -- reset on dialog open
setTimeout(() => inputRef.current?.focus(), 50)
}
- }, [open, availableFields])
+ }, [open, availableFields, editingView])
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
@@ -62,7 +71,7 @@ export function CreateViewDialog({ open, onClose, onCreate, availableFields, val