Add support for recipe recipe ingredients

This commit is contained in:
Christopher C. Wells 2021-01-23 10:07:03 -08:00 committed by Christopher Charbonneau Wells
parent ff829d9d8d
commit 983b7695dd
6 changed files with 81 additions and 11 deletions

View File

@ -8,6 +8,7 @@ use App\Models\Recipe;
use App\Models\RecipeStep; use App\Models\RecipeStep;
use App\Rules\ArrayNotEmpty; use App\Rules\ArrayNotEmpty;
use App\Rules\StringIsDecimalOrFraction; use App\Rules\StringIsDecimalOrFraction;
use App\Rules\UsesIngredientTrait;
use App\Support\Number; use App\Support\Number;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
@ -83,6 +84,7 @@ class RecipeController extends Controller
'amount' => $old['amount'][$key], 'amount' => $old['amount'][$key],
'unit' => $old['unit'][$key], 'unit' => $old['unit'][$key],
'ingredient_id' => $ingredient_id, 'ingredient_id' => $ingredient_id,
'ingredient_type' => $old['type'][$key],
'ingredient_name' => $old['name'][$key], 'ingredient_name' => $old['name'][$key],
'detail' => $old['detail'][$key], 'detail' => $old['detail'][$key],
]; ];
@ -94,7 +96,8 @@ class RecipeController extends Controller
'original_key' => $key, 'original_key' => $key,
'amount' => $ingredientAmount->amount_formatted, 'amount' => $ingredientAmount->amount_formatted,
'unit' => $ingredientAmount->unit, 'unit' => $ingredientAmount->unit,
'ingredient_id' => $ingredientAmount->ingredient->id, 'ingredient_id' => $ingredientAmount->ingredient_id,
'ingredient_type' => $ingredientAmount->ingredient_type,
'ingredient_name' => $ingredientAmount->ingredient->name, 'ingredient_name' => $ingredientAmount->ingredient->name,
'detail' => $ingredientAmount->detail, 'detail' => $ingredientAmount->detail,
]; ];
@ -160,12 +163,22 @@ class RecipeController extends Controller
'ingredients.detail.*' => 'nullable|string', 'ingredients.detail.*' => 'nullable|string',
'ingredients.id' => ['required', 'array', new ArrayNotEmpty], 'ingredients.id' => ['required', 'array', new ArrayNotEmpty],
'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable', 'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable',
'ingredients.type' => ['required', 'array', new ArrayNotEmpty],
'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()],
'ingredients.original_key' => 'nullable|array', 'ingredients.original_key' => 'nullable|array',
'steps.step' => ['required', 'array', new ArrayNotEmpty], 'steps.step' => ['required', 'array', new ArrayNotEmpty],
'steps.step.*' => 'nullable|string', 'steps.step.*' => 'nullable|string',
'steps.original_key' => 'nullable|array', 'steps.original_key' => 'nullable|array',
]); ]);
// Validate that no ingredients are recursive.
// TODO: refactor as custom validator.
foreach (array_filter($input['ingredients']['id']) as $key => $id) {
if ($input['ingredients']['type'][$key] == Recipe::class && $id == $recipe->id) {
return back()->withInput()->withErrors('To understand recursion, you must understand recursion. Remove this recipe from this recipe.');
}
}
$recipe->fill([ $recipe->fill([
'name' => Str::lower($input['name']), 'name' => Str::lower($input['name']),
'description' => $input['description'], 'description' => $input['description'],
@ -202,7 +215,7 @@ class RecipeController extends Controller
'weight' => $weight++, 'weight' => $weight++,
]); ]);
$ingredient_amounts[$key]->ingredient() $ingredient_amounts[$key]->ingredient()
->associate(Food::where('id', $ingredient_id)->first()); ->associate($input['ingredients']['type'][$key]::where('id', $ingredient_id)->first());
} }
$recipe->ingredientAmounts()->saveMany($ingredient_amounts); $recipe->ingredientAmounts()->saveMany($ingredient_amounts);

View File

@ -78,11 +78,6 @@ class Recipe extends Model
'sodiumPerServing', 'sodiumPerServing',
]; ];
/**
* @inheritdoc
*/
protected $with = ['ingredientAmounts'];
/** /**
* Get the steps for this Recipe. * Get the steps for this Recipe.
*/ */

View File

