mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 21:54:56 +00:00
Generated by Spark: Delete connection types --> one to one mapping only. One dot only has one arrow at both from and to.
This commit is contained in:
@@ -30,8 +30,6 @@ import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Plus, Trash, Sparkle, DotsThree, Package } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
type ConnectionType = 'dependency' | 'association' | 'inheritance' | 'composition' | 'aggregation'
|
||||
|
||||
interface FeatureIdea {
|
||||
id: string
|
||||
title: string
|
||||
@@ -51,7 +49,6 @@ interface IdeaGroup {
|
||||
}
|
||||
|
||||
interface IdeaEdgeData {
|
||||
type: ConnectionType
|
||||
label?: string
|
||||
}
|
||||
|
||||
@@ -151,22 +148,11 @@ const SEED_IDEAS: FeatureIdea[] = [
|
||||
const CATEGORIES = ['AI/ML', 'Collaboration', 'Community', 'DevOps', 'Testing', 'Performance', 'Design', 'Database', 'Mobile', 'Accessibility', 'Productivity', 'Security', 'Analytics', 'Other']
|
||||
const PRIORITIES = ['low', 'medium', 'high'] as const
|
||||
const STATUSES = ['idea', 'planned', 'in-progress', 'completed'] as const
|
||||
const CONNECTION_TYPES = ['dependency', 'association', 'inheritance', 'composition', 'aggregation'] as const
|
||||
|
||||
const CONNECTION_STYLES = {
|
||||
dependency: { stroke: '#70c0ff', strokeDasharray: '8,4', strokeWidth: 2.5, markerEnd: MarkerType.ArrowClosed },
|
||||
association: { stroke: '#a78bfa', strokeDasharray: '', strokeWidth: 2.5, markerEnd: MarkerType.Arrow },
|
||||
inheritance: { stroke: '#60d399', strokeDasharray: '', strokeWidth: 2.5, markerEnd: MarkerType.ArrowClosed },
|
||||
composition: { stroke: '#f87171', strokeDasharray: '', strokeWidth: 2.5, markerEnd: MarkerType.ArrowClosed },
|
||||
aggregation: { stroke: '#facc15', strokeDasharray: '', strokeWidth: 2.5, markerEnd: MarkerType.Arrow },
|
||||
}
|
||||
|
||||
const CONNECTION_LABELS = {
|
||||
dependency: 'depends on',
|
||||
association: 'relates to',
|
||||
inheritance: 'extends',
|
||||
composition: 'contains',
|
||||
aggregation: 'has',
|
||||
const CONNECTION_STYLE = {
|
||||
stroke: '#a78bfa',
|
||||
strokeWidth: 2.5,
|
||||
markerEnd: MarkerType.ArrowClosed
|
||||
}
|
||||
|
||||
const STATUS_COLORS = {
|
||||
@@ -310,10 +296,10 @@ export function FeatureIdeaCloud() {
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'left',
|
||||
type: 'default',
|
||||
animated: true,
|
||||
data: { type: 'dependency', label: 'requires' },
|
||||
markerEnd: { type: MarkerType.ArrowClosed, color: '#70c0ff', width: 20, height: 20 },
|
||||
style: { stroke: '#70c0ff', strokeDasharray: '8,4', strokeWidth: 2.5 },
|
||||
animated: false,
|
||||
data: { label: 'requires' },
|
||||
markerEnd: { type: MarkerType.ArrowClosed, color: '#a78bfa', width: 20, height: 20 },
|
||||
style: { stroke: '#a78bfa', strokeWidth: 2.5 },
|
||||
},
|
||||
{
|
||||
id: 'edge-2',
|
||||
@@ -322,8 +308,8 @@ export function FeatureIdeaCloud() {
|
||||
sourceHandle: 'bottom',
|
||||
targetHandle: 'top',
|
||||
type: 'default',
|
||||
data: { type: 'association', label: 'works with' },
|
||||
markerEnd: { type: MarkerType.Arrow, color: '#a78bfa', width: 20, height: 20 },
|
||||
data: { label: 'works with' },
|
||||
markerEnd: { type: MarkerType.ArrowClosed, color: '#a78bfa', width: 20, height: 20 },
|
||||
style: { stroke: '#a78bfa', strokeWidth: 2.5 },
|
||||
},
|
||||
{
|
||||
@@ -333,9 +319,9 @@ export function FeatureIdeaCloud() {
|
||||
sourceHandle: 'bottom',
|
||||
targetHandle: 'left',
|
||||
type: 'default',
|
||||
data: { type: 'composition', label: 'includes' },
|
||||
markerEnd: { type: MarkerType.ArrowClosed, color: '#f87171', width: 20, height: 20 },
|
||||
style: { stroke: '#f87171', strokeWidth: 2.5 },
|
||||
data: { label: 'includes' },
|
||||
markerEnd: { type: MarkerType.ArrowClosed, color: '#a78bfa', width: 20, height: 20 },
|
||||
style: { stroke: '#a78bfa', strokeWidth: 2.5 },
|
||||
},
|
||||
])
|
||||
const [savedNodePositions, setSavedNodePositions] = useKV<Record<string, { x: number; y: number }>>('feature-idea-node-positions', {})
|
||||
@@ -350,7 +336,6 @@ export function FeatureIdeaCloud() {
|
||||
const [viewDialogOpen, setViewDialogOpen] = useState(false)
|
||||
const [edgeDialogOpen, setEdgeDialogOpen] = useState(false)
|
||||
const [debugPanelOpen, setDebugPanelOpen] = useState(false)
|
||||
const [connectionType, setConnectionType] = useState<ConnectionType>('association')
|
||||
const edgeReconnectSuccessful = useRef(true)
|
||||
|
||||
const safeIdeas = ideas || SEED_IDEAS
|
||||
@@ -523,7 +508,6 @@ export function FeatureIdeaCloud() {
|
||||
targetHandleId
|
||||
)
|
||||
|
||||
const style = CONNECTION_STYLES[connectionType]
|
||||
const newEdge: Edge<IdeaEdgeData> = {
|
||||
id: `edge-${Date.now()}`,
|
||||
source: sourceNodeId,
|
||||
@@ -531,19 +515,18 @@ export function FeatureIdeaCloud() {
|
||||
...(params.sourceHandle && { sourceHandle: params.sourceHandle }),
|
||||
...(params.targetHandle && { targetHandle: params.targetHandle }),
|
||||
type: 'default',
|
||||
data: { type: connectionType, label: CONNECTION_LABELS[connectionType] },
|
||||
data: { label: 'relates to' },
|
||||
markerEnd: {
|
||||
type: style.markerEnd,
|
||||
color: style.stroke,
|
||||
type: CONNECTION_STYLE.markerEnd,
|
||||
color: CONNECTION_STYLE.stroke,
|
||||
width: 20,
|
||||
height: 20
|
||||
},
|
||||
style: {
|
||||
stroke: style.stroke,
|
||||
strokeDasharray: style.strokeDasharray,
|
||||
strokeWidth: style.strokeWidth
|
||||
stroke: CONNECTION_STYLE.stroke,
|
||||
strokeWidth: CONNECTION_STYLE.strokeWidth
|
||||
},
|
||||
animated: connectionType === 'dependency',
|
||||
animated: false,
|
||||
}
|
||||
|
||||
const updatedEdges = addEdge(newEdge, filteredEdges)
|
||||
@@ -573,7 +556,7 @@ export function FeatureIdeaCloud() {
|
||||
return updatedEdges
|
||||
})
|
||||
},
|
||||
[connectionType, setEdges, setSavedEdges, validateAndRemoveConflicts]
|
||||
[setEdges, setSavedEdges, validateAndRemoveConflicts]
|
||||
)
|
||||
|
||||
const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge<IdeaEdgeData>) => {
|
||||
@@ -809,22 +792,20 @@ export function FeatureIdeaCloud() {
|
||||
|
||||
const handleSaveEdge = () => {
|
||||
if (selectedEdge) {
|
||||
const style = CONNECTION_STYLES[selectedEdge.data!.type]
|
||||
const updatedEdge = {
|
||||
...selectedEdge,
|
||||
data: selectedEdge.data,
|
||||
markerEnd: {
|
||||
type: style.markerEnd,
|
||||
color: style.stroke,
|
||||
type: CONNECTION_STYLE.markerEnd,
|
||||
color: CONNECTION_STYLE.stroke,
|
||||
width: 20,
|
||||
height: 20
|
||||
},
|
||||
style: {
|
||||
stroke: style.stroke,
|
||||
strokeDasharray: style.strokeDasharray,
|
||||
strokeWidth: style.strokeWidth
|
||||
stroke: CONNECTION_STYLE.stroke,
|
||||
strokeWidth: CONNECTION_STYLE.strokeWidth
|
||||
},
|
||||
animated: selectedEdge.data!.type === 'dependency',
|
||||
animated: false,
|
||||
}
|
||||
|
||||
const updatedEdges = edges.map(e => e.id === selectedEdge.id ? updatedEdge : e)
|
||||
@@ -921,24 +902,6 @@ export function FeatureIdeaCloud() {
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="hsl(var(--border))" />
|
||||
<Controls showInteractive={false} />
|
||||
|
||||
<Panel position="top-left" className="flex gap-2 items-center">
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-card border border-border rounded-md shadow-lg">
|
||||
<span className="text-xs font-medium text-muted-foreground">Connection Type:</span>
|
||||
<select
|
||||
value={connectionType}
|
||||
onChange={(e) => setConnectionType(e.target.value as ConnectionType)}
|
||||
className="text-sm bg-transparent border-none outline-none pr-1 font-medium"
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{CONNECTION_TYPES.map(type => (
|
||||
<option key={type} value={type}>
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel position="top-right" className="flex gap-2">
|
||||
<TooltipProvider>
|
||||
@@ -985,47 +948,6 @@ export function FeatureIdeaCloud() {
|
||||
</TooltipProvider>
|
||||
</Panel>
|
||||
|
||||
<Panel position="bottom-left">
|
||||
<div className="bg-card border border-border rounded-lg shadow-lg p-3 text-xs">
|
||||
<p className="font-semibold mb-2 text-foreground">Connection Types:</p>
|
||||
<div className="space-y-1.5">
|
||||
{CONNECTION_TYPES.map(type => {
|
||||
const style = CONNECTION_STYLES[type]
|
||||
return (
|
||||
<div key={type} className="flex items-center gap-2">
|
||||
<svg width="50" height="12" className="shrink-0">
|
||||
<defs>
|
||||
<marker
|
||||
id={`arrow-${type}`}
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="8"
|
||||
refY="3"
|
||||
orient="auto"
|
||||
markerUnits="strokeWidth"
|
||||
>
|
||||
<path d="M0,0 L0,6 L9,3 z" fill={style.stroke} />
|
||||
</marker>
|
||||
</defs>
|
||||
<line
|
||||
x1="2"
|
||||
y1="6"
|
||||
x2="48"
|
||||
y2="6"
|
||||
stroke={style.stroke}
|
||||
strokeWidth={2.5}
|
||||
strokeDasharray={style.strokeDasharray}
|
||||
markerEnd={`url(#arrow-${type})`}
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-muted-foreground capitalize">{type}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel position="bottom-right">
|
||||
<div className="bg-card border border-border rounded-lg shadow-lg p-2 text-xs text-muted-foreground max-w-sm">
|
||||
<p className="mb-1">💡 <strong>Tip:</strong> Double-click ideas to view details</p>
|
||||
@@ -1076,28 +998,43 @@ export function FeatureIdeaCloud() {
|
||||
<div className="p-2 space-y-2 text-xs font-mono">
|
||||
{safeIdeas.slice(0, 10).map((idea) => {
|
||||
const nodeEdges = edges.filter(e => e.source === idea.id || e.target === idea.id)
|
||||
const leftHandle = edges.find(e => e.target === idea.id && e.targetHandle === 'left')
|
||||
const rightHandle = edges.find(e => e.source === idea.id && e.sourceHandle === 'right')
|
||||
const topHandle = edges.find(e => e.target === idea.id && e.targetHandle === 'top')
|
||||
const bottomHandle = edges.find(e => e.source === idea.id && e.sourceHandle === 'bottom')
|
||||
const leftHandles = edges.filter(e => e.target === idea.id && e.targetHandle === 'left')
|
||||
const rightHandles = edges.filter(e => e.source === idea.id && e.sourceHandle === 'right')
|
||||
const topHandles = edges.filter(e => e.target === idea.id && e.targetHandle === 'top')
|
||||
const bottomHandles = edges.filter(e => e.source === idea.id && e.sourceHandle === 'bottom')
|
||||
|
||||
const hasViolation = leftHandles.length > 1 || rightHandles.length > 1 || topHandles.length > 1 || bottomHandles.length > 1
|
||||
|
||||
return (
|
||||
<div key={idea.id} className="p-2 bg-muted/30 rounded border">
|
||||
<div className="font-semibold truncate mb-1" title={idea.title}>
|
||||
<div key={idea.id} className={`p-2 rounded border ${hasViolation ? 'bg-red-500/20 border-red-500' : 'bg-muted/30'}`}>
|
||||
<div className="font-semibold truncate mb-1 flex items-center gap-2" title={idea.title}>
|
||||
{hasViolation && <span className="text-red-500">⚠️</span>}
|
||||
{idea.title}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-1 text-[10px]">
|
||||
<div className={`p-1 rounded text-center ${leftHandle ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted'}`}>
|
||||
← {leftHandle ? '✓' : '○'}
|
||||
<div className={`p-1 rounded text-center ${
|
||||
leftHandles.length > 1 ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' :
|
||||
leftHandles.length === 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted'
|
||||
}`}>
|
||||
← {leftHandles.length > 0 ? `✓${leftHandles.length > 1 ? `(${leftHandles.length})` : ''}` : '○'}
|
||||
</div>
|
||||
<div className={`p-1 rounded text-center ${rightHandle ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted'}`}>
|
||||
→ {rightHandle ? '✓' : '○'}
|
||||
<div className={`p-1 rounded text-center ${
|
||||
rightHandles.length > 1 ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' :
|
||||
rightHandles.length === 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted'
|
||||
}`}>
|
||||
→ {rightHandles.length > 0 ? `✓${rightHandles.length > 1 ? `(${rightHandles.length})` : ''}` : '○'}
|
||||
</div>
|
||||
<div className={`p-1 rounded text-center ${topHandle ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted'}`}>
|
||||
↑ {topHandle ? '✓' : '○'}
|
||||
<div className={`p-1 rounded text-center ${
|
||||
topHandles.length > 1 ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' :
|
||||
topHandles.length === 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted'
|
||||
}`}>
|
||||
↑ {topHandles.length > 0 ? `✓${topHandles.length > 1 ? `(${topHandles.length})` : ''}` : '○'}
|
||||
</div>
|
||||
<div className={`p-1 rounded text-center ${bottomHandle ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted'}`}>
|
||||
↓ {bottomHandle ? '✓' : '○'}
|
||||
<div className={`p-1 rounded text-center ${
|
||||
bottomHandles.length > 1 ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' :
|
||||
bottomHandles.length === 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted'
|
||||
}`}>
|
||||
↓ {bottomHandles.length > 0 ? `✓${bottomHandles.length > 1 ? `(${bottomHandles.length})` : ''}` : '○'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 text-[10px] text-muted-foreground">
|
||||
@@ -1115,17 +1052,45 @@ export function FeatureIdeaCloud() {
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<div className="font-semibold text-xs text-green-700 dark:text-green-300 flex items-center gap-1">
|
||||
✅ Test Status
|
||||
</div>
|
||||
<div className="text-xs space-y-0.5">
|
||||
<div>• Each handle can connect to exactly 1 other handle</div>
|
||||
<div>• New connections automatically remove conflicts</div>
|
||||
<div>• Remapping preserves 1:1 constraint</div>
|
||||
<div>• Changes persist to database immediately</div>
|
||||
</div>
|
||||
</div>
|
||||
{(() => {
|
||||
const violations: string[] = []
|
||||
safeIdeas.forEach(idea => {
|
||||
const leftHandles = edges.filter(e => e.target === idea.id && e.targetHandle === 'left')
|
||||
const rightHandles = edges.filter(e => e.source === idea.id && e.sourceHandle === 'right')
|
||||
const topHandles = edges.filter(e => e.target === idea.id && e.targetHandle === 'top')
|
||||
const bottomHandles = edges.filter(e => e.source === idea.id && e.sourceHandle === 'bottom')
|
||||
|
||||
if (leftHandles.length > 1) violations.push(`${idea.title}: Left handle has ${leftHandles.length} connections`)
|
||||
if (rightHandles.length > 1) violations.push(`${idea.title}: Right handle has ${rightHandles.length} connections`)
|
||||
if (topHandles.length > 1) violations.push(`${idea.title}: Top handle has ${topHandles.length} connections`)
|
||||
if (bottomHandles.length > 1) violations.push(`${idea.title}: Bottom handle has ${bottomHandles.length} connections`)
|
||||
})
|
||||
|
||||
return violations.length > 0 ? (
|
||||
<div className="space-y-1 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<div className="font-semibold text-xs text-red-700 dark:text-red-300 flex items-center gap-1">
|
||||
❌ CONSTRAINT VIOLATIONS DETECTED
|
||||
</div>
|
||||
<div className="text-xs space-y-0.5 text-red-600 dark:text-red-400">
|
||||
{violations.map((v, i) => (
|
||||
<div key={i}>• {v}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<div className="font-semibold text-xs text-green-700 dark:text-green-300 flex items-center gap-1">
|
||||
✅ All Constraints Satisfied
|
||||
</div>
|
||||
<div className="text-xs space-y-0.5">
|
||||
<div>• Each handle has at most 1 connection ✓</div>
|
||||
<div>• New connections automatically remove conflicts ✓</div>
|
||||
<div>• Remapping preserves 1:1 constraint ✓</div>
|
||||
<div>• Changes persist to database immediately ✓</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</Card>
|
||||
</Panel>
|
||||
@@ -1386,10 +1351,12 @@ export function FeatureIdeaCloud() {
|
||||
const isOutgoing = edge.source === selectedIdea.id
|
||||
return (
|
||||
<div key={edge.id} className="flex items-center gap-2 text-sm p-2 bg-muted rounded">
|
||||
<Badge variant="outline" className="text-xs">{edge.data?.type || 'unknown'}</Badge>
|
||||
<span className="flex-1">
|
||||
{isOutgoing ? '→' : '←'} {otherIdea?.title || 'Unknown'}
|
||||
</span>
|
||||
{edge.data?.label && (
|
||||
<Badge variant="outline" className="text-xs">{edge.data.label}</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -1441,32 +1408,10 @@ export function FeatureIdeaCloud() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Type</label>
|
||||
<select
|
||||
value={selectedEdge.data.type}
|
||||
onChange={(e) => setSelectedEdge({
|
||||
...selectedEdge,
|
||||
data: {
|
||||
...selectedEdge.data!,
|
||||
type: e.target.value as ConnectionType,
|
||||
label: CONNECTION_LABELS[e.target.value as ConnectionType]
|
||||
}
|
||||
})}
|
||||
className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm"
|
||||
>
|
||||
{CONNECTION_TYPES.map(type => (
|
||||
<option key={type} value={type}>
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Label</label>
|
||||
<Input
|
||||
value={selectedEdge.data.label || ''}
|
||||
value={selectedEdge.data?.label || ''}
|
||||
onChange={(e) => setSelectedEdge({
|
||||
...selectedEdge,
|
||||
data: {
|
||||
@@ -1474,19 +1419,15 @@ export function FeatureIdeaCloud() {
|
||||
label: e.target.value
|
||||
}
|
||||
})}
|
||||
placeholder={CONNECTION_LABELS[selectedEdge.data.type]}
|
||||
placeholder="relates to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-muted rounded-lg text-sm">
|
||||
<p className="font-medium mb-2">Connection Type Guide:</p>
|
||||
<ul className="space-y-1 text-xs text-muted-foreground">
|
||||
<li><strong>Dependency:</strong> One feature needs another to function</li>
|
||||
<li><strong>Association:</strong> Features work together or are related</li>
|
||||
<li><strong>Inheritance:</strong> One feature extends another</li>
|
||||
<li><strong>Composition:</strong> One feature contains another as essential part</li>
|
||||
<li><strong>Aggregation:</strong> One feature has another as optional part</li>
|
||||
</ul>
|
||||
<p className="font-medium mb-2">💡 Connection Info:</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Each connection point can have exactly one arrow. Creating a new connection from an occupied point will automatically remap the existing connection.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user