feat: US-009 - Frontend - Edit habit modal

This commit is contained in:
Echo
2026-02-10 16:45:11 +00:00
parent 60bf92a610
commit f838958bf2
2 changed files with 347 additions and 15 deletions

View File

@@ -853,6 +853,15 @@
function showAddHabitModal() {
const modal = document.getElementById('habitModal');
const form = document.getElementById('habitForm');
const modalTitle = document.querySelector('.modal-title');
const submitBtnText = document.getElementById('submitBtnText');
// Reset editing state
editingHabitId = null;
// Reset modal title and button text to create mode
modalTitle.textContent = 'Add Habit';
submitBtnText.textContent = 'Create Habit';
// Reset form
form.reset();
@@ -877,6 +886,9 @@
function closeHabitModal() {
const modal = document.getElementById('habitModal');
modal.classList.remove('active');
// Reset editing state
editingHabitId = null;
}
// Close modal when clicking outside
@@ -1004,6 +1016,9 @@
const submitBtn = document.getElementById('submitHabitBtn');
const submitBtnText = document.getElementById('submitBtnText');
// Determine if we're creating or editing
const isEditing = editingHabitId !== null;
// Get form values
const name = document.getElementById('habitName').value.trim();
const category = document.getElementById('habitCategory').value;
@@ -1061,11 +1076,14 @@
// Show loading state
submitBtn.disabled = true;
submitBtnText.textContent = 'Creating...';
submitBtnText.textContent = isEditing ? 'Saving...' : 'Creating...';
try {
const response = await fetch('/echo/api/habits', {
method: 'POST',
const url = isEditing ? `/echo/api/habits/${editingHabitId}` : '/echo/api/habits';
const method = isEditing ? 'PUT' : 'POST';
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
@@ -1078,16 +1096,16 @@
}
// Success
showToast('Habit created successfully!', 'success');
showToast(isEditing ? 'Habit updated!' : 'Habit created successfully!', 'success');
closeHabitModal();
await loadHabits();
} catch (error) {
console.error('Failed to create habit:', error);
showToast('Failed to create habit: ' + error.message, 'error');
console.error(`Failed to ${isEditing ? 'update' : 'create'} habit:`, error);
showToast(`Failed to ${isEditing ? 'update' : 'create'} habit: ` + error.message, 'error');
} finally {
submitBtn.disabled = false;
submitBtnText.textContent = 'Create Habit';
submitBtnText.textContent = isEditing ? 'Save Changes' : 'Create Habit';
}
}
@@ -1108,9 +1126,71 @@
}, 3000);
}
// Show edit habit modal (placeholder)
// Show edit habit modal
let editingHabitId = null; // Track which habit we're editing
function showEditHabitModal(habitId) {
alert('Edit Habit modal - coming in next story!');
const habit = habits.find(h => h.id === habitId);
if (!habit) {
showToast('Habit not found', 'error');
return;
}
editingHabitId = habitId;
const modal = document.getElementById('habitModal');
const form = document.getElementById('habitForm');
const modalTitle = document.querySelector('.modal-title');
const submitBtn = document.getElementById('submitHabitBtn');
const submitBtnText = document.getElementById('submitBtnText');
// Change modal title and button text
modalTitle.textContent = 'Edit Habit';
submitBtnText.textContent = 'Save Changes';
// Pre-populate form fields
document.getElementById('habitName').value = habit.name;
document.getElementById('habitCategory').value = habit.category || 'health';
document.getElementById('habitPriority').value = habit.priority || 3;
document.getElementById('habitNotes').value = habit.notes || '';
document.getElementById('frequencyType').value = habit.frequency?.type || 'daily';
document.getElementById('reminderTime').value = habit.reminderTime || '';
// Set selected color and icon
selectedColor = habit.color || '#3B82F6';
selectedIcon = habit.icon || 'dumbbell';
// Initialize color picker with current selection
initColorPicker();
// Initialize icon picker with current selection
initIconPicker();
// Update frequency params and pre-populate
updateFrequencyParams();
// Pre-populate frequency params based on type
const frequencyType = habit.frequency?.type;
if (frequencyType === 'specific_days' && habit.frequency.days) {
const dayCheckboxes = document.querySelectorAll('input[name="day"]');
dayCheckboxes.forEach(cb => {
cb.checked = habit.frequency.days.includes(parseInt(cb.value));
});
} else if (frequencyType === 'x_per_week' && habit.frequency.count) {
const xPerWeekInput = document.getElementById('xPerWeek');
if (xPerWeekInput) {
xPerWeekInput.value = habit.frequency.count;
}
} else if (frequencyType === 'custom' && habit.frequency.interval) {
const customIntervalInput = document.getElementById('customInterval');
if (customIntervalInput) {
customIntervalInput.value = habit.frequency.interval;
}
}
// Show modal
modal.classList.add('active');
lucide.createIcons();
}
// Delete habit (placeholder)

