All files / src/components/knowledge KnowledgeNav.tsx

86.36% Statements 19/22
75% Branches 12/16
90.9% Functions 10/11
86.36% Lines 19/22

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                                                                                        77x   77x   4x 4x 4x           77x 3x 3x     77x   1x         77x   2x 2x         77x                   56x                                     2x             4x                   21x       1x                                                                        
/**
 * Knowledge graph sidebar navigation — search, workspace filter, and node list.
 *
 * Rendered inside the shared Sidebar component when the Knowledge tab is active.
 *
 * @module
 */
 
import { useState, useCallback, type FormEvent, type JSX } from "react";
import type { Workspace } from "../../hooks/types.js";
import type { GraphNode } from "../../hooks/types.js";
import styles from "./KnowledgeNav.module.scss";
 
/** Props for the KnowledgeNav sidebar component. */
export interface KnowledgeNavProps {
  /** Nodes currently in the graph. */
  nodes: GraphNode[];
  /** All workspaces for the filter dropdown. */
  workspaces: Workspace[];
  /** Whether a search or load operation is in progress. */
  loading: boolean;
  /** Active search query (non-empty means search is active). */
  searchQuery: string;
  /** Execute a semantic search. */
  onSearch: (query: string) => void;
  /** Clear search and reload recent nodes. */
  onClearSearch: () => void;
  /** Select a node by ID (e.g., open detail panel). */
  onSelectNode: (nodeId: string) => void;
  /** Filter by workspace (empty string means all workspaces). */
  onWorkspaceChange: (workspaceId: string) => void;
}
 
/** Sidebar content for the Knowledge tab. */
export function KnowledgeNav({
  nodes,
  workspaces,
  loading,
  searchQuery,
  onSearch,
  onClearSearch,
  onSelectNode,
  onWorkspaceChange,
}: KnowledgeNavProps): JSX.Element {
  const [searchInput, setSearchInput] = useState("");
 
  const handleSearch = useCallback(
    (e: FormEvent) => {
      e.preventDefault();
      if (searchInput.trim()) {
        onSearch(searchInput.trim());
      }
    },
    [searchInput, onSearch],
  );
 
  const handleClear = useCallback(() => {
    setSearchInput("");
    onClearSearch();
  }, [onClearSearch]);
 
  const handleNodeClick = useCallback(
    (nodeId: string) => {
      onSelectNode(nodeId);
    },
    [onSelectNode],
  );
 
  const handleWorkspaceChange = useCallback(
    (wsId: string) => {
      setSearchInput("");
      onWorkspaceChange(wsId);
    },
    [onWorkspaceChange],
  );
 
  return (
    <div className={styles.nav} data-testid="knowledge-nav">
      {/* Search */}
      <form className={styles.searchForm} onSubmit={handleSearch}>
        <input
          className={styles.searchInput}
          type="text"
          placeholder="Search..."
          value={searchInput}
          onChange={(e) => {
            setSearchInput(e.target.value);
          }}
          data-testid="knowledge-search-input"
          aria-label="Search knowledge nodes"
        />
        <button type="submit" className={styles.searchButton} disabled={loading}>
          Go
        </button>
      </form>
      {searchQuery && (
        <button type="button" className={styles.clearButton} onClick={handleClear}>
          Clear search
        </button>
      )}
 
      {/* Workspace filter */}
      <select
        className={styles.workspaceSelect}
        onChange={(e) => {
          handleWorkspaceChange(e.target.value);
        }}
        data-testid="knowledge-workspace-filter"
        aria-label="Filter by workspace"
      >
        <option value="">All workspaces</option>
        {workspaces.map((ws) => (
          <option key={ws.id} value={ws.id}>
            {ws.name}
          </option>
        ))}
      </select>
 
      {/* Node list */}
      <div className={styles.listHeader}>Nodes ({nodes.length})</div>
      <ul className={styles.nodeList}>
        {nodes.map((node: GraphNode) => (
          <li
            key={node.id}
            className={styles.nodeItem}
            onClick={() => {
              handleNodeClick(node.id);
            }}
            onKeyDown={(e) => {
              Iif (e.key === "Enter" || e.key === " ") {
                e.preventDefault();
                handleNodeClick(node.id);
              }
            }}
            role="button"
            tabIndex={0}
          >
            <span
              className={styles.indicator}
              style={{
                backgroundColor:
                  node.kind === "reference"
                    ? "#4A9EFF"
                    : node.category === "decision"
                      ? "#22C55E"
                      : node.category === "concept"
                        ? "#A855F7"
                        : node.category === "snippet"
                          ? "#6B7280"
                          : "#EAB308",
              }}
            />
            <span className={styles.label}>{node.label}</span>
            <span className={styles.badge}>
              {node.kind === "reference" ? node.sourceType : node.category}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}