pqina / filepond

🌊 A flexible and fun JavaScript file upload library
https://pqina.nl/filepond
MIT License
15.08k stars 824 forks source link

[Bug] Failure to do `PATCH` request with Inertia form #1003

Closed MotionPhix closed 5 days ago

MotionPhix commented 1 week ago

Is there an existing issue for this?

Have you updated FilePond and its plugins?

Describe the bug

Firstly, I am using Inertiajs, Vue 3, and Laravel for my application. I have the following form for creating projects and uploading files to the backend. When creating a project (that's through a post request), everything works just fine. As you can see below, I am using the same form for creating and updating a project. The issue happens when I want to update a project.

<script setup lang="ts">
import { Head, Link, useForm } from "@inertiajs/vue3";

import InputError from "@/Components/InputError.vue";

import ContactSelector from "@/Components/Contact/ContactSelector.vue";

import AuthLayout from "@/Layouts/AuthLayout.vue";

import { IconPlus } from "@tabler/icons-vue";

import Spinner from "@/Components/Spinner.vue";

import TextInput from "@/Components/TextInput.vue";

import { UseDark } from "@vueuse/components";

import { DatePicker } from 'v-calendar'

import { ref } from "vue";

import 'v-calendar/style.css'

import { Project } from "@/types";

import Navheader from "@/Components/Backend/Navheader.vue";

import vueFilePond, { setOptions } from "vue-filepond";
import "filepond/dist/filepond.min.css";
import "filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css";

import FilePondPluginFileValidateType from "filepond-plugin-file-validate-type";
import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size';
import FilePondPluginImagePreview from "filepond-plugin-image-preview";

import type { FilePond } from "filepond";
import PreTap from "@/Components/PreTap.vue";

const FilePondInput = vueFilePond(
  FilePondPluginFileValidateType,
  FilePondPluginFileValidateSize,
  FilePondPluginImagePreview
);

const projectGalleryPond = ref<FilePond | null>(null);
const multiFile = ref([]);

const props = defineProps<{
  project: Project;
}>();

const form = useForm({
  name: props.project.name,
  description: props.project.description,
  poster: props.project.poster_url,
  customer_id: props.project?.customer_id,
  production: props.project.production ?? new Date(),
  images: props.project.media
});

const handlePondInit = () => {

  setOptions({
    credits: false,
    server: {
      url: undefined,
      patch: null
    }
  });

  if (props.project.media) {

    multiFile.value = props.project.media.map((image) => ({
      source: image.original_url,

      options: {
        type: 'server',
      },

    })) as any;

  }

}

const handleAddImage = () => {

  const files = projectGalleryPond.value?.getFiles();

  if (files && files.length) {

    form.images = files.map(fileItem => fileItem.file as File) as any;

  } else {

    form.images = []

  }

};

const handleRemoveImage = () => {

  handleAddImage()

};

function onSubmit() {

  form.transform((data) => {

    const formData: Partial<Project> = {
      name: data.name,
      poster_url: data.poster,
      production: data.production,
      customer_id: data.customer_id,
    };

    if (!! data.description) {
      formData.description = data.description
    }

    if (data.images?.length) {
      formData.media = data.images
    }

    return formData;

  })

  if (props.project.pid) {

    form.put(route('auth.projects.update', props.project.pid), {
      preserveScroll: true,

      onSuccess: () => {
        form.reset()
        projectGalleryPond.value?.removeFiles();
      },
    });

    return;

  }

  form.post(route('auth.projects.store'), {
    preserveScroll: true,

    onSuccess: () => {
      form.reset()
      projectGalleryPond.value?.removeFiles();
    },
  });
}

const disabledDates = ref([
  {
    repeat: {
      weekdays: [1, 7],
    },
  },
])

defineOptions({
  layout: AuthLayout,
});
</script>

<template>
    <Head
      :title="props.project.pid ? `Edit ${props.project.name}` : 'New project'"
    />

    <Navheader>

      <nav
        class="flex items-center w-full gap-1 mx-auto dark:text-white dark:border-gray-700"
      >
      <h2 class="text-xl font-semibold dark:text-gray-300 sm:inline-block">
        New project
      </h2>

      <span class="flex-1"></span>

      <button
        type="submit"
        @click.prevent="onSubmit"
        :disabled="form.processing"
        class="inline-flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium text-gray-800 bg-white border border-gray-200 shadow-sm -ms-px first:rounded-s-lg first:ms-0 rounded-s-lg rounded-e-lg focus:z-10 hover:bg-gray-50 focus:outline-none focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800 dark:focus:bg-neutral-800">

        <IconPlus stroke="2.5" size="16" />

        <span>
          {{ props.project.pid ? "Update" : "Create" }}
        </span>

        <Spinner v-if="form.processing" />

      </button>

      <Link
        as="button"
        :href="route('auth.projects.index')"
        class="inline-flex items-center px-3 py-2 text-sm font-medium text-gray-800 border border-transparent rounded-lg gap-x-2 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
        Cancel
      </Link>

    </nav>

  </Navheader>

  <article class="sm:px-6 lg:px-8">

    <section class="max-w-2xl px-6 py-12 mx-auto">

      <form>

        <div
          class="grid grid-cols-1 gap-4 mb-4 sm:gap-8 sm:grid-cols-2">

          <section class="grid col-span-2 gap-8 sm:grid-cols-2">

            <div>
              <label
                for="name"
                class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
              >
                Project name
              </label>

              <TextInput
                id="name"
                v-model="form.name"
                placeholder="Type project's name"
                class="w-full"
                type="text"
              />

              <InputError :message="form.errors.name" />
            </div>

            <div>
              <label
                for="company"
                class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
                Customer
              </label>

              <ContactSelector
                v-model="form.customer_id"
                placeholder="Pick a project's customer" />

              <InputError :message="form.errors.customer_id" />
            </div>

          </section>

          <div class="col-span-2">
            <label
              class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
              Completion date
            </label>

            <UseDark v-slot="{ isDark }">

              <DatePicker
                v-model="form.production"
                is-required show-weeknumbers
                :disabled-dates="disabledDates"
                :is-dark="isDark"
                title-position="left"
                expanded
                :masks="{
                  input: 'DD-MM-YYYY',
                }" />

            </UseDark>

            <InputError :message="form.errors.production" />
          </div>

          <div class="col-span-2">
            <label
              for="description"
              class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
              Description
            </label>

            <PreTap
              v-model="form.description"
              placeholder="Say a few things worthy noting about the project" />

            <InputError :message="form.errors.description" />
          </div>

          <div class="col-span-2">
            <label
              for="images"
              class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
              Project Images
            </label>

            <FilePondInput
              name="Project Images"
              ref="projectGalleryPond"
              :files="multiFile"
              max-file-size="2MB"
              label-idle="Drop project images here..."
              :allow-multiple="true"
              :allow-mage-preview="true"
              :allow-paste="true"
              :allow-reorder="true"
              accepted-file-types="image/jpeg, image/png"
              @init="handlePondInit"
              @addfile="handleAddImage"
              @removefile="handleRemoveImage"
            />

            <InputError :message="form.errors.images" />
          </div>

        </div>

      </form>

    </section>

  </article>

