2

Context I'm developing a Laravel application with React (using Inertia.js) where users can update "achievements". These achievements include a banner image and a description section that can contain multiple images, a slider, and YouTube video links. The update form seems to work (no client or server-side errors), but the files (banner and description images) are not being updated.

Problem When I try to update an achievement by changing the banner image or adding/modifying description

Images:

  • The form submits without any apparent error.
  • I'm redirected as if everything was validated.
  • Server logs show that the update was performed.
  • However, when I return to the edit page, the new images are not there.

Maybe I shouldn't be using Inertia JS? I can create an achievement without any problem, and I'm able to modify all the text elements. However, when I modify things like the title and the banner, for example, the changes seem to be saved, but when I return to the page, neither the title nor the image is updated.

What I've Tried

  • Checking server-side logs
  • Adding additional logging on both client and server sides
  • Verifying server configuration for file uploads
  • Ensuring that the enctype="multipart/form-data" attribute is set on the form
php

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreAchievementRequest;
use Illuminate\Http\Request;
use App\Models\Achievement;
use Inertia\Inertia;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;

class AdminAchievementController extends Controller
{
    public function edit(Achievement $achievement)
    {
        $tags = [
            'La stratégie',
            'Les fondations',
            'Le studio',
            'Le web',
            'Les relations presse',
            'L\'évènementiel',
            'Les expertises complémentaires',
            'Le social media',
        ];

        return Inertia::render('Achievements/Admin/Edit', [
            'achievement' => $achievement,
            'tags' => $tags,
        ]);
    }

