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:
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user