platform icon indicating copy to clipboard operation
platform copied to clipboard

Duplicate request when redirecting

Open bramr94 opened this issue 6 months ago • 2 comments

Describe the bug When I save a model and redirect back to the index of the resource, the request is executed twice.

Image

This only happens to this resource, the rest of the application works fine. I want to show a toast with a message that the model is updated, but this is only shown in the first request and not in the second one.

To Reproduce

I don't really know how to reproduce this bug, at the end of the save method I execute the following code:

Toast::success('Offer is updated');

return redirect()->route('platform.offer');

Expected behavior

A single request when redirecting.

Desktop (please complete the following information):

  • OS: MacOs Sequoia
  • Browser: Firefox, Chrome, Safari

Server (please complete the following information):

  • Platfrom Version: 14.50.1, but is also present in earlier versions and the newest version.
  • Laravel Version: 12
  • PHP Version: 8.3
  • Database: MySQL
  • Database Version: 8

bramr94 avatar Jun 10 '25 12:06 bramr94

Could you please confirm that when you updated the package, you also updated the corresponding JS/CSS files? Or there any difference in the <head> tag content in these two responses?

Sometimes that can be the cause of unexpected behavior.

tabuna avatar Jun 10 '25 13:06 tabuna

@tabuna Thanks for taking a look! I used the php artisan orchid:publish command and cleared the cache. There is no difference in the head tag

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
    <title>
        Offerteverzoeken                    - Snelverwijspunt
            </title>
    <meta name="csrf_token" content="3aEZKA4aYaCPY7e7X7pZEi5929mg4sRsKj1YMzX7" id="csrf_token">
    <meta name="auth" content="1" id="auth">
            <link rel="stylesheet" type="text/css" href="/vendor/orchid/css/orchid.css?id=5fe52bcaa6d6337cc2328a4b6eebffec"  data-turbo-track="reload" >
    
    
    <meta name="view-transition" content="same-origin">
    <meta name="turbo-root" content="/dashboard">
    <meta name="turbo-refresh-method" content="replace">
    <meta name="turbo-refresh-scroll" content="reset">
    <meta name="turbo-prefetch" content="false">
    <meta name="dashboard-prefix" content="/dashboard">

    
            <script src="/vendor/orchid/js/manifest.js?id=a0cf0beb2ef26ed536c04092e6558f2a" data-turbo-track="reload" type="text/javascript"></script>
            <script src="/vendor/orchid/js/vendor.js?id=54146473cb4502c6d8a726b7d3048bfc" data-turbo-track="reload" type="text/javascript"></script>
            <script src="/vendor/orchid/js/orchid.js?id=586757a8b108298b24d0bb7fd74f0acf" data-turbo-track="reload" type="text/javascript"></script>
    
            <link rel="stylesheet" href="/css/app.css?id=2ca7648144cffbf2fe3460d68350b2c7" data-turbo-track="reload">
            <link rel="stylesheet" href="/css/app.css" data-turbo-track="reload">
            <link rel="stylesheet" href="/css/large-container.css" data-turbo-track="reload">
</head>

There was inline styling from the Laravel debugbar that I've removed.

bramr94 avatar Jun 10 '25 14:06 bramr94

@tabuna I have a platforms.php routes file that contains all the routes. When I move the route mentioned above outside this file, for example to the web.php file, the toasts work and only 1 request is fired. So I thought it had something to do with a middleware, but after disabling and enabling the middleware one by one, I still couldn't get it to work.

Do you have any idea what else this issue can be?

bramr94 avatar Jun 18 '25 06:06 bramr94

I'm a bit concerned that the screenshot shows two identical GET requests, but their sizes differ significantly — 92.06 vs. 74.87. This discrepancy seems quite substantial and might warrant more attention than just a notification.

Would it be possible for you to share the code behind your screen?

tabuna avatar Jun 18 '25 07:06 tabuna

@tabuna This is the code, I've removed some sensitive stuff. But the duplicate request is still present with this code.

<?php

namespace App\Orchid\Screens\Offer;

