diff --git a/apps/web/cypress/e2e/planning.feature b/apps/web/cypress/e2e/planning.feature
index 3b3b30a..53692ee 100644
--- a/apps/web/cypress/e2e/planning.feature
+++ b/apps/web/cypress/e2e/planning.feature
@@ -21,7 +21,7 @@ Feature: Adding a recipe to the planning
And adding the imported recipe to the planning will succeed
When I visit "/"
And I click the add button for the first empty planning slot
- And I click the button "Sources"
+ And I click the button "TheMealDB"
And I click the source item "Fish Pie"
And I click the link "Importer cette recette"
Then I should see "Cette recette sera automatiquement ajoutée à votre planning une fois importée."
diff --git a/apps/web/cypress/e2e/recipe-sources.feature b/apps/web/cypress/e2e/recipe-sources.feature
index 9fc8a56..85a7379 100644
--- a/apps/web/cypress/e2e/recipe-sources.feature
+++ b/apps/web/cypress/e2e/recipe-sources.feature
@@ -9,13 +9,12 @@ Feature: Browsing external recipe sources
And the disliked ingredients list is empty
And the planning request returns nothing
- Scenario: Prompts to enable a source when the household hasn't enabled any
+ Scenario: Shows no source tab when the household hasn't enabled any
Given the recipe catalog contains nothing
And the sources reference list has options
And the household's enabled sources are empty
When I visit "/recettes"
- And I click the button "Sources"
- Then I should see "Aucune source n'est activée"
+ Then I should not see "TheMealDB"
Scenario: Browses an enabled source, distinguishing already-imported items from new ones
Given the recipe catalog contains nothing
@@ -24,7 +23,7 @@ Feature: Browsing external recipe sources
And browsing TheMealDB returns some items
And recipe 2's detail is available
When I visit "/recettes"
- And I click the button "Sources"
+ And I click the button "TheMealDB"
Then I should see the source item "Chicken Handi"
And I should see the source item "Fish Pie"
And the source item "Chicken Handi" should be marked as already imported
@@ -40,11 +39,22 @@ Feature: Browsing external recipe sources
And browsing TheMealDB returns some items
And previewing TheMealDB item "9999" is available
When I visit "/recettes"
- And I click the button "Sources"
+ And I click the button "TheMealDB"
And I click the source item "Fish Pie"
- Then the recipe detail panel heading should be "Fish Pie"
+ Then the URL should include "/recettes/sources/theMealDb/9999"
+ And the recipe detail panel heading should be "Fish Pie"
And I should see the highlighted technique "Cuire"
+ Scenario: Deep-links straight to a not-yet-imported item's own page
+ Given the recipe catalog contains nothing
+ And the sources reference list has options
+ And the household has enabled TheMealDB
+ And browsing TheMealDB returns some items
+ And previewing TheMealDB item "9999" is available
+ When I visit "/recettes/sources/theMealDb/9999"
+ Then the recipe detail panel heading should be "Fish Pie"
+ And I should see "Importer cette recette"
+
Scenario: Reviews an import, resolving an unrecognized ingredient before confirming
Given the recipe catalog contains nothing
And the sources reference list has options
@@ -54,7 +64,7 @@ Feature: Browsing external recipe sources
And the ingredient and diet catalog is available for import
And importing the previewed item will succeed and return id 99
When I visit "/recettes"
- And I click the button "Sources"
+ And I click the button "TheMealDB"
And I click the source item "Fish Pie"
And I click the link "Importer cette recette"
Then the "recipe-name" field should have the value "Fish Pie"
diff --git a/apps/web/cypress/support/step_definitions/common.steps.ts b/apps/web/cypress/support/step_definitions/common.steps.ts
index a4ac86a..8084ff2 100644
--- a/apps/web/cypress/support/step_definitions/common.steps.ts
+++ b/apps/web/cypress/support/step_definitions/common.steps.ts
@@ -133,10 +133,10 @@ When("I scroll to the section {string}", (legend: string) => {
cy.contains("legend", legend).scrollIntoView();
});
-// `.recipe-detail-panel` is used by both a saved recipe's real detail
-// (RecipeDetailPanel) and an unsaved source item's read-only preview
-// (SourceItemPreviewPanel) — recipes.feature and recipe-sources.feature
-// both need this.
+// `.recipe-detail-panel` is used by both a saved recipe's real detail and
+// an unsaved source item's read-only preview (RecipeDetailPanel's
+// `"loaded"`/`"loaded-draft"` states, same component for both) —
+// recipes.feature and recipe-sources.feature both need this.
Then("the recipe detail panel heading should be {string}", (text: string) => {
cy.get(".recipe-detail-panel").contains("h2", text).should("be.visible");
});
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 206e7c3..88911dd 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -61,6 +61,7 @@ export function App() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/apps/web/src/features/planning/RecipePickerDialog.tsx b/apps/web/src/features/planning/RecipePickerDialog.tsx
index feaf62e..6958790 100644
--- a/apps/web/src/features/planning/RecipePickerDialog.tsx
+++ b/apps/web/src/features/planning/RecipePickerDialog.tsx
@@ -17,7 +17,13 @@ import { DietTagSelect } from "../recipes/DietTagSelect";
import { IngredientPicker } from "../recipes/IngredientPicker";
import { RecipeSourcesPanel } from "../recipes/RecipeSourcesPanel";
import { RecipeTable } from "../recipes/RecipeTable";
-import { RecipeTabs, type RecipesPageTab } from "../recipes/RecipeTabs";
+import {
+ RecipeTabs,
+ type RecipesPageTab,
+ isSourceTab,
+ parseSourceTabValue,
+} from "../recipes/RecipeTabs";
+import { useEnabledSources } from "../recipes/useEnabledSources";
import "./recipe-picker-dialog.scss";
/** Debounce for the search field — same value as `RecipesPage`'s. */
@@ -48,13 +54,14 @@ export interface PlanningSlot {
* with three extra filters layered on top of the plain name search
* (ingredients / regime / "convient à tout le foyer" toggle, all wired to
* `GET /recipes`'s corresponding query params) since browsing here is
- * about finding something to cook, not just looking something up. The
- * "Sources" tab is included too (unlike an earlier version of this dialog
- * — see `ImportRecipePage`'s `planningSlot`, the review/import flow that
- * made including it here worthwhile): picking an already-imported item
- * behaves exactly like picking a regular recipe, and picking one that
- * isn't imported yet hands off to that review screen, which adds the
- * freshly-created recipe straight to this slot once it's saved.
+ * about finding something to cook, not just looking something up. Each
+ * household-enabled source's own tab is included too (unlike an earlier
+ * version of this dialog — see `ImportRecipePage`'s `planningSlot`, the
+ * review/import flow that made including them here worthwhile): picking
+ * an already-imported item behaves exactly like picking a regular recipe,
+ * and picking one that isn't imported yet hands off to that review
+ * screen, which adds the freshly-created recipe straight to this slot
+ * once it's saved.
*
* Mounted only while open (see `PlanningPage`, same conditional-mount
* convention as its own `CalendarPopover`) — every piece of local state
@@ -81,6 +88,8 @@ export function RecipePickerDialog({
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState("favoris");
+ const activeSourceKey = parseSourceTabValue(activeTab);
+ const enabledSources = useEnabledSources();
const [search, setSearch] = useState("");
const [debouncedSearch, setDebouncedSearch] = useState("");
const [selectedIngredientIds, setSelectedIngredientIds] = useState([]);
@@ -128,9 +137,11 @@ export function RecipePickerDialog({
}, []);
useEffect(() => {
- // The "sources" tab doesn't query the recipe table at all — same guard
- // as `RecipesPage`'s own identical effect.
- if (activeTab === "sources") return;
+ // A source's own tab doesn't query the recipe table at all — same
+ // guard as `RecipesPage`'s own identical effect (the type-guard, not
+ // just `activeSourceKey !== null`, is what narrows `activeTab` to
+ // `RecipeTab` below).
+ if (isSourceTab(activeTab)) return;
let cancelled = false;
setListState({ status: "loading" });
@@ -157,7 +168,7 @@ export function RecipePickerDialog({
selectedIngredientIds.includes(ingredient.id),
);
- /** Picking an already-imported source item (`RecipeSourcesPanel`'s "sources" tab) — resolved to its full recipe, then treated exactly like picking that same recipe from one of the regular tabs, moving straight to the confirm-portions step below. */
+ /** Picking an already-imported source item (one of the source tabs' `RecipeSourcesPanel`) — resolved to its full recipe, then treated exactly like picking that same recipe from one of the regular tabs, moving straight to the confirm-portions step below. */
function handleSelectImportedRecipe(recipeId: number) {
setSourceSelectError(false);
apiClient
@@ -230,7 +241,7 @@ export function RecipePickerDialog({
return (