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\Rules\ArrayNotEmpty;
use App\Rules\StringIsDecimalOrFraction;
use App\Rules\UsesIngredientTrait;
use App\Support\Number;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
@ -83,6 +84,7 @@ class RecipeController extends Controller
'amount' => $old['amount'][$key],
'unit' => $old['unit'][$key],
'ingredient_id' => $ingredient_id,
'ingredient_type' => $old['type'][$key],
'ingredient_name' => $old['name'][$key],
'detail' => $old['detail'][$key],
];
@ -94,7 +96,8 @@ class RecipeController extends Controller
'original_key' => $key,
'amount' => $ingredientAmount->amount_formatted,
'unit' => $ingredientAmount->unit,
'ingredient_id' => $ingredientAmount->ingredient->id,
'ingredient_id' => $ingredientAmount->ingredient_id,
'ingredient_type' => $ingredientAmount->ingredient_type,
'ingredient_name' => $ingredientAmount->ingredient->name,
'detail' => $ingredientAmount->detail,
];
@ -160,12 +163,22 @@ class RecipeController extends Controller
'ingredients.detail.*' => 'nullable|string',
'ingredients.id' => ['required', 'array', new ArrayNotEmpty],
'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',
'steps.step' => ['required', 'array', new ArrayNotEmpty],
'steps.step.*' => 'nullable|string',
'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([
'name' => Str::lower($input['name']),
'description' => $input['description'],
@ -202,7 +215,7 @@ class RecipeController extends Controller
'weight' => $weight++,
]);
$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);

View File

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

View File

@ -8,6 +8,22 @@ use Illuminate\Support\Collection;
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.
*/

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][]"
value="{{ $defaultId ?? '' }}"
x-ref="ingredients"/>
<x-inputs.input type="hidden"
name="ingredients[type][]"
value="{{ $defaultType ?? '' }}"
x-ref="ingredients_type"/>
<x-inputs.input type="text"
name="ingredients[name][]"
value="{{ $defaultName ?? '' }}"
@ -19,9 +23,12 @@
<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"
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 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 class="text-sm text-gray-600" x-text="result.brand" x-show="result.brand"></div>
<div class="text-sm">
@ -54,7 +61,10 @@
if ($event.target.value !== '') {
fetch('{{ route('ingredient-picker.search') }}?term=' + $event.target.value)
.then(response => response.json())
.then(data => { this.results = data; this.searching = true; });
.then(data => {
this.results = data;
this.searching = true;
});
}
},
['@focusout.debounce.200ms']() {
@ -65,8 +75,9 @@
['@click']($event) {
let selected = $event.target;
if (selected.dataset.id) {
this.$refs.ingredients_name.value = selected.dataset.value;
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;
}
}

View File

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