fix: handle filename collisions in create flows

This commit is contained in:
lucaronin
2026-04-22 22:37:01 +02:00
parent a27a70e552
commit bf13eed3ab
18 changed files with 562 additions and 171 deletions

View File

@@ -46,6 +46,19 @@ describe('CreateTypeDialog', () => {
await waitFor(() => expect(onClose).toHaveBeenCalled())
})
it('stays open when create reports a handled collision', async () => {
const onClose = vi.fn()
const onCreate = vi.fn().mockResolvedValue(false)
render(<CreateTypeDialog open={true} onClose={onClose} onCreate={onCreate} />)
fireEvent.change(screen.getByPlaceholderText('e.g. Recipe, Book, Habit...'), { target: { value: 'Recipe' } })
fireEvent.click(screen.getByRole('button', { name: 'Create' }))
await waitFor(() => expect(onCreate).toHaveBeenCalledWith('Recipe'))
expect(onClose).not.toHaveBeenCalled()
expect(screen.getByText('Create New Type')).toBeInTheDocument()
})
it('calls onClose when Cancel is clicked', () => {
const onClose = vi.fn()
render(<CreateTypeDialog open={true} onClose={onClose} onCreate={() => {}} />)

View File

@@ -6,14 +6,14 @@ import { Input } from '@/components/ui/input'
interface CreateTypeDialogProps {
open: boolean
onClose: () => void
onCreate: (name: string) => void | Promise<void>
onCreate: (name: string) => boolean | void | Promise<boolean | void>
initialName?: string
}
interface CreateTypeDialogFormProps {
initialName: string
onClose: () => void
onCreate: (name: string) => void | Promise<void>
onCreate: (name: string) => boolean | void | Promise<boolean | void>
}
function CreateTypeDialogForm({ initialName, onClose, onCreate }: CreateTypeDialogFormProps) {
@@ -23,8 +23,8 @@ function CreateTypeDialogForm({ initialName, onClose, onCreate }: CreateTypeDial
e.preventDefault()
const trimmed = name.trim()
if (!trimmed) return
await onCreate(trimmed)
onClose()
const created = await onCreate(trimmed)
if (created !== false) onClose()
}
return (

View File

@@ -239,7 +239,7 @@ export function DynamicPropertiesPanel({
onDeleteProperty?: (key: string) => void
onAddProperty?: (key: string, value: FrontmatterValue) => void
onNavigate?: (target: string) => void
onCreateMissingType?: (typeName: string) => void | Promise<void>
onCreateMissingType?: (typeName: string) => boolean | void | Promise<boolean | void>
}) {
const {
editingKey, setEditingKey, showAddDialog, setShowAddDialog, displayOverrides,

View File

@@ -52,7 +52,7 @@ interface EditorProps {
onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
onDeleteProperty?: (path: string, key: string) => Promise<void>
onAddProperty?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
onCreateMissingType?: (path: string, missingType: string, nextTypeName: string) => Promise<void>
onCreateMissingType?: (path: string, missingType: string, nextTypeName: string) => Promise<boolean | void>
onCreateAndOpenNote?: (title: string) => Promise<boolean>
onInitializeProperties?: (path: string) => void
showAIChat?: boolean
@@ -360,7 +360,7 @@ function EditorLayout({
onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
onDeleteProperty?: (path: string, key: string) => Promise<void>
onAddProperty?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
onCreateMissingType?: (path: string, missingType: string, nextTypeName: string) => Promise<void>
onCreateMissingType?: (path: string, missingType: string, nextTypeName: string) => Promise<boolean | void>
onCreateAndOpenNote?: (title: string) => Promise<boolean>
onInitializeProperties?: (path: string) => void
onFileCreated?: (relativePath: string) => void

View File

@@ -27,7 +27,7 @@ interface EditorRightPanelProps {
onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
onDeleteProperty?: (path: string, key: string) => Promise<void>
onAddProperty?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
onCreateMissingType?: (path: string, missingType: string, nextTypeName: string) => Promise<void>
onCreateMissingType?: (path: string, missingType: string, nextTypeName: string) => Promise<boolean | void>
onCreateAndOpenNote?: (title: string) => Promise<boolean>
onInitializeProperties?: (path: string) => void
onToggleRawEditor?: () => void
@@ -110,7 +110,7 @@ export function EditorRightPanel({
onUpdateFrontmatter={onUpdateFrontmatter}
onDeleteProperty={onDeleteProperty}
onAddProperty={onAddProperty}
onCreateMissingType={onCreateMissingType}
onCreateMissingType={onCreateMissingType as ((path: string, missingType: string, nextTypeName: string) => Promise<void>) | undefined}
onCreateAndOpenNote={onCreateAndOpenNote}
onInitializeProperties={onInitializeProperties}
onToggleRawEditor={onToggleRawEditor}

View File

@@ -118,7 +118,7 @@ function MissingTypeWarning({
onCreateMissingType,
}: {
missingTypeName: string
onCreateMissingType?: (typeName: string) => void | Promise<void>
onCreateMissingType?: (typeName: string) => boolean | void | Promise<boolean | void>
}) {
const [dialogOpen, setDialogOpen] = useState(false)
const canCreateMissingType = Boolean(onCreateMissingType)
@@ -148,9 +148,7 @@ function MissingTypeWarning({
<CreateTypeDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
onCreate={async (typeName) => {
await onCreateMissingType?.(typeName)
}}
onCreate={(typeName) => onCreateMissingType?.(typeName)}
initialName={missingTypeName}
/>
)}
@@ -165,7 +163,7 @@ function TypeRowValue({
}: {
children: ReactNode
missingTypeName?: string | null
onCreateMissingType?: (typeName: string) => void | Promise<void>
onCreateMissingType?: (typeName: string) => boolean | void | Promise<boolean | void>
}) {
return (
<div className="flex min-w-0 items-center justify-start gap-1">
@@ -191,7 +189,7 @@ function ReadOnlyType({
customColorKey?: string | null
onNavigate?: (target: string) => void
missingTypeName?: string | null
onCreateMissingType?: (typeName: string) => void | Promise<void>
onCreateMissingType?: (typeName: string) => boolean | void | Promise<boolean | void>
}) {
if (!isA) return null
return (
@@ -230,7 +228,7 @@ interface TypeSelectorProps {
onUpdateProperty?: (key: string, value: FrontmatterValue) => void
onNavigate?: (target: string) => void
missingTypeName?: string | null
onCreateMissingType?: (typeName: string) => void | Promise<void>
onCreateMissingType?: (typeName: string) => boolean | void | Promise<boolean | void>
}
export function TypeSelector({ onUpdateProperty, ...props }: TypeSelectorProps) {

View File

@@ -8,7 +8,7 @@ interface InspectorPropertyActionsConfig {
onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
onDeleteProperty?: (path: string, key: string) => Promise<void>
onAddProperty?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
onCreateMissingType?: (path: string, missingType: string, nextTypeName: string) => Promise<void>
onCreateMissingType?: (path: string, missingType: string, nextTypeName: string) => Promise<boolean | void>
}
function bindEntryAction<TArgs extends unknown[], TResult>(
@@ -21,7 +21,7 @@ function bindEntryAction<TArgs extends unknown[], TResult>(
function bindMissingTypeAction(
entry: VaultEntry | null,
action: ((path: string, missingType: string, nextTypeName: string) => Promise<void>) | undefined,
action: ((path: string, missingType: string, nextTypeName: string) => Promise<boolean | void>) | undefined,
) {
const missingType = entry?.isA
if (!entry || !missingType || !action) return undefined