dcasia / custom-relationship-field

Emulate HasMany relationship without having a real relationship set between resources
MIT License
30 stars 6 forks source link

After updated Nova package doesn't work #24

Open LorenzoAlu opened 11 months ago

LorenzoAlu commented 11 months ago

Hello

First of all, I congratulate you on a job well done.

Second I wanted to open this issue because after updating to the latest version of nova 4.29.5 the package unfortunately stops working.

I attach image with the error.

Screenshot 2023-11-12 alle 14 11 20
milewski commented 11 months ago

Hi I'm not able to reproduce this issue, can you show me some code of how are you using this package?

LorenzoAlu commented 11 months ago

sure. :)

This is models:

Course

<?php declare(strict_types=1);

namespace App\Nova\Resources\v1;

use App\Enums\CourseType;
use App\Enums\RoleType;
use App\Models\Database\v1\Course;
use App\Models\Database\v1\User;
use App\Nova\Filters\v1\CourseDateTimeFilter;
use App\Nova\Filters\v1\TrainerFilter;
use App\Nova\Metrics\v1\AcceptedParticipants;
use App\Nova\Metrics\v1\AcceptedPerRejectedParticipants;
use App\Nova\Metrics\v1\NewParticipants;
use App\Nova\Metrics\v1\NewParticipantsPerDay;
use App\Nova\Metrics\v1\PresentParticipants;
use App\Nova\Metrics\v1\PresentsPerAbsents;
use App\Nova\Metrics\v1\RejectedParticipants;
use Carbon\Carbon;
use Closure;
use Database\Constants\MigrationConstants;
use DigitalCreative\CustomRelationshipField\CustomRelationshipField;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Schema\Builder;
use Illuminate\Http\Request;
use Laravel\Nova\Card;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\BelongsToMany;
use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\FormData;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Textarea;
use Laravel\Nova\Http\Requests\NovaRequest;
use ShuvroRoy\NovaTabs\Tab;
use ShuvroRoy\NovaTabs\Tabs;
use ShuvroRoy\NovaTabs\Traits\HasTabs;

/**
 * Class CourseResource.
 *
 * @mixin Course
 */
final class CourseResource extends Resource
{
    use HasTabs;

    /**
     * @var class-string<Course>
     */
    public static string $model = Course::class;

    /**
     * {@inheritdoc}
     */
    public static $search = [
        'title',
    ];

    /**
     * {@inheritdoc}
     */
    public static $title = 'title';

