From deb6998c976c01756ab907d7bfc41b398dd69125 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 25 May 2026 10:41:24 +0200 Subject: [PATCH] refactor: consolidate type definition lookup --- src/hooks/useEntryActions.ts | 11 +-- .../useEntryActions.typeVisibility.test.ts | 13 ++++ src/utils/typeDefinitions.ts | 74 +++++++++++++++++++ src/utils/typeVisibility.ts | 53 ++++--------- 4 files changed, 105 insertions(+), 46 deletions(-) create mode 100644 src/utils/typeDefinitions.ts diff --git a/src/hooks/useEntryActions.ts b/src/hooks/useEntryActions.ts index dfdcd1df..e4241cbe 100644 --- a/src/hooks/useEntryActions.ts +++ b/src/hooks/useEntryActions.ts @@ -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 { - 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 { diff --git a/src/hooks/useEntryActions.typeVisibility.test.ts b/src/hooks/useEntryActions.typeVisibility.test.ts index 32401948..a5981546 100644 --- a/src/hooks/useEntryActions.typeVisibility.test.ts +++ b/src/hooks/useEntryActions.typeVisibility.test.ts @@ -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 }) diff --git a/src/utils/typeDefinitions.ts b/src/utils/typeDefinitions.ts new file mode 100644 index 00000000..ccde8827 --- /dev/null +++ b/src/utils/typeDefinitions.ts @@ -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): string { + return typeWorkspaceKey({ path: entry.workspace?.path }) +} + +export function isMarkdownEntry(entry: Pick): 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 +} diff --git a/src/utils/typeVisibility.ts b/src/utils/typeVisibility.ts index 0d4267f9..fb64ea37 100644 --- a/src/utils/typeVisibility.ts +++ b/src/utils/typeVisibility.ts @@ -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> -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): 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 }) }