use App\Actions\Offer\CalculateReminderDays;
use App\Actions\Offer\Mail\SendOfferPartnerMail;
use App\Helpers\FilterCookie;
use App\Http\Resources\Planningsagenda\OfferResource;
use App\Jobs\SendMailJob;
use App\Mail\Offer\OfferMail;
use App\Mail\Offer\ReminderMail;
use App\Models\Logbook;
use App\Models\Offer;
use App\Orchid\Filters\Offer\EmployeeCodeFilter;
use App\Orchid\Filters\Offer\InterventionFilter;
use App\Orchid\Filters\Offer\PartnerFilter;
use App\Orchid\Filters\Offer\StatusFilter;
use App\Orchid\Layouts\Offer\DeclineReasonModal;
use App\Orchid\Layouts\Offer\OfferFilterLayout;
use App\Orchid\Layouts\Offer\OfferLayout;
use App\Orchid\Layouts\Offer\PlanningsagendaModal;
use App\Orchid\Layouts\Offer\ViewNotesModal;
use App\Services\OfferHistoryService;
use App\Services\PlanningsagendaService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Orchid\Screen\Actions\Link;
use Orchid\Screen\Layouts\Modal;
use Orchid\Screen\Screen;
use Orchid\Support\Facades\Dashboard;
use Orchid\Support\Facades\Layout;
use Orchid\Support\Facades\Toast;

class OfferScreen extends Screen
{
    public string $name = 'Offerteverzoeken';

    /**
     * Query data.
     *
     * @return array
     */
    public function query(): array
    {
        $user = Auth::user();
        FilterCookie::set(['intervention', 'partner', 'status']);

        $offers = Offer::query()
            ->filters()
            ->filtersApply([
                PartnerFilter::class,
                InterventionFilter::class,
                StatusFilter::class,
                EmployeeCodeFilter::class
            ])
            ->with(['partner', 'agreements'])
            ->where('created_at', '>', Carbon::now()->subYears(2))
            ->paginate();

        return [
            'offers' => $offers,
        ];
    }

    /**
     * Button commands.
     *
     * @return \Orchid\Screen\Action[]
     */
    public function commandBar(): array
    {
        return [
            Link::make('Offerte aanvragen')
                ->route('platform.offer.create')
                ->icon('plus')
                ->rawClick(),
            Link::make('Importeren')
                ->route('platform.offer.import')
                ->icon('cloud-upload'),
            Link::make('Verwijzingen export')
                ->route('platform.reference.exports')
                ->icon('database'),
            Link::make('Offerte export')
                ->route('platform.offer.exports')
                ->icon('database')
        ];
    }

    /**
     * Views.
     *
     * @return \Orchid\Screen\Layout[]|string[]
     */
    public function layout(): array
    {
        Dashboard::registerResource('stylesheets', '/css/large-container.css');

        return [
            OfferFilterLayout::class,
            OfferLayout::class,
            Layout::modal('declineReasonModal', DeclineReasonModal::class)
                ->title('Reden afwijzing')
                ->withoutApplyButton()
                ->async('asyncGetDeclineReason')
                ->size(Modal::SIZE_LG),
            Layout::modal('planningsagendaModal', PlanningsagendaModal::class)
                ->withoutApplyButton()
                ->async('asyncPlanningsagendaJson')
                ->size(Modal::SIZE_LG),
            Layout::modal('viewNotesModal', ViewNotesModal::class)
                ->withoutApplyButton()
                ->async('asyncGetNotes')
                ->size(Modal::SIZE_LG),
        ];
    }

    public function mail(Offer $offer): RedirectResponse
    {
        $offer->loadMissing('intervention');

        $mails = [];
        $offer->addCompanyContactsToMailTargets($mails, true);

        if (empty($mails)) {
            Toast::error('De mail kan niet verstuurd worden omdat er geen werkgever contactpersoon e-mail adres ingevuld is');
            return redirect()->back();
        }

        if ($offer->status === Offer::OFFER_STATE_SENT) {
            SendMailJob::dispatch($mails, new ReminderMail($offer), $offer);
            Toast::success('Herinneringsmail is verstuurd.');

            $offer->update([
                'offer_reminder_date' => Carbon::now(),
                'offer_reminder_sent' => 1,
                'sent_to_name_title' => $offer->company_contact_title,
                'sent_to_name' => $offer->company_contact_name,
                'editor' => 'Herinnering verstuurd door '.Auth::user()->name,
            ]);

            Logbook::log(Logbook::EVENT_UPDATED, 'Herinneringsmail is verstuurd', $offer, $offer->offer_number);
        }

        if ($offer->status === Offer::OFFER_STATE_NEW) {
            SendMailJob::dispatch($mails, new OfferMail($offer), $offer);

            Toast::success('Offertemail is verstuurd.');
            $update = [
                'status' => Offer::OFFER_STATE_SENT,
                'sent_to_name_title' => $offer->company_contact_title,
                'sent_to_name' => $offer->company_contact_name,
                'status_updated_at' => Carbon::now(),
                'editor' => 'Offertemail verstuurd door '.Auth::user()->name,
                'offer_sent_date' => Carbon::now(),
                'offer_reminder_date' => CalculateReminderDays::handle($offer),
            ];

            (new OfferHistoryService())->store($offer, $update);
            $offer->update($update);

            // Send the offer after the update because we need the reminder date.
            SendOfferPartnerMail::handle($offer);

            Logbook::log(Logbook::EVENT_UPDATED, 'Offertemail is verstuurd', $offer, $offer->offer_number);
        }

        return redirect()->back();
    }

