Compare commits

..

10 commits

Author SHA1 Message Date
f19365e20e Actualiser .github/workflows/ci.yml
Some checks failed
CI / e2e (push) Waiting to run
CI / intent-service-test (push) Waiting to run
CI / build (push) Waiting to run
CI / lint (push) Successful in 45s
CI / test (push) Failing after 7m40s
2026-08-27 18:55:05 +02:00
8a51f0bd6c Actualiser .github/workflows/ci.yml
Some checks failed
CI / build (push) Waiting to run
CI / e2e (push) Waiting to run
CI / lint (push) Successful in 1m41s
CI / test (push) Failing after 8m25s
CI / intent-service-test (push) Has been cancelled
2026-08-27 18:43:38 +02:00
32c7ed48c6 Actualiser .github/workflows/ci.yml
Some checks failed
CI / lint (push) Successful in 41s
CI / test (push) Failing after 1m2s
CI / intent-service-test (push) Failing after 3s
CI / build (push) Successful in 54s
CI / e2e (push) Has been cancelled
2026-08-27 18:35:36 +02:00
0ab30a4588 Actualiser .github/workflows/ci.yml
Some checks failed
CI / lint (push) Successful in 47s
CI / test (push) Failing after 8s
CI / intent-service-test (push) Failing after 3s
CI / build (push) Successful in 59s
CI / e2e (push) Has been cancelled
2026-08-27 18:26:06 +02:00
3c04efd16f Actualiser .github/workflows/ci.yml
Some checks failed
CI / e2e (push) Waiting to run
CI / lint (push) Successful in 45s
CI / test (push) Failing after 8s
CI / intent-service-test (push) Failing after 3s
CI / build (push) Has been cancelled
2026-08-27 18:19:10 +02:00
7c423cc0fd Actualiser .github/workflows/ci.yml
Some checks failed
CI / lint (push) Successful in 46s
CI / test (push) Failing after 6s
CI / intent-service-test (push) Failing after 3s
CI / build (push) Successful in 57s
CI / e2e (push) Has been cancelled
2026-08-27 18:13:56 +02:00
d58491e6f9 Merge pull request 'feat(recipes): fusionne l'édition de la technique et des métadonnées dans le correcteur' (#6) from feat/tech-step-correction-merged-editor into main
Some checks failed
CI / lint (push) Successful in 1m38s
CI / test (push) Failing after 2s
CI / intent-service-test (push) Failing after 5s
CI / build (push) Successful in 51s
CI / e2e (push) Failing after 5m57s
Reviewed-on: #6
2026-08-27 17:41:01 +02:00
478914787f feat(recipes): fusionne l'edition de la technique et des metadonnees
Deux retours utilisateur distincts, meme cause : le popover de correction
presentait la selection de technique et l'edition des ingredients/
ustensiles comme deux etapes successives (liste plate -> "Valider" pour
la selection, puis un ecran separe pour les metadonnees), et cette liste
de techniques elle-meme n'etait qu'un flot de ~74 boutons sans recherche
ni tri, illisible en pratique.

TechStepCorrectionPopover.tsx : les deux fonctions fusionnent en un seul
ecran — la technique se choisit desormais via le meme CatalogSearchPicker
(recherche + liste filtrée) deja utilise pour les ingredients/ustensiles,
avec le choix courant marque visuellement (nouveau prop `selectedId`), et
les sections Ingredients/Ustensiles restent affichees en permanence a cote
plutot que masquees tant qu'aucune technique n'est choisie. "Valider" reste
desactive tant qu'aucune technique n'est selectionnee.

CatalogSearchPicker.tsx : nouveau prop optionnel `selectedId` pour marquer
visuellement l'item courant dans la liste (utilise par le picker de
technique, pas par les sous-flux ingredient/ustensile qui n'ont pas de
notion de "choix courant").

