Implements two key knowledge module features: **#62 - Backlinks Display:** - Added BacklinksList component to show entries that link to current entry - Fetches backlinks from /api/knowledge/entries/:slug/backlinks - Displays entry title, summary, and link context - Clickable links to navigate to linking entries - Loading, error, and empty states **#64 - Wiki-Link Rendering:** - Added WikiLinkRenderer component to parse and render wiki-links - Supports [[slug]] and [[slug|display text]] syntax - Converts wiki-links to clickable navigation links - Distinct styling (blue color, dotted underline) - XSS protection via HTML escaping - Memoized HTML processing for performance **Components:** - BacklinksList.tsx - Backlinks display with empty/loading/error states - WikiLinkRenderer.tsx - Wiki-link parser and renderer - Updated EntryViewer.tsx to use WikiLinkRenderer - Integrated BacklinksList into entry detail page **API:** - Added fetchBacklinks() function in knowledge.ts - Added KnowledgeBacklink type to shared types **Tests:** - Comprehensive tests for BacklinksList (8 tests) - Comprehensive tests for WikiLinkRenderer (14 tests) - All tests passing with Vitest **Type Safety:** - Strict TypeScript compliance - No 'any' types - Proper error handling
This commit is contained in:
@@ -8,7 +8,14 @@ import { EntryViewer } from "@/components/knowledge/EntryViewer";
|
||||
import { EntryEditor } from "@/components/knowledge/EntryEditor";
|
||||
import { EntryMetadata } from "@/components/knowledge/EntryMetadata";
|
||||
import { VersionHistory } from "@/components/knowledge/VersionHistory";
|
||||
import { fetchEntry, updateEntry, deleteEntry, fetchTags } from "@/lib/api/knowledge";
|
||||
import { BacklinksList } from "@/components/knowledge/BacklinksList";
|
||||
import {
|
||||
fetchEntry,
|
||||
updateEntry,
|
||||
deleteEntry,
|
||||
fetchTags,
|
||||
fetchBacklinks,
|
||||
} from "@/lib/api/knowledge";
|
||||
|
||||
/**
|
||||
* Knowledge Entry Detail/Editor Page
|
||||
@@ -25,6 +32,11 @@ export default function EntryPage() {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Backlinks state
|
||||
const [backlinks, setBacklinks] = useState<any[]>([]);
|
||||
const [backlinksLoading, setBacklinksLoading] = useState(false);
|
||||
const [backlinksError, setBacklinksError] = useState<string | null>(null);
|
||||
|
||||
// Edit state
|
||||
const [editTitle, setEditTitle] = useState("");
|
||||
const [editContent, setEditContent] = useState("");
|
||||
@@ -56,6 +68,25 @@ export default function EntryPage() {
|
||||
void loadEntry();
|
||||
}, [slug]);
|
||||
|
||||
// Load backlinks
|
||||
useEffect(() => {
|
||||
async function loadBacklinks(): Promise<void> {
|
||||
try {
|
||||
setBacklinksLoading(true);
|
||||
setBacklinksError(null);
|
||||
const data = await fetchBacklinks(slug);
|
||||
setBacklinks(data.backlinks);
|
||||
} catch (err) {
|
||||
setBacklinksError(
|
||||
err instanceof Error ? err.message : "Failed to load backlinks"
|
||||
);
|
||||
} finally {
|
||||
setBacklinksLoading(false);
|
||||
}
|
||||
}
|
||||
void loadBacklinks();
|
||||
}, [slug]);
|
||||
|
||||
// Load available tags
|
||||
useEffect(() => {
|
||||
async function loadTags(): Promise<void> {
|
||||
@@ -324,7 +355,18 @@ export default function EntryPage() {
|
||||
{isEditing ? (
|
||||
<EntryEditor content={editContent} onChange={setEditContent} />
|
||||
) : activeTab === "content" ? (
|
||||
<EntryViewer entry={entry} />
|
||||
<>
|
||||
<EntryViewer entry={entry} />
|
||||
|
||||
{/* Backlinks Section */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<BacklinksList
|
||||
backlinks={backlinks}
|
||||
isLoading={backlinksLoading}
|
||||
error={backlinksError}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<VersionHistory slug={slug} onRestore={handleVersionRestore} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user