fix: disambiguate sidebar type visibility by vault
This commit is contained in:
@@ -1,6 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildRelationshipGroups, countAllByFilter, countAllNotesByFilter, countByFilter, filterEntries } from './noteListHelpers'
|
||||
import { allSelection, makeEntry, mockEntries } from '../test-utils/noteListTestUtils'
|
||||
import type { WorkspaceIdentity } from '../types'
|
||||
|
||||
const mainWorkspace: WorkspaceIdentity = {
|
||||
id: 'main',
|
||||
label: 'Main',
|
||||
alias: 'main',
|
||||
path: '/vault/main',
|
||||
shortLabel: 'MA',
|
||||
color: 'blue',
|
||||
icon: null,
|
||||
mounted: true,
|
||||
available: true,
|
||||
defaultForNewNotes: true,
|
||||
}
|
||||
|
||||
const workWorkspace: WorkspaceIdentity = {
|
||||
id: 'work',
|
||||
label: 'Work',
|
||||
alias: 'work',
|
||||
path: '/vault/work',
|
||||
shortLabel: 'WK',
|
||||
color: 'green',
|
||||
icon: null,
|
||||
mounted: true,
|
||||
available: true,
|
||||
defaultForNewNotes: false,
|
||||
}
|
||||
|
||||
describe('filterEntries', () => {
|
||||
it('returns empty for entity selections because entity view uses grouped relationships', () => {
|
||||
@@ -41,6 +68,18 @@ describe('filterEntries', () => {
|
||||
expect(result.map((entry) => entry.title)).toEqual(['Active'])
|
||||
})
|
||||
|
||||
it('filters duplicate type sections by workspace visibility', () => {
|
||||
const entries = [
|
||||
makeEntry({ path: '/vault/main/project.md', title: 'Project', isA: 'Type', workspace: mainWorkspace, visible: false }),
|
||||
makeEntry({ path: '/vault/work/project.md', title: 'Project', isA: 'Type', workspace: workWorkspace, visible: null }),
|
||||
makeEntry({ path: '/vault/main/main-project.md', title: 'Main Project', isA: 'Project', workspace: mainWorkspace }),
|
||||
makeEntry({ path: '/vault/work/work-project.md', title: 'Work Project', isA: 'Project', workspace: workWorkspace }),
|
||||
]
|
||||
|
||||
const result = filterEntries(entries, { kind: 'sectionGroup', type: 'Project' })
|
||||
expect(result.map((entry) => entry.title)).toEqual(['Work Project'])
|
||||
})
|
||||
|
||||
it('filters all notes by open sub-filter', () => {
|
||||
const entries = [
|
||||
makeEntry({ path: '/1.md', title: 'Active', isA: 'Project' }),
|
||||
@@ -196,6 +235,18 @@ describe('countByFilter', () => {
|
||||
expect(countByFilter(entries, 'Project')).toEqual({ open: 2, archived: 1 })
|
||||
})
|
||||
|
||||
it('counts duplicate type entries only from visible workspaces', () => {
|
||||
const entries = [
|
||||
makeEntry({ path: '/vault/main/project.md', title: 'Project', isA: 'Type', workspace: mainWorkspace, visible: false }),
|
||||
makeEntry({ path: '/vault/work/project.md', title: 'Project', isA: 'Type', workspace: workWorkspace, visible: null }),
|
||||
makeEntry({ path: '/vault/main/main-project.md', title: 'Main Project', isA: 'Project', workspace: mainWorkspace }),
|
||||
makeEntry({ path: '/vault/work/work-project.md', title: 'Work Project', isA: 'Project', workspace: workWorkspace }),
|
||||
makeEntry({ path: '/vault/work/archived-project.md', title: 'Archived Project', isA: 'Project', workspace: workWorkspace, archived: true }),
|
||||
]
|
||||
|
||||
expect(countByFilter(entries, 'Project')).toEqual({ open: 1, archived: 1 })
|
||||
})
|
||||
|
||||
it('returns zeros when a type has no matching entries', () => {
|
||||
expect(countByFilter([], 'Project')).toEqual({ open: 0, archived: 0 })
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { evaluateView } from './viewFilters'
|
||||
import { viewMatchesSelection } from './viewIdentity'
|
||||
import { wikilinkTarget, resolveEntry } from './wikilink'
|
||||
import { buildTypeVisibilityLookup, isSectionEntryVisibleForType } from './typeVisibility'
|
||||
|
||||
export type NoteListFilter = 'open' | 'archived'
|
||||
|
||||
@@ -518,7 +519,8 @@ function filterFolderEntries(entries: VaultEntry[], selection: Extract<SidebarSe
|
||||
}
|
||||
|
||||
function filterSectionGroupEntries(entries: VaultEntry[], type: string, subFilter?: NoteListFilter): VaultEntry[] {
|
||||
const typeEntries = entries.filter((entry) => isMarkdown(entry) && entry.isA === type)
|
||||
const typeVisibility = buildTypeVisibilityLookup(entries)
|
||||
const typeEntries = entries.filter((entry) => isSectionEntryVisibleForType(entry, type, typeVisibility))
|
||||
return subFilter ? applySubFilter(typeEntries, subFilter) : typeEntries.filter(isActive)
|
||||
}
|
||||
|
||||
@@ -565,9 +567,10 @@ export function filterEntries(
|
||||
|
||||
/** Count notes per sub-filter for a given type. */
|
||||
export function countByFilter(entries: VaultEntry[], type: string): Record<NoteListFilter, number> {
|
||||
const typeVisibility = buildTypeVisibilityLookup(entries)
|
||||
let open = 0, archived = 0
|
||||
for (const e of entries) {
|
||||
if (!isMarkdown(e) || e.isA !== type) continue
|
||||
if (!isSectionEntryVisibleForType(e, type, typeVisibility)) continue
|
||||
if (e.archived) archived++
|
||||
else open++
|
||||
}
|
||||
|
||||
110
src/utils/typeVisibility.ts
Normal file
110
src/utils/typeVisibility.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { VaultEntry, WorkspaceIdentity } from '../types'
|
||||
|
||||
const NO_WORKSPACE_KEY = '__tolaria_no_workspace__'
|
||||
|
||||
export type TypeVisibilityLookup = Record<string, Record<string, boolean>>
|
||||
|
||||
function isMarkdown(entry: VaultEntry): boolean {
|
||||
return entry.fileKind === 'markdown' || !entry.fileKind
|
||||
}
|
||||
|
||||
function typeKey(type: string): string {
|
||||
return type.trim().toLowerCase()
|
||||
}
|
||||
|
||||
function workspaceKey(path?: string | null): string {
|
||||
return path?.trim() || NO_WORKSPACE_KEY
|
||||
}
|
||||
|
||||
function entryWorkspaceKey(entry: Pick<VaultEntry, 'workspace'>): string {
|
||||
return workspaceKey(entry.workspace?.path)
|
||||
}
|
||||
|
||||
function isActiveTypeDefinition(entry: VaultEntry): boolean {
|
||||
return isMarkdown(entry) && entry.isA === 'Type' && !entry.archived
|
||||
}
|
||||
|
||||
export function buildTypeVisibilityLookup(entries: VaultEntry[]): TypeVisibilityLookup {
|
||||
const lookup: TypeVisibilityLookup = {}
|
||||
for (const entry of entries) {
|
||||
if (!isActiveTypeDefinition(entry)) continue
|
||||
const key = typeKey(entry.title)
|
||||
if (!key) continue
|
||||
lookup[key] = lookup[key] ?? {}
|
||||
lookup[key][entryWorkspaceKey(entry)] = entry.visible !== false
|
||||
}
|
||||
return lookup
|
||||
}
|
||||
|
||||
export function isTypeVisibleInWorkspace(
|
||||
lookup: TypeVisibilityLookup,
|
||||
type: string,
|
||||
workspacePath?: string | null,
|
||||
): boolean {
|
||||
const typeLookup = lookup[typeKey(type)]
|
||||
if (!typeLookup) return true
|
||||
const visible = typeLookup[workspaceKey(workspacePath)]
|
||||
return visible !== false
|
||||
}
|
||||
|
||||
export function isSectionEntryVisibleForType(
|
||||
entry: VaultEntry,
|
||||
type: string,
|
||||
lookup: TypeVisibilityLookup,
|
||||
): boolean {
|
||||
if (!isMarkdown(entry) || entry.isA !== type) return false
|
||||
return isTypeVisibleInWorkspace(lookup, type, entry.workspace?.path)
|
||||
}
|
||||
|
||||
function isMatchingTypeDefinition(entry: VaultEntry, type: string): boolean {
|
||||
return isActiveTypeDefinition(entry) && typeKey(entry.title) === typeKey(type)
|
||||
}
|
||||
|
||||
export function isTypeSectionVisible(
|
||||
entries: VaultEntry[],
|
||||
type: string,
|
||||
lookup: TypeVisibilityLookup = buildTypeVisibilityLookup(entries),
|
||||
): boolean {
|
||||
let hasMatchingTypeDefinition = false
|
||||
|
||||
for (const entry of entries) {
|
||||
if (isSectionEntryVisibleForType(entry, type, lookup)) return true
|
||||
if (!isMatchingTypeDefinition(entry, type)) continue
|
||||
hasMatchingTypeDefinition = true
|
||||
if (isTypeVisibleInWorkspace(lookup, type, entry.workspace?.path)) return true
|
||||
}
|
||||
|
||||
return !hasMatchingTypeDefinition
|
||||
}
|
||||
|
||||
function workspaceOrderIndex(workspace: WorkspaceIdentity, orderedWorkspacePaths: readonly string[]): number {
|
||||
const index = orderedWorkspacePaths.indexOf(workspace.path)
|
||||
return index === -1 ? Number.MAX_SAFE_INTEGER : index
|
||||
}
|
||||
|
||||
export function collectTypeVisibilityWorkspaces(
|
||||
entries: VaultEntry[],
|
||||
orderedWorkspacePaths: readonly string[] = [],
|
||||
): WorkspaceIdentity[] {
|
||||
const workspacesByPath = new Map<string, WorkspaceIdentity>()
|
||||
for (const entry of entries) {
|
||||
const workspace = entry.workspace
|
||||
if (!workspace || workspacesByPath.has(workspace.path)) continue
|
||||
workspacesByPath.set(workspace.path, workspace)
|
||||
}
|
||||
return [...workspacesByPath.values()].sort((a, b) => (
|
||||
workspaceOrderIndex(a, orderedWorkspacePaths) - workspaceOrderIndex(b, orderedWorkspacePaths)
|
||||
))
|
||||
}
|
||||
|
||||
export function findTypeDefinitionForWorkspace(
|
||||
entries: VaultEntry[],
|
||||
type: string,
|
||||
workspacePath: string,
|
||||
): VaultEntry | null {
|
||||
const key = typeKey(type)
|
||||
return entries.find((entry) => (
|
||||
isMatchingTypeDefinition(entry, key)
|
||||
&& entry.workspace?.path === workspacePath
|
||||
)) ?? null
|
||||
}
|
||||
Reference in New Issue
Block a user