feat: customize all-notes note list columns
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user