feat(web): align notes runtime with backend
This commit is contained in:
parent
a6f614f43a
commit
8340b1d489
@ -1,8 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { AppShell } from "@/components/AppShell";
|
import { AppShell } from "@/components/AppShell";
|
||||||
import { mockNotes, mockOperatorWorkflows, mockSavedViews, mockWorkspaces } from "@/lib/mock-data";
|
import { listNoteSummaries, listWorkspaceSummaries } from "@/lib/notes-client";
|
||||||
|
import { mockOperatorWorkflows, mockSavedViews } from "@/lib/mock-data";
|
||||||
|
import type { NoteSummary, WorkspaceSummary } from "@/lib/types";
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const recentNotes = mockNotes.slice(0, 3);
|
const [notes, setNotes] = useState<NoteSummary[]>([]);
|
||||||
|
const [workspaces, setWorkspaces] = useState<WorkspaceSummary[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const [nextNotes, nextWorkspaces] = await Promise.all([
|
||||||
|
listNoteSummaries(),
|
||||||
|
listWorkspaceSummaries(),
|
||||||
|
]);
|
||||||
|
setNotes(nextNotes);
|
||||||
|
setWorkspaces(nextWorkspaces);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Unable to load dashboard data");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const recentNotes = notes.slice(0, 3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
@ -13,11 +37,11 @@ export default function DashboardPage() {
|
|||||||
<section style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: "var(--ml-space-4)" }}>
|
<section style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: "var(--ml-space-4)" }}>
|
||||||
<div className="surface-card" style={{ padding: "var(--ml-space-5)" }}>
|
<div className="surface-card" style={{ padding: "var(--ml-space-5)" }}>
|
||||||
<div style={{ color: "var(--ml-text-secondary)" }}>Active workspaces</div>
|
<div style={{ color: "var(--ml-text-secondary)" }}>Active workspaces</div>
|
||||||
<div style={{ fontSize: "var(--ml-fs-3xl)", fontWeight: 700, marginTop: "var(--ml-space-2)" }}>{mockWorkspaces.length}</div>
|
<div style={{ fontSize: "var(--ml-fs-3xl)", fontWeight: 700, marginTop: "var(--ml-space-2)" }}>{workspaces.length}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="surface-card" style={{ padding: "var(--ml-space-5)" }}>
|
<div className="surface-card" style={{ padding: "var(--ml-space-5)" }}>
|
||||||
<div style={{ color: "var(--ml-text-secondary)" }}>Tracked notes</div>
|
<div style={{ color: "var(--ml-text-secondary)" }}>Tracked notes</div>
|
||||||
<div style={{ fontSize: "var(--ml-fs-3xl)", fontWeight: 700, marginTop: "var(--ml-space-2)" }}>{mockNotes.length}</div>
|
<div style={{ fontSize: "var(--ml-fs-3xl)", fontWeight: 700, marginTop: "var(--ml-space-2)" }}>{notes.length}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="surface-card" style={{ padding: "var(--ml-space-5)" }}>
|
<div className="surface-card" style={{ padding: "var(--ml-space-5)" }}>
|
||||||
<div style={{ color: "var(--ml-text-secondary)" }}>Pending review surfaces</div>
|
<div style={{ color: "var(--ml-text-secondary)" }}>Pending review surfaces</div>
|
||||||
@ -67,6 +91,7 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
|
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
|
||||||
<div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>Recent note activity</div>
|
<div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>Recent note activity</div>
|
||||||
|
{error ? <div style={{ color: "var(--ml-text-secondary)" }}>{error}</div> : null}
|
||||||
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
||||||
{recentNotes.map((note) => (
|
{recentNotes.map((note) => (
|
||||||
<article key={note.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
<article key={note.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
import { notFound } from "next/navigation";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
import { AppShell } from "@/components/AppShell";
|
import { AppShell } from "@/components/AppShell";
|
||||||
import { NoteEditor } from "@/components/NoteEditor";
|
import { NoteEditor } from "@/components/NoteEditor";
|
||||||
import { MetadataPanel } from "@/components/MetadataPanel";
|
import { MetadataPanel } from "@/components/MetadataPanel";
|
||||||
@ -6,19 +9,38 @@ import { LinkedNotesPanel } from "@/components/LinkedNotesPanel";
|
|||||||
import { TaskReviewPanel } from "@/components/TaskReviewPanel";
|
import { TaskReviewPanel } from "@/components/TaskReviewPanel";
|
||||||
import { ArtifactPanel } from "@/components/ArtifactPanel";
|
import { ArtifactPanel } from "@/components/ArtifactPanel";
|
||||||
import { AgentTimeline } from "@/components/AgentTimeline";
|
import { AgentTimeline } from "@/components/AgentTimeline";
|
||||||
import { getNoteById } from "@/lib/mock-data";
|
import { getNoteDetail } from "@/lib/notes-client";
|
||||||
import { mockAgentTimeline } from "@/lib/review-data";
|
import { mockAgentTimeline } from "@/lib/review-data";
|
||||||
|
import type { NoteDetail } from "@/lib/types";
|
||||||
|
|
||||||
export default async function NoteDetailPage({
|
export default function NoteDetailPage() {
|
||||||
params,
|
const params = useParams<{ noteId: string }>();
|
||||||
}: {
|
const noteId = params.noteId;
|
||||||
params: Promise<{ noteId: string }>;
|
const [note, setNote] = useState<NoteDetail | null>(null);
|
||||||
}) {
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { noteId } = await params;
|
|
||||||
const note = getNoteById(noteId);
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
setNote(await getNoteDetail(noteId));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Unable to load note");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [noteId]);
|
||||||
|
|
||||||
if (!note) {
|
if (!note) {
|
||||||
notFound();
|
return (
|
||||||
|
<AppShell
|
||||||
|
title="Note detail"
|
||||||
|
description="Editable note surface with metadata, linked context, tasks, and artifact placeholders."
|
||||||
|
actions={<div className="badge">Loading</div>}
|
||||||
|
>
|
||||||
|
<section className="surface-card" style={{ padding: "var(--ml-space-6)", color: "var(--ml-text-secondary)" }}>
|
||||||
|
{error ?? "Loading note…"}
|
||||||
|
</section>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,8 +1,40 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { AppShell } from "@/components/AppShell";
|
import { AppShell } from "@/components/AppShell";
|
||||||
import { mockNotes, mockSavedViews } from "@/lib/mock-data";
|
import { listNoteSummaries } from "@/lib/notes-client";
|
||||||
|
import { mockSavedViews } from "@/lib/mock-data";
|
||||||
|
import type { NoteSummary } from "@/lib/types";
|
||||||
|
|
||||||
export default function SearchPage() {
|
export default function SearchPage() {
|
||||||
|
const [notes, setNotes] = useState<NoteSummary[]>([]);
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
setNotes(await listNoteSummaries());
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Unable to load notes");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredNotes = useMemo(() => {
|
||||||
|
const normalized = query.trim().toLowerCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return notes.filter((note) =>
|
||||||
|
note.title.toLowerCase().includes(normalized) ||
|
||||||
|
note.excerpt.toLowerCase().includes(normalized) ||
|
||||||
|
note.tags.some((tag) => tag.toLowerCase().includes(normalized)),
|
||||||
|
);
|
||||||
|
}, [notes, query]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
title="Search"
|
title="Search"
|
||||||
@ -38,6 +70,8 @@ export default function SearchPage() {
|
|||||||
aria-label="Search notes"
|
aria-label="Search notes"
|
||||||
className="input-shell"
|
className="input-shell"
|
||||||
placeholder="Search notes, tags, tasks, and linked context"
|
placeholder="Search notes, tags, tasks, and linked context"
|
||||||
|
value={query}
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: "var(--ml-space-2)", flexWrap: "wrap" }}>
|
||||||
<span className="badge">workspace:all</span>
|
<span className="badge">workspace:all</span>
|
||||||
@ -45,8 +79,9 @@ export default function SearchPage() {
|
|||||||
<span className="badge">source:manual+agent</span>
|
<span className="badge">source:manual+agent</span>
|
||||||
<span className="badge">matched:title+tags</span>
|
<span className="badge">matched:title+tags</span>
|
||||||
</div>
|
</div>
|
||||||
|
{error ? <div style={{ color: "var(--ml-text-secondary)" }}>{error}</div> : null}
|
||||||
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
||||||
{mockNotes.map((note) => (
|
{filteredNotes.map((note) => (
|
||||||
<Link key={note.id} href={`/notes/${note.id}`} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
<Link key={note.id} href={`/notes/${note.id}`} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.5fr) repeat(3, minmax(100px, auto))", gap: "var(--ml-space-3)", alignItems: "start" }}>
|
<div style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.5fr) repeat(3, minmax(100px, auto))", gap: "var(--ml-space-3)", alignItems: "start" }}>
|
||||||
<div style={{ display: "grid", gap: "var(--ml-space-2)" }}>
|
<div style={{ display: "grid", gap: "var(--ml-space-2)" }}>
|
||||||
|
|||||||
@ -1,8 +1,43 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { AppShell } from "@/components/AppShell";
|
import { AppShell } from "@/components/AppShell";
|
||||||
import { getNotesForWorkspace, mockSavedViews, mockWorkspaces } from "@/lib/mock-data";
|
import { listNoteSummaries, listWorkspaceSummaries } from "@/lib/notes-client";
|
||||||
|
import { mockSavedViews } from "@/lib/mock-data";
|
||||||
|
import type { NoteSummary, WorkspaceSummary } from "@/lib/types";
|
||||||
|
|
||||||
export default function WorkspacesPage() {
|
export default function WorkspacesPage() {
|
||||||
|
const [notes, setNotes] = useState<NoteSummary[]>([]);
|
||||||
|
const [workspaces, setWorkspaces] = useState<WorkspaceSummary[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const [nextNotes, nextWorkspaces] = await Promise.all([
|
||||||
|
listNoteSummaries(),
|
||||||
|
listWorkspaceSummaries(),
|
||||||
|
]);
|
||||||
|
setNotes(nextNotes);
|
||||||
|
setWorkspaces(nextWorkspaces);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Unable to load workspaces");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const notesByWorkspace = useMemo(
|
||||||
|
() =>
|
||||||
|
new Map(
|
||||||
|
workspaces.map((workspace) => [
|
||||||
|
workspace.id,
|
||||||
|
notes.filter((note) => note.workspaceId === workspace.id),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
[notes, workspaces],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
title="Workspaces"
|
title="Workspaces"
|
||||||
@ -41,9 +76,14 @@ export default function WorkspacesPage() {
|
|||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<section className="surface-card" style={{ padding: "var(--ml-space-6)", color: "var(--ml-text-secondary)" }}>
|
||||||
|
{error}
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
<section style={{ display: "grid", gap: "var(--ml-space-4)" }}>
|
<section style={{ display: "grid", gap: "var(--ml-space-4)" }}>
|
||||||
{mockWorkspaces.map((workspace) => {
|
{workspaces.map((workspace) => {
|
||||||
const notes = getNotesForWorkspace(workspace.id);
|
const workspaceNotes = notesByWorkspace.get(workspace.id) ?? [];
|
||||||
return (
|
return (
|
||||||
<article key={workspace.id} className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
|
<article key={workspace.id} className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-4)", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-4)", flexWrap: "wrap" }}>
|
||||||
@ -62,7 +102,7 @@ export default function WorkspacesPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
||||||
{notes.map((note) => (
|
{workspaceNotes.map((note) => (
|
||||||
<Link key={note.id} href={`/notes/${note.id}`} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
<Link key={note.id} href={`/notes/${note.id}`} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
||||||
<strong>{note.title}</strong>
|
<strong>{note.title}</strong>
|
||||||
<span style={{ color: "var(--ml-text-secondary)" }}>{note.excerpt}</span>
|
<span style={{ color: "var(--ml-text-secondary)" }}>{note.excerpt}</span>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { createAuthProvider } from "@bytelyst/react-auth";
|
import { createAuthProvider } from "@bytelyst/react-auth";
|
||||||
import type { ProductUser } from "@/lib/types";
|
import type { ProductUser } from "@/lib/types";
|
||||||
import { PRODUCT_ID } from "@/lib/product-config";
|
import { PLATFORM_SERVICE_URL, PRODUCT_ID } from "@/lib/product-config";
|
||||||
|
|
||||||
interface LoginResponse {
|
interface LoginResponse {
|
||||||
user?: ProductUser;
|
user?: ProductUser;
|
||||||
@ -10,14 +10,8 @@ interface LoginResponse {
|
|||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const demoUser: ProductUser = {
|
|
||||||
email: "operator@bytelyst.dev",
|
|
||||||
name: "ByteLyst Operator",
|
|
||||||
role: "admin",
|
|
||||||
workspaceId: "workspace-product",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const { AuthProvider, useAuth } = createAuthProvider<ProductUser>({
|
export const { AuthProvider, useAuth } = createAuthProvider<ProductUser>({
|
||||||
|
baseUrl: PLATFORM_SERVICE_URL,
|
||||||
storagePrefix: PRODUCT_ID,
|
storagePrefix: PRODUCT_ID,
|
||||||
loginEndpoint: "/auth/login",
|
loginEndpoint: "/auth/login",
|
||||||
registerEndpoint: "/auth/register",
|
registerEndpoint: "/auth/register",
|
||||||
@ -28,25 +22,9 @@ export const { AuthProvider, useAuth } = createAuthProvider<ProductUser>({
|
|||||||
mapLoginResponse: (data: unknown) => {
|
mapLoginResponse: (data: unknown) => {
|
||||||
const result = data as LoginResponse;
|
const result = data as LoginResponse;
|
||||||
return {
|
return {
|
||||||
user: result.user ?? demoUser,
|
user: result.user as ProductUser,
|
||||||
accessToken: result.accessToken ?? "demo-access-token",
|
accessToken: result.accessToken ?? "",
|
||||||
refreshToken: result.refreshToken ?? "demo-refresh-token",
|
refreshToken: result.refreshToken ?? "",
|
||||||
};
|
|
||||||
},
|
|
||||||
onLoginFallback: async () => ({
|
|
||||||
user: demoUser,
|
|
||||||
accessToken: "demo-access-token",
|
|
||||||
refreshToken: "demo-refresh-token",
|
|
||||||
}),
|
|
||||||
onInit: () => {
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: demoUser,
|
|
||||||
accessToken: "demo-access-token",
|
|
||||||
refreshToken: "demo-refresh-token",
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
137
web/src/lib/notes-client.ts
Normal file
137
web/src/lib/notes-client.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { createApiClient } from "@bytelyst/api-client";
|
||||||
|
import { NOTES_API_URL, PRODUCT_ID } from "@/lib/product-config";
|
||||||
|
import type { NoteDetail, NoteSummary, WorkspaceSummary } from "@/lib/types";
|
||||||
|
|
||||||
|
type NoteDoc = {
|
||||||
|
id: string;
|
||||||
|
workspaceId: string;
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
status: "draft" | "active" | "archived";
|
||||||
|
tags: string[];
|
||||||
|
updatedAt: string;
|
||||||
|
updatedBy: string;
|
||||||
|
createdBy: string;
|
||||||
|
sourceType?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkspaceDoc = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
members: Array<{ userId: string; role: string }>;
|
||||||
|
updatedAt: string;
|
||||||
|
updatedBy: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NoteListResponse = {
|
||||||
|
items: NoteDoc[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkspaceListResponse = {
|
||||||
|
items: WorkspaceDoc[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAccessToken(): string | null {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return localStorage.getItem(`${PRODUCT_ID}_access_token`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNotesApiClient() {
|
||||||
|
return createApiClient({
|
||||||
|
baseUrl: NOTES_API_URL,
|
||||||
|
getToken: getAccessToken,
|
||||||
|
defaultHeaders: {
|
||||||
|
"x-product-id": PRODUCT_ID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorkspaceMap(workspaces: WorkspaceDoc[]) {
|
||||||
|
return new Map(workspaces.map((workspace) => [workspace.id, workspace]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNoteSummary(note: NoteDoc): NoteSummary {
|
||||||
|
return {
|
||||||
|
id: note.id,
|
||||||
|
workspaceId: note.workspaceId,
|
||||||
|
title: note.title,
|
||||||
|
excerpt: note.body.slice(0, 160),
|
||||||
|
status: note.status,
|
||||||
|
tags: note.tags,
|
||||||
|
updatedAt: note.updatedAt,
|
||||||
|
updatedBy: note.updatedBy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toWorkspaceSummary(workspace: WorkspaceDoc, notes: NoteDoc[]): WorkspaceSummary {
|
||||||
|
const owner = workspace.members.find((member) => member.role === "owner")?.userId ?? workspace.updatedBy;
|
||||||
|
const noteCount = notes.filter((note) => note.workspaceId === workspace.id).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: workspace.id,
|
||||||
|
name: workspace.name,
|
||||||
|
description: workspace.description ?? "",
|
||||||
|
owner,
|
||||||
|
noteCount,
|
||||||
|
visibility: workspace.members.length > 1 ? "shared" : "private",
|
||||||
|
updatedAt: workspace.updatedAt,
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listWorkspaceSummaries(): Promise<WorkspaceSummary[]> {
|
||||||
|
const api = createNotesApiClient();
|
||||||
|
const [workspaceResponse, noteResponse] = await Promise.all([
|
||||||
|
api.fetch<WorkspaceListResponse>("/workspaces"),
|
||||||
|
api.fetch<NoteListResponse>("/notes"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return workspaceResponse.items.map((workspace) => toWorkspaceSummary(workspace, noteResponse.items));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listNoteSummaries(): Promise<NoteSummary[]> {
|
||||||
|
const api = createNotesApiClient();
|
||||||
|
const response = await api.fetch<NoteListResponse>("/notes");
|
||||||
|
return response.items.map(toNoteSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listNotesForWorkspace(workspaceId: string): Promise<NoteSummary[]> {
|
||||||
|
const api = createNotesApiClient();
|
||||||
|
const response = await api.fetch<NoteListResponse>(`/notes?workspaceId=${encodeURIComponent(workspaceId)}`);
|
||||||
|
return response.items.map(toNoteSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNoteDetail(noteId: string): Promise<NoteDetail | null> {
|
||||||
|
const api = createNotesApiClient();
|
||||||
|
const [workspaceResponse, noteResponse] = await Promise.all([
|
||||||
|
api.fetch<WorkspaceListResponse>("/workspaces"),
|
||||||
|
api.fetch<NoteListResponse>("/notes"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const note = noteResponse.items.find((item) => item.id === noteId);
|
||||||
|
if (!note) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceMap = buildWorkspaceMap(workspaceResponse.items);
|
||||||
|
const workspace = workspaceMap.get(note.workspaceId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toNoteSummary(note),
|
||||||
|
body: note.body,
|
||||||
|
metadata: {
|
||||||
|
owner: workspace?.members.find((member) => member.role === "owner")?.userId ?? note.updatedBy,
|
||||||
|
source: note.sourceType ?? "manual",
|
||||||
|
reviewState: "none",
|
||||||
|
taskCount: 0,
|
||||||
|
artifactCount: 0,
|
||||||
|
},
|
||||||
|
linkedNotes: [],
|
||||||
|
tasks: [],
|
||||||
|
artifacts: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user