    public function update(Request $request, Achievement $achievement)
    {

        // Log all request data
        Log::info('Contenu de la requête:', $request->all());

        // Log file information if present
        if ($request->hasFile('banner')) {
            $file = $request->file('banner');
            Log::info('Informations sur le fichier banner:', [
                'name' => $file->getClientOriginalName(),
                'size' => $file->getSize(),
                'mime' => $file->getMimeType()
            ]);
        } else {
            Log::info('Aucun fichier banner dans la requête');
        }

        // Log headers
        Log::info('En-têtes de la requête:', $request->headers->all());

        $rules = [
            'title' => 'sometimes|required|string|max:255',
            'sub_title' => 'sometimes|required|string|max:255',
            'script' => 'sometimes|required|array',
            'script.*' => 'required|string',
            'answer' => 'sometimes|required|array',
            'answer.*' => 'required|string',
            'tag' => 'sometimes|required|array',
            'tag.*' => 'boolean',
            'description' => 'sometimes|nullable|array',
            'description.*.type' => 'sometimes|required|in:image,slider,youtube',
            'description.*.position' => 'sometimes|required|integer|min:1',
            'description.*.legend' => 'sometimes|nullable|string',
            'description.*.url' => 'sometimes|nullable|url',
            'site_url' => 'sometimes|nullable|array|max:3',
            'site_url.*.url' => 'required|url',
            'site_url.*.url_text' => 'required|string|max:255',
            'published' => 'sometimes|required|boolean',
            'show_on_homepage' => 'sometimes|required|boolean',
        ];

        // Ajout conditionnel des règles pour les fichiers
        if ($request->hasFile('banner')) {
            $rules['banner'] = 'required|file|mimes:jpeg,png,jpg,gif,svg,webp|max:5000';
        }

        if ($request->has('description')) {
            foreach ($request->input('description') as $key => $item) {
                if (isset($item['type']) && $item['type'] === 'image' && $request->hasFile("description.{$key}.image")) {
                    $rules["description.{$key}.image"] = 'required|file|mimes:jpeg,png,jpg,gif,svg,webp|max:5000';
                }
                if (isset($item['type']) && $item['type'] === 'slider') {
                    foreach ($item['slides'] as $slideKey => $slide) {
                        if ($request->hasFile("description.{$key}.slides.{$slideKey}")) {
                            $rules["description.{$key}.slides.{$slideKey}"] = 'required|file|mimes:jpeg,png,jpg,gif,svg,webp|max:5000';
                        }
                    }
                }
            }
        }

        $validator = Validator::make($request->all(), $rules);

        if ($validator->fails()) {
            return redirect()->back()->withErrors($validator)->withInput();
        }

        $validated = $validator->validated();

        if (isset($validated['tag'])) {
            $validated['tag'] = array_map('intval', $validated['tag']);
        }

        if ($request->hasFile('banner')) {
            Log::info('Fichier banner reçu', ['filename' => $request->file('banner')->getClientOriginalName()]);

            if ($achievement->banner) {
                Log::info('Suppression de l\'ancienne bannière', ['old_banner' => $achievement->banner]);
                Storage::delete('public/' . $achievement->banner);
            }

            $fileName = time() . '-' . $request->file('banner')->getClientOriginalName();
            $bannerPath = $request->file('banner')->storeAs('images', $fileName, 'public');
            $validated['banner'] = $bannerPath;

            Log::info('Nouvelle bannière enregistrée', ['new_banner' => $bannerPath]);
        } else {
            Log::info('Aucun nouveau fichier banner reçu');
        }

        // Traitement de la description
        if (isset($validated['description'])) {
            foreach ($validated['description'] as $key => $item) {
                if ($item['type'] === 'image' && $request->hasFile("description.{$key}.image")) {
                    if (isset($achievement->description[$key]['image'])) {
                        Storage::delete('public/' . $achievement->description[$key]['image']);
                    }
                    $fileName = time() . '-' . $request->file("description.{$key}.image")->getClientOriginalName();
                    $imagePath = $request->file("description.{$key}.image")->storeAs('images', $fileName, 'public');
                    $validated['description'][$key]['image'] = $imagePath;
                } elseif ($item['type'] === 'slider' && isset($item['slides'])) {
                    foreach ($item['slides'] as $slideKey => $slide) {
                        if ($request->hasFile("description.{$key}.slides.{$slideKey}")) {
                            if (isset($achievement->description[$key]['slides'][$slideKey])) {
                                Storage::delete('public/' . $achievement->description[$key]['slides'][$slideKey]);
                            }
                            $fileName = time() . '-' . $request->file("description.{$key}.slides.{$slideKey}")->getClientOriginalName();
                            $slidePath = $request->file("description.{$key}.slides.{$slideKey}")->storeAs('images', $fileName, 'public');
                            $validated['description'][$key]['slides'][$slideKey] = $slidePath;
                        }
                    }
                }
            }
        }

        // Mise à jour de la date de publication si nécessaire
        if (isset($validated['published'])) {
            $validated['published_at'] = $validated['published'] ? now() : null;
        }

        try {
            Log::info('Tentative de mise à jour de l\'achievement', ['data' => $validated]);
            $achievement->update($validated);
            Log::info('Achievement mis à jour avec succès');
            return redirect()->route('admin.achievements.index')->with('success', 'Réalisation mise à jour avec succès.');
        } catch (\Exception $e) {
            Log::error('Erreur lors de la mise à jour de l\'achievement', ['error' => $e->getMessage()]);
            return redirect()->back()->withErrors(['error' => 'Erreur lors de la mise à jour de la réalisation: ' . $e->getMessage()]);
        }
    }
}
import React, { useState, useEffect } from 'react';
import { Head, useForm } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { SCII_URL } from '@/config';

