diff --git a/apps/web/cypress/e2e/planning.feature b/apps/web/cypress/e2e/planning.feature
index 53692ee..eaa7546 100644
--- a/apps/web/cypress/e2e/planning.feature
+++ b/apps/web/cypress/e2e/planning.feature
@@ -15,7 +15,7 @@ Feature: Adding a recipe to the planning
And browsing TheMealDB returns some items
And the planning request reflects whatever's been added so far
- Scenario: Adds a not-yet-imported source item to a planning slot, importing it on the way
+ Scenario: Adds a not-yet-imported source item to a planning slot, landing on the review screen since an ingredient needs resolving
Given previewing TheMealDB item "9999" is available
And importing the previewed item will succeed and return id 99
And adding the imported recipe to the planning will succeed
@@ -23,7 +23,6 @@ Feature: Adding a recipe to the planning
And I click the add button for the first empty planning slot
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."
When I choose an ingredient for the unresolved line "some mystery paste"
@@ -34,3 +33,15 @@ Feature: Adding a recipe to the planning
Then the planning add request should have included recipe 99, weekDay "lundi", meal "petit-dejeuner", and portions 4
And the URL should be the home page
And the recipe "Fish Pie" should appear in the first planning slot with 4 portions
+
+ Scenario: Adds a fully-resolved not-yet-imported item to a planning slot transparently, with no review screen at all
+ Given previewing TheMealDB item "7777" is fully resolved as "Ratatouille"
+ And importing item "7777" will succeed and return id 100
+ And adding recipe 100 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 "TheMealDB"
+ And I click the source item "Ratatouille"
+ Then the import request for "7777" should have included the name "Ratatouille"
+ And the planning add request should have included recipe 100, weekDay "lundi", meal "petit-dejeuner", and portions 4
+ And the URL should be the home page
diff --git a/apps/web/cypress/e2e/planning.ts b/apps/web/cypress/e2e/planning.ts
index dd078bb..2aadca6 100644
--- a/apps/web/cypress/e2e/planning.ts
+++ b/apps/web/cypress/e2e/planning.ts
@@ -49,6 +49,18 @@ Given("browsing TheMealDB returns some items", () => {
alreadyImported: false,
recipeId: null,
},
+ // Draft's own preview ("previewing TheMealDB item ... is fully
+ // resolved") has nothing left for a person to fix — unlike "Fish
+ // Pie" above, exercises the transparent-import path instead of the
+ // review screen.
+ {
+ externalId: "7777",
+ title: "Ratatouille",
+ picture: null,
+ url: "https://www.themealdb.com/meal/7777",
+ alreadyImported: false,
+ recipeId: null,
+ },
],
nextCursor: null,
},
@@ -89,6 +101,76 @@ Given("previewing TheMealDB item {string} is available", (externalId: string) =>
});
});
+// Unlike "previewing TheMealDB item ... is available" above, every
+// ingredient line here already resolved to a real ingredient/unit/quantity
+// — `tryBuildCompleteImport` (RecipePickerDialog.tsx) accepts a draft
+// shaped exactly like this one as-is, no review screen needed.
+Given(
+ "previewing TheMealDB item {string} is fully resolved as {string}",
+ (externalId: string, name: string) => {
+ cy.intercept("GET", `**/sources/theMealDb/preview/${externalId}`, {
+ statusCode: 200,
+ body: {
+ sourceKey: "theMealDb",
+ externalId,
+ name,
+ description: null,
+ picture: null,
+ portions: 4,
+ sourceUrl: `https://www.themealdb.com/meal/${externalId}`,
+ ingredients: [
+ {
+ rawText: "1 onion",
+ quantity: 1,
+ ingredient: {
+ id: 1,
+ key: "onion",
+ icon: "VEGETABLE",
+ category: "freshProduce",
+ subcategory: "vegetables",
+ reproducible: false,
+ allergens: [],
+ diets: [],
+ },
+ unit: { id: 1, key: "piece", type: "COUNT", toBaseFactor: 1 },
+ },
+ ],
+ steps: [{ description: "Cuire à la poêle.", picture: null, techSteps: [] }],
+ },
+ });
+ },
+);
+
+Given(
+ "importing item {string} will succeed and return id {int}",
+ (externalId: string, id: number) => {
+ cy.intercept("POST", `**/sources/theMealDb/import/${externalId}`, {
+ statusCode: 201,
+ body: { id },
+ }).as("importItem");
+ },
+);
+
+Given("adding recipe {int} to the planning will succeed", (recipeId: number) => {
+ cy.intercept("POST", "**/planning/items", {
+ statusCode: 201,
+ body: {
+ id: 2,
+ weekDay: "lundi",
+ meal: "petit-dejeuner",
+ portions: 4,
+ recipe: { id: recipeId, name: "Ratatouille" },
+ },
+ }).as("addPlanningItem");
+});
+
+Then(
+ "the import request for {string} should have included the name {string}",
+ (_externalId: string, name: string) => {
+ cy.wait("@importItem").its("request.body.name").should("eq", name);
+ },
+);
+
// Covers every reference catalog both `RecipePickerDialog` (ingredients/
// diets, for its own filters) and `ImportRecipePage` (ingredients/diets/
// units, for the review form) fetch — same endpoints, one fixture for both.
diff --git a/apps/web/cypress/e2e/recipe-sources.feature b/apps/web/cypress/e2e/recipe-sources.feature
index 85a7379..16dfee8 100644
--- a/apps/web/cypress/e2e/recipe-sources.feature
+++ b/apps/web/cypress/e2e/recipe-sources.feature
@@ -45,7 +45,7 @@ Feature: Browsing external recipe sources
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
+ Scenario: Deep-links straight to a not-yet-imported item's own page, with no import affordance at all
Given the recipe catalog contains nothing
And the sources reference list has options
And the household has enabled TheMealDB
@@ -53,31 +53,5 @@ Feature: Browsing external recipe sources
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
- And the household has enabled TheMealDB
- And browsing TheMealDB returns some items
- And previewing TheMealDB item "9999" is available
- 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 "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"
- And the recipe should include the ingredient "Oignon"
-
- When I choose an ingredient for the unresolved line "some mystery paste"
- And I select the ingredient "Sel" from the picker
- Then the unresolved ingredients section should no longer be shown
- And there should be 2 ingredient rows
-
- When I select unit "unité" for the first ingredient
- And I fill in the last ingredient's quantity with "1" and unit "unité"
- Then the "Importer" button should not be disabled
- When I click the button "Importer"
- Then the import request should have included ingredient 2 with quantity 1 and unitId 1
- And the URL should include "/recettes/99"
+ And I should not see "Importer cette recette"
+ And I should see a discreet link to the item's original page
diff --git a/apps/web/cypress/e2e/recipe-sources.ts b/apps/web/cypress/e2e/recipe-sources.ts
index c630282..f183bc2 100644
--- a/apps/web/cypress/e2e/recipe-sources.ts
+++ b/apps/web/cypress/e2e/recipe-sources.ts
@@ -135,66 +135,9 @@ Then("the source item {string} should be marked as already imported", (title: st
cy.contains("tr", title).find(".source-item-table__imported-badge").should("be.visible");
});
-// ImportRecipePage (the review screen) loads its own ingredient/diet/unit
-// catalogs the same way RecipeFormPage does — "onion" matches the resolved
-// line in "previewing TheMealDB item ... is available" above, "salt" is
-// what "some mystery paste" (unresolved in that same fixture) gets
-// corrected to in the review-and-import scenario.
-Given("the ingredient and diet catalog is available for import", () => {
- cy.intercept("GET", "**/reference/ingredients", {
- statusCode: 200,
- body: [
- {
- id: 1,
- key: "onion",
- icon: "VEGETABLE",
- category: "freshProduce",
- subcategory: "vegetables",
- allergens: [],
- diets: [],
- },
- {
- id: 2,
- key: "salt",
- icon: "SPICE",
- category: "condimentsAndSpices",
- subcategory: "spices",
- allergens: [],
- diets: [],
- },
- ],
- });
- cy.intercept("GET", "**/reference/diets", {
- statusCode: 200,
- body: [{ id: 1, key: "omnivore" }],
- });
- cy.intercept("GET", "**/reference/units", {
- statusCode: 200,
- body: [{ id: 1, key: "piece", type: "COUNT", toBaseFactor: 1 }],
- });
+// Browsing a source (outside of adding-to-planning, `RecipePickerDialog`'s
+// own scenarios in planning.ts) never imports anything — the only action
+// this preview offers is a discreet way out to the item's own page.
+Then("I should see a discreet link to the item's original page", () => {
+ cy.get(".recipe-detail-panel__source-link").should("be.visible");
});
-
-Given("importing the previewed item will succeed and return id {int}", (id: number) => {
- cy.intercept("POST", "**/sources/theMealDb/import/9999", { statusCode: 201, body: { id } }).as(
- "importRecipe",
- );
-});
-
-When("I choose an ingredient for the unresolved line {string}", (rawText: string) => {
- cy.contains(".import-recipe__unresolved-row", rawText)
- .contains("button", "Choisir un ingrédient")
- .click();
-});
-
-Then("the unresolved ingredients section should no longer be shown", () => {
- cy.get(".import-recipe__unresolved").should("not.exist");
-});
-
-Then(
- "the import request should have included ingredient {int} with quantity {int} and unitId {int}",
- (ingredientId: number, quantity: number, unitId: number) => {
- cy.wait("@importRecipe")
- .its("request.body.ingredients")
- .should("include.deep.members", [{ ingredientId, quantity, unitId }]);
- },
-);
diff --git a/apps/web/src/features/planning/RecipePickerDialog.tsx b/apps/web/src/features/planning/RecipePickerDialog.tsx
index 6958790..5177365 100644
--- a/apps/web/src/features/planning/RecipePickerDialog.tsx
+++ b/apps/web/src/features/planning/RecipePickerDialog.tsx
@@ -1,14 +1,17 @@
import {
+ type BrowsableSourceItemView,
type DietView,
ErrorCode,
type IngredientView,
type Meal,
type PlanningItemView,
type RecipeSummaryView,
+ type RecipeView,
type WeekDay,
} from "@batch-cooking/shared";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
import { ApiError, apiClient } from "../../api/client";
import { CheckboxOption } from "../../components/ui/Checkbox";
import { Dialog } from "../../components/ui/Dialog";
@@ -23,6 +26,7 @@ import {
isSourceTab,
parseSourceTabValue,
} from "../recipes/RecipeTabs";
+import { tryBuildCompleteImport } from "../recipes/recipe-import-draft";
import { useEnabledSources } from "../recipes/useEnabledSources";
import "./recipe-picker-dialog.scss";
@@ -55,13 +59,12 @@ export interface PlanningSlot {
* (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. 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.
+ * household-enabled source's own tab is included too: picking an
+ * already-imported item behaves exactly like picking a regular recipe,
+ * and picking one that isn't imported yet is what actually imports it —
+ * nowhere else in the app does (see `handleSelectDraftItem`) — since a
+ * source item only ever becomes a real, saved `Recipe` as a side effect of
+ * someone adding it to their planning.
*
* Mounted only while open (see `PlanningPage`, same conditional-mount
* convention as its own `CalendarPopover`) — every piece of local state
@@ -71,10 +74,14 @@ export interface PlanningSlot {
* Selecting a row doesn't navigate anywhere (unlike `RecipesPage`'s own
* use of `RecipeTable`) — it switches this same dialog to a small
* "how many portions?" confirmation step, then calls `POST
- * /planning/items` on submit. The one exception is picking a not-yet-
- * imported source item, which does navigate away entirely (to
- * `/recettes/importer/...`) — that flow has its own portions field
- * already, on the review screen itself.
+ * /planning/items` on submit. Picking a not-yet-imported source item is
+ * handled differently still (`handleSelectDraftItem`): when the draft has
+ * everything a real recipe needs, it's imported and added to this slot
+ * transparently — no extra screen, same as picking anything else. Only
+ * when something's actually missing (an ingredient the automatic matcher
+ * couldn't resolve, say) does this navigate away entirely, to the review
+ * screen (`/recettes/importer/...`), which has its own portions field
+ * already.
*/
export function RecipePickerDialog({
slot,
@@ -86,6 +93,7 @@ export function RecipePickerDialog({
onAdded: (item: PlanningItemView) => void;
}) {
const { t } = useTranslation();
+ const navigate = useNavigate();
const [activeTab, setActiveTab] = useState
{t("planning.picker.addingDraft")}
+ ) : ( +{draft.description}
diff --git a/apps/web/src/features/recipes/RecipeSourcesPanel.tsx b/apps/web/src/features/recipes/RecipeSourcesPanel.tsx index 51c59ca..99e2a86 100644 --- a/apps/web/src/features/recipes/RecipeSourcesPanel.tsx +++ b/apps/web/src/features/recipes/RecipeSourcesPanel.tsx @@ -1,4 +1,4 @@ -import type { BrowsableSourceItemView, Meal, WeekDay } from "@batch-cooking/shared"; +import type { BrowsableSourceItemView } from "@batch-cooking/shared"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { apiClient } from "../../api/client"; @@ -40,8 +40,9 @@ type BrowseState = * The right-hand preview reuses `RecipeDetailPanel` itself (its * `"loaded-draft"` state) rather than a separate component — viewing a * not-yet-imported item is meant to look and feel exactly like viewing any - * other recipe, differing only in which actions are offered (there's an - * "Importer" button where Modifier/Supprimer would be). + * other recipe, minus the actions that don't apply to something that isn't + * saved yet. This is `RecipesPage`'s own mode: browsing/reading only, + * nothing here ever imports anything (see `onSelectDraftItem` below). * * Selecting an already-imported item navigates straight to its real * recipe (`/recettes/:id`, leaving this tab) — `onSelectImportedRecipe` @@ -58,20 +59,29 @@ type BrowseState = * (`/recettes/sources/:sourceKey/:externalId`) without this panel needing * to know anything about routing itself — it reports selection changes * upward, and re-previews on mount/prop-change if handed one back. - * `RecipePickerDialog` leaves both unset: previewing inside that modal has - * no URL of its own to keep in sync. + * `RecipePickerDialog` leaves both unset: it never previews a not-yet- + * imported item inline at all (see `onSelectDraftItem`). */ export function RecipeSourcesPanel({ sourceKey, onSelectImportedRecipe, - planningSlot, + onSelectDraftItem, initialSelection, onItemSelected, }: { sourceKey: string; onSelectImportedRecipe: (recipeId: number) => void; - /** Forwarded as-is to `RecipeDetailPanel` — see its own doc comment. Only ever set by `RecipePickerDialog`. */ - planningSlot?: { date: string; weekDay: WeekDay; meal: Meal }; + /** + * Set only by `RecipePickerDialog` — picking a not-yet-imported item + * while adding to a planning slot isn't something to preview inline + * here at all (there's no "import" button on that preview to lead + * anywhere any more — see `RecipeDetailPanel`'s own doc comment). When + * set, a not-yet-imported row's click hands the item straight to this + * instead of previewing it, and the caller takes it from there + * (`RecipePickerDialog.handleSelectDraftItem`: import transparently when + * nothing's missing, otherwise hand off to the review screen). + */ + onSelectDraftItem?: (item: BrowsableSourceItemView) => void; initialSelection?: SourceItemSelection; onItemSelected?: (item: SourceItemSelection | null) => void; }) { @@ -168,6 +178,10 @@ export function RecipeSourcesPanel({ onSelectImportedRecipe(item.recipeId); return; } + if (onSelectDraftItem) { + onSelectDraftItem(item); + return; + } const selection = { sourceKey, externalId: item.externalId }; setSelectedExternalId(item.externalId); setPreviewState({ status: "loading" }); @@ -221,7 +235,7 @@ export function RecipeSourcesPanel({ )} -