Improve error messages in recipe update

This commit is contained in:
Christopher C. Wells 2021-04-12 21:20:13 -07:00
parent c9ef13a0d4
commit dbee32dc14
5 changed files with 101 additions and 56 deletions

View File

@ -2,19 +2,16 @@
namespace App\Http\Controllers;
use App\Http\Requests\UpdateRecipeRequest;
use App\Models\Food;
use App\Models\IngredientAmount;
use App\Models\Recipe;
use App\Models\RecipeSeparator;
use App\Models\RecipeStep;
use App\Rules\ArrayNotEmpty;
use App\Rules\StringIsDecimalOrFraction;
use App\Rules\UsesIngredientTrait;
use App\Support\Number;
use App\Support\Nutrients;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@ -44,12 +41,9 @@ class RecipeController extends Controller
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Throwable
*/
public function store(Request $request): RedirectResponse
public function store(UpdateRecipeRequest $request): RedirectResponse
{
return $this->update($request, new Recipe());
}
@ -187,50 +181,11 @@ class RecipeController extends Controller
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Recipe $recipe
*
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Throwable
*/
public function update(Request $request, Recipe $recipe): RedirectResponse
public function update(UpdateRecipeRequest $request, Recipe $recipe): RedirectResponse
{
$input = $request->validate([
'name' => ['required', 'string'],
'description' => ['nullable', 'string'],
'description_delta' => ['nullable', 'string'],
'image' => ['nullable', 'file', 'mimes:jpg,png,gif'],
'remove_image' => ['nullable', 'boolean'],
'servings' => ['required', 'numeric'],
'time_prep' => ['nullable', 'numeric'],
'time_cook' => ['nullable', 'numeric'],
'weight' => ['nullable', 'numeric'],
'source' => ['nullable', 'string'],
'ingredients.amount' => ['required', 'array', new ArrayNotEmpty],
'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsDecimalOrFraction],
'ingredients.unit' => ['required', 'array'],
'ingredients.unit.*' => ['required_with:ingredients.id.*'],
'ingredients.detail' => ['required', 'array'],
'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.key' => ['nullable', 'array'],
'ingredients.key.*' => ['nullable', 'int'],
'ingredients.weight' => ['required', 'array', new ArrayNotEmpty],
'ingredients.weight.*' => ['required', 'int'],
'separators.key' => ['nullable', 'array'],
'separators.key.*' => ['nullable', 'int'],
'separators.weight' => ['nullable', 'array'],
'separators.weight.*' => ['required', 'int'],
'separators.text' => ['nullable', 'array'],
'separators.text.*' => ['nullable', 'string'],
'steps.step' => ['required', 'array', new ArrayNotEmpty],
'steps.step.*' => ['nullable', 'string'],
'steps.key' => ['nullable', 'array'],
]);
$input = $request->validated();
// Validate that no ingredients are recursive.
// TODO: refactor as custom validator.
@ -285,7 +240,7 @@ class RecipeController extends Controller
->usingFileName("{$recipe->slug}.{$file->extension()}")
->toMediaCollection();
}
elseif (isset($input['remove_image']) && (bool) $input['remove_image']) {
elseif (isset($input['remove_image']) && $input['remove_image']) {
$recipe->clearMediaCollection();
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Http\Requests;
use App\Rules\ArrayNotEmpty;
use App\Rules\StringIsDecimalOrFraction;
use App\Rules\UsesIngredientTrait;
use Illuminate\Foundation\Http\FormRequest;
class UpdateRecipeRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['required', 'string'],
'description' => ['nullable', 'string'],
'description_delta' => ['nullable', 'string'],
'image' => ['nullable', 'file', 'mimes:jpg,png,gif'],
'remove_image' => ['nullable', 'boolean'],
'servings' => ['required', 'numeric'],
'time_prep' => ['nullable', 'numeric'],
'time_cook' => ['nullable', 'numeric'],
'weight' => ['nullable', 'numeric'],
'source' => ['nullable', 'string'],
'ingredients.amount' => ['required', 'array', new ArrayNotEmpty],
'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsDecimalOrFraction],
'ingredients.unit' => ['required', 'array'],
'ingredients.unit.*' => ['required_with:ingredients.id.*'],
'ingredients.detail' => ['required', 'array'],
'ingredients.detail.*' => ['nullable', 'string'],
'ingredients.id' => ['required', 'array', new ArrayNotEmpty],
'ingredients.id.*' => ['required_with:ingredients.amount.*', 'required_with:ingredients.unit.*', 'nullable'],
'ingredients.type' => ['required', 'array', new ArrayNotEmpty],
'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()],
'ingredients.key' => ['nullable', 'array'],
'ingredients.key.*' => ['nullable', 'int'],
'ingredients.weight' => ['required', 'array', new ArrayNotEmpty],
'ingredients.weight.*' => ['required', 'int'],
'separators.key' => ['nullable', 'array'],
'separators.key.*' => ['nullable', 'int'],
'separators.weight' => ['nullable', 'array'],
'separators.weight.*' => ['required', 'int'],
'separators.text' => ['nullable', 'array'],
'separators.text.*' => ['nullable', 'string'],
'steps.step' => ['required', 'array', new ArrayNotEmpty],
'steps.step.*' => ['nullable', 'string'],
'steps.key' => ['nullable', 'array'],
];
}
/**
* @inheritdoc
*/
public function messages(): array
{
return [
'ingredients.id.*.required_with' => 'Missing :attribute in Ingredients.',
'ingredients.amount.*.required_with' => 'Missing :attribute in Ingredients.',
'ingredients.unit.*.required_with' => 'Missing :attribute in Ingredients.',
];
}
/**
* @inheritdoc
*/
public function attributes(): array
{
return [
'ingredients.id.*' => 'ingredient name',
'ingredients.amount.*' => 'ingredient amount',
'ingredients.unit.*' => 'ingredient unit',
];
}
}

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,5 @@
@props(['hasError' => false])
<div x-data="picker()">
<div>
<div>
@ -11,7 +13,7 @@
x-ref="ingredients_type"/>
<x-inputs.input type="text"
name="ingredients[name][]"
class="w-full"
class="w-full{{ $hasError ? ' border-red-600' : '' }}"
value="{{ $defaultName ?? '' }}"
placeholder="Search..."
autocomplete="off"

