feat: enhance canvas and layout components with new features and improvements

- Added remote image patterns to the Next.js configuration for enhanced image handling.
- Updated TypeScript configuration to exclude the 'implement' directory.
- Refactored layout component to fetch initial authentication token and pass it to Providers.
- Replaced CanvasToolbar with CanvasSidebar for improved UI layout and functionality.
- Enhanced Canvas component with new drag-and-drop file upload capabilities and batch node movement.
- Updated various node components to support new status handling and improved user interactions.
- Added debounced saving for note and prompt nodes to optimize performance.
This commit is contained in:
Matthias
2026-03-25 17:58:58 +01:00
parent d1834c5694
commit ca40f5cb13
27 changed files with 1363 additions and 207 deletions

View File

@@ -15,6 +15,7 @@ import type * as edges from "../edges.js";
import type * as helpers from "../helpers.js";
import type * as http from "../http.js";
import type * as nodes from "../nodes.js";
import type * as storage from "../storage.js";
import type {
ApiFromModules,
@@ -30,6 +31,7 @@ declare const fullApi: ApiFromModules<{
helpers: typeof helpers;
http: typeof http;
nodes: typeof nodes;
storage: typeof storage;
}>;
/**

View File

@@ -17,10 +17,14 @@ export async function requireAuth(
): Promise<AuthUser> {
const user = await authComponent.safeGetAuthUser(ctx);
if (!user) {
console.error("[requireAuth] safeGetAuthUser returned null");
throw new Error("Unauthenticated");
}
const userId = user.userId ?? String(user._id);
if (!userId) {
console.error("[requireAuth] safeGetAuthUser returned user without userId", {
userRecordId: String(user._id),
});
throw new Error("Unauthenticated");
}
return { ...user, userId };

View File

@@ -22,6 +22,18 @@ async function getCanvasOrThrow(
return canvas;
}
async function getCanvasIfAuthorized(
ctx: QueryCtx | MutationCtx,
canvasId: Id<"canvases">,
userId: string
) {
const canvas = await ctx.db.get(canvasId);
if (!canvas || canvas.ownerId !== userId) {
return null;
}
return canvas;
}
// ============================================================================
// Queries
// ============================================================================
@@ -35,10 +47,29 @@ export const list = query({
const user = await requireAuth(ctx);
await getCanvasOrThrow(ctx, canvasId, user.userId);
return await ctx.db
const nodes = await ctx.db
.query("nodes")
.withIndex("by_canvas", (q) => q.eq("canvasId", canvasId))
.collect();
return Promise.all(
nodes.map(async (node) => {
const data = node.data as Record<string, unknown> | undefined;
if (!data?.storageId) {
return node;
}
const url = await ctx.storage.getUrl(data.storageId as Id<"_storage">);
return {
...node,
data: {
...data,
url: url ?? undefined,
},
};
})
);
},
});
@@ -52,7 +83,11 @@ export const get = query({
const node = await ctx.db.get(nodeId);
if (!node) return null;
await getCanvasOrThrow(ctx, node.canvasId, user.userId);
const canvas = await getCanvasIfAuthorized(ctx, node.canvasId, user.userId);
if (!canvas) {
return null;
}
return node;
},
});
@@ -67,7 +102,10 @@ export const listByType = query({
},
handler: async (ctx, { canvasId, type }) => {
const user = await requireAuth(ctx);
await getCanvasOrThrow(ctx, canvasId, user.userId);
const canvas = await getCanvasIfAuthorized(ctx, canvasId, user.userId);
if (!canvas) {
return [];
}
return await ctx.db
.query("nodes")

10
convex/storage.ts Normal file
View File

@@ -0,0 +1,10 @@
import { mutation } from "./_generated/server";
import { requireAuth } from "./helpers";
export const generateUploadUrl = mutation({
args: {},
handler: async (ctx) => {
await requireAuth(ctx);
return await ctx.storage.generateUploadUrl();
},
});