All files / src/components Inspector.tsx

88% Statements 44/50
59.25% Branches 32/54
78.57% Functions 11/14
92.3% Lines 36/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173                                                      67x 62x 29x           29x 28x 14x 3x             67x 62x   29x 29x 29x   29x   29x 28x   14x 20x 10x 10x 10x 9x 9x           29x         67x                                       37x                                               67x 67x 67x 67x 62x 62x 62x     67x       67x       67x       67x                                                                  
import { useMemo, useCallback } from 'react'
import type { VaultEntry, GitCommit } from '../types'
import { cn } from '@/lib/utils'
import { SlidersHorizontal, X } from '@phosphor-icons/react'
import { parseFrontmatter } from '../utils/frontmatter'
import { DynamicPropertiesPanel } from './DynamicPropertiesPanel'
import { DynamicRelationshipsPanel, BacklinksPanel, ReferencedByPanel, GitHistoryPanel } from './InspectorPanels'
import { wikilinkTarget } from '../utils/wikilink'
import type { ReferencedByItem } from './InspectorPanels'
 
export type FrontmatterValue = string | number | boolean | string[] | null
 
interface InspectorProps {
  collapsed: boolean
  onToggle: () => void
  entry: VaultEntry | null
  content: string | null
  entries: VaultEntry[]
  gitHistory: GitCommit[]
  onNavigate: (target: string) => void
  onViewCommitDiff?: (commitHash: string) => void
  onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
  onDeleteProperty?: (path: string, key: string) => Promise<void>
  onAddProperty?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
}
 
function useBacklinks(entry: VaultEntry | null, entries: VaultEntry[]): VaultEntry[] {
  return useMemo(() => {
    if (!entry) return []
    const matchTargets = new Set([
      entry.title, ...entry.aliases,
      entry.filename.replace(/\.md$/, ''),
      entry.path.replace(/^.*\/Laputa\//, '').replace(/\.md$/, ''),
    ])
 
    return entries.filter((e) => {
      if (e.path === entry.path) return false
      return e.outgoingLinks.some((target) =>
        matchTargets.has(target) || matchTargets.has(target.split('/').pop() ?? '')
      )
    })
  }, [entry, entries])
}
 
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 (
    <div className="flex items-center border-b border-border" style={{ height: 45, padding: '0 12px', gap: 8 }} data-tauri-drag-region>
      {collapsed ? (
        <button className="shrink-0 border-none bg-transparent p-1 text-muted-foreground cursor-pointer hover:text-foreground" onClick={onToggle} style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
          <SlidersHorizontal size={16} />
        </button>
      ) : (
        <>
          <SlidersHorizontal size={16} className="shrink-0 text-muted-foreground" />
          <span className="flex-1 text-muted-foreground" style={{ fontSize: 13, fontWeight: 600 }}>Properties</span>
          <button className="shrink-0 border-none bg-transparent p-1 text-muted-foreground cursor-pointer hover:text-foreground" onClick={onToggle} style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
            <X size={16} />
          </button>
        </>
      )}
    </div>
  )
}
 
function EmptyInspector() {
  return (
    <>
      <div><p className="m-0 text-[13px] text-muted-foreground">No note selected</p></div>
      <div><p className="m-0 text-[13px] text-muted-foreground">No relationships</p></div>
      <div>
        <h4 className="font-mono-overline mb-2 text-muted-foreground">Referenced by</h4>
        <p className="m-0 text-[13px] text-muted-foreground">No references</p>
      </div>
      <div>
        <h4 className="font-mono-overline mb-2 text-muted-foreground">Backlinks</h4>
        <p className="m-0 text-[13px] text-muted-foreground">No backlinks</p>
      </div>
      <div>
        <h4 className="font-mono-overline mb-2 text-muted-foreground">History</h4>
        <p className="m-0 text-[13px] text-muted-foreground">No revision history</p>
      </div>
    </>
  )
}
 
export function Inspector({
  collapsed, onToggle, entry, content, entries, gitHistory, onNavigate,
  onViewCommitDiff, onUpdateFrontmatter, onDeleteProperty, onAddProperty,
}: InspectorProps) {
  const backlinks = useBacklinks(entry, entries)
  const referencedBy = useReferencedBy(entry, entries)
  const frontmatter = useMemo(() => parseFrontmatter(content), [content])
  const typeEntryMap = useMemo(() => {
    const map: Record<string, VaultEntry> = {}
    for (const e of entries) { if (e.isA === 'Type') map[e.title] = e }
    return map
  }, [entries])
 
  const handleUpdateProperty = useCallback((key: string, value: FrontmatterValue) => {
    if (entry && onUpdateFrontmatter) onUpdateFrontmatter(entry.path, key, value)
  }, [entry, onUpdateFrontmatter])
 
  const handleDeleteProperty = useCallback((key: string) => {
    if (entry && onDeleteProperty) onDeleteProperty(entry.path, key)
  }, [entry, onDeleteProperty])
 
  const handleAddProperty = useCallback((key: string, value: FrontmatterValue) => {
    if (entry && onAddProperty) onAddProperty(entry.path, key, value)
  }, [entry, onAddProperty])
 
  return (
    <aside className={cn("flex flex-1 flex-col overflow-y-auto border-l border-border bg-background text-foreground transition-[width] duration-200", collapsed && "!w-10 !min-w-10")}>
      <InspectorHeader collapsed={collapsed} onToggle={onToggle} />
      {!collapsed && (
        <div className="flex flex-col gap-4 p-3">
          {entry ? (
            <>
              <DynamicPropertiesPanel
                entry={entry} content={content} frontmatter={frontmatter}
                entries={entries}
                onUpdateProperty={onUpdateFrontmatter ? handleUpdateProperty : undefined}
                onDeleteProperty={onDeleteProperty ? handleDeleteProperty : undefined}
                onAddProperty={onAddProperty ? handleAddProperty : undefined}
                onNavigate={onNavigate}
              />
              <DynamicRelationshipsPanel
                frontmatter={frontmatter} entries={entries} typeEntryMap={typeEntryMap} onNavigate={onNavigate}
                onAddProperty={onAddProperty ? handleAddProperty : undefined}
                onUpdateProperty={onUpdateFrontmatter ? handleUpdateProperty : undefined}
                onDeleteProperty={onDeleteProperty ? handleDeleteProperty : undefined}
              />
              <ReferencedByPanel items={referencedBy} typeEntryMap={typeEntryMap} onNavigate={onNavigate} />
              <BacklinksPanel backlinks={backlinks} typeEntryMap={typeEntryMap} onNavigate={onNavigate} />
              <GitHistoryPanel commits={gitHistory} onViewCommitDiff={onViewCommitDiff} />
            </>
          ) : (
            <EmptyInspector />
          )}
        </div>
      )}
    </aside>
  )
}