Skip to content

Commit 3e138d2

Browse files
authored
Merge pull request #592 from wger-project/ingredient-form-ux
IngredientForm UX fixes
2 parents a3734a4 + 436ee2a commit 3e138d2

File tree

3 files changed

+80
-26
lines changed

3 files changed

+80
-26
lines changed

lib/widgets/nutrition/forms.dart

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@ class IngredientFormState extends State<IngredientForm> {
169169
final _timeController = TextEditingController(); // optional
170170
final _mealItem = MealItem.empty();
171171

172-
bool validIngredientId = false;
173172
@override
174173
void initState() {
175174
super.initState();
@@ -182,6 +181,26 @@ class IngredientFormState extends State<IngredientForm> {
182181

183182
MealItem get mealItem => _mealItem;
184183

184+
void selectIngredient(int id, String name, num? amount) {
185+
setState(() {
186+
_mealItem.ingredientId = id;
187+
_ingredientController.text = name;
188+
_ingredientIdController.text = id.toString();
189+
if (amount != null) {
190+
_amountController.text = amount.toStringAsFixed(0);
191+
_mealItem.amount = amount;
192+
}
193+
});
194+
}
195+
196+
// note: does not reset text search and amount inputs
197+
void unSelectIngredient() {
198+
setState(() {
199+
_mealItem.ingredientId = 0;
200+
_ingredientIdController.text = '';
201+
});
202+
}
203+
185204
@override
186205
Widget build(BuildContext context) {
187206
final String unit = AppLocalizations.of(context).g;
@@ -197,6 +216,8 @@ class IngredientFormState extends State<IngredientForm> {
197216
_ingredientController,
198217
barcode: widget.barcode,
199218
test: widget.test,
219+
selectIngredient: selectIngredient,
220+
unSelectIngredient: unSelectIngredient,
200221
),
201222
Row(
202223
children: [
@@ -287,7 +308,7 @@ class IngredientFormState extends State<IngredientForm> {
287308
),
288309
],
289310
),
290-
if (validIngredientId)
311+
if (ingredientIdController.text.isNotEmpty && _amountController.text.isNotEmpty)
291312
Padding(
292313
padding: const EdgeInsets.all(8.0),
293314
child: Column(
@@ -302,10 +323,9 @@ class IngredientFormState extends State<IngredientForm> {
302323
builder: (BuildContext context, AsyncSnapshot<Ingredient> snapshot) {
303324
if (snapshot.hasData) {
304325
_mealItem.ingredient = snapshot.data!;
305-
return ListTile(
306-
leading: IngredientAvatar(ingredient: _mealItem.ingredient),
307-
title:
308-
Text(getShortNutritionValues(_mealItem.nutritionalValues, context)),
326+
return MealItemTile(
327+
ingredient: _mealItem.ingredient,
328+
nutritionalValues: _mealItem.nutritionalValues,
309329
);
310330
} else if (snapshot.hasError) {
311331
return Padding(
@@ -328,9 +348,7 @@ class IngredientFormState extends State<IngredientForm> {
328348
),
329349
),
330350
ElevatedButton(
331-
key: const Key(
332-
SUBMIT_BUTTON_KEY_NAME), // needed? mealItemForm had it, but not ingredientlogform
333-
351+
key: const Key(SUBMIT_BUTTON_KEY_NAME),
334352
child: Text(AppLocalizations.of(context).save),
335353
onPressed: () async {
336354
if (!_form.currentState!.validate()) {
@@ -365,15 +383,9 @@ class IngredientFormState extends State<IngredientForm> {
365383
return Card(
366384
child: ListTile(
367385
onTap: () {
368-
_ingredientController.text = widget.recent[index].ingredient.name;
369-
_ingredientIdController.text =
370-
widget.recent[index].ingredient.id.toString();
371-
_amountController.text = widget.recent[index].amount.toStringAsFixed(0);
372-
setState(() {
373-
_mealItem.ingredientId = widget.recent[index].ingredientId;
374-
_mealItem.amount = widget.recent[index].amount;
375-
validIngredientId = true;
376-
});
386+
final ingredient = widget.recent[index].ingredient;
387+
selectIngredient(
388+
ingredient.id, ingredient.name, widget.recent[index].amount);
377389
},
378390
title: Text(
379391
'${widget.recent[index].ingredient.name} (${widget.recent[index].amount.toStringAsFixed(0)}$unit)'),

lib/widgets/nutrition/widgets.dart

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import 'package:wger/models/exercises/ingredient_api.dart';
3232
import 'package:wger/models/nutrition/ingredient.dart';
3333
import 'package:wger/models/nutrition/log.dart';
3434
import 'package:wger/models/nutrition/nutritional_plan.dart';
35+
import 'package:wger/models/nutrition/nutritional_values.dart';
3536
import 'package:wger/providers/nutrition.dart';
3637
import 'package:wger/widgets/core/core.dart';
3738
import 'package:wger/widgets/nutrition/helpers.dart';
@@ -62,12 +63,17 @@ class IngredientTypeahead extends StatefulWidget {
6263
final bool? test;
6364
final bool showScanner;
6465

66+
final Function(int id, String name, num? amount) selectIngredient;
67+
final Function() unSelectIngredient;
68+
6569
const IngredientTypeahead(
6670
this._ingredientIdController,
6771
this._ingredientController, {
6872
this.showScanner = true,
6973
this.test = false,
7074
this.barcode = '',
75+
required this.selectIngredient,
76+
required this.unSelectIngredient,
7177
});
7278

7379
@override
@@ -118,6 +124,10 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
118124
}
119125
return null;
120126
},
127+
onChanged: (value) {
128+
// unselect to start a new search
129+
widget.unSelectIngredient();
130+
},
121131
decoration: InputDecoration(
122132
prefixIcon: const Icon(Icons.search),
123133
labelText: AppLocalizations.of(context).searchIngredient,
@@ -126,7 +136,8 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
126136
);
127137
},
128138
suggestionsCallback: (pattern) {
129-
if (pattern == '') {
139+
// don't do search if user has already loaded a specific item
140+
if (pattern == '' || widget._ingredientIdController.text.isNotEmpty) {
130141
return null;
131142
}
132143

@@ -151,10 +162,7 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
151162
child: child,
152163
),
153164
onSelected: (suggestion) {
154-
//SuggestionsController.of(context).;
155-
156-
widget._ingredientIdController.text = suggestion.data.id.toString();
157-
widget._ingredientController.text = suggestion.value;
165+
widget.selectIngredient(suggestion.data.id, suggestion.value, null);
158166
},
159167
),
160168
if (Localizations.localeOf(context).languageCode != LANGUAGE_SHORT_ENGLISH)
@@ -188,6 +196,7 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
188196
}
189197
final result = await Provider.of<NutritionPlansProvider>(context, listen: false)
190198
.searchIngredientWithCode(barcode);
199+
// TODO: show spinner...
191200
if (!mounted) {
192201
return;
193202
}
@@ -198,14 +207,22 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
198207
builder: (ctx) => AlertDialog(
199208
key: const Key('found-dialog'),
200209
title: Text(AppLocalizations.of(context).productFound),
201-
content: Text(AppLocalizations.of(context).productFoundDescription(result.name)),
210+
content: Column(
211+
mainAxisSize: MainAxisSize.min,
212+
children: [
213+
Text(AppLocalizations.of(context).productFoundDescription(result.name)),
214+
MealItemTile(
215+
ingredient: result,
216+
nutritionalValues: result.nutritionalValues,
217+
),
218+
],
219+
),
202220
actions: [
203221
TextButton(
204222
key: const Key('found-dialog-confirm-button'),
205223
child: Text(MaterialLocalizations.of(context).continueButtonLabel),
206224
onPressed: () {
207-
widget._ingredientController.text = result.name;
208-
widget._ingredientIdController.text = result.id.toString();
225+
widget.selectIngredient(result.id, result.name, null);
209226
Navigator.of(ctx).pop();
210227
},
211228
),
@@ -357,3 +374,23 @@ class IngredientAvatar extends StatelessWidget {
357374
: const CircleIconAvatar(Icon(Icons.image, color: Colors.grey));
358375
}
359376
}
377+
378+
class MealItemTile extends StatelessWidget {
379+
final Ingredient ingredient;
380+
final NutritionalValues nutritionalValues;
381+
382+
const MealItemTile({
383+
super.key,
384+
required this.ingredient,
385+
required this.nutritionalValues,
386+
});
387+
388+
@override
389+
Widget build(BuildContext context) {
390+
return ListTile(
391+
leading: IngredientAvatar(ingredient: ingredient),
392+
title: Text(getShortNutritionValues(nutritionalValues, context)),
393+
// subtitle: Text(ingredient.id.toString()),
394+
);
395+
}
396+
}

test/nutrition/nutritional_meal_item_form_test.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,11 @@ void main() {
305305

306306
expect(formState.ingredientIdController.text, '1');
307307

308+
// once ID and weight are set, it'll fetchIngredient and show macros preview
309+
when(mockNutrition.fetchIngredient(1)).thenAnswer((_) => Future.value(
310+
Ingredient.fromJson(jsonDecode(fixture('nutrition/ingredient_59887_response.json'))),
311+
));
312+
308313
await tester.enterText(find.byKey(const Key('field-weight')), '2');
309314
await tester.pumpAndSettle();
310315

0 commit comments

Comments
 (0)