mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 21:54:56 +00:00
Generated by Spark: Would be nice if I could connect ideas together like UML
This commit is contained in:
@@ -7,10 +7,20 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Plus, Trash, Sparkle, MagnifyingGlassMinus, MagnifyingGlassPlus, ArrowsOut, Hand, Link as LinkIcon, Selection, DotsThree } from '@phosphor-icons/react'
|
||||
import { Plus, Trash, Sparkle, MagnifyingGlassMinus, MagnifyingGlassPlus, ArrowsOut, Hand, Link as LinkIcon, Selection, DotsThree, X } from '@phosphor-icons/react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
type ConnectionType = 'dependency' | 'association' | 'inheritance' | 'composition' | 'aggregation'
|
||||
|
||||
interface Connection {
|
||||
id: string
|
||||
fromId: string
|
||||
toId: string
|
||||
type: ConnectionType
|
||||
label?: string
|
||||
}
|
||||
|
||||
interface FeatureIdea {
|
||||
id: string
|
||||
title: string
|
||||
@@ -22,6 +32,7 @@ interface FeatureIdea {
|
||||
x: number
|
||||
y: number
|
||||
connectedTo?: string[]
|
||||
connections?: Connection[]
|
||||
}
|
||||
|
||||
const SEED_IDEAS: FeatureIdea[] = [
|
||||
@@ -35,7 +46,6 @@ const SEED_IDEAS: FeatureIdea[] = [
|
||||
createdAt: Date.now() - 10000000,
|
||||
x: 100,
|
||||
y: 150,
|
||||
connectedTo: ['idea-8'],
|
||||
},
|
||||
{
|
||||
id: 'idea-2',
|
||||
@@ -47,7 +57,6 @@ const SEED_IDEAS: FeatureIdea[] = [
|
||||
createdAt: Date.now() - 9000000,
|
||||
x: 600,
|
||||
y: 250,
|
||||
connectedTo: ['idea-4'],
|
||||
},
|
||||
{
|
||||
id: 'idea-3',
|
||||
@@ -70,7 +79,6 @@ const SEED_IDEAS: FeatureIdea[] = [
|
||||
createdAt: Date.now() - 7000000,
|
||||
x: 700,
|
||||
y: 600,
|
||||
connectedTo: ['idea-8'],
|
||||
},
|
||||
{
|
||||
id: 'idea-5',
|
||||
@@ -115,7 +123,6 @@ const SEED_IDEAS: FeatureIdea[] = [
|
||||
createdAt: Date.now() - 3000000,
|
||||
x: 300,
|
||||
y: 400,
|
||||
connectedTo: ['idea-5'],
|
||||
},
|
||||
{
|
||||
id: 'idea-9',
|
||||
@@ -141,9 +148,32 @@ const SEED_IDEAS: FeatureIdea[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const SEED_CONNECTIONS: Connection[] = [
|
||||
{ id: 'conn-1', fromId: 'idea-1', toId: 'idea-8', type: 'dependency', label: 'requires' },
|
||||
{ id: 'conn-2', fromId: 'idea-2', toId: 'idea-4', type: 'association', label: 'works with' },
|
||||
{ id: 'conn-3', fromId: 'idea-8', toId: 'idea-5', type: 'composition', label: 'includes' },
|
||||
]
|
||||
|
||||
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: 'hsl(var(--accent))', strokeDasharray: '8,4', arrowType: 'open' },
|
||||
association: { stroke: 'hsl(var(--primary))', strokeDasharray: '', arrowType: 'line' },
|
||||
inheritance: { stroke: 'hsl(var(--chart-2))', strokeDasharray: '', arrowType: 'hollow' },
|
||||
composition: { stroke: 'hsl(var(--destructive))', strokeDasharray: '', arrowType: 'diamond-filled' },
|
||||
aggregation: { stroke: 'hsl(var(--chart-4))', strokeDasharray: '', arrowType: 'diamond-hollow' },
|
||||
}
|
||||
|
||||
const CONNECTION_LABELS = {
|
||||
dependency: 'depends on',
|
||||
association: 'relates to',
|
||||
inheritance: 'extends',
|
||||
composition: 'contains',
|
||||
aggregation: 'has',
|
||||
}
|
||||
|
||||
const STATUS_COLORS = {
|
||||
idea: 'bg-muted text-muted-foreground',
|
||||
@@ -160,9 +190,12 @@ const PRIORITY_COLORS = {
|
||||
|
||||
export function FeatureIdeaCloud() {
|
||||
const [ideas, setIdeas] = useKV<FeatureIdea[]>('feature-ideas', SEED_IDEAS)
|
||||
const [connections, setConnections] = useKV<Connection[]>('feature-connections', SEED_CONNECTIONS)
|
||||
const [selectedIdea, setSelectedIdea] = useState<FeatureIdea | null>(null)
|
||||
const [selectedConnection, setSelectedConnection] = useState<Connection | null>(null)
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
||||
const [viewDialogOpen, setViewDialogOpen] = useState(false)
|
||||
const [connectionDialogOpen, setConnectionDialogOpen] = useState(false)
|
||||
const canvasRef = useRef<HTMLDivElement>(null)
|
||||
const [zoom, setZoom] = useState(1)
|
||||
const [pan, setPan] = useState({ x: 0, y: 0 })
|
||||
@@ -172,15 +205,20 @@ export function FeatureIdeaCloud() {
|
||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 })
|
||||
const [tool, setTool] = useState<'select' | 'pan' | 'connect'>('select')
|
||||
const [connectingFrom, setConnectingFrom] = useState<string | null>(null)
|
||||
const [filtersPanelOpen, setFiltersPanelOpen] = useState(false)
|
||||
const [connectionType, setConnectionType] = useState<ConnectionType>('association')
|
||||
const [hoveredConnection, setHoveredConnection] = useState<string | null>(null)
|
||||
|
||||
const safeIdeas = ideas || SEED_IDEAS
|
||||
const safeConnections = connections || SEED_CONNECTIONS
|
||||
|
||||
useEffect(() => {
|
||||
if (!ideas || ideas.length === 0) {
|
||||
setIdeas(SEED_IDEAS)
|
||||
}
|
||||
}, [ideas, setIdeas])
|
||||
if (!connections || connections.length === 0) {
|
||||
setConnections(SEED_CONNECTIONS)
|
||||
}
|
||||
}, [ideas, setIdeas, connections, setConnections])
|
||||
|
||||
const handleAddIdea = () => {
|
||||
const canvasCenterX = (window.innerWidth / 2 - pan.x) / zoom
|
||||
@@ -211,21 +249,26 @@ export function FeatureIdeaCloud() {
|
||||
e.stopPropagation()
|
||||
if (!connectingFrom) {
|
||||
setConnectingFrom(idea.id)
|
||||
toast.info('Click another idea to connect')
|
||||
toast.info(`Click another idea to connect (${CONNECTION_LABELS[connectionType]})`)
|
||||
} else if (connectingFrom !== idea.id) {
|
||||
setIdeas((currentIdeas) =>
|
||||
(currentIdeas || []).map(i => {
|
||||
if (i.id === connectingFrom) {
|
||||
const connectedTo = i.connectedTo || []
|
||||
if (!connectedTo.includes(idea.id)) {
|
||||
return { ...i, connectedTo: [...connectedTo, idea.id] }
|
||||
}
|
||||
}
|
||||
return i
|
||||
})
|
||||
const existingConnection = safeConnections.find(
|
||||
c => c.fromId === connectingFrom && c.toId === idea.id
|
||||
)
|
||||
|
||||
if (existingConnection) {
|
||||
toast.error('Connection already exists')
|
||||
} else {
|
||||
const newConnection: Connection = {
|
||||
id: `conn-${Date.now()}`,
|
||||
fromId: connectingFrom,
|
||||
toId: idea.id,
|
||||
type: connectionType,
|
||||
label: CONNECTION_LABELS[connectionType],
|
||||
}
|
||||
setConnections((current) => [...(current || []), newConnection])
|
||||
toast.success('Ideas connected!')
|
||||
}
|
||||
setConnectingFrom(null)
|
||||
toast.success('Ideas connected!')
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -261,12 +304,30 @@ export function FeatureIdeaCloud() {
|
||||
|
||||
const handleDeleteIdea = (id: string) => {
|
||||
setIdeas((currentIdeas) => (currentIdeas || []).filter(i => i.id !== id))
|
||||
setConnections((currentConnections) =>
|
||||
(currentConnections || []).filter(c => c.fromId !== id && c.toId !== id)
|
||||
)
|
||||
setEditDialogOpen(false)
|
||||
setViewDialogOpen(false)
|
||||
setSelectedIdea(null)
|
||||
toast.success('Idea deleted')
|
||||
}
|
||||
|
||||
const handleDeleteConnection = (connectionId: string) => {
|
||||
setConnections((current) => (current || []).filter(c => c.id !== connectionId))
|
||||
setConnectionDialogOpen(false)
|
||||
setSelectedConnection(null)
|
||||
toast.success('Connection removed')
|
||||
}
|
||||
|
||||
const handleConnectionClick = (connection: Connection, e: React.MouseEvent) => {
|
||||
if (tool === 'select') {
|
||||
e.stopPropagation()
|
||||
setSelectedConnection(connection)
|
||||
setConnectionDialogOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleGenerateIdeas = async () => {
|
||||
toast.info('Generating ideas with AI...')
|
||||
|
||||
@@ -386,34 +447,175 @@ export function FeatureIdeaCloud() {
|
||||
setPan({ x: newPanX, y: newPanY })
|
||||
}
|
||||
|
||||
const renderArrowhead = (connection: Connection, x: number, y: number, angle: number) => {
|
||||
const style = CONNECTION_STYLES[connection.type]
|
||||
const size = 12
|
||||
|
||||
if (style.arrowType === 'open') {
|
||||
const points = [
|
||||
[x, y],
|
||||
[x - size * Math.cos(angle - Math.PI / 6), y - size * Math.sin(angle - Math.PI / 6)],
|
||||
[x - size * Math.cos(angle + Math.PI / 6), y - size * Math.sin(angle + Math.PI / 6)]
|
||||
]
|
||||
return (
|
||||
<polyline
|
||||
points={points.map(p => p.join(',')).join(' ')}
|
||||
fill="none"
|
||||
stroke={style.stroke}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
)
|
||||
} else if (style.arrowType === 'line') {
|
||||
return (
|
||||
<line
|
||||
x1={x}
|
||||
y1={y}
|
||||
x2={x - size * Math.cos(angle)}
|
||||
y2={y - size * Math.sin(angle)}
|
||||
stroke={style.stroke}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
)
|
||||
} else if (style.arrowType === 'hollow') {
|
||||
const points = [
|
||||
[x, y],
|
||||
[x - size * Math.cos(angle - Math.PI / 6), y - size * Math.sin(angle - Math.PI / 6)],
|
||||
[x - size * 0.7 * Math.cos(angle), y - size * 0.7 * Math.sin(angle)],
|
||||
[x - size * Math.cos(angle + Math.PI / 6), y - size * Math.sin(angle + Math.PI / 6)]
|
||||
]
|
||||
return (
|
||||
<polygon
|
||||
points={points.map(p => p.join(',')).join(' ')}
|
||||
fill="hsl(var(--background))"
|
||||
stroke={style.stroke}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
)
|
||||
} else if (style.arrowType === 'diamond-filled') {
|
||||
const points = [
|
||||
[x, y],
|
||||
[x - size * 0.6 * Math.cos(angle - Math.PI / 3), y - size * 0.6 * Math.sin(angle - Math.PI / 3)],
|
||||
[x - size * Math.cos(angle), y - size * Math.sin(angle)],
|
||||
[x - size * 0.6 * Math.cos(angle + Math.PI / 3), y - size * 0.6 * Math.sin(angle + Math.PI / 3)]
|
||||
]
|
||||
return (
|
||||
<polygon
|
||||
points={points.map(p => p.join(',')).join(' ')}
|
||||
fill={style.stroke}
|
||||
stroke={style.stroke}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
)
|
||||
} else if (style.arrowType === 'diamond-hollow') {
|
||||
const points = [
|
||||
[x, y],
|
||||
[x - size * 0.6 * Math.cos(angle - Math.PI / 3), y - size * 0.6 * Math.sin(angle - Math.PI / 3)],
|
||||
[x - size * Math.cos(angle), y - size * Math.sin(angle)],
|
||||
[x - size * 0.6 * Math.cos(angle + Math.PI / 3), y - size * 0.6 * Math.sin(angle + Math.PI / 3)]
|
||||
]
|
||||
return (
|
||||
<polygon
|
||||
points={points.map(p => p.join(',')).join(' ')}
|
||||
fill="hsl(var(--background))"
|
||||
stroke={style.stroke}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const renderConnections = () => {
|
||||
const connections: React.ReactNode[] = []
|
||||
safeIdeas.forEach((fromIdea) => {
|
||||
fromIdea.connectedTo?.forEach((toId) => {
|
||||
const toIdea = safeIdeas.find(i => i.id === toId)
|
||||
if (toIdea) {
|
||||
const fromX = fromIdea.x * zoom + pan.x + 120
|
||||
const fromY = fromIdea.y * zoom + pan.y + 80
|
||||
const toX = toIdea.x * zoom + pan.x + 120
|
||||
const toY = toIdea.y * zoom + pan.y + 80
|
||||
|
||||
connections.push(
|
||||
const elements: React.ReactNode[] = []
|
||||
|
||||
safeConnections.forEach((connection) => {
|
||||
const fromIdea = safeIdeas.find(i => i.id === connection.fromId)
|
||||
const toIdea = safeIdeas.find(i => i.id === connection.toId)
|
||||
|
||||
if (fromIdea && toIdea) {
|
||||
const fromX = fromIdea.x * zoom + pan.x + 120
|
||||
const fromY = fromIdea.y * zoom + pan.y + 80
|
||||
const toX = toIdea.x * zoom + pan.x + 120
|
||||
const toY = toIdea.y * zoom + pan.y + 80
|
||||
|
||||
const dx = toX - fromX
|
||||
const dy = toY - fromY
|
||||
const angle = Math.atan2(dy, dx)
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
const arrowSize = 12
|
||||
const endX = toX - arrowSize * Math.cos(angle)
|
||||
const endY = toY - arrowSize * Math.sin(angle)
|
||||
|
||||
const midX = (fromX + toX) / 2
|
||||
const midY = (fromY + toY) / 2
|
||||
|
||||
const style = CONNECTION_STYLES[connection.type]
|
||||
const isHovered = hoveredConnection === connection.id
|
||||
|
||||
elements.push(
|
||||
<g key={connection.id}>
|
||||
<line
|
||||
key={`${fromIdea.id}-${toId}`}
|
||||
x1={fromX}
|
||||
y1={fromY}
|
||||
x2={toX}
|
||||
y2={toY}
|
||||
stroke="hsl(var(--accent))"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5,5"
|
||||
opacity={0.5}
|
||||
x2={endX}
|
||||
y2={endY}
|
||||
stroke={style.stroke}
|
||||
strokeWidth={isHovered ? 3 : 2}
|
||||
strokeDasharray={style.strokeDasharray}
|
||||
opacity={isHovered ? 0.9 : 0.6}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onMouseEnter={() => setHoveredConnection(connection.id)}
|
||||
onMouseLeave={() => setHoveredConnection(null)}
|
||||
onClick={(e) => handleConnectionClick(connection, e as any)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
<line
|
||||
x1={fromX}
|
||||
y1={fromY}
|
||||
x2={endX}
|
||||
y2={endY}
|
||||
stroke="transparent"
|
||||
strokeWidth={12}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onMouseEnter={() => setHoveredConnection(connection.id)}
|
||||
onMouseLeave={() => setHoveredConnection(null)}
|
||||
onClick={(e) => handleConnectionClick(connection, e as any)}
|
||||
/>
|
||||
|
||||
{renderArrowhead(connection, toX, toY, angle)}
|
||||
|
||||
{(isHovered || connection.label) && (
|
||||
<g>
|
||||
<rect
|
||||
x={midX - 40}
|
||||
y={midY - 12}
|
||||
width={80}
|
||||
height={24}
|
||||
fill="hsl(var(--card))"
|
||||
stroke="hsl(var(--border))"
|
||||
strokeWidth={1}
|
||||
rx={4}
|
||||
opacity={0.95}
|
||||
/>
|
||||
<text
|
||||
x={midX}
|
||||
y={midY + 5}
|
||||
textAnchor="middle"
|
||||
fill="hsl(var(--foreground))"
|
||||
fontSize="11"
|
||||
fontWeight="500"
|
||||
>
|
||||
{connection.label || CONNECTION_LABELS[connection.type]}
|
||||
</text>
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
})
|
||||
return connections
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -472,6 +674,23 @@ export function FeatureIdeaCloud() {
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
{tool === 'connect' && (
|
||||
<div className="flex items-center gap-2 px-3 bg-card border border-border rounded-md shadow-lg">
|
||||
<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>
|
||||
)}
|
||||
|
||||
<div className="w-px bg-border mx-1" />
|
||||
|
||||
<TooltipProvider>
|
||||
@@ -530,8 +749,34 @@ export function FeatureIdeaCloud() {
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 right-4 z-10 bg-card border border-border rounded-lg shadow-lg p-2 text-xs text-muted-foreground">
|
||||
<p>💡 <strong>Tip:</strong> Double-click to view, drag to move, scroll to zoom</p>
|
||||
<div className="absolute bottom-4 left-4 z-10 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="40" height="12" className="shrink-0">
|
||||
<line
|
||||
x1="0"
|
||||
y1="6"
|
||||
x2="40"
|
||||
y2="6"
|
||||
stroke={style.stroke}
|
||||
strokeWidth={2}
|
||||
strokeDasharray={style.strokeDasharray}
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-muted-foreground capitalize">{type}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 right-4 z-10 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>
|
||||
<p>🔗 Use Connect tool to create UML-style relationships</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -760,6 +1005,31 @@ export function FeatureIdeaCloud() {
|
||||
<label className="text-xs font-medium text-muted-foreground">Created</label>
|
||||
<p className="text-sm">{new Date(selectedIdea.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground mb-2 block">Connections</label>
|
||||
<div className="space-y-2">
|
||||
{safeConnections
|
||||
.filter(c => c.fromId === selectedIdea.id || c.toId === selectedIdea.id)
|
||||
.map(conn => {
|
||||
const otherIdea = safeIdeas.find(i =>
|
||||
i.id === (conn.fromId === selectedIdea.id ? conn.toId : conn.fromId)
|
||||
)
|
||||
const isOutgoing = conn.fromId === selectedIdea.id
|
||||
return (
|
||||
<div key={conn.id} className="flex items-center gap-2 text-sm p-2 bg-muted rounded">
|
||||
<Badge variant="outline" className="text-xs">{conn.type}</Badge>
|
||||
<span className="flex-1">
|
||||
{isOutgoing ? '→' : '←'} {otherIdea?.title || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{safeConnections.filter(c => c.fromId === selectedIdea.id || c.toId === selectedIdea.id).length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">No connections</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -776,6 +1046,108 @@ export function FeatureIdeaCloud() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={connectionDialogOpen} onOpenChange={setConnectionDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connection Details</DialogTitle>
|
||||
<DialogDescription>
|
||||
Manage the relationship between ideas
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedConnection && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">From</label>
|
||||
<p className="text-sm font-medium">
|
||||
{safeIdeas.find(i => i.id === selectedConnection.fromId)?.title}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">To</label>
|
||||
<p className="text-sm font-medium">
|
||||
{safeIdeas.find(i => i.id === selectedConnection.toId)?.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Type</label>
|
||||
<select
|
||||
value={selectedConnection.type}
|
||||
onChange={(e) => setSelectedConnection({
|
||||
...selectedConnection,
|
||||
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={selectedConnection.label || ''}
|
||||
onChange={(e) => setSelectedConnection({
|
||||
...selectedConnection,
|
||||
label: e.target.value
|
||||
})}
|
||||
placeholder={CONNECTION_LABELS[selectedConnection.type]}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex justify-between w-full">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => selectedConnection && handleDeleteConnection(selectedConnection.id)}
|
||||
>
|
||||
<Trash size={16} className="mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setConnectionDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
if (selectedConnection) {
|
||||
setConnections((current) =>
|
||||
(current || []).map(c =>
|
||||
c.id === selectedConnection.id ? selectedConnection : c
|
||||
)
|
||||
)
|
||||
setConnectionDialogOpen(false)
|
||||
toast.success('Connection updated!')
|
||||
}
|
||||
}}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user