    /**
     * {@inheritdoc}
     */
    public function cards(NovaRequest $request): array
    {
        /** @var string $cardsWidth */
        $cardsWidth = auth()->user()?->hasAnyOfRoles([
            RoleType::ROLE_TRAINER()->value,
            RoleType::ROLE_SUPERVISOR()->value,
        ]) ?
            Card::ONE_HALF_WIDTH :
            Card::ONE_THIRD_WIDTH;

        return [
            (new NewParticipants())->onlyOnDetail()
                                   ->width($cardsWidth)
                                   ->canSee(
                                       fn (Request $request): bool => $this->isAdmin($request)
                                   ),

            (new AcceptedParticipants())->onlyOnDetail()
                                        ->canSee(
                                            fn (Request $request): bool => $this->isAdmin($request)
                                        ),

            (new RejectedParticipants())->onlyOnDetail()
                                        ->canSee(
                                            fn (Request $request): bool => $this->isAdmin($request)
                                        ),

            (new PresentParticipants())->onlyOnDetail()
                                       ->width($cardsWidth)
                                       ->canSee(
                                           fn (Request $request): bool => $this->isTrainer($request) ||
                                               $this->isSupervisor($request)
                                       ),

            (new PresentsPerAbsents())->onlyOnDetail()
                                      ->canSee(
                                          fn (Request $httpRequest): bool => $this->isAdmin($httpRequest) &&
                                              self::getCourse($request)->is_in_past
                                      ),

            (new AcceptedPerRejectedParticipants())->onlyOnDetail()
                                                   ->canSee(
                                                       fn (
                                                           Request $httpRequest
                                                       ): bool => $this->isAdmin($httpRequest) &&
                                                           self::getCourse($request)->is_in_past
                                                   ),

            (new NewParticipantsPerDay())->onlyOnDetail()
                                         ->canSee(
                                             fn (Request $request): bool => $this->isAdmin($request)
                                         ),
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function fields(NovaRequest $request): array
    {
        return [
            Tabs::make('Corso', [
                Tab::make(__('Dettagli Corso'), [
                    Text::make(__('Titolo'), 'title')
                        ->rules([
                            'required',
                            'max:' . Builder::$defaultStringLength,
                        ])
                        ->sortable(),

                    DateTime::make(__('Data Inizio'), 'start_date')
                            ->rules([
                                'required',
                                'after_or_equal:today',
                            ])
                            ->step(Carbon::MINUTES_PER_HOUR)
                            ->filterable(),

                    DateTime::make(__('Data Fine'), 'end_date')
                            ->dependsOn(
                                ['start_date'],
                                static function (DateTime $field, NovaRequest $request, FormData $formData): void {
                                    if (!$formData->get('start_date')) {
                                        $field->readonly();
                                    }

                                    $field->min($formData->get('start_date') ?? 'tomorrow');
                                }
                            )
                            ->rules([
                                'required',
                                'after:valid_from',
                            ])
                            ->step(Carbon::MINUTES_PER_HOUR)
                            ->filterable(),

                    Select::make(__('Tipo'), 'type')
                          ->rules('required')
                          ->options(static fn (): array => CourseType::toArray())
                          ->displayUsingLabels()
                          ->filterable(),

                    Text::make(__('Città'), 'city')
                        ->hide()
                        ->dependsOn(
                            ['type'],
                            $this->showIfCourseIsInSite()
                        )
                        ->filterable(),

                    Text::make(__('Location'), 'location')
                        ->hide()
                        ->dependsOn(
                            ['type'],
                            $this->showIfCourseIsInSite()
                        )
                        ->suggestions(
                            Course::pluck('location')->filter()
                                  ->values()
                                  ->toArray()
                        )
                        ->filterable(),

                    BelongsTo::make(__('Formatore'), 'user', UserResource::class)
                             ->relatableQueryUsing(
                                 fn (
                                     NovaRequest $request,
                                     EloquentBuilder $query
                                 ): EloquentBuilder => $this->isAdmin($request) ?
                                     $query->withTrainerRole() :
                                     $query->whereId($request->user()?->getKey())
                             )
                             ->rules('required')
                             ->withoutTrashed()
                             ->default(fn ($request) => $this->isTrainer($request) ? $request->user()->getKey() : null),

                    Textarea::make(__('Descrizione'), 'description')
                            ->rules([
                                'required',
                                'max:' . MigrationConstants::SUPER_LONG_STRING_MAX_LENGTH,
                            ])
                            ->sortable(),
                ]),
            ]),

            Tabs::make(__('Sezione Partecipanti'), [
                HasMany::make(__('Partecipanti in attesa'), 'participants', PendingCourseParticipantResource::class)
                       ->canSee(fn (Request $request): bool => $this->isAdmin($request)),

                HasMany::make(__('Partecipanti Accettati'), 'participants', AcceptedCourseParticipantResource::class),

                HasMany::make(__('Partecipanti Rifiutati'), 'participants', RejectedCourseParticipantResource::class)
                       ->canSee(fn (Request $request): bool => $this->isAdmin($request)),
            ]),

            Tabs::make(__('Sezione Follow Up'), [
                HasMany::make(__('Follow Up Singolo'), 'followUpSingles', FollowUpSingleResource::class),

                BelongsToMany::make(__('Follow Up Multiplo'), 'followUpMultiples', FollowUpMultipleResource::class),
            ]),

            HasMany::make(__('Clienti Invitati'), 'shops', CourseInvitationResource::class),

            Tabs::make(__('Sezione Inviti'), [
                CustomRelationshipField::make(__('Punti Vendita Selezionati'), 'selectedShops', SelectedShopResource::class)
                                       ->canSee(fn (
                                           Request $request
                                       ) => $this->isAdmin($request) && !$this->is_in_past),

                CustomRelationshipField::make(__('Seleziona Punti Vendita'), 'unselectedShops', UnselectedShopResource::class)
                                       ->canSee(fn (
                                           Request $request
                                       ) => $this->isAdmin($request) && !$this->is_in_past),
            ]),
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function filters(NovaRequest $request): array
    {
        return [
            new CourseDateTimeFilter(),
            (new TrainerFilter())
                ->canSee(fn () => $this->isAdmin($request)),
        ];
    }

    /**
     * {@inheritdoc}
     */
    public static function indexQuery(NovaRequest $request, $query): EloquentBuilder
    {
        /** @var User $user */
        $user = $request->user();

        if ($user->hasRole(RoleType::ROLE_TRAINER()->value)) {
            return $query->whereUserId($user->id);
        }

        return $query;
    }

    /**
     * {@inheritdoc}
     */
    public static function label(): array|string|null
    {
        return __('Corsi');
    }

    /**
     * {@inheritdoc}
     */
    public static function singularLabel(): array|string|null
    {
        return __('Corso');
    }

    /**
     * @return Closure
     */
    private function showIfCourseIsInSite(): Closure
    {
        return static function (Text $field, NovaRequest $request, FormData $formData): void {
            if ($formData->get('type') === CourseType::ONSITE()->value) {
                $field->show();
            }
        };
    }
}

and this is Unselected shop Resource

<?php declare(strict_types=1);

namespace App\Nova\Resources\v1;

use App\Models\Database\v1\Course;
use App\Nova\Actions\v1\SelectedShop;
use App\Nova\Filters\v1\CustomerFilter;
use App\Policies\Traits\ShopListsPolicy;
use DigitalCreative\CustomRelationshipField\CustomRelationshipFieldTrait;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Laravel\Nova\Http\Requests\NovaRequest;

/**
 * Class UnselectedShopResource.
 */
final class UnselectedShopResource extends WithShopAttributes
{
    use CustomRelationshipFieldTrait;
    use ShopListsPolicy;

    /**
     * @var string
     */
    private const CONFIRM_BUTTON_TEXT = 'Seleziona Punti Vendita';

    /**
     * @var string
     */
    private const CONFIRM_TEXT = 'Selezionare Punto Vendita?';

    /**
     * @return CustomerFilter
     */
    public function getCustomerFilter(): CustomerFilter
    {
        return new CustomerFilter(true);
    }

    /**
     * {@inheritdoc}
     */
    public static function label(): array|string|null
    {
        return __('Invita Punti Vendita');
    }

    /**
     * {@inheritdoc}
     */
    public static function singularLabel(): array|string|null
    {
        return __('Invita Punto Vendita');
    }

    /**
     * @param NovaRequest $request
     *
     * @return array
     */
    public function unselectedShopsActions(NovaRequest $request): array
    {
        return [
            SelectedShop::make()
                        ->showInline()
                        ->confirmText(self::CONFIRM_TEXT)
                        ->confirmButtonText(self::CONFIRM_BUTTON_TEXT),
        ];
    }

    /**
     * @param NovaRequest $request
     * @param EloquentBuilder $query
     * @param Model $model
     *
     * @return EloquentBuilder
     */
    public static function unselectedShopsQuery(
        NovaRequest $request,
        EloquentBuilder $query,
        Model $model
    ): EloquentBuilder {
        if (!$model instanceof Course) {
            self::cleanQuerySorting($request, $query);

            return $query->orderBy(self::DEFAULT_SORTING_COLUMN);
        }

        self::cleanQuerySorting($request, $query);

        /** @var EloquentBuilder $query */
        $query = $query->active()
                       ->withExternalManagersMail()
                       ->whereDoesntHave(
                           'courses',
                           fn (EloquentBuilder $query): EloquentBuilder => $query->where('course_id', $model->id)
                       );

        return $query->orderBy(self::DEFAULT_SORTING_COLUMN);
    }
}

if you want more information I'm here ;)

Package works perfect with Nova 4.27.12,but after this release I have that error in more Nova Resources classes.

milewski commented 11 months ago

Can you isolate the issue? for example remove as much as possible from your CourseResource class to find when it breaks? I suspect the issue is with the Tabs package,

Also, your CustomRelationshipFieldTrait trait is in the wrong file, it should be on CourseResource instead

LorenzoAlu commented 11 months ago

Sure ;)


/**
 * Class CourseResource.
 *
 * @mixin Course
 */
final class CourseResource extends Resource
{
    use HasTabs;

    ... 

    /**
     * {@inheritdoc}
     */
    public function fields(NovaRequest $request): array
    {
        return [
            Tabs::make('Corso', [
                Tab::make(__('Dettagli Corso'), [

                    ..... 

                ]),
            ]),

            Tabs::make(__('Sezione Partecipanti'), [
                    .....
            ]),

            Tabs::make(__('Sezione Follow Up'), [
                    ......
            ]),

            .....

            Tabs::make(__('Sezione Inviti'), [
                CustomRelationshipField::make(__('Punti Vendita Selezionati'), 'selectedShops', SelectedShopResource::class)
                                       ->canSee(fn (
                                           Request $request
                                       ) => $this->isAdmin($request) && !$this->is_in_past),

                CustomRelationshipField::make(__('Seleziona Punti Vendita'), 'unselectedShops', UnselectedShopResource::class)
                                       ->canSee(fn (
                                           Request $request
                                       ) => $this->isAdmin($request) && !$this->is_in_past),
            ]),
        ];
    }

    ...

}
/**
 * Class UnselectedShopResource.
 */
final class UnselectedShopResource extends WithShopAttributes
{
    use CustomRelationshipFieldTrait;
    use ShopListsPolicy;

    ......

    /**
     * @param NovaRequest $request
     *
     * @return array
     */
    public function unselectedShopsActions(NovaRequest $request): array
    {
        return [
            SelectedShop::make()
                        ->showInline()
                        ->confirmText(self::CONFIRM_TEXT)
                        ->confirmButtonText(self::CONFIRM_BUTTON_TEXT),
        ];
    }

    /**
     * @param NovaRequest $request
     * @param EloquentBuilder $query
     * @param Model $model
     *
     * @return EloquentBuilder
     */
    public static function unselectedShopsQuery(
        NovaRequest $request,
        EloquentBuilder $query,
        Model $model
    ): EloquentBuilder {
        if (!$model instanceof Course) {
            self::cleanQuerySorting($request, $query);

            return $query->orderBy(self::DEFAULT_SORTING_COLUMN);
        }

        self::cleanQuerySorting($request, $query);

        /** @var EloquentBuilder $query */
        $query = $query->active()
                       ->withExternalManagersMail()
                       ->whereDoesntHave(
                           'courses',
                           fn (EloquentBuilder $query): EloquentBuilder => $query->where('course_id', $model->id)
                       );

        return $query->orderBy(self::DEFAULT_SORTING_COLUMN);
    }
}

Error occurs in CourseResource index only afteI update package. If you need more code ask me :D

milewski commented 11 months ago

Lol but I have the same setup as you (just don't have the tabs traits/package) and it is working on my end...

Could that be WithShopAttributes has some other traits in there interfering with it?

The only thing could cause this issue is if you have other traits that are also overriding the same methods as the CustomRelationshipFieldTrait

LorenzoAlu commented 11 months ago

Unfortunaly that class doesn't have trait :(

<?php declare(strict_types=1);

namespace App\Nova\Resources\v1;

use App\Models\Database\v1\Shop;
use App\Nova\Filters\v1\CustomerFilter;
use App\Nova\Filters\v1\ExternalManagerFilter;
use App\Nova\Filters\v1\MultiSelectFilter;
use App\Rules\v1\IsValidateEmail;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Schema\Builder;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
use Outl1ne\MultiselectField\Multiselect;

/**
 * Class WithShopAttributes.
 *
 * @mixin Shop
 */
abstract class WithShopAttributes extends Resource
{
    /**
     * @var int
     */
    public const MAX_MAIL_NUMBER = 4;

    /**
     * @var string
     */
    protected const DEFAULT_SORTING_COLUMN = 'shop_name';

    /**
     * @var class-string<Shop>
     */
    public static string $model = Shop::class;

    /**
     * {@inheritdoc}
     */
    public static $search = [
        'shop_code',
        'shop_name',
        'street',
        'city',
    ];

    /**
     * {@inheritdoc}
     */
    public static $title = 'shop_name';

    /**
     * {@inheritdoc}
     */
    public function fields(NovaRequest $request): array
    {
        return [
            Number::make(__('Codice Porta'), 'shop_code')
                  ->rules('required')
                  ->sortable(),

            Text::make(__('Ragione Sociale'), 'shop_name')
                ->rules([
                    'required',
                    'max:' . Builder::$defaultStringLength,
                ])
                ->sortable(),

            Text::make(__('Indirizzo'), 'street')
                ->rules([
                    'max:' . Builder::$defaultStringLength,
                ])
                ->sortable(),

            Text::make(__('Città'), 'city')
                ->rules([
                    'max:' . Builder::$defaultStringLength,
                ])
                ->sortable(),

            Text::make(__('Provincia'), 'province')
                ->rules([
                    'max:' . Builder::$defaultStringLength,
                ])
                ->sortable(),

            Text::make(__('Regione'), 'region')
                ->rules([
                    'max:' . Builder::$defaultStringLength,
                ])
                ->sortable(),

            Text::make(__('CAP'), 'cap')
                ->hideFromIndex()
                ->rules([
                    'max:' . Builder::$defaultStringLength,
                ])
                ->sortable(),

            Multiselect::make('Email Referenti Esterni', 'external_managers_mail')
                       ->taggable()
                       ->max(self::MAX_MAIL_NUMBER)
                       ->rules([
                           new IsValidateEmail(),
                       ])
                       ->saveAsJSON()
                       ->sortable(),

            Boolean::make(__('Attivo'), 'is_active')
                   ->filterable(),

            BelongsTo::make(__('Cliente'), 'customer', CustomerResource::class)
                     ->withoutTrashed()
                     ->searchable(),

            BelongsTo::make(__('Referente Interno'), 'internalManager', InternalManagerResource::class)
                     ->nullable()
                     ->withoutTrashed(),

            HasMany::make(__('Partecipanti'), 'participants', ParticipantResource::class),
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function filters(NovaRequest $request): array
    {
        return [
            $this->getCustomerFilter(),
            new ExternalManagerFilter(),
            new MultiSelectFilter(__('Provincia'), 'province'),
            new MultiSelectFilter(__('Citta'), 'city'),
            new MultiSelectFilter(__('Regione'), 'region'),
        ];
    }

    /**
     * @return CustomerFilter
     */
    abstract public function getCustomerFilter(): CustomerFilter;

    /**
     * @param NovaRequest $request
     * @param EloquentBuilder $query
     *
     * @return void
     */
    protected static function cleanQuerySorting(NovaRequest $request, EloquentBuilder $query): void
    {
        if (!$request->get('orderBy')) {
            $query->getQuery()->orders = [];
        }
    }
}
lollocoverzen commented 9 months ago

hi again :)

I'll try to add that method in UnselectedShopResource class


/**
 * Class UnselectedShopResource.
 */
final class UnselectedShopResource extends WithShopAttributes
{

    /**
     * @param NovaRequest $request
     *
     * @return array
     */
    public function unselectedShopsActions(NovaRequest $request): array
    {
        return [
            SelectedShop::make()
                        ->showInline()
                        ->confirmText(self::CONFIRM_TEXT)
                        ->confirmButtonText(self::CONFIRM_BUTTON_TEXT),
        ];
    }

    public function unselectedShopsFields(NovaRequest $request)
    {
        return $this->fields($request);
    }
}

Visualization works. But when i try to trigger an action response in always 404.

Screenshot 2024-01-20 alle 17 16 04 Screenshot 2024-01-20 alle 17 16 09

I don't know why with 4.27.12 this package works perfect. I try to remove Tabs package but application not works :(

lollocoverzen commented 9 months ago

Further notice :)

After investigating the error it seems the package update itself is causing the error. I thought I had the latest version installed. instead I am at version 1.0.0. The package stops working when switching from this version to the latest one, v1.1.2.