Tests Cypress (component + e2e) et traductions mis a jour pour ce nouvel
ecran unique.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 08:30:59 +02:00
5ab9832131 fix(recipes): pre-selectionne la technique deja detectee dans le correcteur
Ouvrir le popover de correction depuis un highlight existant tombait sur
la liste de choix (avec l'option "Aucune technique ici"), pas sur la vue
metadonnees — pour voir/editer les ingredients/ustensiles deja rattaches
a une technique correctement detectee, il fallait recliquer cette meme
technique dans la liste, sans aucun indice que c'est ce qu'il fallait
faire (rien ne la distingue des autres dans cette liste). Resultat cote
utilisateur : la fonctionnalite de correction/edition de metadonnees
etait techniquement presente mais invisible en pratique.

TechStepCorrectionPopover demarre desormais selectionne sur
`previousTechStepId` quand il est defini (un clic sur un highlight
existant) — droit dans la vue Ingredients/Ustensiles, deja pre-remplie.
"Changer" reste disponible pour rejoindre la liste complete (relabelliser
ou supprimer la correspondance).

Tests Cypress (component + e2e) mis a jour pour ce nouveau point d'entree
par defaut.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 08:13:41 +02:00
kyuno053
eef5db92b5
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:51:45 +02:00
7 changed files with 227 additions and 158 deletions

View file

@ -12,7 +12,7 @@ on:
push:
env:
DATABASE_URL: "postgresql://ci:ci@localhost:5432/batchcooking_ci?schema=public"
DATABASE_URL: "postgresql://ci:ci@localhost:5433/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: actions/checkout@v4
- uses: https://github.com/actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: https://github.com/pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: https://github.com/actions/setup-node@v4
with:
node-version: 22
cache: pnpm
@ -55,27 +55,25 @@ jobs:
POSTGRES_PASSWORD: ci
POSTGRES_DB: batchcooking_ci
ports:
- 5432:5432
- 5433:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v4
- uses: https://github.com/actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: https://github.com/pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: https://github.com/actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- uses: actions/setup-python@v5
- uses: https://github.com/astral-sh/setup-uv@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
@ -109,13 +107,11 @@ jobs:
intent-service-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: https://github.com/actions/checkout@v4
- uses: actions/setup-python@v5
- uses: https://github.com/astral-sh/setup-uv@v5
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Install services/tech-step-intent-service
@ -128,11 +124,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: https://github.com/actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: https://github.com/pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: https://github.com/actions/setup-node@v4
with:
node-version: 22
cache: pnpm
@ -143,17 +139,17 @@ jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: https://github.com/actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: https://github.com/pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: https://github.com/actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Cache Cypress binary
uses: actions/cache@v4
uses: https://github.com/actions/cache@v4
with:
path: ~/.cache/Cypress
key: cypress-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}

View file

