-
Notifications
You must be signed in to change notification settings - Fork 2
Claude/graph sharing feature x9qdt #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
88e23f9
ecc0791
fc9b1cd
53d6418
6aaf28b
1c67a21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import uuid | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('block_manager', '0004_add_user_to_project'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='project', | ||
| name='share_token', | ||
| field=models.UUIDField( | ||
| blank=True, | ||
| db_index=True, | ||
| default=None, | ||
| help_text='Unique token for public sharing; generated on first share', | ||
| null=True, | ||
| unique=True, | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name='project', | ||
| name='is_shared', | ||
| field=models.BooleanField( | ||
| default=False, | ||
| help_text='Whether this project is publicly accessible via share link', | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -209,9 +209,10 @@ class Meta: | |
| model = Project | ||
| fields = [ | ||
| 'id', 'name', 'description', 'framework', | ||
| 'share_token', 'is_shared', | ||
| 'created_at', 'updated_at' | ||
| ] | ||
| read_only_fields = ['id', 'created_at', 'updated_at'] | ||
| read_only_fields = ['id', 'share_token', 'is_shared', 'created_at', 'updated_at'] | ||
|
Comment on lines
211
to
+215
|
||
|
|
||
|
|
||
| class ProjectDetailSerializer(serializers.ModelSerializer): | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,122 @@ | ||||||||||
| import uuid | ||||||||||
| from rest_framework import status | ||||||||||
| from rest_framework.decorators import api_view | ||||||||||
| from rest_framework.response import Response | ||||||||||
| from django.shortcuts import get_object_or_404 | ||||||||||
|
|
||||||||||
| from block_manager.models import Project | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @api_view(['GET']) | ||||||||||
| def get_shared_project(request, share_token): | ||||||||||
| """ | ||||||||||
| Public endpoint — returns project metadata for a shared project. | ||||||||||
| No authentication required. | ||||||||||
| Returns 404 if the token doesn't exist or sharing is disabled. | ||||||||||
| """ | ||||||||||
| try: | ||||||||||
| project = Project.objects.get(share_token=share_token, is_shared=True) | ||||||||||
| except Project.DoesNotExist: | ||||||||||
| return Response( | ||||||||||
| {'error': 'Shared project not found or link is no longer active'}, | ||||||||||
| status=status.HTTP_404_NOT_FOUND | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| owner_display_name = None | ||||||||||
| if project.user: | ||||||||||
| owner_display_name = project.user.display_name or project.user.email | ||||||||||
|
||||||||||
| if project.user: | |
| owner_display_name = project.user.display_name or project.user.email | |
| if project.user and getattr(project.user, 'display_name', None): | |
| owner_display_name = project.user.display_name |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,9 +37,10 @@ const nodeTypes = { | |
| interface CanvasProps { | ||
| onDragStart: (type: string) => void | ||
| onRegisterAddNode: (handler: (blockType: string) => void) => void | ||
| readOnly?: boolean | ||
| } | ||
|
|
||
| function FlowCanvas({ onRegisterAddNode }: { onRegisterAddNode: (handler: (blockType: string) => void) => void }) { | ||
| function FlowCanvas({ onRegisterAddNode, readOnly = false }: { onRegisterAddNode: (handler: (blockType: string) => void) => void; readOnly?: boolean }) { | ||
| const { | ||
| nodes, | ||
| edges, | ||
|
|
@@ -88,8 +89,9 @@ function FlowCanvas({ onRegisterAddNode }: { onRegisterAddNode: (handler: (block | |
| // Validation is now triggered manually via the Validate button in Header | ||
| // Removed automatic validation on nodes/edges change | ||
|
|
||
| // Keyboard shortcuts for undo/redo/delete/group/expand | ||
| // Keyboard shortcuts for undo/redo/delete/group/expand (disabled in read-only mode) | ||
| useEffect(() => { | ||
| if (readOnly) return | ||
| const handleKeyDown = (e: KeyboardEvent) => { | ||
| // Check for Ctrl (Windows/Linux) or Cmd (Mac) | ||
| const isMod = e.ctrlKey || e.metaKey | ||
|
|
@@ -130,7 +132,7 @@ function FlowCanvas({ onRegisterAddNode }: { onRegisterAddNode: (handler: (block | |
|
|
||
| window.addEventListener('keydown', handleKeyDown) | ||
| return () => window.removeEventListener('keydown', handleKeyDown) | ||
| }, [undo, redo, removeNode, removeEdge, selectedNodeId, selectedEdgeId, setSelectedEdgeId, nodes]) | ||
| }, [readOnly, undo, redo, removeNode, removeEdge, selectedNodeId, selectedEdgeId, setSelectedEdgeId, nodes]) | ||
|
|
||
| // Find a suitable position for a new node | ||
| const findAvailablePosition = useCallback(() => { | ||
|
|
@@ -400,6 +402,22 @@ function FlowCanvas({ onRegisterAddNode }: { onRegisterAddNode: (handler: (block | |
| [nodes, setNodes] | ||
| ) | ||
|
|
||
| // In read-only mode we still need to propagate selection changes so that | ||
| // ReactFlow sets node.selected=true (which reveals the group-block expand button | ||
| // and drives ConfigPanel display). We filter out position/remove/add changes so | ||
| // nothing can mutate the graph structure. | ||
| const onNodesChangeReadOnly = useCallback( | ||
| (changes: any) => { | ||
| const allowed = changes.filter((c: any) => | ||
| c.type === 'select' || c.type === 'dimensions' || c.type === 'reset' | ||
| ) | ||
| if (allowed.length > 0) { | ||
| setNodes(applyNodeChanges(allowed, nodes)) | ||
| } | ||
| }, | ||
| [nodes, setNodes] | ||
| ) | ||
|
|
||
| const onEdgesChange = useCallback( | ||
| (changes: any) => { | ||
| setEdges(applyEdgeChanges(changes, edges)) | ||
|
|
@@ -688,32 +706,33 @@ function FlowCanvas({ onRegisterAddNode }: { onRegisterAddNode: (handler: (block | |
| return ( | ||
| <div | ||
| className="flex-1 h-full" | ||
| onDrop={onDrop} | ||
| onDragOver={onDragOver} | ||
| onDrop={readOnly ? undefined : onDrop} | ||
| onDragOver={readOnly ? undefined : onDragOver} | ||
| > | ||
| <HistoryToolbar /> | ||
| {!readOnly && <HistoryToolbar />} | ||
| <ReactFlow | ||
| nodes={nodesWithHandlers} | ||
| edges={edges} | ||
| onNodesChange={onNodesChange} | ||
| onEdgesChange={onEdgesChange} | ||
| onConnect={onConnect} | ||
| onNodeClick={isInteractive ? onNodeClick : undefined} | ||
| onEdgeClick={isInteractive ? onEdgeClick : undefined} | ||
| onPaneClick={isInteractive ? onPaneClick : undefined} | ||
| onPaneContextMenu={isInteractive ? onPaneContextMenu : undefined} | ||
| onNodeContextMenu={isInteractive ? onNodeContextMenu : undefined} | ||
| onReconnect={onReconnect} | ||
| edgesReconnectable={true} | ||
| onNodesChange={readOnly ? onNodesChangeReadOnly : onNodesChange} | ||
| onEdgesChange={readOnly ? undefined : onEdgesChange} | ||
|
Comment on lines
713
to
+717
|
||
| onConnect={readOnly ? undefined : onConnect} | ||
| onNodeClick={onNodeClick} | ||
| onEdgeClick={readOnly ? undefined : onEdgeClick} | ||
| onPaneClick={onPaneClick} | ||
| onPaneContextMenu={readOnly ? undefined : (isInteractive ? onPaneContextMenu : undefined)} | ||
| onNodeContextMenu={readOnly ? undefined : (isInteractive ? onNodeContextMenu : undefined)} | ||
| onReconnect={readOnly ? undefined : onReconnect} | ||
| edgesReconnectable={!readOnly} | ||
| nodeTypes={nodeTypes} | ||
| connectionLineComponent={CustomConnectionLine} | ||
| fitView | ||
| minZoom={0.5} | ||
| maxZoom={1.5} | ||
| nodesDraggable={isInteractive} | ||
| nodesConnectable={isInteractive} | ||
| elementsSelectable={isInteractive} | ||
| onInteractiveChange={setIsInteractive} | ||
| nodesDraggable={readOnly ? false : isInteractive} | ||
| nodesConnectable={readOnly ? false : isInteractive} | ||
| elementsSelectable={true} | ||
| deleteKeyCode={readOnly ? null : undefined} | ||
| onInteractiveChange={readOnly ? undefined : setIsInteractive} | ||
| defaultEdgeOptions={{ | ||
| animated: true, | ||
| style: { stroke: '#6366f1', strokeWidth: 2 } | ||
|
|
@@ -751,7 +770,7 @@ function FlowCanvas({ onRegisterAddNode }: { onRegisterAddNode: (handler: (block | |
| position="bottom-right" | ||
| /> | ||
| </ReactFlow> | ||
| {contextMenu && ( | ||
| {!readOnly && contextMenu && ( | ||
| <ContextMenu | ||
| x={contextMenu.x} | ||
| y={contextMenu.y} | ||
|
|
@@ -803,10 +822,10 @@ function FlowCanvas({ onRegisterAddNode }: { onRegisterAddNode: (handler: (block | |
| ) | ||
| } | ||
|
|
||
| export default function Canvas({ onDragStart, onRegisterAddNode }: CanvasProps) { | ||
| export default function Canvas({ onDragStart, onRegisterAddNode, readOnly = false }: CanvasProps) { | ||
| return ( | ||
| <ReactFlowProvider> | ||
| <FlowCanvas onRegisterAddNode={onRegisterAddNode} /> | ||
| <FlowCanvas onRegisterAddNode={onRegisterAddNode} readOnly={readOnly} /> | ||
| </ReactFlowProvider> | ||
| ) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
share_token is marked unique=True, which already creates an index in most databases; db_index=True is likely redundant and may create unnecessary DB overhead. Consider removing db_index=True here (and in the migration) unless there’s a DB-specific reason to keep it.