feat: tag saved plants and filter the list by tag - #33
Open
gwado wants to merge 4 commits into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lets users organize scanned plants by free-form context (garden, work, research, ...) instead of a single flat list.
Plantgains atagsfield (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.PlantsModelexposessetPlantTags(id, tags),allPlants()(full list as a plainQVariantList, for client-side filtering), andallTags()(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.TagChip.qmlcomponent, used both as a removable tag (PlantPage) and a selectable filter chip (MainPage).Type of change
New feature
Test plan
clickable build --arch arm64builds cleanly; the produced package passesclickable's click-review.TagChip'sonRemove/onClicked(plain properties, not real signals) were assigned bare expressions instead offunction () {...}, which QML evaluates as an immediate binding rather than a deferred callback — this caused a binding loop (visible inadb/clickable logas "Binding loop detected for property 'onRemove'"/"'model'") that silently broke further tag updates after the first one. Fixed by wrapping all three call sites infunction () { ... }.Notes
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.