@ -90,36 +90,65 @@ describe("TechStepCorrectionPopover", () => {
);
});
it("shows the selected text and every technique option once loaded", () => {
it("shows the selected text, the technique catalog (searchable) and the metadata sections all together", () => {
cy.mount(<Harness />);
cy.wait("@getTechSteps");
cy.contains(".tech-step-correction-popover__selection", "Cuire").should("be.visible");
cy.get(".tech-step-correction-popover__list button").should("have.length", 2);
// 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");
});
it("offers a 'no technique here' option only when correcting an existing match", () => {
it("offers a 'no technique here' option, and marks the current pick, 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("selecting a technique reveals the Ingrédients/Ustensiles sections instead of submitting immediately", () => {
it("picking a technique from the catalog selects it without submitting immediately", () => {
cy.mount(<Harness />);
cy.wait("@getTechSteps");
cy.contains(".tech-step-correction-popover__list button", "Mijoter").click();
cy.contains(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
"Mijoter",
).click();
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(".tech-step-correction-popover__chosen-technique", "Mijoter");
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,
@ -136,7 +165,10 @@ describe("TechStepCorrectionPopover", () => {
cy.mount(<Harness onSubmitted={onSubmitted} />);
cy.wait("@getTechSteps");
cy.contains(".tech-step-correction-popover__list button", "Mijoter").click();
cy.contains(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
"Mijoter",
).click();
cy.contains("button", "Valider").click();
cy.wait("@submitCorrection").its("request.body").should("deep.equal", {
@ -162,11 +194,14 @@ describe("TechStepCorrectionPopover", () => {
}).as("submitCorrection");
cy.mount(<Harness />);
cy.wait("@getTechSteps");
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(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
"Mijoter",
).click();
cy.contains("button", "+ Ajouter un ingrédient").click();
cy.contains(".catalog-search-picker button", "Beurre").click();
cy.get('input[type="number"]').type("50");
cy.get("select").select(String(gram.id));
@ -199,9 +234,11 @@ 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();
@ -217,7 +254,10 @@ describe("TechStepCorrectionPopover", () => {
cy.mount(<Harness onClose={onClose} />);
cy.wait("@getTechSteps");
cy.contains(".tech-step-correction-popover__list button", "Cuire").click();
cy.contains(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
"Cuire",
).click();
cy.contains("button", "Valider").click();
cy.wait("@submitCorrection");

View file

@ -110,13 +110,20 @@ Then("I should see the technique correction options", () => {
cy.get(".tech-step-correction-popover").should("be.visible");
});
// 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.
// 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.
When("I choose {string} as the correct technique", (label: string) => {
cy.contains(".tech-step-correction-popover__list button", label).click();
cy.contains(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
label,
).click();
cy.contains(".tech-step-correction-popover__confirm-button", "Valider").click();
});

View file

@ -729,40 +729,38 @@
margin: 0 0 var(--space-sm);
}
&__list {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
list-style: none;
margin: 0 0 var(--space-sm);
padding: 0;
button {
padding: 0.3rem 0.6rem;
// 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);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-pill);
cursor: pointer;
&: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;
}
color: var(--color-text-muted);
}
}
// 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);
&__technique-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-sm);
}
&__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;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
@ -808,36 +806,27 @@
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 {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-sm);
margin: 0;
margin: 0 0 var(--space-xs);
font-weight: 600;
}
&__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 {
margin-top: var(--space-sm);
h4 {
margin: 0 0 var(--space-xs);
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
// "+ 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
// "+ 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
// top-level choice competing with the chips above it.
> button {
background: none;
@ -925,7 +914,10 @@
list-style: none;
margin: 0;
padding: 0;
max-height: 8rem;
// 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;
overflow-y: auto;
button {
@ -941,6 +933,18 @@
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,25 +3,30 @@ 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 "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.
* 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.
*
* Deliberately just `{ id, label }` in, `id` out no `IngredientView`/
* `UtensilView` dependency here, so this stays reusable for any future
* "search this small reference catalog" need without growing a new prop
* per catalog shape.
* `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.
*/
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;
@ -48,7 +53,13 @@ export function CatalogSearchPicker({
<ul className="catalog-search-picker__list">
{visible.map((item) => (
<li key={item.id}>
<button type="button" onClick={() => onSelect(item.id)}>
<button
type="button"
className={
item.id === selectedId ? "catalog-search-picker__item--selected" : undefined
}
onClick={() => onSelect(item.id)}
>
{item.label}
</button>
</li>

View file

@ -61,12 +61,17 @@ function toPendingUtensil(view: StepTechStepUtensilView): PendingUtensil {
* first the description stays fully visible and selectable the whole
* time, nothing overlays it.
*
* **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.
* **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.
*
* The two metadata sections are pre-seeded from `existingIngredients`/
* `existingUtensils` (whatever's already attached to this occurrence, auto-
@ -118,7 +123,14 @@ export function TechStepCorrectionPopover({
const { t } = useTranslation();
const popoverRef = useRef<HTMLDivElement>(null);
const [techSteps, setTechSteps] = useState<TechStepView[] | null>(null);
const [selectedTechStepId, setSelectedTechStepId] = useState<number | 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 [catalogs, setCatalogs] = useState<{
ingredients: IngredientView[];
units: UnitView[];
@ -173,12 +185,12 @@ export function TechStepCorrectionPopover({
};
}, []);
// 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.
// 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.
useEffect(() => {
if (selectedTechStepId === null || catalogs !== null) return;
if (catalogs !== null) return;
let cancelled = false;
Promise.all([apiClient.getIngredients(), apiClient.getUnits(), apiClient.getUtensils()])
.then(([ingredients, units, utensils]) => {
@ -190,7 +202,7 @@ export function TechStepCorrectionPopover({
return () => {
cancelled = true;
};
}, [selectedTechStepId, catalogs]);
}, [catalogs]);
// Consumes a span `StepDescription` just resolved on this popover's
// behalf (see `resolvedMetadataSpan`'s own doc comment above) — opens the
@ -391,47 +403,42 @@ 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">
<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__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>
<section className="tech-step-correction-popover__metadata-section">
<h4>{t("recipes.techStepCorrection.ingredientsSection")}</h4>
@ -494,7 +501,7 @@ export function TechStepCorrectionPopover({
type="button"
className="tech-step-correction-popover__confirm-button"
onClick={confirm}
disabled={isSubmitting}
disabled={isSubmitting || selectedTechStepId === null}
>
{t("recipes.techStepCorrection.confirm")}
</button>

View file

@ -170,8 +170,12 @@
"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",