Canvas: build application infrastructure with drag and drop
Change-Id: I5cfd12e67794f3376c5c025af29470d52d77cf16
diff --git a/apps/canvas/src/Messages.tsx b/apps/canvas/src/Messages.tsx
new file mode 100644
index 0000000..94cd42e
--- /dev/null
+++ b/apps/canvas/src/Messages.tsx
@@ -0,0 +1,58 @@
+import { Button } from "./components/ui/button";
+import { AppNode, AppState, Message, nodeLabel, useMessages, useStateStore } from "./lib/state";
+import { useCallback, useEffect, useState } from "react";
+import { useNodes } from "@xyflow/react";
+import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./components/ui/accordion";
+import { Badge } from "./components/ui/badge";
+
+export function Messages() {
+ const store = useStateStore();
+ const nodes = useNodes<AppNode>();
+ const [nodeMap, setNodeMap] = useState<Map<string, AppNode>>();
+ useEffect(() => {
+ setNodeMap(new Map(nodes.map((n) => [n.id, n])));
+ }, [nodes, setNodeMap]);
+ const onClick = useCallback((fn?: (state: AppState) => void) => {
+ return () => {
+ if (fn) {
+ fn(store);
+ }
+ };
+ }, [store]);
+ const messages = useMessages();
+ const [grouped, setGrouped] = useState<Map<string, Message[]>>(new Map());
+ useEffect(() => {
+ const g = new Map<string, Message[]>();
+ messages.forEach((m) => {
+ const id = m.nodeId || "global";
+ const existing: Message[] = g.get(id) || [];
+ existing.push(m);
+ g.set(id, existing);
+ });
+ setGrouped(g);
+ }, [messages, setGrouped]);
+ const [open, setOpen] = useState<string[]>([...grouped.keys()]);
+ useEffect(() => {
+ // TODO(gio): do not reopen closed ones
+ setOpen([...grouped.keys()]);
+ }, [grouped, setOpen]);
+ return (
+ <Accordion type="multiple" value={open} onValueChange={(v) => setOpen(v)}>
+ {[...grouped.entries()].map(([id, messages]) => (
+ <AccordionItem key={id} value={id}>
+ <AccordionTrigger className="flex flex-row-reverse !space-x-4 !justify-end">
+ <Badge>{messages.length}</Badge>
+ <div>{id === "global" ? "Global" : nodeLabel(nodeMap?.get(id)!)}</div>
+ </AccordionTrigger>
+ <AccordionContent>
+ <div className="flex flex-col space-y-1">
+ {messages.map((m) => (
+ <Button key={m.id} variant="ghost" style={{ justifyContent: "flex-start" }} onMouseOver={onClick(m.onHighlight)} onMouseLeave={onClick(m.onLooseHighlight)} onClick={onClick(m.onClick)}>{m.message}</Button>
+ ))}
+ </div>
+ </AccordionContent>
+ </AccordionItem>
+ ))}
+ </Accordion>
+ )
+}
\ No newline at end of file