Compare commits
9 commits
fix/tech-s
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f7aa696f55 | |||
| f19365e20e | |||
| 8a51f0bd6c | |||
| 32c7ed48c6 | |||
| 0ab30a4588 | |||
| 3c04efd16f | |||
| 7c423cc0fd | |||
| d58491e6f9 | |||
| 478914787f |
7 changed files with 233 additions and 186 deletions
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
|
|
@ -12,7 +12,7 @@ on:
|
|||
push:
|
||||
|
||||
env:
|
||||
DATABASE_URL: "postgresql://ci:ci@localhost:5432/batchcooking_ci?schema=public"
|
||||
DATABASE_URL: "postgresql://ci:ci@postgres: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: 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') }}
|
||||
|
|
|
|||
|
|
@ -90,41 +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);
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
// An existing match starts pre-selected straight into the metadata view
|
||||
// (see TechStepCorrectionPopover's own doc comment on why) — "Changer"
|
||||
// reaches the pick list, where the remove option lives.
|
||||
cy.mount(<Harness previousTechStepId={cook.id} />);
|
||||
cy.wait("@getTechSteps");
|
||||
cy.get(".tech-step-correction-popover__remove").should("not.exist");
|
||||
cy.contains("button", "Changer").click();
|
||||
cy.get(".tech-step-correction-popover__remove").should("exist");
|
||||
});
|
||||
|
||||
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__list button", "Mijoter").click();
|
||||
|
||||
cy.get(".tech-step-correction-popover__list").should("not.exist");
|
||||
// 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, 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("picking a technique from the catalog selects it without 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__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,
|
||||
|
|
@ -141,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", {
|
||||
|
|
@ -167,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));
|
||||
|
|
@ -224,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");
|
||||
|
|
|
|||
|
|
@ -110,18 +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. This step is
|
||||
// only ever reached after clicking an *existing* highlight (see the
|
||||
// previous step above), which now opens straight into that same
|
||||
// technique's metadata view, not the pick list — "Changer" reaches the
|
||||
// list this step actually needs to pick a different one from.
|
||||
// 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("button", "Changer").click();
|
||||
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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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-
|
||||
|
|
@ -120,14 +125,11 @@ export function TechStepCorrectionPopover({
|
|||
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 —
|
||||
// straight into the Ingrédients/Ustensiles view below, not the pick-a-
|
||||
// technique list. Without this, editing an already-correct technique's
|
||||
// metadata required re-picking that exact same technique from the list
|
||||
// first, with nothing in the list marking it as the current one — the
|
||||
// metadata sections were technically reachable but effectively
|
||||
// undiscoverable. "Changer de technique" (below) still reaches the full
|
||||
// list — to relabel, or to remove via the option only shown there.
|
||||
// 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[];
|
||||
|
|
@ -183,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]) => {
|
||||
|
|
@ -200,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
|
||||
|
|
@ -401,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>
|
||||
|
|
@ -504,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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue