feat(web): add knowledge entry editor page (KNOW-006)
This commit is contained in:
336
apps/web/src/app/(authenticated)/knowledge/[slug]/page.tsx
Normal file
336
apps/web/src/app/(authenticated)/knowledge/[slug]/page.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import type { KnowledgeEntryWithTags, KnowledgeTag } from "@mosaic/shared";
|
||||
import { EntryStatus, Visibility } from "@mosaic/shared";
|
||||
import { EntryViewer } from "@/components/knowledge/EntryViewer";
|
||||
import { EntryEditor } from "@/components/knowledge/EntryEditor";
|
||||
import { EntryMetadata } from "@/components/knowledge/EntryMetadata";
|
||||
import { fetchEntry, updateEntry, deleteEntry, fetchTags } from "@/lib/api/knowledge";
|
||||
|
||||
/**
|
||||
* Knowledge Entry Detail/Editor Page
|
||||
* View and edit mode for a single knowledge entry
|
||||
*/
|
||||
export default function EntryPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const slug = params.slug as string;
|
||||
|
||||
const [entry, setEntry] = useState<KnowledgeEntryWithTags | null>(null);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Edit state
|
||||
const [editTitle, setEditTitle] = useState("");
|
||||
const [editContent, setEditContent] = useState("");
|
||||
const [editStatus, setEditStatus] = useState<EntryStatus>(EntryStatus.DRAFT);
|
||||
const [editVisibility, setEditVisibility] = useState<Visibility>(Visibility.WORKSPACE);
|
||||
const [editTags, setEditTags] = useState<string[]>([]);
|
||||
const [availableTags, setAvailableTags] = useState<KnowledgeTag[]>([]);
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
|
||||
// Load entry data
|
||||
useEffect(() => {
|
||||
async function loadEntry(): Promise<void> {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await fetchEntry(slug);
|
||||
setEntry(data);
|
||||
setEditTitle(data.title);
|
||||
setEditContent(data.content);
|
||||
setEditStatus(data.status);
|
||||
setEditVisibility(data.visibility);
|
||||
setEditTags(data.tags.map((tag) => tag.id));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load entry");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
void loadEntry();
|
||||
}, [slug]);
|
||||
|
||||
// Load available tags
|
||||
useEffect(() => {
|
||||
async function loadTags(): Promise<void> {
|
||||
try {
|
||||
const tags = await fetchTags();
|
||||
setAvailableTags(tags);
|
||||
} catch (err) {
|
||||
console.error("Failed to load tags:", err);
|
||||
}
|
||||
}
|
||||
void loadTags();
|
||||
}, []);
|
||||
|
||||
// Track unsaved changes
|
||||
useEffect(() => {
|
||||
if (!entry || !isEditing) {
|
||||
setHasUnsavedChanges(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const changed =
|
||||
editTitle !== entry.title ||
|
||||
editContent !== entry.content ||
|
||||
editStatus !== entry.status ||
|
||||
editVisibility !== entry.visibility ||
|
||||
JSON.stringify(editTags.sort()) !==
|
||||
JSON.stringify(entry.tags.map((t) => t.id).sort());
|
||||
|
||||
setHasUnsavedChanges(changed);
|
||||
}, [entry, isEditing, editTitle, editContent, editStatus, editVisibility, editTags]);
|
||||
|
||||
// Warn before leaving with unsaved changes
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent): string => {
|
||||
if (hasUnsavedChanges) {
|
||||
e.preventDefault();
|
||||
return "You have unsaved changes. Are you sure you want to leave?";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
}, [hasUnsavedChanges]);
|
||||
|
||||
// Save changes
|
||||
const handleSave = useCallback(async (): Promise<void> => {
|
||||
if (!entry || isSaving || !editTitle.trim() || !editContent.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const updated = await updateEntry(slug, {
|
||||
title: editTitle.trim(),
|
||||
content: editContent.trim(),
|
||||
status: editStatus,
|
||||
visibility: editVisibility,
|
||||
tags: editTags,
|
||||
});
|
||||
|
||||
setEntry(updated);
|
||||
setHasUnsavedChanges(false);
|
||||
setIsEditing(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to save changes");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [entry, slug, editTitle, editContent, editStatus, editVisibility, editTags, isSaving]);
|
||||
|
||||
// Cmd+S / Ctrl+S to save
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent): void => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "s" && isEditing) {
|
||||
e.preventDefault();
|
||||
void handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [handleSave, isEditing]);
|
||||
|
||||
const handleEdit = (): void => {
|
||||
if (!entry) return;
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleCancel = (): void => {
|
||||
if (!entry) return;
|
||||
|
||||
if (
|
||||
!hasUnsavedChanges ||
|
||||
confirm("You have unsaved changes. Are you sure you want to cancel?")
|
||||
) {
|
||||
setEditTitle(entry.title);
|
||||
setEditContent(entry.content);
|
||||
setEditStatus(entry.status);
|
||||
setEditVisibility(entry.visibility);
|
||||
setEditTags(entry.tags.map((tag) => tag.id));
|
||||
setIsEditing(false);
|
||||
setHasUnsavedChanges(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (): Promise<void> => {
|
||||
if (
|
||||
!confirm(
|
||||
"Are you sure you want to delete this entry? It will be archived and can be restored later."
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteEntry(slug);
|
||||
router.push("/knowledge");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to delete entry");
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
|
||||
<div className="h-64 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && !entry) {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
|
||||
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
{isEditing ? (
|
||||
<div className="space-y-4">
|
||||
<EntryMetadata
|
||||
title={editTitle}
|
||||
status={editStatus}
|
||||
visibility={editVisibility}
|
||||
selectedTags={editTags}
|
||||
availableTags={availableTags}
|
||||
onTitleChange={setEditTitle}
|
||||
onStatusChange={setEditStatus}
|
||||
onVisibilityChange={setEditVisibility}
|
||||
onTagsChange={setEditTags}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{entry.title}
|
||||
</h1>
|
||||
<div className="mt-3 flex items-center gap-4 flex-wrap">
|
||||
{/* Status Badge */}
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||
entry.status === EntryStatus.PUBLISHED
|
||||
? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
|
||||
: entry.status === EntryStatus.DRAFT
|
||||
? "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200"
|
||||
: "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
{entry.status}
|
||||
</span>
|
||||
|
||||
{/* Visibility Badge */}
|
||||
<span className="px-3 py-1 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">
|
||||
{entry.visibility}
|
||||
</span>
|
||||
|
||||
{/* Tags */}
|
||||
{entry.tags.map((tag) => (
|
||||
<span
|
||||
key={tag.id}
|
||||
className="px-3 py-1 rounded-full text-xs font-medium bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300"
|
||||
style={tag.color ? { backgroundColor: tag.color } : undefined}
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
|
||||
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="mb-6">
|
||||
{isEditing ? (
|
||||
<EntryEditor content={editContent} onChange={setEditContent} />
|
||||
) : (
|
||||
<EntryViewer entry={entry} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-between items-center pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div>
|
||||
{isEditing && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={isSaving}
|
||||
className="px-4 py-2 text-sm font-medium text-red-700 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300 disabled:opacity-50"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
disabled={isSaving}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || !editTitle.trim() || !editContent.trim()}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{isSaving ? "Saving..." : "Save Changes"}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleEdit}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<p className="mt-4 text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||
Press <kbd className="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded">Cmd+S</kbd>{" "}
|
||||
or <kbd className="px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded">Ctrl+S</kbd> to
|
||||
save
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { createEntry, fetchTags } from "@/lib/api/knowledge";
|
||||
* New Knowledge Entry Page
|
||||
* Form for creating a new knowledge entry
|
||||
*/
|
||||
export default function NewEntryPage(): JSX.Element {
|
||||
export default function NewEntryPage() {
|
||||
const router = useRouter();
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
|
||||
@@ -10,7 +10,7 @@ interface EntryEditorProps {
|
||||
/**
|
||||
* EntryEditor - Markdown editor with live preview
|
||||
*/
|
||||
export function EntryEditor({ content, onChange }: EntryEditorProps): JSX.Element {
|
||||
export function EntryEditor({ content, onChange }: EntryEditorProps) {
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
return (
|
||||
|
||||
@@ -29,7 +29,7 @@ export function EntryMetadata({
|
||||
onStatusChange,
|
||||
onVisibilityChange,
|
||||
onTagsChange,
|
||||
}: EntryMetadataProps): JSX.Element {
|
||||
}: EntryMetadataProps) {
|
||||
const handleTagToggle = (tagId: string): void => {
|
||||
if (selectedTags.includes(tagId)) {
|
||||
onTagsChange(selectedTags.filter((id) => id !== tagId));
|
||||
|
||||
@@ -10,7 +10,7 @@ interface EntryViewerProps {
|
||||
/**
|
||||
* EntryViewer - Displays rendered markdown content
|
||||
*/
|
||||
export function EntryViewer({ entry }: EntryViewerProps): JSX.Element {
|
||||
export function EntryViewer({ entry }: EntryViewerProps) {
|
||||
return (
|
||||
<div className="entry-viewer">
|
||||
<div className="entry-content">
|
||||
|
||||
7
apps/web/src/components/knowledge/index.ts
Normal file
7
apps/web/src/components/knowledge/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Knowledge module components
|
||||
*/
|
||||
|
||||
export { EntryViewer } from "./EntryViewer";
|
||||
export { EntryEditor } from "./EntryEditor";
|
||||
export { EntryMetadata } from "./EntryMetadata";
|
||||
@@ -4,12 +4,14 @@
|
||||
*/
|
||||
|
||||
import type { KnowledgeEntryWithTags, KnowledgeTag } from "@mosaic/shared";
|
||||
import { EntryStatus } from "@mosaic/shared";
|
||||
import { apiGet, type ApiResponse } from "./client";
|
||||
import { EntryStatus, Visibility } from "@mosaic/shared";
|
||||
import { apiGet, apiPost, apiPatch, apiDelete, type ApiResponse } from "./client";
|
||||
|
||||
export interface EntryFilters {
|
||||
status?: EntryStatus;
|
||||
visibility?: Visibility;
|
||||
tag?: string;
|
||||
tags?: string[];
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
@@ -26,6 +28,24 @@ export interface EntriesResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateEntryData {
|
||||
title: string;
|
||||
content: string;
|
||||
summary?: string;
|
||||
status?: EntryStatus;
|
||||
visibility?: Visibility;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateEntryData {
|
||||
title?: string;
|
||||
content?: string;
|
||||
summary?: string;
|
||||
status?: EntryStatus;
|
||||
visibility?: Visibility;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch knowledge entries with optional filters
|
||||
*/
|
||||
@@ -35,23 +55,71 @@ export async function fetchEntries(filters?: EntryFilters): Promise<EntriesRespo
|
||||
if (filters?.status) {
|
||||
params.append("status", filters.status);
|
||||
}
|
||||
if (filters?.visibility) {
|
||||
params.append("visibility", filters.visibility);
|
||||
}
|
||||
if (filters?.tag) {
|
||||
params.append("tag", filters.tag);
|
||||
}
|
||||
if (filters?.tags && filters.tags.length > 0) {
|
||||
filters.tags.forEach((tag) => params.append("tags", tag));
|
||||
}
|
||||
if (filters?.page) {
|
||||
params.append("page", filters.page.toString());
|
||||
}
|
||||
if (filters?.limit) {
|
||||
params.append("limit", filters.limit.toString());
|
||||
}
|
||||
if (filters?.search) {
|
||||
params.append("search", filters.search);
|
||||
}
|
||||
if (filters?.sortBy) {
|
||||
params.append("sortBy", filters.sortBy);
|
||||
}
|
||||
if (filters?.sortOrder) {
|
||||
params.append("sortOrder", filters.sortOrder);
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const endpoint = queryString ? `/api/knowledge/entries?${queryString}` : "/api/knowledge/entries";
|
||||
const endpoint = queryString
|
||||
? `/api/knowledge/entries?${queryString}`
|
||||
: "/api/knowledge/entries";
|
||||
|
||||
const response = await apiGet<EntriesResponse>(endpoint);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single knowledge entry by slug
|
||||
*/
|
||||
export async function fetchEntry(slug: string): Promise<KnowledgeEntryWithTags> {
|
||||
return apiGet<KnowledgeEntryWithTags>(`/api/knowledge/entries/${slug}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new knowledge entry
|
||||
*/
|
||||
export async function createEntry(data: CreateEntryData): Promise<KnowledgeEntryWithTags> {
|
||||
return apiPost<KnowledgeEntryWithTags>("/api/knowledge/entries", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing knowledge entry
|
||||
*/
|
||||
export async function updateEntry(
|
||||
slug: string,
|
||||
data: UpdateEntryData
|
||||
): Promise<KnowledgeEntryWithTags> {
|
||||
return apiPatch<KnowledgeEntryWithTags>(`/api/knowledge/entries/${slug}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete (archive) a knowledge entry
|
||||
*/
|
||||
export async function deleteEntry(slug: string): Promise<void> {
|
||||
await apiDelete(`/api/knowledge/entries/${slug}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all knowledge tags
|
||||
*/
|
||||
@@ -73,14 +141,30 @@ export const mockEntries: KnowledgeEntryWithTags[] = [
|
||||
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: "PUBLIC" as const,
|
||||
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: "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(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -92,14 +176,30 @@ export const mockEntries: KnowledgeEntryWithTags[] = [
|
||||
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: "WORKSPACE" as const,
|
||||
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: "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(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -111,14 +211,30 @@ export const mockEntries: KnowledgeEntryWithTags[] = [
|
||||
contentHtml: "<h1>API Docs</h1><p>Work in progress...</p>",
|
||||
summary: "Comprehensive API documentation for developers.",
|
||||
status: EntryStatus.DRAFT,
|
||||
visibility: "PRIVATE" as const,
|
||||
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: "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(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -130,14 +246,30 @@ export const mockEntries: KnowledgeEntryWithTags[] = [
|
||||
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: "WORKSPACE" as const,
|
||||
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: "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(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -149,23 +281,87 @@ export const mockEntries: KnowledgeEntryWithTags[] = [
|
||||
contentHtml: "<h1>Meeting Notes</h1><p>Old archived notes...</p>",
|
||||
summary: "Meeting notes from Q4 2025 - archived for reference.",
|
||||
status: EntryStatus.ARCHIVED,
|
||||
visibility: "PRIVATE" as const,
|
||||
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() },
|
||||
{
|
||||
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() },
|
||||
{
|
||||
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(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -32,6 +32,12 @@ export enum WorkspaceMemberRole {
|
||||
GUEST = "GUEST",
|
||||
}
|
||||
|
||||
export enum TeamMemberRole {
|
||||
OWNER = "OWNER",
|
||||
ADMIN = "ADMIN",
|
||||
MEMBER = "MEMBER",
|
||||
}
|
||||
|
||||
export enum ActivityAction {
|
||||
CREATED = "CREATED",
|
||||
UPDATED = "UPDATED",
|
||||
|
||||
Reference in New Issue
Block a user