feat: customize all-notes note list columns
This commit is contained in:
27
src/App.tsx
27
src/App.tsx
@@ -505,9 +505,23 @@ function App() {
|
||||
})
|
||||
}, [setInspectorCollapsed])
|
||||
|
||||
const handleCustomizeInboxColumns = useCallback(() => {
|
||||
openNoteListPropertiesPicker('inbox')
|
||||
}, [])
|
||||
const handleCustomizeNoteListColumns = useCallback(() => {
|
||||
if (effectiveSelection.kind !== 'filter') return
|
||||
if (effectiveSelection.filter === 'all') {
|
||||
openNoteListPropertiesPicker('all')
|
||||
return
|
||||
}
|
||||
if (effectiveSelection.filter === 'inbox') {
|
||||
openNoteListPropertiesPicker('inbox')
|
||||
}
|
||||
}, [effectiveSelection])
|
||||
|
||||
const handleUpdateAllNotesNoteListProperties = useCallback((value: string[] | null) => {
|
||||
updateConfig('allNotes', {
|
||||
...(vaultConfig.allNotes ?? { noteListProperties: null }),
|
||||
noteListProperties: value && value.length > 0 ? value : null,
|
||||
})
|
||||
}, [updateConfig, vaultConfig.allNotes])
|
||||
|
||||
const handleUpdateInboxNoteListProperties = useCallback((value: string[] | null) => {
|
||||
updateConfig('inbox', {
|
||||
@@ -796,8 +810,9 @@ function App() {
|
||||
onOpenInNewWindow: handleOpenInNewWindow,
|
||||
onToggleFavorite: entryActions.handleToggleFavorite,
|
||||
onToggleOrganized: explicitOrganizationEnabled ? entryActions.handleToggleOrganized : undefined,
|
||||
onCustomizeInboxColumns: handleCustomizeInboxColumns,
|
||||
canCustomizeInboxColumns: explicitOrganizationEnabled && effectiveSelection.kind === 'filter' && effectiveSelection.filter === 'inbox',
|
||||
onCustomizeNoteListColumns: handleCustomizeNoteListColumns,
|
||||
canCustomizeNoteListColumns: effectiveSelection.kind === 'filter'
|
||||
&& (effectiveSelection.filter === 'all' || (explicitOrganizationEnabled && effectiveSelection.filter === 'inbox')),
|
||||
onRestoreDeletedNote: activeDeletedFile ? () => { void handleDiscardFile(activeDeletedFile.relativePath) } : undefined,
|
||||
canRestoreDeletedNote: !!activeDeletedFile,
|
||||
})
|
||||
@@ -891,7 +906,7 @@ function App() {
|
||||
{effectiveSelection.kind === 'filter' && effectiveSelection.filter === 'pulse' ? (
|
||||
<PulseView vaultPath={resolvedPath} onOpenNote={handlePulseOpenNote} sidebarCollapsed={!sidebarVisible} onExpandSidebar={() => setViewMode('all')} />
|
||||
) : (
|
||||
<NoteList entries={vault.entries} selection={effectiveSelection} selectedNote={activeTab?.entry ?? null} noteListFilter={noteListFilter} onNoteListFilterChange={setNoteListFilter} inboxPeriod={inboxPeriod} modifiedFiles={vault.modifiedFiles} modifiedFilesError={vault.modifiedFilesError} getNoteStatus={vault.getNoteStatus} sidebarCollapsed={!sidebarVisible} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={notes.handleReplaceActiveTab} onCreateNote={notes.handleCreateNoteImmediate} onBulkOrganize={explicitOrganizationEnabled ? bulkActions.handleBulkOrganize : undefined} onBulkArchive={bulkActions.handleBulkArchive} onBulkDeletePermanently={deleteActions.handleBulkDeletePermanently} onUpdateTypeSort={notes.handleUpdateFrontmatter} updateEntry={vault.updateEntry} onOpenInNewWindow={handleOpenEntryInNewWindow} onDiscardFile={handleDiscardFile} onAutoTriggerDiff={() => diffToggleRef.current()} onOpenDeletedNote={handleOpenDeletedNote} inboxNoteListProperties={vaultConfig.inbox?.noteListProperties ?? null} onUpdateInboxNoteListProperties={handleUpdateInboxNoteListProperties} views={vault.views} visibleNotesRef={visibleNotesRef} multiSelectionCommandRef={multiSelectionCommandRef} />
|
||||
<NoteList entries={vault.entries} selection={effectiveSelection} selectedNote={activeTab?.entry ?? null} noteListFilter={noteListFilter} onNoteListFilterChange={setNoteListFilter} inboxPeriod={inboxPeriod} modifiedFiles={vault.modifiedFiles} modifiedFilesError={vault.modifiedFilesError} getNoteStatus={vault.getNoteStatus} sidebarCollapsed={!sidebarVisible} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={notes.handleReplaceActiveTab} onCreateNote={notes.handleCreateNoteImmediate} onBulkOrganize={explicitOrganizationEnabled ? bulkActions.handleBulkOrganize : undefined} onBulkArchive={bulkActions.handleBulkArchive} onBulkDeletePermanently={deleteActions.handleBulkDeletePermanently} onUpdateTypeSort={notes.handleUpdateFrontmatter} updateEntry={vault.updateEntry} onOpenInNewWindow={handleOpenEntryInNewWindow} onDiscardFile={handleDiscardFile} onAutoTriggerDiff={() => diffToggleRef.current()} onOpenDeletedNote={handleOpenDeletedNote} allNotesNoteListProperties={vaultConfig.allNotes?.noteListProperties ?? null} onUpdateAllNotesNoteListProperties={handleUpdateAllNotesNoteListProperties} inboxNoteListProperties={vaultConfig.inbox?.noteListProperties ?? null} onUpdateInboxNoteListProperties={handleUpdateInboxNoteListProperties} views={vault.views} visibleNotesRef={visibleNotesRef} multiSelectionCommandRef={multiSelectionCommandRef} />
|
||||
)}
|
||||
</div>
|
||||
<ResizeHandle onResize={layout.handleNoteListResize} />
|
||||
|
||||
@@ -196,6 +196,52 @@ describe('NoteList rendering', () => {
|
||||
expect(screen.queryByText('Luca')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the all-notes customize-columns action and falls back to type-defined chips', () => {
|
||||
renderNoteList({
|
||||
entries: makeBookTypeEntries(['Priority'], { properties: { Priority: 'High', Owner: 'Luca' } }),
|
||||
selection: allSelection,
|
||||
allNotesNoteListProperties: null,
|
||||
onUpdateAllNotesNoteListProperties: () => undefined,
|
||||
})
|
||||
|
||||
expect(screen.getByTitle('Customize All Notes columns')).toBeInTheDocument()
|
||||
expect(screen.getByText('High')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Luca')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens the all-notes column picker from the global event and saves new columns', () => {
|
||||
const onUpdateAllNotesNoteListProperties = vi.fn()
|
||||
const archivedOwnerEntry = makeEntry({
|
||||
path: '/vault/book-archive.md',
|
||||
filename: 'book-archive.md',
|
||||
title: 'Archived Book',
|
||||
isA: 'Book',
|
||||
archived: true,
|
||||
properties: { Owner: 'Luca' },
|
||||
})
|
||||
|
||||
renderNoteList({
|
||||
entries: [
|
||||
...makeBookTypeEntries(['Priority'], { properties: { Priority: 'High' } }),
|
||||
archivedOwnerEntry,
|
||||
],
|
||||
selection: allSelection,
|
||||
allNotesNoteListProperties: null,
|
||||
onUpdateAllNotesNoteListProperties,
|
||||
})
|
||||
|
||||
act(() => {
|
||||
openNoteListPropertiesPicker('all')
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('list-properties-popover')).toBeInTheDocument()
|
||||
expect(screen.getByRole('checkbox', { name: 'Priority' })).toBeChecked()
|
||||
expect(screen.getByRole('checkbox', { name: 'Owner' })).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: 'Owner' }))
|
||||
expect(onUpdateAllNotesNoteListProperties).toHaveBeenCalledWith(['Priority', 'Owner'])
|
||||
})
|
||||
|
||||
it('opens the inbox column picker from the global event and saves new columns', () => {
|
||||
const onUpdateInboxNoteListProperties = vi.fn()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { DeletedNoteEntry } from './noteListUtils'
|
||||
import { useMultiSelect, type MultiSelectState } from '../../hooks/useMultiSelect'
|
||||
import { useNoteListKeyboard } from '../../hooks/useNoteListKeyboard'
|
||||
import { prefetchNoteContent } from '../../hooks/useTabManagement'
|
||||
import type { NoteListPropertiesScope } from './noteListPropertiesEvents'
|
||||
|
||||
// --- useTypeEntryMap ---
|
||||
|
||||
@@ -26,19 +27,71 @@ export function useTypeEntryMap(entries: VaultEntry[]) {
|
||||
|
||||
// --- useFilteredEntries ---
|
||||
|
||||
export function useFilteredEntries(entries: VaultEntry[], selection: SidebarSelection, modifiedPathSet: Set<string>, modifiedSuffixes: string[], modifiedFiles?: ModifiedFile[], subFilter?: NoteListFilter, inboxPeriod?: InboxPeriod, views?: ViewFile[]) {
|
||||
interface FilteredEntriesParams {
|
||||
entries: VaultEntry[]
|
||||
selection: SidebarSelection
|
||||
modifiedPathSet: Set<string>
|
||||
modifiedSuffixes: string[]
|
||||
modifiedFiles?: ModifiedFile[]
|
||||
subFilter?: NoteListFilter
|
||||
inboxPeriod?: InboxPeriod
|
||||
views?: ViewFile[]
|
||||
}
|
||||
|
||||
function buildFilteredEntries({
|
||||
entries,
|
||||
selection,
|
||||
isEntityView,
|
||||
isChangesView,
|
||||
isInboxView,
|
||||
modifiedPathSet,
|
||||
modifiedSuffixes,
|
||||
modifiedFiles,
|
||||
subFilter,
|
||||
inboxPeriod,
|
||||
views,
|
||||
}: FilteredEntriesParams & {
|
||||
isEntityView: boolean
|
||||
isChangesView: boolean
|
||||
isInboxView: boolean
|
||||
}) {
|
||||
if (isEntityView) return []
|
||||
if (isChangesView) {
|
||||
if (modifiedFiles) return buildChangesEntries(entries, modifiedFiles)
|
||||
return entries.filter((entry) => isModifiedEntry(entry.path, modifiedPathSet, modifiedSuffixes))
|
||||
}
|
||||
if (isInboxView) return filterInboxEntries(entries, inboxPeriod ?? 'month')
|
||||
return filterEntries(entries, selection, subFilter, views)
|
||||
}
|
||||
|
||||
export function useFilteredEntries({
|
||||
entries,
|
||||
selection,
|
||||
modifiedPathSet,
|
||||
modifiedSuffixes,
|
||||
modifiedFiles,
|
||||
subFilter,
|
||||
inboxPeriod,
|
||||
views,
|
||||
}: FilteredEntriesParams) {
|
||||
const isEntityView = selection.kind === 'entity'
|
||||
const isChangesView = selection.kind === 'filter' && selection.filter === 'changes'
|
||||
const isInboxView = selection.kind === 'filter' && selection.filter === 'inbox'
|
||||
return useMemo(() => {
|
||||
if (isEntityView) return []
|
||||
if (isChangesView) {
|
||||
if (modifiedFiles) return buildChangesEntries(entries, modifiedFiles)
|
||||
return entries.filter((e) => isModifiedEntry(e.path, modifiedPathSet, modifiedSuffixes))
|
||||
}
|
||||
if (isInboxView) return filterInboxEntries(entries, inboxPeriod ?? 'month')
|
||||
return filterEntries(entries, selection, subFilter, views)
|
||||
}, [entries, selection, isEntityView, isChangesView, isInboxView, modifiedPathSet, modifiedSuffixes, modifiedFiles, subFilter, inboxPeriod, views])
|
||||
return buildFilteredEntries({
|
||||
entries,
|
||||
selection,
|
||||
isEntityView,
|
||||
isChangesView,
|
||||
isInboxView,
|
||||
modifiedPathSet,
|
||||
modifiedSuffixes,
|
||||
modifiedFiles,
|
||||
subFilter,
|
||||
inboxPeriod,
|
||||
views,
|
||||
})
|
||||
}, [entries, inboxPeriod, isChangesView, isEntityView, isInboxView, modifiedFiles, modifiedPathSet, modifiedSuffixes, selection, subFilter, views])
|
||||
}
|
||||
|
||||
// --- useNoteListData ---
|
||||
@@ -57,7 +110,16 @@ export function useNoteListData({ entries, selection, query, listSort, listDirec
|
||||
const isEntityView = selection.kind === 'entity'
|
||||
const isArchivedView = (selection.kind === 'filter' && selection.filter === 'archived') || subFilter === 'archived'
|
||||
|
||||
const filteredEntries = useFilteredEntries(entries, selection, modifiedPathSet, modifiedSuffixes, modifiedFiles, subFilter, inboxPeriod, views)
|
||||
const filteredEntries = useFilteredEntries({
|
||||
entries,
|
||||
selection,
|
||||
modifiedPathSet,
|
||||
modifiedSuffixes,
|
||||
modifiedFiles,
|
||||
subFilter,
|
||||
inboxPeriod,
|
||||
views,
|
||||
})
|
||||
|
||||
const searched = useMemo(() => {
|
||||
const sorted = [...filteredEntries].sort(getSortComparator(listSort, listDirection))
|
||||
@@ -114,6 +176,11 @@ function persistSortToType(path: string, config: SortConfig, persistence: SortPe
|
||||
clearListSortFromLocalStorage()
|
||||
}
|
||||
|
||||
function resolveTypeSortPersistenceTarget(groupLabel: string, typeDocument: VaultEntry | null, persistence: SortPersistence | null) {
|
||||
if (groupLabel !== '__list__' || !typeDocument || !persistence) return null
|
||||
return { path: typeDocument.path, persistence }
|
||||
}
|
||||
|
||||
function migrateListSortToType(typeDoc: VaultEntry, sortPrefs: Record<string, SortConfig>, migrationDone: Set<string>, persistence: SortPersistence) {
|
||||
if (typeDoc.sort || migrationDone.has(typeDoc.path)) return
|
||||
const lsConfig = sortPrefs['__list__']
|
||||
@@ -163,14 +230,19 @@ export function useNoteListSort({ entries, selection, modifiedPathSet, modifiedS
|
||||
}, [typeDocument, sortPrefs, persistence])
|
||||
|
||||
const handleSortChange = useCallback((groupLabel: string, option: SortOption, direction: SortDirection) => {
|
||||
if (groupLabel === '__list__' && typeDocument && persistence) {
|
||||
persistSortToType(typeDocument.path, { option, direction }, persistence)
|
||||
} else {
|
||||
saveGroupSort(groupLabel, option, direction, setSortPrefs)
|
||||
}
|
||||
const typeSortTarget = resolveTypeSortPersistenceTarget(groupLabel, typeDocument, persistence)
|
||||
if (!typeSortTarget) return saveGroupSort(groupLabel, option, direction, setSortPrefs)
|
||||
persistSortToType(typeSortTarget.path, { option, direction }, typeSortTarget.persistence)
|
||||
}, [typeDocument, persistence])
|
||||
|
||||
const filteredEntries = useFilteredEntries(entries, selection, modifiedPathSet, modifiedSuffixes, undefined, subFilter, inboxPeriod)
|
||||
const filteredEntries = useFilteredEntries({
|
||||
entries,
|
||||
selection,
|
||||
modifiedPathSet,
|
||||
modifiedSuffixes,
|
||||
subFilter,
|
||||
inboxPeriod,
|
||||
})
|
||||
const customProperties = useMemo(() => extractSortableProperties(filteredEntries), [filteredEntries])
|
||||
const listSort = useMemo<SortOption>(() => deriveEffectiveSort(listConfig.option, customProperties), [listConfig.option, customProperties])
|
||||
const listDirection = listSort === listConfig.option ? listConfig.direction : 'desc'
|
||||
@@ -306,7 +378,7 @@ function collectTypeAvailableProperties(entries: VaultEntry[], typeName: string)
|
||||
return collectAvailableProperties(entries.filter((entry) => entry.isA === typeName))
|
||||
}
|
||||
|
||||
function deriveInboxDefaultDisplay(entries: VaultEntry[], typeEntryMap: Record<string, VaultEntry>): string[] {
|
||||
function deriveDefaultDisplay(entries: VaultEntry[], typeEntryMap: Record<string, VaultEntry>): string[] {
|
||||
const ordered: string[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
@@ -322,38 +394,42 @@ function deriveInboxDefaultDisplay(entries: VaultEntry[], typeEntryMap: Record<s
|
||||
}
|
||||
|
||||
export interface NoteListPropertyPicker {
|
||||
scope: 'inbox' | 'type'
|
||||
scope: NoteListPropertiesScope
|
||||
availableProperties: string[]
|
||||
currentDisplay: string[]
|
||||
onSave: (value: string[] | null) => void
|
||||
triggerTitle: string
|
||||
}
|
||||
|
||||
interface BuildInboxPropertyPickerParams {
|
||||
isInboxView: boolean
|
||||
onUpdateInboxNoteListProperties?: (value: string[] | null) => void
|
||||
inboxAvailableProperties: string[]
|
||||
hasCustomInboxProperties: boolean
|
||||
inboxNoteListProperties?: string[] | null
|
||||
inboxDefaultDisplay: string[]
|
||||
interface BuildFilterPropertyPickerParams {
|
||||
scope: Exclude<NoteListPropertiesScope, 'type'>
|
||||
isActive: boolean
|
||||
availableProperties: string[]
|
||||
hasCustomProperties: boolean
|
||||
noteListProperties?: string[] | null
|
||||
defaultDisplay: string[]
|
||||
onSave?: (value: string[] | null) => void
|
||||
triggerTitle: string
|
||||
}
|
||||
|
||||
function buildInboxPropertyPicker({
|
||||
isInboxView,
|
||||
onUpdateInboxNoteListProperties,
|
||||
inboxAvailableProperties,
|
||||
hasCustomInboxProperties,
|
||||
inboxNoteListProperties,
|
||||
inboxDefaultDisplay,
|
||||
}: BuildInboxPropertyPickerParams): NoteListPropertyPicker | null {
|
||||
if (!isInboxView || !onUpdateInboxNoteListProperties) return null
|
||||
function buildFilterPropertyPicker({
|
||||
scope,
|
||||
isActive,
|
||||
availableProperties,
|
||||
hasCustomProperties,
|
||||
noteListProperties,
|
||||
defaultDisplay,
|
||||
onSave,
|
||||
triggerTitle,
|
||||
}: BuildFilterPropertyPickerParams): NoteListPropertyPicker | null {
|
||||
if (!isActive || !onSave) return null
|
||||
|
||||
return {
|
||||
scope: 'inbox',
|
||||
availableProperties: inboxAvailableProperties,
|
||||
currentDisplay: hasCustomInboxProperties ? inboxNoteListProperties ?? [] : inboxDefaultDisplay,
|
||||
onSave: onUpdateInboxNoteListProperties,
|
||||
triggerTitle: 'Customize Inbox columns',
|
||||
scope,
|
||||
availableProperties,
|
||||
currentDisplay: hasCustomProperties ? noteListProperties ?? [] : defaultDisplay,
|
||||
onSave,
|
||||
triggerTitle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +463,8 @@ interface UseListPropertyPickerParams {
|
||||
inboxPeriod: InboxPeriod
|
||||
typeDocument: VaultEntry | null
|
||||
typeEntryMap: Record<string, VaultEntry>
|
||||
allNotesNoteListProperties?: string[] | null
|
||||
onUpdateAllNotesNoteListProperties?: (value: string[] | null) => void
|
||||
inboxNoteListProperties?: string[] | null
|
||||
onUpdateInboxNoteListProperties?: (value: string[] | null) => void
|
||||
onUpdateTypeSort?: (path: string, key: string, value: string | number | boolean | string[] | null) => void
|
||||
@@ -398,17 +476,37 @@ export function useListPropertyPicker({
|
||||
inboxPeriod,
|
||||
typeDocument,
|
||||
typeEntryMap,
|
||||
allNotesNoteListProperties,
|
||||
onUpdateAllNotesNoteListProperties,
|
||||
inboxNoteListProperties,
|
||||
onUpdateInboxNoteListProperties,
|
||||
onUpdateTypeSort,
|
||||
}: UseListPropertyPickerParams) {
|
||||
const isAllNotesView = selection.kind === 'filter' && selection.filter === 'all'
|
||||
const isInboxView = selection.kind === 'filter' && selection.filter === 'inbox'
|
||||
const isSectionGroup = selection.kind === 'sectionGroup'
|
||||
|
||||
const allNotesEntries = useMemo(
|
||||
() => isAllNotesView
|
||||
? [
|
||||
...filterEntries(entries, selection, 'open'),
|
||||
...filterEntries(entries, selection, 'archived'),
|
||||
]
|
||||
: [],
|
||||
[entries, isAllNotesView, selection],
|
||||
)
|
||||
const inboxEntries = useMemo(
|
||||
() => isInboxView ? filterInboxEntries(entries, inboxPeriod) : [],
|
||||
[entries, inboxPeriod, isInboxView],
|
||||
)
|
||||
const allNotesAvailableProperties = useMemo(
|
||||
() => collectAvailableProperties(allNotesEntries),
|
||||
[allNotesEntries],
|
||||
)
|
||||
const allNotesDefaultDisplay = useMemo(
|
||||
() => deriveDefaultDisplay(allNotesEntries, typeEntryMap),
|
||||
[allNotesEntries, typeEntryMap],
|
||||
)
|
||||
const typeAvailableProperties = useMemo(
|
||||
() => typeDocument ? collectTypeAvailableProperties(entries, typeDocument.title) : [],
|
||||
[entries, typeDocument],
|
||||
@@ -418,20 +516,34 @@ export function useListPropertyPicker({
|
||||
[inboxEntries],
|
||||
)
|
||||
const inboxDefaultDisplay = useMemo(
|
||||
() => deriveInboxDefaultDisplay(inboxEntries, typeEntryMap),
|
||||
() => deriveDefaultDisplay(inboxEntries, typeEntryMap),
|
||||
[inboxEntries, typeEntryMap],
|
||||
)
|
||||
const hasCustomAllNotesProperties = !!(allNotesNoteListProperties && allNotesNoteListProperties.length > 0)
|
||||
const hasCustomInboxProperties = !!(inboxNoteListProperties && inboxNoteListProperties.length > 0)
|
||||
const inboxDisplayOverride = isInboxView && hasCustomInboxProperties ? inboxNoteListProperties : null
|
||||
const displayPropsOverride = isAllNotesView && hasCustomAllNotesProperties
|
||||
? allNotesNoteListProperties
|
||||
: (isInboxView && hasCustomInboxProperties ? inboxNoteListProperties : null)
|
||||
|
||||
const propertyPicker = useMemo<NoteListPropertyPicker | null>(() => {
|
||||
return buildInboxPropertyPicker({
|
||||
isInboxView,
|
||||
onUpdateInboxNoteListProperties,
|
||||
inboxAvailableProperties,
|
||||
hasCustomInboxProperties,
|
||||
inboxNoteListProperties,
|
||||
inboxDefaultDisplay,
|
||||
return buildFilterPropertyPicker({
|
||||
scope: 'all',
|
||||
isActive: isAllNotesView,
|
||||
availableProperties: allNotesAvailableProperties,
|
||||
hasCustomProperties: hasCustomAllNotesProperties,
|
||||
noteListProperties: allNotesNoteListProperties,
|
||||
defaultDisplay: allNotesDefaultDisplay,
|
||||
onSave: onUpdateAllNotesNoteListProperties,
|
||||
triggerTitle: 'Customize All Notes columns',
|
||||
}) ?? buildFilterPropertyPicker({
|
||||
scope: 'inbox',
|
||||
isActive: isInboxView,
|
||||
availableProperties: inboxAvailableProperties,
|
||||
hasCustomProperties: hasCustomInboxProperties,
|
||||
noteListProperties: inboxNoteListProperties,
|
||||
defaultDisplay: inboxDefaultDisplay,
|
||||
onSave: onUpdateInboxNoteListProperties,
|
||||
triggerTitle: 'Customize Inbox columns',
|
||||
}) ?? buildTypePropertyPicker({
|
||||
isSectionGroup,
|
||||
typeDocument,
|
||||
@@ -439,6 +551,12 @@ export function useListPropertyPicker({
|
||||
typeAvailableProperties,
|
||||
})
|
||||
}, [
|
||||
allNotesAvailableProperties,
|
||||
allNotesDefaultDisplay,
|
||||
allNotesNoteListProperties,
|
||||
hasCustomAllNotesProperties,
|
||||
isAllNotesView,
|
||||
onUpdateAllNotesNoteListProperties,
|
||||
hasCustomInboxProperties,
|
||||
inboxAvailableProperties,
|
||||
inboxDefaultDisplay,
|
||||
@@ -451,7 +569,7 @@ export function useListPropertyPicker({
|
||||
typeDocument,
|
||||
])
|
||||
|
||||
return { inboxDisplayOverride, propertyPicker }
|
||||
return { displayPropsOverride, propertyPicker }
|
||||
}
|
||||
|
||||
// --- useNoteListInteractions ---
|
||||
@@ -473,6 +591,29 @@ interface UseNoteListInteractionsParams {
|
||||
onCreateNote: (type?: string) => void
|
||||
}
|
||||
|
||||
function resolveChangesContextMenuEntry(
|
||||
event: React.KeyboardEvent<HTMLDivElement>,
|
||||
isChangesView: boolean,
|
||||
onDiscardFile: ((relativePath: string) => Promise<void>) | undefined,
|
||||
highlightedPath: string | null,
|
||||
searched: VaultEntry[],
|
||||
) {
|
||||
if (!isChangesView || !onDiscardFile || !event.shiftKey || event.key !== 'F10' || !highlightedPath) return null
|
||||
return searched.find((candidate) => candidate.path === highlightedPath) ?? null
|
||||
}
|
||||
|
||||
function openHighlightedChangesContextMenu(
|
||||
entry: VaultEntry,
|
||||
openContextMenuForEntry: (entry: VaultEntry, point: { x: number; y: number }) => void,
|
||||
) {
|
||||
const row = document.querySelector<HTMLElement>(`[data-note-path="${entry.path}"]`)
|
||||
const rect = row?.getBoundingClientRect()
|
||||
openContextMenuForEntry(entry, {
|
||||
x: rect ? rect.left + 24 : 160,
|
||||
y: rect ? rect.bottom - 8 : 160,
|
||||
})
|
||||
}
|
||||
|
||||
export function useNoteListInteractions({
|
||||
searched,
|
||||
selectedNotePath,
|
||||
@@ -547,19 +688,17 @@ export function useNoteListInteractions({
|
||||
])
|
||||
|
||||
const handleListKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (isChangesView && onDiscardFile && event.shiftKey && event.key === 'F10' && noteListKeyboard.highlightedPath) {
|
||||
const entry = searched.find((candidate) => candidate.path === noteListKeyboard.highlightedPath)
|
||||
if (!entry) return
|
||||
|
||||
const entry = resolveChangesContextMenuEntry(
|
||||
event,
|
||||
isChangesView,
|
||||
onDiscardFile,
|
||||
noteListKeyboard.highlightedPath,
|
||||
searched,
|
||||
)
|
||||
if (entry) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const row = document.querySelector<HTMLElement>(`[data-note-path="${entry.path}"]`)
|
||||
const rect = row?.getBoundingClientRect()
|
||||
openContextMenuForEntry(entry, {
|
||||
x: rect ? rect.left + 24 : 160,
|
||||
y: rect ? rect.bottom - 8 : 160,
|
||||
})
|
||||
openHighlightedChangesContextMenu(entry, openContextMenuForEntry)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type NoteListPropertiesScope = 'type' | 'inbox'
|
||||
export type NoteListPropertiesScope = 'type' | 'inbox' | 'all'
|
||||
|
||||
export interface OpenListPropertiesEventDetail {
|
||||
scope: NoteListPropertiesScope
|
||||
|
||||
@@ -69,6 +69,264 @@ function useBulkActions(
|
||||
}
|
||||
}
|
||||
|
||||
function useFilterCounts(entries: VaultEntry[], selection: SidebarSelection) {
|
||||
return useMemo(() => {
|
||||
if (selection.kind === 'sectionGroup') return countByFilter(entries, selection.type)
|
||||
if (selection.kind === 'folder') return countAllByFilter(entries)
|
||||
if (selection.kind === 'filter' && selection.filter === 'all') return countAllByFilter(entries)
|
||||
return { open: 0, archived: 0 }
|
||||
}, [entries, selection])
|
||||
}
|
||||
|
||||
interface UseNoteListContentParams {
|
||||
entries: VaultEntry[]
|
||||
selection: SidebarSelection
|
||||
noteListFilter: NoteListFilter
|
||||
inboxPeriod: InboxPeriod
|
||||
modifiedFiles?: ModifiedFile[]
|
||||
modifiedSuffixes: string[]
|
||||
modifiedPathSet: Set<string>
|
||||
isInboxView: boolean
|
||||
allNotesNoteListProperties?: string[] | null
|
||||
onUpdateAllNotesNoteListProperties?: (value: string[] | null) => void
|
||||
inboxNoteListProperties?: string[] | null
|
||||
onUpdateInboxNoteListProperties?: (value: string[] | null) => void
|
||||
onUpdateTypeSort?: (path: string, key: string, value: string | number | boolean | string[] | null) => void
|
||||
updateEntry?: (path: string, patch: Partial<VaultEntry>) => void
|
||||
views?: ViewFile[]
|
||||
visibleNotesRef?: React.MutableRefObject<VaultEntry[]>
|
||||
}
|
||||
|
||||
function useNoteListContent({
|
||||
entries,
|
||||
selection,
|
||||
noteListFilter,
|
||||
inboxPeriod,
|
||||
modifiedFiles,
|
||||
modifiedSuffixes,
|
||||
modifiedPathSet,
|
||||
isInboxView,
|
||||
allNotesNoteListProperties,
|
||||
onUpdateAllNotesNoteListProperties,
|
||||
inboxNoteListProperties,
|
||||
onUpdateInboxNoteListProperties,
|
||||
onUpdateTypeSort,
|
||||
updateEntry,
|
||||
views,
|
||||
visibleNotesRef,
|
||||
}: UseNoteListContentParams) {
|
||||
const subFilter = (selection.kind === 'sectionGroup' || selection.kind === 'folder')
|
||||
? noteListFilter
|
||||
: undefined
|
||||
const effectiveInboxPeriod = isInboxView ? inboxPeriod : undefined
|
||||
const { listSort, listDirection, customProperties, handleSortChange, sortPrefs, typeDocument } = useNoteListSort({
|
||||
entries,
|
||||
selection,
|
||||
modifiedPathSet,
|
||||
modifiedSuffixes,
|
||||
subFilter,
|
||||
inboxPeriod: effectiveInboxPeriod,
|
||||
onUpdateTypeSort,
|
||||
updateEntry,
|
||||
})
|
||||
const { search, setSearch, query, searchVisible, toggleSearch } = useNoteListSearch()
|
||||
const typeEntryMap = useTypeEntryMap(entries)
|
||||
const { displayPropsOverride, propertyPicker } = useListPropertyPicker({
|
||||
entries,
|
||||
selection,
|
||||
inboxPeriod,
|
||||
typeDocument,
|
||||
typeEntryMap,
|
||||
allNotesNoteListProperties,
|
||||
onUpdateAllNotesNoteListProperties,
|
||||
inboxNoteListProperties,
|
||||
onUpdateInboxNoteListProperties,
|
||||
onUpdateTypeSort,
|
||||
})
|
||||
const { isEntityView, isArchivedView, searched, searchedGroups } = useNoteListData({
|
||||
entries,
|
||||
selection,
|
||||
query,
|
||||
listSort,
|
||||
listDirection,
|
||||
modifiedPathSet,
|
||||
modifiedSuffixes,
|
||||
modifiedFiles,
|
||||
subFilter,
|
||||
inboxPeriod: effectiveInboxPeriod,
|
||||
views,
|
||||
})
|
||||
useVisibleNotesSync({ visibleNotesRef, isEntityView, searched, searchedGroups })
|
||||
|
||||
return {
|
||||
customProperties,
|
||||
displayPropsOverride,
|
||||
handleSortChange,
|
||||
isArchivedView,
|
||||
isEntityView,
|
||||
listDirection,
|
||||
listSort,
|
||||
propertyPicker,
|
||||
query,
|
||||
search,
|
||||
searchVisible,
|
||||
searched,
|
||||
searchedGroups,
|
||||
setSearch,
|
||||
sortPrefs,
|
||||
toggleSearch,
|
||||
typeDocument,
|
||||
typeEntryMap,
|
||||
}
|
||||
}
|
||||
|
||||
interface UseNoteListInteractionStateParams {
|
||||
searched: VaultEntry[]
|
||||
selectedNotePath: string | null
|
||||
selection: SidebarSelection
|
||||
noteListFilter: NoteListFilter
|
||||
isArchivedView: boolean
|
||||
isChangesView: boolean
|
||||
isEntityView: boolean
|
||||
modifiedFiles?: ModifiedFile[]
|
||||
onReplaceActiveTab: (entry: VaultEntry) => void
|
||||
onSelectNote: (entry: VaultEntry) => void
|
||||
onOpenDeletedNote?: (entry: DeletedNoteEntry) => void
|
||||
onOpenInNewWindow?: (entry: VaultEntry) => void
|
||||
onAutoTriggerDiff?: () => void
|
||||
onDiscardFile?: (relativePath: string) => Promise<void>
|
||||
onCreateNote: (type?: string) => void
|
||||
onBulkArchive?: (paths: string[]) => void
|
||||
onBulkDeletePermanently?: (paths: string[]) => void
|
||||
}
|
||||
|
||||
function useNoteListInteractionState({
|
||||
searched,
|
||||
selectedNotePath,
|
||||
selection,
|
||||
noteListFilter,
|
||||
isArchivedView,
|
||||
isChangesView,
|
||||
isEntityView,
|
||||
modifiedFiles,
|
||||
onReplaceActiveTab,
|
||||
onSelectNote,
|
||||
onOpenDeletedNote,
|
||||
onOpenInNewWindow,
|
||||
onAutoTriggerDiff,
|
||||
onDiscardFile,
|
||||
onCreateNote,
|
||||
onBulkArchive,
|
||||
onBulkDeletePermanently,
|
||||
}: UseNoteListInteractionStateParams) {
|
||||
const changesContextMenu = useChangesContextMenu({ isChangesView, onDiscardFile, modifiedFiles })
|
||||
const {
|
||||
collapsedGroups,
|
||||
handleClickNote,
|
||||
handleCreateNote,
|
||||
handleListKeyDown,
|
||||
multiSelect,
|
||||
noteListKeyboard,
|
||||
toggleGroup,
|
||||
} = useNoteListInteractions({
|
||||
searched,
|
||||
selectedNotePath,
|
||||
selection,
|
||||
noteListFilter,
|
||||
isEntityView,
|
||||
isChangesView,
|
||||
onReplaceActiveTab,
|
||||
onSelectNote,
|
||||
onOpenDeletedNote,
|
||||
onOpenInNewWindow,
|
||||
onAutoTriggerDiff,
|
||||
onDiscardFile,
|
||||
openContextMenuForEntry: changesContextMenu.openContextMenuForEntry,
|
||||
onCreateNote,
|
||||
})
|
||||
const getChangeStatus = useChangeStatusResolver(isChangesView, modifiedFiles)
|
||||
const {
|
||||
handleBulkArchive,
|
||||
handleBulkDeletePermanently,
|
||||
handleBulkUnarchive,
|
||||
} = useBulkActions(multiSelect, onBulkArchive, onBulkDeletePermanently, isArchivedView)
|
||||
|
||||
return {
|
||||
changesContextMenu,
|
||||
collapsedGroups,
|
||||
getChangeStatus,
|
||||
handleBulkArchive,
|
||||
handleBulkDeletePermanently,
|
||||
handleBulkUnarchive,
|
||||
handleClickNote,
|
||||
handleCreateNote,
|
||||
handleListKeyDown,
|
||||
multiSelect,
|
||||
noteListKeyboard,
|
||||
toggleGroup,
|
||||
}
|
||||
}
|
||||
|
||||
interface UseRenderItemParams {
|
||||
entries: VaultEntry[]
|
||||
selectedNotePath: string | null
|
||||
typeEntryMap: Record<string, VaultEntry>
|
||||
displayPropsOverride?: string[] | null
|
||||
isChangesView: boolean
|
||||
onDiscardFile?: (relativePath: string) => Promise<void>
|
||||
resolvedGetNoteStatus: (path: string) => NoteStatus
|
||||
getChangeStatus: (path: string) => string | undefined
|
||||
handleClickNote: (entry: VaultEntry, event?: React.MouseEvent) => void
|
||||
noteContextMenu?: ((entry: VaultEntry, event: React.MouseEvent) => void) | undefined
|
||||
multiSelect: MultiSelectState
|
||||
noteListKeyboard: { highlightedPath: string | null }
|
||||
}
|
||||
|
||||
function useRenderItem({
|
||||
entries,
|
||||
selectedNotePath,
|
||||
typeEntryMap,
|
||||
displayPropsOverride,
|
||||
isChangesView,
|
||||
onDiscardFile,
|
||||
resolvedGetNoteStatus,
|
||||
getChangeStatus,
|
||||
handleClickNote,
|
||||
noteContextMenu,
|
||||
multiSelect,
|
||||
noteListKeyboard,
|
||||
}: UseRenderItemParams) {
|
||||
const contextMenuHandler = isChangesView && onDiscardFile ? noteContextMenu : undefined
|
||||
|
||||
return useCallback((entry: VaultEntry) => (
|
||||
<NoteItem
|
||||
key={entry.path}
|
||||
entry={entry}
|
||||
isSelected={selectedNotePath === entry.path}
|
||||
isMultiSelected={multiSelect.selectedPaths.has(entry.path)}
|
||||
isHighlighted={entry.path === noteListKeyboard.highlightedPath}
|
||||
noteStatus={resolvedGetNoteStatus(entry.path)}
|
||||
changeStatus={getChangeStatus(entry.path)}
|
||||
typeEntryMap={typeEntryMap}
|
||||
allEntries={entries}
|
||||
displayPropsOverride={displayPropsOverride}
|
||||
onClickNote={handleClickNote}
|
||||
onContextMenu={contextMenuHandler}
|
||||
/>
|
||||
), [
|
||||
contextMenuHandler,
|
||||
displayPropsOverride,
|
||||
entries,
|
||||
getChangeStatus,
|
||||
handleClickNote,
|
||||
multiSelect.selectedPaths,
|
||||
noteListKeyboard.highlightedPath,
|
||||
resolvedGetNoteStatus,
|
||||
selectedNotePath,
|
||||
typeEntryMap,
|
||||
])
|
||||
}
|
||||
|
||||
export interface NoteListProps {
|
||||
entries: VaultEntry[]
|
||||
selection: SidebarSelection
|
||||
@@ -92,6 +350,8 @@ export interface NoteListProps {
|
||||
onDiscardFile?: (relativePath: string) => Promise<void>
|
||||
onAutoTriggerDiff?: () => void
|
||||
onOpenDeletedNote?: (entry: DeletedNoteEntry) => void
|
||||
allNotesNoteListProperties?: string[] | null
|
||||
onUpdateAllNotesNoteListProperties?: (value: string[] | null) => void
|
||||
inboxNoteListProperties?: string[] | null
|
||||
onUpdateInboxNoteListProperties?: (value: string[] | null) => void
|
||||
views?: ViewFile[]
|
||||
@@ -120,123 +380,99 @@ export function useNoteListModel({
|
||||
onDiscardFile,
|
||||
onAutoTriggerDiff,
|
||||
onOpenDeletedNote,
|
||||
allNotesNoteListProperties,
|
||||
onUpdateAllNotesNoteListProperties,
|
||||
inboxNoteListProperties,
|
||||
onUpdateInboxNoteListProperties,
|
||||
views,
|
||||
visibleNotesRef,
|
||||
}: NoteListProps) {
|
||||
const { modifiedPathSet, modifiedSuffixes, resolvedGetNoteStatus } = useModifiedFilesState(modifiedFiles, getNoteStatus)
|
||||
const { isSectionGroup, isFolderView, isInboxView, isAllNotesView, isChangesView, showFilterPills } = useViewFlags(selection)
|
||||
const subFilter = showFilterPills ? noteListFilter : undefined
|
||||
|
||||
const filterCounts = useMemo(
|
||||
() => isSectionGroup && selection.kind === 'sectionGroup'
|
||||
? countByFilter(entries, selection.type)
|
||||
: (isAllNotesView || isFolderView)
|
||||
? countAllByFilter(entries)
|
||||
: { open: 0, archived: 0 },
|
||||
[entries, isSectionGroup, isAllNotesView, isFolderView, selection],
|
||||
)
|
||||
|
||||
const { listSort, listDirection, customProperties, handleSortChange, sortPrefs, typeDocument } = useNoteListSort({
|
||||
entries,
|
||||
selection,
|
||||
modifiedPathSet,
|
||||
modifiedSuffixes,
|
||||
subFilter,
|
||||
inboxPeriod: isInboxView ? inboxPeriod : undefined,
|
||||
onUpdateTypeSort,
|
||||
updateEntry,
|
||||
})
|
||||
const { search, setSearch, query, searchVisible, toggleSearch } = useNoteListSearch()
|
||||
const typeEntryMap = useTypeEntryMap(entries)
|
||||
const { inboxDisplayOverride, propertyPicker } = useListPropertyPicker({
|
||||
entries,
|
||||
selection,
|
||||
inboxPeriod,
|
||||
const { isInboxView, isChangesView, showFilterPills } = useViewFlags(selection)
|
||||
const filterCounts = useFilterCounts(entries, selection)
|
||||
const {
|
||||
customProperties,
|
||||
displayPropsOverride,
|
||||
handleSortChange,
|
||||
isArchivedView,
|
||||
isEntityView,
|
||||
listDirection,
|
||||
listSort,
|
||||
propertyPicker,
|
||||
query,
|
||||
search,
|
||||
searchVisible,
|
||||
searched,
|
||||
searchedGroups,
|
||||
setSearch,
|
||||
sortPrefs,
|
||||
toggleSearch,
|
||||
typeDocument,
|
||||
typeEntryMap,
|
||||
} = useNoteListContent({
|
||||
entries,
|
||||
selection,
|
||||
noteListFilter,
|
||||
inboxPeriod,
|
||||
modifiedFiles,
|
||||
modifiedSuffixes,
|
||||
modifiedPathSet,
|
||||
isInboxView,
|
||||
allNotesNoteListProperties,
|
||||
onUpdateAllNotesNoteListProperties,
|
||||
inboxNoteListProperties,
|
||||
onUpdateInboxNoteListProperties,
|
||||
onUpdateTypeSort,
|
||||
})
|
||||
const { isEntityView, isArchivedView, searched, searchedGroups } = useNoteListData({
|
||||
entries,
|
||||
selection,
|
||||
query,
|
||||
listSort,
|
||||
listDirection,
|
||||
modifiedPathSet,
|
||||
modifiedSuffixes,
|
||||
modifiedFiles,
|
||||
subFilter,
|
||||
inboxPeriod: isInboxView ? inboxPeriod : undefined,
|
||||
updateEntry,
|
||||
views,
|
||||
visibleNotesRef,
|
||||
})
|
||||
useVisibleNotesSync({ visibleNotesRef, isEntityView, searched, searchedGroups })
|
||||
|
||||
const changesContextMenu = useChangesContextMenu({ isChangesView, onDiscardFile, modifiedFiles })
|
||||
const {
|
||||
changesContextMenu,
|
||||
collapsedGroups,
|
||||
getChangeStatus,
|
||||
handleBulkArchive,
|
||||
handleBulkDeletePermanently,
|
||||
handleBulkUnarchive,
|
||||
handleClickNote,
|
||||
handleCreateNote,
|
||||
handleListKeyDown,
|
||||
multiSelect,
|
||||
noteListKeyboard,
|
||||
toggleGroup,
|
||||
} = useNoteListInteractions({
|
||||
} = useNoteListInteractionState({
|
||||
searched,
|
||||
selectedNotePath: selectedNote?.path ?? null,
|
||||
selection,
|
||||
noteListFilter,
|
||||
isArchivedView,
|
||||
isEntityView,
|
||||
isChangesView,
|
||||
modifiedFiles,
|
||||
onReplaceActiveTab,
|
||||
onSelectNote,
|
||||
onOpenDeletedNote,
|
||||
onOpenInNewWindow,
|
||||
onAutoTriggerDiff,
|
||||
onDiscardFile,
|
||||
openContextMenuForEntry: changesContextMenu.openContextMenuForEntry,
|
||||
onCreateNote,
|
||||
onBulkArchive,
|
||||
onBulkDeletePermanently,
|
||||
})
|
||||
const getChangeStatus = useChangeStatusResolver(isChangesView, modifiedFiles)
|
||||
|
||||
const {
|
||||
handleBulkArchive,
|
||||
handleBulkDeletePermanently,
|
||||
handleBulkUnarchive,
|
||||
} = useBulkActions(multiSelect, onBulkArchive, onBulkDeletePermanently, isArchivedView)
|
||||
|
||||
const renderItem = useCallback((entry: VaultEntry) => (
|
||||
<NoteItem
|
||||
key={entry.path}
|
||||
entry={entry}
|
||||
isSelected={selectedNote?.path === entry.path}
|
||||
isMultiSelected={multiSelect.selectedPaths.has(entry.path)}
|
||||
isHighlighted={entry.path === noteListKeyboard.highlightedPath}
|
||||
noteStatus={resolvedGetNoteStatus(entry.path)}
|
||||
changeStatus={getChangeStatus(entry.path)}
|
||||
typeEntryMap={typeEntryMap}
|
||||
allEntries={entries}
|
||||
displayPropsOverride={inboxDisplayOverride}
|
||||
onClickNote={handleClickNote}
|
||||
onContextMenu={isChangesView && onDiscardFile ? changesContextMenu.handleNoteContextMenu : undefined}
|
||||
/>
|
||||
), [
|
||||
const renderItem = useRenderItem({
|
||||
entries,
|
||||
selectedNote?.path,
|
||||
multiSelect.selectedPaths,
|
||||
noteListKeyboard.highlightedPath,
|
||||
resolvedGetNoteStatus,
|
||||
getChangeStatus,
|
||||
selectedNotePath: selectedNote?.path ?? null,
|
||||
typeEntryMap,
|
||||
inboxDisplayOverride,
|
||||
handleClickNote,
|
||||
displayPropsOverride,
|
||||
isChangesView,
|
||||
onDiscardFile,
|
||||
changesContextMenu.handleNoteContextMenu,
|
||||
])
|
||||
resolvedGetNoteStatus,
|
||||
getChangeStatus,
|
||||
handleClickNote,
|
||||
noteContextMenu: changesContextMenu.handleNoteContextMenu,
|
||||
multiSelect,
|
||||
noteListKeyboard,
|
||||
})
|
||||
|
||||
return {
|
||||
title: resolveHeaderTitle(selection, typeDocument, views),
|
||||
|
||||
@@ -13,15 +13,17 @@ interface ViewCommandsConfig {
|
||||
onZoomIn: () => void
|
||||
onZoomOut: () => void
|
||||
onZoomReset: () => void
|
||||
onCustomizeInboxColumns?: () => void
|
||||
canCustomizeInboxColumns?: boolean
|
||||
onCustomizeNoteListColumns?: () => void
|
||||
canCustomizeNoteListColumns?: boolean
|
||||
noteListColumnsLabel: string
|
||||
}
|
||||
|
||||
export function buildViewCommands(config: ViewCommandsConfig): CommandAction[] {
|
||||
const {
|
||||
hasActiveNote, activeNoteModified,
|
||||
onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, onToggleAIChat,
|
||||
zoomLevel, onZoomIn, onZoomOut, onZoomReset, onCustomizeInboxColumns, canCustomizeInboxColumns,
|
||||
zoomLevel, onZoomIn, onZoomOut, onZoomReset,
|
||||
onCustomizeNoteListColumns, canCustomizeNoteListColumns, noteListColumnsLabel,
|
||||
} = config
|
||||
|
||||
return [
|
||||
@@ -33,7 +35,7 @@ export function buildViewCommands(config: ViewCommandsConfig): CommandAction[] {
|
||||
{ id: 'toggle-raw-editor', label: 'Toggle Raw Editor', group: 'View', keywords: ['raw', 'source', 'markdown', 'frontmatter', 'code', 'textarea'], enabled: hasActiveNote, execute: () => onToggleRawEditor?.() },
|
||||
{ id: 'toggle-ai-panel', label: 'Toggle AI Panel', group: 'View', shortcut: '⇧⌘L', keywords: ['ai', 'agent', 'chat', 'assistant', 'contextual'], enabled: true, execute: () => onToggleAIChat?.() },
|
||||
{ id: 'toggle-backlinks', label: 'Toggle Backlinks', group: 'View', keywords: ['backlinks', 'references', 'links', 'mentions', 'incoming'], enabled: hasActiveNote, execute: onToggleInspector },
|
||||
{ id: 'customize-inbox-columns', label: 'Customize Inbox columns', group: 'View', keywords: ['inbox', 'columns', 'chips', 'properties', 'note list'], enabled: !!(canCustomizeInboxColumns && onCustomizeInboxColumns), execute: () => onCustomizeInboxColumns?.() },
|
||||
{ id: 'customize-note-list-columns', label: noteListColumnsLabel, group: 'View', keywords: ['all notes', 'inbox', 'columns', 'chips', 'properties', 'note list'], enabled: !!(canCustomizeNoteListColumns && onCustomizeNoteListColumns), execute: () => onCustomizeNoteListColumns?.() },
|
||||
{ id: 'zoom-in', label: `Zoom In (${zoomLevel}%)`, group: 'View', shortcut: '⌘=', keywords: ['zoom', 'bigger', 'larger', 'scale'], enabled: zoomLevel < 150, execute: onZoomIn },
|
||||
{ id: 'zoom-out', label: `Zoom Out (${zoomLevel}%)`, group: 'View', shortcut: '⌘-', keywords: ['zoom', 'smaller', 'scale'], enabled: zoomLevel > 80, execute: onZoomOut },
|
||||
{ id: 'zoom-reset', label: 'Reset Zoom', group: 'View', shortcut: '⌘0', keywords: ['zoom', 'actual', 'default', '100'], enabled: zoomLevel !== 100, execute: onZoomReset },
|
||||
|
||||
@@ -80,8 +80,8 @@ interface AppCommandsConfig {
|
||||
onOpenInNewWindow?: () => void
|
||||
onToggleFavorite?: (path: string) => void
|
||||
onToggleOrganized?: (path: string) => void
|
||||
onCustomizeInboxColumns?: () => void
|
||||
canCustomizeInboxColumns?: boolean
|
||||
onCustomizeNoteListColumns?: () => void
|
||||
canCustomizeNoteListColumns?: boolean
|
||||
onRestoreDeletedNote?: () => void
|
||||
canRestoreDeletedNote?: boolean
|
||||
}
|
||||
@@ -222,8 +222,8 @@ function createCommandRegistryConfig(config: AppCommandsConfig): Parameters<type
|
||||
onOpenInNewWindow: config.onOpenInNewWindow,
|
||||
onToggleFavorite: config.onToggleFavorite,
|
||||
onToggleOrganized: config.onToggleOrganized,
|
||||
onCustomizeInboxColumns: config.onCustomizeInboxColumns,
|
||||
canCustomizeInboxColumns: config.canCustomizeInboxColumns,
|
||||
onCustomizeNoteListColumns: config.onCustomizeNoteListColumns,
|
||||
canCustomizeNoteListColumns: config.canCustomizeNoteListColumns,
|
||||
onRestoreDeletedNote: config.onRestoreDeletedNote,
|
||||
canRestoreDeletedNote: config.canRestoreDeletedNote,
|
||||
}
|
||||
|
||||
@@ -167,29 +167,43 @@ describe('useCommandRegistry', () => {
|
||||
})
|
||||
|
||||
it('includes Customize Inbox columns when the Inbox action is available', () => {
|
||||
const onCustomizeInboxColumns = vi.fn()
|
||||
const onCustomizeNoteListColumns = vi.fn()
|
||||
const config = makeConfig({
|
||||
selection: { kind: 'filter', filter: 'inbox' },
|
||||
onCustomizeInboxColumns,
|
||||
canCustomizeInboxColumns: true,
|
||||
onCustomizeNoteListColumns,
|
||||
canCustomizeNoteListColumns: true,
|
||||
})
|
||||
const { result } = renderHook(() => useCommandRegistry(config))
|
||||
const cmd = findCommand(result.current, 'customize-inbox-columns')
|
||||
const cmd = findCommand(result.current, 'customize-note-list-columns')
|
||||
expect(cmd).toBeDefined()
|
||||
expect(cmd!.enabled).toBe(true)
|
||||
expect(cmd!.label).toBe('Customize Inbox columns')
|
||||
|
||||
cmd!.execute()
|
||||
expect(onCustomizeInboxColumns).toHaveBeenCalled()
|
||||
expect(onCustomizeNoteListColumns).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('disables Customize Inbox columns outside the Inbox view', () => {
|
||||
it('includes Customize All Notes columns in the all-notes view', () => {
|
||||
const config = makeConfig({
|
||||
selection: { kind: 'filter', filter: 'all' },
|
||||
onCustomizeInboxColumns: vi.fn(),
|
||||
canCustomizeInboxColumns: false,
|
||||
onCustomizeNoteListColumns: vi.fn(),
|
||||
canCustomizeNoteListColumns: true,
|
||||
})
|
||||
const { result } = renderHook(() => useCommandRegistry(config))
|
||||
const cmd = findCommand(result.current, 'customize-inbox-columns')
|
||||
const cmd = findCommand(result.current, 'customize-note-list-columns')
|
||||
expect(cmd).toBeDefined()
|
||||
expect(cmd!.enabled).toBe(true)
|
||||
expect(cmd!.label).toBe('Customize All Notes columns')
|
||||
})
|
||||
|
||||
it('disables note-list column customization outside supported views', () => {
|
||||
const config = makeConfig({
|
||||
selection: { kind: 'sectionGroup', type: 'Book' },
|
||||
onCustomizeNoteListColumns: vi.fn(),
|
||||
canCustomizeNoteListColumns: false,
|
||||
})
|
||||
const { result } = renderHook(() => useCommandRegistry(config))
|
||||
const cmd = findCommand(result.current, 'customize-note-list-columns')
|
||||
expect(cmd!.enabled).toBe(false)
|
||||
})
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ interface CommandRegistryConfig {
|
||||
onOpenInNewWindow?: () => void
|
||||
onToggleFavorite?: (path: string) => void
|
||||
onToggleOrganized?: (path: string) => void
|
||||
onCustomizeInboxColumns?: () => void
|
||||
canCustomizeInboxColumns?: boolean
|
||||
onCustomizeNoteListColumns?: () => void
|
||||
canCustomizeNoteListColumns?: boolean
|
||||
onRestoreDeletedNote?: () => void
|
||||
canRestoreDeletedNote?: boolean
|
||||
onQuickOpen: () => void
|
||||
@@ -105,7 +105,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
|
||||
onReloadVault, onRepairVault,
|
||||
onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon,
|
||||
onOpenInNewWindow, onToggleFavorite, onToggleOrganized,
|
||||
onCustomizeInboxColumns, canCustomizeInboxColumns,
|
||||
onCustomizeNoteListColumns, canCustomizeNoteListColumns,
|
||||
onRestoreDeletedNote, canRestoreDeletedNote,
|
||||
selection, noteListFilter, onSetNoteListFilter,
|
||||
} = config
|
||||
@@ -119,6 +119,9 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
|
||||
const isArchived = activeEntry?.archived ?? false
|
||||
const isFavorite = activeEntry?.favorite ?? false
|
||||
const isSectionGroup = selection?.kind === 'sectionGroup'
|
||||
const noteListColumnsLabel = selection?.kind === 'filter' && selection.filter === 'all'
|
||||
? 'Customize All Notes columns'
|
||||
: 'Customize Inbox columns'
|
||||
|
||||
const vaultTypes = useMemo(() => extractVaultTypes(entries), [entries])
|
||||
|
||||
@@ -136,7 +139,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
|
||||
...buildViewCommands({
|
||||
hasActiveNote, activeNoteModified, onSetViewMode, onToggleInspector,
|
||||
onToggleDiff, onToggleRawEditor, onToggleAIChat, zoomLevel, onZoomIn, onZoomOut, onZoomReset,
|
||||
onCustomizeInboxColumns, canCustomizeInboxColumns,
|
||||
onCustomizeNoteListColumns, canCustomizeNoteListColumns, noteListColumnsLabel,
|
||||
}),
|
||||
...buildSettingsCommands({
|
||||
mcpStatus, vaultCount, isGettingStartedHidden,
|
||||
@@ -173,7 +176,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
|
||||
onSetNoteIcon, onRemoveNoteIcon, activeNoteHasIcon,
|
||||
isSectionGroup, noteListFilter, onSetNoteListFilter,
|
||||
onOpenInNewWindow, onToggleFavorite, isFavorite,
|
||||
onToggleOrganized, onCustomizeInboxColumns, canCustomizeInboxColumns,
|
||||
onToggleOrganized, onCustomizeNoteListColumns, canCustomizeNoteListColumns, noteListColumnsLabel,
|
||||
onRestoreDeletedNote, canRestoreDeletedNote, activeEntry,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -132,6 +132,11 @@ export interface InboxConfig {
|
||||
explicitOrganization?: boolean | null
|
||||
}
|
||||
|
||||
/** Vault-scoped UI configuration stored locally per vault path. */
|
||||
export interface AllNotesConfig {
|
||||
noteListProperties: string[] | null
|
||||
}
|
||||
|
||||
/** Vault-scoped UI configuration stored locally per vault path. */
|
||||
export interface VaultConfig {
|
||||
zoom: number | null
|
||||
@@ -141,6 +146,7 @@ export interface VaultConfig {
|
||||
status_colors: Record<string, string> | null
|
||||
property_display_modes: Record<string, string> | null
|
||||
inbox?: InboxConfig | null
|
||||
allNotes?: AllNotesConfig | null
|
||||
}
|
||||
|
||||
export interface PulseFile {
|
||||
|
||||
Reference in New Issue
Block a user