feat(notes): wire backend-backed note workspace slice
This commit is contained in:
parent
418ecaacc5
commit
5995b6c725
@ -24,6 +24,23 @@ type NoteListResponse = {
|
||||
items: NoteDoc[];
|
||||
};
|
||||
|
||||
type CreateNoteInput = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
title: string;
|
||||
body: string;
|
||||
tags?: string[];
|
||||
links?: string[];
|
||||
};
|
||||
|
||||
function generateNoteId(): string {
|
||||
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
||||
return globalThis.crypto.randomUUID();
|
||||
}
|
||||
|
||||
return `note-${Date.now()}`;
|
||||
}
|
||||
|
||||
function mapWorkspaceNames(workspaces: MobileWorkspace[]) {
|
||||
return new Map(workspaces.map((workspace) => [workspace.id, workspace.name]));
|
||||
}
|
||||
@ -59,6 +76,32 @@ export async function getNote(id: string, workspaceId: string): Promise<MobileNo
|
||||
return toMobileNote(note, mapWorkspaceNames(workspaces));
|
||||
}
|
||||
|
||||
export async function createNote(
|
||||
workspaceId: string,
|
||||
title: string,
|
||||
body: string,
|
||||
): Promise<MobileNote> {
|
||||
const trimmedTitle = title.trim() || 'Untitled draft';
|
||||
const payload: CreateNoteInput = {
|
||||
id: generateNoteId(),
|
||||
workspaceId,
|
||||
title: trimmedTitle,
|
||||
body,
|
||||
tags: [],
|
||||
links: [],
|
||||
};
|
||||
|
||||
const [created, workspaces] = await Promise.all([
|
||||
getApiClient().fetch<NoteDoc>('/notes', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
listWorkspaces(),
|
||||
]);
|
||||
|
||||
return toMobileNote(created, mapWorkspaceNames(workspaces));
|
||||
}
|
||||
|
||||
export async function updateNote(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
|
||||
@ -45,9 +45,13 @@ export default function CaptureScreen() {
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
saveDraft(title, body);
|
||||
setSaved(true);
|
||||
onPress={async () => {
|
||||
const didSave = await saveDraft(activeWorkspaceId, title, body);
|
||||
setSaved(didSave);
|
||||
if (!didSave) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTitle('');
|
||||
setBody('');
|
||||
}}
|
||||
@ -55,7 +59,7 @@ export default function CaptureScreen() {
|
||||
>
|
||||
<Text style={styles.buttonText}>Save draft</Text>
|
||||
</Pressable>
|
||||
{saved ? <Text style={styles.saved}>Draft saved locally in this scaffold step.</Text> : null}
|
||||
{saved ? <Text style={styles.saved}>Draft saved to the product backend.</Text> : null}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Offline queue readiness</Text>
|
||||
<Text style={styles.cardBody}>Queue capacity: {OFFLINE_QUEUE_MAX_SIZE} items</Text>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import { getNote, listNotes, updateNote as persistNote, type MobileNote } from '../api/notes';
|
||||
import { createNote as persistNewNote, getNote, listNotes, updateNote as persistNote, type MobileNote } from '../api/notes';
|
||||
|
||||
export type NotesState = {
|
||||
notes: MobileNote[];
|
||||
@ -7,7 +7,7 @@ export type NotesState = {
|
||||
isLoading: boolean;
|
||||
hydrate: () => Promise<void>;
|
||||
openNote: (id: string) => Promise<void>;
|
||||
saveDraft: (title: string, body: string) => void;
|
||||
saveDraft: (workspaceId: string | null, title: string, body: string) => Promise<boolean>;
|
||||
updateNote: (id: string, title: string, body: string) => Promise<void>;
|
||||
};
|
||||
|
||||
@ -31,18 +31,19 @@ export const useNotesStore = create<NotesState>((set, get) => ({
|
||||
const note = await getNote(id, current.workspaceId);
|
||||
set({ selectedNote: note, isLoading: false });
|
||||
},
|
||||
saveDraft(title: string, body: string) {
|
||||
const draft: MobileNote = {
|
||||
id: `draft-${Date.now()}`,
|
||||
workspaceId: 'drafts',
|
||||
title: title.trim() || 'Untitled draft',
|
||||
body,
|
||||
workspaceName: 'Drafts',
|
||||
status: 'draft',
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
async saveDraft(workspaceId: string | null, title: string, body: string) {
|
||||
if (!workspaceId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
set({ notes: [draft, ...get().notes], selectedNote: draft });
|
||||
set({ isLoading: true });
|
||||
const created = await persistNewNote(workspaceId, title, body);
|
||||
set({
|
||||
notes: [created, ...get().notes],
|
||||
selectedNote: created,
|
||||
isLoading: false,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
async updateNote(id: string, title: string, body: string) {
|
||||
const nextTitle = title.trim() || 'Untitled draft';
|
||||
|
||||
@ -3,29 +3,78 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { listNoteSummaries, listWorkspaceSummaries } from "@/lib/notes-client";
|
||||
import { mockOperatorWorkflows, mockSavedViews } from "@/lib/mock-data";
|
||||
import { listApprovalQueue } from "@/lib/review-client";
|
||||
import type { NoteSummary, WorkspaceSummary } from "@/lib/types";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [notes, setNotes] = useState<NoteSummary[]>([]);
|
||||
const [workspaces, setWorkspaces] = useState<WorkspaceSummary[]>([]);
|
||||
const [pendingReviewCount, setPendingReviewCount] = useState(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const [nextNotes, nextWorkspaces] = await Promise.all([
|
||||
const [nextNotes, nextWorkspaces, nextApprovalQueue] = await Promise.all([
|
||||
listNoteSummaries(),
|
||||
listWorkspaceSummaries(),
|
||||
listApprovalQueue(),
|
||||
]);
|
||||
setNotes(nextNotes);
|
||||
setWorkspaces(nextWorkspaces);
|
||||
setPendingReviewCount(nextApprovalQueue.length);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Unable to load dashboard data");
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const savedViews = [
|
||||
{
|
||||
id: "workspace-all",
|
||||
name: "All workspaces",
|
||||
scope: "workspace",
|
||||
description: "Current workspace inventory derived from backend-backed workspace data.",
|
||||
query: "visibility:any sort:updated",
|
||||
resultCount: workspaces.length,
|
||||
},
|
||||
{
|
||||
id: "draft-notes",
|
||||
name: "Draft notes",
|
||||
scope: "search",
|
||||
description: "Draft notes currently tracked across the active knowledge base.",
|
||||
query: "status:draft",
|
||||
resultCount: notes.filter((note) => note.status === "draft").length,
|
||||
},
|
||||
{
|
||||
id: "pending-review",
|
||||
name: "Pending review",
|
||||
scope: "review",
|
||||
description: "Current agent-mediated items awaiting review.",
|
||||
query: "state:draft|proposed",
|
||||
resultCount: pendingReviewCount,
|
||||
},
|
||||
];
|
||||
|
||||
const operatorWorkflows = [
|
||||
{
|
||||
id: "workflow-approvals",
|
||||
name: "Approval triage",
|
||||
owner: "Operator",
|
||||
queueCount: pendingReviewCount,
|
||||
sla: "< 4h",
|
||||
status: pendingReviewCount > 3 ? "at_risk" : "healthy",
|
||||
},
|
||||
{
|
||||
id: "workflow-workspaces",
|
||||
name: "Workspace coverage",
|
||||
owner: "Knowledge Ops",
|
||||
queueCount: workspaces.length,
|
||||
sla: "< 1d",
|
||||
status: workspaces.length > 5 ? "at_risk" : "healthy",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const recentNotes = notes.slice(0, 3);
|
||||
|
||||
return (
|
||||
@ -45,7 +94,7 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
<div className="surface-card" style={{ padding: "var(--ml-space-5)" }}>
|
||||
<div style={{ color: "var(--ml-text-secondary)" }}>Pending review surfaces</div>
|
||||
<div style={{ fontSize: "var(--ml-fs-3xl)", fontWeight: 700, marginTop: "var(--ml-space-2)" }}>2</div>
|
||||
<div style={{ fontSize: "var(--ml-fs-3xl)", fontWeight: 700, marginTop: "var(--ml-space-2)" }}>{pendingReviewCount}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -53,7 +102,7 @@ export default function DashboardPage() {
|
||||
<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 }}>Saved views</div>
|
||||
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
||||
{mockSavedViews.map((view) => (
|
||||
{savedViews.map((view) => (
|
||||
<article key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
|
||||
<strong>{view.name}</strong>
|
||||
@ -72,7 +121,7 @@ export default function DashboardPage() {
|
||||
<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 }}>Operator workflows</div>
|
||||
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
||||
{mockOperatorWorkflows.map((workflow) => (
|
||||
{operatorWorkflows.map((workflow) => (
|
||||
<article key={workflow.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
|
||||
<strong>{workflow.name}</strong>
|
||||
|
||||
@ -4,7 +4,6 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { AgentTimeline } from "@/components/AgentTimeline";
|
||||
import { ProposalReviewCard } from "@/components/ProposalReviewCard";
|
||||
import { mockOperatorWorkflows } from "@/lib/mock-data";
|
||||
import { listAgentTimeline, listApprovalQueue } from "@/lib/review-client";
|
||||
import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types";
|
||||
|
||||
@ -29,6 +28,24 @@ export default function ReviewsPage() {
|
||||
}, []);
|
||||
|
||||
const featuredProposal = useMemo(() => approvalQueue[0] ?? null, [approvalQueue]);
|
||||
const operatorWorkflows = [
|
||||
{
|
||||
id: "workflow-approvals",
|
||||
name: "Approval triage",
|
||||
owner: "Operator",
|
||||
queueCount: approvalQueue.length,
|
||||
sla: "< 4h",
|
||||
status: approvalQueue.length > 3 ? "at_risk" : "healthy",
|
||||
},
|
||||
{
|
||||
id: "workflow-agent-activity",
|
||||
name: "Agent activity review",
|
||||
owner: "Knowledge Ops",
|
||||
queueCount: timeline.length,
|
||||
sla: "< 1d",
|
||||
status: timeline.length > 6 ? "at_risk" : "healthy",
|
||||
},
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
@ -40,7 +57,7 @@ export default function ReviewsPage() {
|
||||
<aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
|
||||
<div style={{ fontWeight: 700 }}>Operator workflows</div>
|
||||
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
||||
{mockOperatorWorkflows.map((workflow) => (
|
||||
{operatorWorkflows.map((workflow) => (
|
||||
<div key={workflow.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
||||
<strong>{workflow.name}</strong>
|
||||
<span style={{ color: "var(--ml-text-secondary)" }}>Owner: {workflow.owner}</span>
|
||||
|
||||
@ -4,7 +4,6 @@ import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { listNoteSummaries } from "@/lib/notes-client";
|
||||
import { mockSavedViews } from "@/lib/mock-data";
|
||||
import type { NoteSummary } from "@/lib/types";
|
||||
|
||||
export default function SearchPage() {
|
||||
@ -35,6 +34,25 @@ export default function SearchPage() {
|
||||
);
|
||||
}, [notes, query]);
|
||||
|
||||
const savedViews = [
|
||||
{
|
||||
id: "search-launch-readiness",
|
||||
name: "Launch readiness",
|
||||
query: "tag:launch tag:mvp status:active",
|
||||
resultCount: notes.filter(
|
||||
(note) =>
|
||||
note.status === "active" &&
|
||||
note.tags.some((tag) => tag === "launch" || tag === "mvp")
|
||||
).length,
|
||||
},
|
||||
{
|
||||
id: "search-drafts",
|
||||
name: "Draft notes",
|
||||
query: "status:draft",
|
||||
resultCount: notes.filter((note) => note.status === "draft").length,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
title="Search"
|
||||
@ -45,15 +63,13 @@ export default function SearchPage() {
|
||||
<aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
|
||||
<div style={{ fontWeight: 700 }}>Saved searches</div>
|
||||
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
||||
{mockSavedViews
|
||||
.filter((view) => view.scope === "search")
|
||||
.map((view) => (
|
||||
<div key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
||||
<strong>{view.name}</strong>
|
||||
<span style={{ color: "var(--ml-text-secondary)" }}>{view.query}</span>
|
||||
<span className="badge">{view.resultCount} results</span>
|
||||
</div>
|
||||
))}
|
||||
{savedViews.map((view) => (
|
||||
<div key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
||||
<strong>{view.name}</strong>
|
||||
<span style={{ color: "var(--ml-text-secondary)" }}>{view.query}</span>
|
||||
<span className="badge">{view.resultCount} results</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "grid", gap: "var(--ml-space-2)" }}>
|
||||
|
||||
@ -4,7 +4,6 @@ import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { listNoteSummaries, listWorkspaceSummaries } from "@/lib/notes-client";
|
||||
import { mockSavedViews } from "@/lib/mock-data";
|
||||
import type { NoteSummary, WorkspaceSummary } from "@/lib/types";
|
||||
|
||||
export default function WorkspacesPage() {
|
||||
@ -38,6 +37,21 @@ export default function WorkspacesPage() {
|
||||
[notes, workspaces],
|
||||
);
|
||||
|
||||
const savedViews = [
|
||||
{
|
||||
id: "workspace-all",
|
||||
name: "All workspaces",
|
||||
description: "Current backend-backed workspace inventory.",
|
||||
resultCount: workspaces.length,
|
||||
},
|
||||
{
|
||||
id: "workspace-shared",
|
||||
name: "Shared workspaces",
|
||||
description: "Workspaces with more than one member.",
|
||||
resultCount: workspaces.filter((workspace) => workspace.visibility === "shared").length,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
title="Workspaces"
|
||||
@ -48,15 +62,13 @@ export default function WorkspacesPage() {
|
||||
<aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
|
||||
<div style={{ fontWeight: 700 }}>Saved views</div>
|
||||
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
|
||||
{mockSavedViews
|
||||
.filter((view) => view.scope === "workspace")
|
||||
.map((view) => (
|
||||
<div key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
||||
<strong>{view.name}</strong>
|
||||
<span style={{ color: "var(--ml-text-secondary)" }}>{view.description}</span>
|
||||
<span className="badge">{view.resultCount} results</span>
|
||||
</div>
|
||||
))}
|
||||
{savedViews.map((view) => (
|
||||
<div key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
|
||||
<strong>{view.name}</strong>
|
||||
<span style={{ color: "var(--ml-text-secondary)" }}>{view.description}</span>
|
||||
<span className="badge">{view.resultCount} results</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { createApiClient } from "@bytelyst/api-client";
|
||||
import { extractSuggestedTasks } from "@/lib/extraction-client";
|
||||
import { NOTES_API_URL, PRODUCT_ID } from "@/lib/product-config";
|
||||
import type { AgentTimelineItem, ArtifactSummary, NoteDetail, NoteSummary, NoteTask, WorkspaceSummary } from "@/lib/types";
|
||||
import type { AgentTimelineItem, ArtifactSummary, LinkedNote, NoteDetail, NoteSummary, NoteTask, WorkspaceSummary } from "@/lib/types";
|
||||
|
||||
type NoteDoc = {
|
||||
id: string;
|
||||
@ -78,6 +78,18 @@ type NoteAgentActionListResponse = {
|
||||
items: NoteAgentActionDoc[];
|
||||
};
|
||||
|
||||
type NoteRelationshipDoc = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
fromNoteId: string;
|
||||
toNoteId: string;
|
||||
relationshipType: string;
|
||||
};
|
||||
|
||||
type NoteRelationshipListResponse = {
|
||||
items: NoteRelationshipDoc[];
|
||||
};
|
||||
|
||||
function getAccessToken(): string | null {
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
@ -100,6 +112,10 @@ function buildWorkspaceMap(workspaces: WorkspaceDoc[]) {
|
||||
return new Map(workspaces.map((workspace) => [workspace.id, workspace]));
|
||||
}
|
||||
|
||||
function buildNoteMap(notes: NoteDoc[]) {
|
||||
return new Map(notes.map((note) => [note.id, note]));
|
||||
}
|
||||
|
||||
function toNoteSummary(note: NoteDoc): NoteSummary {
|
||||
return {
|
||||
id: note.id,
|
||||
@ -176,6 +192,38 @@ function toReviewState(
|
||||
return "none";
|
||||
}
|
||||
|
||||
function toLinkedNotes(
|
||||
noteId: string,
|
||||
relationships: NoteRelationshipDoc[],
|
||||
noteMap: Map<string, NoteDoc>,
|
||||
): LinkedNote[] {
|
||||
return relationships.flatMap((relationship) => {
|
||||
const relatedNoteId =
|
||||
relationship.fromNoteId === noteId
|
||||
? relationship.toNoteId
|
||||
: relationship.toNoteId === noteId
|
||||
? relationship.fromNoteId
|
||||
: null;
|
||||
|
||||
if (!relatedNoteId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const relatedNote = noteMap.get(relatedNoteId);
|
||||
if (!relatedNote) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: relatedNote.id,
|
||||
title: relatedNote.title,
|
||||
relationship: relationship.relationshipType,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
export async function listWorkspaceSummaries(): Promise<WorkspaceSummary[]> {
|
||||
const api = createNotesApiClient();
|
||||
const [workspaceResponse, noteResponse] = await Promise.all([
|
||||
@ -211,6 +259,7 @@ export async function getNoteDetail(noteId: string): Promise<NoteDetail | null>
|
||||
}
|
||||
|
||||
const workspaceMap = buildWorkspaceMap(workspaceResponse.items);
|
||||
const noteMap = buildNoteMap(noteResponse.items);
|
||||
const workspace = workspaceMap.get(note.workspaceId);
|
||||
const [taskResponse, artifactResponse, actionResponse] = await Promise.all([
|
||||
api.fetch<NoteTaskListResponse>(
|
||||
@ -223,6 +272,19 @@ export async function getNoteDetail(noteId: string): Promise<NoteDetail | null>
|
||||
`/note-agent-actions?workspaceId=${encodeURIComponent(note.workspaceId)}¬eId=${encodeURIComponent(note.id)}`
|
||||
),
|
||||
]);
|
||||
let relationshipResponse: NoteRelationshipListResponse = { items: [] };
|
||||
|
||||
try {
|
||||
const fetchedRelationships = await api.fetch<NoteRelationshipListResponse>(
|
||||
`/note-relationships?workspaceId=${encodeURIComponent(note.workspaceId)}¬eId=${encodeURIComponent(note.id)}`
|
||||
);
|
||||
relationshipResponse =
|
||||
fetchedRelationships && Array.isArray(fetchedRelationships.items)
|
||||
? fetchedRelationships
|
||||
: { items: [] };
|
||||
} catch {
|
||||
relationshipResponse = { items: [] };
|
||||
}
|
||||
|
||||
const extractedTasks = await extractSuggestedTasks(note.body).catch(() => []);
|
||||
const existingTaskTitles = new Set(taskResponse.items.map((task) => task.title.trim().toLowerCase()));
|
||||
@ -234,6 +296,7 @@ export async function getNoteDetail(noteId: string): Promise<NoteDetail | null>
|
||||
const timeline = actionResponse.items
|
||||
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
||||
.map(toTimelineItem);
|
||||
const linkedNotes = toLinkedNotes(note.id, relationshipResponse.items, noteMap);
|
||||
|
||||
return {
|
||||
...toNoteSummary(note),
|
||||
@ -245,7 +308,7 @@ export async function getNoteDetail(noteId: string): Promise<NoteDetail | null>
|
||||
taskCount: tasks.length,
|
||||
artifactCount: artifacts.length,
|
||||
},
|
||||
linkedNotes: [],
|
||||
linkedNotes,
|
||||
tasks,
|
||||
artifacts,
|
||||
timeline,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user