fix: exclude attachments from all notes totals
This commit is contained in:
@@ -874,6 +874,88 @@ describe('Sidebar', () => {
|
||||
expect(topNav.children[0].textContent).toContain('All Notes')
|
||||
})
|
||||
|
||||
it('excludes attachments-folder markdown from top-nav note totals', () => {
|
||||
const entries: VaultEntry[] = [
|
||||
{
|
||||
path: '/vault/note/real-note.md',
|
||||
filename: 'real-note.md',
|
||||
title: 'Real Note',
|
||||
isA: 'Note',
|
||||
aliases: [], belongsTo: [], relatedTo: [],
|
||||
status: null, owner: null, cadence: null,
|
||||
archived: false,
|
||||
modifiedAt: 1700000000, createdAt: null,
|
||||
fileSize: 310, snippet: '', wordCount: 120,
|
||||
relationships: {}, icon: null, color: null, order: null,
|
||||
sidebarLabel: null, template: null, sort: null, view: null,
|
||||
outgoingLinks: [], properties: {},
|
||||
},
|
||||
{
|
||||
path: '/vault/attachments/reference.md',
|
||||
filename: 'reference.md',
|
||||
title: 'Attachment Markdown',
|
||||
isA: 'Note',
|
||||
aliases: [], belongsTo: [], relatedTo: [],
|
||||
status: null, owner: null, cadence: null,
|
||||
archived: false,
|
||||
modifiedAt: 1700000000, createdAt: null,
|
||||
fileSize: 220, snippet: '', wordCount: 50,
|
||||
relationships: {}, icon: null, color: null, order: null,
|
||||
sidebarLabel: null, template: null, sort: null, view: null,
|
||||
outgoingLinks: [], properties: {},
|
||||
},
|
||||
{
|
||||
path: '/vault/attachments/nested/archive.md',
|
||||
filename: 'archive.md',
|
||||
title: 'Attachment Archive',
|
||||
isA: 'Note',
|
||||
aliases: [], belongsTo: [], relatedTo: [],
|
||||
status: null, owner: null, cadence: null,
|
||||
archived: true,
|
||||
modifiedAt: 1700000000, createdAt: null,
|
||||
fileSize: 180, snippet: '', wordCount: 25,
|
||||
relationships: {}, icon: null, color: null, order: null,
|
||||
sidebarLabel: null, template: null, sort: null, view: null,
|
||||
outgoingLinks: [], properties: {},
|
||||
},
|
||||
{
|
||||
path: '/vault/archive/real-archive.md',
|
||||
filename: 'real-archive.md',
|
||||
title: 'Real Archive',
|
||||
isA: 'Note',
|
||||
aliases: [], belongsTo: [], relatedTo: [],
|
||||
status: null, owner: null, cadence: null,
|
||||
archived: true,
|
||||
modifiedAt: 1700000000, createdAt: null,
|
||||
fileSize: 280, snippet: '', wordCount: 90,
|
||||
relationships: {}, icon: null, color: null, order: null,
|
||||
sidebarLabel: null, template: null, sort: null, view: null,
|
||||
outgoingLinks: [], properties: {},
|
||||
},
|
||||
{
|
||||
path: '/vault/attachments/image.png',
|
||||
filename: 'image.png',
|
||||
title: 'image.png',
|
||||
isA: null,
|
||||
aliases: [], belongsTo: [], relatedTo: [],
|
||||
status: null, owner: null, cadence: null,
|
||||
archived: false,
|
||||
modifiedAt: 1700000000, createdAt: null,
|
||||
fileSize: 1024, snippet: '', wordCount: 0,
|
||||
relationships: {}, icon: null, color: null, order: null,
|
||||
sidebarLabel: null, template: null, sort: null, view: null,
|
||||
fileKind: 'binary',
|
||||
outgoingLinks: [], properties: {},
|
||||
},
|
||||
]
|
||||
|
||||
render(<Sidebar entries={entries} selection={defaultSelection} onSelect={() => {}} />)
|
||||
|
||||
const topNav = screen.getByTestId('sidebar-top-nav')
|
||||
expect(topNav.children[1].textContent).toContain('All Notes1')
|
||||
expect(topNav.children[2].textContent).toContain('Archive1')
|
||||
})
|
||||
|
||||
it('does not show inline entries — no child items in type sections', () => {
|
||||
const entriesWithEmoji: VaultEntry[] = [
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
ViewFile,
|
||||
} from '../../types'
|
||||
import type { NoteListFilter } from '../../utils/noteListHelpers'
|
||||
import { countByFilter, countAllByFilter } from '../../utils/noteListHelpers'
|
||||
import { countByFilter, countAllByFilter, countAllNotesByFilter } from '../../utils/noteListHelpers'
|
||||
import { NoteItem } from '../NoteItem'
|
||||
import type { MultiSelectState } from '../../hooks/useMultiSelect'
|
||||
import { resolveHeaderTitle, type DeletedNoteEntry } from './noteListUtils'
|
||||
@@ -76,7 +76,7 @@ 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)
|
||||
if (selection.kind === 'filter' && selection.filter === 'all') return countAllNotesByFilter(entries)
|
||||
return { open: 0, archived: 0 }
|
||||
}, [entries, selection])
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useMemo, useEffect, useCallback, type RefObject } from 'react
|
||||
import type { VaultEntry } from '../../types'
|
||||
import { APP_STORAGE_KEYS, LEGACY_APP_STORAGE_KEYS, getAppStorageItem } from '../../constants/appStorage'
|
||||
import { buildTypeEntryMap } from '../../utils/typeColors'
|
||||
import { countAllNotesByFilter } from '../../utils/noteListHelpers'
|
||||
import { buildDynamicSections, sortSections } from '../../utils/sidebarSections'
|
||||
|
||||
export type SidebarGroupKey = 'favorites' | 'views' | 'sections' | 'folders'
|
||||
@@ -58,13 +59,8 @@ export function useSidebarCollapsed() {
|
||||
|
||||
export function useEntryCounts(entries: VaultEntry[]) {
|
||||
return useMemo(() => {
|
||||
let active = 0
|
||||
let archived = 0
|
||||
for (const entry of entries) {
|
||||
if (entry.archived) archived++
|
||||
else active++
|
||||
}
|
||||
return { activeCount: active, archivedCount: archived }
|
||||
const counts = countAllNotesByFilter(entries)
|
||||
return { activeCount: counts.open, archivedCount: counts.archived }
|
||||
}, [entries])
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { countAllByFilter, countByFilter, filterEntries } from './noteListHelpers'
|
||||
import { countAllByFilter, countAllNotesByFilter, countByFilter, filterEntries } from './noteListHelpers'
|
||||
import { allSelection, makeEntry, mockEntries } from '../test-utils/noteListTestUtils'
|
||||
|
||||
describe('filterEntries', () => {
|
||||
@@ -62,6 +62,17 @@ describe('filterEntries', () => {
|
||||
const result = filterEntries(entries, allSelection, 'archived')
|
||||
expect(result.map((entry) => entry.title)).toEqual(['Archived'])
|
||||
})
|
||||
|
||||
it('excludes attachments-folder markdown from the All Notes view', () => {
|
||||
const entries = [
|
||||
makeEntry({ path: '/vault/note/real-note.md', title: 'Real Note', isA: 'Note' }),
|
||||
makeEntry({ path: '/vault/attachments/reference.md', title: 'Attachment Markdown', isA: 'Note' }),
|
||||
makeEntry({ path: '/vault/attachments/nested/diagram.md', title: 'Nested Attachment Markdown', isA: 'Note' }),
|
||||
]
|
||||
|
||||
const result = filterEntries(entries, allSelection, 'open')
|
||||
expect(result.map((entry) => entry.title)).toEqual(['Real Note'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('countByFilter', () => {
|
||||
@@ -102,3 +113,17 @@ describe('countAllByFilter', () => {
|
||||
expect(countAllByFilter(entries)).toEqual({ open: 1, archived: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('countAllNotesByFilter', () => {
|
||||
it('excludes attachments-folder files from All Notes totals', () => {
|
||||
const entries = [
|
||||
makeEntry({ path: '/vault/note/real-note.md', isA: 'Note' }),
|
||||
makeEntry({ path: '/vault/attachments/reference.md', isA: 'Note' }),
|
||||
makeEntry({ path: '/vault/attachments/archive.md', isA: 'Note', archived: true }),
|
||||
makeEntry({ path: '/vault/attachments/image.png', fileKind: 'binary' }),
|
||||
makeEntry({ path: '/vault/archive/real-archive.md', isA: 'Note', archived: true }),
|
||||
]
|
||||
|
||||
expect(countAllNotesByFilter(entries)).toEqual({ open: 1, archived: 1 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -310,6 +310,7 @@ export function buildRelationshipGroups(
|
||||
|
||||
const isActive = (e: VaultEntry) => !e.archived
|
||||
const isMarkdown = (e: VaultEntry) => e.fileKind === 'markdown' || !e.fileKind
|
||||
const ATTACHMENTS_FOLDER = 'attachments'
|
||||
|
||||
function applySubFilter(entries: VaultEntry[], subFilter: NoteListFilter): VaultEntry[] {
|
||||
if (subFilter === 'archived') return entries.filter((e) => e.archived)
|
||||
@@ -321,26 +322,46 @@ function isInFolder(entryPath: string, folderRelPath: string): boolean {
|
||||
return entryPath.includes(needle) || entryPath.startsWith(folderRelPath + '/')
|
||||
}
|
||||
|
||||
export function isAllNotesEntry(entry: VaultEntry): boolean {
|
||||
return isMarkdown(entry) && !isInFolder(entry.path, ATTACHMENTS_FOLDER)
|
||||
}
|
||||
|
||||
function filterViewEntries(entries: VaultEntry[], filename: string, views?: ViewFile[]): VaultEntry[] {
|
||||
const view = views?.find((candidate) => candidate.filename === filename)
|
||||
if (!view) return []
|
||||
return evaluateView(view.definition, entries.filter(isMarkdown))
|
||||
}
|
||||
|
||||
function filterFolderEntries(entries: VaultEntry[], folderPath: string, subFilter?: NoteListFilter): VaultEntry[] {
|
||||
// Folder view shows ALL files (text + binary), not just markdown
|
||||
const folderEntries = entries.filter((entry) => isInFolder(entry.path, folderPath))
|
||||
return subFilter ? applySubFilter(folderEntries, subFilter) : folderEntries.filter(isActive)
|
||||
}
|
||||
|
||||
function filterSectionGroupEntries(entries: VaultEntry[], type: string, subFilter?: NoteListFilter): VaultEntry[] {
|
||||
const typeEntries = entries.filter((entry) => isMarkdown(entry) && entry.isA === type)
|
||||
return subFilter ? applySubFilter(typeEntries, subFilter) : typeEntries.filter(isActive)
|
||||
}
|
||||
|
||||
function filterTopLevelEntries(
|
||||
entries: VaultEntry[],
|
||||
selection: Extract<SidebarSelection, { kind: 'filter' }>,
|
||||
subFilter?: NoteListFilter,
|
||||
): VaultEntry[] {
|
||||
const filterableEntries = selection.filter === 'all'
|
||||
? entries.filter(isAllNotesEntry)
|
||||
: entries.filter(isMarkdown)
|
||||
if (selection.filter === 'all' && subFilter) return applySubFilter(filterableEntries, subFilter)
|
||||
return filterByFilterType(filterableEntries, selection.filter)
|
||||
}
|
||||
|
||||
function filterByKind(entries: VaultEntry[], selection: SidebarSelection, subFilter?: NoteListFilter, views?: ViewFile[]): VaultEntry[] {
|
||||
if (selection.kind === 'entity') return []
|
||||
if (selection.kind === 'view') {
|
||||
const view = views?.find((v) => v.filename === selection.filename)
|
||||
if (!view) return []
|
||||
return evaluateView(view.definition, entries.filter(isMarkdown))
|
||||
}
|
||||
if (selection.kind === 'folder') {
|
||||
// Folder view shows ALL files (text + binary), not just markdown
|
||||
const folderEntries = entries.filter((e) => isInFolder(e.path, selection.path))
|
||||
return subFilter ? applySubFilter(folderEntries, subFilter) : folderEntries.filter(isActive)
|
||||
}
|
||||
if (selection.kind === 'sectionGroup') {
|
||||
const typeEntries = entries.filter((e) => isMarkdown(e) && e.isA === selection.type)
|
||||
return subFilter ? applySubFilter(typeEntries, subFilter) : typeEntries.filter(isActive)
|
||||
}
|
||||
// Non-folder views: only markdown files
|
||||
const mdEntries = entries.filter(isMarkdown)
|
||||
if (selection.filter === 'all' && subFilter) return applySubFilter(mdEntries, subFilter)
|
||||
return filterByFilterType(mdEntries, selection.filter)
|
||||
if (selection.kind === 'view') return filterViewEntries(entries, selection.filename, views)
|
||||
if (selection.kind === 'folder') return filterFolderEntries(entries, selection.path, subFilter)
|
||||
if (selection.kind === 'sectionGroup') return filterSectionGroupEntries(entries, selection.type, subFilter)
|
||||
if (selection.kind === 'filter') return filterTopLevelEntries(entries, selection, subFilter)
|
||||
return []
|
||||
}
|
||||
|
||||
function filterByFilterType(entries: VaultEntry[], filter: string): VaultEntry[] {
|
||||
@@ -366,17 +387,25 @@ export function countByFilter(entries: VaultEntry[], type: string): Record<NoteL
|
||||
return { open, archived }
|
||||
}
|
||||
|
||||
/** Count notes per sub-filter across all entries (no type filter). */
|
||||
export function countAllByFilter(entries: VaultEntry[]): Record<NoteListFilter, number> {
|
||||
function countEntriesByArchiveStatus(entries: VaultEntry[]): Record<NoteListFilter, number> {
|
||||
let open = 0, archived = 0
|
||||
for (const e of entries) {
|
||||
if (!isMarkdown(e)) continue
|
||||
if (e.archived) archived++
|
||||
for (const entry of entries) {
|
||||
if (entry.archived) archived++
|
||||
else open++
|
||||
}
|
||||
return { open, archived }
|
||||
}
|
||||
|
||||
/** Count notes per sub-filter across all entries (no type filter). */
|
||||
export function countAllByFilter(entries: VaultEntry[]): Record<NoteListFilter, number> {
|
||||
return countEntriesByArchiveStatus(entries.filter(isMarkdown))
|
||||
}
|
||||
|
||||
/** Count All Notes-eligible documents per sub-filter, excluding files under attachments/. */
|
||||
export function countAllNotesByFilter(entries: VaultEntry[]): Record<NoteListFilter, number> {
|
||||
return countEntriesByArchiveStatus(entries.filter(isAllNotesEntry))
|
||||
}
|
||||
|
||||
// --- Inbox ---
|
||||
|
||||
/** Check if entry belongs in the Inbox (markdown only, not organized, not archived, not a Type). */
|
||||
|
||||
Reference in New Issue
Block a user