View File

@@ -3,6 +3,7 @@ Test suite for Habits frontend page structure and navigation
Story US-006: Frontend - Page structure, layout, and navigation link
Story US-007: Frontend - Habit card component
Story US-008: Frontend - Create habit modal with all options
Story US-009: Frontend - Edit habit modal
"""
import sys
@@ -417,17 +418,18 @@ def test_client_side_validation():
print("✓ Test 28: Client-side validation checks name required")
def test_submit_posts_to_api():
"""Test 29: Submit sends POST /echo/api/habits and refreshes list"""
"""Test 29: Submit sends POST /echo/api/habits (or PUT for edit) and refreshes list"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
assert 'submitHabitForm' in content, "Should have submitHabitForm function"
submit_func = content[content.find('function submitHabitForm'):]
assert "fetch('/echo/api/habits'" in submit_func or 'fetch("/echo/api/habits"' in submit_func, \
"Should POST to /echo/api/habits"
assert "'POST'" in submit_func or '"POST"' in submit_func, \
"Should use POST method"
# Check for conditional URL and method (since US-009 added edit support)
assert ('/echo/api/habits' in submit_func), \
"Should use /echo/api/habits endpoint"
assert ("'POST'" in submit_func or '"POST"' in submit_func or "'PUT'" in submit_func or '"PUT"' in submit_func), \
"Should use POST or PUT method"
assert 'JSON.stringify' in submit_func, "Should send JSON body"
assert 'loadHabits()' in submit_func, "Should refresh habit list on success"
@@ -508,6 +510,244 @@ def test_typecheck_us008():
print("✓ Test 33: Typecheck passes (all modal functions defined)")
def test_edit_modal_opens_on_gear_icon():
"""Test 34: Clicking gear icon on habit card opens edit modal"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check that gear icon exists with onclick handler
assert 'settings' in content, "Should have settings icon (gear)"
assert "showEditHabitModal" in content, "Should have showEditHabitModal function call"
# Check that showEditHabitModal function is defined and not a placeholder
assert 'function showEditHabitModal(habitId)' in content, "showEditHabitModal should be defined"
assert 'editingHabitId = habitId' in content or 'editingHabitId=habitId' in content, \
"Should set editingHabitId"
assert 'const habit = habits.find(h => h.id === habitId)' in content or \
'const habit=habits.find(h=>h.id===habitId)' in content, \
"Should find habit by ID"
print("✓ Test 34: Edit modal opens on gear icon click")
def test_edit_modal_prepopulated():
"""Test 35: Edit modal is pre-populated with all existing habit data"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check that form fields are pre-populated
assert "getElementById('habitName').value = habit.name" in content or \
"getElementById('habitName').value=habit.name" in content, \
"Should pre-populate habit name"
assert "getElementById('habitCategory').value = habit.category" in content or \
"getElementById('habitCategory').value=habit.category" in content, \
"Should pre-populate category"
assert "getElementById('habitPriority').value = habit.priority" in content or \
"getElementById('habitPriority').value=habit.priority" in content, \
"Should pre-populate priority"
assert "getElementById('habitNotes').value = habit.notes" in content or \
"getElementById('habitNotes').value=habit.notes" in content, \
"Should pre-populate notes"
assert "getElementById('frequencyType').value = habit.frequency" in content or \
"getElementById('frequencyType').value=habit.frequency" in content, \
"Should pre-populate frequency type"
# Check color and icon selection
assert 'selectedColor = habit.color' in content or 'selectedColor=habit.color' in content, \
"Should set selectedColor from habit"
assert 'selectedIcon = habit.icon' in content or 'selectedIcon=habit.icon' in content, \
"Should set selectedIcon from habit"
print("✓ Test 35: Edit modal pre-populated with habit data")
def test_edit_modal_title_and_button():
"""Test 36: Modal title shows 'Edit Habit' and button shows 'Save Changes'"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check that modal title is changed to Edit Habit
assert "modalTitle.textContent = 'Edit Habit'" in content or \
'modalTitle.textContent="Edit Habit"' in content or \
"modalTitle.textContent='Edit Habit'" in content, \
"Should set modal title to 'Edit Habit'"
# Check that submit button text is changed
assert "submitBtnText.textContent = 'Save Changes'" in content or \
'submitBtnText.textContent="Save Changes"' in content or \
"submitBtnText.textContent='Save Changes'" in content, \
"Should set button text to 'Save Changes'"
print("✓ Test 36: Modal title shows 'Edit Habit' and button shows 'Save Changes'")
def test_edit_modal_frequency_params():
"""Test 37: Frequency params display correctly for habit's current frequency type"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check that updateFrequencyParams is called
assert 'updateFrequencyParams()' in content, "Should call updateFrequencyParams()"
# Check that frequency params are pre-populated for specific types
assert 'specific_days' in content and 'habit.frequency.days' in content, \
"Should handle specific_days frequency params"
assert 'x_per_week' in content and 'habit.frequency.count' in content, \
"Should handle x_per_week frequency params"
assert 'custom' in content and 'habit.frequency.interval' in content, \
"Should handle custom frequency params"
# Check that day checkboxes are pre-populated
assert 'cb.checked = habit.frequency.days.includes' in content or \
'cb.checked=habit.frequency.days.includes' in content, \
"Should pre-select days for specific_days frequency"
print("✓ Test 37: Frequency params display correctly for current frequency")
def test_edit_modal_icon_color_pickers():
"""Test 38: Icon and color pickers show current selections"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check that pickers are initialized after setting values
assert 'initColorPicker()' in content, "Should call initColorPicker()"
assert 'initIconPicker()' in content, "Should call initIconPicker()"
# Check that selectedColor and selectedIcon are set before initialization
showEditIndex = content.find('function showEditHabitModal')
initColorIndex = content.find('initColorPicker()', showEditIndex)
selectedColorIndex = content.find('selectedColor = habit.color', showEditIndex)
assert selectedColorIndex > 0 and selectedColorIndex < initColorIndex, \
"Should set selectedColor before calling initColorPicker()"
print("✓ Test 38: Icon and color pickers show current selections")
def test_edit_modal_submit_put():
"""Test 39: Submit sends PUT /echo/api/habits/{id} and refreshes list on success"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check that editingHabitId is tracked
assert 'let editingHabitId' in content or 'editingHabitId' in content, \
"Should track editingHabitId"
# Check that isEditing is determined
assert 'const isEditing = editingHabitId !== null' in content or \
'const isEditing=editingHabitId!==null' in content or \
'isEditing = editingHabitId !== null' in content, \
"Should determine if editing"
# Check that URL and method are conditional
assert "const url = isEditing ? `/echo/api/habits/${editingHabitId}` : '/echo/api/habits'" in content or \
'const url=isEditing?`/echo/api/habits/${editingHabitId}`' in content or \
"url = isEditing ? `/echo/api/habits/${editingHabitId}` : '/echo/api/habits'" in content, \
"URL should be conditional based on isEditing"
assert "const method = isEditing ? 'PUT' : 'POST'" in content or \
"const method=isEditing?'PUT':'POST'" in content or \
"method = isEditing ? 'PUT' : 'POST'" in content, \
"Method should be conditional (PUT for edit, POST for create)"
# Check that loadHabits is called after success
assert 'await loadHabits()' in content, "Should refresh habit list after success"
print("✓ Test 39: Submit sends PUT and refreshes list")
def test_edit_modal_toast_messages():
"""Test 40: Toast shown for success and error"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check for conditional success message
assert "isEditing ? 'Habit updated!' : 'Habit created successfully!'" in content or \
"isEditing?'Habit updated!':'Habit created successfully!'" in content, \
"Should show different toast message for edit vs create"
# Check that error toast handles both edit and create
assert 'Failed to ${isEditing' in content or 'Failed to ' + '${isEditing' in content, \
"Error toast should be conditional"
print("✓ Test 40: Toast messages for success and error")
def test_edit_modal_add_resets_state():
"""Test 41: showAddHabitModal resets editingHabitId and modal UI"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Find showAddHabitModal function
add_modal_start = content.find('function showAddHabitModal()')
add_modal_end = content.find('function ', add_modal_start + 1)
add_modal_func = content[add_modal_start:add_modal_end]
# Check that editingHabitId is reset
assert 'editingHabitId = null' in add_modal_func or 'editingHabitId=null' in add_modal_func, \
"showAddHabitModal should reset editingHabitId to null"
# Check that modal title is reset to 'Add Habit'
assert "modalTitle.textContent = 'Add Habit'" in add_modal_func or \
'modalTitle.textContent="Add Habit"' in add_modal_func, \
"Should reset modal title to 'Add Habit'"
# Check that button text is reset to 'Create Habit'
assert "submitBtnText.textContent = 'Create Habit'" in add_modal_func or \
'submitBtnText.textContent="Create Habit"' in add_modal_func, \
"Should reset button text to 'Create Habit'"
print("✓ Test 41: showAddHabitModal resets editing state")
def test_edit_modal_close_resets_state():
"""Test 42: closeHabitModal resets editingHabitId"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Find closeHabitModal function
close_modal_start = content.find('function closeHabitModal()')
close_modal_end = content.find('function ', close_modal_start + 1)
close_modal_func = content[close_modal_start:close_modal_end]
# Check that editingHabitId is reset when closing
assert 'editingHabitId = null' in close_modal_func or 'editingHabitId=null' in close_modal_func, \
"closeHabitModal should reset editingHabitId to null"
print("✓ Test 42: closeHabitModal resets editing state")
def test_edit_modal_no_console_errors():
"""Test 43: No obvious console error sources in edit modal code"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check for common error patterns
assert content.count('getElementById(') > 0, "Should use getElementById"
# Check that habit is validated before use
showEditIndex = content.find('function showEditHabitModal')
showEditEnd = content.find('\n }', showEditIndex + 500) # Find end of function
showEditFunc = content[showEditIndex:showEditEnd]
assert 'if (!habit)' in showEditFunc or 'if(!habit)' in showEditFunc, \
"Should check if habit exists before using it"
assert 'showToast' in showEditFunc and 'error' in showEditFunc, \
"Should show error toast if habit not found"
print("✓ Test 43: No obvious console error sources")
def test_typecheck_us009():
"""Test 44: Typecheck passes - all edit modal functions and variables defined"""
habits_path = Path(__file__).parent.parent / 'habits.html'
content = habits_path.read_text()
# Check that editingHabitId is declared
assert 'let editingHabitId' in content, "editingHabitId should be declared"
# Check that showEditHabitModal is fully implemented (not placeholder)
assert 'function showEditHabitModal(habitId)' in content, "showEditHabitModal should be defined"
assert 'alert' not in content[content.find('function showEditHabitModal'):content.find('function showEditHabitModal')+1000], \
"showEditHabitModal should not be a placeholder with alert()"
# Check that submitHabitForm handles both create and edit
assert 'const isEditing' in content or 'isEditing' in content, \
"submitHabitForm should determine if editing"
print("✓ Test 44: Typecheck passes (edit modal fully implemented)")
def run_all_tests():
"""Run all tests in sequence"""
tests = [
@@ -547,9 +787,21 @@ def run_all_tests():
test_toast_notifications,
test_modal_no_console_errors,
test_typecheck_us008,
# US-009 tests
test_edit_modal_opens_on_gear_icon,
test_edit_modal_prepopulated,
test_edit_modal_title_and_button,
test_edit_modal_frequency_params,
test_edit_modal_icon_color_pickers,
test_edit_modal_submit_put,
test_edit_modal_toast_messages,
test_edit_modal_add_resets_state,
test_edit_modal_close_resets_state,
test_edit_modal_no_console_errors,
test_typecheck_us009,
]
print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, and US-008...\n")
print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, US-008, and US-009...\n")
failed = []
for test in tests: