Skip to main content
Glama

Agile Backlog MCP

by ehartye
StoryFormModal.tsx•7.29 kB
import { useState, useEffect } from 'react'; import { X } from 'lucide-react'; interface Story { id?: number; epic_id: number | null; title: string; description: string; acceptance_criteria?: string | null; status: 'todo' | 'in_progress' | 'review' | 'done' | 'blocked'; priority: 'low' | 'medium' | 'high' | 'critical'; points?: number; } interface Epic { id: number; title: string; } interface StoryFormModalProps { isOpen: boolean; onClose: () => void; onSave: () => void; story?: Story | null; projectId: number; } export default function StoryFormModal({ isOpen, onClose, onSave, story, projectId }: StoryFormModalProps) { const [formData, setFormData] = useState<Story>({ epic_id: null, title: '', description: '', acceptance_criteria: '', status: 'todo', priority: 'medium', points: undefined, }); const [epics, setEpics] = useState<Epic[]>([]); useEffect(() => { if (isOpen) { fetchEpics(); } }, [isOpen, projectId]); useEffect(() => { if (story) { setFormData(story); } else { setFormData({ epic_id: null, title: '', description: '', acceptance_criteria: '', status: 'todo', priority: 'medium', points: undefined, }); } }, [story]); const fetchEpics = async () => { try { const response = await fetch(`/api/epics?project_id=${projectId}`); const data = await response.json(); setEpics(data); } catch (error) { console.error('Failed to fetch epics:', error); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { const url = story ? `/api/stories/${story.id}` : '/api/stories'; const method = story ? 'PATCH' : 'POST'; const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData), }); if (response.ok) { onSave(); onClose(); } } catch (error) { console.error('Failed to save story:', error); } }; if (!isOpen) return null; return ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4"> <div className="flex items-center justify-between p-6 border-b"> <h2 className="text-xl font-semibold">{story ? 'Edit Story' : 'Create Story'}</h2> <button onClick={onClose} className="text-gray-400 hover:text-gray-600"> <X size={24} /> </button> </div> <form onSubmit={handleSubmit} className="p-6 space-y-4"> <div> <label className="block text-sm font-medium text-gray-700 mb-1">Epic (Optional)</label> <select value={formData.epic_id || ''} onChange={(e) => setFormData({ ...formData, epic_id: e.target.value ? parseInt(e.target.value) : null })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" > <option value="">No Epic</option> {epics.map((epic) => ( <option key={epic.id} value={epic.id}> {epic.title} </option> ))} </select> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">Title</label> <input type="text" value={formData.title} onChange={(e) => setFormData({ ...formData, title: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" required /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">Description</label> <textarea value={formData.description} onChange={(e) => setFormData({ ...formData, description: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" rows={4} required /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">Acceptance Criteria (Optional)</label> <textarea value={formData.acceptance_criteria || ''} onChange={(e) => setFormData({ ...formData, acceptance_criteria: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" rows={3} placeholder="Enter acceptance criteria..." /> </div> <div className="grid grid-cols-3 gap-4"> <div> <label className="block text-sm font-medium text-gray-700 mb-1">Status</label> <select value={formData.status} onChange={(e) => setFormData({ ...formData, status: e.target.value as Story['status'] })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" > <option value="todo">To Do</option> <option value="in_progress">In Progress</option> <option value="review">Review</option> <option value="done">Done</option> <option value="blocked">Blocked</option> </select> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">Priority</label> <select value={formData.priority} onChange={(e) => setFormData({ ...formData, priority: e.target.value as Story['priority'] })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" > <option value="low">Low</option> <option value="medium">Medium</option> <option value="high">High</option> <option value="critical">Critical</option> </select> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">Points</label> <input type="number" value={formData.points || ''} onChange={(e) => setFormData({ ...formData, points: e.target.value ? parseInt(e.target.value) : undefined })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" min="0" /> </div> </div> <div className="flex gap-3 pt-4"> <button type="submit" className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium" > {story ? 'Update' : 'Create'} </button> <button type="button" onClick={onClose} className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 font-medium" > Cancel </button> </div> </form> </div> </div> ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ehartye/agile-backlog-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server