diff --git a/src/components/Inspector.tsx b/src/components/Inspector.tsx index edfe5693..f344a515 100644 --- a/src/components/Inspector.tsx +++ b/src/components/Inspector.tsx @@ -4,7 +4,8 @@ import { cn } from '@/lib/utils' import { SlidersHorizontal, X } from '@phosphor-icons/react' import { parseFrontmatter } from '../utils/frontmatter' import { DynamicPropertiesPanel } from './DynamicPropertiesPanel' -import { DynamicRelationshipsPanel, BacklinksPanel, GitHistoryPanel } from './InspectorPanels' +import { DynamicRelationshipsPanel, BacklinksPanel, ReferencedByPanel, GitHistoryPanel, wikilinkTarget } from './InspectorPanels' +import type { ReferencedByItem } from './InspectorPanels' export type FrontmatterValue = string | number | boolean | string[] | null @@ -40,6 +41,35 @@ function useBacklinks(entry: VaultEntry | null, entries: VaultEntry[], allConten }, [entry, entries, allContent]) } +function useReferencedBy(entry: VaultEntry | null, entries: VaultEntry[]): ReferencedByItem[] { + return useMemo(() => { + if (!entry) return [] + + const pathStem = entry.path.replace(/^.*\/Laputa\//, '').replace(/\.md$/, '') + const filenameStem = entry.filename.replace(/\.md$/, '') + const matchTargets = new Set([pathStem, filenameStem, entry.title, ...entry.aliases]) + + const results: ReferencedByItem[] = [] + + for (const other of entries) { + if (other.path === entry.path) continue + + for (const [key, refs] of Object.entries(other.relationships)) { + if (key === 'Type') continue + for (const ref of refs) { + const target = wikilinkTarget(ref) + if (matchTargets.has(target) || matchTargets.has(target.split('/').pop() ?? '')) { + results.push({ entry: other, viaKey: key }) + break + } + } + } + } + + return results + }, [entry, entries]) +} + function InspectorHeader({ collapsed, onToggle }: { collapsed: boolean; onToggle: () => void }) { return (
@@ -65,6 +95,10 @@ function EmptyInspector() { <>

No note selected

No relationships

+
+

Referenced by

+

No references

+

Backlinks

No backlinks

@@ -82,6 +116,7 @@ export function Inspector({ onViewCommitDiff, onUpdateFrontmatter, onDeleteProperty, onAddProperty, }: InspectorProps) { const backlinks = useBacklinks(entry, entries, allContent) + const referencedBy = useReferencedBy(entry, entries) const frontmatter = useMemo(() => parseFrontmatter(content), [content]) const handleUpdateProperty = useCallback((key: string, value: FrontmatterValue) => { @@ -111,6 +146,7 @@ export function Inspector({ onNavigate={onNavigate} /> + diff --git a/src/components/InspectorPanels.tsx b/src/components/InspectorPanels.tsx index c84693ac..12d8be91 100644 --- a/src/components/InspectorPanels.tsx +++ b/src/components/InspectorPanels.tsx @@ -32,7 +32,7 @@ function wikilinkDisplay(ref: string): string { return last.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) } -function wikilinkTarget(ref: string): string { +export function wikilinkTarget(ref: string): string { const inner = ref.replace(/^\[\[|\]\]$/g, '') const pipeIdx = inner.indexOf('|') return pipeIdx !== -1 ? inner.slice(0, pipeIdx) : inner @@ -217,6 +217,62 @@ export function BacklinksPanel({ backlinks, onNavigate }: { backlinks: VaultEntr ) } +export interface ReferencedByItem { + entry: VaultEntry + viaKey: string +} + +export function ReferencedByPanel({ items, onNavigate }: { + items: ReferencedByItem[] + onNavigate: (target: string) => void +}) { + const grouped = useMemo(() => { + const map = new Map() + for (const item of items) { + const existing = map.get(item.viaKey) + if (existing) existing.push(item.entry) + else map.set(item.viaKey, [item.entry]) + } + return Array.from(map.entries()) + }, [items]) + + return ( +
+

+ Referenced by {items.length > 0 && {items.length}} +

+ {items.length === 0 + ?

No references

+ : ( +
+ {grouped.map(([viaKey, groupEntries]) => ( +
+ + via {viaKey} + +
+ {groupEntries.map((e) => ( + onNavigate(e.title)} + title={e.trashed ? 'Trashed' : e.archived ? 'Archived' : undefined} + TypeIcon={getTypeIcon(e.isA ?? undefined)} + /> + ))} +
+
+ ))} +
+ ) + } +
+ ) +} + function formatRelativeDate(timestamp: number): string { const now = Math.floor(Date.now() / 1000) const days = Math.floor((now - timestamp) / 86400)