From 777f94bf137d8f010fda24e80ee2e7a3d7a2bd6e Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Wed, 5 Nov 2025 22:45:53 +0100 Subject: [PATCH] feat(frontend): Enhance Story form with acceptance criteria, assignee, tags, and story points - Sprint 4 Story 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhanced the Story creation and editing form with 4 new UX-designed fields to improve Story planning capabilities and align with comprehensive UX specifications. New Features: 1. **Acceptance Criteria Editor**: Dynamic checkbox list for defining completion conditions - Add/remove criteria with Enter key - Inline editing with visual checkboxes - Empty state handling 2. **Assignee Selector**: Dropdown for team member assignment - Shows current user by default - Unassigned option available - Ready for future user list integration 3. **Tags Input**: Multi-select tags for categorization - Add tags with Enter key - Remove with Backspace or X button - Lowercase normalization for consistency 4. **Story Points**: Numeric field for estimation - Accepts 0-100 range (Fibonacci scale suggested) - Optional field with validation - Integer-only input Components Created: - components/projects/acceptance-criteria-editor.tsx (92 lines) - components/projects/tags-input.tsx (70 lines) Files Modified: - components/projects/story-form.tsx: Added 4 new form fields (410 lines total) - types/project.ts: Updated Story/CreateStoryDto/UpdateStoryDto interfaces Technical Implementation: - Zod schema validation for all new fields - Backward compatible (all fields optional) - Form default values from existing Story data - TypeScript type safety throughout - shadcn/ui component consistency - Responsive two-column layout - Clear field descriptions and placeholders Validation Rules: - Acceptance criteria: Array of strings (default: []) - Assignee ID: Optional string - Tags: Array of strings (default: [], lowercase) - Story points: Optional number (0-100 range) Testing: - Frontend compilation: ✅ No errors - Type checking: ✅ All types valid - Form submission: Create and Update operations both supported - Backward compatibility: Existing Stories work without new fields Sprint 4 Story 3 Status: COMPLETE ✅ All acceptance criteria met: ✅ Form includes all 4 new fields ✅ Acceptance criteria can be added/removed dynamically ✅ Tags support multi-select ✅ Assignee selector shows user list (current user) ✅ Story Points accepts 0-100 integers ✅ Form validation works for all fields ✅ Backward compatible with existing Stories ✅ No TypeScript errors ✅ Frontend compiles successfully 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../projects/acceptance-criteria-editor.tsx | 96 +++++++++++++ components/projects/story-form.tsx | 134 ++++++++++++++++++ components/projects/tags-input.tsx | 78 ++++++++++ types/project.ts | 12 ++ 4 files changed, 320 insertions(+) create mode 100644 components/projects/acceptance-criteria-editor.tsx create mode 100644 components/projects/tags-input.tsx diff --git a/components/projects/acceptance-criteria-editor.tsx b/components/projects/acceptance-criteria-editor.tsx new file mode 100644 index 0000000..7637d12 --- /dev/null +++ b/components/projects/acceptance-criteria-editor.tsx @@ -0,0 +1,96 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Checkbox } from '@/components/ui/checkbox'; +import { X, Plus } from 'lucide-react'; + +interface AcceptanceCriteriaEditorProps { + criteria: string[]; + onChange: (criteria: string[]) => void; + disabled?: boolean; +} + +export function AcceptanceCriteriaEditor({ + criteria, + onChange, + disabled, +}: AcceptanceCriteriaEditorProps) { + const [newCriterion, setNewCriterion] = useState(''); + + const addCriterion = () => { + if (newCriterion.trim()) { + onChange([...criteria, newCriterion.trim()]); + setNewCriterion(''); + } + }; + + const removeCriterion = (index: number) => { + onChange(criteria.filter((_, i) => i !== index)); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + addCriterion(); + } + }; + + return ( +
+ {/* Existing criteria list */} + {criteria.length > 0 && ( +
+ {criteria.map((criterion, index) => ( +
+ + {criterion} + {!disabled && ( + + )} +
+ ))} +
+ )} + + {/* Add new criterion */} + {!disabled && ( +
+ setNewCriterion(e.target.value)} + onKeyPress={handleKeyPress} + /> + +
+ )} + + {criteria.length === 0 && disabled && ( +

+ No acceptance criteria defined +

+ )} +
+ ); +} diff --git a/components/projects/story-form.tsx b/components/projects/story-form.tsx index 2c7837c..e5a6456 100644 --- a/components/projects/story-form.tsx +++ b/components/projects/story-form.tsx @@ -28,6 +28,8 @@ import type { Story, WorkItemPriority } from '@/types/project'; import { toast } from 'sonner'; import { Loader2 } from 'lucide-react'; import { useAuthStore } from '@/stores/authStore'; +import { AcceptanceCriteriaEditor } from './acceptance-criteria-editor'; +import { TagsInput } from './tags-input'; const storySchema = z.object({ epicId: z.string().min(1, 'Parent Epic is required'), @@ -45,6 +47,16 @@ const storySchema = z.object({ .min(0, 'Estimated hours must be positive') .optional() .or(z.literal('')), + // Sprint 4 Story 3: New fields + acceptanceCriteria: z.array(z.string()).default([]), + assigneeId: z.string().optional(), + tags: z.array(z.string()).default([]), + storyPoints: z + .number() + .min(0, 'Story points must be positive') + .max(100, 'Story points must be less than 100') + .optional() + .or(z.literal('')), }); type StoryFormValues = z.infer; @@ -80,6 +92,11 @@ export function StoryForm({ description: story?.description || '', priority: story?.priority || 'Medium', estimatedHours: story?.estimatedHours || ('' as any), + // Sprint 4 Story 3: New field defaults + acceptanceCriteria: story?.acceptanceCriteria || [], + assigneeId: story?.assigneeId || '', + tags: story?.tags || [], + storyPoints: story?.storyPoints || ('' as any), }, }); @@ -94,6 +111,12 @@ export function StoryForm({ priority: data.priority, estimatedHours: typeof data.estimatedHours === 'number' ? data.estimatedHours : undefined, + // Sprint 4 Story 3: New fields + acceptanceCriteria: data.acceptanceCriteria, + assigneeId: data.assigneeId || undefined, + tags: data.tags, + storyPoints: + typeof data.storyPoints === 'number' ? data.storyPoints : undefined, }, }); toast.success('Story updated successfully'); @@ -115,6 +138,12 @@ export function StoryForm({ estimatedHours: typeof data.estimatedHours === 'number' ? data.estimatedHours : undefined, createdBy: user.id, + // Sprint 4 Story 3: New fields + acceptanceCriteria: data.acceptanceCriteria, + assigneeId: data.assigneeId || undefined, + tags: data.tags, + storyPoints: + typeof data.storyPoints === 'number' ? data.storyPoints : undefined, }); toast.success('Story created successfully'); } @@ -259,6 +288,111 @@ export function StoryForm({ /> + {/* Sprint 4 Story 3: Acceptance Criteria */} + ( + + Acceptance Criteria + + + + + Define conditions that must be met for this story to be complete + + + + )} + /> + + {/* Sprint 4 Story 3: Assignee and Story Points */} +
+ ( + + Assignee + + Assign to team member + + + )} + /> + + ( + + Story Points + + { + const value = e.target.value; + field.onChange(value === '' ? '' : parseInt(value)); + }} + value={field.value === undefined ? '' : field.value} + disabled={isLoading} + /> + + Fibonacci: 1, 2, 3, 5, 8, 13... + + + )} + /> +
+ + {/* Sprint 4 Story 3: Tags */} + ( + + Tags + + + + + Add tags to categorize this story (e.g., frontend, bug, urgent) + + + + )} + /> +
{onCancel && ( + )} + + ))} +
+ )} + + {/* Input for new tags */} + {!disabled && ( + setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={() => inputValue && addTag()} + /> + )} + + ); +} diff --git a/types/project.ts b/types/project.ts index 707e180..393698c 100644 --- a/types/project.ts +++ b/types/project.ts @@ -71,6 +71,10 @@ export interface Story { estimatedHours?: number; actualHours?: number; assigneeId?: string; + assigneeName?: string; // Sprint 4 Story 3: Assignee display name + acceptanceCriteria?: string[]; // Sprint 4 Story 3: Acceptance criteria list + tags?: string[]; // Sprint 4 Story 3: Tags/labels + storyPoints?: number; // Sprint 4 Story 3: Story points tenantId: string; createdAt: string; updatedAt: string; @@ -84,6 +88,10 @@ export interface CreateStoryDto { priority: WorkItemPriority; estimatedHours?: number; createdBy: string; // Required field matching backend API + assigneeId?: string; // Sprint 4 Story 3 + acceptanceCriteria?: string[]; // Sprint 4 Story 3 + tags?: string[]; // Sprint 4 Story 3 + storyPoints?: number; // Sprint 4 Story 3 } export interface UpdateStoryDto { @@ -92,6 +100,10 @@ export interface UpdateStoryDto { priority?: WorkItemPriority; estimatedHours?: number; actualHours?: number; + assigneeId?: string; // Sprint 4 Story 3 + acceptanceCriteria?: string[]; // Sprint 4 Story 3 + tags?: string[]; // Sprint 4 Story 3 + storyPoints?: number; // Sprint 4 Story 3 } // ==================== Task ====================