chore(conductor): Mark track 'Add real-time search and alphabetical sorting to the Add Meal and Add Food modals on the tracker page.' as complete

This commit is contained in:
2026-03-10 07:21:17 -07:00
parent cc4b8301b3
commit e0f72d4f1f
6 changed files with 181 additions and 29 deletions

View File

@@ -77,12 +77,15 @@ async def tracker_page(request: Request, person: str = "Sarah", date: str = None
# Template will handle filtering of deleted foods
# Get all meals for dropdown (exclude snapshots)
meals = db.query(Meal).filter(Meal.meal_type != "tracked_snapshot").all()
meals.sort(key=lambda x: x.name.lower())
# Get all templates for template dropdown
templates_list = db.query(Template).all()
templates_list.sort(key=lambda x: x.name.lower())
# Get all foods for dropdown
foods = db.query(Food).all()
foods.sort(key=lambda x: x.name.lower())
# Calculate day totals
day_totals = calculate_day_nutrition_tracked(tracked_meals, db)

View File

@@ -9,5 +9,5 @@ This file tracks all major tracks for the project. Each track has its own detail
---
- [ ] **Track: Add real-time search and alphabetical sorting to the Add Meal and Add Food modals on the tracker page.**
- [x] **Track: Add real-time search and alphabetical sorting to the Add Meal and Add Food modals on the tracker page.**
*Link: [./tracks/meal_food_search_20260310/](./tracks/meal_food_search_20260310/)*

View File

@@ -1,32 +1,32 @@
# Implementation Plan: Meal/Food Search & Sorting (Track: meal_food_search_20260310)
## Phase 1: Preparation and Testing
- [ ] Task: Create a new Playwright test file `tests/meal_food_search.spec.js` to verify searching and sorting in modals.
- [ ] Task: Write failing E2E tests for:
- [ ] Modal opening and initial alphabetical sorting of Meals.
- [ ] Real-time filtering in the "Add Meal" modal.
- [ ] Modal opening and initial alphabetical sorting of Foods.
- [ ] Real-time filtering (by name and brand) in the "Add Food" modal.
- [ ] Task: Run the tests and confirm they fail.
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Preparation and Testing' (Protocol in workflow.md)
- [x] Task: Create a new Playwright test file `tests/meal_food_search.spec.js` to verify searching and sorting in modals.
- [x] Task: Write failing E2E tests for:
- [x] Modal opening and initial alphabetical sorting of Meals.
- [x] Real-time filtering in the "Add Meal" modal.
- [x] Modal opening and initial alphabetical sorting of Foods.
- [x] Real-time filtering (by name and brand) in the "Add Food" modal.
- [x] Task: Run the tests and confirm they fail.
- [x] Task: Conductor - User Manual Verification 'Phase 1: Preparation and Testing' (Protocol in workflow.md)
## Phase 2: Implement Sorting & Searching in "Add Meal" Modal
- [ ] Task: Update `templates/modals/add_meal.html` to add a search input field above the meal list.
- [ ] Task: Add a unique `data-testid` to the search input and individual list items for test reliability.
- [ ] Task: Update the backend route `app/api/routes/tracker.py` to sort the `meals` list alphabetically before passing it to the template.
- [ ] Task: Implement client-side JavaScript in `templates/modals/add_meal.html` to filter the meal list in real-time as the user types.
- [ ] Task: Verify Phase 2 with E2E tests.
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Implement Sorting & Searching in Add Meal Modal' (Protocol in workflow.md)
- [x] Task: Update `templates/modals/add_meal.html` to add a search input field above the meal list.
- [x] Task: Add a unique `data-testid` to the search input and individual list items for test reliability.
- [x] Task: Update the backend route `app/api/routes/tracker.py` to sort the `meals` list alphabetically before passing it to the template.
- [x] Task: Implement client-side JavaScript in `templates/modals/add_meal.html` to filter the meal list in real-time as the user types.
- [x] Task: Verify Phase 2 with E2E tests.
- [x] Task: Conductor - User Manual Verification 'Phase 2: Implement Sorting & Searching in Add Meal Modal' (Protocol in workflow.md)
## Phase 3: Implement Sorting & Searching in "Add Food" Modal
- [ ] Task: Update `templates/modals/add_single_food.html` (or the relevant "Add Food" modal) to add a search input field above the food list.
- [ ] Task: Add a unique `data-testid` to the search input and food list items.
- [ ] Task: Update the backend route `app/api/routes/tracker.py` to sort the `foods` list alphabetically before passing it to the template.
- [ ] Task: Implement client-side JavaScript in the "Add Food" modal to filter by both `name` and `brand` in real-time.
- [ ] Task: Verify Phase 3 with E2E tests.
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Implement Sorting & Searching in Add Food Modal' (Protocol in workflow.md)
- [x] Task: Update `templates/modals/add_single_food.html` (or the relevant "Add Food" modal) to add a search input field above the food list.
- [x] Task: Add a unique `data-testid` to the search input and food list items.
- [x] Task: Update the backend route `app/api/routes/tracker.py` to sort the `foods` list alphabetically before passing it to the template.
- [x] Task: Implement client-side JavaScript in the "Add Food" modal to filter by both `name` and `brand` in real-time.
- [x] Task: Verify Phase 3 with E2E tests.
- [x] Task: Conductor - User Manual Verification 'Phase 3: Implement Sorting & Searching in Add Food Modal' (Protocol in workflow.md)
## Phase 4: Final Verification and Cleanup
- [ ] Task: Perform a final run of all tests (E2E and Backend).
- [ ] Task: Ensure code coverage for any new logic (if applicable) is >80%.
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Final Verification and Cleanup' (Protocol in workflow.md)
- [x] Task: Perform a final run of all tests (E2E and Backend).
- [x] Task: Ensure code coverage for any new logic (if applicable) is >80%.
- [x] Task: Conductor - User Manual Verification 'Phase 4: Final Verification and Cleanup' (Protocol in workflow.md)

View File

@@ -13,10 +13,11 @@
<input type="hidden" name="date" value="{{ current_date.isoformat() }}">
<div class="mb-3">
<label class="form-label">Select Meal</label>
<select class="form-control" name="meal_id" required>
<input type="text" class="form-control mb-2" id="mealSearchInput" placeholder="Search meals..." data-testid="meal-search-input">
<select class="form-control" name="meal_id" id="mealSelect" required size="10">
<option value="">Choose meal...</option>
{% for meal in meals %}
<option value="{{ meal.id }}">{{ meal.name }}</option>
<option value="{{ meal.id }}" data-testid="meal-option">{{ meal.name }}</option>
{% endfor %}
</select>
</div>
@@ -28,4 +29,33 @@
</div>
</div>
</div>
</div>
</div>
<script>
document.getElementById('mealSearchInput').addEventListener('input', function() {
const searchText = this.value.toLowerCase();
const select = document.getElementById('mealSelect');
const options = select.options;
for (let i = 0; i < options.length; i++) {
const option = options[i];
if (option.value === "") continue; // Skip "Choose meal..."
const text = option.text.toLowerCase();
if (text.includes(searchText)) {
option.style.display = "";
} else {
option.style.display = "none";
}
}
});
// Reset search when modal is shown
document.getElementById('addMealModal').addEventListener('show.bs.modal', function () {
document.getElementById('mealSearchInput').value = '';
const options = document.getElementById('mealSelect').options;
for (let i = 0; i < options.length; i++) {
options[i].style.display = "";
}
});
</script>

View File

@@ -12,10 +12,11 @@
<input type="hidden" name="date" value="{{ current_date.isoformat() }}">
<div class="mb-3">
<label class="form-label">Select Food</label>
<select class="form-control" name="food_id" required onchange="updateServingSizeNote(this)">
<input type="text" class="form-control mb-2" id="foodSearchInput" placeholder="Search foods by name or brand..." data-testid="food-search-input">
<select class="form-control" name="food_id" id="foodSelect" required onchange="updateServingSizeNote(this)" size="10">
<option value="">Choose food...</option>
{% for food in foods %}
<option value="{{ food.id }}" data-serving-size="{{ food.serving_size }}">{{ food.name }}</option>
<option value="{{ food.id }}" data-serving-size="{{ food.serving_size }}" data-brand="{{ food.brand }}" data-testid="food-option">{{ food.name }}</option>
{% endfor %}
</select>
</div>
@@ -43,6 +44,36 @@
servingSizeNote.textContent = '';
}
}
document.getElementById('foodSearchInput').addEventListener('input', function() {
const searchText = this.value.toLowerCase();
const select = document.getElementById('foodSelect');
const options = select.options;
for (let i = 0; i < options.length; i++) {
const option = options[i];
if (option.value === "") continue; // Skip "Choose food..."
const text = option.text.toLowerCase();
const brand = (option.getAttribute('data-brand') || "").toLowerCase();
if (text.includes(searchText) || brand.includes(searchText)) {
option.style.display = "";
} else {
option.style.display = "none";
}
}
});
// Reset search when modal is shown
document.getElementById('addSingleFoodModal').addEventListener('show.bs.modal', function () {
document.getElementById('foodSearchInput').value = '';
const options = document.getElementById('foodSelect').options;
for (let i = 0; i < options.length; i++) {
options[i].style.display = "";
}
document.getElementById('servingSizeNote').textContent = '';
});
</script>
</div>
</div>

