feat: recipe categories with filter sheet on list#166
Open
plusmobileapps wants to merge 6 commits into
Open
Conversation
|
@claude should i be creating a separate table in supabase that is just for the categories and make use of a foreign key in the recipe? using the category name as a string seems fragile if the user were to ever change the category name? |
5528d88 to
a8b2ae7
Compare
Introduces a Category model and a many-to-many recipe ↔ category relationship that supports built-in presets (Breakfast, Lunch, etc.) alongside user-created categories. - Schema: adds Category.sq and RecipeCategory.sq join table. Since the app hasn't shipped, the SQLDelight migration history is collapsed to v1 (deletes 1.sqm-6.sqm and old schema snapshots; only the .sq files remain as the source of truth). - Data layer: Category data class, BuiltinCategory presets enum, CategoryRepository (offline-first local insert with background Supabase sync via CategoryRemoteDataSource), and a Set<Category> field on Recipe. - RecipeRepository: filter API to query recipes by attached categories. - Supabase: single migration creating categories + recipe_categories tables with row-level security policies scoped to the owner. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a category picker to the edit-recipe screen: tap a chip to open a modal bottom sheet with checkboxes for each built-in preset plus the user's own categories. A "+" button in the sheet header lets the user create a new category inline; the local DB insert is offline-first and the row appears in the list immediately, with remote sync happening in the background. Materialized built-in rows are filtered out of the user-categories section to avoid double-rendering. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a sort & filter modal bottom sheet to the recipe list, opened from the filter icon in the app bar. The sheet has three sections: - Sort (recently added, A→Z, top rated, etc.) - Filter (favorites, rated, quick recipes) - Filter by category — chips for each built-in preset plus the user's own categories. Selecting any chip filters the underlying list. The sheet opens fully expanded (skipPartiallyExpanded = true) so the filter sections aren't hidden behind a half-snap. An active filter count is shown as a badge on the icon. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Renders attached categories as a FlowRow of small secondary-container pills between the hero details and the description, across compact, landscape, and expanded layouts. Built-in presets resolve via pickerLabelRes() so labels follow the user's locale; user-created categories use their stored name. Adds focused snapshot coverage for the new RecipeCategoriesRow composable (mixed light/dark + FlowRow wrapping at 360dp) and wires the screenshot-test module's missing recipe:core/data deps. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Locks in the steady states for the new category surfaces: - CategoryPickerContent — empty + populated-with-selection, light/dark - SortFilterSheetContent — populated with user categories + with a category selected, light/dark - Recipe list rendered with an active category filter, light/dark ModalBottomSheet doesn't render under the screenshot test plugin, so the picker/filter tests target the sheet content directly. The inline create-row state machine is left to viewmodel unit tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
c22b42e to
7ea4a0b
Compare
References were left over from a recording done before the picker row gained heightIn(min = 48.dp) + Modifier.toggleable, so CI's render no longer matched the committed PNGs. Replaced the four CategoryPicker references with CI's rendered output to bring them back in sync. Co-Authored-By: Claude Opus 4.7 <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
Adds recipe menu categories to chef-mate. Two user-facing pieces:
FilterChips (one per category, plus a leading "None" chip) styled to match the existing sort/filter chip pattern. Tapping the selected chip clears it.Model.totalActiveFilterCount, so the user gets a single visible indicator that filters are active.RecipeCategoryis an enum with stable string IDs (NOT ordinals — same reasoning as the bottom-nav reorder PR; we don't want renumbering to corrupt persisted data). Defaults:breakfast,lunch,dinner,appetizer,side,dessert,snack,drink,other. Recipes can have at most one primary category. Anullcategory counts as "uncategorized" for filtering and is matched only when the user includesOTHERin the filter set.Selection persists across config changes / process death via the same
Settingslayer as the existing sort + filter prefs (key:recipe_list_active_categories, comma-separated stable IDs).Schema changes
Local (SQLDelight)
category TEXTonRecipetable.client/database/core/.../migrations/7.sqm8.db.Remote (Supabase) — manual task
A Supabase migration is included as a file in this PR but must be run manually before merging:
File:
supabase/migrations/20260508_add_recipe_category.sqlStored as free-form
TEXT(noCHECKconstraint) to keep parity with how the existingclient_id/ similar identifier columns are stored on this schema, and so future categories can be added on the client without a coordinated server migration. The client validates known category IDs via theRecipeCategoryenum.Apply command (Supabase CLI):
Or via psql:
psql "$SUPABASE_DB_URL" -f supabase/migrations/20260508_add_recipe_category.sqlTasks:
20260508_add_recipe_category.sqlon staging20260508_add_recipe_category.sqlon productionSync
RemoteRecipecarries acategory: String?field (column namecategory). The repository's three sync paths (push add, push dirty update, pull from remote) all round-trip the category, so editing a category on one device propagates to others.Backfill / migration behaviour
Recipes created before this column was added will have
null/ "Other" category until the user edits them. They appear in:No data migration is performed —
nullis treated semantically as "uncategorized" rather than auto-assigned toOTHER, so we can distinguish "user explicitly chose Other" from "user hasn't set a category yet" if we ever want to.Tests
Unit tests (
./gradlew :client:recipe:list:impl:jvmTest :client:recipe:core:impl:jvmTest— passing locally):EditRecipeViewModelTest: starting category is null, an existing recipe's category seeds the picker, selecting + saving persists the chosen category, clearing back to null persists null.RecipeListViewModelTest: empty filter set returns everything, single-category filter, multi-category union, empty result,OTHERmatchesnull-category recipes,OTHER-not-selected excludes them, category + legacyFAVORITESfilter AND together, persistence round-trips through Settings,clearFilters()clears categories.Screenshot tests (
client/ui/screenshot-test/.../RecipeCategoryFilterScreenshotTest.kt):SortFilterSheetContentwas extracted from theModalBottomSheetwrapper so the screenshot can render the panel directly (Dialog-backed composables don't render reliably under the screenshot plugin).Conflicts to watch
RecipeListScreenindirectly through navigation/top-bar wiring. This PR adds chips and badge logic inside the existingSortFilterBottomSheetand bumps the badge to usetotalActiveFilterCount— should rebase cleanly but the top-barBadgedBoxblock is a likely textual conflict if feat(settings): reorder bottom navigation tabs #163 also restyles it.Test plan
2) and is tintedOther, Apply — verify pre-category recipes match🤖 Generated with Claude Code