Merge branch 'main' into pr-728

This commit is contained in:
github-actions[bot]
2026-05-25 08:56:32 +00:00
committed by GitHub
4 changed files with 105 additions and 46 deletions

View File

@@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react'
import type { VaultEntry } from '../types'
import { isMissingFrontmatterTargetError, type FrontmatterOpOptions } from './frontmatterOps'
import { trackEvent } from '../lib/telemetry'
import { findTypeDefinition } from '../utils/typeDefinitions'
interface EntryActionsConfig {
entries: VaultEntry[]
@@ -49,14 +50,6 @@ interface RenameTypeSectionArgs {
label: string
}
function findTypeEntry(entries: VaultEntry[], typeName: string, typeEntryPath?: string): VaultEntry | undefined {
if (typeEntryPath) {
const entry = entries.find((candidate) => candidate.path === typeEntryPath)
if (entry?.isA === 'Type') return entry
}
return entries.find((entry) => entry.isA === 'Type' && entry.title === typeName)
}
function logOptimisticRollback(label: string, error: unknown): void {
if (isMissingFrontmatterTargetError(error)) {
console.warn(label, error)
@@ -70,7 +63,7 @@ async function findOrCreateType(
typeName: string,
typeEntryPath?: string,
): Promise<VaultEntry | null> {
const existingType = findTypeEntry(deps.entries, typeName, typeEntryPath)
const existingType = findTypeDefinition({ entries: deps.entries, type: typeName, typeEntryPath })
if (existingType) return existingType
if (typeEntryPath) return null
try {

View File

@@ -23,6 +23,19 @@ function setup(entries: VaultEntry[]) {
}
describe('useEntryActions type visibility', () => {
it('targets an existing Type entry using normalized Type identity', async () => {
const typeEntry = makeEntry({ isA: 'Type', title: 'Journal', path: '/vault/journal.md', visible: null })
const { result, createTypeEntry, handleUpdateFrontmatter, updateEntry } = setup([typeEntry])
await act(async () => {
await result.current.handleToggleTypeVisibility(' journal ')
})
expect(createTypeEntry).not.toHaveBeenCalled()
expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/journal.md', 'visible', false)
expect(updateEntry).toHaveBeenCalledWith('/vault/journal.md', { visible: false })
})
it('targets the provided Type entry path when duplicate type names exist', async () => {
const hiddenTypeEntry = makeEntry({ isA: 'Type', title: 'Journal', path: '/vault/main/journal.md', visible: false })
const visibleTypeEntry = makeEntry({ isA: 'Type', title: 'Journal', path: '/vault/work/journal.md', visible: null })

View File

@@ -0,0 +1,74 @@
import type { VaultEntry } from '../types'
const NO_WORKSPACE_KEY = '__tolaria_no_workspace__'
interface TypeNameQuery {
type: string
}
interface WorkspacePathQuery {
path?: string | null
}
interface TypeDefinitionLookup {
entries: VaultEntry[]
type: string
typeEntryPath?: string
}
interface WorkspaceTypeDefinitionLookup {
entries: VaultEntry[]
type: string
workspacePath: string
}
export function normalizeTypeName({ type }: TypeNameQuery): string {
return type.trim().toLowerCase()
}
export function typeWorkspaceKey({ path }: WorkspacePathQuery): string {
return path?.trim() || NO_WORKSPACE_KEY
}
export function entryTypeWorkspaceKey(entry: Pick<VaultEntry, 'workspace'>): string {
return typeWorkspaceKey({ path: entry.workspace?.path })
}
export function isMarkdownEntry(entry: Pick<VaultEntry, 'fileKind'>): boolean {
return entry.fileKind === 'markdown' || !entry.fileKind
}
export function isActiveTypeDefinition(entry: VaultEntry): boolean {
return isMarkdownEntry(entry) && entry.isA === 'Type' && !entry.archived
}
export function isTypeDefinitionForName(entry: VaultEntry, query: TypeNameQuery): boolean {
return isActiveTypeDefinition(entry) && normalizeTypeName({ type: entry.title }) === normalizeTypeName(query)
}
export function findTypeDefinition({
entries,
type,
typeEntryPath,
}: TypeDefinitionLookup): VaultEntry | null {
if (typeEntryPath) {
const entry = entries.find((candidate) => candidate.path === typeEntryPath)
return entry && isActiveTypeDefinition(entry) ? entry : null
}
const key = normalizeTypeName({ type })
if (!key) return null
return entries.find((entry) => isTypeDefinitionForName(entry, { type: key })) ?? null
}
export function findTypeDefinitionForWorkspace({
entries,
type,
workspacePath,
}: WorkspaceTypeDefinitionLookup): VaultEntry | null {
const key = normalizeTypeName({ type })
if (!key) return null
return entries.find((entry) => (
isTypeDefinitionForName(entry, { type: key })
&& entry.workspace?.path === workspacePath
)) ?? null
}

View File

@@ -1,37 +1,24 @@
import type { VaultEntry, WorkspaceIdentity } from '../types'
const NO_WORKSPACE_KEY = '__tolaria_no_workspace__'
import {
entryTypeWorkspaceKey,
findTypeDefinitionForWorkspace as findTypeDefinitionForWorkspaceByQuery,
isActiveTypeDefinition,
isMarkdownEntry,
isTypeDefinitionForName,
normalizeTypeName,
typeWorkspaceKey,
} from './typeDefinitions'
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)
const key = normalizeTypeName({ type: entry.title })
if (!key) continue
lookup[key] = lookup[key] ?? {}
lookup[key][entryWorkspaceKey(entry)] = entry.visible !== false
lookup[key][entryTypeWorkspaceKey(entry)] = entry.visible !== false
}
return lookup
}
@@ -41,9 +28,9 @@ export function isTypeVisibleInWorkspace(
type: string,
workspacePath?: string | null,
): boolean {
const typeLookup = lookup[typeKey(type)]
const typeLookup = lookup[normalizeTypeName({ type })]
if (!typeLookup) return true
const visible = typeLookup[workspaceKey(workspacePath)]
const visible = typeLookup[typeWorkspaceKey({ path: workspacePath })]
return visible !== false
}
@@ -52,14 +39,10 @@ export function isSectionEntryVisibleForType(
type: string,
lookup: TypeVisibilityLookup,
): boolean {
if (!isMarkdown(entry) || entry.isA !== type) return false
if (!isMarkdownEntry(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,
@@ -69,7 +52,7 @@ export function isTypeSectionVisible(
for (const entry of entries) {
if (isSectionEntryVisibleForType(entry, type, lookup)) return true
if (!isMatchingTypeDefinition(entry, type)) continue
if (!isTypeDefinitionForName(entry, { type })) continue
hasMatchingTypeDefinition = true
if (isTypeVisibleInWorkspace(lookup, type, entry.workspace?.path)) return true
}
@@ -102,9 +85,5 @@ export function findTypeDefinitionForWorkspace(
type: string,
workspacePath: string,
): VaultEntry | null {
const key = typeKey(type)
return entries.find((entry) => (
isMatchingTypeDefinition(entry, key)
&& entry.workspace?.path === workspacePath
)) ?? null
return findTypeDefinitionForWorkspaceByQuery({ entries, type, workspacePath })
}