@ -8,6 +8,22 @@ use Illuminate\Support\Collection;
trait Ingredient trait Ingredient
{ {
/**
* Add special `type` attribute to appends.
*/
public function initializeIngredient(): void {
$this->appends[] = 'type';
}
/**
* Gets the class name.
*
* This is necessary e.g. to provide data in ingredient picker responses.
*/
public function getTypeAttribute(): string {
return $this::class;
}
/** /**
* Get all of the ingredient amounts associated with the ingredient. * Get all of the ingredient amounts associated with the ingredient.
*/ */

View File

@ -0,0 +1,34 @@
<?php
namespace App\Rules;
use App\Models\Traits\Ingredient;
use Illuminate\Contracts\Validation\Rule;
class UsesIngredientTrait implements Rule
{
/**
* Determine if the array is empty.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value): bool
{
return (
class_exists($value)
&& in_array(Ingredient::class, class_uses_recursive($value))
);
}
/**
* Get the validation error message.
*
* @return string
*/
public function message(): string
{
return 'Invalid ingredient type :input.';
}
}

View File

@ -5,6 +5,10 @@
name="ingredients[id][]" name="ingredients[id][]"
value="{{ $defaultId ?? '' }}" value="{{ $defaultId ?? '' }}"
x-ref="ingredients"/> x-ref="ingredients"/>
<x-inputs.input type="hidden"
name="ingredients[type][]"
value="{{ $defaultType ?? '' }}"
x-ref="ingredients_type"/>
<x-inputs.input type="text" <x-inputs.input type="text"
name="ingredients[name][]" name="ingredients[name][]"
value="{{ $defaultName ?? '' }}" value="{{ $defaultName ?? '' }}"
@ -19,9 +23,12 @@
<template x-for="result in results" :key="result.id"> <template x-for="result in results" :key="result.id">
<div class="p-1 border-b-2 border-gray-500 hover:bg-yellow-300 cursor-pointer" <div class="p-1 border-b-2 border-gray-500 hover:bg-yellow-300 cursor-pointer"
x-bind:data-id="result.id" x-bind:data-id="result.id"
x-bind:data-value="result.name"> x-bind:data-type="result.type"
x-bind:data-name="result.name">
<div class="pointer-events-none"> <div class="pointer-events-none">
<div x-text="result.name"></div> <div>
<span x-text="result.name"></span><span class="text-gray-600" x-text="', ' + result.detail" x-show="result.detail"></span>
</div>
<div x-show="result.serving_size"> <div x-show="result.serving_size">
<div class="text-sm text-gray-600" x-text="result.brand" x-show="result.brand"></div> <div class="text-sm text-gray-600" x-text="result.brand" x-show="result.brand"></div>
<div class="text-sm"> <div class="text-sm">
@ -54,7 +61,10 @@
if ($event.target.value !== '') { if ($event.target.value !== '') {
fetch('{{ route('ingredient-picker.search') }}?term=' + $event.target.value) fetch('{{ route('ingredient-picker.search') }}?term=' + $event.target.value)
.then(response => response.json()) .then(response => response.json())
.then(data => { this.results = data; this.searching = true; }); .then(data => {
this.results = data;
this.searching = true;
});
} }
}, },
['@focusout.debounce.200ms']() { ['@focusout.debounce.200ms']() {
@ -65,8 +75,9 @@
['@click']($event) { ['@click']($event) {
let selected = $event.target; let selected = $event.target;
if (selected.dataset.id) { if (selected.dataset.id) {
this.$refs.ingredients_name.value = selected.dataset.value;
this.$refs.ingredients.value = selected.dataset.id; this.$refs.ingredients.value = selected.dataset.id;
this.$refs.ingredients_type.value = selected.dataset.type;
this.$refs.ingredients_name.value = selected.dataset.name;
this.searching = false; this.searching = false;
} }
} }

View File

@ -10,6 +10,7 @@
<option value=""></option> <option value=""></option>
</x-inputs.select> </x-inputs.select>
<x-ingredient-picker :default-id="$ingredient_id ?? null" <x-ingredient-picker :default-id="$ingredient_id ?? null"
:default-type="$ingredient_type ?? null"
:default-name="$ingredient_name ?? null" /> :default-name="$ingredient_name ?? null" />
<x-inputs.input type="text" <x-inputs.input type="text"
class="block" class="block"