feat: implement create new type feature
Core implementation: - CreateTypeDialog: simple dialog with name field for creating new types - handleCreateType in useNoteActions: creates Type documents in type/ folder - Dynamic sidebar sections: custom types (Recipe, Book, etc.) appear as new sidebar sections after built-in ones, each with a + button for instances - Updated CreateNoteDialog: accepts custom types, shows them with blue accent - handleCreateNote now supports custom types (folder = lowercased type name) Product decisions: - The + on Types section opens CreateTypeDialog (not CreateNoteDialog) - Custom type sections use FileText icon and blue accent color by default - Section labels are pluralized (e.g., "Recipes", "Books") Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
42
src/App.tsx
42
src/App.tsx
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Sidebar } from './components/Sidebar'
|
||||
import { NoteList } from './components/NoteList'
|
||||
import { Editor } from './components/Editor'
|
||||
import { ResizeHandle } from './components/ResizeHandle'
|
||||
import { CreateNoteDialog, type NoteType } from './components/CreateNoteDialog'
|
||||
import { CreateNoteDialog } from './components/CreateNoteDialog'
|
||||
import { CreateTypeDialog } from './components/CreateTypeDialog'
|
||||
import { QuickOpenPalette } from './components/QuickOpenPalette'
|
||||
import { Toast } from './components/Toast'
|
||||
import { CommitDialog } from './components/CommitDialog'
|
||||
@@ -29,6 +30,12 @@ const VAULTS = [
|
||||
{ label: 'Demo', path: '/Users/luca/Workspace/laputa-app/demo-vault' },
|
||||
]
|
||||
|
||||
const BUILT_IN_TYPE_NAMES = new Set([
|
||||
'Project', 'Experiment', 'Responsibility', 'Procedure',
|
||||
'Person', 'Event', 'Topic', 'Type', 'Note', 'Essay',
|
||||
'Quarter', 'Journal', 'Evergreen',
|
||||
])
|
||||
|
||||
function App() {
|
||||
const [selection, setSelection] = useState<SidebarSelection>(DEFAULT_SELECTION)
|
||||
const [sidebarWidth, setSidebarWidth] = useState(250)
|
||||
@@ -37,7 +44,8 @@ function App() {
|
||||
const [inspectorCollapsed, setInspectorCollapsed] = useState(false)
|
||||
const [gitHistory, setGitHistory] = useState<GitCommit[]>([])
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
const [createNoteDefaultType, setCreateNoteDefaultType] = useState<NoteType | undefined>()
|
||||
const [createNoteDefaultType, setCreateNoteDefaultType] = useState<string | undefined>()
|
||||
const [showCreateTypeDialog, setShowCreateTypeDialog] = useState(false)
|
||||
const [showQuickOpen, setShowQuickOpen] = useState(false)
|
||||
const [showCommitDialog, setShowCommitDialog] = useState(false)
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null)
|
||||
@@ -47,6 +55,15 @@ function App() {
|
||||
const vault = useVaultLoader(vaultPath)
|
||||
const notes = useNoteActions(vault.addEntry, vault.updateContent, vault.entries, setToastMessage)
|
||||
|
||||
// Derive custom types from vault (Type entries not in built-in list)
|
||||
const customTypes = useMemo(
|
||||
() => vault.entries
|
||||
.filter((e) => e.isA === 'Type' && !BUILT_IN_TYPE_NAMES.has(e.title))
|
||||
.map((e) => e.title)
|
||||
.sort(),
|
||||
[vault.entries],
|
||||
)
|
||||
|
||||
// Reset UI state when vault changes
|
||||
const handleSwitchVault = useCallback((path: string) => {
|
||||
setVaultPath(path)
|
||||
@@ -65,10 +82,19 @@ function App() {
|
||||
}, [notes.activeTabPath, vault.loadGitHistory])
|
||||
|
||||
const openCreateDialog = useCallback((type?: string) => {
|
||||
setCreateNoteDefaultType(type as NoteType | undefined)
|
||||
setCreateNoteDefaultType(type)
|
||||
setShowCreateDialog(true)
|
||||
}, [])
|
||||
|
||||
const openCreateTypeDialog = useCallback(() => {
|
||||
setShowCreateTypeDialog(true)
|
||||
}, [])
|
||||
|
||||
const handleCreateType = useCallback((name: string) => {
|
||||
notes.handleCreateType(name)
|
||||
setToastMessage(`Type "${name}" created`)
|
||||
}, [notes, setToastMessage])
|
||||
|
||||
useAppKeyboard({
|
||||
onQuickOpen: () => setShowQuickOpen(true),
|
||||
onCreateNote: openCreateDialog,
|
||||
@@ -107,7 +133,7 @@ function App() {
|
||||
<div className="app-shell">
|
||||
<div className="app">
|
||||
<div className="app__sidebar" style={{ width: sidebarWidth }}>
|
||||
<Sidebar entries={vault.entries} selection={selection} onSelect={setSelection} onSelectNote={notes.handleSelectNote} onCreateType={openCreateDialog} modifiedCount={vault.modifiedFiles.length} onCommitPush={() => setShowCommitDialog(true)} />
|
||||
<Sidebar entries={vault.entries} selection={selection} onSelect={setSelection} onSelectNote={notes.handleSelectNote} onCreateType={openCreateDialog} onCreateNewType={openCreateTypeDialog} modifiedCount={vault.modifiedFiles.length} onCommitPush={() => setShowCommitDialog(true)} />
|
||||
</div>
|
||||
<ResizeHandle onResize={handleSidebarResize} />
|
||||
<div className="app__note-list" style={{ width: noteListWidth }}>
|
||||
@@ -154,6 +180,12 @@ function App() {
|
||||
onClose={() => setShowCreateDialog(false)}
|
||||
onCreate={notes.handleCreateNote}
|
||||
defaultType={createNoteDefaultType}
|
||||
customTypes={customTypes}
|
||||
/>
|
||||
<CreateTypeDialog
|
||||
open={showCreateTypeDialog}
|
||||
onClose={() => setShowCreateTypeDialog(false)}
|
||||
onCreate={handleCreateType}
|
||||
/>
|
||||
<CommitDialog
|
||||
open={showCommitDialog}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const NOTE_TYPES = [
|
||||
const BUILT_IN_TYPES = [
|
||||
'Note',
|
||||
'Project',
|
||||
'Experiment',
|
||||
@@ -15,18 +15,20 @@ const NOTE_TYPES = [
|
||||
'Topic',
|
||||
] as const
|
||||
|
||||
export type NoteType = (typeof NOTE_TYPES)[number]
|
||||
export type NoteType = (typeof BUILT_IN_TYPES)[number]
|
||||
|
||||
interface CreateNoteDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onCreate: (title: string, type: NoteType) => void
|
||||
defaultType?: NoteType
|
||||
onCreate: (title: string, type: string) => void
|
||||
defaultType?: string
|
||||
/** Custom types from the vault (Type documents not in built-in list) */
|
||||
customTypes?: string[]
|
||||
}
|
||||
|
||||
export function CreateNoteDialog({ open, onClose, onCreate, defaultType }: CreateNoteDialogProps) {
|
||||
export function CreateNoteDialog({ open, onClose, onCreate, defaultType, customTypes = [] }: CreateNoteDialogProps) {
|
||||
const [title, setTitle] = useState('')
|
||||
const [type, setType] = useState<NoteType>('Note')
|
||||
const [type, setType] = useState<string>('Note')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -68,7 +70,7 @@ export function CreateNoteDialog({ open, onClose, onCreate, defaultType }: Creat
|
||||
Type
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{NOTE_TYPES.map((t) => (
|
||||
{BUILT_IN_TYPES.map((t) => (
|
||||
<Button
|
||||
key={t}
|
||||
type="button"
|
||||
@@ -83,6 +85,23 @@ export function CreateNoteDialog({ open, onClose, onCreate, defaultType }: Creat
|
||||
{t}
|
||||
</Button>
|
||||
))}
|
||||
{customTypes.map((t) => (
|
||||
<Button
|
||||
key={t}
|
||||
type="button"
|
||||
variant={type === t ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn(
|
||||
"rounded-full text-xs",
|
||||
type === t
|
||||
? "bg-[var(--accent-blue)] text-white"
|
||||
: "border-[var(--accent-blue)] text-[var(--accent-blue)]"
|
||||
)}
|
||||
onClick={() => setType(t)}
|
||||
>
|
||||
{t}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
64
src/components/CreateTypeDialog.tsx
Normal file
64
src/components/CreateTypeDialog.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
interface CreateTypeDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onCreate: (name: string) => void
|
||||
}
|
||||
|
||||
export function CreateTypeDialog({ open, onClose, onCreate }: CreateTypeDialogProps) {
|
||||
const [name, setName] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setName('')
|
||||
setTimeout(() => inputRef.current?.focus(), 50)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
const trimmed = name.trim()
|
||||
if (!trimmed) return
|
||||
onCreate(trimmed)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(isOpen) => { if (!isOpen) onClose() }}>
|
||||
<DialogContent showCloseButton={false} className="sm:max-w-[380px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Type</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Type Name
|
||||
</label>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder="e.g. Recipe, Book, Habit..."
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Creates a type document. Its properties become defaults for new docs of this type.
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!name.trim()}>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, memo, type ComponentType } from 'react'
|
||||
import { useState, useMemo, memo, type ComponentType } from 'react'
|
||||
import type { VaultEntry, SidebarSelection } from '../types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronRight, ChevronDown, GitCommitHorizontal, Plus } from 'lucide-react'
|
||||
@@ -25,6 +25,7 @@ interface SidebarProps {
|
||||
onSelect: (selection: SidebarSelection) => void
|
||||
onSelectNote?: (entry: VaultEntry) => void
|
||||
onCreateType?: (type: string) => void
|
||||
onCreateNewType?: () => void
|
||||
modifiedCount?: number
|
||||
onCommitPush?: () => void
|
||||
}
|
||||
@@ -34,7 +35,13 @@ const TOP_NAV = [
|
||||
{ label: 'Favorites', filter: 'favorites' as const, Icon: Star },
|
||||
]
|
||||
|
||||
const SECTION_GROUPS: { label: string; type: string; Icon: ComponentType<IconProps> }[] = [
|
||||
interface SectionGroup {
|
||||
label: string
|
||||
type: string
|
||||
Icon: ComponentType<IconProps>
|
||||
}
|
||||
|
||||
const BUILT_IN_SECTION_GROUPS: SectionGroup[] = [
|
||||
{ label: 'Projects', type: 'Project', Icon: Wrench },
|
||||
{ label: 'Experiments', type: 'Experiment', Icon: Flask },
|
||||
{ label: 'Responsibilities', type: 'Responsibility', Icon: Target },
|
||||
@@ -45,7 +52,9 @@ const SECTION_GROUPS: { label: string; type: string; Icon: ComponentType<IconPro
|
||||
{ label: 'Types', type: 'Type', Icon: StackSimple },
|
||||
]
|
||||
|
||||
export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onSelectNote, onCreateType, modifiedCount = 0, onCommitPush }: SidebarProps) {
|
||||
const BUILT_IN_TYPES = new Set(BUILT_IN_SECTION_GROUPS.map((s) => s.type))
|
||||
|
||||
export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onSelectNote, onCreateType, onCreateNewType, modifiedCount = 0, onCommitPush }: SidebarProps) {
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
|
||||
const toggleSection = (type: string) => {
|
||||
setCollapsed((prev) => ({ ...prev, [type]: !prev[type] }))
|
||||
@@ -62,6 +71,113 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS
|
||||
return false
|
||||
}
|
||||
|
||||
// Derive custom type sections from Type entries not in the built-in list
|
||||
const customSectionGroups: SectionGroup[] = useMemo(() => {
|
||||
return entries
|
||||
.filter((e) => e.isA === 'Type' && !BUILT_IN_TYPES.has(e.title))
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
.map((e) => ({
|
||||
label: e.title + 's',
|
||||
type: e.title,
|
||||
Icon: FileText,
|
||||
}))
|
||||
}, [entries])
|
||||
|
||||
const allSectionGroups = useMemo(
|
||||
() => [...BUILT_IN_SECTION_GROUPS, ...customSectionGroups],
|
||||
[customSectionGroups],
|
||||
)
|
||||
|
||||
const renderSection = ({ label, type, Icon }: SectionGroup) => {
|
||||
const items = entries.filter((e) => e.isA === type)
|
||||
const isCollapsed = collapsed[type] ?? false
|
||||
const isTopic = type === 'Topic'
|
||||
const isTypeSection = type === 'Type'
|
||||
|
||||
const handlePlusClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (isTypeSection) {
|
||||
onCreateNewType?.()
|
||||
} else {
|
||||
onCreateType?.(type)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={type} style={{ padding: '4px 6px' }}>
|
||||
{/* Section header row */}
|
||||
<div
|
||||
className={cn(
|
||||
"group/section flex cursor-pointer select-none items-center justify-between rounded transition-colors",
|
||||
isActive({ kind: 'sectionGroup', type })
|
||||
? "bg-secondary"
|
||||
: "hover:bg-accent"
|
||||
)}
|
||||
style={{ padding: '6px 16px', borderRadius: 4, gap: 8 }}
|
||||
onClick={() => onSelect({ kind: 'sectionGroup', type })}
|
||||
>
|
||||
<div className="flex items-center" style={{ gap: 8 }}>
|
||||
<Icon size={16} style={{ color: getTypeColor(type) }} />
|
||||
<span className="text-[13px] font-medium text-foreground">{label}</span>
|
||||
</div>
|
||||
<div className="flex items-center" style={{ gap: 2 }}>
|
||||
{(onCreateType || (isTypeSection && onCreateNewType)) && (
|
||||
<button
|
||||
className="flex shrink-0 items-center justify-center rounded border-none bg-transparent p-0 text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover/section:opacity-100 cursor-pointer"
|
||||
style={{ width: 20, height: 20 }}
|
||||
onClick={handlePlusClick}
|
||||
aria-label={isTypeSection ? 'Create new Type' : `Create new ${type}`}
|
||||
title={isTypeSection ? 'New Type' : `New ${type}`}
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="flex shrink-0 items-center border-none bg-transparent p-0 text-inherit cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleSection(type)
|
||||
}}
|
||||
aria-label={isCollapsed ? `Expand ${label}` : `Collapse ${label}`}
|
||||
>
|
||||
{isCollapsed ? <ChevronRight size={12} /> : <ChevronDown size={12} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Children items */}
|
||||
{!isCollapsed && items.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{items.map((entry) => (
|
||||
<div
|
||||
key={entry.path}
|
||||
className={cn(
|
||||
"cursor-pointer truncate rounded-md text-[13px] font-normal transition-colors",
|
||||
isActive(isTopic ? { kind: 'topic', entry } : { kind: 'entity', entry })
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
style={{
|
||||
padding: '4px 16px 4px 28px',
|
||||
...(isActive(isTopic ? { kind: 'topic', entry } : { kind: 'entity', entry }) && {
|
||||
backgroundColor: getTypeLightColor(entry.isA ?? ''),
|
||||
color: getSectionColor(entry),
|
||||
}),
|
||||
}}
|
||||
onClick={() => {
|
||||
onSelect(isTopic ? { kind: 'topic', entry } : { kind: 'entity', entry })
|
||||
onSelectNote?.(entry)
|
||||
}}
|
||||
>
|
||||
{entry.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="flex h-full flex-col overflow-hidden border-r border-[var(--sidebar-border)] bg-sidebar text-sidebar-foreground" style={{ paddingTop: 38 } as React.CSSProperties}>
|
||||
{/* Native macOS title bar on top */}
|
||||
@@ -116,91 +232,8 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Groups */}
|
||||
{SECTION_GROUPS.map(({ label, type, Icon }) => {
|
||||
const items = type === 'Topic'
|
||||
? entries.filter((e) => e.isA === 'Topic')
|
||||
: entries.filter((e) => e.isA === type)
|
||||
const isCollapsed = collapsed[type] ?? false
|
||||
const isTopic = type === 'Topic'
|
||||
|
||||
return (
|
||||
<div key={type} style={{ padding: '4px 6px' }}>
|
||||
{/* Section header row */}
|
||||
<div
|
||||
className={cn(
|
||||
"group/section flex cursor-pointer select-none items-center justify-between rounded transition-colors",
|
||||
isActive({ kind: 'sectionGroup', type })
|
||||
? "bg-secondary"
|
||||
: "hover:bg-accent"
|
||||
)}
|
||||
style={{ padding: '6px 16px', borderRadius: 4, gap: 8 }}
|
||||
onClick={() => onSelect({ kind: 'sectionGroup', type })}
|
||||
>
|
||||
<div className="flex items-center" style={{ gap: 8 }}>
|
||||
<Icon size={16} style={{ color: getTypeColor(type) }} />
|
||||
<span className="text-[13px] font-medium text-foreground">{label}</span>
|
||||
</div>
|
||||
<div className="flex items-center" style={{ gap: 2 }}>
|
||||
{onCreateType && (
|
||||
<button
|
||||
className="flex shrink-0 items-center justify-center rounded border-none bg-transparent p-0 text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover/section:opacity-100 cursor-pointer"
|
||||
style={{ width: 20, height: 20 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onCreateType(type)
|
||||
}}
|
||||
aria-label={`Create new ${type}`}
|
||||
title={`New ${type}`}
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="flex shrink-0 items-center border-none bg-transparent p-0 text-inherit cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleSection(type)
|
||||
}}
|
||||
aria-label={isCollapsed ? `Expand ${label}` : `Collapse ${label}`}
|
||||
>
|
||||
{isCollapsed ? <ChevronRight size={12} /> : <ChevronDown size={12} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Children items */}
|
||||
{!isCollapsed && items.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{items.map((entry) => (
|
||||
<div
|
||||
key={entry.path}
|
||||
className={cn(
|
||||
"cursor-pointer truncate rounded-md text-[13px] font-normal transition-colors",
|
||||
isActive(isTopic ? { kind: 'topic', entry } : { kind: 'entity', entry })
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
style={{
|
||||
padding: '4px 16px 4px 28px',
|
||||
...(isActive(isTopic ? { kind: 'topic', entry } : { kind: 'entity', entry }) && {
|
||||
backgroundColor: getTypeLightColor(entry.isA ?? ''),
|
||||
color: getSectionColor(entry),
|
||||
}),
|
||||
}}
|
||||
onClick={() => {
|
||||
onSelect(isTopic ? { kind: 'topic', entry } : { kind: 'entity', entry })
|
||||
onSelectNote?.(entry)
|
||||
}}
|
||||
>
|
||||
{entry.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{/* Section Groups (built-in + custom) */}
|
||||
{allSectionGroups.map(renderSection)}
|
||||
</nav>
|
||||
|
||||
{/* Commit button — always visible */}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
import { isTauri, mockInvoke, addMockEntry, updateMockContent } from '../mock-tauri'
|
||||
import type { VaultEntry } from '../types'
|
||||
import type { FrontmatterValue } from '../components/Inspector'
|
||||
import type { NoteType } from '../components/CreateNoteDialog'
|
||||
|
||||
interface Tab {
|
||||
entry: VaultEntry
|
||||
@@ -173,21 +172,23 @@ export function useNoteActions(
|
||||
}
|
||||
}, [entries, handleSelectNote])
|
||||
|
||||
const handleCreateNote = useCallback(async (title: string, type: NoteType) => {
|
||||
const handleCreateNote = useCallback(async (title: string, type: string) => {
|
||||
const typeToFolder: Record<string, string> = {
|
||||
Note: 'note', Project: 'project', Experiment: 'experiment',
|
||||
Responsibility: 'responsibility', Procedure: 'procedure',
|
||||
Person: 'person', Event: 'event', Topic: 'topic',
|
||||
}
|
||||
const folder = typeToFolder[type] || 'note'
|
||||
// Custom types use lowercased type name as folder
|
||||
const folder = typeToFolder[type] || type.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
|
||||
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
|
||||
const path = `/Users/luca/Laputa/${folder}/${slug}.md`
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
|
||||
const noStatusTypes = new Set(['Topic', 'Person'])
|
||||
const newEntry: VaultEntry = {
|
||||
path, filename: `${slug}.md`, title, isA: type,
|
||||
aliases: [], belongsTo: [], relatedTo: [],
|
||||
status: type === 'Topic' || type === 'Person' ? null : 'Active',
|
||||
status: noStatusTypes.has(type) ? null : 'Active',
|
||||
owner: null, cadence: null, modifiedAt: now, createdAt: now, fileSize: 0,
|
||||
snippet: '', relationships: {},
|
||||
}
|
||||
@@ -207,6 +208,29 @@ export function useNoteActions(
|
||||
handleSelectNote(newEntry)
|
||||
}, [handleSelectNote, addEntry])
|
||||
|
||||
const handleCreateType = useCallback(async (typeName: string) => {
|
||||
const slug = typeName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
|
||||
const path = `/Users/luca/Laputa/type/${slug}.md`
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
|
||||
const newEntry: VaultEntry = {
|
||||
path, filename: `${slug}.md`, title: typeName, isA: 'Type',
|
||||
aliases: [], belongsTo: [], relatedTo: [],
|
||||
status: null, owner: null, cadence: null,
|
||||
modifiedAt: now, createdAt: now, fileSize: 0,
|
||||
snippet: '', relationships: {},
|
||||
}
|
||||
|
||||
const content = `---\nIs A: Type\n---\n\n# ${typeName}\n\n`
|
||||
|
||||
if (!isTauri()) {
|
||||
addMockEntry(newEntry, content)
|
||||
}
|
||||
|
||||
addEntry(newEntry, content)
|
||||
handleSelectNote(newEntry)
|
||||
}, [handleSelectNote, addEntry])
|
||||
|
||||
const handleUpdateFrontmatter = useCallback(async (path: string, key: string, value: FrontmatterValue) => {
|
||||
try {
|
||||
let newContent: string
|
||||
@@ -272,6 +296,7 @@ export function useNoteActions(
|
||||
handleSwitchTab,
|
||||
handleNavigateWikilink,
|
||||
handleCreateNote,
|
||||
handleCreateType,
|
||||
handleUpdateFrontmatter,
|
||||
handleDeleteProperty,
|
||||
handleAddProperty,
|
||||
|
||||
Reference in New Issue
Block a user