Quand le catalogue seede ne couvre pas un ingredient, l'utilisateur pouvait etre bloque (creation manuelle) ou perdre silencieusement la ligne (import). Une ligne de recette accepte desormais `placeholderName` (texte libre) au lieu de `ingredientId` : l'API cree une ligne `Ingredient` `isPlaceholder` (cle `placeholder:<uuid>`, `displayName`, `createdById`) dans la transaction de la recette, et emet `ingredient.placeholder_created`. Ces lignes sont exclues de `GET /reference/ingredients` et de `ingredient-matcher`. Front : bouton "Ajouter << ... >>" dans l'etat vide de `IngredientPicker` (formulaire + import), badge "a completer" sur la ligne, helper `ingredientLabel` applique partout ou un libelle d'ingredient est rendu. Admin : `/admin/catalog/*` (+ page `apps/admin-web`) liste les placeholders regroupes par nom normalise, "marquer traite" (`reviewedAt`) et purge des orphelins. La promotion en vraie entree catalogue reste manuelle. Migration `ingredient_placeholder` ecrite a la main (Postgres indisponible). Suites Mocha DB-backed ecrites, non executees en session ; test pur `normalizePlaceholderName` + Cypress admin-web/web verts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
73 lines
2.9 KiB
TypeScript
73 lines
2.9 KiB
TypeScript
import type { IngredientView } from "@batch-cooking/shared";
|
|
import { useTranslation } from "react-i18next";
|
|
import { IngredientPicker } from "../recipes/ingredients/IngredientPicker";
|
|
// Reuses `IngredientPicker` verbatim (built for the recipe form's
|
|
// ingredient picker, features/recipes/) rather than a second search+browse
|
|
// field — same reference ingredient list, same category/search browsing,
|
|
// just without the recipe form's quantity/unit per line. Its own
|
|
// recipes.scss import already covers `.ingredient-picker`; this file's
|
|
// explicit import below only adds `.disliked-ingredients-field__*` (see
|
|
// recipes.scss — colocated there since it's the same "reference ingredient
|
|
// picker" visual family, even though this field lives in the profile
|
|
// feature).
|
|
import { IngredientTypeIcon } from "../recipes/ingredients/ingredient-icons";
|
|
import { ingredientLabel } from "../recipes/ingredients/ingredient-label";
|
|
import "../recipes/recipes.scss";
|
|
import "./profile-forms.scss";
|
|
|
|
/**
|
|
* Search-and-add + removable-chips field for the current user's personal
|
|
* "disliked ingredients" list — a taste preference, distinct from
|
|
* `AllergySelect`'s medical allergy list. Used on `/parametres/preferences`
|
|
* (`PreferencesPage`); crossed against a recipe's own ingredients on its
|
|
* detail panel (`RecipeDetailPanel`) to surface just the relevant ones.
|
|
*/
|
|
export function DislikedIngredientsField({
|
|
ingredients,
|
|
value,
|
|
onChange,
|
|
}: {
|
|
ingredients: IngredientView[];
|
|
value: number[];
|
|
onChange: (ingredientIds: number[]) => void;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const selected = ingredients.filter((ingredient) => value.includes(ingredient.id));
|
|
|
|
function add(ingredient: IngredientView) {
|
|
onChange([...value, ingredient.id]);
|
|
}
|
|
|
|
function remove(ingredientId: number) {
|
|
onChange(value.filter((id) => id !== ingredientId));
|
|
}
|
|
|
|
return (
|
|
// `<fieldset>`/`<legend>` (not a bare `<label>`, which only associates
|
|
// with a single control) — this labels the composite widget (chips +
|
|
// search), same reasoning as AllergySelect.
|
|
<fieldset className="disliked-ingredients-field">
|
|
<legend>{t("preferences.form.dislikedIngredientsLabel")}</legend>
|
|
{selected.length > 0 && (
|
|
<ul className="disliked-ingredients-field__chips">
|
|
{selected.map((ingredient) => (
|
|
<li key={ingredient.id} className="disliked-ingredients-field__chip">
|
|
<span aria-hidden="true">
|
|
<IngredientTypeIcon icon={ingredient.icon} />
|
|
</span>
|
|
{ingredientLabel(ingredient, t)}
|
|
<button
|
|
type="button"
|
|
onClick={() => remove(ingredient.id)}
|
|
title={t("preferences.form.removeDislikedIngredient")}
|
|
>
|
|
✕
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
<IngredientPicker ingredients={ingredients} excludeIds={value} onSelect={add} />
|
|
</fieldset>
|
|
);
|
|
}
|