Compare commits

..

1 commit

Author SHA1 Message Date
a488704a77 feat(recipes): permet d'associer ingredients/ustensiles a une correction de technique
Etend le flux de correction existant (TechStepCorrectionPopover) pour que
l'utilisateur associe lui-meme des ingredients (avec quantite/unite) et
des ustensiles a la technique qu'il corrige, avec le meme marquage
source: "manual" que la technique elle-meme.

Backend :
- submitTechStepCorrectionSchema (packages/shared) accepte des tableaux
  ingredients/utensils optionnels, chacun avec son propre span [start,end)
  selectionne par l'utilisateur. Omis = ne touche pas aux metadonnees
  existantes ; tableau (meme vide) = remplace tout ce qui existait sur
  cette occurrence (auto ET manuel precedent - decision validee avec
  l'utilisateur).
- applyManualCorrection (recipe-tech-step-correction.service.ts) ecrit
  les nouvelles lignes StepTechStepIngredient/StepTechStepUtensil apres
  avoir vide celles de l'occurrence via deleteMany - meme chemin de code
  que ce soit une creation ou une mise a jour de la technique.
- Nouveaux asserts d'existence (ingredient/unite/ustensile) + validation
  de span, nouveau code d'erreur UTENSIL_NOT_FOUND.
- source ajoute a StepTechStepIngredientView/StepTechStepUtensilView
  (le calque manquait ce que la colonne DB portait deja).

Frontend :
- TechStepCorrectionPopover passe d'un clic = soumission immediate a un
  flux selection-puis-confirmation, avec deux nouvelles sections
  Ingredients/Ustensiles pre-remplies avec l'existant.
- Ajouter un ingredient/ustensile demande une selection de texte dediee
  dans la description encore visible (StepDescription geree via un
  nouvel etat pendingSpanRequest/resolvedMetadataSpan) - pas de raccourci
  sur le span de la correction elle-meme.
- Nouveau CatalogSearchPicker.tsx, plus leger que IngredientPicker pour
  ce contexte de popover, reutilise pour les deux catalogues.
- getUtensils() ajoute a apiClient.

Tests : nouveaux cas Mocha (attache/remplace/omission/validations) dans
recipe-tech-step-correction.test.ts, TechStepCorrectionPopover.cy.tsx
etendu avec le nouveau flux, recipes.ts (e2e) ajuste au clic Valider
supplementaire.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 21:08:18 +02:00
8 changed files with 155 additions and 232 deletions

View file

@ -12,7 +12,7 @@ on:
push:
env:
DATABASE_URL: "postgresql://ci:ci@postgres:5432/batchcooking_ci?schema=public"
DATABASE_URL: "postgresql://ci:ci@localhost:5432/batchcooking_ci?schema=public"
# Test-only secret, never used outside CI — real deployments must set their own.
JWT_SECRET: "ci-only-secret-not-used-anywhere-else-32chars+"
# Same reasoning as JWT_SECRET above — lets tech-step-worker.routes.test.ts
@ -33,11 +33,11 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: https://github.com/actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
@ -55,25 +55,27 @@ jobs:
POSTGRES_PASSWORD: ci
POSTGRES_DB: batchcooking_ci
ports:
- 5433:5432
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: https://github.com/actions/checkout@v4
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: https://github.com/actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- uses: https://github.com/astral-sh/setup-uv@v5
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v3
with:
enable-cache: true
# `services:` (like the `postgres` container above) can only pull an
@ -107,11 +109,13 @@ jobs:
intent-service-test:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
- uses: actions/checkout@v4
- uses: https://github.com/astral-sh/setup-uv@v5
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Install services/tech-step-intent-service
@ -124,11 +128,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: https://github.com/actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
@ -139,22 +143,17 @@ jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
- name: Install Xvfb and Cypress dependencies
run: |
apt-get update
apt-get install -y xvfb libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth dbus-x11
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: https://github.com/actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Cache Cypress binary
uses: https://github.com/actions/cache@v4
uses: actions/cache@v4
with:
path: ~/.cache/Cypress
key: cypress-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
@ -164,10 +163,7 @@ jobs:
# download (see apps/web's cypress caveat in the README) — install it
# explicitly so `cypress run` finds it.
- run: pnpm --filter web exec cypress install
- name: Run E2E tests
env:
HOST: "0.0.0.0"
run: pnpm --filter web e2e
- run: pnpm --filter web e2e
# No dev server needed here — Cypress spins up its own Vite dev
# server internally for component testing (see cypress.config.ts's
# `component.devServer`), unlike `e2e` above which needs the real app

View file

@ -90,65 +90,36 @@ describe("TechStepCorrectionPopover", () => {
);
});
it("shows the selected text, the technique catalog (searchable) and the metadata sections all together", () => {
it("shows the selected text and every technique option once loaded", () => {
cy.mount(<Harness />);
cy.wait("@getTechSteps");
cy.contains(".tech-step-correction-popover__selection", "Cuire").should("be.visible");
// Merged editor (see TechStepCorrectionPopover's own doc comment) — no
// separate "pick, then metadata reveals itself" step, both render at
// once, and the technique catalog goes through the same searchable
// `CatalogSearchPicker` as the ingredient/utensil sub-flows (a plain
// unfiltered list of the real ~74-entry catalog isn't browsable).
cy.get(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
).should("have.length", 2);
cy.contains("h4", "Ingrédients").should("be.visible");
cy.contains("h4", "Ustensiles").should("be.visible");
cy.contains("button", "Valider").should("be.visible");
cy.get(".tech-step-correction-popover__list button").should("have.length", 2);
});
it("offers a 'no technique here' option, and marks the current pick, only when correcting an existing match", () => {
it("offers a 'no technique here' option only when correcting an existing match", () => {
cy.mount(<Harness previousTechStepId={null} />);
cy.wait("@getTechSteps");
cy.get(".tech-step-correction-popover__remove").should("not.exist");
cy.contains(".tech-step-correction-popover__chosen-technique", "Aucune technique sélectionnée");
cy.mount(<Harness previousTechStepId={cook.id} />);
cy.wait("@getTechSteps");
cy.get(".tech-step-correction-popover__remove").should("exist");
cy.contains(".tech-step-correction-popover__chosen-technique", "Cuire");
cy.contains(".catalog-search-picker__list button", "Cuire").should(
"have.class",
"catalog-search-picker__item--selected",
);
});
it("picking a technique from the catalog selects it without submitting immediately", () => {
it("selecting a technique reveals the Ingrédients/Ustensiles sections instead of submitting immediately", () => {
cy.mount(<Harness />);
cy.wait("@getTechSteps");
cy.contains(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
"Mijoter",
).click();
cy.contains(".tech-step-correction-popover__list button", "Mijoter").click();
cy.contains(".tech-step-correction-popover__chosen-technique", "Mijoter");
cy.get(".tech-step-correction-popover__list").should("not.exist");
cy.contains("h4", "Ingrédients").should("be.visible");
cy.contains("h4", "Ustensiles").should("be.visible");
cy.contains("button", "Valider").should("be.visible");
});
it("Valider stays disabled until a technique is actually picked", () => {
cy.mount(<Harness />);
cy.wait("@getTechSteps");
cy.contains("button", "Valider").should("be.disabled");
cy.contains(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
"Mijoter",
).click();
cy.contains("button", "Valider").should("not.be.disabled");
});
it("submits the selected technique (no metadata touched) with ingredients/utensils omitted from the request", () => {
cy.intercept("POST", "**/recipes/2/steps/2/corrections", {
statusCode: 201,
@ -165,10 +136,7 @@ describe("TechStepCorrectionPopover", () => {
cy.mount(<Harness onSubmitted={onSubmitted} />);
cy.wait("@getTechSteps");
cy.contains(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
"Mijoter",
).click();
cy.contains(".tech-step-correction-popover__list button", "Mijoter").click();
cy.contains("button", "Valider").click();
cy.wait("@submitCorrection").its("request.body").should("deep.equal", {
@ -194,13 +162,10 @@ describe("TechStepCorrectionPopover", () => {
}).as("submitCorrection");
cy.mount(<Harness />);
cy.wait("@getTechSteps");
cy.wait(["@getIngredients", "@getUnits", "@getUtensils"]);
cy.contains(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
"Mijoter",
).click();
cy.contains(".tech-step-correction-popover__list button", "Mijoter").click();
cy.contains("button", "+ Ajouter un ingrédient").click();
cy.wait(["@getIngredients", "@getUnits", "@getUtensils"]);
cy.contains(".catalog-search-picker button", "Beurre").click();
cy.get('input[type="number"]').type("50");
@ -234,11 +199,9 @@ describe("TechStepCorrectionPopover", () => {
existingUtensils={[{ utensil: pan, start: 14, end: 23, source: "auto" }]}
/>,
);
// An existing match starts pre-selected on itself (see
// TechStepCorrectionPopover's own doc comment) — the metadata sections,
// pre-seeded from `existingIngredients`/`existingUtensils`, are visible
// immediately, no need to re-pick "Cuire" from a list first.
cy.wait("@getTechSteps");
cy.contains(".tech-step-correction-popover__list button", "Mijoter").click();
cy.wait(["@getIngredients", "@getUnits", "@getUtensils"]);
cy.contains(".tech-step-correction-popover__chip", "50 g Beurre").find("button").click();
@ -254,10 +217,7 @@ describe("TechStepCorrectionPopover", () => {
cy.mount(<Harness onClose={onClose} />);
cy.wait("@getTechSteps");
cy.contains(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
"Cuire",
).click();
cy.contains(".tech-step-correction-popover__list button", "Cuire").click();
cy.contains("button", "Valider").click();
cy.wait("@submitCorrection");

View file

@ -110,20 +110,13 @@ Then("I should see the technique correction options", () => {
cy.get(".tech-step-correction-popover").should("be.visible");
});
// Picking a technique only *selects* it — it takes a separate "Valider"
// click to actually submit (room was made for attaching ingredient/utensil
// metadata alongside it, see `TechStepCorrectionPopover.tsx`'s own doc
// comment on its merged editor) — folded into this one step since nothing
// in this scenario cares about that intermediate state on its own. The
// technique catalog is picked via the same searchable `CatalogSearchPicker`
// the ingredient/utensil sub-flows use, scoped to
// `__technique-section` since that same search-and-pick component is
// reused inside this popover for more than just techniques.
// Picking a technique only *selects* it now — it takes a separate
// "Valider" click to actually submit (room was made for attaching
// ingredient/utensil metadata first, see `TechStepCorrectionPopover.tsx`'s
// own doc comment) — folded into this one step since nothing in this
// scenario cares about that intermediate state on its own.
When("I choose {string} as the correct technique", (label: string) => {
cy.contains(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
label,
).click();
cy.contains(".tech-step-correction-popover__list button", label).click();
cy.contains(".tech-step-correction-popover__confirm-button", "Valider").click();
});

View file

@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"test": "echo \"no unit tests yet\" && exit 0",

View file

@ -729,38 +729,40 @@
margin: 0 0 var(--space-sm);
}
// Technique picker + Ingrédients/Ustensiles render together as one
// screen now (see `TechStepCorrectionPopover.tsx`'s own doc comment) —
// this section just needs its own small header row, the actual picker
// is `.catalog-search-picker` (below), reused as-is from the ingredient/
// utensil sub-flows.
&__technique-section {
h4 {
margin: 0 0 var(--space-xs);
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}
&__technique-header {
&__list {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-sm);
}
flex-wrap: wrap;
gap: var(--space-xs);
list-style: none;
margin: 0 0 var(--space-sm);
padding: 0;
&__remove {
padding: 0.2rem 0.5rem;
font-size: var(--font-size-sm);
color: var(--color-error);
background: none;
border: 1px solid var(--color-error);
border-radius: var(--radius-pill);
cursor: pointer;
button {
padding: 0.3rem 0.6rem;
font-size: var(--font-size-sm);
color: var(--color-text);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-pill);
cursor: pointer;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover:not(:disabled) {
background: color-mix(in srgb, var(--color-primary) 14%, transparent);
border-color: var(--color-primary);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// Nested (rather than a sibling `&__remove` block) so its border-color
// wins over the plain `button` rule above by class-count specificity,
// no `!important` needed.
.tech-step-correction-popover__remove {
color: var(--color-error);
border-color: var(--color-error);
}
}
@ -806,27 +808,36 @@
gap: var(--space-sm);
}
// "Aucune technique sélectionnée."/"Technique retenue : X" no button
// here anymore (re-picking happens directly through the search picker
// right below, see `TechStepCorrectionPopover.tsx`'s doc comment on the
// merged editor), just a small status line.
&__chosen-technique {
margin: 0 0 var(--space-xs);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-sm);
margin: 0;
font-weight: 600;
}
&__metadata-section {
margin-top: var(--space-sm);
&__change {
background: none;
border: none;
color: var(--color-primary);
text-decoration: underline;
cursor: pointer;
padding: 0;
font-size: var(--font-size-sm);
font-weight: 400;
}
&__metadata-section {
h4 {
margin: 0 0 var(--space-xs);
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
// "+ Ajouter…" button deliberately a plain text-link style, not
// another pill button (`.catalog-search-picker__list button`) this
// is a secondary action inside an already-open popover, not a
// "+ Ajouter…" button deliberately the same plain-text-link styling
// as `&__change` above, not another pill button (`&__list button`)
// this is a secondary action inside an already-open popover, not a
// top-level choice competing with the chips above it.
> button {
background: none;
@ -914,10 +925,7 @@
list-style: none;
margin: 0;
padding: 0;
// Raised from the original 8rem this component is now also the
// technique picker (~74 entries, see this file's own doc comment),
// where 8rem left only a couple of rows visible before scrolling.
max-height: 14rem;
max-height: 8rem;
overflow-y: auto;
button {
@ -933,18 +941,6 @@
background: color-mix(in srgb, var(--color-primary) 14%, transparent);
border-color: var(--color-primary);
}
// The technique picker's current pick (`selectedId` prop) — stays
// visually marked even while filtered/scrolled past, so re-opening
// this popover's picker doesn't read as "nothing chosen yet" when
// something already is. Unused by the ingredient/utensil sub-flows
// (they never pass `selectedId` each pick there just appends a
// fresh mention, nothing to mark as "current").
&.catalog-search-picker__item--selected {
background: color-mix(in srgb, var(--color-primary) 20%, transparent);
border-color: var(--color-primary);
font-weight: 600;
}
}
}
}

View file

@ -3,30 +3,25 @@ import { useState } from "react";
/**
* Small search-and-pick list a lighter alternative to `IngredientPicker.tsx`
* (category/subcategory grid + allergen/diet toggles) for a context that
* doesn't have room for that: `TechStepCorrectionPopover.tsx`'s ingredient/
* utensil/**technique** pickers, all embedded in a small popover rather than
* a full recipe form. Reused for all three an ingredient, a utensil, and a
* technique are all "search a reference list by translated label, pick one"
* from this component's point of view, the only difference is which
* `items`/labels the caller passes in. The technique catalog in particular
* (~74 entries) is exactly the case a plain unfiltered list stops being
* readable at the original motivation for adding search here at all.
* doesn't have room for that: `TechStepCorrectionPopover.tsx`'s "attach an
* ingredient/utensil to this correction" flow, embedded in a small popover
* rather than a full recipe form. Reused for both an ingredient and a
* utensil are both "search a short reference list by translated label, pick
* one" from this component's point of view, the only difference is which
* `items`/labels the caller passes in.
*
* Deliberately just `{ id, label }` in, `id` out no `IngredientView`/
* `UtensilView`/`TechStepView` dependency here, so this stays reusable for
* any future "search this small reference catalog" need without growing a
* new prop per catalog shape.
* `UtensilView` dependency here, so this stays reusable for any future
* "search this small reference catalog" need without growing a new prop
* per catalog shape.
*/
export function CatalogSearchPicker({
items,
selectedId,
onSelect,
placeholder,
emptyLabel,
}: {
items: { id: number; label: string }[];
/** The currently-picked item, if any — marked with a distinct modifier class so it stays visible at a glance while browsing/filtering a longer list (e.g. `TechStepCorrectionPopover`'s ~74-entry technique catalog), not just implied by whatever's selected elsewhere on screen. Omit for a picker with no notion of a "current" pick (the ingredient/utensil span sub-flows — each `onSelect` there just appends a brand-new mention, nothing to mark as already chosen). */
selectedId?: number;
onSelect: (id: number) => void;
placeholder: string;
emptyLabel: string;
@ -53,13 +48,7 @@ export function CatalogSearchPicker({
<ul className="catalog-search-picker__list">
{visible.map((item) => (
<li key={item.id}>
<button
type="button"
className={
item.id === selectedId ? "catalog-search-picker__item--selected" : undefined
}
onClick={() => onSelect(item.id)}
>
<button type="button" onClick={() => onSelect(item.id)}>
{item.label}
</button>
</li>

View file

@ -61,17 +61,12 @@ function toPendingUtensil(view: StepTechStepUtensilView): PendingUtensil {
* first the description stays fully visible and selectable the whole
* time, nothing overlays it.
*
* **One merged editor, not a wizard**: picking a technique
* (`CatalogSearchPicker`, searchable the reference catalog is ~74
* entries, an unfiltered flat list wasn't browsable) and editing its
* Ingrédients/Ustensiles metadata render together on the same screen,
* always there's no separate "pick, then a metadata step reveals
* itself" sequence to go through, and no dead end where metadata is
* technically attachable but not visible until some other action happens
* first. A single "Valider" submits everything at once; disabled until a
* technique is actually selected (there's nothing to attach metadata to
* otherwise). **Removing** a match (`submit(null)`) stays its own
* immediate action next to the picker nothing to attach when removing.
* **Removing** a match (`submit(null)`) stays a single immediate action
* nothing to attach when removing. **Picking/relabeling** a technique used
* to submit immediately too; it no longer does, so there's room to attach
* metadata before committing clicking a technique now only *selects* it
* (`selectedTechStepId`), revealing the Ingrédients/Ustensiles sections and
* a final "Valider" button that submits everything together.
*
* The two metadata sections are pre-seeded from `existingIngredients`/
* `existingUtensils` (whatever's already attached to this occurrence, auto-
@ -123,14 +118,7 @@ export function TechStepCorrectionPopover({
const { t } = useTranslation();
const popoverRef = useRef<HTMLDivElement>(null);
const [techSteps, setTechSteps] = useState<TechStepView[] | null>(null);
// Opening this popover on an *already-detected* match (`previousTechStepId
// !== null`, i.e. the user clicked an existing highlight rather than
// selecting fresh text) starts pre-selected on that same technique, its
// name shown next to the picker right away — since the picker and the
// metadata sections render together regardless (see this component's own
// doc comment), this just saves re-picking the technique that's already
// correct before its metadata becomes editable.
const [selectedTechStepId, setSelectedTechStepId] = useState<number | null>(previousTechStepId);
const [selectedTechStepId, setSelectedTechStepId] = useState<number | null>(null);
const [catalogs, setCatalogs] = useState<{
ingredients: IngredientView[];
units: UnitView[];
@ -185,12 +173,12 @@ export function TechStepCorrectionPopover({
};
}, []);
// Fetched unconditionally on mount — the Ingrédients/Ustensiles sections
// render alongside the technique picker from the start (see this
// component's own doc comment on the merged editor), so there's no later
// point to defer this to anymore.
// Only fetched once a technique is actually selected — the Ingrédients/
// Ustensiles sections (the only things that need these) don't render
// before then, so a popover only ever used to relabel/remove a technique
// never pays for these three extra requests.
useEffect(() => {
if (catalogs !== null) return;
if (selectedTechStepId === null || catalogs !== null) return;
let cancelled = false;
Promise.all([apiClient.getIngredients(), apiClient.getUnits(), apiClient.getUtensils()])
.then(([ingredients, units, utensils]) => {
@ -202,7 +190,7 @@ export function TechStepCorrectionPopover({
return () => {
cancelled = true;
};
}, [catalogs]);
}, [selectedTechStepId, catalogs]);
// Consumes a span `StepDescription` just resolved on this popover's
// behalf (see `resolvedMetadataSpan`'s own doc comment above) — opens the
@ -403,42 +391,47 @@ export function TechStepCorrectionPopover({
</div>
) : techSteps === null ? (
<p>{t("recipes.loading")}</p>
) : selectedTechStepId === null ? (
<ul className="tech-step-correction-popover__list">
{previousTechStepId !== null && (
<li>
<button
type="button"
disabled={isSubmitting}
onClick={removeMatch}
className="tech-step-correction-popover__remove"
>
{t("recipes.techStepCorrection.removeMatch")}
</button>
</li>
)}
{techSteps.map((techStep) => (
<li key={techStep.id}>
<button
type="button"
disabled={isSubmitting}
onClick={() => setSelectedTechStepId(techStep.id)}
>
{t(`catalog.techSteps.${techStep.key}`)}
</button>
</li>
))}
</ul>
) : (
<div className="tech-step-correction-popover__confirm">
<section className="tech-step-correction-popover__technique-section">
<div className="tech-step-correction-popover__technique-header">
<h4>{t("recipes.techStepCorrection.techniqueSection")}</h4>
{previousTechStepId !== null && (
<button
type="button"
disabled={isSubmitting}
onClick={removeMatch}
className="tech-step-correction-popover__remove"
>
{t("recipes.techStepCorrection.removeMatch")}
</button>
)}
</div>
<p className="tech-step-correction-popover__chosen-technique">
{selectedTechStepId !== null
? t("recipes.techStepCorrection.currentTechnique", {
technique: t(
`catalog.techSteps.${techSteps.find((ts) => ts.id === selectedTechStepId)?.key ?? ""}`,
),
})
: t("recipes.techStepCorrection.noTechniqueSelected")}
</p>
<CatalogSearchPicker
items={techSteps.map((techStep) => ({
id: techStep.id,
label: t(`catalog.techSteps.${techStep.key}`),
}))}
selectedId={selectedTechStepId ?? undefined}
onSelect={setSelectedTechStepId}
placeholder={t("recipes.techStepCorrection.searchTechniquePlaceholder")}
emptyLabel={t("recipes.techStepCorrection.noTechniqueFound")}
/>
</section>
<p className="tech-step-correction-popover__chosen-technique">
{t(
`catalog.techSteps.${techSteps.find((ts) => ts.id === selectedTechStepId)?.key ?? ""}`,
)}
<button
type="button"
className="tech-step-correction-popover__change"
onClick={() => setSelectedTechStepId(null)}
disabled={isSubmitting}
>
{t("recipes.techStepCorrection.changeTechnique")}
</button>
</p>
<section className="tech-step-correction-popover__metadata-section">
<h4>{t("recipes.techStepCorrection.ingredientsSection")}</h4>
@ -501,7 +494,7 @@ export function TechStepCorrectionPopover({
type="button"
className="tech-step-correction-popover__confirm-button"
onClick={confirm}
disabled={isSubmitting || selectedTechStepId === null}
disabled={isSubmitting}
>
{t("recipes.techStepCorrection.confirm")}
</button>

View file

@ -170,12 +170,8 @@
"cancel": "Annuler",
"manualTooltip": "{{technique}} (correction manuelle)",
"discoverabilityHint": "💡 Sélectionnez du texte, ou cliquez sur une technique surlignée, pour la corriger.",
"changeTechnique": "Changer",
"confirm": "Valider",
"techniqueSection": "Technique",
"currentTechnique": "Technique retenue : {{technique}}",
"noTechniqueSelected": "Aucune technique sélectionnée.",
"searchTechniquePlaceholder": "Rechercher une technique…",
"noTechniqueFound": "Aucune technique trouvée.",
"ingredientsSection": "Ingrédients",
"utensilsSection": "Ustensiles",
"addIngredient": "+ Ajouter un ingrédient",