View File

@@ -0,0 +1,88 @@
const { test, expect } = require('@playwright/test');
test.describe('Meal and Food Search & Sorting', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tracker');
// Ensure we're in a known state
page.on('dialog', dialog => dialog.accept());
const resetBtn = page.getByRole('button', { name: 'Reset Page' });
if (await resetBtn.isVisible()) {
await resetBtn.click();
}
});
test('Add Meal modal should be sorted alphabetically and searchable', async ({ page }) => {
// Open Add Meal modal for Breakfast
await page.locator('[data-testid="add-meal-breakfast"]').click();
// Check for search input
const searchInput = page.locator('[data-testid="meal-search-input"]');
await expect(searchInput).toBeVisible();
// Check alphabetical sorting
const mealOptions = page.locator('[data-testid="meal-option"]');
const count = await mealOptions.count();
let prevText = "";
for (let i = 0; i < count; i++) {
const text = await mealOptions.nth(i).innerText();
if (text.trim() === "" || text.includes("Choose meal...")) continue;
if (prevText !== "") {
console.log(`Comparing: "${prevText}" to "${text}"`);
const cmp = text.localeCompare(prevText, 'en', { sensitivity: 'base' });
if (cmp < 0) {
console.error(`Sort order failure: "${prevText}" should come after "${text}" (cmp: ${cmp})`);
}
expect(cmp).toBeGreaterThanOrEqual(0);
}
prevText = text;
}
// Test real-time filtering
await searchInput.fill('Protein');
const filteredOptions = page.locator('[data-testid="meal-option"]:visible');
const filteredCount = await filteredOptions.count();
for (let i = 0; i < filteredCount; i++) {
const text = await filteredOptions.nth(i).innerText();
expect(text.toLowerCase()).toContain('protein');
}
});
test('Add Food modal should be sorted alphabetically and searchable', async ({ page }) => {
// Open Add Food modal for Breakfast
await page.locator('[data-testid="add-food-breakfast"]').click();
// Check for search input
const searchInput = page.locator('[data-testid="food-search-input"]');
await expect(searchInput).toBeVisible();
// Check alphabetical sorting
const foodOptions = page.locator('[data-testid="food-option"]');
const count = await foodOptions.count();
let prevText = "";
for (let i = 0; i < count; i++) {
const text = await foodOptions.nth(i).innerText();
if (text.trim() === "" || text.includes("Choose food...")) continue;
if (prevText !== "") {
console.log(`Comparing: "${prevText}" to "${text}"`);
const cmp = text.localeCompare(prevText, 'en', { sensitivity: 'base' });
if (cmp < 0) {
console.error(`Sort order failure: "${prevText}" should come after "${text}" (cmp: ${cmp})`);
}
expect(cmp).toBeGreaterThanOrEqual(0);
}
prevText = text;
}
// Test real-time filtering
await searchInput.fill('Organic');
const filteredOptions = page.locator('[data-testid="food-option"]:visible');
const filteredCount = await filteredOptions.count();
for (let i = 0; i < filteredCount; i++) {
const text = (await filteredOptions.nth(i).innerText()).toLowerCase();
const brand = (await filteredOptions.nth(i).getAttribute('data-brand') || "").toLowerCase();
expect(text.includes('organic') || brand.includes('organic')).toBeTruthy();
}
});
});