Merge branch 'main' into pr-726
This commit is contained in:
@@ -36,6 +36,7 @@ files:
|
||||
command.navigation.showArchivedNotes: 241399b908884adfe8608a3ab827ca74
|
||||
command.navigation.listType: 4f70c27c6826a37543c8ada561397c22
|
||||
command.note.newNote: 23c4edda8b815f750782f01870d27271
|
||||
command.note.newNoteInCurrentFolder: 167b765903c70ab1b645908ff8a899ab
|
||||
command.note.newType: adce5108f18945cc502a06c02445e32d
|
||||
command.note.newTypedNote: 1493eda772c2179cb6247169d00723b2
|
||||
command.note.saveNote: 2a309cda46e95b00d2a5a8afb3cc0047
|
||||
@@ -366,6 +367,7 @@ files:
|
||||
sidebar.action.createFolder: 5dc1e97c14fbe72d8b566ce29c39f010
|
||||
sidebar.action.renameFolder: db76034f8218cda07dadb746a5643019
|
||||
sidebar.action.deleteFolder: 2a2f15300f6f7c81d69860ac1796b517
|
||||
sidebar.action.createNodeInFolderMenu: 857a479229a4ab0bc62bbfb621d3180a
|
||||
sidebar.action.revealFolderMenu: 76f4ed52da9937ae432dcf5a108fabb4
|
||||
sidebar.action.copyFolderPathMenu: 1779ec6018a385912c1a5499a1bbf2b2
|
||||
sidebar.action.renameFolderMenu: deb1d8fa030292e7eda2c244b313aca2
|
||||
|
||||
@@ -4,6 +4,7 @@ import { describe, it, expect, vi } from 'vitest'
|
||||
import { FolderTree } from './FolderTree'
|
||||
import { FOLDER_ROW_SINGLE_CLICK_DELAY_MS } from './folder-tree/useFolderRowInteractions'
|
||||
import { FOLDER_ROW_NESTING_INDENT, getFolderConnectorLeft } from './folder-tree/folderTreeLayout'
|
||||
import { CREATE_NOTE_IN_FOLDER_EVENT } from '../hooks/noteCreationRequests'
|
||||
import type { FolderNode, SidebarSelection } from '../types'
|
||||
|
||||
const mockFolders: FolderNode[] = [
|
||||
@@ -526,6 +527,44 @@ describe('FolderTree', () => {
|
||||
expect(onCopyFolderPath).toHaveBeenCalledWith('projects')
|
||||
})
|
||||
|
||||
it('creates a note in the right-clicked mounted folder', () => {
|
||||
const onCreateNoteInFolder = vi.fn()
|
||||
const folders: FolderNode[] = [
|
||||
{
|
||||
name: 'Personal',
|
||||
path: '',
|
||||
rootPath: '/Users/luca/Personal',
|
||||
children: [{ name: 'projects', path: 'projects', rootPath: '/Users/luca/Personal', children: [] }],
|
||||
},
|
||||
{
|
||||
name: 'Team',
|
||||
path: '',
|
||||
rootPath: '/Users/luca/Team',
|
||||
children: [{ name: 'projects', path: 'projects', rootPath: '/Users/luca/Team', children: [] }],
|
||||
},
|
||||
]
|
||||
|
||||
render(
|
||||
<FolderTree
|
||||
folders={folders}
|
||||
selection={defaultSelection}
|
||||
onSelect={vi.fn()}
|
||||
vaultRootPath="/Users/luca/Team"
|
||||
/>,
|
||||
)
|
||||
|
||||
window.addEventListener(CREATE_NOTE_IN_FOLDER_EVENT, onCreateNoteInFolder)
|
||||
fireEvent.contextMenu(screen.getAllByTestId('folder-row:projects')[1])
|
||||
fireEvent.click(screen.getByTestId('create-node-in-folder-menu-item'))
|
||||
|
||||
expect(onCreateNoteInFolder).toHaveBeenCalledOnce()
|
||||
expect((onCreateNoteInFolder.mock.calls[0][0] as CustomEvent).detail).toEqual({
|
||||
folderPath: 'projects',
|
||||
rootPath: '/Users/luca/Team',
|
||||
})
|
||||
window.removeEventListener(CREATE_NOTE_IN_FOLDER_EVENT, onCreateNoteInFolder)
|
||||
})
|
||||
|
||||
it('keeps destructive folder actions off the vault root row and menu', () => {
|
||||
render(
|
||||
<FolderTree
|
||||
|
||||
@@ -162,6 +162,7 @@ export const FolderTree = memo(function FolderTree({
|
||||
closeContextMenu,
|
||||
contextMenu,
|
||||
handleCopyPathFromMenu,
|
||||
handleCreateNoteFromMenu,
|
||||
handleDeleteFromMenu,
|
||||
handleOpenMenu,
|
||||
handleRevealFromMenu,
|
||||
@@ -221,6 +222,7 @@ export const FolderTree = memo(function FolderTree({
|
||||
onDelete={handleDeleteFromMenu}
|
||||
onReveal={handleRevealFromMenu}
|
||||
onCopyPath={handleCopyPathFromMenu}
|
||||
onCreateNote={handleCreateNoteFromMenu}
|
||||
onRename={handleRenameFromMenu}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ArrowsOut as Maximize2 } from '@phosphor-icons/react'
|
||||
import { useEffect, useId, useMemo, useRef, useState, type SyntheticEvent } from 'react'
|
||||
import { useEffect, useId, useMemo, useState, type SyntheticEvent } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -159,13 +159,7 @@ function MermaidLightbox({ svg }: { svg: string }) {
|
||||
}
|
||||
|
||||
function MermaidSourceFallback({ source }: { source: string }) {
|
||||
const sourceRef = useRef<HTMLPreElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
sourceRef.current?.setAttribute('aria-label', 'Mermaid source')
|
||||
}, [])
|
||||
|
||||
return <pre ref={sourceRef}><code>{source}</code></pre>
|
||||
return <pre aria-label="Mermaid source"><code>{source}</code></pre>
|
||||
}
|
||||
|
||||
export function MermaidDiagram({ diagram, source }: MermaidDiagramProps) {
|
||||
|
||||
@@ -188,6 +188,26 @@ describe('NoteList rendering', () => {
|
||||
expect(onCreateNote).toHaveBeenCalledWith(undefined)
|
||||
})
|
||||
|
||||
it('shows the active folder name and creates notes inside that folder', () => {
|
||||
const { onCreateNote } = renderNoteList({
|
||||
selection: {
|
||||
kind: 'folder',
|
||||
path: 'Projects/2026 Planning',
|
||||
rootPath: '/Users/luca/Laputa',
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.getByRole('heading', { name: '2026 Planning' })).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTitle('Create new note'))
|
||||
|
||||
expect(onCreateNote).toHaveBeenCalledWith(undefined, {
|
||||
creationPath: 'folder_header',
|
||||
folderPath: 'Projects/2026 Planning',
|
||||
vaultPath: '/Users/luca/Laputa',
|
||||
})
|
||||
})
|
||||
|
||||
it('pins the current entity and shows grouped children', () => {
|
||||
renderNoteList({ selection: { kind: 'entity', entry: mockEntries[0] } })
|
||||
expect(screen.getAllByText('Build Laputa App').length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { RefObject } from 'react'
|
||||
import { ClipboardText, FolderOpen, PencilSimple, Trash } from '@phosphor-icons/react'
|
||||
import { ClipboardText, FolderOpen, PencilSimple, Plus, Trash } from '@phosphor-icons/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { translate, type AppLocale } from '../../lib/i18n'
|
||||
|
||||
export interface FolderContextMenuState {
|
||||
path: string
|
||||
rootPath?: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
@@ -15,6 +16,7 @@ interface FolderContextMenuProps {
|
||||
onDelete?: (folderPath: string) => void
|
||||
onReveal?: (folderPath: string) => void
|
||||
onCopyPath?: (folderPath: string) => void
|
||||
onCreateNote?: (folderPath: string, rootPath?: string) => void
|
||||
onRename: (folderPath: string) => void
|
||||
locale?: AppLocale
|
||||
}
|
||||
@@ -25,6 +27,7 @@ export function FolderContextMenu({
|
||||
onDelete,
|
||||
onReveal,
|
||||
onCopyPath,
|
||||
onCreateNote,
|
||||
onRename,
|
||||
locale = 'en',
|
||||
}: FolderContextMenuProps) {
|
||||
@@ -38,6 +41,18 @@ export function FolderContextMenu({
|
||||
style={{ left: menu.x, top: menu.y, minWidth: 180 }}
|
||||
data-testid="folder-context-menu"
|
||||
>
|
||||
{onCreateNote && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-auto w-full justify-start gap-2 px-2 py-1.5 text-sm"
|
||||
onClick={() => onCreateNote(menu.path, menu.rootPath)}
|
||||
data-testid="create-node-in-folder-menu-item"
|
||||
>
|
||||
<Plus size={14} />
|
||||
{translate(locale, 'sidebar.action.createNodeInFolderMenu')}
|
||||
</Button>
|
||||
)}
|
||||
{onReveal && (
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, type MouseEvent as ReactMouseEvent } from 'react'
|
||||
import type { FolderNode } from '../../types'
|
||||
import type { FolderFileActions } from '../../hooks/useFileActions'
|
||||
import { requestCreateNoteInFolder } from '../../hooks/noteCreationRequests'
|
||||
import { useSidebarContextMenu } from '../sidebar/sidebarHooks'
|
||||
|
||||
interface UseFolderContextMenuInput {
|
||||
@@ -19,12 +20,17 @@ export function useFolderContextMenu({
|
||||
contextMenu,
|
||||
contextMenuRef,
|
||||
openContextMenuFromPointer,
|
||||
} = useSidebarContextMenu<string>()
|
||||
} = useSidebarContextMenu<{ path: string; rootPath?: string }>()
|
||||
|
||||
const handleOpenMenu = useCallback((node: FolderNode, event: ReactMouseEvent<HTMLElement>) => {
|
||||
openContextMenuFromPointer(node.path, event)
|
||||
openContextMenuFromPointer({ path: node.path, rootPath: node.rootPath }, event)
|
||||
}, [openContextMenuFromPointer])
|
||||
|
||||
const handleCreateNoteFromMenu = useCallback((folderPath: string, rootPath?: string) => {
|
||||
closeContextMenu()
|
||||
requestCreateNoteInFolder(folderPath, rootPath)
|
||||
}, [closeContextMenu])
|
||||
|
||||
const handleRenameFromMenu = useCallback((folderPath: string) => {
|
||||
closeContextMenu()
|
||||
onStartRenameFolder?.(folderPath)
|
||||
@@ -45,7 +51,8 @@ export function useFolderContextMenu({
|
||||
folderFileActions?.copyFolderPath(folderPath)
|
||||
}, [closeContextMenu, folderFileActions])
|
||||
const menu = contextMenu ? {
|
||||
path: contextMenu.target,
|
||||
path: contextMenu.target.path,
|
||||
rootPath: contextMenu.target.rootPath,
|
||||
x: contextMenu.pos.x,
|
||||
y: contextMenu.pos.y,
|
||||
} : null
|
||||
@@ -54,6 +61,7 @@ export function useFolderContextMenu({
|
||||
closeContextMenu,
|
||||
contextMenu: menu,
|
||||
handleCopyPathFromMenu,
|
||||
handleCreateNoteFromMenu,
|
||||
handleDeleteFromMenu,
|
||||
handleOpenMenu,
|
||||
handleRevealFromMenu,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react'
|
||||
import type { VaultEntry, SidebarSelection, ModifiedFile, NoteStatus, ViewDefinition, ViewFile } from '../../types'
|
||||
import type { ImmediateCreateOptions } from '../../hooks/useNoteCreation'
|
||||
import {
|
||||
type SortOption, type SortDirection, type SortConfig, type NoteListFilter,
|
||||
getSortComparator, extractSortableProperties,
|
||||
@@ -996,7 +997,36 @@ interface UseNoteListInteractionsParams {
|
||||
onAutoTriggerDiff?: () => void
|
||||
onDiscardFile?: (relativePath: string) => Promise<void>
|
||||
openContextMenuForEntry: (entry: VaultEntry, point: { x: number; y: number }) => void
|
||||
onCreateNote: (type?: string) => void
|
||||
onCreateNote: (type?: string, options?: ImmediateCreateOptions) => void
|
||||
}
|
||||
|
||||
function createNoteRequestForSelection(selection: SidebarSelection): {
|
||||
options?: ImmediateCreateOptions
|
||||
type?: string
|
||||
} {
|
||||
if (selection.kind === 'sectionGroup') return { type: selection.type }
|
||||
if (selection.kind === 'folder') {
|
||||
return {
|
||||
options: {
|
||||
creationPath: 'folder_header',
|
||||
folderPath: selection.path,
|
||||
vaultPath: selection.rootPath,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function createNoteForSelection(
|
||||
onCreateNote: (type?: string, options?: ImmediateCreateOptions) => void,
|
||||
selection: SidebarSelection,
|
||||
): void {
|
||||
const request = createNoteRequestForSelection(selection)
|
||||
if (request.options) {
|
||||
onCreateNote(request.type, request.options)
|
||||
return
|
||||
}
|
||||
onCreateNote(request.type)
|
||||
}
|
||||
|
||||
function resolveChangesContextMenuEntry(
|
||||
@@ -1232,7 +1262,7 @@ export function useNoteListInteractions({
|
||||
})
|
||||
|
||||
const handleCreateNote = useCallback(() => {
|
||||
onCreateNote(selection.kind === 'sectionGroup' ? selection.type : undefined)
|
||||
createNoteForSelection(onCreateNote, selection)
|
||||
}, [onCreateNote, selection])
|
||||
|
||||
const toggleGroup = useCallback((label: string) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { VaultEntry, SidebarSelection, SidebarFilter, ModifiedFile, NoteSta
|
||||
import type { RelationshipGroup } from '../../utils/noteListHelpers'
|
||||
import { translate, type AppLocale } from '../../lib/i18n'
|
||||
import { filenameStemToTitle } from '../../utils/noteTitle'
|
||||
import { vaultRelativePathLabel } from '../../utils/notePathIdentity'
|
||||
|
||||
export interface DeletedNoteEntry extends VaultEntry {
|
||||
__deletedNotePreview: true
|
||||
@@ -30,12 +31,20 @@ function resolveSelectionFilterTitle(selection: SidebarSelection, locale: AppLoc
|
||||
return translate(locale, FILTER_TITLE_KEYS[selection.filter])
|
||||
}
|
||||
|
||||
function resolveFolderTitle(selection: SidebarSelection): string | null {
|
||||
if (selection.kind !== 'folder') return null
|
||||
if (selection.path.trim()) return vaultRelativePathLabel(selection.path)
|
||||
return selection.rootPath ? vaultRelativePathLabel(selection.rootPath) : null
|
||||
}
|
||||
|
||||
export function resolveHeaderTitle(selection: SidebarSelection, typeDocument: VaultEntry | null, views?: ViewFile[], locale: AppLocale = 'en'): string {
|
||||
if (selection.kind === 'view') {
|
||||
const view = views?.find((v) => v.filename === selection.filename)
|
||||
return view?.definition.name ?? translate(locale, 'noteList.title.view')
|
||||
}
|
||||
if (selection.kind === 'entity') return selection.entry.title
|
||||
const folderTitle = resolveFolderTitle(selection)
|
||||
if (folderTitle) return folderTitle
|
||||
if (typeDocument) return typeDocument.title
|
||||
|
||||
return resolveSelectionFilterTitle(selection, locale) ?? translate(locale, 'noteList.title.notes')
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { NoteListFilter } from '../../utils/noteListHelpers'
|
||||
import { countByFilter, countAllByFilter, countAllNotesByFilter } from '../../utils/noteListHelpers'
|
||||
import type { AllNotesFileVisibility } from '../../utils/allNotesFileVisibility'
|
||||
import type { GitRepositoryOption } from '../../utils/gitRepositories'
|
||||
import type { ImmediateCreateOptions } from '../../hooks/useNoteCreation'
|
||||
import { NoteItem } from '../NoteItem'
|
||||
import { prefetchNoteContent } from '../../hooks/useTabManagement'
|
||||
import type { MultiSelectState } from '../../hooks/useMultiSelect'
|
||||
@@ -312,7 +313,7 @@ interface UseNoteListInteractionStateParams {
|
||||
onCopyFilePath?: (path: string) => void
|
||||
onAutoTriggerDiff?: () => void
|
||||
onDiscardFile?: (relativePath: string) => Promise<void>
|
||||
onCreateNote: (type?: string) => void
|
||||
onCreateNote: (type?: string, options?: ImmediateCreateOptions) => void
|
||||
onBulkArchive?: (paths: string[]) => void
|
||||
onBulkDeletePermanently?: (paths: string[]) => void
|
||||
locale: AppLocale
|
||||
|
||||
@@ -25,6 +25,7 @@ const STATIC_LABEL_KEYS: Partial<Record<string, TranslationKey>> = {
|
||||
'filter-open': 'command.navigation.showOpenNotes',
|
||||
'filter-archived': 'command.navigation.showArchivedNotes',
|
||||
'create-note': 'command.note.newNote',
|
||||
'create-note-current-folder': 'command.note.newNoteInCurrentFolder',
|
||||
'create-type': 'command.note.newType',
|
||||
'save-note': 'command.note.saveNote',
|
||||
'paste-plain-text': 'command.note.pastePlainText',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { APP_COMMAND_IDS, getAppCommandShortcutDisplay } from '../appCommandCatalog'
|
||||
import { buildEditorFindCommands } from './editorFindCommands'
|
||||
import type { ImmediateCreateOptions } from '../useNoteCreation'
|
||||
import type { CommandAction } from './types'
|
||||
|
||||
interface NoteCommandsConfig {
|
||||
@@ -8,8 +9,9 @@ interface NoteCommandsConfig {
|
||||
activeFileKind?: 'markdown' | 'text' | 'binary'
|
||||
isArchived: boolean
|
||||
activeNoteHasIcon?: boolean
|
||||
onCreateNote: () => void
|
||||
onCreateNote: (type?: string, options?: ImmediateCreateOptions) => void
|
||||
onCreateType?: () => void
|
||||
currentFolderCreateOptions?: ImmediateCreateOptions
|
||||
onSave: () => void
|
||||
onFindInNote?: () => void
|
||||
onReplaceInNote?: () => void
|
||||
@@ -63,6 +65,16 @@ function createNoteCommand(config: NoteCommandConfig): CommandAction {
|
||||
}
|
||||
}
|
||||
|
||||
function buildCurrentFolderNoteCommand(config: NoteCommandsConfig): CommandAction {
|
||||
return createNoteCommand({
|
||||
id: 'create-note-current-folder',
|
||||
label: 'Create New Note in Current Folder',
|
||||
keywords: ['new', 'create', 'add', 'folder', 'current'],
|
||||
enabled: config.currentFolderCreateOptions !== undefined,
|
||||
execute: () => config.onCreateNote(undefined, config.currentFolderCreateOptions),
|
||||
})
|
||||
}
|
||||
|
||||
function buildCoreNoteCommands(config: NoteCommandsConfig): CommandAction[] {
|
||||
return [
|
||||
createNoteCommand({
|
||||
@@ -73,6 +85,7 @@ function buildCoreNoteCommands(config: NoteCommandsConfig): CommandAction[] {
|
||||
enabled: true,
|
||||
execute: config.onCreateNote,
|
||||
}),
|
||||
buildCurrentFolderNoteCommand(config),
|
||||
createNoteCommand({
|
||||
id: 'create-type',
|
||||
label: 'New Type',
|
||||
|
||||
33
src/hooks/noteCreationRequests.ts
Normal file
33
src/hooks/noteCreationRequests.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useEffect } from 'react'
|
||||
import type { ImmediateCreateOptions } from './useNoteCreation'
|
||||
|
||||
export const CREATE_NOTE_IN_FOLDER_EVENT = 'tolaria:create-note-in-folder'
|
||||
|
||||
interface CreateNoteInFolderDetail {
|
||||
folderPath: string
|
||||
rootPath?: string
|
||||
}
|
||||
|
||||
type ImmediateCreate = (type?: string, options?: ImmediateCreateOptions) => void
|
||||
|
||||
export function requestCreateNoteInFolder(folderPath: string, rootPath?: string): void {
|
||||
window.dispatchEvent(new CustomEvent<CreateNoteInFolderDetail>(CREATE_NOTE_IN_FOLDER_EVENT, {
|
||||
detail: { folderPath, rootPath },
|
||||
}))
|
||||
}
|
||||
|
||||
export function useCreateNoteInFolderRequests(createNote: ImmediateCreate): void {
|
||||
useEffect(() => {
|
||||
const handleCreateNoteInFolder = (event: Event) => {
|
||||
const { folderPath, rootPath } = (event as CustomEvent<CreateNoteInFolderDetail>).detail
|
||||
createNote(undefined, {
|
||||
creationPath: 'folder_context_menu',
|
||||
folderPath,
|
||||
vaultPath: rootPath,
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener(CREATE_NOTE_IN_FOLDER_EVENT, handleCreateNoteInFolder)
|
||||
return () => window.removeEventListener(CREATE_NOTE_IN_FOLDER_EVENT, handleCreateNoteInFolder)
|
||||
}, [createNote])
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import type { NoteWidthMode, SidebarSelection, SidebarFilter, VaultEntry } from
|
||||
import { requestAddRemote } from '../utils/addRemoteEvents'
|
||||
import type { NoteListFilter } from '../utils/noteListHelpers'
|
||||
import type { ViewMode } from './useViewMode'
|
||||
import type { ImmediateCreateOptions } from './useNoteCreation'
|
||||
import type { NoteListMultiSelectionCommands } from '../components/note-list/multiSelectionCommands'
|
||||
import type { GitRepositoryOption } from '../utils/gitRepositories'
|
||||
|
||||
@@ -29,7 +30,7 @@ interface AppCommandsConfig {
|
||||
onFindInNote?: () => void
|
||||
onReplaceInNote?: () => void
|
||||
onPastePlainText: () => void
|
||||
onCreateNote: () => void
|
||||
onCreateNote: (type?: string, options?: ImmediateCreateOptions) => void
|
||||
onCreateNoteOfType: (type: string) => void
|
||||
onSave: () => void
|
||||
onOpenSettings: () => void
|
||||
|
||||
@@ -652,6 +652,36 @@ describe('useCommandRegistry', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('exposes a current-folder create command only when a folder is selected', () => {
|
||||
const onCreateNote = vi.fn()
|
||||
const { result } = renderHook(() => useCommandRegistry(makeConfig({
|
||||
onCreateNote,
|
||||
selection: { kind: 'folder', path: 'Projects/2026 Planning', rootPath: '/vault' },
|
||||
})))
|
||||
|
||||
const command = findCommand(result.current, 'create-note-current-folder')
|
||||
expect(command).toMatchObject({
|
||||
label: 'Create New Note in Current Folder',
|
||||
group: 'Note',
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
command!.execute()
|
||||
expect(onCreateNote).toHaveBeenCalledWith(undefined, {
|
||||
creationPath: 'folder_command_palette',
|
||||
folderPath: 'Projects/2026 Planning',
|
||||
vaultPath: '/vault',
|
||||
})
|
||||
})
|
||||
|
||||
it('disables the current-folder create command outside folder selections', () => {
|
||||
const { result } = renderHook(() => useCommandRegistry(makeConfig({
|
||||
selection: { kind: 'filter', filter: 'all' },
|
||||
})))
|
||||
|
||||
expect(findCommand(result.current, 'create-note-current-folder')?.enabled).toBe(false)
|
||||
})
|
||||
|
||||
it('exposes paste without formatting in the command palette', () => {
|
||||
const onPastePlainText = vi.fn()
|
||||
const { result } = renderHook(() => useCommandRegistry(makeConfig({ onPastePlainText })))
|
||||
|
||||
@@ -17,6 +17,7 @@ import { buildFilterCommands } from './commands/filterCommands'
|
||||
import { localizeCommandActions } from './commands/localizeCommands'
|
||||
import { extractVaultTypes } from '../utils/vaultTypes'
|
||||
import type { GitRepositoryOption } from '../utils/gitRepositories'
|
||||
import type { ImmediateCreateOptions } from './useNoteCreation'
|
||||
|
||||
// Re-export types and helpers for backward compatibility
|
||||
export type { CommandAction, CommandGroup } from './commands/types'
|
||||
@@ -65,7 +66,7 @@ interface CommandRegistryConfig {
|
||||
onRestoreDeletedNote?: () => void
|
||||
canRestoreDeletedNote?: boolean
|
||||
onQuickOpen: () => void
|
||||
onCreateNote: () => void
|
||||
onCreateNote: (type?: string, options?: ImmediateCreateOptions) => void
|
||||
onCreateNoteOfType: (type: string) => void
|
||||
onSave: () => void
|
||||
onPastePlainText: () => void
|
||||
@@ -129,6 +130,15 @@ interface CommandRegistryConfig {
|
||||
onSetNoteListFilter?: (filter: NoteListFilter) => void
|
||||
}
|
||||
|
||||
function currentFolderCreateOptions(selection: SidebarSelection | undefined): ImmediateCreateOptions | undefined {
|
||||
if (selection?.kind !== 'folder') return undefined
|
||||
return {
|
||||
creationPath: 'folder_command_palette',
|
||||
folderPath: selection.path,
|
||||
vaultPath: selection.rootPath,
|
||||
}
|
||||
}
|
||||
|
||||
export function useCommandRegistry(config: CommandRegistryConfig): import('./commands/types').CommandAction[] {
|
||||
const {
|
||||
activeTabPath, entries, modifiedCount,
|
||||
@@ -166,6 +176,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
|
||||
const isArchived = activeEntry?.archived ?? false
|
||||
const isFavorite = activeEntry?.favorite ?? false
|
||||
const isSectionGroup = selection?.kind === 'sectionGroup'
|
||||
const folderCreateOptions = useMemo(() => currentFolderCreateOptions(selection), [selection])
|
||||
const noteListColumnsLabel = config.noteListColumnsLabel ?? (
|
||||
selection?.kind === 'filter' && selection.filter === 'all'
|
||||
? 'Customize All Notes columns'
|
||||
@@ -195,7 +206,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
|
||||
|
||||
const noteCommands = useMemo(() => buildNoteCommands({
|
||||
hasActiveNote, activeTabPath, activeFileKind: activeEntry?.fileKind ?? 'markdown', isArchived,
|
||||
onCreateNote, onCreateType, onSave,
|
||||
currentFolderCreateOptions: folderCreateOptions, onCreateNote, onCreateType, onSave,
|
||||
onFindInNote, onReplaceInNote, onPastePlainText,
|
||||
onDeleteNote, onArchiveNote, onUnarchiveNote,
|
||||
onChangeNoteType, onMoveNoteToFolder, canMoveNoteToFolder,
|
||||
@@ -206,7 +217,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
|
||||
onRestoreDeletedNote, canRestoreDeletedNote,
|
||||
}), [
|
||||
hasActiveNote, activeTabPath, activeEntry?.fileKind, isArchived,
|
||||
onCreateNote, onCreateType, onSave, onFindInNote, onReplaceInNote, onPastePlainText, onDeleteNote, onArchiveNote, onUnarchiveNote,
|
||||
folderCreateOptions, onCreateNote, onCreateType, onSave, onFindInNote, onReplaceInNote, onPastePlainText, onDeleteNote, onArchiveNote, onUnarchiveNote,
|
||||
onChangeNoteType, onMoveNoteToFolder, canMoveNoteToFolder,
|
||||
onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon, onOpenInNewWindow,
|
||||
onRevealActiveFile, onCopyActiveFilePath, onOpenActiveFileExternal,
|
||||
|
||||
@@ -362,6 +362,34 @@ describe('useNoteCreation hook', () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('handleCreateNoteImmediate can target a nested folder in a mounted vault', async () => {
|
||||
vi.mocked(isTauri).mockReturnValue(true)
|
||||
vi.mocked(invoke).mockResolvedValueOnce(undefined)
|
||||
vi.spyOn(Date, 'now').mockReturnValue(1700000000000)
|
||||
const { result } = renderHook(() => useNoteCreation(makeConfig(), tabDeps))
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleCreateNoteImmediate(undefined, {
|
||||
creationPath: 'folder_header',
|
||||
folderPath: 'Projects/2026 Planning',
|
||||
vaultPath: '/Users/luca/Team',
|
||||
})
|
||||
await flushImmediateCreate()
|
||||
})
|
||||
|
||||
const createdPath = '/Users/luca/Team/Projects/2026 Planning/untitled-note-1700000000.md'
|
||||
expect(vi.mocked(invoke)).toHaveBeenCalledWith('create_note_content', {
|
||||
path: createdPath,
|
||||
content: expect.stringContaining('type: Note'),
|
||||
vaultPath: '/Users/luca/Team',
|
||||
})
|
||||
expect(addEntry).toHaveBeenCalledWith(expect.objectContaining({
|
||||
path: createdPath,
|
||||
workspace: expect.objectContaining({ path: '/Users/luca/Team' }),
|
||||
}))
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('handleCreateNoteImmediate generates unique names on rapid calls via timestamp', async () => {
|
||||
vi.useFakeTimers()
|
||||
let ts = 1700000000000
|
||||
|
||||
@@ -6,11 +6,17 @@ import { slugifyNoteStem as slugify } from '../utils/noteSlug'
|
||||
import { resolveEntry } from '../utils/wikilink'
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
import { cacheNoteContent } from './useTabManagement'
|
||||
import { findByCollidingNotePath, joinVaultPath, notePathFilename } from '../utils/notePathIdentity'
|
||||
import {
|
||||
findByCollidingNotePath,
|
||||
joinVaultPath,
|
||||
normalizeVaultRelativePath,
|
||||
notePathFilename,
|
||||
} from '../utils/notePathIdentity'
|
||||
import { canonicalFrontmatterKey } from '../utils/systemMetadata'
|
||||
import { canonicalizeTypeName } from '../utils/vaultTypes'
|
||||
import { labelFromWorkspacePath, workspaceIdentityFromVault } from '../utils/workspaces'
|
||||
import type { VaultOption } from '../components/status-bar/types'
|
||||
import { useCreateNoteInFolderRequests } from './noteCreationRequests'
|
||||
|
||||
export interface NewEntryParams {
|
||||
path: string
|
||||
@@ -639,7 +645,20 @@ interface ImmediateCreateDeps {
|
||||
setToastMessage: (msg: string | null) => void
|
||||
}
|
||||
|
||||
interface ImmediateCreateRequest {
|
||||
type ImmediateCreationPath =
|
||||
| 'cmd_n'
|
||||
| 'folder_command_palette'
|
||||
| 'folder_context_menu'
|
||||
| 'folder_header'
|
||||
| 'type_section'
|
||||
|
||||
export interface ImmediateCreateOptions {
|
||||
creationPath?: ImmediateCreationPath
|
||||
folderPath?: string
|
||||
vaultPath?: string
|
||||
}
|
||||
|
||||
interface ImmediateCreateRequest extends ImmediateCreateOptions {
|
||||
type?: string
|
||||
}
|
||||
|
||||
@@ -697,16 +716,26 @@ async function persistImmediateEntry(
|
||||
}
|
||||
|
||||
/** Create an untitled note and write its backing file before opening it. */
|
||||
async function createNoteImmediate(deps: ImmediateCreateDeps, type?: string): Promise<boolean> {
|
||||
const noteType = type || 'Note'
|
||||
function resolveImmediateCreationVaultPath(deps: ImmediateCreateDeps, request: ImmediateCreateRequest): string {
|
||||
return request.vaultPath ?? resolveCreationVaultPath(deps.vaultPath, deps.defaultWorkspacePath, deps.vaults)
|
||||
}
|
||||
|
||||
function immediateNoteRelativePath(slug: string, folderPath?: string): string {
|
||||
const folder = normalizeVaultRelativePath(folderPath ?? '')
|
||||
return folder ? `${folder}/${slug}.md` : `${slug}.md`
|
||||
}
|
||||
|
||||
async function createNoteImmediate(deps: ImmediateCreateDeps, request: ImmediateCreateRequest): Promise<boolean> {
|
||||
const noteType = request.type || 'Note'
|
||||
const slug = generateUntitledFilename(deps.entries, noteType, deps.pendingSlugs)
|
||||
const title = slug_to_title(slug)
|
||||
const template = resolveTemplate({ entries: deps.entries, typeName: noteType })
|
||||
const defaults = resolveTypeInstanceDefaults({ entries: deps.entries, typeName: noteType })
|
||||
const status = null
|
||||
const creationVaultPath = resolveCreationVaultPath(deps.vaultPath, deps.defaultWorkspacePath, deps.vaults)
|
||||
const creationVaultPath = resolveImmediateCreationVaultPath(deps, request)
|
||||
const relativePath = immediateNoteRelativePath(slug, request.folderPath)
|
||||
const entry = {
|
||||
...buildNewEntry({ path: joinVaultPath(creationVaultPath, `${slug}.md`), slug, title, type: noteType, status }),
|
||||
...buildNewEntry({ path: joinVaultPath(creationVaultPath, relativePath), slug, title, type: noteType, status }),
|
||||
workspace: workspaceForVaultPath(creationVaultPath, deps.vaults, deps.defaultWorkspacePath),
|
||||
}
|
||||
const resolved = applyTypeDefaults({
|
||||
@@ -728,7 +757,7 @@ function trackImmediateCreate(request: ImmediateCreateRequest, didCreate: boolea
|
||||
if (!didCreate) return
|
||||
trackEvent('note_created', {
|
||||
has_type: request.type ? 1 : 0,
|
||||
creation_path: request.type ? 'type_section' : 'cmd_n',
|
||||
creation_path: request.creationPath ?? (request.type ? 'type_section' : 'cmd_n'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -784,7 +813,9 @@ function useLatestImmediateCreateDeps(
|
||||
return { latestDepsRef, syncDeps }
|
||||
}
|
||||
|
||||
function useImmediateCreateQueue(config: ImmediateCreateQueueConfig): (type?: string) => void {
|
||||
function useImmediateCreateQueue(
|
||||
config: ImmediateCreateQueueConfig,
|
||||
): (type?: string, options?: ImmediateCreateOptions) => void {
|
||||
const pendingSlugsRef = useRef<Set<string>>(new Set())
|
||||
const queuedImmediateCreatesRef = useRef<ImmediateCreateRequest[]>([])
|
||||
const immediateCreateLockedRef = useRef(false)
|
||||
@@ -797,7 +828,7 @@ function useImmediateCreateQueue(config: ImmediateCreateQueueConfig): (type?: st
|
||||
if (!deps) return
|
||||
|
||||
try {
|
||||
const didCreate = await createNoteImmediate(deps, request.type)
|
||||
const didCreate = await createNoteImmediate(deps, request)
|
||||
trackImmediateCreate(request, didCreate)
|
||||
} catch (error) {
|
||||
console.warn('Failed to create immediate note:', error)
|
||||
@@ -831,9 +862,9 @@ function useImmediateCreateQueue(config: ImmediateCreateQueueConfig): (type?: st
|
||||
}
|
||||
}, [])
|
||||
|
||||
return useCallback((type?: string) => {
|
||||
return useCallback((type?: string, options: ImmediateCreateOptions = {}) => {
|
||||
syncDeps()
|
||||
const request = { type }
|
||||
const request = { ...options, type }
|
||||
if (immediateCreateLockedRef.current) {
|
||||
queuedImmediateCreatesRef.current.push(request)
|
||||
return
|
||||
@@ -930,6 +961,7 @@ export function useNoteCreation(config: NoteCreationConfig, tabDeps: CreationTab
|
||||
removePendingSave,
|
||||
setToastMessage,
|
||||
})
|
||||
useCreateNoteInFolderRequests(handleCreateNoteImmediate)
|
||||
|
||||
return {
|
||||
handleCreateNote,
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Паказаць архіваваныя нататкі",
|
||||
"command.navigation.listType": "Спіс {type}",
|
||||
"command.note.newNote": "Новая нататка",
|
||||
"command.note.newNoteInCurrentFolder": "Стварыць новую нататку ў бягучай папцы",
|
||||
"command.note.newType": "Новы тып",
|
||||
"command.note.newTypedNote": "Стварыць {type}",
|
||||
"command.note.saveNote": "Захаваць нататку",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Стварыць папку",
|
||||
"sidebar.action.renameFolder": "Перайменаваць папку",
|
||||
"sidebar.action.deleteFolder": "Выдаліць папку",
|
||||
"sidebar.action.createNodeInFolderMenu": "Стварыць новы вузел у гэтай папцы",
|
||||
"sidebar.action.revealFolderMenu": "Паказаць у Finder / Explorer",
|
||||
"sidebar.action.copyFolderPathMenu": "Капіяваць шлях",
|
||||
"sidebar.action.renameFolderMenu": "Перайменаваць…",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Pakazać archivavanyja natatki",
|
||||
"command.navigation.listType": "Spis {type}",
|
||||
"command.note.newNote": "Novaja natatka",
|
||||
"command.note.newNoteInCurrentFolder": "Stvaryć novuju natatku ŭ biahučaj papcy",
|
||||
"command.note.newType": "Novy typ",
|
||||
"command.note.newTypedNote": "Stvaryć {type}",
|
||||
"command.note.saveNote": "Zachavać natatku",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Stvaryć papku",
|
||||
"sidebar.action.renameFolder": "Pierajmienavać papku",
|
||||
"sidebar.action.deleteFolder": "Vydalić papku",
|
||||
"sidebar.action.createNodeInFolderMenu": "Stvaryć novy vuziel u hetaj papcy",
|
||||
"sidebar.action.revealFolderMenu": "Pakazać u Finder / Explorer",
|
||||
"sidebar.action.copyFolderPathMenu": "Kapijavać šliach",
|
||||
"sidebar.action.renameFolderMenu": "Pierajmienavać…",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Archivierte Notizen anzeigen",
|
||||
"command.navigation.listType": "{type}-Liste",
|
||||
"command.note.newNote": "Neue Notiz",
|
||||
"command.note.newNoteInCurrentFolder": "Neue Notiz im aktuellen Ordner erstellen",
|
||||
"command.note.newType": "Neuer Typ",
|
||||
"command.note.newTypedNote": "Neuer {type}",
|
||||
"command.note.saveNote": "Notiz speichern",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Ordner erstellen",
|
||||
"sidebar.action.renameFolder": "Ordner umbenennen",
|
||||
"sidebar.action.deleteFolder": "Ordner löschen",
|
||||
"sidebar.action.createNodeInFolderMenu": "Neuen Knoten in diesem Ordner erstellen",
|
||||
"sidebar.action.revealFolderMenu": "Im Finder anzeigen",
|
||||
"sidebar.action.copyFolderPathMenu": "Ordnerpfad kopieren",
|
||||
"sidebar.action.renameFolderMenu": "Ordner umbenennen …",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Show Archived Notes",
|
||||
"command.navigation.listType": "List {type}",
|
||||
"command.note.newNote": "New Note",
|
||||
"command.note.newNoteInCurrentFolder": "Create New Note in Current Folder",
|
||||
"command.note.newType": "New Type",
|
||||
"command.note.newTypedNote": "New {type}",
|
||||
"command.note.saveNote": "Save Note",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Create folder",
|
||||
"sidebar.action.renameFolder": "Rename folder",
|
||||
"sidebar.action.deleteFolder": "Delete folder",
|
||||
"sidebar.action.createNodeInFolderMenu": "Create new node in this folder",
|
||||
"sidebar.action.revealFolderMenu": "Reveal in Finder",
|
||||
"sidebar.action.copyFolderPathMenu": "Copy folder path",
|
||||
"sidebar.action.renameFolderMenu": "Rename folder...",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Mostrar notas archivadas",
|
||||
"command.navigation.listType": "Lista {type}",
|
||||
"command.note.newNote": "Nueva nota",
|
||||
"command.note.newNoteInCurrentFolder": "Crear nueva nota en la carpeta actual",
|
||||
"command.note.newType": "Nuevo tipo",
|
||||
"command.note.newTypedNote": "Nuevo {type}",
|
||||
"command.note.saveNote": "Guardar nota",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Crear carpeta",
|
||||
"sidebar.action.renameFolder": "Cambiar el nombre de la carpeta",
|
||||
"sidebar.action.deleteFolder": "Eliminar carpeta",
|
||||
"sidebar.action.createNodeInFolderMenu": "Crear un nuevo nodo en esta carpeta",
|
||||
"sidebar.action.revealFolderMenu": "Mostrar en el Finder",
|
||||
"sidebar.action.copyFolderPathMenu": "Copiar ruta de la carpeta",
|
||||
"sidebar.action.renameFolderMenu": "Cambiar el nombre de la carpeta...",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Mostrar notas archivadas",
|
||||
"command.navigation.listType": "Lista {type}",
|
||||
"command.note.newNote": "Nueva nota",
|
||||
"command.note.newNoteInCurrentFolder": "Crear nueva nota en la carpeta actual",
|
||||
"command.note.newType": "Nuevo tipo",
|
||||
"command.note.newTypedNote": "Nuevo {type}",
|
||||
"command.note.saveNote": "Guardar nota",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Crear carpeta",
|
||||
"sidebar.action.renameFolder": "Cambiar el nombre de la carpeta",
|
||||
"sidebar.action.deleteFolder": "Eliminar carpeta",
|
||||
"sidebar.action.createNodeInFolderMenu": "Crear un nuevo nodo en esta carpeta",
|
||||
"sidebar.action.revealFolderMenu": "Mostrar en el Finder",
|
||||
"sidebar.action.copyFolderPathMenu": "Copiar ruta de la carpeta",
|
||||
"sidebar.action.renameFolderMenu": "Cambiar el nombre de la carpeta...",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Afficher les notes archivées",
|
||||
"command.navigation.listType": "Liste {type}",
|
||||
"command.note.newNote": "Nouvelle note",
|
||||
"command.note.newNoteInCurrentFolder": "Créer une nouvelle note dans le dossier actuel",
|
||||
"command.note.newType": "Nouveau type",
|
||||
"command.note.newTypedNote": "Nouveau {type}",
|
||||
"command.note.saveNote": "Enregistrer la note",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Créer un dossier",
|
||||
"sidebar.action.renameFolder": "Renommer le dossier",
|
||||
"sidebar.action.deleteFolder": "Supprimer le dossier",
|
||||
"sidebar.action.createNodeInFolderMenu": "Créer un nouveau nœud dans ce dossier",
|
||||
"sidebar.action.revealFolderMenu": "Afficher dans le Finder",
|
||||
"sidebar.action.copyFolderPathMenu": "Copier le chemin du dossier",
|
||||
"sidebar.action.renameFolderMenu": "Renommer le dossier…",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Mostra note archiviate",
|
||||
"command.navigation.listType": "Elenco {type}",
|
||||
"command.note.newNote": "Nuova nota",
|
||||
"command.note.newNoteInCurrentFolder": "Crea nuova nota nella cartella corrente",
|
||||
"command.note.newType": "Nuovo tipo",
|
||||
"command.note.newTypedNote": "Nuovo {type}",
|
||||
"command.note.saveNote": "Salva nota",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Crea cartella",
|
||||
"sidebar.action.renameFolder": "Rinomina cartella",
|
||||
"sidebar.action.deleteFolder": "Elimina cartella",
|
||||
"sidebar.action.createNodeInFolderMenu": "Crea un nuovo nodo in questa cartella",
|
||||
"sidebar.action.revealFolderMenu": "Mostra in Finder",
|
||||
"sidebar.action.copyFolderPathMenu": "Copia percorso della cartella",
|
||||
"sidebar.action.renameFolderMenu": "Rinomina cartella...",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "アーカイブされたノートを表示",
|
||||
"command.navigation.listType": "{type}のリスト",
|
||||
"command.note.newNote": "新規ノート",
|
||||
"command.note.newNoteInCurrentFolder": "現在のフォルダに新しいノートを作成",
|
||||
"command.note.newType": "新しいタイプ",
|
||||
"command.note.newTypedNote": "新規{type}",
|
||||
"command.note.saveNote": "メモを保存",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "フォルダーを作成",
|
||||
"sidebar.action.renameFolder": "フォルダー名を変更",
|
||||
"sidebar.action.deleteFolder": "フォルダーを削除",
|
||||
"sidebar.action.createNodeInFolderMenu": "このフォルダに新しいノードを作成",
|
||||
"sidebar.action.revealFolderMenu": "Finder で表示",
|
||||
"sidebar.action.copyFolderPathMenu": "フォルダパスをコピー",
|
||||
"sidebar.action.renameFolderMenu": "フォルダー名を変更...",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "보관된 노트 표시",
|
||||
"command.navigation.listType": "{type} 목록",
|
||||
"command.note.newNote": "새 노트",
|
||||
"command.note.newNoteInCurrentFolder": "현재 폴더에 새 노트 만들기",
|
||||
"command.note.newType": "새 유형",
|
||||
"command.note.newTypedNote": "새 {type}",
|
||||
"command.note.saveNote": "노트 저장",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "폴더 만들기",
|
||||
"sidebar.action.renameFolder": "폴더 이름 변경",
|
||||
"sidebar.action.deleteFolder": "폴더 삭제",
|
||||
"sidebar.action.createNodeInFolderMenu": "이 폴더에 새 노드 만들기",
|
||||
"sidebar.action.revealFolderMenu": "Finder에서 표시",
|
||||
"sidebar.action.copyFolderPathMenu": "폴더 경로 복사",
|
||||
"sidebar.action.renameFolderMenu": "폴더 이름 변경...",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Pokaż zarchiwizowane notatki",
|
||||
"command.navigation.listType": "Lista {type}",
|
||||
"command.note.newNote": "Nowa notatka",
|
||||
"command.note.newNoteInCurrentFolder": "Utwórz nową notatkę w bieżącym folderze",
|
||||
"command.note.newType": "Nowy typ",
|
||||
"command.note.newTypedNote": "Nowy {type}",
|
||||
"command.note.saveNote": "Zapisz notatkę",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Utwórz folder",
|
||||
"sidebar.action.renameFolder": "Zmień nazwę folderu",
|
||||
"sidebar.action.deleteFolder": "Usuń folder",
|
||||
"sidebar.action.createNodeInFolderMenu": "Utwórz nowy węzeł w tym folderze",
|
||||
"sidebar.action.revealFolderMenu": "Pokaż w Finderze",
|
||||
"sidebar.action.copyFolderPathMenu": "Kopiuj ścieżkę folderu",
|
||||
"sidebar.action.renameFolderMenu": "Zmień nazwę folderu...",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Exibir notas arquivadas",
|
||||
"command.navigation.listType": "Lista {type}",
|
||||
"command.note.newNote": "Nova nota",
|
||||
"command.note.newNoteInCurrentFolder": "Criar nova nota na pasta atual",
|
||||
"command.note.newType": "Novo tipo",
|
||||
"command.note.newTypedNote": "Novo {type}",
|
||||
"command.note.saveNote": "Salvar nota",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Criar pasta",
|
||||
"sidebar.action.renameFolder": "Renomear pasta",
|
||||
"sidebar.action.deleteFolder": "Excluir pasta",
|
||||
"sidebar.action.createNodeInFolderMenu": "Criar novo nó nesta pasta",
|
||||
"sidebar.action.revealFolderMenu": "Exibir no Finder",
|
||||
"sidebar.action.copyFolderPathMenu": "Copiar caminho da pasta",
|
||||
"sidebar.action.renameFolderMenu": "Renomear pasta...",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Mostrar notas arquivadas",
|
||||
"command.navigation.listType": "Lista {type}",
|
||||
"command.note.newNote": "Nova nota",
|
||||
"command.note.newNoteInCurrentFolder": "Criar nova nota na pasta atual",
|
||||
"command.note.newType": "Novo tipo",
|
||||
"command.note.newTypedNote": "Novo {type}",
|
||||
"command.note.saveNote": "Guardar nota",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Criar pasta",
|
||||
"sidebar.action.renameFolder": "Mudar o nome da pasta",
|
||||
"sidebar.action.deleteFolder": "Eliminar pasta",
|
||||
"sidebar.action.createNodeInFolderMenu": "Criar novo nó nesta pasta",
|
||||
"sidebar.action.revealFolderMenu": "Mostrar no Finder",
|
||||
"sidebar.action.copyFolderPathMenu": "Copiar caminho da pasta",
|
||||
"sidebar.action.renameFolderMenu": "Mudar o nome da pasta...",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Показать заархивированные заметки",
|
||||
"command.navigation.listType": "Список {type}",
|
||||
"command.note.newNote": "Новая заметка",
|
||||
"command.note.newNoteInCurrentFolder": "Создать новую заметку в текущей папке",
|
||||
"command.note.newType": "Новый тип",
|
||||
"command.note.newTypedNote": "Новый {type}",
|
||||
"command.note.saveNote": "Сохранить заметку",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Создать папку",
|
||||
"sidebar.action.renameFolder": "Переименовать папку",
|
||||
"sidebar.action.deleteFolder": "Удалить папку",
|
||||
"sidebar.action.createNodeInFolderMenu": "Создать новый узел в этой папке",
|
||||
"sidebar.action.revealFolderMenu": "Показать в Finder",
|
||||
"sidebar.action.copyFolderPathMenu": "Копировать путь к папке",
|
||||
"sidebar.action.renameFolderMenu": "Переименовать папку...",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "Hiển thị ghi chú đã lưu trữ",
|
||||
"command.navigation.listType": "Danh sách {type}",
|
||||
"command.note.newNote": "Ghi chú mới",
|
||||
"command.note.newNoteInCurrentFolder": "Tạo ghi chú mới trong thư mục hiện tại",
|
||||
"command.note.newType": "Loại mới",
|
||||
"command.note.newTypedNote": "{type} mới",
|
||||
"command.note.saveNote": "Lưu ghi chú",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "Tạo thư mục",
|
||||
"sidebar.action.renameFolder": "Đổi tên thư mục",
|
||||
"sidebar.action.deleteFolder": "Xóa thư mục",
|
||||
"sidebar.action.createNodeInFolderMenu": "Tạo nút mới trong thư mục này",
|
||||
"sidebar.action.revealFolderMenu": "Hiển thị trong Finder",
|
||||
"sidebar.action.copyFolderPathMenu": "Sao chép đường dẫn thư mục",
|
||||
"sidebar.action.renameFolderMenu": "Đổi tên thư mục...",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "显示归档笔记",
|
||||
"command.navigation.listType": "列出{type}",
|
||||
"command.note.newNote": "新建笔记",
|
||||
"command.note.newNoteInCurrentFolder": "在当前文件夹中创建新笔记",
|
||||
"command.note.newType": "新建类型",
|
||||
"command.note.newTypedNote": "新建{type}",
|
||||
"command.note.saveNote": "保存笔记",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "创建文件夹",
|
||||
"sidebar.action.renameFolder": "重命名文件夹",
|
||||
"sidebar.action.deleteFolder": "删除文件夹",
|
||||
"sidebar.action.createNodeInFolderMenu": "在此文件夹中创建新节点",
|
||||
"sidebar.action.revealFolderMenu": "在访达中显示",
|
||||
"sidebar.action.copyFolderPathMenu": "复制文件夹路径",
|
||||
"sidebar.action.renameFolderMenu": "重命名文件夹...",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"command.navigation.showArchivedNotes": "顯示歸檔筆記",
|
||||
"command.navigation.listType": "列出{type}",
|
||||
"command.note.newNote": "新建筆記",
|
||||
"command.note.newNoteInCurrentFolder": "在目前資料夾中建立新筆記",
|
||||
"command.note.newType": "新建型別",
|
||||
"command.note.newTypedNote": "新建{type}",
|
||||
"command.note.saveNote": "儲存筆記",
|
||||
@@ -364,6 +365,7 @@
|
||||
"sidebar.action.createFolder": "建立資料夾",
|
||||
"sidebar.action.renameFolder": "重新命名資料夾",
|
||||
"sidebar.action.deleteFolder": "刪除資料夾",
|
||||
"sidebar.action.createNodeInFolderMenu": "在此資料夾中建立新節點",
|
||||
"sidebar.action.revealFolderMenu": "在訪達中顯示",
|
||||
"sidebar.action.copyFolderPathMenu": "複製資料夾路徑",
|
||||
"sidebar.action.renameFolderMenu": "重新命名資料夾...",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { expect, type Page } from '@playwright/test'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import type { FolderNode } from '../../src/types'
|
||||
import { installFixtureVaultDesktopBridgeInBrowser } from './fixtureVaultDesktopBridge'
|
||||
|
||||
const FIXTURE_VAULT = path.resolve('tests/fixtures/test-vault')
|
||||
@@ -15,6 +16,7 @@ interface FixtureVaultPageArgs {
|
||||
page: Page
|
||||
vaultPath: string
|
||||
isGitRepo: boolean
|
||||
folders: FolderNode[]
|
||||
}
|
||||
|
||||
interface FixturePageArgs {
|
||||
@@ -24,6 +26,7 @@ interface FixturePageArgs {
|
||||
interface FixtureVaultOptions {
|
||||
isGitRepo?: boolean
|
||||
expectedReadyTitle?: string
|
||||
folders?: FolderNode[]
|
||||
}
|
||||
|
||||
interface CopyDirArgs {
|
||||
@@ -68,8 +71,8 @@ export function removeFixtureVaultCopy(tempVaultDir: string | null | undefined):
|
||||
removeFixtureVaultDirectory({ tempVaultDir })
|
||||
}
|
||||
|
||||
async function installFixtureVaultInitScript({ page, vaultPath, isGitRepo }: FixtureVaultPageArgs): Promise<void> {
|
||||
await page.addInitScript(({ dismissedKey, initialIsGitRepo, resolvedVaultPath }: { dismissedKey: string; initialIsGitRepo: boolean; resolvedVaultPath: string }) => {
|
||||
async function installFixtureVaultInitScript({ page, vaultPath, isGitRepo, folders }: FixtureVaultPageArgs): Promise<void> {
|
||||
await page.addInitScript(({ dismissedKey, fixtureFolders, initialIsGitRepo, resolvedVaultPath }: { dismissedKey: string; fixtureFolders: FolderNode[]; initialIsGitRepo: boolean; resolvedVaultPath: string }) => {
|
||||
localStorage.clear()
|
||||
localStorage.setItem(dismissedKey, '1')
|
||||
let gitRepoReady = initialIsGitRepo
|
||||
@@ -303,7 +306,7 @@ async function installFixtureVaultInitScript({ page, vaultPath, isGitRepo }: Fix
|
||||
const buildFixtureReadHandlers = () => ({
|
||||
list_vault: (commandArgs?: FixtureCommandArgs) => readVaultList(commandArgs),
|
||||
reload_vault: (commandArgs?: FixtureCommandArgs) => readVaultList(commandArgs, true),
|
||||
list_vault_folders: () => [],
|
||||
list_vault_folders: () => fixtureFolders,
|
||||
list_views: () => [],
|
||||
get_modified_files: () => [],
|
||||
detect_renames: () => [],
|
||||
@@ -437,6 +440,7 @@ async function installFixtureVaultInitScript({ page, vaultPath, isGitRepo }: Fix
|
||||
})
|
||||
}, {
|
||||
dismissedKey: CLAUDE_CODE_ONBOARDING_DISMISSED_KEY,
|
||||
fixtureFolders: folders,
|
||||
initialIsGitRepo: isGitRepo,
|
||||
resolvedVaultPath: vaultPath,
|
||||
})
|
||||
@@ -460,6 +464,7 @@ export async function openFixtureVault(
|
||||
page,
|
||||
vaultPath,
|
||||
isGitRepo: options.isGitRepo ?? true,
|
||||
folders: options.folders ?? [],
|
||||
})
|
||||
await waitForFixtureVaultReady({
|
||||
page,
|
||||
|
||||
44
tests/smoke/create-note-in-folder.spec.ts
Normal file
44
tests/smoke/create-note-in-folder.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { test, expect } from '@playwright/test'
|
||||
import {
|
||||
createFixtureVaultCopy,
|
||||
openFixtureVaultDesktopHarness,
|
||||
removeFixtureVaultCopy,
|
||||
} from '../helpers/fixtureVault'
|
||||
import { executeCommand, openCommandPalette } from './helpers'
|
||||
|
||||
let tempVaultDir: string
|
||||
|
||||
function untitledNotesInProject(): string[] {
|
||||
const projectDir = path.join(tempVaultDir, 'project')
|
||||
return fs.readdirSync(projectDir).filter((name) => /^untitled-note-\d+(?:-\d+)?\.md$/.test(name))
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
tempVaultDir = createFixtureVaultCopy()
|
||||
await openFixtureVaultDesktopHarness(page, tempVaultDir, {
|
||||
folders: [{ children: [], name: 'project', path: 'project' }],
|
||||
})
|
||||
await expect(page.getByTestId('folder-row:project')).toBeVisible({ timeout: 5_000 })
|
||||
})
|
||||
|
||||
test.afterEach(() => {
|
||||
removeFixtureVaultCopy(tempVaultDir)
|
||||
})
|
||||
|
||||
test('creates new notes inside the targeted folder @smoke', async ({ page }) => {
|
||||
await page.getByTestId('folder-row:project').click({ button: 'right' })
|
||||
await page.getByTestId('create-node-in-folder-menu-item').click()
|
||||
|
||||
await expect.poll(untitledNotesInProject, { timeout: 5_000 }).toHaveLength(1)
|
||||
|
||||
await page.getByTestId('folder-row:project').click()
|
||||
await openCommandPalette(page)
|
||||
await executeCommand(page, 'Create New Note in Current Folder')
|
||||
|
||||
await expect.poll(untitledNotesInProject, { timeout: 5_000 }).toHaveLength(2)
|
||||
for (const filename of untitledNotesInProject()) {
|
||||
expect(fs.readFileSync(path.join(tempVaultDir, 'project', filename), 'utf8')).toContain('type: Note')
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user