Skip to content

feat: add scan and create-custom actions to no-results search state (#578) - #995

Open
ibrahim-iqbal wants to merge 4 commits into
simonoppowa:developfrom
ibrahim-iqbal:feat/empty-search-actions-578
Open

feat: add scan and create-custom actions to no-results search state (#578)#995
ibrahim-iqbal wants to merge 4 commits into
simonoppowa:developfrom
ibrahim-iqbal:feat/empty-search-actions-578

Conversation

@ibrahim-iqbal

Copy link
Copy Markdown
Contributor

Summary

Zero-result searches used to hit a wall — just a "No results found" hint, no next step. This PR turns that empty state into a useful one by surfacing the two things the app can actually do next: scan a barcode or open the custom-food form.

Type of change

  • New feature
  • Localization

Related issues

Fixes #578.

Changes

  • `NoResultsWidget` now takes optional `onScanBarcode` and `onCreateCustomFood` callbacks and renders "Scan barcode" (`OutlinedButton.icon`) and "Create custom food" (`FilledButton.tonalIcon`) under a short explanatory subtitle when either callback is supplied. The bare-hint form still works for any caller that leaves both null.
  • All four call sites in `AddMealScreen` (All / Products / Food / Recent) pass both callbacks. Barcode reuses `_onBarcodeIconPressed`; create-custom reads the current imperial-units flag from `AddMealBloc` (via a new `_onCustomAddFromEmptyState` helper) before running the same confirm-then-open-editor flow as the app-bar Add button.
  • `Semantics(identifier: 'search-no-results-scan-barcode' | 'search-no-results-create-custom-food')` on both buttons, per the ADB / Appium driver convention in `AGENTS.md`.
  • Three new ARB keys: `noResultsSubtitle`, `noResultsScanBarcode`, `noResultsCreateCustomFood`. Added to every locale ARB (all nine stay at the same key count) with the English text carried across as a fallback until translations land via Weblate / Crowdin.
  • New `test/widget_test/no_results_widget_test.dart` covering the three visible shapes: no callbacks (bare hint), both callbacks (buttons render and fire), and scan-only (create button hidden). All 3 pass locally on Flutter 3.44.4.

Acceptance criteria coverage from the issue

  • Distinguish untouched/empty query from completed-with-zero-results — kept as-is; the existing `_pendingSpinner` / `DefaultsResultsWidget` / `NoResultsWidget` split already handles this.
  • Explain that no matching foods were found — new subtitle string.
  • Offer actions to scan a barcode and create a custom food.
  • Preserve the current query — the `ValueNotifier` in `_AddMealScreenState` already survives navigation to the scanner / editor and back.
  • Do not show zero-result state while providers are still loading — unchanged; `_productsPending` / `_foodPending` gate the empty state on both single-source and merged views.
  • Handle partial provider failures separately from genuine zero results — not tackled here. Currently a failed source is treated as answered (comment in `_buildResults`) so a merged view can still surface the other source's results. Turning this into a distinct visible state feels like its own PR; happy to tackle in a follow-up if you'd like.
  • Stable Semantics identifiers.
  • Widget tests for the visible shapes of `NoResultsWidget`. I did not add BLoC-level widget tests for initial / loading / results / partial-failure states of the whole search — those cover a lot of surface area beyond the empty-state fix and would be better as a separate test PR.
  • Localized strings in every ARB (English text carried into the locale files).

Screenshots / recordings

Not attached — I don't have a device set up locally to record right now. Layout is a `Wrap` under the existing `EmptyHint`, so buttons flow to a second row on narrow widths.

Test plan

  • Described steps below were followed locally
  • Unit / widget tests added or updated
  • Manual check on Android (not run — happy to run once someone with a device tries it)
  • Manual check on iOS

Steps

  1. Open a meal type from Home (e.g. Breakfast).
  2. Type a query with no matches (e.g. `asdfqwer`).
  3. Confirm the empty state shows the subtitle plus both action buttons.
  4. Tap "Scan barcode" — scanner opens with the current meal + day.
  5. Tap "Create custom food" — the existing confirm dialog opens; confirming pushes the edit-meal editor.
  6. Repeat with the Recent chip empty (no recent meals for this meal type).

Checklist

  • Code follows project style (`dart format`, 120-char line width — analyzer clean on the three changed files)
  • New interactive widgets include `Semantics(identifier: '...')`
  • Localization updated in ARBs (`lib/generated/` is gitignored — regenerated locally via `flutter gen-l10n` before running tests)
  • No codegen-touching changes
  • No secrets or `.env` values committed
  • PR title follows conventional commit style

simonoppowa added a commit that referenced this pull request Sep 4, 2026
…ing them (#1045)

Codex reviews pull requests vendor-side and reads AGENTS.md to do it, so
the conventions no generic reviewer could know now live in a Code Review
Rules section near the top of the file.

The rules are shaped by what a hand-run review of #995 got right and
wrong. The one check CI cannot do is a key present in every locale with
the English text still in it: check_l10n fails only on *missing* keys, so
that ships green. Two rules exist purely to prevent confident false
findings — ARB key counts legitimately differ between any two locales
because every file carries its own @key metadata, and `container: true`
is conditional on a layout-greedy parent, so its absence under a Wrap is
correct.

Codex truncates instruction files at 32 KiB, keeping the head and
dropping the tail, with no warning anywhere a human looks. AGENTS.md was
at 90.6% of that. Two blocks no reviewer can act on move out to make
room: the ADB driver scripts and uiautomator gotchas to
tools/adb/README.md, and the demo seeder explanation to
docs/demo-data.md. Both keep a pointer.

check_agents_md then fails the build before the cap is reached, and
asserts the rules stay near the head where truncation cannot take them.
Pure bash, so it runs first in linux-checks and needs no toolchain.

Also groups the four weekly Dependabot ecosystems, which previously
fanned out into a burst of individual bump PRs, and stops the PR template
asking contributors to edit gitignored lib/generated/.

AGENTS.md: 29,681 -> 28,140 bytes.
@simonoppowa

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 431e41fb60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +531 to +534
final state = locator<AddMealBloc>().state;
final imperial = state is AddMealLoadedState
? state.usesImperialUnits
: false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse the initialized bloc when reading unit settings

For users who selected imperial food units, this lookup creates a fresh AddMealBloc because that type is registered as a factory; the new instance has never received InitializeAddMealEvent, so the state is always initial and imperial always falls back to false. Consequently, creating a custom food from any new empty-state button opens the editor in metric mode, unlike the existing app-bar action, which reads its initialized bloc instance. Retain and read the screen's initialized bloc or pass the unit setting already available in the result state.

AGENTS.md reference: AGENTS.md:L355-L360

Useful? React with 👍 / 👎.

Comment on lines +391 to +395
return NoResultsWidget(
onScanBarcode: _onBarcodeIconPressed,
onCreateCustomFood: () =>
_onCustomAddFromEmptyState(),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the full no-results state out of partial-result lists

When a remote source returns no rows or fails but matching custom, recipe, history, or cached meals exist, remoteSourceEmpty appends this widget after those visible matches. Passing the callbacks now makes that footer say “No foods matched that search” and display the full recovery actions, directly contradicting the results immediately above it; this path needs a distinct “no remote results” footer or should omit the new full-empty-state content.

Useful? React with 👍 / 👎.

Comment on lines +19 to +20
return Column(
mainAxisSize: MainAxisSize.min,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the expanded empty state vertically scrollable

On a landscape phone or with a sufficiently large system text scale, the search header, source chips, and results header leave less vertical space than the fixed 72-pixel icon, padding, subtitle, and newly added action buttons require. This non-scrollable Column cannot shrink inside the tightly constrained results area, so it produces a RenderFlex overflow and can push the recovery buttons off-screen; allow the empty state to scroll or otherwise adapt to the available height.

Useful? React with 👍 / 👎.

@simonoppowa

Copy link
Copy Markdown
Owner

Thanks for this — the empty state is a real gap and the two actions are the right ones. A few things to fix before it can go in.

The localization is the blocker, and it is mostly copy-paste

The PR adds three keys, and all three carry the English string in every one of the eight non-English ARBs. CONTRIBUTING.md asks for a real translation rather than English as a placeholder, and just check_l10n cannot catch this — it fails only on missing keys, so a key present everywhere with English in it passes CI green.

The good news: the app already says one of these. customMealBarcodeScanButton is already exactly "Scan barcode" in English and translated in all nine locales. Please copy those values verbatim rather than write a second phrasing for the same action:

locale value to copy
de Barcode scannen
cs Naskenovat čárový kód
it Scansiona codice a barre
pl Skanuj kod kreskowy
sk Naskenovať kód
tr Barkod tara
uk Сканувати штрих-код
zh 扫描条形码

For the other two, these follow the terminology already in the ARBs — newCustomMealLabel and customMealsMergeAction for the noun, createRecipeTitle for the verb form:

noResultsCreateCustomFood

de Eigenes Lebensmittel erstellen
cs Vytvořit vlastní potravinu
it Crea alimento personalizzato
pl Utwórz własny produkt
sk Vytvoriť vlastné jedlo
tr Özel yiyecek oluştur
uk Створити власний продукт
zh 创建自定义食物

noResultsSubtitle

de Zu dieser Suche passt kein Lebensmittel. Versuche einen anderen Namen, scanne den Barcode oder lege ein eigenes an.
cs Tomuto hledání neodpovídá žádná potravina. Zkuste jiný název, naskenujte čárový kód nebo si vytvořte vlastní.
it Nessun alimento corrisponde a questa ricerca. Prova un altro nome, scansiona il codice a barre o creane uno personalizzato.
pl Żaden produkt nie pasuje do tego wyszukiwania. Spróbuj innej nazwy, zeskanuj kod kreskowy lub utwórz własny.
sk Tomuto vyhľadávaniu nezodpovedá žiadne jedlo. Skúste iný názov, naskenujte čiarový kód alebo si vytvorte vlastné.

(Machine translation is an accepted starting point here per CONTRIBUTING.md; native-speaker review after merge is welcome.)

Three code points

The unit lookup returns the wrong bloc. AddMealBloc is registered with registerFactory, so locator<AddMealBloc>() hands back a fresh instance that never received InitializeAddMealEvent — its state is always initial, so usesImperialUnits always reads false. An imperial user creating a custom food from the new empty state gets the editor in metric, unlike the existing app-bar action which reads its initialized instance. Read the screen's bloc, or pass the flag from the state you already have.

The footer contradicts the results above it. At the remoteSourceEmpty trailing item, passing the callbacks makes this widget render "No foods matched that search" plus the full recovery actions underneath real custom, recipe, history or cached matches. That path wants a narrower "no remote results" footer, or the bare widget without the new content.

Same on the Recent tab. With an empty query and no history the actioned widget now claims a search returned nothing when no search ran. noMealsRecentlyAddedLabel already exists and is the right copy there.

Two smaller ones

The Column isn't scrollable — in landscape or at a large text scale, the 72px icon plus subtitle plus two buttons can overflow the results area and push the buttons off-screen.

And a naming inconsistency worth catching now: the new button says "Create custom food" while the dialog it opens (createCustomDialogTitle) says "Create custom meal item?" — two nouns for one concept, one tap apart. Not introduced by you, but this PR is what puts them next to each other.

The Semantics(identifier:) values and the ARB key parity are both correct — thanks for getting those right, they're easy to miss.

@ibrahim-iqbal

Copy link
Copy Markdown
Contributor Author

Addressed all three points in 555f3ce:

  1. Imperial fallback: dropped the AddMealBloc lookup and now read usesImperialUnits off the source-specific state (ProductsLoadedState / FoodLoadedState / RecentMealLoadedState) that's already in scope at each call site. The factory registration made the old lookup return metric every time.
  2. remoteSourceEmpty footer: the products/food ListView footer now renders as const NoResultsWidget() with no callbacks, so the appended row shows the bare hint instead of contradicting the visible matches above. Recovery actions stay on the pure empty state.
  3. Overflow in landscape / large text scales: wrapped the widget's Column in SingleChildScrollView so the empty state scrolls when the results area is tighter than icon + subtitle + buttons need.

Existing widget tests still pass.

@ibrahim-iqbal

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough pass — everything's in as of 60117ba.

Localization — all three keys now carry a real string in every locale. noResultsScanBarcode is copied verbatim from customMealBarcodeScanButton per locale; noResultsCreateCustomFood and the five subtitles use your values. For the three subtitles you didn't supply (tr / uk / zh) I machine-translated against the nouns already in those ARBs (yiyecek / продукт / 食物, matching newCustomMealLabel) — happy to take a native pass post-merge. l10n_untranslated.json is {} after flutter gen-l10n.

Recent tab — empty query + no history now renders EmptyHint with noMealsRecentlyAddedLabel. The actioned search empty state only shows when a non-empty query filters Recent to nothing.

Unit lookup / footer / scroll — these landed in 555f3ce before this: imperial now comes from the source-specific loaded state at each call site, the remoteSourceEmpty footer is the bare widget, and NoResultsWidget is wrapped in SingleChildScrollView.

Naming — I kept "Create custom food" because it matches the ARB majority (newCustomMealLabel = "New Custom Food", customMealsMergeAction = "Merge with another custom food"); createCustomDialogTitle ("custom meal item") is the outlier. Aligning that title means touching nine locales for a string this PR doesn't own, so I left it — glad to open a small follow-up if you want it brought in line.

simonoppowa added a commit that referenced this pull request Sep 4, 2026
Both come from findings that were verified against the code after an
automated review raised them, on #995 and #1010.

Attribution. On #1010 half the findings were true about the code and
aimed at the wrong person: the reviewer did not separate what that PR
introduced from prose it merely inherited, and asked a contributor to fix
a sentence they had not touched. On a documentation-correction PR that
distinction is the whole value of the review.

Precedent. #995 added a key meaning "Scan barcode" and left it English
in eight locales. The useful finding was not that it needed translating:
customMealBarcodeScanButton is already exactly "Scan barcode" and
already translated in all nine. Neither the automated review nor a
hand-run one spotted the duplicate — it turned up only by looking for
precedents first. A second phrasing for one action is a worse outcome
than a late translation, and it is the one a reviewer can prevent.

AGENTS.md 28,140 -> 28,705 bytes; 2,295 under the check_agents_md guard.
@simonoppowa
simonoppowa changed the base branch from main to develop September 7, 2026 13:44
@simonoppowa

Copy link
Copy Markdown
Owner

Thanks for this — retargeting the base from main to develop, and flagging what that changes for you.

main only ever receives release merges in this repo (docs/RELEASING.md), so a feature PR based on it can't land as-is: merging it would put a change on main that develop doesn't have, which is the backport gap the release runbook exists to prevent. develop is the branch feature work targets.

The side effect is that GitHub now shows this as conflicting. That isn't a problem with your change — the branch was cut from main, and develop is currently 42 commits ahead of it, so the comparison is against a very different tree.

To get it green, rebase onto current develop:

git remote add upstream https://github.com/simonoppowa/OpenNutriTracker.git
git fetch upstream
git rebase upstream/develop
git push --force-with-lease

Your actual change (the scan and create-custom actions on the no-results search state, #578) should survive that cleanly — the conflicts are almost entirely in files you didn't touch, inherited from the old base.

Happy to help if the rebase turns up anything awkward.

…imonoppowa#578)

Zero-result searches previously only showed the "No results found" hint,
so users had no obvious next step even though the app can scan a barcode
or open a custom-food form.

NoResultsWidget now takes optional onScanBarcode and onCreateCustomFood
callbacks and renders "Scan barcode" and "Create custom food" buttons
under a short explanatory subtitle when they're supplied. The bare
"No results found" is still available for any caller that leaves both
callbacks null.

The four AddMealScreen call sites (All / Products / Food / Recent) pass
both callbacks: barcode reuses the existing scanner-navigation helper,
and create-custom reads the current imperial-units flag from the
AddMealBloc before opening the same confirm-and-open-editor flow as the
app-bar Add button.

Semantics identifiers on both buttons for the ADB / Appium drivers
described in AGENTS.md.

New strings added to every ARB (English text carried into locale files
until they're translated). Widget tests cover the three visible shapes:
no callbacks, both callbacks, and scan-only.
- Reuse the source-specific state's usesImperialUnits instead of the
  factory-registered AddMealBloc (which returned a fresh, uninitialized
  instance and always fell back to metric).
- Drop the recovery actions from the remoteSourceEmpty footer inside
  the products/food ListViews so it doesn't contradict the visible
  matches above it — the footer renders as the bare hint, actions
  stay on the pure empty state.
- Wrap NoResultsWidget in a SingleChildScrollView so the icon+text+
  buttons survive landscape and large system text scales without
  overflowing the tightly constrained results area.
noResultsScanBarcode copies customMealBarcodeScanButton verbatim in
every locale so the two scan actions read identically. The create and
subtitle strings follow the noun already used by newCustomMealLabel /
customMealsMergeAction per locale. tr, uk and zh subtitles are machine
translated as a starting point; the other five are as supplied in
review.
…ty state

With an empty query and no logged meals, the Recent tab rendered the
"no foods matched that search" state even though no search ran. Use
noMealsRecentlyAddedLabel there; the actioned search empty state now
only appears when a non-empty query filters Recent down to nothing.
@ibrahim-iqbal
ibrahim-iqbal force-pushed the feat/empty-search-actions-578 branch from 60117ba to bfd476a Compare September 8, 2026 06:48
@ibrahim-iqbal

Copy link
Copy Markdown
Contributor Author

Thanks for the retarget and the runbook. Rebased onto develop — since the branch was originally cut off main a straight git rebase upstream/develop tried to replay develop's own commits back on top of mine, so I cherry-picked the four changes onto a fresh develop head instead. Diff is now scoped exactly to the empty-state work (add_meal_screen, no_results_widget, the nine ARB entries you reviewed, and the widget test). CI is running.

Force-pushed with --force-with-lease.

@simonoppowa

Copy link
Copy Markdown
Owner

Thanks for the rebase and the translation pass — the ARBs are exactly right now (all nine at parity, Scan barcode copied verbatim per locale), analyze and tests are green on a local merge with develop, and the Recent-tab split is the behaviour I wanted.

One small thing before I merge, on the overflow fix from the last round. The SingleChildScrollView in NoResultsWidget only scrolls when its parent bounds its height. On the All and Recent tabs it sits inside an Expanded, so it works there — but on the Products and Food tabs (add_meal_screen.dart:397 and :450) it is returned as a plain child of the tab's Column, gets unbounded height, and never scrolls; the Column overflows instead. I reproduced it in a widget test at a 260px-tall results area: 32px overflow, maxScrollExtent 0. Wrapping those two returns in Flexible(child: ...), the same way the ListView right below them already is, fixes it.

Could you also change "Fixes #578" to "Part of #578"? You were upfront that partial-failure handling and the state tests are deferred, and I'd like the issue to stay open for those. Happy to take the follow-up whenever you have time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add next-step actions to empty food search results

2 participants