feat(web): wire knowledge pages to real API data #476
@@ -2,23 +2,25 @@
|
|||||||
|
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import type { KnowledgeEntryWithTags, KnowledgeTag } from "@mosaic/shared";
|
||||||
import type { EntryStatus } from "@mosaic/shared";
|
import type { EntryStatus } from "@mosaic/shared";
|
||||||
import { EntryList } from "@/components/knowledge/EntryList";
|
import { EntryList } from "@/components/knowledge/EntryList";
|
||||||
import { EntryFilters } from "@/components/knowledge/EntryFilters";
|
import { EntryFilters } from "@/components/knowledge/EntryFilters";
|
||||||
import { ImportExportActions } from "@/components/knowledge";
|
import { ImportExportActions } from "@/components/knowledge";
|
||||||
import { mockEntries, mockTags } from "@/lib/api/knowledge";
|
import { fetchEntries, fetchTags } from "@/lib/api/knowledge";
|
||||||
|
import type { EntriesResponse } from "@/lib/api/knowledge";
|
||||||
|
import { MosaicSpinner } from "@/components/ui/MosaicSpinner";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
export default function KnowledgePage(): ReactElement {
|
export default function KnowledgePage(): ReactElement {
|
||||||
// TODO: Replace with real API call when backend is ready
|
// Data state
|
||||||
// const { data: entries, isLoading } = useQuery({
|
const [entries, setEntries] = useState<KnowledgeEntryWithTags[]>([]);
|
||||||
// queryKey: ["knowledge-entries"],
|
const [tags, setTags] = useState<KnowledgeTag[]>([]);
|
||||||
// queryFn: fetchEntries,
|
const [totalEntries, setTotalEntries] = useState(0);
|
||||||
// });
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isLoading] = useState(false);
|
|
||||||
|
|
||||||
// Filter and sort state
|
// Filter and sort state
|
||||||
const [selectedStatus, setSelectedStatus] = useState<EntryStatus | "all">("all");
|
const [selectedStatus, setSelectedStatus] = useState<EntryStatus | "all">("all");
|
||||||
@@ -31,60 +33,65 @@ export default function KnowledgePage(): ReactElement {
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const itemsPerPage = 10;
|
const itemsPerPage = 10;
|
||||||
|
|
||||||
// Client-side filtering and sorting
|
// Load tags on mount
|
||||||
const filteredAndSortedEntries = useMemo(() => {
|
useEffect(() => {
|
||||||
let filtered = [...mockEntries];
|
let cancelled = false;
|
||||||
|
|
||||||
// Filter by status
|
fetchTags()
|
||||||
if (selectedStatus !== "all") {
|
.then((result) => {
|
||||||
filtered = filtered.filter((entry) => entry.status === selectedStatus);
|
if (!cancelled) {
|
||||||
}
|
setTags(result);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
console.error("Failed to load tags:", err);
|
||||||
|
});
|
||||||
|
|
||||||
// Filter by tag
|
return (): void => {
|
||||||
if (selectedTag !== "all") {
|
cancelled = true;
|
||||||
filtered = filtered.filter((entry) =>
|
};
|
||||||
entry.tags.some((tag: { slug: string }) => tag.slug === selectedTag)
|
}, []);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by search query
|
// Load entries when filters/sort/page change
|
||||||
if (searchQuery.trim()) {
|
const loadEntries = useCallback(async (): Promise<void> => {
|
||||||
const query = searchQuery.toLowerCase();
|
setIsLoading(true);
|
||||||
filtered = filtered.filter(
|
setError(null);
|
||||||
(entry) =>
|
|
||||||
entry.title.toLowerCase().includes(query) ||
|
|
||||||
(entry.summary?.toLowerCase().includes(query) ?? false) ||
|
|
||||||
entry.tags.some((tag: { name: string }): boolean =>
|
|
||||||
tag.name.toLowerCase().includes(query)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort entries
|
try {
|
||||||
filtered.sort((a, b) => {
|
const filters: Record<string, unknown> = {
|
||||||
let comparison = 0;
|
page: currentPage,
|
||||||
|
limit: itemsPerPage,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
};
|
||||||
|
|
||||||
if (sortBy === "title") {
|
if (selectedStatus !== "all") {
|
||||||
comparison = a.title.localeCompare(b.title);
|
filters.status = selectedStatus;
|
||||||
} else if (sortBy === "createdAt") {
|
}
|
||||||
comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
if (selectedTag !== "all") {
|
||||||
} else {
|
filters.tag = selectedTag;
|
||||||
// updatedAt
|
}
|
||||||
comparison = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
|
if (searchQuery.trim()) {
|
||||||
|
filters.search = searchQuery.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortOrder === "asc" ? comparison : -comparison;
|
const response: EntriesResponse = await fetchEntries(
|
||||||
});
|
filters as Parameters<typeof fetchEntries>[0]
|
||||||
|
);
|
||||||
|
setEntries(response.data);
|
||||||
|
setTotalEntries(response.meta?.total ?? response.data.length);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to load entries");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [currentPage, itemsPerPage, sortBy, sortOrder, selectedStatus, selectedTag, searchQuery]);
|
||||||
|
|
||||||
return filtered;
|
useEffect(() => {
|
||||||
}, [selectedStatus, selectedTag, searchQuery, sortBy, sortOrder]);
|
void loadEntries();
|
||||||
|
}, [loadEntries]);
|
||||||
|
|
||||||
// Pagination
|
const totalPages = Math.max(1, Math.ceil(totalEntries / itemsPerPage));
|
||||||
const totalPages = Math.ceil(filteredAndSortedEntries.length / itemsPerPage);
|
|
||||||
const paginatedEntries = filteredAndSortedEntries.slice(
|
|
||||||
(currentPage - 1) * itemsPerPage,
|
|
||||||
currentPage * itemsPerPage
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset to page 1 when filters change
|
// Reset to page 1 when filters change
|
||||||
const handleFilterChange = (callback: () => void): void => {
|
const handleFilterChange = (callback: () => void): void => {
|
||||||
@@ -101,6 +108,16 @@ export default function KnowledgePage(): ReactElement {
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isLoading && entries.length === 0) {
|
||||||
|
return (
|
||||||
|
<main className="container mx-auto px-4 py-8 max-w-5xl">
|
||||||
|
<div className="flex justify-center items-center py-20">
|
||||||
|
<MosaicSpinner size={48} label="Loading knowledge base..." />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="container mx-auto px-4 py-8 max-w-5xl">
|
<main className="container mx-auto px-4 py-8 max-w-5xl">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -125,14 +142,37 @@ export default function KnowledgePage(): ReactElement {
|
|||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<ImportExportActions
|
<ImportExportActions
|
||||||
onImportComplete={() => {
|
onImportComplete={() => {
|
||||||
// TODO: Refresh the entry list when real API is connected
|
void loadEntries();
|
||||||
// For now, this would trigger a refetch of the entries
|
|
||||||
window.location.reload();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Error state */}
|
||||||
|
{error && (
|
||||||
|
<div
|
||||||
|
className="mb-6 p-4 rounded-lg border"
|
||||||
|
style={{
|
||||||
|
borderColor: "var(--danger)",
|
||||||
|
background: "rgba(229,72,77,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="text-sm" style={{ color: "var(--danger)" }}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
void loadEntries();
|
||||||
|
}}
|
||||||
|
className="mt-2 text-sm font-medium underline"
|
||||||
|
style={{ color: "var(--danger)" }}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<EntryFilters
|
<EntryFilters
|
||||||
selectedStatus={selectedStatus}
|
selectedStatus={selectedStatus}
|
||||||
@@ -140,7 +180,7 @@ export default function KnowledgePage(): ReactElement {
|
|||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
sortOrder={sortOrder}
|
sortOrder={sortOrder}
|
||||||
tags={mockTags}
|
tags={tags}
|
||||||
onStatusChange={(status) => {
|
onStatusChange={(status) => {
|
||||||
handleFilterChange(() => {
|
handleFilterChange(() => {
|
||||||
setSelectedStatus(status);
|
setSelectedStatus(status);
|
||||||
@@ -161,7 +201,7 @@ export default function KnowledgePage(): ReactElement {
|
|||||||
|
|
||||||
{/* Entry list */}
|
{/* Entry list */}
|
||||||
<EntryList
|
<EntryList
|
||||||
entries={paginatedEntries}
|
entries={entries}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { KnowledgeEntryWithTags } from "@mosaic/shared";
|
import type { KnowledgeEntryWithTags } from "@mosaic/shared";
|
||||||
import { EntryCard } from "./EntryCard";
|
import { EntryCard } from "./EntryCard";
|
||||||
import { BookOpen } from "lucide-react";
|
import { BookOpen } from "lucide-react";
|
||||||
|
import { MosaicSpinner } from "@/components/ui/MosaicSpinner";
|
||||||
|
|
||||||
interface EntryListProps {
|
interface EntryListProps {
|
||||||
entries: KnowledgeEntryWithTags[];
|
entries: KnowledgeEntryWithTags[];
|
||||||
@@ -20,18 +21,22 @@ export function EntryList({
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center p-12">
|
<div className="flex justify-center items-center p-12">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
<MosaicSpinner size={36} label="Loading entries..." />
|
||||||
<span className="ml-3 text-gray-600">Loading entries...</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center p-12 bg-white rounded-lg shadow-sm border border-gray-200">
|
<div
|
||||||
<BookOpen className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
className="text-center p-12 rounded-lg border"
|
||||||
<p className="text-lg text-gray-700 font-medium">No entries found</p>
|
style={{ background: "var(--surface)", borderColor: "var(--border)" }}
|
||||||
<p className="text-sm text-gray-500 mt-2">
|
>
|
||||||
|
<BookOpen className="w-12 h-12 mx-auto mb-3" style={{ color: "var(--text-muted)" }} />
|
||||||
|
<p className="text-lg font-medium" style={{ color: "var(--text-muted)" }}>
|
||||||
|
No entries found
|
||||||
|
</p>
|
||||||
|
<p className="text-sm mt-2" style={{ color: "var(--text-muted)" }}>
|
||||||
Try adjusting your filters or create a new entry
|
Try adjusting your filters or create a new entry
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { fetchKnowledgeGraph } from "@/lib/api/knowledge";
|
import { fetchKnowledgeGraph } from "@/lib/api/knowledge";
|
||||||
|
import { MosaicSpinner } from "@/components/ui/MosaicSpinner";
|
||||||
import ELK from "elkjs/lib/elk.bundled.js";
|
import ELK from "elkjs/lib/elk.bundled.js";
|
||||||
|
|
||||||
// PDA-friendly status colors from CLAUDE.md
|
// PDA-friendly status colors from CLAUDE.md
|
||||||
@@ -376,10 +377,7 @@ export function KnowledgeGraphViewer({
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen">
|
<div className="flex items-center justify-center h-screen">
|
||||||
<div
|
<MosaicSpinner size={48} label="Loading knowledge graph..." />
|
||||||
data-testid="loading-spinner"
|
|
||||||
className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -387,11 +385,14 @@ export function KnowledgeGraphViewer({
|
|||||||
if (error || !graphData) {
|
if (error || !graphData) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-screen p-8">
|
<div className="flex flex-col items-center justify-center h-screen p-8">
|
||||||
<div className="text-red-500 text-xl font-semibold mb-2">Error Loading Graph</div>
|
<div className="text-xl font-semibold mb-2" style={{ color: "var(--danger)" }}>
|
||||||
|
Error Loading Graph
|
||||||
|
</div>
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">{error}</div>
|
<div className="text-sm text-gray-500 dark:text-gray-400">{error}</div>
|
||||||
<button
|
<button
|
||||||
onClick={loadGraph}
|
onClick={loadGraph}
|
||||||
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
className="mt-4 px-4 py-2 rounded text-white"
|
||||||
|
style={{ background: "var(--danger)" }}
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { fetchKnowledgeStats } from "@/lib/api/knowledge";
|
import { fetchKnowledgeStats } from "@/lib/api/knowledge";
|
||||||
|
import { MosaicSpinner } from "@/components/ui/MosaicSpinner";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
interface KnowledgeStats {
|
interface KnowledgeStats {
|
||||||
@@ -61,13 +62,20 @@ export function StatsDashboard(): React.JSX.Element {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center p-12">
|
<div className="flex items-center justify-center p-12">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
|
<MosaicSpinner size={36} label="Loading statistics..." />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !stats) {
|
if (error || !stats) {
|
||||||
return <div className="p-8 text-center text-red-500">Error loading statistics: {error}</div>;
|
return (
|
||||||
|
<div className="p-8 text-center">
|
||||||
|
<p className="font-medium mb-2" style={{ color: "var(--danger)" }}>
|
||||||
|
Error loading statistics
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">{error}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { overview, mostConnected, recentActivity, tagDistribution } = stats;
|
const { overview, mostConnected, recentActivity, tagDistribution } = stats;
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import type {
|
|||||||
KnowledgeTag,
|
KnowledgeTag,
|
||||||
KnowledgeEntryVersionWithAuthor,
|
KnowledgeEntryVersionWithAuthor,
|
||||||
PaginatedResponse,
|
PaginatedResponse,
|
||||||
|
EntryStatus,
|
||||||
|
Visibility,
|
||||||
} from "@mosaic/shared";
|
} from "@mosaic/shared";
|
||||||
import { EntryStatus, Visibility } from "@mosaic/shared";
|
|
||||||
import { apiGet, apiPost, apiPatch, apiDelete, type ApiResponse } from "./client";
|
import { apiGet, apiPost, apiPatch, apiDelete, type ApiResponse } from "./client";
|
||||||
|
|
||||||
export interface EntryFilters {
|
export interface EntryFilters {
|
||||||
@@ -370,241 +371,3 @@ export async function fetchKnowledgeGraph(filters?: {
|
|||||||
const endpoint = queryString ? `/api/knowledge/graph?${queryString}` : "/api/knowledge/graph";
|
const endpoint = queryString ? `/api/knowledge/graph?${queryString}` : "/api/knowledge/graph";
|
||||||
return apiGet(endpoint);
|
return apiGet(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock entries for development (until backend endpoints are ready)
|
|
||||||
*/
|
|
||||||
export const mockEntries: KnowledgeEntryWithTags[] = [
|
|
||||||
{
|
|
||||||
id: "entry-1",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
slug: "getting-started",
|
|
||||||
title: "Getting Started with Mosaic Stack",
|
|
||||||
content: "# Getting Started\n\nWelcome to Mosaic Stack...",
|
|
||||||
contentHtml: "<h1>Getting Started</h1><p>Welcome to Mosaic Stack...</p>",
|
|
||||||
summary: "A comprehensive guide to getting started with the Mosaic Stack platform.",
|
|
||||||
status: EntryStatus.PUBLISHED,
|
|
||||||
visibility: Visibility.PUBLIC,
|
|
||||||
createdBy: "user-1",
|
|
||||||
updatedBy: "user-1",
|
|
||||||
createdAt: new Date("2026-01-20"),
|
|
||||||
updatedAt: new Date("2026-01-28"),
|
|
||||||
tags: [
|
|
||||||
{
|
|
||||||
id: "tag-1",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "Tutorial",
|
|
||||||
slug: "tutorial",
|
|
||||||
color: "#3B82F6",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tag-2",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "Onboarding",
|
|
||||||
slug: "onboarding",
|
|
||||||
color: "#10B981",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "entry-2",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
slug: "architecture-overview",
|
|
||||||
title: "Architecture Overview",
|
|
||||||
content: "# Architecture\n\nThe Mosaic Stack architecture...",
|
|
||||||
contentHtml: "<h1>Architecture</h1><p>The Mosaic Stack architecture...</p>",
|
|
||||||
summary: "Overview of the system architecture and design patterns used in Mosaic Stack.",
|
|
||||||
status: EntryStatus.PUBLISHED,
|
|
||||||
visibility: Visibility.WORKSPACE,
|
|
||||||
createdBy: "user-1",
|
|
||||||
updatedBy: "user-1",
|
|
||||||
createdAt: new Date("2026-01-15"),
|
|
||||||
updatedAt: new Date("2026-01-27"),
|
|
||||||
tags: [
|
|
||||||
{
|
|
||||||
id: "tag-3",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "Architecture",
|
|
||||||
slug: "architecture",
|
|
||||||
color: "#8B5CF6",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tag-4",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "Technical",
|
|
||||||
slug: "technical",
|
|
||||||
color: "#F59E0B",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "entry-3",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
slug: "api-documentation-draft",
|
|
||||||
title: "API Documentation (Draft)",
|
|
||||||
content: "# API Docs\n\nWork in progress...",
|
|
||||||
contentHtml: "<h1>API Docs</h1><p>Work in progress...</p>",
|
|
||||||
summary: "Comprehensive API documentation for developers.",
|
|
||||||
status: EntryStatus.DRAFT,
|
|
||||||
visibility: Visibility.PRIVATE,
|
|
||||||
createdBy: "user-1",
|
|
||||||
updatedBy: "user-1",
|
|
||||||
createdAt: new Date("2026-01-29"),
|
|
||||||
updatedAt: new Date("2026-01-29"),
|
|
||||||
tags: [
|
|
||||||
{
|
|
||||||
id: "tag-4",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "Technical",
|
|
||||||
slug: "technical",
|
|
||||||
color: "#F59E0B",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tag-5",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "API",
|
|
||||||
slug: "api",
|
|
||||||
color: "#EF4444",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "entry-4",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
slug: "deployment-guide",
|
|
||||||
title: "Deployment Guide",
|
|
||||||
content: "# Deployment\n\nHow to deploy Mosaic Stack...",
|
|
||||||
contentHtml: "<h1>Deployment</h1><p>How to deploy Mosaic Stack...</p>",
|
|
||||||
summary: "Step-by-step guide for deploying Mosaic Stack to production.",
|
|
||||||
status: EntryStatus.PUBLISHED,
|
|
||||||
visibility: Visibility.WORKSPACE,
|
|
||||||
createdBy: "user-1",
|
|
||||||
updatedBy: "user-1",
|
|
||||||
createdAt: new Date("2026-01-18"),
|
|
||||||
updatedAt: new Date("2026-01-25"),
|
|
||||||
tags: [
|
|
||||||
{
|
|
||||||
id: "tag-6",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "DevOps",
|
|
||||||
slug: "devops",
|
|
||||||
color: "#14B8A6",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tag-1",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "Tutorial",
|
|
||||||
slug: "tutorial",
|
|
||||||
color: "#3B82F6",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "entry-5",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
slug: "old-meeting-notes",
|
|
||||||
title: "Q4 2025 Meeting Notes",
|
|
||||||
content: "# Meeting Notes\n\nOld archived notes...",
|
|
||||||
contentHtml: "<h1>Meeting Notes</h1><p>Old archived notes...</p>",
|
|
||||||
summary: "Meeting notes from Q4 2025 - archived for reference.",
|
|
||||||
status: EntryStatus.ARCHIVED,
|
|
||||||
visibility: Visibility.PRIVATE,
|
|
||||||
createdBy: "user-1",
|
|
||||||
updatedBy: "user-1",
|
|
||||||
createdAt: new Date("2025-12-15"),
|
|
||||||
updatedAt: new Date("2026-01-05"),
|
|
||||||
tags: [
|
|
||||||
{
|
|
||||||
id: "tag-7",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "Meetings",
|
|
||||||
slug: "meetings",
|
|
||||||
color: "#6B7280",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const mockTags: KnowledgeTag[] = [
|
|
||||||
{
|
|
||||||
id: "tag-1",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "Tutorial",
|
|
||||||
slug: "tutorial",
|
|
||||||
color: "#3B82F6",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tag-2",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "Onboarding",
|
|
||||||
slug: "onboarding",
|
|
||||||
color: "#10B981",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tag-3",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "Architecture",
|
|
||||||
slug: "architecture",
|
|
||||||
color: "#8B5CF6",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tag-4",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "Technical",
|
|
||||||
slug: "technical",
|
|
||||||
color: "#F59E0B",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tag-5",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "API",
|
|
||||||
slug: "api",
|
|
||||||
color: "#EF4444",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tag-6",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "DevOps",
|
|
||||||
slug: "devops",
|
|
||||||
color: "#14B8A6",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tag-7",
|
|
||||||
workspaceId: "workspace-1",
|
|
||||||
name: "Meetings",
|
|
||||||
slug: "meetings",
|
|
||||||
color: "#6B7280",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|||||||
Reference in New Issue
Block a user