feat: customize all-notes note list columns

This commit is contained in:
lucaronin
2026-04-16 02:18:43 +02:00
parent f3ee593dee
commit bbf98d6112
11 changed files with 629 additions and 167 deletions

View File

@@ -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()

View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
export type NoteListPropertiesScope = 'type' | 'inbox'
export type NoteListPropertiesScope = 'type' | 'inbox' | 'all'
export interface OpenListPropertiesEventDetail {
scope: NoteListPropertiesScope

View File

@@ -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),