View File

@ -1,5 +1,13 @@
@php($key = $key ?? null)
@error("ingredients.amount.{$key}")
@php($amount_error = 'border-red-600')
@enderror
@error("ingredients.unit.{$key}")
@php($unit_error = 'border-red-600')
@enderror
<div class="ingredient draggable">
<x-inputs.input type="hidden" name="ingredients[key][]" :value="$key ?? null" />
<x-inputs.input type="hidden" name="ingredients[key][]" :value="$key" />
<x-inputs.input type="hidden" name="ingredients[weight][]" :value="$weight ?? null" />
<div class="flex items-center space-x-2">
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0 w-full">
@ -11,16 +19,17 @@
<div class="w-full">
<x-ingredient-picker :default-id="$ingredient_id ?? null"
:default-type="$ingredient_type ?? null"
:default-name="$ingredient_name ?? null" />
:default-name="$ingredient_name ?? null"
:has-error="(isset($amount) || isset($unit)) && empty($ingredient_id)"/>
</div>
<x-inputs.input name="ingredients[amount][]"
type="text"
size="5"
placeholder="Amount"
class="block"
class="block {{ $amount_error ?? null }}"
:value="$amount ?? null" />
<x-inputs.select name="ingredients[unit][]"
class="block"
class="block {{ $unit_error ?? null }}"
:options="$units_supported ?? []"
:selectedValue="$unit ?? null">
<option value="" selected>Unit</option>