export default function Edit({ auth, achievement, tags }) {
    const { data, setData, put, processing, errors } = useForm({
        banner: achievement.banner,
        title: achievement.title,
        sub_title: achievement.sub_title,
        script: achievement.script || [],
        answer: achievement.answer || [],
        tag: achievement.tag || {},
        description: achievement.description || [],
        site_url: achievement.site_url || [],
        published: achievement.published,
        show_on_homepage: achievement.show_on_homepage,
    });

    const [previewBanner, setPreviewBanner] = useState(achievement.banner ? `${SCII_URL}${achievement.banner}` : null);

    const [descriptionItems, setDescriptionItems] = useState(achievement.description || []);

    useEffect(() => {
        setData('description', descriptionItems);
    }, [descriptionItems]);

    const handleAddSiteUrl = () => {
        if (data.site_url.length < 3) {
            setData('site_url', [...data.site_url, { url: '', url_text: '' }]);
        }
    };

    const handleRemoveSiteUrl = (index) => {
        const newSiteUrls = data.site_url.filter((_, i) => i !== index);
        setData('site_url', newSiteUrls);
    };

    const handleChangeSiteUrl = (index, field, value) => {
        const newSiteUrls = data.site_url.map((site, i) =>
            i === index ? { ...site, [field]: value } : site
        );
        setData('site_url', newSiteUrls);
    };

    const handleAddField = (field) => {
        setData(field, [...data[field], '']);
    };

    const handleRemoveField = (field, index) => {
        const newData = data[field].filter((_, i) => i !== index);
        setData(field, newData);
    };

    const handleChangeField = (field, index, value) => {
        const newData = data[field].map((item, i) => (i === index ? value : item));
        setData(field, newData);
    };

    const handleAddDescriptionItem = (type) => {
        const newItem = {
            type,
            position: descriptionItems.length + 1,
            ...(type === 'image' && { image: null, legend: '', url: '' }),
            ...(type === 'slider' && { slides: [] }),
            ...(type === 'youtube' && { url: '' }),
        };
        setDescriptionItems([...descriptionItems, newItem]);
    };

    const handleRemoveDescriptionItem = (index) => {
        const newItems = descriptionItems.filter((_, i) => i !== index);
        setDescriptionItems(newItems);
    };

    const handleChangeDescriptionItem = (index, field, value) => {
        const newItems = descriptionItems.map((item, i) =>
            i === index ? { ...item, [field]: value } : item
        );
        setDescriptionItems(newItems);
    };

    const handleImageUpload = (index, e) => {
        const file = e.target.files[0];
        handleChangeDescriptionItem(index, 'image', file);
    };

    const handleAddSliderImage = (index) => {
        const newItems = descriptionItems.map((item, i) =>
            i === index ? { ...item, slides: [...item.slides, null] } : item
        );
        setDescriptionItems(newItems);
    };

    const handleRemoveSliderImage = (itemIndex, slideIndex) => {
        const newItems = descriptionItems.map((item, i) =>
            i === itemIndex ? { ...item, slides: item.slides.filter((_, j) => j !== slideIndex) } : item
        );
        setDescriptionItems(newItems);
    };

    const handleChangeSliderImage = (itemIndex, slideIndex, file) => {
        const newItems = descriptionItems.map((item, i) =>
            i === itemIndex ? {
                ...item,
                slides: item.slides.map((slide, j) => j === slideIndex ? file : slide)
            } : item
        );
        setDescriptionItems(newItems);
    };

    const handleBannerChange = (e) => {
        const file = e.target.files[0];
        if (file) {
            setData('banner', file);  // Stocke le fichier sélectionné dans l'état
            setPreviewBanner(URL.createObjectURL(file));  // Optionnel pour la prévisualisation
        }
    };

    const handleSubmit = (e) => {
        e.preventDefault();

        // Créer un objet FormData pour gérer les fichiers
        const formData = new FormData();

        // Ajouter les autres champs du formulaire dans FormData
        formData.append('title', data.title);
        formData.append('sub_title', data.sub_title);
        formData.append('published', data.published);
        formData.append('show_on_homepage', data.show_on_homepage);

        // Si vous avez des objets, vous devrez les sérialiser (comme description)
        formData.append('description', JSON.stringify(descriptionItems));

        if (data.banner instanceof File) {
            formData.append('banner', data.banner);
            console.log('Banner file added to FormData:', data.banner);
        } else {
            console.log('No new banner file selected');
        }

        // Si vous avez d'autres fichiers dans descriptionItems (par exemple pour les sliders)
        descriptionItems.forEach((item, index) => {
            if (item.type === 'image' && item.image instanceof File) {
                formData.append(`description[${index}][image]`, item.image);
            }
            if (item.type === 'slider') {
                item.slides.forEach((slide, slideIndex) => {
                    if (slide instanceof File) {
                        formData.append(`description[${index}][slides][${slideIndex}]`, slide);
                    }
                });
            }
        });

        // Ajouter tous les champs au FormData
        Object.keys(data).forEach(key => {
            if (key === 'banner' && data[key] instanceof File) {
                formData.append(key, data[key]);
            } else if (typeof data[key] !== 'undefined' && data[key] !== null) {
                formData.append(key, JSON.stringify(data[key]));
            }
        });

        // Log le contenu du FormData
        for (let [key, value] of formData.entries()) {
            console.log(key, value);
        }

        // Utiliser la méthode post ou put d'Inertia en mode multipart
        put(route('admin.achievements.update', achievement.id), formData, {
            forceFormData: true,  // Forcer Inertia à utiliser FormData au lieu de JSON
        });
    };


    return (
        <AuthenticatedLayout user={auth.user}>
            <Head title="Modification d'une réalisation" />

            <div className="achievements-create-form-container">
                <form onSubmit={handleSubmit} encType="multipart/form-data">
                    <div className="form-group-input-banner">
                        <label htmlFor="banner">Bannière</label>
                        <input
                            type="file"
                            id="banner"
                            onChange={handleBannerChange}
                        />
                        {errors.banner && <div className="text-red-500">{errors.banner}</div>}
                        {previewBanner && <img src={previewBanner} alt="Preview banner" className="mt-2 max-w-xs" />}
                    </div>

                    <div className="form-group-input-title-sub-title">
                        <div className="form-group-input-title">
                            <label htmlFor="title">Titre</label>
                            <input
                                type="text"
                                id="title"
                                value={data.title}
                                onChange={(e) => setData('title', e.target.value)}
                            />
                            {errors.title && <div className="text-red-500">{errors.title}</div>}
                        </div>

                        <div className="form-group-input-sub-title">
                            <label htmlFor="sub_title">Sous titre / Phrase d'accroche</label>
                            <input
                                type="text"
                                id="sub_title"
                                value={data.sub_title}
                                onChange={(e) => setData('sub_title', e.target.value)}
                            />
                            {errors.sub_title && <div className="text-red-500">{errors.sub_title}</div>}
                        </div>
                    </div>

                    <div className="form-group-input-script">
                        <label>Le Script</label>
                        {data.script.map((p, index) => (
                            <div key={index}>
                                <textarea
                                    value={p}
                                    onChange={(e) => handleChangeField('script', index, e.target.value)}
                                />
                                <button type="button" onClick={() => handleRemoveField('script', index)}>Supprimer</button>
                            </div>
                        ))}
                        <button type="button" onClick={() => handleAddField('script')}>Ajouter un paragraphe</button>
                        {errors.script && <div className="text-red-500">{errors.script}</div>}
                    </div>

                    <div className="form-group-input-answer">
                        <label>La Réponse</label>
                        {data.answer.map((p, index) => (
                            <div key={index}>
                                <textarea
                                    value={p}
                                    onChange={(e) => handleChangeField('answer', index, e.target.value)}
                                />
                                <button type="button" onClick={() => handleRemoveField('answer', index)}>Supprimer</button>
                            </div>
                        ))}
                        <button type="button" onClick={() => handleAddField('answer')}>Ajouter un paragraphe</button>
                        {errors.answer && <div className="text-red-500">{errors.answer}</div>}
                    </div>

                    <div className="form-group-input-tags">
                        <label>Tags</label>
                        <div className="form-group-input-tags-container">
                            {tags.map((tag, index) => (
                                <div key={tag}>
                                    <label htmlFor={`tag_${tag}`}>{tag}</label>
                                    <input
                                        type="checkbox"
                                        id={`tag_${tag}`}
                                        checked={data.tag[tag] || false}
                                        onChange={(e) => {
                                            setData('tag', {
                                                ...data.tag,
                                                [tag]: e.target.checked
                                            });
                                        }}
                                    />
                                </div>
                            ))}
                        </div>
                        {errors.tag && <div className="text-red-500">{errors.tag}</div>}
                    </div>

                    <div className="form-group-input-description">
                        <label>Description</label>
                        <button type="button" onClick={() => handleAddDescriptionItem('image')}>Ajouter une image</button>
                        <button type="button" onClick={() => handleAddDescriptionItem('slider')}>Ajouter un slider</button>
                        <button type="button" onClick={() => handleAddDescriptionItem('youtube')}>Ajouter une vidéo YouTube</button>

                        {descriptionItems.map((item, index) => (
                            <div key={index} className="mb-2">
                                {item.type === 'image' && (
                                    <div className="form-group-input-description-item-image">
                                        <input
                                            type="file"
                                            accept="image/*"
                                            onChange={(e) => handleImageUpload(index, e)}
                                        />
                                        {item.image && <img src={`${SCII_URL}${item.image}`} alt="Current image" className="mt-2 max-w-xs" />}
                                        <input
                                            type="text"
                                            placeholder="Légende"
                                            value={item.legend}
                                            onChange={(e) => handleChangeDescriptionItem(index, 'legend', e.target.value)}
                                        />
                                        <input
                                            type="url"
                                            placeholder="URL associée (optionnel)"
                                            value={item.url}
                                            onChange={(e) => handleChangeDescriptionItem(index, 'url', e.target.value)}
                                        />
                                    </div>
                                )}
                                {item.type === 'slider' && (
                                    <>
                                        {item.slides.map((slide, slideIndex) => (
                                            <div key={slideIndex}>
                                                <input
                                                    type="file"
                                                    accept="image/*"
                                                    onChange={(e) => handleChangeSliderImage(index, slideIndex, e.target.files[0])}
                                                />
                                                {slide && <img src={`${SCII_URL}${slide}`} alt={`Slide ${slideIndex + 1}`} className="mt-2 max-w-xs" />}
                                                <button type="button" onClick={() => handleRemoveSliderImage(index, slideIndex)}>Supprimer</button>
                                            </div>
                                        ))}
                                        <button type="button" onClick={() => handleAddSliderImage(index)}>Ajouter une image au slider</button>
                                    </>
                                )}
                                {item.type === 'youtube' && (
                                    <input
                                        type="text"
                                        placeholder="URL de la vidéo YouTube"
                                        value={item.url}
                                        onChange={(e) => handleChangeDescriptionItem(index, 'url', e.target.value)}
                                    />
                                )}
                                <button type="button" onClick={() => handleRemoveDescriptionItem(index)}>Supprimer</button>
                            </div>
                        ))}
                    </div>

                    <div className="form-group-input-site-url">
                        <label>Lien vers site(s) lié(s)</label>
                        {data.site_url.map((site, index) => (
                            <div key={index}>
                                <input
                                    type="url"
                                    placeholder="URL"
                                    value={site.url}
                                    onChange={(e) => handleChangeSiteUrl(index, 'url', e.target.value)}
                                />
                                <input
                                    type="text"
                                    placeholder="Texte du bouton"
                                    value={site.url_text}
                                    onChange={(e) => handleChangeSiteUrl(index, 'url_text', e.target.value)}
                                />
                                <button type="button" onClick={() => handleRemoveSiteUrl(index)}>Supprimer</button>
                            </div>
                        ))}
                        {data.site_url.length < 3 && (
                            <button type="button" onClick={handleAddSiteUrl}>Ajouter un site</button>
                        )}
                        {errors.site_url && <div className="text-red-500">{errors.site_url}</div>}
                    </div>

                    <div className='form-group-input-published-show-on-homepage'>
                        <div className="form-group-input-published">
                            <label htmlFor="published">Publié</label>
                            <input
                                type="checkbox"
                                id="published"
                                checked={data.published}
                                onChange={(e) => setData('published', e.target.checked)}
                            />
                            {errors.published && <div className="text-red-500">{errors.published}</div>}
                        </div>

                        <div className="form-group-input-show-on-homepage">
                            <label htmlFor="show_on_homepage">Mettre en avant sur la page d'accueil</label>
                            <input
                                type="checkbox"
                                id="show_on_homepage"
                                checked={data.show_on_homepage}
                                onChange={(e) => setData('show_on_homepage', e.target.checked)}
                            />
                            {errors.show_on_homepage && <div className="text-red-500">{errors.show_on_homepage}</div>}
                        </div>
                    </div>

                    <button type="submit" disabled={processing}>Mettre à jour</button>
                </form>
            </div>
        </AuthenticatedLayout>
    );
}

1 Answer 1

1

please refer to inertiajs doc in the Multipart limitations it shows that you have to use the post method, and then add this_method: 'put' to your FormData. also refer to this question

Sign up to request clarification or add additional context in comments.

1 Comment

It works! Next time, I'll search better in the documentation. Thank you so much. Have a great day! :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.