    public function remove(Request $request): RedirectResponse
    {
        $statesToCheck = $request->boolean('closed')
            ? [Offer::OFFER_STATE_ACCEPTED, Offer::OFFER_STATE_DECLINED, Offer::OFFER_STATE_EXPIRED]
            : [Offer::OFFER_STATE_NEW, Offer::OFFER_STATE_SENT];

        $offer = Offer::query()
            ->whereKey($request->input('id'))
            ->whereIn('partner_id', Auth::user()->partners ?? [])
            ->whereIn('status', $statesToCheck)
            ->first();

        if (is_null($offer)) {
            Toast::error('Geen rechten om deze actie uit te voeren');
            return redirect()->route('platform.offer', FilterCookie::get());
        }

        if ($offer->pdf_upload) {
            Storage::disk('private')->delete($offer->pdf_upload);
        }

        $offer->deleteCustomOfferUpload();
        $offer->delete();

        Logbook::log(Logbook::EVENT_DELETED, 'Offerte is verwijderd', $offer, $offer->offer_number);
        Toast::info('De offerte is verwijderd.');

        return redirect()->route('platform.offer', FilterCookie::get());
    }

    public function previewReset(Request $request)
    {
        Offer::where('id', '=', $request->get('id'))
            ->whereIn('partner_id', Auth::user()->partners ?? [])
            ->update([
                'views' => 0,
            ]);

        Toast::info('Preview teller is gereset.');
        return redirect()->route('platform.offer', FilterCookie::get());
    }

    public function asyncPlanningsagendaJson(Request $request)
    {
        $offer = Offer::where('id', '=', $request->get('id'))
            ->whereIn('partner_id', Auth::user()->partners ?? [])
            ->firstOrFail();

        $offerResource = new OfferResource($offer);
        return [
            'json' => json_encode(
                $offerResource->resolve(),
                JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
            )
        ];
    }

    public function asyncGetDeclineReason(Request $request)
    {
        /** @var Offer $offer */
        $offer = Offer::where('id', '=', $request->get('id'))
            ->firstOrFail();
        return [
            'offer_number' => $offer->offer_number,
            'status' => Offer::OFFER_STATES[$offer->status],
            'declined_reason' => $offer->parsedDeclinedReason(),
            'declined_explanation' => $offer->declined_explanation
        ];
    }

    public function asyncGetNotes(Request $request)
    {
        /** @var Offer $offer */
        $offer = Offer::where('id', '=', $request->get('id'))
            ->firstOrFail();
        return [
            'internal_note' => $offer->internal_note,
        ];
    }

    /**
     * Resend offer to Planningsagenda
     *
     * @param  Request  $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function resendToPlanningsagenda(Request $request)
    {
        $offer = Offer::whereId($request->get('id'))
            ->first();

        if ($offer && (new PlanningsagendaService())->sendOffer($offer)) {
            Toast::info('Offerte is verstuurd.');
        } else {
            Toast::error('Versturen van offerte is mislukt.');
        }

        return redirect()->route('platform.offer', FilterCookie::get());
    }
}

bramr94 avatar Jun 18 '25 13:06 bramr94

It seems that this construct modifies the header, which might make the page think it's outdated.

Dashboard::registerResource('stylesheets', '/css/large-container.css');

Could you try removing it and see if that helps?

tabuna avatar Jun 20 '25 06:06 tabuna

@tabuna I've found the issue in regard to the toast not showing. In the app.blade.php file all assets are loaded with the data-turbo-track="reload" tag, when I remove this the toast works fine. I've had more problems in the past with Turbo that is why I disabled it in the config file, maybe this check needs to be added in more places? I've also noticed the Turbo middleware is still be loaded despite it being disabled in the config, I can make a PR for this if you want.

Now I've also found another issue with the Cropper field, this might also be an issue with Turbo somewhere in the vendor. It also generates a request when retrieving image. I think because something in the HTML changes which triggers Turbo to reload the page. But maybe you know where that triggers?

bramr94 avatar Jun 20 '25 09:06 bramr94

@tabuna I've found the line that is responsible for the extra request in the cropper field. Only I don't really know why this line is causing the duplicate request, do you have any idea?

https://github.com/orchidsoftware/platform/blob/master/resources/views/fields/cropper.blade.php#L25

bramr94 avatar Jul 11 '25 07:07 bramr94