Skip to content

feat: tag saved plants and filter the list by tag - #33

Open
gwado wants to merge 4 commits into
patrickjane:masterfrom
gwado:feat/plant-tags-and-filters
Open

feat: tag saved plants and filter the list by tag#33
gwado wants to merge 4 commits into
patrickjane:masterfrom
gwado:feat/plant-tags-and-filters

Conversation

@gwado

@gwado gwado commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

⚠️ New feature, not a bug fix. This adds behavior that wasn't broken before — feel free to close if it's out of scope for the project.

Summary

Lets users organize scanned plants by free-form context (garden, work, research, ...) instead of a single flat list.

  • Plant gains a tags field (QStringList), persisted as a JSON array and read back optionally — a plant JSON missing/without this field (older saves) is treated as "no tags", not a load failure, same backward-compatible pattern used for other optional fields in this codebase.
  • Plants::updatePlantTags() rewrites an existing plant's JSON in place to update just its tags, without touching anything else about it.
  • PlantsModel exposes setPlantTags(id, tags), allPlants() (full list as a plain QVariantList, for client-side filtering), and allTags() (deduplicated, sorted list of every tag currently in use, for suggestions/filter chips).
  • PlantPage: add/remove tags on a saved plant, with suggestions drawn from tags already used on other plants (not yet applied to this one). Tagging is entirely optional and not prompted during save — only available from the plant's detail page.
  • MainPage: a horizontal row of filter chips ("All" + one per tag in use) above the saved-plants list, to switch between the full list and only the plants carrying a given tag.
  • New reusable TagChip.qml component, used both as a removable tag (PlantPage) and a selectable filter chip (MainPage).

Type of change

New feature

Test plan

  • clickable build --arch arm64 builds cleanly; the produced package passes clickable's click-review.
  • Built and installed on a physical Pixel 3a (arm64): added/removed tags on multiple saved plants, confirmed suggestions surface tags already used elsewhere, confirmed tags persist across navigating away and reopening the plant, and confirmed the filter chips on the home list correctly narrow down to plants carrying a given tag.
  • Along the way, fixed a real bug this testing surfaced: TagChip's onRemove/onClicked (plain properties, not real signals) were assigned bare expressions instead of function () {...}, which QML evaluates as an immediate binding rather than a deferred callback — this caused a binding loop (visible in adb/clickable log as "Binding loop detected for property 'onRemove'"/"'model'") that silently broke further tag updates after the first one. Fixed by wrapping all three call sites in function () { ... }.
  • One known, accepted trade-off: the "Add a tag" field sits close to the page's bottom edge, and a first attempt at reserving space for the on-screen keyboard (dynamically growing the bottom margin on focus) ended up breaking the field's tap responsiveness after first use, so it was reverted in favor of a fixed margin. This means the on-screen keyboard can still visually crowd the bottom of the page when the field is focused — a cosmetic issue, not a functional one.

Notes

  • No po/ translation update included for the new user-facing strings ("Tags", "Add a tag...", "Add", "All", "No plants with this tag", "Failed to update tags") — happy to run the translation template update if you'd like it folded into this PR.

gwado and others added 4 commits July 16, 2026 21:23
Lets users organize scanned plants by free-form context (garden, work,
research, ...) instead of a single flat list.

- Plant gains a `tags` field (QStringList), persisted as a JSON array
  and read back optionally (missing/absent on older saved plants is
  treated as no tags, not a load failure - same backward-compatible
  pattern as other optional fields).
- Plants::updatePlantTags() rewrites an existing plant's JSON in place
  to update just its tags, without touching anything else about it.
- PlantsModel exposes setPlantTags(id, tags), allPlants() (full list
  as a plain QVariantList, for client-side filtering), and allTags()
  (deduplicated, sorted list of every tag currently in use, for
  suggestions/filter chips).
- PlantPage: add/remove tags on a saved plant, with suggestions drawn
  from tags already used on other plants (not yet applied to this one).
  Tagging is entirely optional, not prompted during save.
- MainPage: a horizontal row of filter chips ("All" + one per tag in
  use) above the saved-plants list, to switch between the full list
  and only the plants carrying a given tag.
- New reusable TagChip.qml component, used both as a removable tag
  (PlantPage) and a selectable filter chip (MainPage).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
onRemove/onClicked on TagChip are plain properties (property var), not
real signals - assigning a bare expression to them (e.g. onRemove:
plantPage.removeTag(modelData)) makes QML treat it as a *binding*,
evaluated immediately and re-evaluated on every dependency change,
instead of a deferred callback. Since removeTag()/addTag() themselves
read/reassign the very state the binding depended on, this created a
binding loop (QML: "Binding loop detected for property 'onRemove'"/
"'model'"), silently breaking further updates: a tag would appear to
be added but the change wouldn't actually stick.

Wrapped all three call sites (PlantPage's tag removal and suggestion
click, MainPage's filter chip click) in function () { ... }, matching
the pattern already used correctly elsewhere for this kind of
property-based callback (e.g. PlantItem's onDelete/onEdit).

Also hardened PlantItem's root width binding (parent ? parent.width :
0) against a transient null parent seen in the logs, made more likely
by the extra model resets tag updates trigger.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The tags section (including the "Add a tag" TextField) was anchored
straight to the page bottom with no keyboard-avoidance handling,
unlike SettingsPage's API key field which already reserves space via
a keyboardRect. With the tag input sitting right where the on-screen
keyboard's own edge/handle appears, swiping up from the bottom while
the field was focused produced a stray partial keyboard-handle
artifact instead of a clean interaction.

Reuses the same keyboardRect pattern already established in
SettingsPage.qml: the tags Column's bottom margin grows to make room
for the keyboard once the tag input gains focus, so the input (and
the keyboard) no longer overlap the page's own bottom edge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gin instead

The previous commit made tagsSection's bottom margin grow/shrink live
based on the tag TextField's focus state, to make room for the
on-screen keyboard. In practice this broke the field itself: it could
be tapped/focused once, but stopped responding to taps afterwards -
very likely the geometry shifting *while the field was gaining focus*
interfered with the touch/focus handling.

Reverting the dynamic part entirely and giving tagsSection a fixed,
slightly larger bottom margin instead. This doesn't fully eliminate
the on-screen keyboard sitting close to the page's own bottom edge,
but it avoids breaking the input to fix what was, at worst, a cosmetic
gesture artifact.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

1 participant