feat: add organized toggle to breadcrumb bar and command palette

- CheckCircle button in breadcrumb bar toggles _organized property
- Green filled icon when organized, gray outline when not
- Command palette: "Mark as Organized" / "Mark as Unorganized"
- _organized hidden from Properties panel
- Handler follows favorite toggle pattern with optimistic updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-04-04 20:54:16 +02:00
parent 49d7353f2d
commit 1145f4a572
460 changed files with 3933 additions and 145 deletions

View File

@@ -507,6 +507,7 @@ function App() {
onSetNoteListFilter: setNoteListFilter,
onOpenInNewWindow: handleOpenInNewWindow,
onToggleFavorite: entryActions.handleToggleFavorite,
onToggleOrganized: entryActions.handleToggleOrganized,
})
const activeTab = notes.tabs.find((t) => t.entry.path === notes.activeTabPath) ?? null
@@ -620,6 +621,7 @@ function App() {
noteList={aiNoteList}
noteListFilter={aiNoteListFilter}
onToggleFavorite={entryActions.handleToggleFavorite}
onToggleOrganized={entryActions.handleToggleOrganized}
onTrashNote={entryActions.handleTrashNote}
onRestoreNote={entryActions.handleRestoreNote}
onDeleteNote={deleteActions.handleDeleteNote}

View File

@@ -14,6 +14,7 @@ import {
Archive,
ArrowUUpLeft,
Star,
CheckCircle,
} from '@phosphor-icons/react'
interface BreadcrumbBarProps {
@@ -30,6 +31,7 @@ interface BreadcrumbBarProps {
inspectorCollapsed?: boolean
onToggleInspector?: () => void
onToggleFavorite?: () => void
onToggleOrganized?: () => void
onTrash?: () => void
onRestore?: () => void
onArchive?: () => void
@@ -58,7 +60,7 @@ function RawToggleButton({ rawMode, onToggleRaw }: { rawMode?: boolean; onToggle
function BreadcrumbActions({ entry, showDiffToggle, diffMode, diffLoading, onToggleDiff,
rawMode, onToggleRaw,
showAIChat, onToggleAIChat, inspectorCollapsed, onToggleInspector,
onToggleFavorite, onTrash, onRestore, onArchive, onUnarchive,
onToggleFavorite, onToggleOrganized, onTrash, onRestore, onArchive, onUnarchive,
}: Omit<BreadcrumbBarProps, 'wordCount'>) {
return (
<div className="breadcrumb-bar__actions ml-auto flex items-center" style={{ gap: 12 }}>
@@ -72,6 +74,16 @@ function BreadcrumbActions({ entry, showDiffToggle, diffMode, diffLoading, onTog
>
<Star size={16} weight={entry.favorite ? 'fill' : 'regular'} />
</button>
<button
className={cn(
"flex items-center justify-center border-none bg-transparent p-0 cursor-pointer transition-colors",
entry.organized ? "text-green-600" : "text-muted-foreground hover:text-foreground"
)}
onClick={onToggleOrganized}
title={entry.organized ? 'Mark as unorganized (back to Inbox)' : 'Mark as organized (remove from Inbox)'}
>
<CheckCircle size={16} weight={entry.organized ? 'fill' : 'regular'} />
</button>
<button
className="flex items-center justify-center border-none bg-transparent p-0 text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
title="Search in file"

View File

@@ -48,6 +48,7 @@ interface EditorProps {
noteList?: NoteListItem[]
noteListFilter?: { type: string | null; query: string }
onToggleFavorite?: (path: string) => void
onToggleOrganized?: (path: string) => void
onTrashNote?: (path: string) => void
onRestoreNote?: (path: string) => void
onDeleteNote?: (path: string) => void
@@ -207,7 +208,7 @@ export const Editor = memo(function Editor(props: EditorProps) {
onUpdateFrontmatter, onDeleteProperty, onAddProperty, onCreateAndOpenNote, onInitializeProperties,
showAIChat, onToggleAIChat,
vaultPath, noteList, noteListFilter,
onToggleFavorite, onTrashNote, onRestoreNote, onDeleteNote, onArchiveNote, onUnarchiveNote,
onToggleFavorite, onToggleOrganized, onTrashNote, onRestoreNote, onDeleteNote, onArchiveNote, onUnarchiveNote,
onContentChange, onSave, onTitleSync,
onFileCreated, onFileModified, onVaultChanged,
onSetNoteIcon, onRemoveNoteIcon,
@@ -254,6 +255,7 @@ export const Editor = memo(function Editor(props: EditorProps) {
onNavigateWikilink={onNavigateWikilink}
onEditorChange={handleEditorChange}
onToggleFavorite={onToggleFavorite}
onToggleOrganized={onToggleOrganized}
onTrashNote={onTrashNote}
onRestoreNote={onRestoreNote}
onDeleteNote={onDeleteNote}

View File

@@ -42,6 +42,7 @@ interface EditorContentProps {
onNavigateWikilink: (target: string) => void
onEditorChange?: () => void
onToggleFavorite?: (path: string) => void
onToggleOrganized?: (path: string) => void
onTrashNote?: (path: string) => void
onRestoreNote?: (path: string) => void
onDeleteNote?: (path: string) => void
@@ -146,6 +147,7 @@ function ActiveTabBreadcrumb({ activeTab, barRef, props }: {
inspectorCollapsed={props.inspectorCollapsed}
onToggleInspector={props.onToggleInspector}
onToggleFavorite={bindPath(props.onToggleFavorite, path)}
onToggleOrganized={bindPath(props.onToggleOrganized, path)}
onTrash={bindPath(props.onTrashNote, path)}
onRestore={bindPath(props.onRestoreNote, path)}
onArchive={bindPath(props.onArchiveNote, path)}

View File

@@ -102,11 +102,11 @@ export const FolderTree = memo(function FolderTree({ folders, selection, onSelec
if (folders.length === 0 && !isCreating) return null
return (
<div style={{ padding: '4px 6px' }}>
<div style={{ padding: '0 6px' }}>
{/* Header */}
<button
className="flex w-full cursor-pointer select-none items-center justify-between border-none bg-transparent text-muted-foreground"
style={{ padding: '6px 14px 6px 16px' }}
style={{ padding: '8px 14px 8px 16px' }}
onClick={() => onToggle ? onToggle() : setInternalCollapsed((v) => !v)}
>
<div className="flex items-center gap-1">

View File

@@ -21,6 +21,8 @@ interface NoteCommandsConfig {
onOpenInNewWindow?: () => void
onToggleFavorite?: (path: string) => void
isFavorite?: boolean
onToggleOrganized?: (path: string) => void
isOrganized?: boolean
}
export function buildNoteCommands(config: NoteCommandsConfig): CommandAction[] {
@@ -31,6 +33,7 @@ export function buildNoteCommands(config: NoteCommandsConfig): CommandAction[] {
onEmptyTrash, trashedCount,
onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon,
onOpenInNewWindow, onToggleFavorite, isFavorite,
onToggleOrganized, isOrganized,
} = config
return [
@@ -55,6 +58,12 @@ export function buildNoteCommands(config: NoteCommandsConfig): CommandAction[] {
enabled: hasActiveNote && !!onToggleFavorite,
execute: () => { if (activeTabPath) onToggleFavorite?.(activeTabPath) },
},
{
id: 'toggle-organized', label: isOrganized ? 'Mark as Unorganized' : 'Mark as Organized', group: 'Note',
keywords: ['organized', 'inbox', 'triage', 'done'],
enabled: hasActiveNote && !!onToggleOrganized,
execute: () => { if (activeTabPath) onToggleOrganized?.(activeTabPath) },
},
{
id: 'set-note-icon', label: 'Set Note Icon', group: 'Note',
keywords: ['icon', 'emoji', 'set', 'add', 'change', 'picker'],

View File

@@ -68,6 +68,7 @@ interface AppCommandsConfig {
onSetNoteListFilter?: (filter: NoteListFilter) => void
onOpenInNewWindow?: () => void
onToggleFavorite?: (path: string) => void
onToggleOrganized?: (path: string) => void
}
/** Sets up keyboard shortcuts, command registry, menu events, and keyboard navigation. */
@@ -212,6 +213,8 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
noteListFilter: config.noteListFilter,
onSetNoteListFilter: config.onSetNoteListFilter,
onOpenInNewWindow: config.onOpenInNewWindow,
onToggleFavorite: config.onToggleFavorite,
onToggleOrganized: config.onToggleOrganized,
})
useKeyboardNavigation({

View File

@@ -31,6 +31,7 @@ interface CommandRegistryConfig {
onRemoveNoteIcon?: () => void
onOpenInNewWindow?: () => void
onToggleFavorite?: (path: string) => void
onToggleOrganized?: (path: string) => void
onQuickOpen: () => void
onCreateNote: () => void
onCreateNoteOfType: (type: string) => void
@@ -86,7 +87,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
mcpStatus, onInstallMcp, onEmptyTrash, trashedCount,
onReloadVault, onRepairVault,
onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon,
onOpenInNewWindow, onToggleFavorite,
onOpenInNewWindow, onToggleFavorite, onToggleOrganized,
selection, noteListFilter, onSetNoteListFilter,
} = config
@@ -110,6 +111,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
onCreateNote, onCreateType, onOpenDailyNote, onSave,
onTrashNote, onRestoreNote, onArchiveNote, onUnarchiveNote,
onEmptyTrash, trashedCount, onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon, onOpenInNewWindow, onToggleFavorite, isFavorite,
onToggleOrganized, isOrganized: activeEntry?.organized ?? false,
}),
...buildGitCommands({ modifiedCount, onCommitPush, onPull, onResolveConflicts, onSelect }),
...buildViewCommands({
@@ -139,5 +141,6 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon,
isSectionGroup, noteListFilter, onSetNoteListFilter,
onOpenInNewWindow, onToggleFavorite, isFavorite,
onToggleOrganized, activeEntry,
])
}

View File

@@ -158,6 +158,32 @@ export function useEntryActions({
}
}, [entries, updateEntry, handleUpdateFrontmatter, handleDeleteProperty, setToastMessage, onFrontmatterPersisted])
const handleToggleOrganized = useCallback(async (path: string) => {
const entry = entries.find((e) => e.path === path)
if (!entry) return
if (entry.organized) {
trackEvent('note_unorganized')
updateEntry(path, { organized: false })
try {
await handleDeleteProperty(path, '_organized', { silent: true })
onFrontmatterPersisted?.()
} catch {
updateEntry(path, { organized: true })
setToastMessage('Failed to unorganize — rolled back')
}
} else {
trackEvent('note_organized')
updateEntry(path, { organized: true })
try {
await handleUpdateFrontmatter(path, '_organized', true, { silent: true })
onFrontmatterPersisted?.()
} catch {
updateEntry(path, { organized: false })
setToastMessage('Failed to organize — rolled back')
}
}
}, [entries, updateEntry, handleUpdateFrontmatter, handleDeleteProperty, setToastMessage, onFrontmatterPersisted])
const handleReorderFavorites = useCallback(async (orderedPaths: string[]) => {
for (let i = 0; i < orderedPaths.length; i++) {
updateEntry(orderedPaths[i], { favoriteIndex: i })
@@ -178,5 +204,5 @@ export function useEntryActions({
onFrontmatterPersisted?.()
}, [entries, handleUpdateFrontmatter, handleDeleteProperty, updateEntry, createTypeEntry, onFrontmatterPersisted])
return { handleTrashNote, handleRestoreNote, handleArchiveNote, handleUnarchiveNote, handleCustomizeType, handleReorderSections, handleUpdateTypeTemplate, handleRenameSection, handleToggleTypeVisibility, handleToggleFavorite, handleReorderFavorites }
return { handleTrashNote, handleRestoreNote, handleArchiveNote, handleUnarchiveNote, handleCustomizeType, handleReorderSections, handleUpdateTypeTemplate, handleRenameSection, handleToggleTypeVisibility, handleToggleFavorite, handleToggleOrganized, handleReorderFavorites }
}

View File

@@ -12,7 +12,7 @@ import { containsWikilinks } from '../components/DynamicPropertiesPanel'
// Keys to skip showing in Properties (handled by dedicated UI or internal)
// Compared case-insensitively via isVisibleProperty()
const SKIP_KEYS = new Set(['aliases', 'workspace', 'title', 'type', 'is_a', 'is a', '_trashed', 'trashed', '_trashed_at', 'trashed_at', 'trashed at', '_archived', 'archived', 'archived_at', 'icon', '_favorite', '_favorite_index'])
const SKIP_KEYS = new Set(['aliases', 'workspace', 'title', 'type', 'is_a', 'is a', '_trashed', 'trashed', '_trashed_at', 'trashed_at', 'trashed at', '_archived', 'archived', 'archived_at', 'icon', '_favorite', '_favorite_index', '_organized'])
function coerceValue(raw: string): FrontmatterValue {
if (raw.toLowerCase() === 'true') return true