</template>

When I am updating the project, while I have managed to make the files display in the Filpond component, the update request hangs up, with a 500 server-error. If I comment out the Filepond code and perform a patch request, everything works just fine. What could I be missing?

The following is my web route file:

Route::group(['prefix' => 'projects'], function () {

    Route::get(
      '/',
      [\App\Http\Controllers\ProjectController::class, 'listing'],
    )->name('auth.projects.index');

    Route::get(
      '/c/{customer:cid?}',
      [\App\Http\Controllers\ProjectController::class, 'create'],
    )->name('auth.projects.create');

    Route::post(
      '/s',
      [\App\Http\Controllers\ProjectController::class, 'store'],
    )->name('auth.projects.store');

    Route::get(
      '/e/{project:pid}',
      [\App\Http\Controllers\ProjectController::class, 'edit'],
    )->name('auth.projects.edit');

    Route::get(
      '/i/{project:pid}',
      [\App\Http\Controllers\ProjectController::class, 'detail'],
    )->name('auth.projects.detail');

    Route::put(
      '/u/{project:pid}',
      [\App\Http\Controllers\ProjectController::class, 'update'],
    )->name('auth.projects.update');

    Route::delete(
      '/d/{project}/{image?}',
      [\App\Http\Controllers\ProjectController::class, 'destroy'],
    )->name('auth.projects.destroy');

  });

I need to upload the files together with the form, not separately. I am using Spartie's Laravel Media Library to handle file associations in the backend:

public function update(Request $request, Project $project)
{
    $customer = Customer::where('cid', $request->customer_id)->first();
    $request->merge(['customer_id' => $customer->id]);

    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'production' => 'required|date',
        'customer_id' => 'required|exists:customers,id',
        'description' => 'nullable|string',
        'images.*' => 'image|mimes:jpeg,png,jpg,gif,svg|max:2048',
    ]);

    // Update project details
    $project->update([
        'name' => $validated['name'],
        'production' => $validated['production'],
        'customer_id' => $customer->id,
        'description' => $validated['description'] ?? null,
    ]);

    // Handle image removals
    $existingMedia = $project->getMedia('bucket');
    $existingMediaUuids = $existingMedia->pluck('uuid')->toArray();
    $requestImageUuids = array_map(fn($image) => $image['uuid'], $request->input('images', []));

    // Delete images that are not present in the request
    foreach ($existingMedia as $media) {
        if (!in_array($media->uuid, $requestImageUuids)) {
            $media->delete();
        }
    }

    // Add new images
    if ($request->hasFile('images')) {
        foreach ($request->file('images') as $image) {
            // Check if the image is already present by its filename
            $fileName = $image->getClientOriginalName();
            $existingFileNames = $existingMedia->pluck('file_name')->toArray();

            // If the image file name is not in the existing list, upload it
            if (!in_array($fileName, $existingFileNames)) {
                $project->addMedia($image)->toMediaCollection('bucket');
            }
        }
    }

    return redirect()->route('auth.projects.index')->with('notify', [
        'type' => 'success',
        'title' => 'Project updated',
        'message' => 'Project has been successfully updated!',
    ]);
}

Reproduction

Hard to reproduce

Environment

- Device: HP Envy 360
- OS: Windows 10
- Browser: Edge
rikschennink commented 5 days ago

This seems very specific to this server/client set up, I don't think this is a FilePond bug. I'd suggest asking this on Stack Overflow instead.

You could start debugging by inspecting your developer tools network tab to see how the PATCH request differs between FilePond and your own PATCH request.