add nova
This commit is contained in:
26
nova/src/Fields/AcceptsTypes.php
Normal file
26
nova/src/Fields/AcceptsTypes.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
trait AcceptsTypes
|
||||
{
|
||||
/**
|
||||
* The file types accepted by the field.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $acceptedTypes;
|
||||
|
||||
/**
|
||||
* Set the fields accepted file types.
|
||||
*
|
||||
* @param string $acceptedTypes
|
||||
* @return $this
|
||||
*/
|
||||
public function acceptedTypes($acceptedTypes)
|
||||
{
|
||||
$this->acceptedTypes = $acceptedTypes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
8
nova/src/Fields/ActionFields.php
Normal file
8
nova/src/Fields/ActionFields.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
class ActionFields extends ResolvedFields
|
||||
{
|
||||
//
|
||||
}
|
||||
33
nova/src/Fields/AsHTML.php
Normal file
33
nova/src/Fields/AsHTML.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Exceptions\HelperNotSupported;
|
||||
|
||||
trait AsHTML
|
||||
{
|
||||
/**
|
||||
* Indicates if the field value should be displayed as HTML.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $asHtml = false;
|
||||
|
||||
/**
|
||||
* Display the field as raw HTML using Vue.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Laravel\Nova\Exceptions\HelperNotSupported
|
||||
*/
|
||||
public function asHtml()
|
||||
{
|
||||
if ($this->copyable) {
|
||||
throw new HelperNotSupported("The `asHtml` option is not available on fields set to `copyable`. Please remove the `copyable` method from the {$this->name} field to enable `asHtml`.");
|
||||
}
|
||||
|
||||
$this->asHtml = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
75
nova/src/Fields/AssociatableRelation.php
Normal file
75
nova/src/Fields/AssociatableRelation.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait AssociatableRelation
|
||||
{
|
||||
/**
|
||||
* The callback that should be run to associate relations.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder):(\Illuminate\Database\Eloquent\Builder))|null
|
||||
*/
|
||||
public $relatableQueryCallback;
|
||||
|
||||
/**
|
||||
* Determines if the display values should be automatically sorted.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool
|
||||
*/
|
||||
public $reordersOnAssociatableCallback = true;
|
||||
|
||||
/**
|
||||
* Determine if the display values should be automatically sorted when rendering associatable relation.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldReorderAssociatableValues(NovaRequest $request)
|
||||
{
|
||||
if (is_callable($this->reordersOnAssociatableCallback)) {
|
||||
return call_user_func($this->reordersOnAssociatableCallback, $request);
|
||||
}
|
||||
|
||||
return $this->reordersOnAssociatableCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine reordering on associatables.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function dontReorderAssociatables()
|
||||
{
|
||||
$this->reordersOnAssociatableCallback = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine reordering on associatables.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):bool)|bool $value
|
||||
* @return $this
|
||||
*/
|
||||
public function reorderAssociatables($value = true)
|
||||
{
|
||||
$this->reordersOnAssociatableCallback = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the associate relations query.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder):(\Illuminate\Database\Eloquent\Builder))|null $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function relatableQueryUsing($callback)
|
||||
{
|
||||
$this->relatableQueryCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
55
nova/src/Fields/AttachableRelation.php
Normal file
55
nova/src/Fields/AttachableRelation.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait AttachableRelation
|
||||
{
|
||||
/**
|
||||
* Determines if the display values should be automatically sorted.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool
|
||||
*/
|
||||
public $reordersOnAttachableCallback = true;
|
||||
|
||||
/**
|
||||
* Determine if the display values should be automatically sorted when rendering attachable relation.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldReorderAttachableValues(NovaRequest $request)
|
||||
{
|
||||
if (is_callable($this->reordersOnAttachableCallback)) {
|
||||
return call_user_func($this->reordersOnAttachableCallback, $request);
|
||||
}
|
||||
|
||||
return $this->reordersOnAttachableCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine reordering on attachables.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function dontReorderAttachables()
|
||||
{
|
||||
$this->reordersOnAttachableCallback = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine reordering on attachables.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool $value
|
||||
* @return $this
|
||||
*/
|
||||
public function reorderAttachables($value = true)
|
||||
{
|
||||
$this->reordersOnAttachableCallback = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
39
nova/src/Fields/Attachments/Attachment.php
Normal file
39
nova/src/Fields/Attachments/Attachment.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Attachments;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* @property string $attachment
|
||||
* @property string $disk
|
||||
*/
|
||||
class Attachment extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'nova_field_attachments';
|
||||
|
||||
/**
|
||||
* The attributes that aren't mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* Purge the attachment.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function purge()
|
||||
{
|
||||
Storage::disk($this->disk)->delete($this->attachment);
|
||||
|
||||
$this->delete();
|
||||
}
|
||||
}
|
||||
52
nova/src/Fields/Attachments/DeleteAttachments.php
Normal file
52
nova/src/Fields/Attachments/DeleteAttachments.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Attachments;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DeleteAttachments
|
||||
{
|
||||
/**
|
||||
* The field instance.
|
||||
*
|
||||
* @var \Laravel\Nova\Fields\Field&\Laravel\Nova\Contracts\Storable
|
||||
*/
|
||||
public $field;
|
||||
|
||||
/**
|
||||
* The attachment model.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Fields\Attachments\Attachment>
|
||||
*/
|
||||
public static $model = Attachment::class;
|
||||
|
||||
/**
|
||||
* Create a new class instance.
|
||||
*
|
||||
* @param \Laravel\Nova\Fields\Field&\Laravel\Nova\Contracts\Storable $field
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($field)
|
||||
{
|
||||
$this->field = $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the attachments associated with the field.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param mixed $model
|
||||
* @return array
|
||||
*/
|
||||
public function __invoke(Request $request, $model)
|
||||
{
|
||||
static::$model::query()
|
||||
->where('attachable_type', $model->getMorphClass())
|
||||
->where('attachable_id', $model->getKey())
|
||||
->get()
|
||||
->each
|
||||
->purge();
|
||||
|
||||
return [$this->field->attribute => ''];
|
||||
}
|
||||
}
|
||||
18
nova/src/Fields/Attachments/DetachAnyAttachment.php
Normal file
18
nova/src/Fields/Attachments/DetachAnyAttachment.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Attachments;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DetachAnyAttachment
|
||||
{
|
||||
/**
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
call_user_func(new DetachAttachment, $request);
|
||||
call_user_func(new DetachPendingAttachment, $request);
|
||||
}
|
||||
}
|
||||
29
nova/src/Fields/Attachments/DetachAttachment.php
Normal file
29
nova/src/Fields/Attachments/DetachAttachment.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Attachments;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class DetachAttachment
|
||||
{
|
||||
/**
|
||||
* The attachment model.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Fields\Attachments\Attachment>
|
||||
*/
|
||||
public static $model = Attachment::class;
|
||||
|
||||
/**
|
||||
* Delete an attachment from the field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(NovaRequest $request)
|
||||
{
|
||||
static::$model::where('url', $request->attachmentUrl)
|
||||
->get()
|
||||
->each
|
||||
->purge();
|
||||
}
|
||||
}
|
||||
31
nova/src/Fields/Attachments/DetachPendingAttachment.php
Normal file
31
nova/src/Fields/Attachments/DetachPendingAttachment.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Attachments;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DetachPendingAttachment
|
||||
{
|
||||
/**
|
||||
* The pending attachment model.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Fields\Attachments\PendingAttachment>
|
||||
*/
|
||||
public static $model = PendingAttachment::class;
|
||||
|
||||
/**
|
||||
* Delete an attachment from the field.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
static::$model::where('draft_id', $request->draftId)
|
||||
->when($request->has('attachment'), function ($query) use ($request) {
|
||||
return $query->where('attachment', $request->attachment);
|
||||
})->get()
|
||||
->each
|
||||
->purge();
|
||||
}
|
||||
}
|
||||
29
nova/src/Fields/Attachments/DiscardPendingAttachments.php
Normal file
29
nova/src/Fields/Attachments/DiscardPendingAttachments.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Attachments;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DiscardPendingAttachments
|
||||
{
|
||||
/**
|
||||
* The pending attachment model.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Fields\Attachments\PendingAttachment>
|
||||
*/
|
||||
public static $model = PendingAttachment::class;
|
||||
|
||||
/**
|
||||
* Discard pending attachments on the field.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
static::$model::where('draft_id', $request->draftId)
|
||||
->get()
|
||||
->each
|
||||
->purge();
|
||||
}
|
||||
}
|
||||
117
nova/src/Fields/Attachments/PendingAttachment.php
Normal file
117
nova/src/Fields/Attachments/PendingAttachment.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Attachments;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Prunable;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Nova\Contracts\Storable;
|
||||
|
||||
/**
|
||||
* @property string $attachment
|
||||
* @property string $disk
|
||||
*/
|
||||
class PendingAttachment extends Model
|
||||
{
|
||||
use Prunable;
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'nova_pending_field_attachments';
|
||||
|
||||
/**
|
||||
* The attributes that aren't mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* The persist attachment model.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Fields\Attachments\Attachment>
|
||||
*/
|
||||
protected static $persistModel = Attachment::class;
|
||||
|
||||
/**
|
||||
* Get persist model instance.
|
||||
*
|
||||
* @return \Laravel\Nova\Fields\Attachments\Attachment
|
||||
*/
|
||||
public function getPersistModel()
|
||||
{
|
||||
return new static::$persistModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the given draft's pending attachments.
|
||||
*
|
||||
* @param string $draftId
|
||||
* @param \Laravel\Nova\Contracts\Storable $field
|
||||
* @param mixed $model
|
||||
* @return void
|
||||
*
|
||||
* @phpstan-param \Laravel\Nova\Fields\Field&\Laravel\Nova\Contracts\Storable $field
|
||||
*/
|
||||
public static function persistDraft($draftId, Storable $field, $model)
|
||||
{
|
||||
static::where('draft_id', $draftId)->get()->each->persist($field, $model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the pending attachment.
|
||||
*
|
||||
* @param \Laravel\Nova\Contracts\Storable $field
|
||||
* @param mixed $model
|
||||
* @return void
|
||||
*
|
||||
* @phpstan-param \Laravel\Nova\Fields\Field&\Laravel\Nova\Contracts\Storable $field
|
||||
*/
|
||||
public function persist(Storable $field, $model)
|
||||
{
|
||||
$disk = $field->getStorageDisk() ?? $field->getDefaultStorageDisk();
|
||||
|
||||
static::$persistModel::create([
|
||||
'attachable_type' => $model->getMorphClass(),
|
||||
'attachable_id' => $model->getKey(),
|
||||
'attachment' => $this->attachment,
|
||||
'disk' => $disk,
|
||||
'url' => Storage::disk($disk)->url($this->attachment),
|
||||
]);
|
||||
|
||||
$this->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the prunable model query.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function prunable()
|
||||
{
|
||||
return static::where('created_at', '<=', now()->subDays(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the model for pruning.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function pruning()
|
||||
{
|
||||
Storage::disk($this->disk)->delete($this->attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge the attachment.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function purge()
|
||||
{
|
||||
$this->prune();
|
||||
}
|
||||
}
|
||||
28
nova/src/Fields/Attachments/PruneStaleAttachments.php
Normal file
28
nova/src/Fields/Attachments/PruneStaleAttachments.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Attachments;
|
||||
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class PruneStaleAttachments
|
||||
{
|
||||
/**
|
||||
* The pending attachment model.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Fields\Attachments\PendingAttachment>
|
||||
*/
|
||||
public static $model = PendingAttachment::class;
|
||||
|
||||
/**
|
||||
* Prune the stale attachments from the system.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
Artisan::call('model:prune', [
|
||||
'--model' => static::$model,
|
||||
'--chunk' => 100,
|
||||
]);
|
||||
}
|
||||
}
|
||||
63
nova/src/Fields/Attachments/StorePendingAttachment.php
Normal file
63
nova/src/Fields/Attachments/StorePendingAttachment.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Attachments;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Nova\Contracts\Storable;
|
||||
|
||||
class StorePendingAttachment
|
||||
{
|
||||
/**
|
||||
* The field instance.
|
||||
*
|
||||
* @var \Laravel\Nova\Fields\Field&\Laravel\Nova\Contracts\Storable
|
||||
*/
|
||||
public $field;
|
||||
|
||||
/**
|
||||
* The pending attachment model.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Fields\Attachments\PendingAttachment>
|
||||
*/
|
||||
public static $model = PendingAttachment::class;
|
||||
|
||||
/**
|
||||
* Create a new invokable instance.
|
||||
*
|
||||
* @param \Laravel\Nova\Contracts\Storable $field
|
||||
* @return void
|
||||
*
|
||||
* @phpstan-param \Laravel\Nova\Fields\Field&\Laravel\Nova\Contracts\Storable $field
|
||||
*/
|
||||
public function __construct(Storable $field)
|
||||
{
|
||||
$this->field = $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a pending attachment to the field.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array{path: string, url: string}
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'attachment' => ['required', 'file'],
|
||||
]);
|
||||
|
||||
$disk = $this->field->getStorageDisk() ?? $this->field->getDefaultStorageDisk();
|
||||
|
||||
static::$model::create([
|
||||
'draft_id' => $request->draftId,
|
||||
'attachment' => $path = $request->file('attachment')->store($this->field->getStorageDir(), $disk),
|
||||
'disk' => $disk,
|
||||
]);
|
||||
|
||||
return [
|
||||
'path' => $path,
|
||||
'url' => Storage::disk($disk)->url($path),
|
||||
];
|
||||
}
|
||||
}
|
||||
58
nova/src/Fields/Audio.php
Normal file
58
nova/src/Fields/Audio.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Audio extends File
|
||||
{
|
||||
use PresentsAudio;
|
||||
|
||||
const PRELOAD_AUTO = 'auto';
|
||||
|
||||
const PRELOAD_METADATA = 'metadata';
|
||||
|
||||
const PRELOAD_NONE = 'none';
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'audio-field';
|
||||
|
||||
/**
|
||||
* The file types accepted by the field.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $acceptedTypes = 'audio/*';
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|callable|null $attribute
|
||||
* @param string|null $disk
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest, object, string, string, ?string, ?string):mixed)|null $storageCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, $disk = 'public', $storageCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $disk, $storageCallback);
|
||||
|
||||
$this->preview(function ($value) {
|
||||
return $value ? Storage::disk($this->getStorageDisk())->url($value) : null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field element for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), $this->audioAttributes());
|
||||
}
|
||||
}
|
||||
48
nova/src/Fields/Avatar.php
Normal file
48
nova/src/Fields/Avatar.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Contracts\Cover;
|
||||
|
||||
class Avatar extends Image implements Cover
|
||||
{
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string|null $name
|
||||
* @param string|null $attribute
|
||||
* @param string|null $disk
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest, object, string, string, ?string, ?string):(mixed))|null $storageCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name = 'Avatar', $attribute = null, $disk = null, $storageCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $disk, $storageCallback);
|
||||
|
||||
$this->rounded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Avatar field using Gravatar service.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @return \Laravel\Nova\Fields\Gravatar
|
||||
*/
|
||||
public static function gravatar($name = 'Avatar', $attribute = 'email')
|
||||
{
|
||||
return new Gravatar($name, $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Avatar field using ui-avatars service.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @return \Laravel\Nova\Fields\UiAvatar
|
||||
*/
|
||||
public static function uiavatar($name = 'Avatar', $attribute = 'name')
|
||||
{
|
||||
return new UiAvatar($name, $attribute);
|
||||
}
|
||||
}
|
||||
292
nova/src/Fields/Badge.php
Normal file
292
nova/src/Fields/Badge.php
Normal file
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Nova\Badge as BadgeComponent;
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Fields\Filters\SelectFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class Badge extends Field implements FilterableField, Unfillable
|
||||
{
|
||||
use FieldFilterable;
|
||||
|
||||
/**
|
||||
* The text alignment for the field's text in tables.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $textAlign = 'center';
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'badge-field';
|
||||
|
||||
/**
|
||||
* The labels that should be applied to the field's possible values.
|
||||
*
|
||||
* @var array<array-key, string>
|
||||
*/
|
||||
public $labels;
|
||||
|
||||
/**
|
||||
* The callback used to determine the field's label.
|
||||
*
|
||||
* @var (callable(mixed):(string))|null
|
||||
*/
|
||||
public $labelCallback;
|
||||
|
||||
/**
|
||||
* The mapping used for matching custom values to in-built badge types.
|
||||
*
|
||||
* @var array<array-key, string>
|
||||
*/
|
||||
public $map;
|
||||
|
||||
/**
|
||||
* Indicates if the field should show icons.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $withIcons = false;
|
||||
|
||||
/**
|
||||
* The built-in badge types and their corresponding CSS classes.
|
||||
*
|
||||
* @var array<array-key, string>
|
||||
*/
|
||||
public $types = [];
|
||||
|
||||
/**
|
||||
* The icons that should be applied to the field's possible values.
|
||||
*
|
||||
* @var array<array-key, string>
|
||||
*/
|
||||
public $icons = [
|
||||
'success' => 'check-circle',
|
||||
'info' => 'information-circle',
|
||||
'danger' => 'exclamation-circle',
|
||||
'warning' => 'exclamation-circle',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|\Closure|callable|object|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, callable $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback);
|
||||
|
||||
$this->addTypes(BadgeComponent::$types);
|
||||
|
||||
$this->exceptOnForms();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add badge types and their corresponding CSS classes to the built-in ones.
|
||||
*
|
||||
* @param array<array-key, string> $types
|
||||
* @return $this
|
||||
*/
|
||||
public function addTypes(array $types)
|
||||
{
|
||||
$this->types = array_merge($this->types, $types);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the badge types and their corresponding CSS classes.
|
||||
*
|
||||
* @param array<array-key, string> $types
|
||||
* @return $this
|
||||
*/
|
||||
public function types(array $types)
|
||||
{
|
||||
$this->types = $types;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the labels for each possible field value.
|
||||
*
|
||||
* @param array<array-key, string> $labels
|
||||
* @return $this
|
||||
*/
|
||||
public function labels(array $labels)
|
||||
{
|
||||
$this->labels = $labels;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback to be used to determine the field's displayable label.
|
||||
*
|
||||
* @param callable(mixed):string $labelCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function label(callable $labelCallback)
|
||||
{
|
||||
$this->labelCallback = $labelCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the possible field values to the built-in badge types.
|
||||
*
|
||||
* @param array<array-key, string> $map
|
||||
* @return $this
|
||||
*/
|
||||
public function map(array $map)
|
||||
{
|
||||
$this->map = $map;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the field to display icons, optionally passing an icon mapping.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withIcons()
|
||||
{
|
||||
$this->withIcons = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the icons for each possible field value.
|
||||
*
|
||||
* @param array<array-key, string> $icons
|
||||
* @return $this
|
||||
*/
|
||||
public function icons($icons)
|
||||
{
|
||||
$this->withIcons = true;
|
||||
$this->icons = $icons;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Badge's CSS classes based on the field's value.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function resolveBadgeClasses()
|
||||
{
|
||||
$mappedValue = $this->map[$this->value] ?? $this->value;
|
||||
|
||||
if (! isset($this->types[$mappedValue])) {
|
||||
throw new Exception("Error trying to find type [{$mappedValue}] inside of the field's type mapping.");
|
||||
}
|
||||
|
||||
return $this->types[$mappedValue];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the display label for the Badge.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function resolveLabel()
|
||||
{
|
||||
return $this->resolveLabelFor($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the display label for the Badge.
|
||||
*
|
||||
* @param string|int $value
|
||||
* @return string
|
||||
*/
|
||||
protected function resolveLabelFor($value)
|
||||
{
|
||||
if (isset($this->labelCallback)) {
|
||||
return call_user_func($this->labelCallback, $value);
|
||||
}
|
||||
|
||||
return $this->labels[$value] ?? $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
return new SelectFilter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return transform(parent::jsonSerialize(), function ($field) {
|
||||
/** @phpstan-ignore-next-line */
|
||||
$options = collect($this->map)->keys()->transform(function ($value) {
|
||||
return ['value' => $value, 'label' => $this->resolveLabelFor($value)];
|
||||
})->all();
|
||||
|
||||
return array_merge(
|
||||
Arr::only($field, [
|
||||
'uniqueKey',
|
||||
'name',
|
||||
'attribute',
|
||||
]),
|
||||
['options' => $options]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the display icon for the Badge.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function resolveIcon()
|
||||
{
|
||||
$mappedValue = $this->map[$this->value] ?? $this->value;
|
||||
|
||||
if (! isset($this->icons[$mappedValue])) {
|
||||
throw new Exception("Error trying to find icon [{$mappedValue}] inside of the field's icon mapping.");
|
||||
}
|
||||
|
||||
return $this->icons[$mappedValue];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the element for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'label' => $this->resolveLabel(),
|
||||
'typeClass' => $this->resolveBadgeClasses(),
|
||||
'icon' => $this->withIcons ? $this->resolveIcon() : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
569
nova/src/Fields/BelongsTo.php
Normal file
569
nova/src/Fields/BelongsTo.php
Normal file
@@ -0,0 +1,569 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Contracts\QueryBuilder;
|
||||
use Laravel\Nova\Contracts\RelatableField;
|
||||
use Laravel\Nova\Fields\Filters\EloquentFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Http\Requests\ResourceIndexRequest;
|
||||
use Laravel\Nova\Resource;
|
||||
use Laravel\Nova\Rules\Relatable;
|
||||
use Laravel\Nova\TrashedStatus;
|
||||
use Laravel\Nova\Util;
|
||||
|
||||
/**
|
||||
* @method static static make(mixed $name, string|null $attribute = null, string|null $resource = null)
|
||||
*/
|
||||
class BelongsTo extends Field implements FilterableField, RelatableField
|
||||
{
|
||||
use AssociatableRelation;
|
||||
use DeterminesIfCreateRelationCanBeShown;
|
||||
use EloquentFilterable;
|
||||
use FormatsRelatableDisplayValues;
|
||||
use Peekable;
|
||||
use ResolvesReverseRelation;
|
||||
use Searchable;
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'belongs-to-field';
|
||||
|
||||
/**
|
||||
* The class name of the related resource.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Resource>
|
||||
*/
|
||||
public $resourceClass;
|
||||
|
||||
/**
|
||||
* The URI key of the related resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $resourceName;
|
||||
|
||||
/**
|
||||
* The resolved BelongsTo Resource.
|
||||
*
|
||||
* @var \Laravel\Nova\Resource|null
|
||||
*/
|
||||
public $belongsToResource;
|
||||
|
||||
/**
|
||||
* The name of the Eloquent "belongs to" relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $belongsToRelationship;
|
||||
|
||||
/**
|
||||
* The key of the related Eloquent model.
|
||||
*
|
||||
* @var string|int|null
|
||||
*/
|
||||
public $belongsToId;
|
||||
|
||||
/**
|
||||
* Indicates if the related resource can be viewed.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $viewable;
|
||||
|
||||
/**
|
||||
* The callback that should be run when the field is filled.
|
||||
*
|
||||
* @var \Closure(\Laravel\Nova\Http\Requests\NovaRequest, mixed):void
|
||||
*/
|
||||
public $filledCallback;
|
||||
|
||||
/**
|
||||
* The attribute that is the inverse of this relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $inverse;
|
||||
|
||||
/**
|
||||
* The displayable singular label of the relation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $singularLabel;
|
||||
|
||||
/**
|
||||
* Indicates whether the field should display the "With Trashed" option.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $displaysWithTrashed = true;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @param class-string<\Laravel\Nova\Resource>|null $resource
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, $resource = null)
|
||||
{
|
||||
parent::__construct($name, $attribute);
|
||||
|
||||
$resource = $resource ?? ResourceRelationshipGuesser::guessResource($name);
|
||||
|
||||
$this->resourceClass = $resource;
|
||||
$this->resourceName = $resource::uriKey();
|
||||
$this->belongsToRelationship = $this->attribute = $attribute ?? ResourceRelationshipGuesser::guessRelation($name);
|
||||
$this->singularLabel = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipName()
|
||||
{
|
||||
return $this->belongsToRelationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipType()
|
||||
{
|
||||
return 'belongsTo';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field should be displayed for the given request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request&\Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(Request $request)
|
||||
{
|
||||
return $this->isNotRedundant($request) && parent::authorize($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field is not redundant.
|
||||
*
|
||||
* Ex: Is this a "user" belongs to field in a blog post list being shown on the "user" detail page.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function isNotRedundant(NovaRequest $request)
|
||||
{
|
||||
return ! $request instanceof ResourceIndexRequest || ! $this->isReverseRelation($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function resolve($resource, $attribute = null)
|
||||
{
|
||||
$value = null;
|
||||
|
||||
if ($resource instanceof Resource || $resource instanceof Model) {
|
||||
if ($resource->relationLoaded($this->attribute)) {
|
||||
$value = $resource->getRelation($this->attribute);
|
||||
} else {
|
||||
$value = $resource->{$this->attribute}()->withoutGlobalScopes()->getResults();
|
||||
}
|
||||
}
|
||||
|
||||
if ($value) {
|
||||
$this->belongsToResource = new $this->resourceClass($value);
|
||||
|
||||
$this->belongsToId = Util::safeInt($value->getKey());
|
||||
|
||||
$this->value = $this->formatDisplayValue($this->belongsToResource);
|
||||
|
||||
$this->viewable = ($this->viewable ?? true) && $this->belongsToResource->authorizedToView(app(NovaRequest::class));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve dependent field value.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function resolveDependentValue(NovaRequest $request)
|
||||
{
|
||||
return $this->belongsToId ?? $this->resolveDefaultValue($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the callback that should be used to resolve the field's value.
|
||||
*
|
||||
* @param callable $displayCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function displayUsing(callable $displayCallback)
|
||||
{
|
||||
return $this->display($displayCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array
|
||||
*/
|
||||
public function getRules(NovaRequest $request)
|
||||
{
|
||||
$query = $this->buildAssociatableQuery(
|
||||
$request, $request->{$this->attribute.'_trashed'} === 'true'
|
||||
)->toBase();
|
||||
|
||||
return array_merge_recursive(parent::getRules($request), [
|
||||
$this->attribute => array_filter([
|
||||
$this->nullable ? 'nullable' : 'required',
|
||||
new Relatable($request, $query),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @return void
|
||||
*/
|
||||
public function fill(NovaRequest $request, $model)
|
||||
{
|
||||
$foreignKey = $this->getRelationForeignKeyName($model->{$this->attribute}());
|
||||
|
||||
parent::fillInto($request, $model, $foreignKey);
|
||||
|
||||
if ($model->isDirty($foreignKey)) {
|
||||
$model->unsetRelation($this->attribute);
|
||||
}
|
||||
|
||||
if (is_callable($this->filledCallback)) {
|
||||
call_user_func($this->filledCallback, $request, $model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @return mixed
|
||||
*/
|
||||
public function fillForAction(NovaRequest $request, $model)
|
||||
{
|
||||
if ($request->exists($this->attribute)) {
|
||||
$value = $request[$this->attribute];
|
||||
|
||||
$model->{$this->attribute} = $this->resourceClass::newModel()->query()->find($value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
if ($request->exists($requestAttribute)) {
|
||||
$value = $request[$requestAttribute];
|
||||
|
||||
$relation = Relation::noConstraints(function () use ($model) {
|
||||
return $model->{$this->attribute}();
|
||||
});
|
||||
|
||||
if ($this->isValidNullValue($value)) {
|
||||
$relation->dissociate();
|
||||
} else {
|
||||
$relation->associate($relation->getQuery()->withoutGlobalScopes()->find($value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an associatable query for the field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param bool $withTrashed
|
||||
* @return \Laravel\Nova\Contracts\QueryBuilder
|
||||
*/
|
||||
public function buildAssociatableQuery(NovaRequest $request, $withTrashed = false)
|
||||
{
|
||||
$model = forward_static_call(
|
||||
[$resourceClass = $this->resourceClass, 'newModel']
|
||||
);
|
||||
|
||||
$query = app()->make(QueryBuilder::class, [$resourceClass]);
|
||||
|
||||
$request->first === 'true'
|
||||
? $query->whereKey($model->newQueryWithoutScopes(), $request->current)
|
||||
: $query->search(
|
||||
$request, $model->newQuery(), $request->search,
|
||||
[], [], TrashedStatus::fromBoolean($withTrashed)
|
||||
);
|
||||
|
||||
return $query->tap(function ($query) use ($request, $model) {
|
||||
if (is_callable($this->relatableQueryCallback)) {
|
||||
call_user_func($this->relatableQueryCallback, $request, $query);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
forward_static_call($this->associatableQueryCallable($request, $model), $request, $query, $this);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associatable query method name.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return array
|
||||
*/
|
||||
protected function associatableQueryCallable(NovaRequest $request, $model)
|
||||
{
|
||||
return ($method = $this->associatableQueryMethod($request, $model))
|
||||
? [$request->resource(), $method]
|
||||
: [$this->resourceClass, 'relatableQuery'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associatable query method name.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return string|null
|
||||
*/
|
||||
protected function associatableQueryMethod(NovaRequest $request, $model)
|
||||
{
|
||||
$method = 'relatable'.Str::plural(class_basename($model));
|
||||
|
||||
if (method_exists($request->resource(), $method)) {
|
||||
return $method;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given associatable resource.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return array
|
||||
*/
|
||||
public function formatAssociatableResource(NovaRequest $request, $resource)
|
||||
{
|
||||
return array_filter([
|
||||
'avatar' => $resource->resolveAvatarUrl($request),
|
||||
'display' => $this->formatDisplayValue($resource),
|
||||
'subtitle' => $resource->subtitle(),
|
||||
'value' => Util::safeInt($resource->getKey()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify if the related resource can be viewed.
|
||||
*
|
||||
* @param bool $value
|
||||
* @return $this
|
||||
*/
|
||||
public function viewable($value = true)
|
||||
{
|
||||
$this->viewable = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a callback that should be run when the field is filled.
|
||||
*
|
||||
* @param \Closure(\Laravel\Nova\Http\Requests\NovaRequest, mixed):void $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function filled($callback)
|
||||
{
|
||||
$this->filledCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for the field.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function setValue($value)
|
||||
{
|
||||
$this->belongsToId = Util::safeInt($value);
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the attribute name of the inverse of the relationship.
|
||||
*
|
||||
* @param string $inverse
|
||||
* @return $this
|
||||
*/
|
||||
public function inverse($inverse)
|
||||
{
|
||||
$this->inverse = $inverse;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the displayable singular label of the resource.
|
||||
*
|
||||
* @param string $singularLabel
|
||||
* @return $this
|
||||
*/
|
||||
public function singularLabel($singularLabel)
|
||||
{
|
||||
$this->singularLabel = $singularLabel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* hides the "With Trashed" option.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withoutTrashed()
|
||||
{
|
||||
$this->displaysWithTrashed = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the sortable uri key for the field.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sortableUriKey()
|
||||
{
|
||||
$request = app(NovaRequest::class);
|
||||
|
||||
return $this->getRelationForeignKeyName($request->newResource()->resource->{$this->attribute}());
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter|null
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
if ($request->viaRelationship()
|
||||
&& ($request->relationshipType ?? null) === 'hasMany'
|
||||
&& $this->resourceClass::uriKey() === $request->viaResource
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new EloquentFilter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define filterable attribute.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return string
|
||||
*/
|
||||
protected function filterableAttribute(NovaRequest $request)
|
||||
{
|
||||
return $this->getRelationForeignKeyName($request->newResource()->resource->{$this->attribute}());
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default filterable callback.
|
||||
*
|
||||
* @return callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):void
|
||||
*/
|
||||
protected function defaultFilterableCallback()
|
||||
{
|
||||
return function (NovaRequest $request, $query, $value, $attribute) {
|
||||
$query->where($attribute, '=', $value);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return transform($this->jsonSerialize(), function ($field) {
|
||||
return [
|
||||
'debounce' => $field['debounce'],
|
||||
'displaysWithTrashed' => $field['displaysWithTrashed'],
|
||||
'label' => $this->resourceClass::label(),
|
||||
'resourceName' => $field['resourceName'],
|
||||
'searchable' => $field['searchable'],
|
||||
'withSubtitles' => $field['withSubtitles'],
|
||||
'uniqueKey' => $field['uniqueKey'],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return with(app(NovaRequest::class), function ($request) {
|
||||
$viewable = ! is_null($this->viewable) ? $this->viewable : $this->resourceClass::authorizedToViewAny($request);
|
||||
|
||||
return array_merge([
|
||||
'belongsToId' => $this->belongsToId,
|
||||
'relationshipType' => $this->relationshipType(),
|
||||
'belongsToRelationship' => $this->belongsToRelationship,
|
||||
'debounce' => $this->debounce,
|
||||
'displaysWithTrashed' => $this->displaysWithTrashed,
|
||||
'label' => $this->resourceClass::label(),
|
||||
'peekable' => $this->isPeekable($request),
|
||||
'hasFieldsToPeekAt' => $this->hasFieldsToPeekAt($request),
|
||||
'resourceName' => $this->resourceName,
|
||||
'reverse' => $this->isReverseRelation($request),
|
||||
'searchable' => $this->isSearchable($request),
|
||||
'withSubtitles' => $this->withSubtitles,
|
||||
'showCreateRelationButton' => $this->createRelationShouldBeShown($request),
|
||||
'singularLabel' => $this->singularLabel,
|
||||
'viewable' => $viewable,
|
||||
], parent::jsonSerialize());
|
||||
});
|
||||
}
|
||||
}
|
||||
446
nova/src/Fields/BelongsToMany.php
Normal file
446
nova/src/Fields/BelongsToMany.php
Normal file
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Contracts\Deletable as DeletableContract;
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Contracts\ListableField;
|
||||
use Laravel\Nova\Contracts\PivotableField;
|
||||
use Laravel\Nova\Contracts\QueryBuilder;
|
||||
use Laravel\Nova\Contracts\RelatableField;
|
||||
use Laravel\Nova\Fields\Filters\EloquentFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Panel;
|
||||
use Laravel\Nova\Rules\RelatableAttachment;
|
||||
use Laravel\Nova\TrashedStatus;
|
||||
|
||||
/**
|
||||
* @method static static make(mixed $name, string|null $attribute = null, string|null $resource = null)
|
||||
*/
|
||||
class BelongsToMany extends Field implements DeletableContract, FilterableField, ListableField, PivotableField, RelatableField
|
||||
{
|
||||
use AttachableRelation;
|
||||
use Collapsable;
|
||||
use Deletable;
|
||||
use DetachesPivotModels;
|
||||
use DeterminesIfCreateRelationCanBeShown;
|
||||
use EloquentFilterable;
|
||||
use FormatsRelatableDisplayValues;
|
||||
use ManyToManyCreationRules;
|
||||
use Searchable;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'belongs-to-many-field';
|
||||
|
||||
/**
|
||||
* The class name of the related resource.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Resource>
|
||||
*/
|
||||
public $resourceClass;
|
||||
|
||||
/**
|
||||
* The URI key of the related resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $resourceName;
|
||||
|
||||
/**
|
||||
* The name of the Eloquent "belongs to many" relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $manyToManyRelationship;
|
||||
|
||||
/**
|
||||
* The callback that should be used to resolve the pivot fields.
|
||||
*
|
||||
* @var callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Model):array<int, \Laravel\Nova\Fields\Field>
|
||||
*/
|
||||
public $fieldsCallback;
|
||||
|
||||
/**
|
||||
* The callback that should be used to resolve the pivot actions.
|
||||
*
|
||||
* @var callable(\Laravel\Nova\Http\Requests\NovaRequest):array<int, \Laravel\Nova\Actions\Action>
|
||||
*/
|
||||
public $actionsCallback;
|
||||
|
||||
/**
|
||||
* The displayable name that should be used to refer to the pivot class.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $pivotName;
|
||||
|
||||
/**
|
||||
* The displayable singular label of the relation.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $singularLabel;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @param class-string<\Laravel\Nova\Resource>|null $resource
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, $resource = null)
|
||||
{
|
||||
parent::__construct($name, $attribute);
|
||||
|
||||
$resource = $resource ?? ResourceRelationshipGuesser::guessResource($name);
|
||||
|
||||
$this->resourceClass = $resource;
|
||||
$this->resourceName = $resource::uriKey();
|
||||
$this->manyToManyRelationship = $this->attribute = $attribute ?? ResourceRelationshipGuesser::guessRelation($name);
|
||||
$this->deleteCallback = $this->detachmentCallback();
|
||||
|
||||
$this->fieldsCallback = function () {
|
||||
return [];
|
||||
};
|
||||
|
||||
$this->actionsCallback = function () {
|
||||
return [];
|
||||
};
|
||||
|
||||
$this->noDuplicateRelations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipName()
|
||||
{
|
||||
return $this->manyToManyRelationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipType()
|
||||
{
|
||||
return 'belongsToMany';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field should be displayed for the given request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(Request $request)
|
||||
{
|
||||
return call_user_func(
|
||||
[$this->resourceClass, 'authorizedToViewAny'], $request
|
||||
) && parent::authorize($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function resolve($resource, $attribute = null)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array
|
||||
*/
|
||||
public function getRules(NovaRequest $request)
|
||||
{
|
||||
$query = $this->buildAttachableQuery(
|
||||
$request, $request->{$this->attribute.'_trashed'} === 'true'
|
||||
)->toBase();
|
||||
|
||||
return array_merge_recursive(parent::getRules($request), [
|
||||
$this->attribute => ['required', new RelatableAttachment($request, $query)],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the creation rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array
|
||||
*/
|
||||
public function getCreationRules(NovaRequest $request)
|
||||
{
|
||||
return array_merge_recursive(parent::getCreationRules($request), [
|
||||
$this->attribute => array_filter($this->getManyToManyCreationRules($request)),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an attachable query for the field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param bool $withTrashed
|
||||
* @return \Laravel\Nova\Contracts\QueryBuilder
|
||||
*/
|
||||
public function buildAttachableQuery(NovaRequest $request, $withTrashed = false)
|
||||
{
|
||||
$model = forward_static_call([$resourceClass = $this->resourceClass, 'newModel']);
|
||||
|
||||
$query = app()->make(QueryBuilder::class, [$resourceClass]);
|
||||
|
||||
$request->first === 'true'
|
||||
? $query->whereKey($model->newQueryWithoutScopes(), $request->current)
|
||||
: $query->search(
|
||||
$request, $model->newQuery(), $request->search,
|
||||
[], [], TrashedStatus::fromBoolean($withTrashed)
|
||||
);
|
||||
|
||||
return $query->tap(function ($query) use ($request, $model) {
|
||||
forward_static_call($this->attachableQueryCallable($request, $model), $request, $query, $this);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachable query method name.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return array
|
||||
*/
|
||||
protected function attachableQueryCallable(NovaRequest $request, $model)
|
||||
{
|
||||
return ($method = $this->attachableQueryMethod($request, $model))
|
||||
? [$request->resource(), $method]
|
||||
: [$this->resourceClass, 'relatableQuery'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachable query method name.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return string|null
|
||||
*/
|
||||
protected function attachableQueryMethod(NovaRequest $request, $model)
|
||||
{
|
||||
$method = 'relatable'.Str::plural(class_basename($model));
|
||||
|
||||
if (method_exists($request->resource(), $method)) {
|
||||
return $method;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given attachable resource.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return array
|
||||
*/
|
||||
public function formatAttachableResource(NovaRequest $request, $resource)
|
||||
{
|
||||
return array_filter([
|
||||
'avatar' => $resource->resolveAvatarUrl($request),
|
||||
'display' => $this->formatDisplayValue($resource),
|
||||
'subtitle' => $resource->subtitle(),
|
||||
'value' => optional(ID::forResource($resource))->value ?? $resource->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback to be executed to retrieve the pivot fields.
|
||||
*
|
||||
* @param callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Model):array<int, \Laravel\Nova\Fields\Field> $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function fields($callback)
|
||||
{
|
||||
$this->fieldsCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback to be executed to retrieve the pivot actions.
|
||||
*
|
||||
* @param callable(\Laravel\Nova\Http\Requests\NovaRequest):array<int, \Laravel\Nova\Actions\Action> $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function actions($callback)
|
||||
{
|
||||
$this->actionsCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the displayable name that should be used to refer to the pivot class.
|
||||
*
|
||||
* @param string $pivotName
|
||||
* @return $this
|
||||
*/
|
||||
public function referToPivotAs($pivotName)
|
||||
{
|
||||
$this->pivotName = $pivotName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the displayable singular label of the resource.
|
||||
*
|
||||
* @param string $singularLabel
|
||||
* @return $this
|
||||
*/
|
||||
public function singularLabel($singularLabel)
|
||||
{
|
||||
$this->singularLabel = $singularLabel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the validation key for the field.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function validationKey()
|
||||
{
|
||||
return $this->attribute != $this->resourceName
|
||||
? $this->resourceName
|
||||
: $this->attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter|null
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
if ($request->viaRelationship()
|
||||
&& ($request->relationshipType ?? null) === 'belongsToMany'
|
||||
&& $this->resourceClass::uriKey() === $request->viaResource
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new EloquentFilter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define filterable attribute.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return string
|
||||
*/
|
||||
protected function filterableAttribute(NovaRequest $request)
|
||||
{
|
||||
if ($request->viaRelationship()) {
|
||||
return $request->model()->getQualifiedKeyName();
|
||||
} else {
|
||||
return $this->resourceClass::newModel()->getQualifiedKeyName();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default filterable callback.
|
||||
*
|
||||
* @return callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):void
|
||||
*/
|
||||
protected function defaultFilterableCallback()
|
||||
{
|
||||
return function (NovaRequest $request, $query, $value, $attribute) {
|
||||
$viaRelationship = $request->viaRelationship() && $request->relationshipType === 'belongsToMany';
|
||||
|
||||
$query->when($viaRelationship, function ($query) use ($value) {
|
||||
$query->whereKey($value);
|
||||
}, function ($query) use ($request, $attribute, $value) {
|
||||
if ($this->resourceClass::uriKey() !== $request->viaResource) {
|
||||
$query->whereHas($this->manyToManyRelationship, function ($query) use ($value) {
|
||||
$query->whereKey($value);
|
||||
});
|
||||
} else {
|
||||
$query->whereRelation($this->manyToManyRelationship, $attribute, '=', $value);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return transform($this->jsonSerialize(), function ($field) {
|
||||
return [
|
||||
'debounce' => $field['debounce'],
|
||||
'displaysWithTrashed' => false,
|
||||
'label' => $this->resourceClass::label(),
|
||||
'resourceName' => $field['resourceName'],
|
||||
'searchable' => $field['searchable'],
|
||||
'withSubtitles' => $field['withSubtitles'],
|
||||
'uniqueKey' => $field['uniqueKey'],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make current field behaves as panel.
|
||||
*
|
||||
* @return \Laravel\Nova\Panel
|
||||
*/
|
||||
public function asPanel()
|
||||
{
|
||||
return Panel::make($this->name, [$this])
|
||||
->withMeta([
|
||||
'prefixComponent' => true,
|
||||
])->withComponent('relationship-panel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return with(app(NovaRequest::class), function ($request) {
|
||||
return array_merge([
|
||||
'collapsable' => $this->collapsable,
|
||||
'collapsedByDefault' => $this->collapsedByDefault,
|
||||
'belongsToManyRelationship' => $this->manyToManyRelationship,
|
||||
'relationshipType' => $this->relationshipType(),
|
||||
'debounce' => $this->debounce,
|
||||
'relatable' => true,
|
||||
'perPage' => $this->resourceClass::$perPageViaRelationship,
|
||||
'validationKey' => $this->validationKey(),
|
||||
'resourceName' => $this->resourceName,
|
||||
'searchable' => $this->searchable,
|
||||
'withSubtitles' => $this->withSubtitles,
|
||||
'singularLabel' => $this->singularLabel ?? $this->resourceClass::singularLabel(),
|
||||
'showCreateRelationButton' => $this->createRelationShouldBeShown($request),
|
||||
], parent::jsonSerialize());
|
||||
});
|
||||
}
|
||||
}
|
||||
146
nova/src/Fields/Boolean.php
Normal file
146
nova/src/Fields/Boolean.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Fields\Filters\BooleanFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class Boolean extends Field implements FilterableField
|
||||
{
|
||||
use FieldFilterable;
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'boolean-field';
|
||||
|
||||
/**
|
||||
* The value to be used when the field is "true".
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $trueValue = true;
|
||||
|
||||
/**
|
||||
* The value to be used when the field is "false".
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $falseValue = false;
|
||||
|
||||
/**
|
||||
* The text alignment for the field's text in tables.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $textAlign = 'center';
|
||||
|
||||
/**
|
||||
* Resolve the given attribute from the given resource.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string $attribute
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function resolveAttribute($resource, $attribute)
|
||||
{
|
||||
$value = parent::resolveAttribute($resource, $attribute);
|
||||
|
||||
return ! is_null($value)
|
||||
? ($value == $this->trueValue ? true : false)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the default value for the field.
|
||||
*
|
||||
* @return bool|null
|
||||
*/
|
||||
public function resolveDefaultValue(NovaRequest $request)
|
||||
{
|
||||
if ($request->isCreateOrAttachRequest() || $request->isActionRequest()) {
|
||||
return parent::resolveDefaultValue($request) ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
if (isset($request[$requestAttribute])) {
|
||||
$model->{$attribute} = $request[$requestAttribute] == 1
|
||||
? $this->trueValue : $this->falseValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the values to store for the field.
|
||||
*
|
||||
* @param mixed $trueValue
|
||||
* @param mixed $falseValue
|
||||
* @return $this
|
||||
*/
|
||||
public function values($trueValue, $falseValue)
|
||||
{
|
||||
return $this->trueValue($trueValue)->falseValue($falseValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the value to store when the field is "true".
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function trueValue($value)
|
||||
{
|
||||
$this->trueValue = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the value to store when the field is "false".
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function falseValue($value)
|
||||
{
|
||||
$this->falseValue = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
return new BooleanFilter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return transform($this->jsonSerialize(), function ($field) {
|
||||
return Arr::only($field, ['uniqueKey']);
|
||||
});
|
||||
}
|
||||
}
|
||||
198
nova/src/Fields/BooleanGroup.php
Normal file
198
nova/src/Fields/BooleanGroup.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Fields\Filters\BooleanGroupFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Nova;
|
||||
|
||||
class BooleanGroup extends Field implements FilterableField
|
||||
{
|
||||
use FieldFilterable;
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'boolean-group-field';
|
||||
|
||||
/**
|
||||
* The text alignment for the field's text in tables.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $textAlign = 'center';
|
||||
|
||||
/**
|
||||
* The text to be used when there are no booleans to show.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $noValueText = 'No Data';
|
||||
|
||||
/**
|
||||
* The options for the field.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $options;
|
||||
|
||||
/**
|
||||
* Determine false values should be hidden.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $hideFalseValues;
|
||||
|
||||
/**
|
||||
* Determine true values should be hidden.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $hideTrueValues;
|
||||
|
||||
/**
|
||||
* Set the options for the field.
|
||||
*
|
||||
* @param \Closure():(array|\Illuminate\Support\Collection)|array|\Illuminate\Support\Collection $options
|
||||
* @return $this
|
||||
*/
|
||||
public function options($options)
|
||||
{
|
||||
if (is_callable($options)) {
|
||||
$options = $options();
|
||||
}
|
||||
|
||||
$this->options = with(collect($options), function ($options) {
|
||||
return $options->map(function ($label, $name) use ($options) {
|
||||
return $options->isAssoc()
|
||||
? ['label' => $label, 'name' => $name]
|
||||
: ['label' => $label, 'name' => $label];
|
||||
})->values()->all();
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether false values should be hidden on the index.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function hideFalseValues()
|
||||
{
|
||||
$this->hideTrueValues = false;
|
||||
$this->hideFalseValues = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether true values should be hidden on the index.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function hideTrueValues()
|
||||
{
|
||||
$this->hideTrueValues = true;
|
||||
$this->hideFalseValues = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the text to be used when there are no booleans to show.
|
||||
*
|
||||
* @param string $text
|
||||
* @return $this
|
||||
*/
|
||||
public function noValueText($text)
|
||||
{
|
||||
$this->noValueText = $text;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
if ($request->exists($requestAttribute)) {
|
||||
$model->{$attribute} = json_decode($request[$requestAttribute], true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
return new BooleanGroupFilter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default filterable callback.
|
||||
*
|
||||
* @return callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):void
|
||||
*/
|
||||
protected function defaultFilterableCallback()
|
||||
{
|
||||
return function (NovaRequest $request, $query, $value, $attribute) {
|
||||
$value = collect($value)->reject(function ($value) {
|
||||
return is_null($value);
|
||||
})->all();
|
||||
|
||||
$query->when(! empty($value), function ($query) use ($value, $attribute) {
|
||||
return $query->whereJsonContains($attribute, $value);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return transform($this->jsonSerialize(), function ($field) {
|
||||
$field['options'] = collect($field['options'])->transform(function ($option) {
|
||||
return [
|
||||
'label' => $option['label'],
|
||||
'value' => $option['name'],
|
||||
];
|
||||
});
|
||||
|
||||
return Arr::only($field, ['uniqueKey', 'options']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'hideTrueValues' => $this->hideTrueValues,
|
||||
'hideFalseValues' => $this->hideFalseValues,
|
||||
'options' => $this->options,
|
||||
'noValueText' => Nova::__($this->noValueText),
|
||||
]);
|
||||
}
|
||||
}
|
||||
171
nova/src/Fields/Code.php
Normal file
171
nova/src/Fields/Code.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class Code extends Field
|
||||
{
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'code-field';
|
||||
|
||||
/**
|
||||
* Indicates if the field is used to manipulate JSON.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $json = false;
|
||||
|
||||
/**
|
||||
* The JSON encoding options.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $jsonOptions;
|
||||
|
||||
/**
|
||||
* Indicates if the element should be shown on the index view.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $showOnIndex = false;
|
||||
|
||||
/**
|
||||
* Indicates the visual height of the Code editor.
|
||||
*
|
||||
* @var string|int
|
||||
*/
|
||||
public $height = 300;
|
||||
|
||||
/**
|
||||
* Resolve the given attribute from the given resource.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string $attribute
|
||||
* @return mixed
|
||||
*/
|
||||
protected function resolveAttribute($resource, $attribute)
|
||||
{
|
||||
$value = parent::resolveAttribute($resource, $attribute);
|
||||
|
||||
if ($this->json) {
|
||||
return json_encode($value, $this->jsonOptions ?? JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
if ($request->exists($requestAttribute)) {
|
||||
$model->{$attribute} = $this->json
|
||||
? json_decode($request[$requestAttribute], true)
|
||||
: $request[$requestAttribute];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the code field is used to manipulate JSON.
|
||||
*
|
||||
* @param int|null $options
|
||||
* @return $this
|
||||
*/
|
||||
public function json($options = null)
|
||||
{
|
||||
$this->json = true;
|
||||
|
||||
$this->jsonOptions = $options ?? JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE;
|
||||
|
||||
return $this->options(['mode' => 'application/json']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the language syntax highlighting mode for the field.
|
||||
*
|
||||
* @param string $language
|
||||
* @return $this
|
||||
*/
|
||||
public function language($language)
|
||||
{
|
||||
return $this->options(['mode' => $language]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Code editor to display all of its contents.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function fullHeight()
|
||||
{
|
||||
$this->height = '100%';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the visual height of the Code editor to automatic.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function autoHeight()
|
||||
{
|
||||
$this->height = 'auto';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the visual height of the Code editor.
|
||||
*
|
||||
* @param string|int $height
|
||||
* @return $this
|
||||
*/
|
||||
public function height($height)
|
||||
{
|
||||
$this->height = $height;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration options for the code editor instance.
|
||||
*
|
||||
* @param array $options
|
||||
* @return $this
|
||||
*/
|
||||
public function options($options)
|
||||
{
|
||||
$currentOptions = $this->meta['options'] ?? [];
|
||||
|
||||
return $this->withMeta([
|
||||
'options' => array_merge($currentOptions, $options),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'height' => $this->height,
|
||||
]);
|
||||
}
|
||||
}
|
||||
55
nova/src/Fields/Collapsable.php
Normal file
55
nova/src/Fields/Collapsable.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
trait Collapsable
|
||||
{
|
||||
/**
|
||||
* The menu's collapsable state.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $collapsable = false;
|
||||
|
||||
/**
|
||||
* Determines whether the menu section should be collapsed by default.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $collapsedByDefault = false;
|
||||
|
||||
/**
|
||||
* Set the menu group as collapsable.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function collapsable()
|
||||
{
|
||||
$this->collapsable = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the menu group as collapsable.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function collapsible()
|
||||
{
|
||||
return $this->collapsable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the menu section as collapsed by default.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function collapsedByDefault()
|
||||
{
|
||||
$this->collapsable();
|
||||
$this->collapsedByDefault = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
43
nova/src/Fields/Color.php
Normal file
43
nova/src/Fields/Color.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class Color extends Field
|
||||
{
|
||||
use HasSuggestions;
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'color-field';
|
||||
|
||||
/**
|
||||
* The text alignment for the field's text in tables.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $textAlign = 'center';
|
||||
|
||||
/**
|
||||
* Prepare the element for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$request = app(NovaRequest::class);
|
||||
|
||||
if ($request->isFormRequest()) {
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'suggestions' => $this->resolveSuggestions($request),
|
||||
]);
|
||||
}
|
||||
|
||||
return parent::jsonSerialize();
|
||||
}
|
||||
}
|
||||
34
nova/src/Fields/Copyable.php
Normal file
34
nova/src/Fields/Copyable.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Exceptions\HelperNotSupported;
|
||||
|
||||
trait Copyable
|
||||
{
|
||||
/**
|
||||
* Indicates if the field value is copyable inside Nova.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $copyable = false;
|
||||
|
||||
/**
|
||||
* Allow the field to be copyable to the clipboard inside Nova.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Laravel\Nova\Exceptions\HelperNotSupported
|
||||
*/
|
||||
public function copyable()
|
||||
{
|
||||
if ($this->asHtml) {
|
||||
throw new HelperNotSupported(sprintf("The `%s::%s` option is not available on fields displayed as HTML. Please remove the `asHtml` method from the {$this->name} field to enable `copyable`.",
|
||||
__CLASS__, 'copyable'));
|
||||
}
|
||||
|
||||
$this->copyable = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
282
nova/src/Fields/Country.php
Normal file
282
nova/src/Fields/Country.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Nova;
|
||||
|
||||
/**
|
||||
* @phpstan-type TOptionValue string
|
||||
* @phpstan-type TOptionLabel \Laravel\Nova\Support\PendingTranslation
|
||||
*/
|
||||
class Country extends Select
|
||||
{
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|\Closure|callable|object|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, callable $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback);
|
||||
|
||||
$this->options(function () {
|
||||
return collect([
|
||||
'AF' => 'Afghanistan',
|
||||
'AX' => 'Aland Islands',
|
||||
'AL' => 'Albania',
|
||||
'DZ' => 'Algeria',
|
||||
'AS' => 'American Samoa',
|
||||
'AD' => 'Andorra',
|
||||
'AO' => 'Angola',
|
||||
'AI' => 'Anguilla',
|
||||
'AQ' => 'Antarctica',
|
||||
'AG' => 'Antigua and Barbuda',
|
||||
'AR' => 'Argentina',
|
||||
'AM' => 'Armenia',
|
||||
'AW' => 'Aruba',
|
||||
'AU' => 'Australia',
|
||||
'AT' => 'Austria',
|
||||
'AZ' => 'Azerbaijan',
|
||||
'BS' => 'Bahamas',
|
||||
'BH' => 'Bahrain',
|
||||
'BD' => 'Bangladesh',
|
||||
'BB' => 'Barbados',
|
||||
'BY' => 'Belarus',
|
||||
'BE' => 'Belgium',
|
||||
'BZ' => 'Belize',
|
||||
'BJ' => 'Benin',
|
||||
'BM' => 'Bermuda',
|
||||
'BT' => 'Bhutan',
|
||||
'BO' => 'Bolivia',
|
||||
'BQ' => 'Bonaire, Sint Eustatius and Saba',
|
||||
'BA' => 'Bosnia and Herzegovina',
|
||||
'BW' => 'Botswana',
|
||||
'BV' => 'Bouvet Island',
|
||||
'BR' => 'Brazil',
|
||||
'IO' => 'British Indian Ocean Territory',
|
||||
'BN' => 'Brunei Darussalam',
|
||||
'BG' => 'Bulgaria',
|
||||
'BF' => 'Burkina Faso',
|
||||
'BI' => 'Burundi',
|
||||
'KH' => 'Cambodia',
|
||||
'CM' => 'Cameroon',
|
||||
'CA' => 'Canada',
|
||||
'CV' => 'Cape Verde',
|
||||
'KY' => 'Cayman Islands',
|
||||
'CF' => 'Central African Republic',
|
||||
'TD' => 'Chad',
|
||||
'CL' => 'Chile',
|
||||
'CN' => 'China',
|
||||
'CX' => 'Christmas Island',
|
||||
'CC' => 'Cocos (Keeling) Islands',
|
||||
'CO' => 'Colombia',
|
||||
'KM' => 'Comoros',
|
||||
'CG' => 'Congo',
|
||||
'CD' => 'Congo, Democratic Republic',
|
||||
'CK' => 'Cook Islands',
|
||||
'CR' => 'Costa Rica',
|
||||
'CI' => "Cote D'Ivoire",
|
||||
'HR' => 'Croatia',
|
||||
'CU' => 'Cuba',
|
||||
'CW' => 'Curaçao',
|
||||
'CY' => 'Cyprus',
|
||||
'CZ' => 'Czech Republic',
|
||||
'DK' => 'Denmark',
|
||||
'DJ' => 'Djibouti',
|
||||
'DM' => 'Dominica',
|
||||
'DO' => 'Dominican Republic',
|
||||
'EC' => 'Ecuador',
|
||||
'EG' => 'Egypt',
|
||||
'SV' => 'El Salvador',
|
||||
'GQ' => 'Equatorial Guinea',
|
||||
'ER' => 'Eritrea',
|
||||
'EE' => 'Estonia',
|
||||
'ET' => 'Ethiopia',
|
||||
'FK' => 'Falkland Islands (Malvinas)',
|
||||
'FO' => 'Faroe Islands',
|
||||
'FJ' => 'Fiji',
|
||||
'FI' => 'Finland',
|
||||
'FR' => 'France',
|
||||
'GF' => 'French Guiana',
|
||||
'PF' => 'French Polynesia',
|
||||
'TF' => 'French Southern Territories',
|
||||
'GA' => 'Gabon',
|
||||
'GM' => 'Gambia',
|
||||
'GE' => 'Georgia',
|
||||
'DE' => 'Germany',
|
||||
'GH' => 'Ghana',
|
||||
'GI' => 'Gibraltar',
|
||||
'GR' => 'Greece',
|
||||
'GL' => 'Greenland',
|
||||
'GD' => 'Grenada',
|
||||
'GP' => 'Guadeloupe',
|
||||
'GU' => 'Guam',
|
||||
'GT' => 'Guatemala',
|
||||
'GG' => 'Guernsey',
|
||||
'GN' => 'Guinea',
|
||||
'GW' => 'Guinea-Bissau',
|
||||
'GY' => 'Guyana',
|
||||
'HT' => 'Haiti',
|
||||
'HM' => 'Heard Island and Mcdonald Islands',
|
||||
'VA' => 'Holy See (Vatican City State)',
|
||||
'HN' => 'Honduras',
|
||||
'HK' => 'Hong Kong',
|
||||
'HU' => 'Hungary',
|
||||
'IS' => 'Iceland',
|
||||
'IN' => 'India',
|
||||
'ID' => 'Indonesia',
|
||||
'IR' => 'Iran, Islamic Republic Of',
|
||||
'IQ' => 'Iraq',
|
||||
'IE' => 'Ireland',
|
||||
'IM' => 'Isle Of Man',
|
||||
'IL' => 'Israel',
|
||||
'IT' => 'Italy',
|
||||
'JM' => 'Jamaica',
|
||||
'JP' => 'Japan',
|
||||
'JE' => 'Jersey',
|
||||
'JO' => 'Jordan',
|
||||
'KZ' => 'Kazakhstan',
|
||||
'KE' => 'Kenya',
|
||||
'KI' => 'Kiribati',
|
||||
'KP' => "Korea, Democratic People's Republic Of",
|
||||
'KR' => 'Korea',
|
||||
'XK' => 'Kosovo',
|
||||
'KW' => 'Kuwait',
|
||||
'KG' => 'Kyrgyzstan',
|
||||
'LA' => "Lao People's Democratic Republic",
|
||||
'LV' => 'Latvia',
|
||||
'LB' => 'Lebanon',
|
||||
'LS' => 'Lesotho',
|
||||
'LR' => 'Liberia',
|
||||
'LY' => 'Libya',
|
||||
'LI' => 'Liechtenstein',
|
||||
'LT' => 'Lithuania',
|
||||
'LU' => 'Luxembourg',
|
||||
'MO' => 'Macao',
|
||||
'MK' => 'Macedonia',
|
||||
'MG' => 'Madagascar',
|
||||
'MW' => 'Malawi',
|
||||
'MY' => 'Malaysia',
|
||||
'MV' => 'Maldives',
|
||||
'ML' => 'Mali',
|
||||
'MT' => 'Malta',
|
||||
'MH' => 'Marshall Islands',
|
||||
'MQ' => 'Martinique',
|
||||
'MR' => 'Mauritania',
|
||||
'MU' => 'Mauritius',
|
||||
'YT' => 'Mayotte',
|
||||
'MX' => 'Mexico',
|
||||
'FM' => 'Micronesia, Federated States Of',
|
||||
'MD' => 'Moldova',
|
||||
'MC' => 'Monaco',
|
||||
'MN' => 'Mongolia',
|
||||
'ME' => 'Montenegro',
|
||||
'MS' => 'Montserrat',
|
||||
'MA' => 'Morocco',
|
||||
'MZ' => 'Mozambique',
|
||||
'MM' => 'Myanmar',
|
||||
'NA' => 'Namibia',
|
||||
'NR' => 'Nauru',
|
||||
'NP' => 'Nepal',
|
||||
'NL' => 'Netherlands',
|
||||
'NC' => 'New Caledonia',
|
||||
'NZ' => 'New Zealand',
|
||||
'NI' => 'Nicaragua',
|
||||
'NE' => 'Niger',
|
||||
'NG' => 'Nigeria',
|
||||
'NU' => 'Niue',
|
||||
'NF' => 'Norfolk Island',
|
||||
'MP' => 'Northern Mariana Islands',
|
||||
'NO' => 'Norway',
|
||||
'OM' => 'Oman',
|
||||
'PK' => 'Pakistan',
|
||||
'PW' => 'Palau',
|
||||
'PS' => 'Palestinian Territory, Occupied',
|
||||
'PA' => 'Panama',
|
||||
'PG' => 'Papua New Guinea',
|
||||
'PY' => 'Paraguay',
|
||||
'PE' => 'Peru',
|
||||
'PH' => 'Philippines',
|
||||
'PN' => 'Pitcairn',
|
||||
'PL' => 'Poland',
|
||||
'PT' => 'Portugal',
|
||||
'PR' => 'Puerto Rico',
|
||||
'QA' => 'Qatar',
|
||||
'RE' => 'Reunion',
|
||||
'RO' => 'Romania',
|
||||
'RU' => 'Russian Federation',
|
||||
'RW' => 'Rwanda',
|
||||
'BL' => 'Saint Barthelemy',
|
||||
'SH' => 'Saint Helena',
|
||||
'KN' => 'Saint Kitts and Nevis',
|
||||
'LC' => 'Saint Lucia',
|
||||
'MF' => 'Saint Martin',
|
||||
'PM' => 'Saint Pierre and Miquelon',
|
||||
'VC' => 'Saint Vincent and Grenadines',
|
||||
'WS' => 'Samoa',
|
||||
'SM' => 'San Marino',
|
||||
'ST' => 'Sao Tome and Principe',
|
||||
'SA' => 'Saudi Arabia',
|
||||
'SN' => 'Senegal',
|
||||
'RS' => 'Serbia',
|
||||
'SC' => 'Seychelles',
|
||||
'SL' => 'Sierra Leone',
|
||||
'SG' => 'Singapore',
|
||||
'SX' => 'Sint Maarten (Dutch part)',
|
||||
'SK' => 'Slovakia',
|
||||
'SI' => 'Slovenia',
|
||||
'SB' => 'Solomon Islands',
|
||||
'SO' => 'Somalia',
|
||||
'ZA' => 'South Africa',
|
||||
'GS' => 'South Georgia and Sandwich Isl.',
|
||||
'SS' => 'South Sudan',
|
||||
'ES' => 'Spain',
|
||||
'LK' => 'Sri Lanka',
|
||||
'SD' => 'Sudan',
|
||||
'SR' => 'Suriname',
|
||||
'SJ' => 'Svalbard and Jan Mayen',
|
||||
'SZ' => 'Swaziland',
|
||||
'SE' => 'Sweden',
|
||||
'CH' => 'Switzerland',
|
||||
'SY' => 'Syrian Arab Republic',
|
||||
'TW' => 'Taiwan',
|
||||
'TJ' => 'Tajikistan',
|
||||
'TZ' => 'Tanzania',
|
||||
'TH' => 'Thailand',
|
||||
'TL' => 'Timor-Leste',
|
||||
'TG' => 'Togo',
|
||||
'TK' => 'Tokelau',
|
||||
'TO' => 'Tonga',
|
||||
'TT' => 'Trinidad and Tobago',
|
||||
'TN' => 'Tunisia',
|
||||
'TR' => 'Turkey',
|
||||
'TM' => 'Turkmenistan',
|
||||
'TC' => 'Turks and Caicos Islands',
|
||||
'TV' => 'Tuvalu',
|
||||
'UG' => 'Uganda',
|
||||
'UA' => 'Ukraine',
|
||||
'AE' => 'United Arab Emirates',
|
||||
'GB' => 'United Kingdom',
|
||||
'US' => 'United States',
|
||||
'UM' => 'United States Outlying Islands',
|
||||
'UY' => 'Uruguay',
|
||||
'UZ' => 'Uzbekistan',
|
||||
'VU' => 'Vanuatu',
|
||||
'VE' => 'Venezuela',
|
||||
'VN' => 'Vietnam',
|
||||
'VG' => 'Virgin Islands, British',
|
||||
'VI' => 'Virgin Islands, U.S.',
|
||||
'WF' => 'Wallis and Futuna',
|
||||
'EH' => 'Western Sahara',
|
||||
'YE' => 'Yemen',
|
||||
'ZM' => 'Zambia',
|
||||
'ZW' => 'Zimbabwe',
|
||||
])->transform(function ($country) {
|
||||
return Nova::__($country);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
273
nova/src/Fields/Currency.php
Normal file
273
nova/src/Fields/Currency.php
Normal file
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Brick\Money\Context;
|
||||
use Brick\Money\Context\CustomContext;
|
||||
use Brick\Money\Money;
|
||||
use NumberFormatter;
|
||||
use Symfony\Polyfill\Intl\Icu\Currencies;
|
||||
|
||||
class Currency extends Number
|
||||
{
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'currency-field';
|
||||
|
||||
/**
|
||||
* The format the field will be displayed in.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $format;
|
||||
|
||||
/**
|
||||
* The locale of the field.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $locale;
|
||||
|
||||
/**
|
||||
* The currency of the value.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $currency;
|
||||
|
||||
/**
|
||||
* The symbol used by the currency.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $currencySymbol = null;
|
||||
|
||||
/**
|
||||
* Whether the currency is using minor units.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $minorUnits = false;
|
||||
|
||||
/**
|
||||
* The context to use when creating the Money instance.
|
||||
*
|
||||
* @var \Brick\Money\Context|null
|
||||
*/
|
||||
public $context = null;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|\Closure|callable|object|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback);
|
||||
|
||||
$this->locale = config('app.locale', 'en');
|
||||
$this->currency = config('nova.currency', 'USD');
|
||||
|
||||
$this->step($this->getStepValue())
|
||||
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
|
||||
$value = $request->$requestAttribute;
|
||||
|
||||
if ($this->minorUnits && ! $this->isValidNullValue($value)) {
|
||||
$model->$attribute = $this->toMoneyInstance(
|
||||
$value * (10 ** Currencies::getFractionDigits($this->currency)),
|
||||
$this->currency
|
||||
)->getMinorAmount()->toInt();
|
||||
} else {
|
||||
$model->$attribute = $value;
|
||||
}
|
||||
})
|
||||
->displayUsing(function ($value) {
|
||||
return ! $this->isValidNullValue($value) ? $this->formatMoney($value) : null;
|
||||
})
|
||||
->resolveUsing(function ($value) {
|
||||
if ($this->isValidNullValue($value) || ! $this->minorUnits) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $this->toMoneyInstance($value)->getAmount()->toFloat();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the value to a Money instance.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param null|string $currency
|
||||
* @return \Brick\Money\Money
|
||||
*/
|
||||
public function toMoneyInstance($value, $currency = null)
|
||||
{
|
||||
$currency = $currency ?? $this->currency;
|
||||
$method = $this->minorUnits ? 'ofMinor' : 'of';
|
||||
|
||||
$context = $this->context ?? new CustomContext(Currencies::getFractionDigits($currency));
|
||||
|
||||
return Money::{$method}($value, $currency, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the field's value into Money format.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param null|string $currency
|
||||
* @param null|string $locale
|
||||
* @return string
|
||||
*/
|
||||
public function formatMoney($value, $currency = null, $locale = null)
|
||||
{
|
||||
$money = $this->toMoneyInstance($value, $currency);
|
||||
|
||||
if (is_null($this->currencySymbol)) {
|
||||
return $money->formatTo($locale ?? $this->locale);
|
||||
}
|
||||
|
||||
return tap(new NumberFormatter($locale ?? $this->locale, NumberFormatter::CURRENCY), function ($formatter) use ($money) {
|
||||
$scale = $money->getAmount()->getScale();
|
||||
|
||||
$formatter->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $this->currencySymbol);
|
||||
$formatter->setSymbol(NumberFormatter::INTL_CURRENCY_SYMBOL, $this->currencySymbol);
|
||||
$formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $scale);
|
||||
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $scale);
|
||||
})->format($money->getAmount()->toFloat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the currency code for the field.
|
||||
*
|
||||
* @param string $currency
|
||||
* @return $this
|
||||
*/
|
||||
public function currency($currency)
|
||||
{
|
||||
$this->currency = strtoupper($currency);
|
||||
|
||||
$this->step($this->getStepValue());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the field locale.
|
||||
*
|
||||
* @param string $locale
|
||||
* @return $this
|
||||
*/
|
||||
public function locale($locale)
|
||||
{
|
||||
$this->locale = $locale;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the symbol used by the field.
|
||||
*
|
||||
* @param string $symbol
|
||||
* @return $this
|
||||
*/
|
||||
public function symbol($symbol)
|
||||
{
|
||||
$this->currencySymbol = $symbol;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the field to use minor units.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function asMinorUnits()
|
||||
{
|
||||
$this->minorUnits = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the field to use major units.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function asMajorUnits()
|
||||
{
|
||||
$this->minorUnits = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the symbol used by the currency.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function resolveCurrencySymbol()
|
||||
{
|
||||
if ($this->currencySymbol) {
|
||||
return $this->currencySymbol;
|
||||
}
|
||||
|
||||
return Currencies::getSymbol($this->currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the context used to create the Money instance.
|
||||
*
|
||||
* @param \Brick\Money\Context $context
|
||||
* @return $this
|
||||
*/
|
||||
public function context(Context $context)
|
||||
{
|
||||
$this->context = $context;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check value for null value.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidNullValue($value)
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::isValidNullValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the step value for the field.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getStepValue()
|
||||
{
|
||||
return (string) 0.1 ** Currencies::getFractionDigits($this->currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'currency' => $this->resolveCurrencySymbol(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
199
nova/src/Fields/Date.php
Normal file
199
nova/src/Fields/Date.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
use Carbon\CarbonInterval;
|
||||
use DateTimeInterface;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Fields\Filters\DateFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class Date extends Field implements FilterableField
|
||||
{
|
||||
use FieldFilterable;
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'date-field';
|
||||
|
||||
/**
|
||||
* The minimum value that can be assigned to the field.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $min;
|
||||
|
||||
/**
|
||||
* The maximum value that can be assigned to the field.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $max;
|
||||
|
||||
/**
|
||||
* The step size the field will increment and decrement by.
|
||||
*
|
||||
* @var string|int|null
|
||||
*/
|
||||
public $step;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|\Closure|callable|object|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, callable $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback ?? function ($value) {
|
||||
if (! is_null($value)) {
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value instanceof CarbonInterface
|
||||
? $value->toDateString()
|
||||
: $value->format('Y-m-d');
|
||||
}
|
||||
|
||||
throw new Exception("Date field must cast to 'date' in Eloquent model.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum value that can be assigned to the field.
|
||||
*
|
||||
* @param \Carbon\CarbonInterface|string $min
|
||||
* @return $this
|
||||
*/
|
||||
public function min($min)
|
||||
{
|
||||
if (is_string($min)) {
|
||||
$min = Carbon::parse($min);
|
||||
}
|
||||
|
||||
$this->min = $min->toDateString();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum value that can be assigned to the field.
|
||||
*
|
||||
* @param \Carbon\CarbonInterface|string $max
|
||||
* @return $this
|
||||
*/
|
||||
public function max($max)
|
||||
{
|
||||
if (is_string($max)) {
|
||||
$max = Carbon::parse($max);
|
||||
}
|
||||
|
||||
$this->max = $max->toDateString();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The step size the field will increment and decrement by.
|
||||
*
|
||||
* @param string|int|\Carbon\CarbonInterval $step
|
||||
* @return $this
|
||||
*/
|
||||
public function step($step)
|
||||
{
|
||||
$this->step = $step instanceof CarbonInterval ? $step->totalDays : $step;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the default value for the field.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function resolveDefaultValue(NovaRequest $request)
|
||||
{
|
||||
/** @var \DateTimeInterface|string|null $value */
|
||||
$value = parent::resolveDefaultValue($request);
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value instanceof CarbonInterface
|
||||
? $value->toDateString()
|
||||
: $value->format('Y-m-d');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
return new DateFilter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default filterable callback.
|
||||
*
|
||||
* @return callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):\Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function defaultFilterableCallback()
|
||||
{
|
||||
return function (NovaRequest $request, $query, $value, $attribute) {
|
||||
[$min, $max] = $value;
|
||||
|
||||
if (! is_null($min) && ! is_null($max)) {
|
||||
return $query->whereBetween($attribute, [$min, $max]);
|
||||
} elseif (! is_null($min)) {
|
||||
return $query->where($attribute, '>=', $min);
|
||||
}
|
||||
|
||||
return $query->where($attribute, '<=', $max);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return transform($this->jsonSerialize(), function ($field) {
|
||||
return Arr::only($field, [
|
||||
'uniqueKey',
|
||||
'name',
|
||||
'attribute',
|
||||
'type',
|
||||
'placeholder',
|
||||
'extraAttributes',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the element for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), array_filter([
|
||||
'min' => $this->min,
|
||||
'max' => $this->max,
|
||||
'step' => $this->step ?? 'any',
|
||||
]));
|
||||
}
|
||||
}
|
||||
229
nova/src/Fields/DateTime.php
Normal file
229
nova/src/Fields/DateTime.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
use Carbon\CarbonInterval;
|
||||
use DateTimeInterface;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Fields\Filters\DateTimeFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class DateTime extends Field implements FilterableField
|
||||
{
|
||||
use FieldFilterable;
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'date-time-field';
|
||||
|
||||
/**
|
||||
* The original raw value of the field.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $originalValue;
|
||||
|
||||
/**
|
||||
* The minimum value that can be assigned to the field.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $min;
|
||||
|
||||
/**
|
||||
* The maximum value that can be assigned to the field.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $max;
|
||||
|
||||
/**
|
||||
* The step size the field will increment and decrement by.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $step;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|\Closure|callable|object|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, callable $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback ?? function ($value, $request) {
|
||||
if (! is_null($value)) {
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value instanceof CarbonInterface
|
||||
? $value->toIso8601String()
|
||||
: $value->format(DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
throw new Exception("DateTime field must cast to 'datetime' in Eloquent model.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum value that can be assigned to the field.
|
||||
*
|
||||
* @param \Carbon\CarbonInterface|string $min
|
||||
* @return $this
|
||||
*/
|
||||
public function min($min)
|
||||
{
|
||||
if (is_string($min)) {
|
||||
$min = Carbon::parse($min);
|
||||
}
|
||||
|
||||
$this->min = $min->toDateTimeLocalString();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum value that can be assigned to the field.
|
||||
*
|
||||
* @param \Carbon\CarbonInterface|string $max
|
||||
* @return $this
|
||||
*/
|
||||
public function max($max)
|
||||
{
|
||||
if (is_string($max)) {
|
||||
$max = Carbon::parse($max);
|
||||
}
|
||||
|
||||
$this->max = $max->toDateTimeLocalString();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The step size the field will increment and decrement by.
|
||||
*
|
||||
* @param int|\Carbon\CarbonInterval $step
|
||||
* @return $this
|
||||
*/
|
||||
public function step($step)
|
||||
{
|
||||
$this->step = $step instanceof CarbonInterval ? $step->totalSeconds : $step;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the default value for the field.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function resolveDefaultValue(NovaRequest $request)
|
||||
{
|
||||
$value = parent::resolveDefaultValue($request);
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value instanceof CarbonInterface
|
||||
? $value->toIso8601String()
|
||||
: $value->format(DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value using the display callback.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param mixed $resource
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
protected function resolveUsingDisplayCallback($value, $resource, $attribute)
|
||||
{
|
||||
$this->usesCustomizedDisplay = true;
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
$this->value = $value instanceof CarbonInterface
|
||||
? $value->toIso8601String()
|
||||
: $value->format(DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
$this->originalValue = $this->value;
|
||||
$this->displayedAs = call_user_func($this->displayCallback, $value, $resource, $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
return new DateTimeFilter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default filterable callback.
|
||||
*
|
||||
* @return callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):\Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function defaultFilterableCallback()
|
||||
{
|
||||
return function (NovaRequest $request, $query, $value, $attribute) {
|
||||
[$min, $max] = $value;
|
||||
|
||||
if (! is_null($min) && ! is_null($max)) {
|
||||
return $query->whereBetween($attribute, [$min, $max]);
|
||||
} elseif (! is_null($min)) {
|
||||
return $query->where($attribute, '>=', $min);
|
||||
}
|
||||
|
||||
return $query->where($attribute, '<=', $max);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return transform($this->jsonSerialize(), function ($field) {
|
||||
return Arr::only($field, [
|
||||
'uniqueKey',
|
||||
'name',
|
||||
'attribute',
|
||||
'type',
|
||||
'placeholder',
|
||||
'extraAttributes',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge([
|
||||
'originalValue' => $this->originalValue,
|
||||
], array_filter([
|
||||
'min' => $this->min,
|
||||
'max' => $this->max,
|
||||
'step' => $this->step ?? 1,
|
||||
]), parent::jsonSerialize());
|
||||
}
|
||||
}
|
||||
76
nova/src/Fields/Deletable.php
Normal file
76
nova/src/Fields/Deletable.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
trait Deletable
|
||||
{
|
||||
/**
|
||||
* The callback used to delete the field.
|
||||
*
|
||||
* @var callable|null
|
||||
*/
|
||||
public $deleteCallback;
|
||||
|
||||
/**
|
||||
* Indicates if the underlying field is deletable.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deletable = true;
|
||||
|
||||
/**
|
||||
* Indicates if the underlying field is prunable.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $prunable = false;
|
||||
|
||||
/**
|
||||
* Specify the callback that should be used to delete the field.
|
||||
*
|
||||
* @param callable $deleteCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function delete(callable $deleteCallback)
|
||||
{
|
||||
$this->deleteCallback = $deleteCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify if the underlying file is able to be deleted.
|
||||
*
|
||||
* @param bool $deletable
|
||||
* @return $this
|
||||
*/
|
||||
public function deletable($deletable = true)
|
||||
{
|
||||
$this->deletable = $deletable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the underlying file should be pruned when the resource is deleted.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isPrunable()
|
||||
{
|
||||
return $this->prunable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify if the underlying file should be pruned when the resource is deleted.
|
||||
*
|
||||
* @param bool $prunable
|
||||
* @return $this
|
||||
*/
|
||||
public function prunable($prunable = true)
|
||||
{
|
||||
$this->prunable = $prunable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
107
nova/src/Fields/Dependent.php
Normal file
107
nova/src/Fields/Dependent.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
/**
|
||||
* @phpstan-type TDependentResolver (callable(static, \Laravel\Nova\Http\Requests\NovaRequest, \Laravel\Nova\Fields\FormData):(void))|class-string
|
||||
*/
|
||||
class Dependent
|
||||
{
|
||||
/**
|
||||
* The dependent context.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public $context = ['create', 'update'];
|
||||
|
||||
/**
|
||||
* The dependent attributes.
|
||||
*
|
||||
* @var array<int, string|\Laravel\Nova\Fields\Field>
|
||||
*/
|
||||
public $attributes = [];
|
||||
|
||||
/**
|
||||
* The dependent resolver.
|
||||
*
|
||||
* @var callable
|
||||
*
|
||||
* @phpstan-var TDependentResolver
|
||||
*/
|
||||
public $resolver;
|
||||
|
||||
/**
|
||||
* The dependent resolved FormData.
|
||||
*
|
||||
* @var \Laravel\Nova\Fields\FormData|null
|
||||
*/
|
||||
public $formData;
|
||||
|
||||
/**
|
||||
* Create a new dependent object.
|
||||
*
|
||||
* @param string|\Laravel\Nova\Fields\Field|array<int, string|\Laravel\Nova\Fields\Field> $attributes
|
||||
* @param callable $resolver
|
||||
* @param string|array<int, string>|null $context
|
||||
*
|
||||
* @phpstan-param TDependentResolver $resolver
|
||||
*/
|
||||
public function __construct($attributes, $resolver, $context = null)
|
||||
{
|
||||
$this->context = Arr::wrap($context ?? $this->context);
|
||||
|
||||
$this->resolver = $resolver;
|
||||
|
||||
$this->attributes = collect(Arr::wrap($attributes))->map(function ($item) {
|
||||
/** @var string|\Laravel\Nova\Fields\Field $item */
|
||||
if ($item instanceof MorphTo) {
|
||||
return [$item->attribute, "{$item->attribute}_type"];
|
||||
}
|
||||
|
||||
return $item instanceof Field ? $item->attribute : $item;
|
||||
})->flatten()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the dependencies for request.
|
||||
*
|
||||
* @param \Laravel\Nova\Fields\Field $field
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return $this
|
||||
*/
|
||||
public function handle(Field $field, NovaRequest $request)
|
||||
{
|
||||
/** @var TDependentResolver|null $resolver */
|
||||
$resolver = (
|
||||
($request->isCreateOrAttachRequest() && ! in_array('create', $this->context))
|
||||
|| ($request->isUpdateOrUpdateAttachedRequest() && ! in_array('update', $this->context))
|
||||
) ? null : $this->resolver;
|
||||
|
||||
$this->formData = FormData::onlyFrom($request, array_merge($this->attributes, [$field->attribute]));
|
||||
|
||||
if (is_string($resolver) && class_exists($resolver)) {
|
||||
$resolver = new $resolver();
|
||||
}
|
||||
|
||||
if (is_callable($resolver)) {
|
||||
call_user_func($resolver, $field, $request, $this->formData);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get depedent attributes.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getAttributes()
|
||||
{
|
||||
return collect($this->attributes)->mapWithKeys(function ($attribute) {
|
||||
return [$attribute => optional($this->formData)->get($attribute)];
|
||||
})->all();
|
||||
}
|
||||
}
|
||||
128
nova/src/Fields/DependentFields.php
Normal file
128
nova/src/Fields/DependentFields.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Support\UndefinedValue;
|
||||
|
||||
/**
|
||||
* @property array $fieldDependencies
|
||||
*/
|
||||
trait DependentFields
|
||||
{
|
||||
/**
|
||||
* Determine of should emit change event.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $dependentShouldEmitChangesEvent = false;
|
||||
|
||||
/**
|
||||
* Resolve the dependent component key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dependentComponentKey()
|
||||
{
|
||||
return sprintf('%s.%s.%s', Str::slug(class_basename(get_called_class())), $this->component, $this->attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve dependent field value.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function resolveDependentValue(NovaRequest $request)
|
||||
{
|
||||
return $this->value ?? $this->resolveDefaultValue($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync depends on logic.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return $this
|
||||
*/
|
||||
public function syncDependsOn(NovaRequest $request)
|
||||
{
|
||||
$this->value = new UndefinedValue();
|
||||
$this->defaultCallback = function () {
|
||||
return new UndefinedValue();
|
||||
};
|
||||
|
||||
$this->applyDependsOn($request);
|
||||
|
||||
$value = with($this->value, function ($value) use ($request) {
|
||||
if ($value instanceof UndefinedValue && $this->requestShouldResolveDefaultValue($request)) {
|
||||
$this->value = null;
|
||||
|
||||
return $this->resolveDefaultValue($request);
|
||||
}
|
||||
|
||||
return $value;
|
||||
});
|
||||
|
||||
$this->dependentShouldEmitChangesEvent = ! $value instanceof UndefinedValue;
|
||||
|
||||
if ($value instanceof UndefinedValue) {
|
||||
$this->value = null;
|
||||
} else {
|
||||
$this->value = ! is_null($value) ? $value : '';
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply depends on logic.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return $this
|
||||
*/
|
||||
public function applyDependsOn(NovaRequest $request)
|
||||
{
|
||||
$this->fieldDependencies = collect($this->fieldDependencies ?? [])
|
||||
->map(function (Dependent $dependent) use ($request) {
|
||||
return $dependent->handle($this, $request);
|
||||
})->all();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get depends on attributes.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
protected function getDependentsAttributes(NovaRequest $request)
|
||||
{
|
||||
/** @var \Illuminate\Support\Collection<string, mixed> $attributes */
|
||||
$attributes = collect($this->fieldDependencies ?? [])->map(function (Dependent $dependent) {
|
||||
return $dependent->getAttributes();
|
||||
})->collapse();
|
||||
|
||||
if ($attributes->isNotEmpty()) {
|
||||
return $attributes->all();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize dependent field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function serializeDependentField(NovaRequest $request): array
|
||||
{
|
||||
return [
|
||||
'dependentComponentKey' => $this->dependentComponentKey(),
|
||||
'dependsOn' => $this->getDependentsAttributes($request),
|
||||
'dependentShouldEmitChangesEvent' => $this->dependentShouldEmitChangesEvent,
|
||||
];
|
||||
}
|
||||
}
|
||||
41
nova/src/Fields/DetachesPivotModels.php
Normal file
41
nova/src/Fields/DetachesPivotModels.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Contracts\Deletable;
|
||||
use Laravel\Nova\DeleteField;
|
||||
use Laravel\Nova\Nova;
|
||||
|
||||
trait DetachesPivotModels
|
||||
{
|
||||
/**
|
||||
* Get the pivot record detachment callback for the field.
|
||||
*
|
||||
* @return \Closure(\Laravel\Nova\Http\Requests\NovaRequest, mixed):bool
|
||||
*/
|
||||
protected function detachmentCallback()
|
||||
{
|
||||
return function ($request, $model) {
|
||||
$pivotAccessor = $model->{$this->attribute}()->getPivotAccessor();
|
||||
|
||||
foreach ($model->{$this->attribute}()->withoutGlobalScopes()->cursor() as $related) {
|
||||
$resource = Nova::newResourceFromModel($related);
|
||||
|
||||
$pivot = $related->{$pivotAccessor};
|
||||
|
||||
$pivotFields = $resource->resolvePivotFields($request, $request->resource);
|
||||
|
||||
$pivotFields->whereInstanceOf(Deletable::class)
|
||||
->filter->isPrunable()
|
||||
->each(function ($field) use ($request, $pivot) {
|
||||
/** @var \Laravel\Nova\Fields\Field&\Laravel\Nova\Contracts\Deletable $field */
|
||||
DeleteField::forRequest($request, $field, $pivot)->save();
|
||||
});
|
||||
|
||||
$pivot->delete();
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
75
nova/src/Fields/DeterminesIfCreateRelationCanBeShown.php
Normal file
75
nova/src/Fields/DeterminesIfCreateRelationCanBeShown.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait DeterminesIfCreateRelationCanBeShown
|
||||
{
|
||||
/**
|
||||
* The callback used to determine if the create relation button should be shown.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool
|
||||
*/
|
||||
public $showCreateRelationButtonCallback;
|
||||
|
||||
/**
|
||||
* Indicates the size the create relation modal should be.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $modalSize = '2xl';
|
||||
|
||||
/**
|
||||
* Set the callback used to determine if the field is required.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function showCreateRelationButton($callback = true)
|
||||
{
|
||||
$this->showCreateRelationButtonCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the create relation button from forms.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function hideCreateRelationButton()
|
||||
{
|
||||
$this->showCreateRelationButtonCallback = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the size used for the create relation modal.
|
||||
*
|
||||
* @param string $size
|
||||
* @return $this
|
||||
*/
|
||||
public function modalSize($size)
|
||||
{
|
||||
return $this->withMeta(['modalSize' => $size]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if Nova should show the edit pivot relation button.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function createRelationShouldBeShown(NovaRequest $request)
|
||||
{
|
||||
return with($this->showCreateRelationButtonCallback, function ($callback) use ($request) {
|
||||
if ($callback === true || (is_callable($callback) && call_user_func($callback, $request))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
37
nova/src/Fields/EloquentFilterable.php
Normal file
37
nova/src/Fields/EloquentFilterable.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Fields\Filters\EloquentFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait EloquentFilterable
|
||||
{
|
||||
use Filterable;
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Fields\Filters\EloquentFilter|null
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
return new EloquentFilter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define filterable attribute.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function filterableAttribute(NovaRequest $request);
|
||||
|
||||
/**
|
||||
* Define the default filterable callback.
|
||||
*
|
||||
* @return callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):void
|
||||
*/
|
||||
abstract protected function defaultFilterableCallback();
|
||||
}
|
||||
70
nova/src/Fields/Email.php
Normal file
70
nova/src/Fields/Email.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Fields\Filters\TextFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class Email extends Text implements FilterableField
|
||||
{
|
||||
use FieldFilterable;
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'email-field';
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string|null $name
|
||||
* @param string|\Closure|callable|object|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name = 'Email', $attribute = 'email', callable $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
return tap(new TextFilter($this), function ($filter) {
|
||||
$filter->component = 'email-field';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return transform($this->jsonSerialize(), function ($field) {
|
||||
return Arr::only($field, [
|
||||
'uniqueKey',
|
||||
'name',
|
||||
'attribute',
|
||||
'type',
|
||||
'min',
|
||||
'max',
|
||||
'step',
|
||||
'pattern',
|
||||
'placeholder',
|
||||
'extraAttributes',
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
61
nova/src/Fields/Expandable.php
Normal file
61
nova/src/Fields/Expandable.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
trait Expandable
|
||||
{
|
||||
/**
|
||||
* The callback to be used to determine whether the field should be expanded.
|
||||
*
|
||||
* @var (callable():(bool))|null
|
||||
*/
|
||||
public $expandableCallback;
|
||||
|
||||
/**
|
||||
* Whether to always show the content for the field expanded or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $alwaysShow = false;
|
||||
|
||||
/**
|
||||
* Always show the content of textarea fields inside Nova.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function alwaysShow()
|
||||
{
|
||||
$this->alwaysShow = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the callback that should be used to determine whether the field should be collapsed.
|
||||
*
|
||||
* @param callable():bool $expandableCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function shouldShow(callable $expandableCallback)
|
||||
{
|
||||
$this->expandableCallback = $expandableCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the field should be expanded.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldBeExpanded()
|
||||
{
|
||||
if ($this->alwaysShow) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isset($this->expandableCallback)
|
||||
? call_user_func($this->expandableCallback)
|
||||
: false;
|
||||
}
|
||||
}
|
||||
916
nova/src/Fields/Field.php
Normal file
916
nova/src/Fields/Field.php
Normal file
@@ -0,0 +1,916 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Traits\Macroable;
|
||||
use Illuminate\Support\Traits\Tappable;
|
||||
use JsonSerializable;
|
||||
use Laravel\Nova\Contracts\Resolvable;
|
||||
use Laravel\Nova\Exceptions\NovaException;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Metrics\HasHelpText;
|
||||
use Laravel\Nova\Util;
|
||||
|
||||
/**
|
||||
* @phpstan-type TFieldValidationRules \Stringable|string|\Illuminate\Contracts\Validation\ValidationRule|\Illuminate\Contracts\Validation\Rule|\Illuminate\Contracts\Validation\InvokableRule|callable
|
||||
* @phpstan-type TValidationRules array<int, TFieldValidationRules>|\Stringable|string|(callable(string, mixed, \Closure):(void))
|
||||
*
|
||||
* @method static static make(mixed $name, string|\Closure|callable|object|null $attribute = null, callable|null $resolveCallback = null)
|
||||
*/
|
||||
#[\AllowDynamicProperties]
|
||||
abstract class Field extends FieldElement implements JsonSerializable, Resolvable
|
||||
{
|
||||
use DependentFields;
|
||||
use HandlesValidation;
|
||||
use HasHelpText;
|
||||
use Macroable;
|
||||
use PeekableFields;
|
||||
use PreviewableFields;
|
||||
use SupportsFullWidthFields;
|
||||
use Tappable;
|
||||
|
||||
const LEFT_ALIGN = 'left';
|
||||
|
||||
const CENTER_ALIGN = 'center';
|
||||
|
||||
const RIGHT_ALIGN = 'right';
|
||||
|
||||
/**
|
||||
* The displayable name of the field.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* The attribute / column name of the field.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $attribute;
|
||||
|
||||
/**
|
||||
* The field's resolved value.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* The value displayed to the user.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $displayedAs;
|
||||
|
||||
/**
|
||||
* The callback to be used to resolve the field's display value.
|
||||
*
|
||||
* @var (callable(mixed, mixed, string):(mixed))|null
|
||||
*/
|
||||
public $displayCallback;
|
||||
|
||||
/**
|
||||
* Indicates whether the display value has been customized by the user.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $usesCustomizedDisplay = false;
|
||||
|
||||
/**
|
||||
* The callback to be used to resolve the field's value.
|
||||
*
|
||||
* @var (callable(mixed, mixed, ?string):(mixed))|null
|
||||
*/
|
||||
public $resolveCallback;
|
||||
|
||||
/**
|
||||
* The callback to be used to hydrate the model attribute.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent, string, string):(mixed))|null
|
||||
*/
|
||||
public $fillCallback;
|
||||
|
||||
/**
|
||||
* The callback to be used for computed field.
|
||||
*
|
||||
* @var (\Closure(mixed):(mixed))|(callable(mixed):(mixed))|null
|
||||
*/
|
||||
protected $computedCallback;
|
||||
|
||||
/**
|
||||
* The callback to be used for the field's default value.
|
||||
*
|
||||
* @var (\Closure(\Laravel\Nova\Http\Requests\NovaRequest):(mixed))|null
|
||||
*/
|
||||
protected $defaultCallback;
|
||||
|
||||
/**
|
||||
* Indicates if the field should be sortable.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $sortable = false;
|
||||
|
||||
/**
|
||||
* Indicates if the field is nullable.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $nullable = false;
|
||||
|
||||
/**
|
||||
* Values which will be replaced to null.
|
||||
*
|
||||
* @var array<int, mixed>
|
||||
*/
|
||||
public $nullValues = [''];
|
||||
|
||||
/**
|
||||
* Indicates if the field was resolved as a pivot field.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $pivot = false;
|
||||
|
||||
/**
|
||||
* The accessor that should be used to refer as a pivot field.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $pivotAccessor;
|
||||
|
||||
/**
|
||||
* The text alignment for the field's text in tables.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $textAlign = 'left';
|
||||
|
||||
/**
|
||||
* Indicates if the field should allow its whitespace to be wrapped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $wrapping = false;
|
||||
|
||||
/**
|
||||
* Indicates if the field label and form element should sit on top of each other.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $stacked = false;
|
||||
|
||||
/**
|
||||
* The custom components registered for fields.
|
||||
*
|
||||
* @var array<class-string<\Laravel\Nova\Fields\Field>, string>
|
||||
*/
|
||||
public static $customComponents = [];
|
||||
|
||||
/**
|
||||
* The callback used to determine if the field is readonly.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool|null
|
||||
*/
|
||||
public $readonlyCallback;
|
||||
|
||||
/**
|
||||
* The callback used to determine if the field is required.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool|null
|
||||
*/
|
||||
public $requiredCallback;
|
||||
|
||||
/**
|
||||
* The resource associated with the field.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $resource;
|
||||
|
||||
/**
|
||||
* Indicates whether the field is visible.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $visible = true;
|
||||
|
||||
/**
|
||||
* The placeholder for the field.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $placeholder;
|
||||
|
||||
/**
|
||||
* Indicated whether the field should show its label.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $withLabel = true;
|
||||
|
||||
/**
|
||||
* Indicated whether the field should display as though it is inline.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $inline = false;
|
||||
|
||||
/**
|
||||
* Indicated whether the field should display as though it is compact.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $compact = false;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|\Closure|callable|object|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, callable $resolveCallback = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->resolveCallback = $resolveCallback;
|
||||
|
||||
$this->default(null);
|
||||
|
||||
if ($attribute instanceof Closure || (is_callable($attribute) && is_object($attribute))) {
|
||||
$this->computedCallback = $attribute;
|
||||
$this->attribute = 'ComputedField';
|
||||
} else {
|
||||
$this->attribute = $attribute ?? str_replace(' ', '_', Str::lower($name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for the field.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function setValue($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stack the label above the field.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function stacked()
|
||||
{
|
||||
$this->stacked = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value for display.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function resolveForDisplay($resource, $attribute = null)
|
||||
{
|
||||
$this->resource = $resource;
|
||||
|
||||
$attribute = $attribute ?? $this->attribute;
|
||||
|
||||
if (! $this->displayCallback) {
|
||||
$this->resolve($resource, $attribute);
|
||||
} elseif (is_callable($this->displayCallback)) {
|
||||
if ($attribute === 'ComputedField') {
|
||||
$this->value = call_user_func($this->computedCallback, $resource);
|
||||
}
|
||||
|
||||
tap($this->value ?? $this->resolveAttribute($resource, $attribute), function ($value) use (
|
||||
$resource,
|
||||
$attribute
|
||||
) {
|
||||
$this->value = $value;
|
||||
$this->resolveUsingDisplayCallback($value, $resource, $attribute);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value using the display callback.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param mixed $resource
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
protected function resolveUsingDisplayCallback($value, $resource, $attribute)
|
||||
{
|
||||
$this->usesCustomizedDisplay = true;
|
||||
$this->displayedAs = call_user_func($this->displayCallback, $value, $resource, $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function resolve($resource, $attribute = null)
|
||||
{
|
||||
$this->resource = $resource;
|
||||
|
||||
$attribute = $attribute ?? $this->attribute;
|
||||
|
||||
if ($attribute === 'ComputedField') {
|
||||
$this->value = call_user_func($this->computedCallback, $resource);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->resolveCallback) {
|
||||
$this->value = $this->resolveAttribute($resource, $attribute);
|
||||
} elseif (is_callable($this->resolveCallback)) {
|
||||
tap($this->resolveAttribute($resource, $attribute), function ($value) use ($resource, $attribute) {
|
||||
$this->value = call_user_func($this->resolveCallback, $value, $resource, $attribute);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the default value for an Action field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return void
|
||||
*/
|
||||
public function resolveForAction($request)
|
||||
{
|
||||
if (! is_null($this->value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->defaultCallback instanceof Closure) {
|
||||
$this->defaultCallback = call_user_func($this->defaultCallback, $request);
|
||||
}
|
||||
|
||||
$this->value = $this->defaultCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the given attribute from the given resource.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string $attribute
|
||||
* @return mixed
|
||||
*/
|
||||
protected function resolveAttribute($resource, $attribute)
|
||||
{
|
||||
return Util::value(data_get($resource, str_replace('->', '.', $attribute)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the callback that should be used to display the field's value.
|
||||
*
|
||||
* @param callable(mixed, mixed, string):mixed $displayCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function displayUsing(callable $displayCallback)
|
||||
{
|
||||
$this->displayCallback = $displayCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the callback that should be used to resolve the field's value.
|
||||
*
|
||||
* @param callable(mixed, mixed, ?string):mixed $resolveCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function resolveUsing(callable $resolveCallback)
|
||||
{
|
||||
$this->resolveCallback = $resolveCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @return mixed
|
||||
*/
|
||||
public function fill(NovaRequest $request, $model)
|
||||
{
|
||||
return $this->fillInto($request, $model, $this->attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @return mixed
|
||||
*/
|
||||
public function fillForAction(NovaRequest $request, $model)
|
||||
{
|
||||
return $this->fill($request, $model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @param string|null $requestAttribute
|
||||
* @return mixed
|
||||
*/
|
||||
public function fillInto(NovaRequest $request, $model, $attribute, $requestAttribute = null)
|
||||
{
|
||||
return $this->fillAttribute($request, $requestAttribute ?? $this->attribute, $model, $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fillAttribute(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
if (isset($this->fillCallback)) {
|
||||
return call_user_func($this->fillCallback, $request, $model, $attribute, $requestAttribute);
|
||||
}
|
||||
|
||||
return $this->fillAttributeFromRequest($request, $requestAttribute, $model, $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $requestAttribute
|
||||
* @param object $model
|
||||
* @param string $attribute
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
if ($request->exists($requestAttribute)) {
|
||||
tap($request->input($requestAttribute), function ($value) use ($model, $attribute) {
|
||||
$value = $this->isValidNullValue($value) ? null : $value;
|
||||
|
||||
$this->fillModelWithData($model, $value, $attribute);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the model's attribute with data.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function fillModelWithData($model, $value, string $attribute)
|
||||
{
|
||||
$attributes = [Str::replace('.', '->', $attribute) => $value];
|
||||
|
||||
$model->forceFill($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field supports null values.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isNullable()
|
||||
{
|
||||
return $this->nullable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Field
|
||||
*/
|
||||
public function compact(bool $compact = true)
|
||||
{
|
||||
$this->compact = $compact;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given value is considered a valid null value
|
||||
* if the field supports them.
|
||||
*
|
||||
* @deprecated Use "isValidNullValue"
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
protected function isNullValue($value)
|
||||
{
|
||||
return $this->isValidNullValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given value is considered a valid null value
|
||||
* if the field supports them.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidNullValue($value)
|
||||
{
|
||||
if (! $this->isNullable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->valueIsConsideredNull($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given value is considered null.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
protected function valueIsConsideredNull($value)
|
||||
{
|
||||
return is_callable($this->nullValues) ? ($this->nullValues)($value) : in_array(
|
||||
$value,
|
||||
(array) $this->nullValues
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a callback that should be used to hydrate the model attribute for the field.
|
||||
*
|
||||
* @param callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent, string, string):mixed $fillCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function fillUsing($fillCallback)
|
||||
{
|
||||
$this->fillCallback = $fillCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that this field should be sortable.
|
||||
*
|
||||
* @param bool $value
|
||||
* @return $this
|
||||
*/
|
||||
public function sortable($value = true)
|
||||
{
|
||||
if (! $this->computed()) {
|
||||
$this->sortable = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the sortable uri key for the field.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sortableUriKey()
|
||||
{
|
||||
return $this->attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the field should be nullable.
|
||||
*
|
||||
* @param bool $nullable
|
||||
* @param array<int, mixed>|\Closure $values
|
||||
* @return $this
|
||||
*/
|
||||
public function nullable($nullable = true, $values = null)
|
||||
{
|
||||
$this->nullable = $nullable;
|
||||
|
||||
if ($values !== null) {
|
||||
$this->nullValues($values);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify nullable values.
|
||||
*
|
||||
* @param array<int, mixed>|\Closure $values
|
||||
* @return $this
|
||||
*/
|
||||
public function nullValues($values)
|
||||
{
|
||||
$this->nullValues = $values;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field is computed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function computed()
|
||||
{
|
||||
return (is_callable($this->attribute) && ! is_string($this->attribute)) || $this->attribute == 'ComputedField';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component name for the field.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function component()
|
||||
{
|
||||
if (isset(static::$customComponents[get_class($this)])) {
|
||||
return static::$customComponents[get_class($this)];
|
||||
}
|
||||
|
||||
return $this->component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the component that should be used by the field.
|
||||
*
|
||||
* @param string $component
|
||||
* @return void
|
||||
*/
|
||||
public static function useComponent($component)
|
||||
{
|
||||
static::$customComponents[get_called_class()] = $component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback used to determine if the field is readonly.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool|null $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function readonly($callback = true)
|
||||
{
|
||||
$this->readonlyCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field is readonly.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isReadonly(NovaRequest $request)
|
||||
{
|
||||
return with($this->readonlyCallback, function ($callback) use ($request) {
|
||||
if ($callback === true || (is_callable($callback) && call_user_func($callback, $request))) {
|
||||
$this->setReadonlyAttribute();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the field to a readonly field.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function setReadonlyAttribute()
|
||||
{
|
||||
$this->withMeta(['extraAttributes' => ['readonly' => true]]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the text alignment of the field.
|
||||
*
|
||||
* @param string $alignment
|
||||
* @return $this
|
||||
*/
|
||||
public function textAlign($alignment)
|
||||
{
|
||||
$this->textAlign = $alignment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback used to determine if the field is required.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool|null $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function required($callback = true)
|
||||
{
|
||||
$this->requiredCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field is required.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isRequired(NovaRequest $request)
|
||||
{
|
||||
return with($this->requiredCallback, function ($callback) use ($request) {
|
||||
if ($callback === true || (is_callable($callback) && call_user_func($callback, $request))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! empty($this->attribute) && is_null($callback)) {
|
||||
if ($request->isResourceIndexRequest() || $request->isLensRequest() || $request->isActionRequest()) {
|
||||
return in_array('required', $this->getCreationRules($request)[$this->attribute]);
|
||||
}
|
||||
|
||||
if ($request->isCreateOrAttachRequest()) {
|
||||
return in_array('required', $this->getCreationRules($request)[$this->attribute]);
|
||||
}
|
||||
|
||||
if ($request->isUpdateOrUpdateAttachedRequest()) {
|
||||
return in_array('required', $this->getUpdateRules($request)[$this->attribute]);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the width for the help text tooltip.
|
||||
*
|
||||
* @param string $helpWidth
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function helpWidth($helpWidth)
|
||||
{
|
||||
throw NovaException::helperNotSupported(__METHOD__, __CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the width of the help text tooltip.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getHelpWidth()
|
||||
{
|
||||
throw NovaException::helperNotSupported(__METHOD__, __CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback to be used for determining the field's default value.
|
||||
*
|
||||
* @param (\Closure(\Laravel\Nova\Http\Requests\NovaRequest):(mixed))|mixed $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function default($callback)
|
||||
{
|
||||
$this->defaultCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the default value for the field.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function resolveDefaultValue(NovaRequest $request)
|
||||
{
|
||||
if ($this->requestShouldResolveDefaultValue($request)) {
|
||||
return $this->resolveDefaultCallback($request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the default callback for the field.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function resolveDefaultCallback(NovaRequest $request)
|
||||
{
|
||||
if (is_null($this->value) && $this->defaultCallback instanceof Closure) {
|
||||
return call_user_func($this->defaultCallback, $request);
|
||||
}
|
||||
|
||||
return $this->defaultCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if request should resolve default value.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function requestShouldResolveDefaultValue(NovaRequest $request)
|
||||
{
|
||||
return $request->isCreateOrAttachRequest() || $request->isActionRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the placeholder text for the field if supported.
|
||||
*
|
||||
* @param string|null $text
|
||||
* @return $this
|
||||
*/
|
||||
public function placeholder($text)
|
||||
{
|
||||
$this->placeholder = $text;
|
||||
|
||||
$this->withMeta(['extraAttributes' => ['placeholder' => $text]]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the field to be visible on the form.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function show()
|
||||
{
|
||||
$this->visible = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the field to be hidden on the form.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function hide()
|
||||
{
|
||||
$this->visible = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
/** @phpstan-ignore-next-line */
|
||||
return with(app(NovaRequest::class), function ($request) {
|
||||
$value = $this->isValidNullValue($this->value) ? null : $this->value;
|
||||
|
||||
return array_merge([
|
||||
'attribute' => $this->attribute,
|
||||
'component' => $this->component(),
|
||||
'compact' => $this->compact,
|
||||
'displayedAs' => $this->displayedAs,
|
||||
'fullWidth' => $this->fullWidth,
|
||||
'helpText' => $this->getHelpText(),
|
||||
'indexName' => $this->name,
|
||||
'inline' => $this->inline,
|
||||
'name' => $this->name,
|
||||
'nullable' => $this->nullable,
|
||||
'panel' => $this->panel,
|
||||
'placeholder' => $this->placeholder,
|
||||
'prefixComponent' => true,
|
||||
'readonly' => $this->isReadonly($request),
|
||||
'required' => $this->isRequired($request),
|
||||
'sortable' => $this->sortable,
|
||||
'sortableUriKey' => $this->sortableUriKey(),
|
||||
'stacked' => $this->stacked,
|
||||
'textAlign' => $this->textAlign,
|
||||
'uniqueKey' => sprintf(
|
||||
'%s-%s-%s',
|
||||
$this->attribute,
|
||||
Str::slug($this->panel ?? 'default'),
|
||||
$this->component()
|
||||
),
|
||||
'usesCustomizedDisplay' => $this->usesCustomizedDisplay,
|
||||
'validationKey' => $this->validationKey(),
|
||||
'value' => $value ?? $this->resolveDefaultValue($request),
|
||||
'visible' => $this->visible,
|
||||
'withLabel' => $this->withLabel,
|
||||
'wrapping' => $this->wrapping,
|
||||
], $this->serializeDependentField($request), $this->meta());
|
||||
});
|
||||
}
|
||||
}
|
||||
344
nova/src/Fields/FieldCollection.php
Normal file
344
nova/src/Fields/FieldCollection.php
Normal file
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\MissingValue;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\LazyCollection;
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Contracts\ListableField;
|
||||
use Laravel\Nova\Contracts\RelatableField;
|
||||
use Laravel\Nova\Contracts\Resolvable;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Panel;
|
||||
use Laravel\Nova\ResourceTool;
|
||||
use Laravel\Nova\ResourceToolElement;
|
||||
use Laravel\Nova\Util;
|
||||
|
||||
/**
|
||||
* @template TKey of int
|
||||
* @template TValue of \Laravel\Nova\Panel|\Laravel\Nova\ResourceToolElement|\Laravel\Nova\Fields\Field|\Illuminate\Http\Resources\MissingValue
|
||||
*
|
||||
* @extends \Illuminate\Support\Collection<TKey, TValue>
|
||||
*/
|
||||
class FieldCollection extends Collection
|
||||
{
|
||||
/**
|
||||
* Assign the fields with the given panels to their parent panel.
|
||||
*
|
||||
* @param string $label
|
||||
* @return static<TKey, TValue>
|
||||
*/
|
||||
public function assignDefaultPanel($label)
|
||||
{
|
||||
new Panel($label, $this->reject(function ($field) {
|
||||
return isset($field->panel);
|
||||
}));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten stacked fields.
|
||||
*
|
||||
* @return static<int, TValue>
|
||||
*/
|
||||
public function flattenStackedFields()
|
||||
{
|
||||
return $this->map(function ($field) {
|
||||
if ($field instanceof Stack) {
|
||||
return $field->fields()->all();
|
||||
}
|
||||
|
||||
return $field;
|
||||
})->flatten();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a given field by its attribute.
|
||||
*
|
||||
* @template TGetDefault
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param TGetDefault|\Closure():TGetDefault $default
|
||||
* @return TValue|TGetDefault
|
||||
*/
|
||||
public function findFieldByAttribute($attribute, $default = null)
|
||||
{
|
||||
return $this->first(function ($field) use ($attribute) {
|
||||
return isset($field->attribute) &&
|
||||
$field->attribute == $attribute;
|
||||
}, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter elements should be displayed for the given request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return static<int, TValue>
|
||||
*/
|
||||
public function authorized(Request $request)
|
||||
{
|
||||
return $this->filter(function ($field) use ($request) {
|
||||
return $field->authorize($request);
|
||||
})->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter elements should be displayed for the given request.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @return static<int, TValue>
|
||||
*/
|
||||
public function resolve($resource)
|
||||
{
|
||||
return $this->each(function ($field) use ($resource) {
|
||||
if ($field instanceof Resolvable) {
|
||||
$field->resolve($resource);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve value of fields for display.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @return static<int, TValue>
|
||||
*/
|
||||
public function resolveForDisplay($resource)
|
||||
{
|
||||
return $this->each(function ($field) use ($resource) {
|
||||
if ($field instanceof ListableField || ! $field instanceof Resolvable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($field->pivot) {
|
||||
$field->resolveForDisplay($resource->{$field->pivotAccessor} ?? new Pivot);
|
||||
} else {
|
||||
$field->resolveForDisplay($resource);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove non-creation fields from the collection.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return static<int, \Laravel\Nova\Fields\Field>
|
||||
*/
|
||||
public function onlyCreateFields(NovaRequest $request, $resource)
|
||||
{
|
||||
return $this->reject(function ($field) use ($resource, $request) {
|
||||
return $field instanceof ListableField ||
|
||||
($field instanceof ResourceTool || $field instanceof ResourceToolElement) ||
|
||||
$field->attribute === 'ComputedField' ||
|
||||
($field instanceof ID && $field->attribute === $resource->getKeyName()) ||
|
||||
! $field->isShownOnCreation($request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove non-update fields from the collection.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return static<int, \Laravel\Nova\Fields\Field>
|
||||
*/
|
||||
public function onlyUpdateFields(NovaRequest $request, $resource)
|
||||
{
|
||||
return $this->reject(function ($field) use ($resource, $request) {
|
||||
return $field instanceof ListableField ||
|
||||
($field instanceof ResourceTool || $field instanceof ResourceToolElement) ||
|
||||
$field->attribute === 'ComputedField' ||
|
||||
($field instanceof ID && $field->attribute === $resource->getKeyName()) ||
|
||||
! $field->isShownOnUpdate($request, $resource);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter fields for showing on detail.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return static<int, \Laravel\Nova\Fields\Field>
|
||||
*/
|
||||
public function filterForDetail(NovaRequest $request, $resource)
|
||||
{
|
||||
return $this->filter(function ($field) use ($resource, $request) {
|
||||
return $field->isShownOnDetail($request, $resource);
|
||||
})->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter fields for showing on preview.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return static<int, \Laravel\Nova\Fields\Field>
|
||||
*/
|
||||
public function filterForPreview(NovaRequest $request, $resource)
|
||||
{
|
||||
return $this->filter(function (Field $field) use ($resource, $request) {
|
||||
return $field->isShownOnPreview($request, $resource);
|
||||
})->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter fields for showing when peeking.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return static<int, \Laravel\Nova\Fields\Field>
|
||||
*/
|
||||
public function filterForPeeking(NovaRequest $request)
|
||||
{
|
||||
return $this
|
||||
->filter(function (Field $field) use ($request) {
|
||||
return $field->isShownWhenPeeking($request);
|
||||
})->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter fields for showing on index.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return static<int, \Laravel\Nova\Fields\Field>
|
||||
*/
|
||||
public function filterForIndex(NovaRequest $request, $resource)
|
||||
{
|
||||
return $this->filter(function ($field) use ($resource, $request) {
|
||||
return $field->isShownOnIndex($request, $resource);
|
||||
})->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject if the field is readonly.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return static<int, TValue>
|
||||
*/
|
||||
public function withoutReadonly(NovaRequest $request)
|
||||
{
|
||||
return $this->reject(function ($field) use ($request) {
|
||||
return $field->isReadonly($request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject if the field is a missing value.
|
||||
*
|
||||
* @return static<int, \Laravel\Nova\Panel|\Laravel\Nova\ResourceToolElement|\Laravel\Nova\Fields\Field>
|
||||
*/
|
||||
public function withoutMissingValues()
|
||||
{
|
||||
return $this->reject(function ($field) {
|
||||
return $field instanceof MissingValue;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject fields which use their own index listings.
|
||||
*
|
||||
* @return static<int, TValue>
|
||||
*/
|
||||
public function withoutListableFields()
|
||||
{
|
||||
return $this->reject(function ($field) {
|
||||
return $field instanceof ListableField;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject if the field is unfillable.
|
||||
*
|
||||
* @return static<int, TValue>
|
||||
*/
|
||||
public function withoutUnfillable()
|
||||
{
|
||||
return $this->reject(function ($field) {
|
||||
return $field instanceof Unfillable;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject fields which are actually ResourceTools.
|
||||
*
|
||||
* @return static<int, TValue>
|
||||
*/
|
||||
public function withoutResourceTools()
|
||||
{
|
||||
return $this->reject(function ($field) {
|
||||
return $field instanceof ResourceToolElement;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the fields to only many-to-many relationships.
|
||||
*
|
||||
* @return static<TKey, \Laravel\Nova\Fields\MorphToMany|\Laravel\Nova\Fields\BelongsToMany>
|
||||
*/
|
||||
public function filterForManyToManyRelations()
|
||||
{
|
||||
return $this->filter(function ($field) {
|
||||
return $field instanceof BelongsToMany || $field instanceof MorphToMany;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject if the field supports Filterable Field.
|
||||
*
|
||||
* @return static<TKey, \Laravel\Nova\Fields\Field&\Laravel\Nova\Contracts\FilterableField>
|
||||
*/
|
||||
public function withOnlyFilterableFields()
|
||||
{
|
||||
return $this->whereInstanceOf(Field::class)
|
||||
->whereInstanceOf(FilterableField::class)
|
||||
->filter(function ($field) {
|
||||
/** @var \Laravel\Nova\Fields\Field&\Laravel\Nova\Contracts\FilterableField $field */
|
||||
return $field->attribute !== 'ComputedField' && ! is_null($field->filterableCallback);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply depends on for the request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return $this
|
||||
*/
|
||||
public function applyDependsOn(NovaRequest $request)
|
||||
{
|
||||
$this->each->applyDependsOn($request);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply depends on for the request with default values.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return $this
|
||||
*/
|
||||
public function applyDependsOnWithDefaultValues(NovaRequest $request)
|
||||
{
|
||||
$payloads = new LazyCollection(function () use ($request) {
|
||||
foreach ($this->items as $field) {
|
||||
$key = $field instanceof RelatableField ? $field->relationshipName() : $field->attribute;
|
||||
|
||||
if ($field instanceof MorphTo) {
|
||||
yield "{$key}_type" => $field->morphToType;
|
||||
}
|
||||
|
||||
yield $key => Util::hydrate($field->resolveDependentValue($request));
|
||||
}
|
||||
});
|
||||
|
||||
$this->each->applyDependsOn(
|
||||
NovaRequest::createFrom($request)->mergeIfMissing($payloads->all())
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
304
nova/src/Fields/FieldElement.php
Normal file
304
nova/src/Fields/FieldElement.php
Normal file
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Element;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
abstract class FieldElement extends Element
|
||||
{
|
||||
/**
|
||||
* The field's assigned panel.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $panel;
|
||||
|
||||
/**
|
||||
* The field's assigned panel.
|
||||
*
|
||||
* @var \Laravel\Nova\Panel|null
|
||||
*/
|
||||
public $assignedPanel;
|
||||
|
||||
/**
|
||||
* Indicates if the element should be shown on the index view.
|
||||
*
|
||||
* @var (callable():(bool))|bool
|
||||
*/
|
||||
public $showOnIndex = true;
|
||||
|
||||
/**
|
||||
* Indicates if the element should be shown on the detail view.
|
||||
*
|
||||
* @var (callable():(bool))|bool
|
||||
*/
|
||||
public $showOnDetail = true;
|
||||
|
||||
/**
|
||||
* Indicates if the element should be shown on the creation view.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool
|
||||
*/
|
||||
public $showOnCreation = true;
|
||||
|
||||
/**
|
||||
* Indicates if the element should be shown on the update view.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest, mixed):(bool))|bool
|
||||
*/
|
||||
public $showOnUpdate = true;
|
||||
|
||||
/**
|
||||
* Specify that the element should be hidden from the index view.
|
||||
*
|
||||
* @param (callable():(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function hideFromIndex($callback = true)
|
||||
{
|
||||
$this->showOnIndex = is_callable($callback) ? function () use ($callback) {
|
||||
return ! call_user_func_array($callback, func_get_args());
|
||||
}
|
||||
: ! $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should be hidden from the detail view.
|
||||
*
|
||||
* @param (callable():(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function hideFromDetail($callback = true)
|
||||
{
|
||||
$this->showOnDetail = is_callable($callback) ? function () use ($callback) {
|
||||
return ! call_user_func_array($callback, func_get_args());
|
||||
}
|
||||
: ! $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should be hidden from the creation view.
|
||||
*
|
||||
* @param (callable():(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function hideWhenCreating($callback = true)
|
||||
{
|
||||
$this->showOnCreation = is_callable($callback) ? function () use ($callback) {
|
||||
return ! call_user_func_array($callback, func_get_args());
|
||||
}
|
||||
: ! $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should be hidden from the update view.
|
||||
*
|
||||
* @param (callable():(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function hideWhenUpdating($callback = true)
|
||||
{
|
||||
$this->showOnUpdate = is_callable($callback) ? function () use ($callback) {
|
||||
return ! call_user_func_array($callback, func_get_args());
|
||||
}
|
||||
: ! $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should be visible on the index view.
|
||||
*
|
||||
* @param (callable():(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function showOnIndex($callback = true)
|
||||
{
|
||||
$this->showOnIndex = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should be hidden from the detail view.
|
||||
*
|
||||
* @param (callable():(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function showOnDetail($callback = true)
|
||||
{
|
||||
$this->showOnDetail = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should be hidden from the creation view.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function showOnCreating($callback = true)
|
||||
{
|
||||
$this->showOnCreation = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should be hidden from the update view.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest, mixed):(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function showOnUpdating($callback = true)
|
||||
{
|
||||
$this->showOnUpdate = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for showing when updating.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return bool
|
||||
*/
|
||||
public function isShownOnUpdate(NovaRequest $request, $resource): bool
|
||||
{
|
||||
if (is_callable($this->showOnUpdate)) {
|
||||
$this->showOnUpdate = call_user_func($this->showOnUpdate, $request, $resource);
|
||||
}
|
||||
|
||||
return $this->showOnUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check showing on index.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return bool
|
||||
*/
|
||||
public function isShownOnIndex(NovaRequest $request, $resource): bool
|
||||
{
|
||||
if (is_callable($this->showOnIndex)) {
|
||||
$this->showOnIndex = call_user_func($this->showOnIndex, $request, $resource);
|
||||
}
|
||||
|
||||
return $this->showOnIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field is to be shown on the detail view.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return bool
|
||||
*/
|
||||
public function isShownOnDetail(NovaRequest $request, $resource): bool
|
||||
{
|
||||
if (is_callable($this->showOnDetail)) {
|
||||
$this->showOnDetail = call_user_func($this->showOnDetail, $request, $resource);
|
||||
}
|
||||
|
||||
return $this->showOnDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for showing when creating.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function isShownOnCreation(NovaRequest $request): bool
|
||||
{
|
||||
if (is_callable($this->showOnCreation)) {
|
||||
$this->showOnCreation = call_user_func($this->showOnCreation, $request);
|
||||
}
|
||||
|
||||
return $this->showOnCreation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should only be shown on the index view.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onlyOnIndex()
|
||||
{
|
||||
$this->showOnIndex = true;
|
||||
$this->showOnDetail = false;
|
||||
$this->showOnCreation = false;
|
||||
$this->showOnUpdate = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should only be shown on the detail view.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onlyOnDetail()
|
||||
{
|
||||
parent::onlyOnDetail();
|
||||
|
||||
$this->showOnIndex = false;
|
||||
$this->showOnDetail = true;
|
||||
$this->showOnCreation = false;
|
||||
$this->showOnUpdate = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should only be shown on forms.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onlyOnForms()
|
||||
{
|
||||
$this->showOnIndex = false;
|
||||
$this->showOnDetail = false;
|
||||
$this->showOnCreation = true;
|
||||
$this->showOnUpdate = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should be hidden from forms.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function exceptOnForms()
|
||||
{
|
||||
$this->showOnIndex = true;
|
||||
$this->showOnDetail = true;
|
||||
$this->showOnCreation = false;
|
||||
$this->showOnUpdate = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field element for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'panel' => $this->panel,
|
||||
]);
|
||||
}
|
||||
}
|
||||
43
nova/src/Fields/FieldFilterable.php
Normal file
43
nova/src/Fields/FieldFilterable.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait FieldFilterable
|
||||
{
|
||||
use Filterable;
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return $this->jsonSerialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default filterable callback.
|
||||
*
|
||||
* @return callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):\Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function defaultFilterableCallback()
|
||||
{
|
||||
return function (NovaRequest $request, $query, $value, $attribute) {
|
||||
return $query->where($attribute, '=', $value);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Define filterable attribute.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return string
|
||||
*/
|
||||
protected function filterableAttribute(NovaRequest $request)
|
||||
{
|
||||
return $this->attribute;
|
||||
}
|
||||
}
|
||||
381
nova/src/Fields/File.php
Normal file
381
nova/src/Fields/File.php
Normal file
@@ -0,0 +1,381 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Contracts\Deletable as DeletableContract;
|
||||
use Laravel\Nova\Contracts\Downloadable as DownloadableContract;
|
||||
use Laravel\Nova\Contracts\Storable as StorableContract;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
/**
|
||||
* @method static static make(mixed $name, string|callable|null $attribute = null, string|null $disk = null, callable|null $storageCallback = null)
|
||||
*/
|
||||
class File extends Field implements DeletableContract, DownloadableContract, StorableContract
|
||||
{
|
||||
use AcceptsTypes;
|
||||
use Deletable;
|
||||
use HasDownload;
|
||||
use HasPreview;
|
||||
use HasThumbnail;
|
||||
use Storable;
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'file-field';
|
||||
|
||||
/**
|
||||
* The callback that should be executed to store the file.
|
||||
*
|
||||
* @var callable(\Laravel\Nova\Http\Requests\NovaRequest, object, string, string, ?string, ?string):mixed
|
||||
*/
|
||||
public $storageCallback;
|
||||
|
||||
/**
|
||||
* The callback that should be used to determine the file's storage name.
|
||||
*
|
||||
* @var (callable(\Illuminate\Http\Request):(string))|null
|
||||
*/
|
||||
public $storeAsCallback;
|
||||
|
||||
/**
|
||||
* The column where the file's original name should be stored.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $originalNameColumn;
|
||||
|
||||
/**
|
||||
* The column where the file's size should be stored.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $sizeColumn;
|
||||
|
||||
/**
|
||||
* The text alignment for the field's text in tables.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $textAlign = 'center';
|
||||
|
||||
/**
|
||||
* Indicates if the element should be shown on the index view.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $showOnIndex = false;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|callable|null $attribute
|
||||
* @param string|null $disk
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest, object, string, string, ?string, ?string):(mixed))|null $storageCallback
|
||||
* @return void
|
||||
*
|
||||
* @phpstan-param callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Model|\Illuminate\Support\Fluent, string, string, ?string, ?string):mixed $storageCallback
|
||||
*/
|
||||
public function __construct($name, $attribute = null, $disk = null, $storageCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute);
|
||||
|
||||
$this->disk($disk);
|
||||
|
||||
$this
|
||||
->store(
|
||||
$storageCallback ?? function ($request, $model, $attribute, $requestAttribute) {
|
||||
return $this->mergeExtraStorageColumns($request, $requestAttribute, [
|
||||
$this->attribute => $this->storeFile($request, $requestAttribute),
|
||||
]);
|
||||
}
|
||||
)
|
||||
->thumbnail(function () {
|
||||
return null;
|
||||
})->preview(function () {
|
||||
return null;
|
||||
})->download(function ($request, $model) {
|
||||
$name = $this->originalNameColumn ? $model->{$this->originalNameColumn} : null;
|
||||
|
||||
return Storage::disk($this->getStorageDisk())->download($this->value, $name);
|
||||
})->delete(function () {
|
||||
if ($this->value) {
|
||||
Storage::disk($this->getStorageDisk())->delete($this->value);
|
||||
|
||||
return $this->columnsThatShouldBeDeleted();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the file on disk.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $requestAttribute
|
||||
* @return string
|
||||
*/
|
||||
protected function storeFile($request, $requestAttribute)
|
||||
{
|
||||
$file = $this->retrieveFileFromRequest($request, $requestAttribute);
|
||||
|
||||
if (! $this->storeAsCallback) {
|
||||
return $file->store($this->getStorageDir(), $this->getStorageDisk());
|
||||
}
|
||||
|
||||
return $file->storeAs(
|
||||
$this->getStorageDir(), call_user_func($this->storeAsCallback, $request), $this->getStorageDisk()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the specified extra file information columns into the storable attributes.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $requestAttribute
|
||||
* @param array $attributes
|
||||
* @return array
|
||||
*/
|
||||
protected function mergeExtraStorageColumns($request, string $requestAttribute, array $attributes)
|
||||
{
|
||||
$file = $this->retrieveFileFromRequest($request, $requestAttribute);
|
||||
|
||||
if ($this->originalNameColumn) {
|
||||
$attributes[$this->originalNameColumn] = $file->getClientOriginalName();
|
||||
}
|
||||
|
||||
if ($this->sizeColumn) {
|
||||
$attributes[$this->sizeColumn] = $file->getSize();
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of the columns that should be deleted and their values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function columnsThatShouldBeDeleted()
|
||||
{
|
||||
$attributes = [$this->attribute => null];
|
||||
|
||||
if ($this->originalNameColumn) {
|
||||
$attributes[$this->originalNameColumn] = null;
|
||||
}
|
||||
|
||||
if ($this->sizeColumn) {
|
||||
$attributes[$this->sizeColumn] = null;
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the disk that the field is stored on.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getStorageDisk()
|
||||
{
|
||||
return $this->disk ?: $this->getDefaultStorageDisk();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback that should be used to store the file.
|
||||
*
|
||||
* @param callable(\Laravel\Nova\Http\Requests\NovaRequest, object, string, string, ?string, ?string):mixed $storageCallback
|
||||
* @return $this
|
||||
*
|
||||
* @phpstan-param callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Model|\Illuminate\Support\Fluent, string, string, ?string, ?string):mixed $storageCallback
|
||||
*/
|
||||
public function store(callable $storageCallback)
|
||||
{
|
||||
$this->storageCallback = $storageCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback that should be used to determine the file's storage name.
|
||||
*
|
||||
* @param callable(\Illuminate\Http\Request):string $storeAsCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function storeAs(callable $storeAsCallback)
|
||||
{
|
||||
$this->storeAsCallback = $storeAsCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback that should be used to retrieve the thumbnail URL.
|
||||
*
|
||||
* @param callable(mixed, string, mixed):?string $thumbnailUrlCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function thumbnail(callable $thumbnailUrlCallback)
|
||||
{
|
||||
$this->thumbnailUrlCallback = $thumbnailUrlCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback that should be used to retrieve the preview URL.
|
||||
*
|
||||
* @param callable(mixed, ?string, mixed):?string $previewUrlCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function preview(callable $previewUrlCallback)
|
||||
{
|
||||
$this->previewUrlCallback = $previewUrlCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the column where the file's original name should be stored.
|
||||
*
|
||||
* @param string $column
|
||||
* @return $this
|
||||
*/
|
||||
public function storeOriginalName($column)
|
||||
{
|
||||
$this->originalNameColumn = $column;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the column where the file size should be stored.
|
||||
*
|
||||
* @param string $column
|
||||
* @return $this
|
||||
*/
|
||||
public function storeSize($column)
|
||||
{
|
||||
$this->sizeColumn = $column;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @return void
|
||||
*/
|
||||
public function fillForAction(NovaRequest $request, $model)
|
||||
{
|
||||
if (isset($request[$this->attribute])) {
|
||||
$model->{$this->attribute} = $request[$this->attribute];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fillAttribute(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
if (is_null($file = $this->retrieveFileFromRequest($request, $requestAttribute))) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hasExistingFile = ! is_null($this->getStoragePath());
|
||||
|
||||
$result = call_user_func(
|
||||
$this->storageCallback,
|
||||
$request,
|
||||
$model,
|
||||
$attribute,
|
||||
$requestAttribute,
|
||||
$this->getStorageDisk(),
|
||||
$this->getStorageDir()
|
||||
);
|
||||
|
||||
if ($result === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($result instanceof Closure) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (! is_array($result)) {
|
||||
return $model->{$attribute} = $result;
|
||||
}
|
||||
|
||||
foreach ($result as $key => $value) {
|
||||
$model->{$key} = $value;
|
||||
}
|
||||
|
||||
if ($this->isPrunable() && $hasExistingFile) {
|
||||
return function () use ($model, $request) {
|
||||
call_user_func(
|
||||
$this->deleteCallback,
|
||||
$request,
|
||||
$model,
|
||||
$this->getStorageDisk(),
|
||||
$this->getStoragePath()
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path that the field is stored at on disk.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getStoragePath()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'thumbnailUrl' => $this->resolveThumbnailUrl(),
|
||||
'previewUrl' => $this->resolvePreviewUrl(),
|
||||
'downloadable' => $this->downloadsAreEnabled && isset($this->downloadResponseCallback) && ! empty($this->value),
|
||||
'deletable' => isset($this->deleteCallback) && $this->deletable,
|
||||
'acceptedTypes' => $this->acceptedTypes,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve file instance from request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $requestAttribute
|
||||
* @return \Illuminate\Http\UploadedFile|null
|
||||
*/
|
||||
protected function retrieveFileFromRequest($request, string $requestAttribute)
|
||||
{
|
||||
$file = Str::contains($requestAttribute, '.') && $request->filled($requestAttribute)
|
||||
? data_get($request->all(), $requestAttribute)
|
||||
: $request->file($requestAttribute);
|
||||
|
||||
return ! is_null($file) && $file->isValid() ? $file : null;
|
||||
}
|
||||
}
|
||||
115
nova/src/Fields/Filterable.php
Normal file
115
nova/src/Fields/Filterable.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait Filterable
|
||||
{
|
||||
/**
|
||||
* The callback used to determine if the field is filterable.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):(void))|null
|
||||
*/
|
||||
public $filterableCallback;
|
||||
|
||||
/**
|
||||
* The callback used to determine if the field is filterable.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):(void))|null $filterableCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function filterable(callable $filterableCallback = null)
|
||||
{
|
||||
if (property_exists($this, 'requiresExplicitFilterableCallback')
|
||||
&& $this->requiresExplicitFilterableCallback === true
|
||||
&& is_null($filterableCallback)
|
||||
) {
|
||||
throw new InvalidArgumentException('$filterableCallback needs to be callable/Closure');
|
||||
}
|
||||
|
||||
$this->filterableCallback = ! is_null($filterableCallback)
|
||||
? $filterableCallback
|
||||
: $this->defaultFilterableCallback();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set field as without filterable.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withoutFilterable()
|
||||
{
|
||||
$this->filterableCallback = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the filter to the given query.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function applyFilter(NovaRequest $request, $query, $value)
|
||||
{
|
||||
call_user_func($this->filterableCallback, $request, $query, $value, $this->filterableAttribute($request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return $this->jsonSerialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter|null
|
||||
*/
|
||||
public function resolveFilter(NovaRequest $request)
|
||||
{
|
||||
return is_callable($this->filterableCallback) ? $this->makeFilter($request) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default filterable callback.
|
||||
*
|
||||
* @return callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed):\Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function defaultFilterableCallback()
|
||||
{
|
||||
return function (NovaRequest $request, $query, $value) {
|
||||
return $query->where($this->filterableAttribute($request), '=', $value);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Define filterable attribute.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return string
|
||||
*/
|
||||
protected function filterableAttribute(NovaRequest $request)
|
||||
{
|
||||
return $this->attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter|null
|
||||
*/
|
||||
abstract protected function makeFilter(NovaRequest $request);
|
||||
}
|
||||
13
nova/src/Fields/Filters/BooleanFilter.php
Normal file
13
nova/src/Fields/Filters/BooleanFilter.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Filters;
|
||||
|
||||
class BooleanFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* The filter's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'boolean-field';
|
||||
}
|
||||
13
nova/src/Fields/Filters/BooleanGroupFilter.php
Normal file
13
nova/src/Fields/Filters/BooleanGroupFilter.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Filters;
|
||||
|
||||
class BooleanGroupFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* The filter's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'boolean-group-field';
|
||||
}
|
||||
57
nova/src/Fields/Filters/DateFilter.php
Normal file
57
nova/src/Fields/Filters/DateFilter.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Filters;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class DateFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* The filter's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'date-field';
|
||||
|
||||
/**
|
||||
* Apply the filter to the given query.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param mixed $value
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function apply(NovaRequest $request, $query, $value)
|
||||
{
|
||||
$value = collect($value)->transform(function ($value) {
|
||||
return ! empty($value) ? rescue(function () use ($value) {
|
||||
return CarbonImmutable::createFromFormat('Y-m-d', $value);
|
||||
}, null) : null;
|
||||
});
|
||||
|
||||
if ($value->filter()->isNotEmpty()) {
|
||||
if ($value[0] instanceof CarbonImmutable) {
|
||||
$value[0] = $value[0]->startOfDay();
|
||||
}
|
||||
|
||||
if ($value[1] instanceof CarbonImmutable) {
|
||||
$value[1] = $value[1]->endOfDay();
|
||||
}
|
||||
|
||||
$this->field->applyFilter($request, $query, $value->all());
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default options for the filter.
|
||||
*
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function default()
|
||||
{
|
||||
return [null, null];
|
||||
}
|
||||
}
|
||||
39
nova/src/Fields/Filters/DateTimeFilter.php
Normal file
39
nova/src/Fields/Filters/DateTimeFilter.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Filters;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class DateTimeFilter extends DateFilter
|
||||
{
|
||||
/**
|
||||
* The filter's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'date-time-field';
|
||||
|
||||
/**
|
||||
* Apply the filter to the given query.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param mixed $value
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function apply(NovaRequest $request, $query, $value)
|
||||
{
|
||||
$value = collect($value)->transform(function ($value) {
|
||||
return ! empty($value) ? rescue(function () use ($value) {
|
||||
return CarbonImmutable::parse($value);
|
||||
}, null) : null;
|
||||
});
|
||||
|
||||
if ($value->filter()->isNotEmpty()) {
|
||||
$this->field->applyFilter($request, $query, $value->all());
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
40
nova/src/Fields/Filters/EloquentFilter.php
Normal file
40
nova/src/Fields/Filters/EloquentFilter.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Filters;
|
||||
|
||||
/**
|
||||
* @template TField of \Laravel\Nova\Contracts\FilterableField&\Laravel\Nova\Contracts\RelatableField&\Laravel\Nova\Fields\Field
|
||||
*/
|
||||
class EloquentFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* The filter's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'eloquent-field';
|
||||
|
||||
/**
|
||||
* Get the key for the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return 'resource:'.$this->field->resourceClass::uriKey().':'.$this->field->attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeField()
|
||||
{
|
||||
if (method_exists($this->field, 'serializeForFilter')) {
|
||||
return $this->field->serializeForFilter();
|
||||
}
|
||||
|
||||
return $this->field->jsonSerialize();
|
||||
}
|
||||
}
|
||||
85
nova/src/Fields/Filters/Filter.php
Normal file
85
nova/src/Fields/Filters/Filter.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Filters;
|
||||
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Filters\Filter as BaseFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
abstract class Filter extends BaseFilter
|
||||
{
|
||||
/**
|
||||
* The filter's field.
|
||||
*
|
||||
* @var \Laravel\Nova\Contracts\FilterableField&\Laravel\Nova\Fields\Field
|
||||
*/
|
||||
public $field;
|
||||
|
||||
/**
|
||||
* Construct a new filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Contracts\FilterableField&\Laravel\Nova\Fields\Field $field
|
||||
*/
|
||||
public function __construct(FilterableField $field)
|
||||
{
|
||||
$this->field = $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the displayable name of the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name()
|
||||
{
|
||||
return $this->field->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key for the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return class_basename($this->field).':'.$this->field->attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the filter to the given query.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param mixed $value
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function apply(NovaRequest $request, $query, $value)
|
||||
{
|
||||
$this->field->applyFilter($request, $query, $value);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeField()
|
||||
{
|
||||
return $this->field->serializeForFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the filter for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'component' => 'filter-'.$this->component,
|
||||
'field' => $this->serializeField(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
33
nova/src/Fields/Filters/MorphToFilter.php
Normal file
33
nova/src/Fields/Filters/MorphToFilter.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Filters;
|
||||
|
||||
class MorphToFilter extends EloquentFilter
|
||||
{
|
||||
/**
|
||||
* The filter's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'morph-to-field';
|
||||
|
||||
/**
|
||||
* Get the key for the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return 'resource:morphable:'.$this->field->attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeField()
|
||||
{
|
||||
return $this->field->serializeForFilter();
|
||||
}
|
||||
}
|
||||
13
nova/src/Fields/Filters/MultiSelectFilter.php
Normal file
13
nova/src/Fields/Filters/MultiSelectFilter.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Filters;
|
||||
|
||||
class MultiSelectFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* The filter's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'multi-select-field';
|
||||
}
|
||||
46
nova/src/Fields/Filters/NumberFilter.php
Normal file
46
nova/src/Fields/Filters/NumberFilter.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Filters;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class NumberFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* The filter's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'number-field';
|
||||
|
||||
/**
|
||||
* Apply the filter to the given query.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param mixed $value
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function apply(NovaRequest $request, $query, $value)
|
||||
{
|
||||
$value = collect($value)->transform(function ($value) {
|
||||
return ! $this->field->isValidNullValue($value) ? $value : null;
|
||||
});
|
||||
|
||||
if ($value->filter()->isNotEmpty()) {
|
||||
$this->field->applyFilter($request, $query, $value->all());
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default options for the filter.
|
||||
*
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function default()
|
||||
{
|
||||
return [null, null];
|
||||
}
|
||||
}
|
||||
13
nova/src/Fields/Filters/SelectFilter.php
Normal file
13
nova/src/Fields/Filters/SelectFilter.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Filters;
|
||||
|
||||
class SelectFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* The filter's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'select-field';
|
||||
}
|
||||
43
nova/src/Fields/Filters/StatusFilter.php
Normal file
43
nova/src/Fields/Filters/StatusFilter.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Filters;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class StatusFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* The filter's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'select-filter';
|
||||
|
||||
/**
|
||||
* Get the filter's available options.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array
|
||||
*/
|
||||
public function options(NovaRequest $request)
|
||||
{
|
||||
return [
|
||||
'loading' => __('Loading'),
|
||||
'finished' => __('Finished'),
|
||||
'failed' => __('Failed'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the filter for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'component' => $this->component,
|
||||
'field' => $this->serializeField(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
13
nova/src/Fields/Filters/TextFilter.php
Normal file
13
nova/src/Fields/Filters/TextFilter.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Filters;
|
||||
|
||||
class TextFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* The filter's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'text-field';
|
||||
}
|
||||
200
nova/src/Fields/FormData.php
Normal file
200
nova/src/Fields/FormData.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Fluent;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
/**
|
||||
* @template TKey of array-key
|
||||
* @template TValue
|
||||
*
|
||||
* @extends \Illuminate\Support\Fluent<TKey, TValue>
|
||||
*/
|
||||
class FormData extends Fluent
|
||||
{
|
||||
/**
|
||||
* The Request instance.
|
||||
*
|
||||
* @var \Laravel\Nova\Http\Requests\NovaRequest
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Create a new fluent instance.
|
||||
*
|
||||
* @param iterable<TKey, TValue> $attributes
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($attributes, NovaRequest $request)
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make fluent payload from request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param array<string, mixed> $fields
|
||||
* @return static
|
||||
*/
|
||||
public static function make(NovaRequest $request, array $fields)
|
||||
{
|
||||
if (! is_null($request->resource) && ! is_null($request->resourceId)) {
|
||||
$fields["resource:{$request->resource}"] = $request->resourceId;
|
||||
}
|
||||
|
||||
if (! is_null($request->viaResource) && ! is_null($request->viaResourceId)) {
|
||||
$fields["resource:{$request->viaResource}"] = $request->viaResourceId;
|
||||
}
|
||||
|
||||
if (! is_null($request->relatedResource) && ! is_null($request->relatedResourceId)) {
|
||||
$fields["resource:{$request->relatedResource}"] = $request->relatedResourceId;
|
||||
}
|
||||
|
||||
return new static($fields, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make fluent payload from request only on specific keys.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param array<int, string> $onlyAttributes
|
||||
* @return static
|
||||
*/
|
||||
public static function onlyFrom(NovaRequest $request, array $onlyAttributes)
|
||||
{
|
||||
$fields = $request->method() === 'GET' && ! is_null($dependsOn = $request->query('dependsOn'))
|
||||
? Arr::only(json_decode(base64_decode($dependsOn), true), $onlyAttributes)
|
||||
: $request->only($onlyAttributes);
|
||||
|
||||
return static::make($request, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a resource attribute from the fluent instance.
|
||||
*
|
||||
* @param string $uriKey
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function resource($uriKey, $default = null)
|
||||
{
|
||||
$key = "resource:{$uriKey}";
|
||||
|
||||
if (! empty($this->request->viaRelationship)
|
||||
&& ($uriKey === $this->request->relatedResource || $uriKey === $this->request->viaResource)
|
||||
) {
|
||||
return $this->get($key, $this->get($this->request->viaRelationship, $default));
|
||||
}
|
||||
|
||||
return $this->get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve input from the request as a Stringable instance.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Support\Stringable
|
||||
*/
|
||||
public function str($key, $default = null)
|
||||
{
|
||||
return $this->string($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve input from the request as a Stringable instance.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return \Illuminate\Support\Stringable
|
||||
*/
|
||||
public function string($key, $default = null)
|
||||
{
|
||||
return Str::of($this->get($key, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve input from the request as a json value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function json($key, $default = null)
|
||||
{
|
||||
$value = $this->get($key, $default);
|
||||
|
||||
return is_string($value) ? json_decode($value, true) : $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve input as a boolean value.
|
||||
*
|
||||
* Returns true when value is "1", "true", "on", and "yes". Otherwise, returns false.
|
||||
*
|
||||
* @param string|null $key
|
||||
* @param bool $default
|
||||
* @return bool
|
||||
*/
|
||||
public function boolean($key = null, $default = false)
|
||||
{
|
||||
return filter_var($this->get($key, $default), FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve input as an integer value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param int $default
|
||||
* @return int
|
||||
*/
|
||||
public function integer($key, $default = 0)
|
||||
{
|
||||
return intval($this->get($key, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve input as a float value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param float $default
|
||||
* @return float
|
||||
*/
|
||||
public function float($key, $default = 0.0)
|
||||
{
|
||||
return floatval($this->get($key, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve input from the request as a Carbon instance.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string|null $format
|
||||
* @param string|null $tz
|
||||
* @return \Illuminate\Support\Carbon|null
|
||||
*
|
||||
* @throws \Carbon\Exceptions\InvalidFormatException
|
||||
*/
|
||||
public function date($key, $format = null, $tz = null)
|
||||
{
|
||||
$value = $this->get($key);
|
||||
|
||||
if (! filled($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_null($format)) {
|
||||
return Date::parse($value, $tz);
|
||||
}
|
||||
|
||||
return Date::createFromFormat($format, $value, $tz);
|
||||
}
|
||||
}
|
||||
53
nova/src/Fields/FormatsRelatableDisplayValues.php
Normal file
53
nova/src/Fields/FormatsRelatableDisplayValues.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Closure;
|
||||
use Laravel\Nova\Nova;
|
||||
use Laravel\Nova\Resource;
|
||||
|
||||
trait FormatsRelatableDisplayValues
|
||||
{
|
||||
/**
|
||||
* The column that should be displayed for the field.
|
||||
*
|
||||
* @var (callable(mixed):(string))|null
|
||||
*/
|
||||
public $display;
|
||||
|
||||
/**
|
||||
* Format the associatable display value.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @return string
|
||||
*/
|
||||
protected function formatDisplayValue($resource)
|
||||
{
|
||||
if (! $resource instanceof Resource) {
|
||||
$resource = Nova::newResourceFromModel($resource);
|
||||
}
|
||||
|
||||
if (is_callable($this->display)) {
|
||||
return call_user_func($this->display, $resource);
|
||||
}
|
||||
|
||||
return $resource->title();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the column that should be displayed for the field.
|
||||
*
|
||||
* @param (\Closure(mixed):(string))|string $display
|
||||
* @return $this
|
||||
*/
|
||||
public function display($display)
|
||||
{
|
||||
$this->display = $display instanceof Closure
|
||||
? $display
|
||||
: function ($resource) use ($display) {
|
||||
return $resource->{$display};
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
51
nova/src/Fields/Gravatar.php
Normal file
51
nova/src/Fields/Gravatar.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
/**
|
||||
* @method static static make(mixed $name = 'Avatar', string|null $attribute = 'email')
|
||||
*/
|
||||
class Gravatar extends Avatar implements Unfillable
|
||||
{
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name = 'Avatar', $attribute = 'email')
|
||||
{
|
||||
parent::__construct($name, $attribute ?? 'email');
|
||||
|
||||
$this->exceptOnForms();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the given attribute from the given resource.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string $attribute
|
||||
* @return mixed
|
||||
*/
|
||||
protected function resolveAttribute($resource, $attribute)
|
||||
{
|
||||
$callback = function () use ($resource, $attribute) {
|
||||
return 'https://www.gravatar.com/avatar/'.md5(strtolower(parent::resolveAttribute($resource, $attribute))).'?s=300';
|
||||
};
|
||||
|
||||
$this->preview($callback)->thumbnail($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge([
|
||||
'indexName' => '',
|
||||
], parent::jsonSerialize());
|
||||
}
|
||||
}
|
||||
206
nova/src/Fields/HandlesValidation.php
Normal file
206
nova/src/Fields/HandlesValidation.php
Normal file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Contracts\Validation\InvokableRule;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type TValidationRules from \Laravel\Nova\Fields\Field
|
||||
* @phpstan-import-type TFieldValidationRules from \Laravel\Nova\Fields\Field
|
||||
*/
|
||||
trait HandlesValidation
|
||||
{
|
||||
/**
|
||||
* The validation attribute for the field.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $validationAttribute;
|
||||
|
||||
/**
|
||||
* The validation rules for creation and updates.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(array|\Stringable|string|callable))|array|\Stringable|string
|
||||
*
|
||||
* @phpstan-var (callable(\Laravel\Nova\Http\Requests\NovaRequest):TValidationRules)|TValidationRules
|
||||
*/
|
||||
public $rules = [];
|
||||
|
||||
/**
|
||||
* The validation rules for creation.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(array|\Stringable|string|callable))|array|\Stringable|string
|
||||
*
|
||||
* @phpstan-var (callable(\Laravel\Nova\Http\Requests\NovaRequest):TValidationRules)|TValidationRules
|
||||
*/
|
||||
public $creationRules = [];
|
||||
|
||||
/**
|
||||
* The validation rules for updates.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(array|\Stringable|string|callable))|array|\Stringable|string
|
||||
*
|
||||
* @phpstan-var (callable(\Laravel\Nova\Http\Requests\NovaRequest):TValidationRules)|TValidationRules
|
||||
*/
|
||||
public $updateRules = [];
|
||||
|
||||
/**
|
||||
* Set the validation rules for the field.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(array|\Stringable|string|callable))|array|\Stringable|string ...$rules
|
||||
* @return $this
|
||||
*
|
||||
* @phpstan-param (callable(\Laravel\Nova\Http\Requests\NovaRequest):TValidationRules)|TValidationRules ...$rules
|
||||
*/
|
||||
public function rules($rules)
|
||||
{
|
||||
$parameters = func_get_args();
|
||||
|
||||
$this->rules = (
|
||||
$rules instanceof Rule ||
|
||||
$rules instanceof InvokableRule ||
|
||||
$rules instanceof ValidationRule ||
|
||||
is_string($rules) ||
|
||||
count($parameters) > 1
|
||||
) ? $parameters : $rules;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array<array-key, array<int, mixed>>
|
||||
*
|
||||
* @phpstan-return array<string, array<int, TFieldValidationRules>>
|
||||
*/
|
||||
public function getRules(NovaRequest $request)
|
||||
{
|
||||
return [
|
||||
$this->attribute => is_callable($this->rules) ? call_user_func($this->rules, $request) : $this->rules,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the creation rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array<array-key, mixed>
|
||||
*
|
||||
* @phpstan-return array<string, array<int, TFieldValidationRules>>
|
||||
*/
|
||||
public function getCreationRules(NovaRequest $request)
|
||||
{
|
||||
$rules = [
|
||||
$this->attribute => is_callable($this->creationRules) ? call_user_func(
|
||||
$this->creationRules,
|
||||
$request
|
||||
) : $this->creationRules,
|
||||
];
|
||||
|
||||
return array_merge_recursive($this->getRules($request), $rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the creation validation rules for the field.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(array|\Stringable|string|callable))|array|\Stringable|string ...$rules
|
||||
* @return $this
|
||||
*
|
||||
* @phpstan-param (callable(\Laravel\Nova\Http\Requests\NovaRequest):TValidationRules)|TValidationRules ...$rules
|
||||
*/
|
||||
public function creationRules($rules)
|
||||
{
|
||||
$parameters = func_get_args();
|
||||
|
||||
$this->creationRules = (
|
||||
$rules instanceof Rule ||
|
||||
$rules instanceof InvokableRule ||
|
||||
$rules instanceof ValidationRule ||
|
||||
is_string($rules) ||
|
||||
count($parameters) > 1
|
||||
) ? $parameters : $rules;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the update rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array<array-key, array<int, mixed>>
|
||||
*
|
||||
* @phpstan-return array<string, array<int, TFieldValidationRules>>
|
||||
*/
|
||||
public function getUpdateRules(NovaRequest $request)
|
||||
{
|
||||
$rules = [
|
||||
$this->attribute => is_callable($this->updateRules) ? call_user_func(
|
||||
$this->updateRules,
|
||||
$request
|
||||
) : $this->updateRules,
|
||||
];
|
||||
|
||||
return array_merge_recursive($this->getRules($request), $rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the creation validation rules for the field.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(array|\Stringable|string|callable))|array|\Stringable|string ...$rules
|
||||
* @return $this
|
||||
*
|
||||
* @phpstan-param (callable(\Laravel\Nova\Http\Requests\NovaRequest):TValidationRules)|TValidationRules ...$rules
|
||||
*/
|
||||
public function updateRules($rules)
|
||||
{
|
||||
$parameters = func_get_args();
|
||||
|
||||
$this->updateRules = (
|
||||
$rules instanceof Rule ||
|
||||
$rules instanceof InvokableRule ||
|
||||
$rules instanceof ValidationRule ||
|
||||
is_string($rules) ||
|
||||
count($parameters) > 1
|
||||
) ? $parameters : $rules;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation attribute for the field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return string
|
||||
*/
|
||||
public function getValidationAttribute(NovaRequest $request)
|
||||
{
|
||||
return $this->validationAttribute ?? Str::singular($this->attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation attribute names for the field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getValidationAttributeNames(NovaRequest $request)
|
||||
{
|
||||
return [$this->validationKey() => $this->name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the validation key for the field.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function validationKey()
|
||||
{
|
||||
return $this->attribute;
|
||||
}
|
||||
}
|
||||
167
nova/src/Fields/HasAttachments.php
Normal file
167
nova/src/Fields/HasAttachments.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Fields\Attachments\DeleteAttachments;
|
||||
use Laravel\Nova\Fields\Attachments\DetachAnyAttachment;
|
||||
use Laravel\Nova\Fields\Attachments\DiscardPendingAttachments;
|
||||
use Laravel\Nova\Fields\Attachments\PendingAttachment;
|
||||
use Laravel\Nova\Fields\Attachments\StorePendingAttachment;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait HasAttachments
|
||||
{
|
||||
use Deletable;
|
||||
use Storable;
|
||||
|
||||
/**
|
||||
* Indicates if the field should accept files.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $withFiles = false;
|
||||
|
||||
/**
|
||||
* The callback that should be executed to store file attachments.
|
||||
*
|
||||
* @var callable(\Illuminate\Http\Request):array{path: string, url: string}
|
||||
*/
|
||||
public $attachCallback;
|
||||
|
||||
/**
|
||||
* The callback that should be executed to delete persisted file attachments.
|
||||
*
|
||||
* @var callable(\Illuminate\Http\Request):void
|
||||
*/
|
||||
public $detachCallback;
|
||||
|
||||
/**
|
||||
* The callback that should be executed to discard file attachments.
|
||||
*
|
||||
* @var callable(\Illuminate\Http\Request):void
|
||||
*/
|
||||
public $discardCallback;
|
||||
|
||||
/**
|
||||
* Specify the callback that should be used to store file attachments.
|
||||
*
|
||||
* @param callable(\Illuminate\Http\Request):array{path: string, url: string} $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function attach(callable $callback)
|
||||
{
|
||||
$this->withFiles = true;
|
||||
|
||||
$this->attachCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback that should be used to delete a single, persisted file attachment.
|
||||
*
|
||||
* @param callable(\Illuminate\Http\Request):void $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function detach(callable $callback)
|
||||
{
|
||||
$this->withFiles = true;
|
||||
|
||||
$this->detachCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback that should be used to discard pending file attachments.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function discard(callable $callback)
|
||||
{
|
||||
$this->withFiles = true;
|
||||
|
||||
$this->discardCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback that should be used to delete the field.
|
||||
*
|
||||
* @param callable(\Laravel\Nova\Http\Requests\NovaRequest, mixed, ?string, ?string):mixed $deleteCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function delete(callable $deleteCallback)
|
||||
{
|
||||
$this->withFiles = true;
|
||||
|
||||
$this->deleteCallback = $deleteCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that file uploads should be allowed.
|
||||
*
|
||||
* @param string $disk
|
||||
* @param string $path
|
||||
* @return $this
|
||||
*/
|
||||
public function withFiles($disk = null, $path = '/')
|
||||
{
|
||||
$this->withFiles = true;
|
||||
|
||||
$this->disk($disk)->path($path);
|
||||
|
||||
$this->attach(new StorePendingAttachment($this))
|
||||
->detach(new DetachAnyAttachment)
|
||||
->delete(new DeleteAttachments($this))
|
||||
->discard(new DiscardPendingAttachments)
|
||||
->prunable();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return void|\Closure
|
||||
*/
|
||||
protected function fillAttributeWithAttachment(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
$callbacks = [];
|
||||
|
||||
$maybeCallback = parent::fillAttribute($request, $requestAttribute, $model, $attribute);
|
||||
|
||||
$attribute = Str::contains($requestAttribute, '.') && $this->attribute !== $requestAttribute
|
||||
? "{$requestAttribute}DraftId"
|
||||
: Str::replace('.', '->', "{$this->attribute}DraftId");
|
||||
|
||||
if (is_callable($maybeCallback)) {
|
||||
$callbacks[] = $maybeCallback;
|
||||
}
|
||||
|
||||
if ($request->{$attribute} && $this->withFiles) {
|
||||
$callbacks[] = function () use ($request, $model, $attribute) {
|
||||
PendingAttachment::persistDraft(
|
||||
$request->{$attribute},
|
||||
$this,
|
||||
$model
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (count($callbacks)) {
|
||||
return function () use ($callbacks) {
|
||||
collect($callbacks)->each->__invoke();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
65
nova/src/Fields/HasDownload.php
Normal file
65
nova/src/Fields/HasDownload.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait HasDownload
|
||||
{
|
||||
/**
|
||||
* The callback used to generate the download HTTP response.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest, \Laravel\Nova\Resource, ?string, ?string):(mixed))|null
|
||||
*/
|
||||
public $downloadResponseCallback;
|
||||
|
||||
/**
|
||||
* Determine if the file is able to be downloaded.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $downloadsAreEnabled = true;
|
||||
|
||||
/**
|
||||
* Disable downloading the file.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function disableDownload()
|
||||
{
|
||||
$this->downloadsAreEnabled = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback that should be used to create a download HTTP response.
|
||||
*
|
||||
* @param callable(\Laravel\Nova\Http\Requests\NovaRequest, \Laravel\Nova\Resource, ?string, ?string):mixed $downloadResponseCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function download(callable $downloadResponseCallback)
|
||||
{
|
||||
$this->downloadResponseCallback = $downloadResponseCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP response to download the underlying field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Laravel\Nova\Resource $resource
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function toDownloadResponse(NovaRequest $request, $resource)
|
||||
{
|
||||
return call_user_func(
|
||||
$this->downloadResponseCallback,
|
||||
$request,
|
||||
$resource->resource,
|
||||
$this->getStorageDisk(),
|
||||
$this->getStoragePath()
|
||||
);
|
||||
}
|
||||
}
|
||||
174
nova/src/Fields/HasMany.php
Normal file
174
nova/src/Fields/HasMany.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Nova\Contracts\ListableField;
|
||||
use Laravel\Nova\Contracts\RelatableField;
|
||||
use Laravel\Nova\Exceptions\HelperNotSupported;
|
||||
use Laravel\Nova\Exceptions\NovaException;
|
||||
use Laravel\Nova\Panel;
|
||||
|
||||
/**
|
||||
* @method static static make(mixed $name, string|null $attribute = null, string|null $resource = null)
|
||||
*/
|
||||
class HasMany extends Field implements ListableField, RelatableField
|
||||
{
|
||||
use Collapsable;
|
||||
|
||||
/**
|
||||
* Add help text to the metric.
|
||||
*
|
||||
* @param string $text
|
||||
* @return $this
|
||||
*
|
||||
* @throws HelperNotSupported
|
||||
*/
|
||||
public function help($text)
|
||||
{
|
||||
throw NovaException::helperNotSupported(__METHOD__, __CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'has-many-field';
|
||||
|
||||
/**
|
||||
* The class name of the related resource.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Resource>
|
||||
*/
|
||||
public $resourceClass;
|
||||
|
||||
/**
|
||||
* The URI key of the related resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $resourceName;
|
||||
|
||||
/**
|
||||
* The name of the Eloquent "has many" relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $hasManyRelationship;
|
||||
|
||||
/**
|
||||
* The displayable singular label of the relation.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $singularLabel;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @param class-string<\Laravel\Nova\Resource>|null $resource
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, $resource = null)
|
||||
{
|
||||
parent::__construct($name, $attribute);
|
||||
|
||||
$resource = $resource ?? ResourceRelationshipGuesser::guessResource($name);
|
||||
|
||||
$this->resourceClass = $resource;
|
||||
$this->resourceName = $resource::uriKey();
|
||||
$this->hasManyRelationship = $this->attribute = $attribute ?? ResourceRelationshipGuesser::guessRelation($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipName()
|
||||
{
|
||||
return $this->hasManyRelationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipType()
|
||||
{
|
||||
return 'hasMany';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field should be displayed for the given request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(Request $request)
|
||||
{
|
||||
return call_user_func(
|
||||
[$this->resourceClass, 'authorizedToViewAny'], $request
|
||||
) && parent::authorize($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function resolve($resource, $attribute = null)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the displayable singular label of the resource.
|
||||
*
|
||||
* @param string $singularLabel
|
||||
* @return $this
|
||||
*/
|
||||
public function singularLabel($singularLabel)
|
||||
{
|
||||
$this->singularLabel = $singularLabel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make current field behaves as panel.
|
||||
*
|
||||
* @return \Laravel\Nova\Panel
|
||||
*/
|
||||
public function asPanel()
|
||||
{
|
||||
return Panel::make($this->name, [$this])
|
||||
->withMeta([
|
||||
'prefixComponent' => true,
|
||||
])->withComponent('relationship-panel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge([
|
||||
'collapsable' => $this->collapsable,
|
||||
'collapsedByDefault' => $this->collapsedByDefault,
|
||||
'hasManyRelationship' => $this->hasManyRelationship,
|
||||
'relatable' => true,
|
||||
'perPage' => $this->resourceClass::$perPageViaRelationship,
|
||||
'resourceName' => $this->resourceName,
|
||||
'singularLabel' => $this->singularLabel ?? $this->resourceClass::singularLabel(),
|
||||
], parent::jsonSerialize());
|
||||
}
|
||||
}
|
||||
95
nova/src/Fields/HasManyThrough.php
Normal file
95
nova/src/Fields/HasManyThrough.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Contracts\ListableField;
|
||||
use Laravel\Nova\Contracts\RelatableField;
|
||||
use Laravel\Nova\Panel;
|
||||
|
||||
/**
|
||||
* @method static static make(mixed $name, string|null $attribute = null, string|null $resource = null)
|
||||
*/
|
||||
class HasManyThrough extends HasMany implements ListableField, RelatableField
|
||||
{
|
||||
use Collapsable;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'has-many-through-field';
|
||||
|
||||
/**
|
||||
* The name of the Eloquent "has many through" relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $hasManyThroughRelationship;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @param class-string<\Laravel\Nova\Resource>|null $resource
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, $resource = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resource);
|
||||
|
||||
$this->hasManyThroughRelationship = $this->attribute = $attribute ?? ResourceRelationshipGuesser::guessRelation($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipName()
|
||||
{
|
||||
return $this->hasManyThroughRelationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipType()
|
||||
{
|
||||
return 'hasManyThrough';
|
||||
}
|
||||
|
||||
/**
|
||||
* Make current field behaves as panel.
|
||||
*
|
||||
* @return \Laravel\Nova\Panel
|
||||
*/
|
||||
public function asPanel()
|
||||
{
|
||||
return Panel::make($this->name, [$this])
|
||||
->withMeta([
|
||||
'prefixComponent' => true,
|
||||
])->withComponent('relationship-panel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge([
|
||||
'collapsable' => $this->collapsable,
|
||||
'collapsedByDefault' => $this->collapsedByDefault,
|
||||
'hasManyThroughRelationship' => $this->hasManyThroughRelationship,
|
||||
'relatable' => true,
|
||||
'perPage' => $this->resourceClass::$perPageViaRelationship,
|
||||
'resourceName' => $this->resourceName,
|
||||
'singularLabel' => $this->singularLabel ?? $this->resourceClass::singularLabel(),
|
||||
], parent::jsonSerialize());
|
||||
}
|
||||
}
|
||||
634
nova/src/Fields/HasOne.php
Normal file
634
nova/src/Fields/HasOne.php
Normal file
@@ -0,0 +1,634 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Nova\Contracts\BehavesAsPanel;
|
||||
use Laravel\Nova\Contracts\RelatableField;
|
||||
use Laravel\Nova\Exceptions\NovaException;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Nova;
|
||||
use Laravel\Nova\Panel;
|
||||
use Laravel\Nova\Util;
|
||||
|
||||
/**
|
||||
* @method static static make(mixed $name, string|null $attribute = null, string|null $resource = null)
|
||||
*/
|
||||
class HasOne extends Field implements BehavesAsPanel, RelatableField
|
||||
{
|
||||
use FormatsRelatableDisplayValues;
|
||||
|
||||
/**
|
||||
* Indicates if the related resource can be viewed.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $viewable = true;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'has-one-field';
|
||||
|
||||
/**
|
||||
* The class name of the related resource.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Resource>
|
||||
*/
|
||||
public $resourceClass;
|
||||
|
||||
/**
|
||||
* The URI key of the related resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $resourceName;
|
||||
|
||||
/**
|
||||
* The displayable singular label of the relation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $singularLabel;
|
||||
|
||||
/**
|
||||
* The resolved HasOne Resource.
|
||||
*
|
||||
* @var \Laravel\Nova\Resource|null
|
||||
*/
|
||||
public $hasOneResource;
|
||||
|
||||
/**
|
||||
* The name of the Eloquent "has one" relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $hasOneRelationship;
|
||||
|
||||
/**
|
||||
* The key of the related Eloquent model.
|
||||
*
|
||||
* @var string|int|null
|
||||
*/
|
||||
public $hasOneId;
|
||||
|
||||
/**
|
||||
* The callback use to determine if the HasOne field has already been filled.
|
||||
*
|
||||
* @var \Closure(\Laravel\Nova\Http\Requests\NovaRequest):bool
|
||||
*/
|
||||
public $filledCallback;
|
||||
|
||||
/**
|
||||
* Determine one-of-many relationship.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $ofManyRelationship = false;
|
||||
|
||||
/**
|
||||
* The cached field is required status.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
protected $isRequired = null;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @param class-string<\Laravel\Nova\Resource>|null $resource
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, $resource = null)
|
||||
{
|
||||
parent::__construct($name, $attribute);
|
||||
|
||||
$resource = $resource ?? ResourceRelationshipGuesser::guessResource($name);
|
||||
|
||||
$this->resourceClass = $resource;
|
||||
$this->resourceName = $resource::uriKey();
|
||||
$this->hasOneRelationship = $this->attribute = $attribute ?? ResourceRelationshipGuesser::guessRelation($name);
|
||||
$this->singularLabel = $resource::singularLabel();
|
||||
|
||||
$this->alreadyFilledWhen(function ($request) {
|
||||
$parentResource = Nova::resourceForKey($request->viaResource);
|
||||
|
||||
if ($this->ofManyRelationship === false && $request->viaRelationship === $this->attribute && $request->viaResourceId) {
|
||||
$parent = $parentResource::newModel()
|
||||
->with($this->attribute)
|
||||
->find($request->viaResourceId);
|
||||
|
||||
return optional($parent->{$this->attribute})->exists === true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})->showOnCreating(function ($request) {
|
||||
return ! in_array($request->relationshipType, ['hasOne', 'morphOne']);
|
||||
})->showOnUpdating(function ($request) {
|
||||
return ! in_array($request->relationshipType, ['hasOne', 'morphOne']);
|
||||
})->nullable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make one-of-many relationship field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @param class-string<\Laravel\Nova\Resource>|null $resource
|
||||
* @return static
|
||||
*/
|
||||
public static function ofMany($name, $attribute = null, $resource = null)
|
||||
{
|
||||
return tap(new static($name, $attribute, $resource), function ($field) {
|
||||
$field->ofManyRelationship = true;
|
||||
$field->readonly();
|
||||
$field->onlyOnDetail();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipName()
|
||||
{
|
||||
return $this->hasOneRelationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipType()
|
||||
{
|
||||
return 'hasOne';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field should be displayed for the given request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(Request $request)
|
||||
{
|
||||
return call_user_func(
|
||||
[$this->resourceClass, 'authorizedToViewAny'], $request
|
||||
) && parent::authorize($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field should be for the given request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function authorizedToRelate(Request $request)
|
||||
{
|
||||
return $request->findResource()->authorizedToAdd($request, $this->resourceClass::newModel())
|
||||
&& $this->resourceClass::authorizedToCreate($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function resolve($resource, $attribute = null)
|
||||
{
|
||||
$value = null;
|
||||
|
||||
if ($resource->relationLoaded($this->attribute)) {
|
||||
$value = $resource->getRelation($this->attribute);
|
||||
}
|
||||
|
||||
if (! $value) {
|
||||
$value = $resource->{$this->attribute}()->withoutGlobalScopes()->getResults();
|
||||
}
|
||||
|
||||
if ($value) {
|
||||
$this->alreadyFilledWhen(function () use ($value) {
|
||||
return optional($value)->exists;
|
||||
});
|
||||
|
||||
$this->hasOneResource = new $this->resourceClass($value);
|
||||
|
||||
$this->hasOneId = optional(ID::forResource($this->hasOneResource))->value ?? $value->getKey();
|
||||
|
||||
$this->value = $this->hasOneId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the displayable singular label of the resource.
|
||||
*
|
||||
* @param string $singularLabel
|
||||
* @return $this
|
||||
*/
|
||||
public function singularLabel($singularLabel)
|
||||
{
|
||||
$this->singularLabel = $singularLabel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make current field behaves as panel.
|
||||
*
|
||||
* @return \Laravel\Nova\Panel
|
||||
*/
|
||||
public function asPanel()
|
||||
{
|
||||
return Panel::make($this->name, [$this])
|
||||
->withMeta([
|
||||
'prefixComponent' => true,
|
||||
])
|
||||
->help($this->getHelpText())
|
||||
->withComponent('relationship-panel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return with(app(NovaRequest::class), function ($request) {
|
||||
if (! is_null($this->requiredCallback)) {
|
||||
$this->nullable = ! with($this->requiredCallback, function ($callback) use ($request) {
|
||||
return $callback === true || (is_callable($callback) && call_user_func($callback, $request));
|
||||
});
|
||||
}
|
||||
|
||||
return array_merge([
|
||||
'resourceName' => $this->resourceName,
|
||||
'hasOneRelationship' => $this->hasOneRelationship,
|
||||
'relationshipType' => $this->relationshipType(),
|
||||
'relationId' => $this->hasOneId,
|
||||
'hasOneId' => $this->hasOneId,
|
||||
'relatable' => true,
|
||||
'singularLabel' => $this->singularLabel,
|
||||
'alreadyFilled' => $this->alreadyFilled($request),
|
||||
'authorizedToView' => optional($this->hasOneResource)->authorizedToView($request) ?? true,
|
||||
'authorizedToCreate' => $this->ofManyRelationship === true ? false : $this->authorizedToRelate($request),
|
||||
'createButtonLabel' => $this->resourceClass::createButtonLabel(),
|
||||
'from' => array_filter([
|
||||
'viaResource' => $request->resource,
|
||||
'viaResourceId' => $request->resourceId,
|
||||
'viaRelationship' => $request->viaRelationship ?? $this->attribute,
|
||||
]),
|
||||
], parent::jsonSerialize());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field is required.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function isRequired(NovaRequest $request)
|
||||
{
|
||||
if (is_null($this->isRequired)) {
|
||||
$this->isRequired = parent::isRequired($request);
|
||||
}
|
||||
|
||||
$this->nullable = ! $this->isRequired;
|
||||
|
||||
return $this->isRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Closure used to determine if the HasOne field has already been filled.
|
||||
*
|
||||
* @param \Closure(\Laravel\Nova\Http\Requests\NovaRequest):bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function alreadyFilledWhen($callback)
|
||||
{
|
||||
$this->filledCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the HasOne field has alreaady been filled.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function alreadyFilled(NovaRequest $request)
|
||||
{
|
||||
return call_user_func($this->filledCallback, $request) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check showing on index.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return bool
|
||||
*/
|
||||
public function isShownOnIndex(NovaRequest $request, $resource): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @param string|null $requestAttribute
|
||||
* @return (\Closure():(void))|null
|
||||
*/
|
||||
public function fillInto(NovaRequest $request, $model, $attribute, $requestAttribute = null)
|
||||
{
|
||||
$resourceClass = $this->resourceClass;
|
||||
$relation = $model->loadMissing($this->hasOneRelationship)->getRelation($this->hasOneRelationship) ?? $resourceClass::newModel();
|
||||
|
||||
$editMode = $relation->exists === false ? 'create' : 'update';
|
||||
|
||||
$filled = collect($request->{$attribute} ?? [])->filter()->isNotEmpty();
|
||||
|
||||
if (
|
||||
$this->ofManyRelationship === true
|
||||
|| ($this->nullable && ! $filled && $editMode === 'create')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$resourceClass = $this->resourceClass;
|
||||
$resource = new $resourceClass($relation);
|
||||
|
||||
$callbacks = $resource->availableFields($request)
|
||||
->when($editMode === 'create', function (FieldCollection $fields) use ($request, $relation) {
|
||||
return $fields->onlyCreateFields($request, $relation);
|
||||
})
|
||||
->when($editMode === 'update', function (FieldCollection $fields) use ($request, $relation) {
|
||||
return $fields->onlyUpdateFields($request, $relation);
|
||||
})
|
||||
->withoutReadonly($request)
|
||||
->withoutUnfillable()
|
||||
->map(function (Field $field) use ($request, $relation, $attribute) {
|
||||
return $field->fillInto($request, $relation, $field->attribute, "{$attribute}.{$field->attribute}");
|
||||
});
|
||||
|
||||
if ($editMode === 'create') {
|
||||
$callbacks->prepend(function () use ($request, $relation, $model) {
|
||||
$model->{$this->hasOneRelationship}()->save($relation);
|
||||
|
||||
Nova::usingActionEvent(function ($actionEvent) use ($request, $relation) {
|
||||
$actionEvent->forResourceCreate(Nova::user($request), $relation)->save();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Nova::usingActionEvent(function ($actionEvent) use ($request, $relation) {
|
||||
$actionEvent->forResourceUpdate(Nova::user($request), $relation)->save();
|
||||
});
|
||||
|
||||
$relation->save();
|
||||
}
|
||||
|
||||
$model->setRelation($this->hasOneRelationship, $relation);
|
||||
|
||||
return function () use ($callbacks) {
|
||||
$callbacks->filter(function ($callback) {
|
||||
return is_callable($callback);
|
||||
})->each->__invoke();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the creation rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array<string, array<int, string|\Illuminate\Validation\Rule|\Illuminate\Contracts\Validation\Rule|callable>>
|
||||
*/
|
||||
public function getCreationRules(NovaRequest $request)
|
||||
{
|
||||
return $this->getAvailableValidationRules($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the update rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array<string, array<int, string|\Illuminate\Validation\Rule|\Illuminate\Contracts\Validation\Rule|callable>>
|
||||
*/
|
||||
public function getUpdateRules(NovaRequest $request)
|
||||
{
|
||||
return $this->getAvailableValidationRules($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array<string, array<int, string|\Illuminate\Validation\Rule|\Illuminate\Contracts\Validation\Rule|callable>>
|
||||
*/
|
||||
protected function getAvailableValidationRules(NovaRequest $request)
|
||||
{
|
||||
$model = $request->findModel();
|
||||
$resourceClass = $this->resourceClass;
|
||||
|
||||
$relation = method_exists($model, $this->hasOneRelationship)
|
||||
? $model->loadMissing($this->hasOneRelationship)->getRelation($this->hasOneRelationship) ?? $resourceClass::newModel()
|
||||
: null;
|
||||
|
||||
if (is_null($relation)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$resource = new $resourceClass($relation);
|
||||
|
||||
return $relation->exists === false
|
||||
? $this->getResourceCreationRules($request, $resource)
|
||||
: $this->getResourceUpdateRules($request, $resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the creation rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Laravel\Nova\Resource $resource
|
||||
* @return array<string, array<int, string|\Illuminate\Validation\Rule|\Illuminate\Contracts\Validation\Rule|callable>>
|
||||
*/
|
||||
public function getResourceCreationRules(NovaRequest $request, $resource)
|
||||
{
|
||||
$replacements = Util::dependentRules($this->attribute);
|
||||
|
||||
return $resource->creationFields($request)
|
||||
->reject(function ($field) use ($request) {
|
||||
return $field instanceof BelongsTo && $field->resourceClass == Nova::resourceForKey($request->resource);
|
||||
})
|
||||
->applyDependsOn($request)
|
||||
->mapWithKeys(function ($field) use ($request) {
|
||||
return $field->getCreationRules($request);
|
||||
})
|
||||
->mapWithKeys(function ($field, $attribute) use ($replacements) {
|
||||
if ($this->nullable === true) {
|
||||
array_push($field, 'sometimes');
|
||||
}
|
||||
|
||||
return ["{$this->attribute}.{$attribute}" => collect($field)->transform(function ($rule) use ($replacements) {
|
||||
if (empty($replacements)) {
|
||||
return $rule;
|
||||
}
|
||||
|
||||
return is_string($rule)
|
||||
? str_replace(array_keys($replacements), array_values($replacements), $rule)
|
||||
: $rule;
|
||||
})->all()];
|
||||
})
|
||||
->prepend(['array', $this->nullable === true ? 'nullable' : 'required'], $this->attribute)
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the update rules for this resource fields.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Laravel\Nova\Resource $resource
|
||||
* @return array<string, array<int, string|\Illuminate\Validation\Rule|\Illuminate\Contracts\Validation\Rule|callable>>
|
||||
*/
|
||||
public function getResourceUpdateRules(NovaRequest $request, $resource)
|
||||
{
|
||||
$replacements = collect([
|
||||
'{{resourceId}}' => str_replace(['\'', '"', ',', '\\'], '', $resource->model()->getKey() ?? ''),
|
||||
])->merge(
|
||||
Util::dependentRules($this->attribute),
|
||||
)->filter()->all();
|
||||
|
||||
return $resource->updateFields($request)
|
||||
->reject($this->rejectRecursiveRelatedResourceFields($request))
|
||||
->applyDependsOn($request)
|
||||
->mapWithKeys(function ($field) use ($request) {
|
||||
return $field->getUpdateRules($request);
|
||||
})
|
||||
->mapWithKeys(function ($field, $attribute) use ($replacements) {
|
||||
if ($this->nullable === true) {
|
||||
array_push($field, 'sometimes');
|
||||
}
|
||||
|
||||
return ["{$this->attribute}.{$attribute}" => collect($field)->transform(function ($rule) use ($replacements) {
|
||||
if (empty($replacements)) {
|
||||
return $rule;
|
||||
}
|
||||
|
||||
return is_string($rule)
|
||||
? str_replace(array_keys($replacements), array_values($replacements), $rule)
|
||||
: $rule;
|
||||
})->all()];
|
||||
})
|
||||
->prepend(['array', $this->nullable === true ? 'nullable' : 'required'], $this->attribute)
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation attribute names for the field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getValidationAttributeNames(NovaRequest $request)
|
||||
{
|
||||
$resourceClass = $this->resourceClass;
|
||||
$resource = new $resourceClass($resourceClass::newModel());
|
||||
|
||||
return $resource->updateFields($request)
|
||||
->reject($this->rejectRecursiveRelatedResourceFields($request))
|
||||
->reject(function ($field) {
|
||||
return empty($field->name);
|
||||
})
|
||||
->mapWithKeys(function ($field) {
|
||||
return ["{$this->attribute}.{$field->attribute}" => $field->name];
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the relationship is a of-many relationship.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function ofManyRelationship()
|
||||
{
|
||||
return $this->ofManyRelationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for showing when creating.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function isShownOnCreation(NovaRequest $request): bool
|
||||
{
|
||||
return call_user_func($this->rejectRecursiveRelatedResourceFields($request), $this) === false
|
||||
&& parent::isShownOnCreation($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for showing when updating.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return bool
|
||||
*/
|
||||
public function isShownOnUpdate(NovaRequest $request, $resource): bool
|
||||
{
|
||||
return call_user_func($this->rejectRecursiveRelatedResourceFields($request), $this) === false
|
||||
&& parent::isShownOnUpdate($request, $resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject recursive related resource fields.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Closure
|
||||
*/
|
||||
protected function rejectRecursiveRelatedResourceFields(NovaRequest $request)
|
||||
{
|
||||
return function ($field) use ($request) {
|
||||
if (! $field instanceof RelatableField) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$relatedResource = $field->resourceName == $request->resource;
|
||||
|
||||
return ($this->relationshipType() === 'hasOne' && $field instanceof BelongsTo && $relatedResource) ||
|
||||
($this->relationshipType() === 'morphOne' && $field instanceof MorphTo && $relatedResource);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the field in the modal preview.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function showOnPreview($callback = true)
|
||||
{
|
||||
throw NovaException::helperNotSupported(__METHOD__, __CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should only be shown on the preview modal.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onlyOnPreview()
|
||||
{
|
||||
throw NovaException::helperNotSupported(__METHOD__, __CLASS__);
|
||||
}
|
||||
}
|
||||
253
nova/src/Fields/HasOneThrough.php
Normal file
253
nova/src/Fields/HasOneThrough.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Nova\Contracts\BehavesAsPanel;
|
||||
use Laravel\Nova\Contracts\RelatableField;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Nova;
|
||||
use Laravel\Nova\Panel;
|
||||
|
||||
/**
|
||||
* @method static static make(mixed $name, string|null $attribute = null, string|null $resource = null)
|
||||
*/
|
||||
class HasOneThrough extends Field implements BehavesAsPanel, RelatableField
|
||||
{
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'has-one-through-field';
|
||||
|
||||
/**
|
||||
* The class name of the related resource.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Resource>
|
||||
*/
|
||||
public $resourceClass;
|
||||
|
||||
/**
|
||||
* The URI key of the related resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $resourceName;
|
||||
|
||||
/**
|
||||
* The displayable singular label of the relation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $singularLabel;
|
||||
|
||||
/**
|
||||
* The resolved HasOneThrough Resource.
|
||||
*
|
||||
* @var \Laravel\Nova\Resource|null
|
||||
*/
|
||||
public $hasOneThroughResource;
|
||||
|
||||
/**
|
||||
* The name of the Eloquent "has one through" relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $hasOneThroughRelationship;
|
||||
|
||||
/**
|
||||
* The key of the related Eloquent model.
|
||||
*
|
||||
* @var string|int|null
|
||||
*/
|
||||
public $hasOneThroughId;
|
||||
|
||||
/**
|
||||
* The callback used to determine if the HasOne field has already been filled.
|
||||
*
|
||||
* @var \Closure(\Laravel\Nova\Http\Requests\NovaRequest):bool
|
||||
*/
|
||||
public $filledCallback;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @param class-string<\Laravel\Nova\Resource>|null $resource
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, $resource = null)
|
||||
{
|
||||
parent::__construct($name, $attribute);
|
||||
|
||||
$resource = $resource ?? ResourceRelationshipGuesser::guessResource($name);
|
||||
|
||||
$this->resourceClass = $resource;
|
||||
$this->resourceName = $resource::uriKey();
|
||||
$this->hasOneThroughRelationship = $this->attribute = $attribute ?? ResourceRelationshipGuesser::guessRelation($name);
|
||||
$this->singularLabel = $resource::singularLabel();
|
||||
|
||||
$this->alreadyFilledWhen(function ($request) {
|
||||
$parentResource = Nova::resourceForKey($request->viaResource);
|
||||
|
||||
if ($parentResource && filled($request->viaResourceId)) {
|
||||
$parent = $parentResource::newModel()->find($request->viaResourceId);
|
||||
|
||||
return optional($parent->{$this->attribute})->exists === true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipName()
|
||||
{
|
||||
return $this->hasOneThroughRelationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipType()
|
||||
{
|
||||
return 'hasOneThrough';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field should be displayed for the given request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(Request $request)
|
||||
{
|
||||
return call_user_func(
|
||||
[$this->resourceClass, 'authorizedToViewAny'], $request
|
||||
) && parent::authorize($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function resolve($resource, $attribute = null)
|
||||
{
|
||||
$value = null;
|
||||
|
||||
if ($resource->relationLoaded($this->attribute)) {
|
||||
$value = $resource->getRelation($this->attribute);
|
||||
}
|
||||
|
||||
if (! $value) {
|
||||
$value = $resource->{$this->attribute}()->withoutGlobalScopes()->getResults();
|
||||
}
|
||||
|
||||
if ($value) {
|
||||
$this->alreadyFilledWhen(function () use ($value) {
|
||||
return optional($value)->exists;
|
||||
});
|
||||
|
||||
$this->hasOneThroughResource = new $this->resourceClass($value);
|
||||
|
||||
$this->hasOneThroughId = optional(ID::forResource($this->hasOneThroughResource))->value ?? $value->getKey();
|
||||
|
||||
$this->value = $this->hasOneThroughId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the displayable singular label of the resource.
|
||||
*
|
||||
* @param string $singularLabel
|
||||
* @return $this
|
||||
*/
|
||||
public function singularLabel($singularLabel)
|
||||
{
|
||||
$this->singularLabel = $singularLabel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make current field behaves as panel.
|
||||
*
|
||||
* @return \Laravel\Nova\Panel
|
||||
*/
|
||||
public function asPanel()
|
||||
{
|
||||
return Panel::make($this->name, [$this])
|
||||
->withMeta([
|
||||
'prefixComponent' => true,
|
||||
])->withComponent('relationship-panel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return with(app(NovaRequest::class), function ($request) {
|
||||
return array_merge([
|
||||
'resourceName' => $this->resourceName,
|
||||
'hasOneThroughRelationship' => $this->hasOneThroughRelationship,
|
||||
'relationId' => $this->hasOneThroughId,
|
||||
'hasOneThroughId' => $this->hasOneThroughId,
|
||||
'authorizedToView' => optional($this->hasOneThroughResource)->authorizedToView($request) ?? true,
|
||||
'relationshipType' => $this->relationshipType(),
|
||||
'relatable' => true,
|
||||
'singularLabel' => $this->singularLabel,
|
||||
'alreadyFilled' => $this->alreadyFilled($request),
|
||||
], parent::jsonSerialize());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Closure used to determine if the HasOne field has already been filled.
|
||||
*
|
||||
* @param \Closure(\Laravel\Nova\Http\Requests\NovaRequest):bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function alreadyFilledWhen($callback)
|
||||
{
|
||||
$this->filledCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the HasOne field has alreaady been filled.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function alreadyFilled(NovaRequest $request)
|
||||
{
|
||||
return call_user_func($this->filledCallback, $request) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check showing on index.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return bool
|
||||
*/
|
||||
public function isShownOnIndex(NovaRequest $request, $resource): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
38
nova/src/Fields/HasPreview.php
Normal file
38
nova/src/Fields/HasPreview.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
trait HasPreview
|
||||
{
|
||||
/**
|
||||
* The callback used to retrieve the preview URL.
|
||||
*
|
||||
* @var (callable(mixed, ?string, mixed):(?string))|null
|
||||
*/
|
||||
public $previewUrlCallback;
|
||||
|
||||
/**
|
||||
* Specify the callback that should be used to retrieve the preview URL.
|
||||
*
|
||||
* @param callable(mixed, ?string, mixed):?string $previewUrlCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function preview(callable $previewUrlCallback)
|
||||
{
|
||||
$this->previewUrlCallback = $previewUrlCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the preview URL for the field.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function resolvePreviewUrl()
|
||||
{
|
||||
return is_callable($this->previewUrlCallback)
|
||||
? call_user_func($this->previewUrlCallback, $this->value, $this->getStorageDisk(), $this->resource)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
43
nova/src/Fields/HasSuggestions.php
Normal file
43
nova/src/Fields/HasSuggestions.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait HasSuggestions
|
||||
{
|
||||
/**
|
||||
* The field's suggestions callback.
|
||||
*
|
||||
* @var array|callable
|
||||
*/
|
||||
public $suggestions;
|
||||
|
||||
/**
|
||||
* Set the callback or array to be used to determine the field's suggestions list.
|
||||
*
|
||||
* @param array|callable $suggestions
|
||||
* @return $this
|
||||
*/
|
||||
public function suggestions($suggestions)
|
||||
{
|
||||
$this->suggestions = $suggestions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the display suggestions for the field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array|null
|
||||
*/
|
||||
public function resolveSuggestions(NovaRequest $request)
|
||||
{
|
||||
if (is_callable($this->suggestions)) {
|
||||
return call_user_func($this->suggestions, $request) ?? null;
|
||||
}
|
||||
|
||||
return $this->suggestions;
|
||||
}
|
||||
}
|
||||
38
nova/src/Fields/HasThumbnail.php
Normal file
38
nova/src/Fields/HasThumbnail.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
trait HasThumbnail
|
||||
{
|
||||
/**
|
||||
* The callback used to retrieve the thumbnail URL.
|
||||
*
|
||||
* @var (callable(mixed, string, mixed):?string)|null
|
||||
*/
|
||||
public $thumbnailUrlCallback;
|
||||
|
||||
/**
|
||||
* Specify the callback that should be used to retrieve the thumbnail URL.
|
||||
*
|
||||
* @param callable(mixed, string, mixed):?string $thumbnailUrlCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function thumbnail(callable $thumbnailUrlCallback)
|
||||
{
|
||||
$this->thumbnailUrlCallback = $thumbnailUrlCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the thumbnail URL for the field.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function resolveThumbnailUrl()
|
||||
{
|
||||
return is_callable($this->thumbnailUrlCallback)
|
||||
? call_user_func($this->thumbnailUrlCallback, $this->value, $this->getStorageDisk(), $this->resource)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
45
nova/src/Fields/Heading.php
Normal file
45
nova/src/Fields/Heading.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
/**
|
||||
* @method static static make(mixed $name, string|null $attribute = null, callable|null $resolveCallback = null)
|
||||
*/
|
||||
class Heading extends Field implements Unfillable
|
||||
{
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'heading-field';
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, callable $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback);
|
||||
|
||||
$this->withMeta(['value' => $name]);
|
||||
$this->hideFromIndex();
|
||||
$this->withMeta(['asHtml' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the field as raw HTML using Vue.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function asHtml()
|
||||
{
|
||||
return $this->withMeta(['asHtml' => true]);
|
||||
}
|
||||
}
|
||||
28
nova/src/Fields/Hidden.php
Normal file
28
nova/src/Fields/Hidden.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
class Hidden extends Text
|
||||
{
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'hidden-field';
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|\Closure|callable|object|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, callable $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback);
|
||||
|
||||
$this->onlyOnForms();
|
||||
}
|
||||
}
|
||||
169
nova/src/Fields/ID.php
Normal file
169
nova/src/Fields/ID.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Util;
|
||||
|
||||
/**
|
||||
* @method static static make(mixed $name = null, string|null $attribute = null, callable|null $resolveCallback = null)
|
||||
*/
|
||||
class ID extends Field
|
||||
{
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'id-field';
|
||||
|
||||
/**
|
||||
* The field's resolved pivot value.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $pivotValue = null;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string|null $name
|
||||
* @param string|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name = null, $attribute = null, $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name ?? 'ID', $attribute, $resolveCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new hidden ID field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $attribute
|
||||
* @param callable|null $resolveCallback
|
||||
* @return \Laravel\Nova\Fields\Hidden
|
||||
*/
|
||||
public static function hidden($name = 'ID', $attribute = 'id', callable $resolveCallback = null)
|
||||
{
|
||||
return Hidden::make($name, $attribute, $resolveCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new, resolved ID field for the given resource.
|
||||
*
|
||||
* @param \Laravel\Nova\Resource $resource
|
||||
* @return static|null
|
||||
*/
|
||||
public static function forResource($resource)
|
||||
{
|
||||
$model = $resource->model();
|
||||
|
||||
/** @var static|null $field */
|
||||
$field = transform(
|
||||
$resource->availableFieldsOnIndexOrDetail(app(NovaRequest::class))
|
||||
->whereInstanceOf(self::class)
|
||||
->first(),
|
||||
function ($field) use ($model) {
|
||||
return tap($field)->resolve($model);
|
||||
},
|
||||
function () use ($model) {
|
||||
return ! is_null($model) && $model->exists ? static::forModel($model) : null;
|
||||
}
|
||||
);
|
||||
|
||||
if ($field instanceof static) {
|
||||
return empty($field->value) && $field->nullable !== true ? null : $field;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new, resolved ID field for the given model.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return static
|
||||
*/
|
||||
public static function forModel($model)
|
||||
{
|
||||
return tap(static::make('ID', $model->getKeyName()), function ($field) use ($model) {
|
||||
$value = $model->getKey();
|
||||
|
||||
if (is_int($value) && $value >= 9007199254740991) {
|
||||
$field->asBigInt();
|
||||
}
|
||||
|
||||
$field->resolve($model);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the given attribute from the given resource.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string $attribute
|
||||
* @return mixed
|
||||
*/
|
||||
protected function resolveAttribute($resource, $attribute)
|
||||
{
|
||||
if ($resource instanceof Model) {
|
||||
$pivotAccessor = $this->pivotAccessor ?? 'pivot';
|
||||
|
||||
$pivotValue = $resource->relationLoaded($pivotAccessor)
|
||||
? optional($resource->{$pivotAccessor})->getKey()
|
||||
: null;
|
||||
|
||||
if (is_int($pivotValue) || is_string($pivotValue)) {
|
||||
$this->pivotValue = $pivotValue;
|
||||
}
|
||||
}
|
||||
|
||||
return Util::safeInt(
|
||||
parent::resolveAttribute($resource, $attribute)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a BIGINT ID field as a string for compatibility with JavaScript.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function asBigInt()
|
||||
{
|
||||
$this->resolveCallback = function ($id) {
|
||||
return (string) $id;
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the ID field from the Nova interface but keep it available for operations.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function hide()
|
||||
{
|
||||
$this->showOnIndex = false;
|
||||
$this->showOnDetail = false;
|
||||
$this->showOnCreation = false;
|
||||
$this->showOnUpdate = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), array_filter([
|
||||
'pivotValue' => $this->pivotValue ?? null,
|
||||
]));
|
||||
}
|
||||
}
|
||||
54
nova/src/Fields/Image.php
Normal file
54
nova/src/Fields/Image.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Nova\Contracts\Cover;
|
||||
|
||||
class Image extends File implements Cover
|
||||
{
|
||||
use PresentsImages;
|
||||
|
||||
/**
|
||||
* Indicates if the element should be shown on the index view.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $showOnIndex = true;
|
||||
|
||||
const ASPECT_AUTO = 'aspect-auto';
|
||||
|
||||
const ASPECT_SQUARE = 'aspect-square';
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @param string|null $disk
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest, object, string, string, ?string, ?string):(mixed))|null $storageCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, $disk = null, $storageCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $disk, $storageCallback);
|
||||
|
||||
$this->acceptedTypes('image/*');
|
||||
|
||||
$this->thumbnail(function () {
|
||||
return $this->value ? Storage::disk($this->getStorageDisk())->url($this->value) : null;
|
||||
})->preview(function () {
|
||||
return $this->value ? Storage::disk($this->getStorageDisk())->url($this->value) : null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field element for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), $this->imageAttributes());
|
||||
}
|
||||
}
|
||||
214
nova/src/Fields/KeyValue.php
Normal file
214
nova/src/Fields/KeyValue.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Nova;
|
||||
|
||||
class KeyValue extends Field
|
||||
{
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'key-value-field';
|
||||
|
||||
public function resolve($resource, $attribute = null)
|
||||
{
|
||||
parent::resolve($resource, $attribute);
|
||||
|
||||
if ($this->value === '{}') {
|
||||
$this->value = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the element should be shown on the index view.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $showOnIndex = false;
|
||||
|
||||
/**
|
||||
* The label that should be used for the key heading.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $keyLabel;
|
||||
|
||||
/**
|
||||
* The label that should be used for the value heading.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $valueLabel;
|
||||
|
||||
/**
|
||||
* The label that should be used for the "add row" button.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $actionText;
|
||||
|
||||
/**
|
||||
* The callback used to determine if the keys are readonly.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool|null
|
||||
*/
|
||||
public $readonlyKeysCallback;
|
||||
|
||||
/**
|
||||
* Determine if new rows are able to be added.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $canAddRow = true;
|
||||
|
||||
/**
|
||||
* Determine if rows are able to be deleted.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $canDeleteRow = true;
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
if ($request->exists($requestAttribute)) {
|
||||
// The value for KeyValue fields are serialized on the front-end using `JSON.stringify`,
|
||||
// so we need to convert it to an associative array before saving it to the database.
|
||||
$this->fillModelWithData($model, json_decode($request[$requestAttribute], true), $attribute);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the model's attribute with data.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param mixed $value
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function fillModelWithData($model, $value, string $attribute)
|
||||
{
|
||||
$model->forceFill([Str::replace('.', '->', $attribute) => $value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The label that should be used for the key table heading.
|
||||
*
|
||||
* @param string $label
|
||||
* @return $this
|
||||
*/
|
||||
public function keyLabel($label)
|
||||
{
|
||||
$this->keyLabel = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The label that should be used for the value table heading.
|
||||
*
|
||||
* @param string $label
|
||||
* @return $this
|
||||
*/
|
||||
public function valueLabel($label)
|
||||
{
|
||||
$this->valueLabel = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The label that should be used for the add row button.
|
||||
*
|
||||
* @param string $label
|
||||
* @return $this
|
||||
*/
|
||||
public function actionText($label)
|
||||
{
|
||||
$this->actionText = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback used to determine if the keys are readonly.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function disableEditingKeys($callback = true)
|
||||
{
|
||||
$this->readonlyKeysCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the keys are readonly.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function readonlyKeys(NovaRequest $request)
|
||||
{
|
||||
return with($this->readonlyKeysCallback, function ($callback) use ($request) {
|
||||
return is_callable($callback) ? call_user_func($callback, $request) : ($callback === true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable adding new rows.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function disableAddingRows()
|
||||
{
|
||||
$this->canAddRow = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable deleting rows.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function disableDeletingRows()
|
||||
{
|
||||
$this->canDeleteRow = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field element for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'keyLabel' => $this->keyLabel ?? Nova::__('Key'),
|
||||
'valueLabel' => $this->valueLabel ?? Nova::__('Value'),
|
||||
'actionText' => $this->actionText ?? Nova::__('Add row'),
|
||||
'readonlyKeys' => $this->readonlyKeys(app(NovaRequest::class)),
|
||||
'canAddRow' => $this->canAddRow,
|
||||
'canDeleteRow' => $this->canDeleteRow,
|
||||
]);
|
||||
}
|
||||
}
|
||||
150
nova/src/Fields/Line.php
Normal file
150
nova/src/Fields/Line.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class Line extends Text implements Unfillable
|
||||
{
|
||||
const HEADING = 'extra-large';
|
||||
|
||||
const BASE = 'large';
|
||||
|
||||
const SUBTITLE = 'medium';
|
||||
|
||||
const SMALL = 'small';
|
||||
|
||||
/**
|
||||
* The type for the line field.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type = self::BASE;
|
||||
|
||||
/**
|
||||
* Extra CSS classes to apply to the line.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $extraClasses = '';
|
||||
|
||||
/**
|
||||
* The line's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'line-field';
|
||||
|
||||
/**
|
||||
* CSS class lookup table for lines.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public static $classes = [
|
||||
self::HEADING => 'text-base font-semibold',
|
||||
self::BASE => 'text-sm',
|
||||
self::SUBTITLE => 'text-xs tracking-loose font-bold uppercase text-80',
|
||||
self::SMALL => 'text-xs',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|callable|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, callable $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback);
|
||||
|
||||
$this->exceptOnForms();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the line as a heading.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function asHeading()
|
||||
{
|
||||
$this->type = static::HEADING;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the line as a subtitle.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function asSubTitle()
|
||||
{
|
||||
$this->type = static::SUBTITLE;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the line with small styles.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function asSmall()
|
||||
{
|
||||
$this->type = static::SMALL;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the line with base styles.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function asBase()
|
||||
{
|
||||
$this->type = static::BASE;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the extra CSS classes to be applied to the line field.
|
||||
*
|
||||
* @param mixed $classes
|
||||
* @return $this
|
||||
*/
|
||||
public function extraClasses($classes)
|
||||
{
|
||||
$this->extraClasses = $classes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display classes for the line.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getClasses()
|
||||
{
|
||||
return array_merge(
|
||||
Arr::wrap(self::$classes[$this->type]),
|
||||
array_filter(Arr::wrap($this->extraClasses))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the line for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'classes' => $this->getClasses(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
83
nova/src/Fields/ManyToManyCreationRules.php
Normal file
83
nova/src/Fields/ManyToManyCreationRules.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Rules\NotAttached;
|
||||
use Laravel\Nova\Rules\NotExactlyAttached;
|
||||
|
||||
trait ManyToManyCreationRules
|
||||
{
|
||||
/**
|
||||
* The callback that should be used to set creation rules callback for the pivot actions.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(array))|null
|
||||
*/
|
||||
public $creationRulesCallback;
|
||||
|
||||
/**
|
||||
* Determine if field allow duplicate relations.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $allowDuplicateRelations = false;
|
||||
|
||||
/**
|
||||
* Set creation rules callback for this relation.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(array))|null $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function creationRules($callback = null)
|
||||
{
|
||||
$this->creationRulesCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set allow same relation rules.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function allowDuplicateRelations()
|
||||
{
|
||||
$this->allowDuplicateRelations = true;
|
||||
|
||||
return $this->creationRules(function ($request) {
|
||||
return [
|
||||
new NotExactlyAttached($request, $request->findModelOrFail()),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set disallow same relation rules.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function noDuplicateRelations()
|
||||
{
|
||||
$this->allowDuplicateRelations = false;
|
||||
|
||||
return $this->creationRules(function ($request) {
|
||||
return [
|
||||
new NotAttached($request, $request->findModelOrFail()),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the creation rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array<int, string|\Illuminate\Validation\Rule|\Illuminate\Contracts\Validation\Rule|callable>
|
||||
*/
|
||||
public function getManyToManyCreationRules(NovaRequest $request)
|
||||
{
|
||||
return transform($this->creationRulesCallback, function ($callback) use ($request) {
|
||||
return Arr::wrap(call_user_func($callback, $request));
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
135
nova/src/Fields/Markdown.php
Normal file
135
nova/src/Fields/Markdown.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Nova\Contracts\Deletable as DeletableContract;
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Contracts\Previewable;
|
||||
use Laravel\Nova\Contracts\Storable as StorableContract;
|
||||
use Laravel\Nova\Fields\Filters\TextFilter;
|
||||
use Laravel\Nova\Fields\Markdown\CommonMarkPreset;
|
||||
use Laravel\Nova\Fields\Markdown\DefaultPreset;
|
||||
use Laravel\Nova\Fields\Markdown\ZeroPreset;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\ManagesPresets;
|
||||
|
||||
class Markdown extends Field implements DeletableContract, FilterableField, Previewable, StorableContract
|
||||
{
|
||||
use Expandable;
|
||||
use FieldFilterable;
|
||||
use HasAttachments;
|
||||
use ManagesPresets;
|
||||
use Storable;
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'markdown-field';
|
||||
|
||||
/**
|
||||
* Indicates if the element should be shown on the index view.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $showOnIndex = false;
|
||||
|
||||
/**
|
||||
* The built-in presets for the Markdown field.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $presets = [
|
||||
'default' => DefaultPreset::class,
|
||||
'commonmark' => CommonMarkPreset::class,
|
||||
'zero' => ZeroPreset::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return void|\Closure
|
||||
*/
|
||||
protected function fillAttribute(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
return $this->fillAttributeWithAttachment($request, $requestAttribute, $model, $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path that the field is stored at on disk.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getStoragePath()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
return new TextFilter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return transform($this->jsonSerialize(), function ($field) {
|
||||
return Arr::only($field, [
|
||||
'uniqueKey',
|
||||
'name',
|
||||
'attribute',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a preview for the given field value.
|
||||
*
|
||||
* @param string|null $value
|
||||
* @return string
|
||||
*/
|
||||
public function previewFor($value)
|
||||
{
|
||||
return $this->renderer()->convert($value ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Laravel\Nova\Fields\Markdown\MarkdownPreset
|
||||
*/
|
||||
public function renderer()
|
||||
{
|
||||
return new $this->presets[$this->preset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the element for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'shouldShow' => $this->shouldBeExpanded(),
|
||||
'preset' => $this->preset,
|
||||
'previewFor' => $this->previewFor($this->value ?? ''),
|
||||
'withFiles' => $this->withFiles,
|
||||
]);
|
||||
}
|
||||
}
|
||||
19
nova/src/Fields/Markdown/CommonMarkPreset.php
Normal file
19
nova/src/Fields/Markdown/CommonMarkPreset.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Markdown;
|
||||
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
|
||||
class CommonMarkPreset implements MarkdownPreset
|
||||
{
|
||||
/**
|
||||
* Convert the given content from markdown to HTML.
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
public function convert(string $content)
|
||||
{
|
||||
return (string) (new CommonMarkConverter())->convert($content);
|
||||
}
|
||||
}
|
||||
19
nova/src/Fields/Markdown/DefaultPreset.php
Normal file
19
nova/src/Fields/Markdown/DefaultPreset.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Markdown;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DefaultPreset implements MarkdownPreset
|
||||
{
|
||||
/**
|
||||
* Convert the given content from markdown to HTML.
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
public function convert(string $content)
|
||||
{
|
||||
return Str::markdown($content);
|
||||
}
|
||||
}
|
||||
14
nova/src/Fields/Markdown/MarkdownPreset.php
Normal file
14
nova/src/Fields/Markdown/MarkdownPreset.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Markdown;
|
||||
|
||||
interface MarkdownPreset
|
||||
{
|
||||
/**
|
||||
* Convert the given content from markdown to HTML.
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
public function convert(string $content);
|
||||
}
|
||||
19
nova/src/Fields/Markdown/ZeroPreset.php
Normal file
19
nova/src/Fields/Markdown/ZeroPreset.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Markdown;
|
||||
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
|
||||
class ZeroPreset implements MarkdownPreset
|
||||
{
|
||||
/**
|
||||
* Convert the given content from markdown to HTML.
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
public function convert(string $content)
|
||||
{
|
||||
return (string) (new CommonMarkConverter(['html_input' => 'strip']))->convert($content);
|
||||
}
|
||||
}
|
||||
16
nova/src/Fields/MorphMany.php
Normal file
16
nova/src/Fields/MorphMany.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
class MorphMany extends HasMany
|
||||
{
|
||||
/**
|
||||
* Get the relationship type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipType()
|
||||
{
|
||||
return 'morphMany';
|
||||
}
|
||||
}
|
||||
16
nova/src/Fields/MorphOne.php
Normal file
16
nova/src/Fields/MorphOne.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
class MorphOne extends HasOne
|
||||
{
|
||||
/**
|
||||
* Get the relationship type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipType()
|
||||
{
|
||||
return 'morphOne';
|
||||
}
|
||||
}
|
||||
722
nova/src/Fields/MorphTo.php
Normal file
722
nova/src/Fields/MorphTo.php
Normal file
@@ -0,0 +1,722 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Contracts\QueryBuilder;
|
||||
use Laravel\Nova\Contracts\RelatableField;
|
||||
use Laravel\Nova\Fields\Filters\MorphToFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Http\Requests\ResourceIndexRequest;
|
||||
use Laravel\Nova\Nova;
|
||||
use Laravel\Nova\Resource;
|
||||
use Laravel\Nova\Rules\Relatable;
|
||||
use Laravel\Nova\TrashedStatus;
|
||||
use Laravel\Nova\Util;
|
||||
|
||||
/**
|
||||
* @method static static make(mixed $name, string|null $attribute = null)
|
||||
*/
|
||||
class MorphTo extends Field implements FilterableField, RelatableField
|
||||
{
|
||||
use AssociatableRelation;
|
||||
use DeterminesIfCreateRelationCanBeShown;
|
||||
use EloquentFilterable;
|
||||
use Peekable;
|
||||
use ResolvesReverseRelation;
|
||||
use Searchable;
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'morph-to-field';
|
||||
|
||||
/**
|
||||
* The class name of the related resource.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Resource>|null
|
||||
*/
|
||||
public $resourceClass;
|
||||
|
||||
/**
|
||||
* The URI key of the related resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $resourceName;
|
||||
|
||||
/**
|
||||
* The resolved MorphTo Resource.
|
||||
*
|
||||
* @var \Laravel\Nova\Resource|null
|
||||
*/
|
||||
public $morphToResource;
|
||||
|
||||
/**
|
||||
* The name of the Eloquent "morph to" relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $morphToRelationship;
|
||||
|
||||
/**
|
||||
* The key of the related Eloquent model.
|
||||
*
|
||||
* @var string|int|null
|
||||
*/
|
||||
public $morphToId;
|
||||
|
||||
/**
|
||||
* The type of the related Eloquent model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $morphToType;
|
||||
|
||||
/**
|
||||
* The types of resources that may be polymorphically related to this resource.
|
||||
*
|
||||
* @var array<array-key, array<string, mixed>>
|
||||
*/
|
||||
public $morphToTypes = [];
|
||||
|
||||
/**
|
||||
* The column that should be displayed for the field.
|
||||
*
|
||||
* @var \Closure|array<class-string<\Laravel\Nova\Resource>, callable>|string
|
||||
*/
|
||||
public $display;
|
||||
|
||||
/**
|
||||
* Indicates if the related resource can be viewed.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $viewable;
|
||||
|
||||
/**
|
||||
* The attribute that is the inverse of this relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $inverse;
|
||||
|
||||
/**
|
||||
* Indicates whether the field should display the "With Trashed" option.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $displaysWithTrashed = true;
|
||||
|
||||
/**
|
||||
* The default related class value for the field.
|
||||
*
|
||||
* @var (\Closure(\Laravel\Nova\Http\Requests\NovaRequest):(class-string<\Laravel\Nova\Resource>))|class-string<\Laravel\Nova\Resource>
|
||||
*/
|
||||
public $defaultResourceCallable;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null)
|
||||
{
|
||||
parent::__construct($name, $attribute);
|
||||
|
||||
$this->morphToRelationship = $this->attribute = $attribute ?? ResourceRelationshipGuesser::guessRelation($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipName()
|
||||
{
|
||||
return $this->morphToRelationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipType()
|
||||
{
|
||||
return 'morphTo';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field should be displayed for the given request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request&\Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(Request $request)
|
||||
{
|
||||
if (! $this->isNotRedundant($request)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::authorize($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field is not redundant.
|
||||
*
|
||||
* See: Explanation on belongsTo field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function isNotRedundant(NovaRequest $request)
|
||||
{
|
||||
return ! $request instanceof ResourceIndexRequest || ! $this->isReverseRelation($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function resolve($resource, $attribute = null)
|
||||
{
|
||||
$value = null;
|
||||
|
||||
if ($resource->relationLoaded($this->attribute)) {
|
||||
$value = $resource->getRelation($this->attribute);
|
||||
}
|
||||
|
||||
if (! $value) {
|
||||
$value = $resource->{$this->attribute}()->withoutGlobalScopes()->getResults();
|
||||
}
|
||||
|
||||
[$this->morphToId, $this->morphToType] = [
|
||||
optional($value)->getKey(),
|
||||
$this->resolveMorphType($resource),
|
||||
];
|
||||
|
||||
if ($resourceClass = $this->resolveResourceClass($value)) {
|
||||
$this->resourceName = $resourceClass::uriKey();
|
||||
}
|
||||
|
||||
if ($value) {
|
||||
if (! is_string($this->resourceClass)) {
|
||||
$this->morphToType = $value->getMorphClass();
|
||||
$this->value = (string) $value->getKey();
|
||||
|
||||
if ($this->value != $value->getKey()) {
|
||||
$this->morphToId = (string) $this->morphToId;
|
||||
}
|
||||
|
||||
$this->viewable = false;
|
||||
} else {
|
||||
$this->morphToResource = new $this->resourceClass($value);
|
||||
|
||||
$this->morphToId = Util::safeInt($this->morphToId);
|
||||
|
||||
$this->value = $this->formatDisplayValue(
|
||||
$value, Nova::resourceForModel($value)
|
||||
);
|
||||
|
||||
$this->viewable = ($this->viewable ?? true) && $this->morphToResource->authorizedToView(app(NovaRequest::class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve dependent field value.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function resolveDependentValue(NovaRequest $request)
|
||||
{
|
||||
return $this->morphToId ?? $this->resolveDefaultValue($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value for display.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function resolveForDisplay($resource, $attribute = null)
|
||||
{
|
||||
$this->resolve($resource, $attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the current resource key for the resource's morph type.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @return string|null
|
||||
*/
|
||||
protected function resolveMorphType($resource)
|
||||
{
|
||||
if (! $type = optional($resource->{$this->attribute}())->getMorphType()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $resource->{$type};
|
||||
|
||||
if ($morphResource = Nova::resourceForModel(Relation::getMorphedModel($value) ?? $value)) {
|
||||
return $morphResource::uriKey();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the resource class for the field.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return string|null
|
||||
*/
|
||||
protected function resolveResourceClass($model)
|
||||
{
|
||||
return $this->resourceClass = Nova::resourceForModel($model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array
|
||||
*/
|
||||
public function getRules(NovaRequest $request)
|
||||
{
|
||||
$possibleTypes = collect($this->morphToTypes)->map->value->values();
|
||||
|
||||
return array_merge_recursive(parent::getRules($request), [
|
||||
$this->attribute.'_type' => [$this->nullable ? 'nullable' : 'required', 'in:'.$possibleTypes->implode(',')],
|
||||
$this->attribute => array_filter([$this->nullable ? 'nullable' : 'required', $this->getRelatableRule($request)]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rule to verify that the selected model is relatable.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Rules\Relatable|null
|
||||
*/
|
||||
protected function getRelatableRule(NovaRequest $request)
|
||||
{
|
||||
if ($relatedResource = Nova::resourceForKey($request->{$this->attribute.'_type'})) {
|
||||
return new Relatable($request, $this->buildMorphableQuery(
|
||||
$request, $relatedResource, $request->{$this->attribute.'_trashed'} === 'true'
|
||||
)->toBase());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @return void
|
||||
*/
|
||||
public function fill(NovaRequest $request, $model)
|
||||
{
|
||||
$instance = Nova::modelInstanceForKey($request->{$this->attribute.'_type'});
|
||||
|
||||
$morphType = $model->{$this->attribute}()->getMorphType();
|
||||
|
||||
$model->{$morphType} = ! is_null($instance)
|
||||
? $this->getMorphAliasForClass(get_class($instance))
|
||||
: null;
|
||||
|
||||
$foreignKey = $this->getRelationForeignKeyName($model->{$this->attribute}());
|
||||
|
||||
if ($model->isDirty([$morphType, $foreignKey])) {
|
||||
$model->unsetRelation($this->attribute);
|
||||
}
|
||||
|
||||
parent::fillInto($request, $model, $foreignKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @return mixed
|
||||
*/
|
||||
public function fillForAction(NovaRequest $request, $model)
|
||||
{
|
||||
if ($request->exists($this->attribute)) {
|
||||
$value = $request[$this->attribute];
|
||||
|
||||
$instance = Nova::modelInstanceForKey($request->{$this->attribute.'_type'});
|
||||
|
||||
$model->{$this->attribute} = $instance->query()->find($value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the morph type alias for the given class.
|
||||
*
|
||||
* @param string $class
|
||||
* @return string
|
||||
*/
|
||||
protected function getMorphAliasForClass($class)
|
||||
{
|
||||
foreach (Relation::$morphMap as $alias => $model) {
|
||||
if ($model == $class) {
|
||||
return $alias;
|
||||
}
|
||||
}
|
||||
|
||||
return $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the morphable query for the field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $relatedResource
|
||||
* @param bool $withTrashed
|
||||
* @return \Laravel\Nova\Contracts\QueryBuilder
|
||||
*/
|
||||
public function buildMorphableQuery(NovaRequest $request, $relatedResource, $withTrashed = false)
|
||||
{
|
||||
$model = $relatedResource::newModel();
|
||||
|
||||
$query = app()->make(QueryBuilder::class, [$relatedResource]);
|
||||
|
||||
$request->first === 'true'
|
||||
? $query->whereKey($model->newQueryWithoutScopes(), $request->current)
|
||||
: $query->search(
|
||||
$request, $model->newQuery(), $request->search,
|
||||
[], [], TrashedStatus::fromBoolean($withTrashed)
|
||||
);
|
||||
|
||||
return $query->tap(function ($query) use ($request, $relatedResource, $model) {
|
||||
if (is_callable($this->relatableQueryCallback)) {
|
||||
call_user_func($this->relatableQueryCallback, $request, $query);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
forward_static_call(
|
||||
$this->morphableQueryCallable($request, $relatedResource, $model),
|
||||
$request, $query, $this
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the morphable query method name.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $relatedResource
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return array
|
||||
*/
|
||||
protected function morphableQueryCallable(NovaRequest $request, $relatedResource, $model)
|
||||
{
|
||||
return ($method = $this->morphableQueryMethod($request, $model))
|
||||
? [$request->resource(), $method]
|
||||
: [$relatedResource, 'relatableQuery'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the morphable query method name.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return string
|
||||
*/
|
||||
protected function morphableQueryMethod(NovaRequest $request, $model)
|
||||
{
|
||||
$method = 'relatable'.Str::plural(class_basename($model));
|
||||
|
||||
return method_exists($request->resource(), $method) ? $method : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given morphable resource.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @param string $relatedResource
|
||||
* @return array
|
||||
*/
|
||||
public function formatMorphableResource(NovaRequest $request, $resource, $relatedResource)
|
||||
{
|
||||
return array_filter([
|
||||
'avatar' => $resource->resolveAvatarUrl($request),
|
||||
'display' => $this->formatDisplayValue($resource, $relatedResource),
|
||||
'subtitle' => $resource->subtitle(),
|
||||
'value' => Util::safeInt($resource->getKey()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the associatable display value.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string $relatedResource
|
||||
* @return string
|
||||
*/
|
||||
protected function formatDisplayValue($resource, $relatedResource)
|
||||
{
|
||||
if (! $resource instanceof Resource) {
|
||||
$resource = Nova::newResourceFromModel($resource);
|
||||
}
|
||||
|
||||
if ($display = $this->displayFor($relatedResource)) {
|
||||
return call_user_func($display, $resource);
|
||||
}
|
||||
|
||||
return (string) $resource->title();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the types of resources that may be related to the resource.
|
||||
*
|
||||
* @param array<int, class-string<\Laravel\Nova\Resource>>|array<class-string<\Laravel\Nova\Resource>, string> $types
|
||||
* @return $this
|
||||
*/
|
||||
public function types(array $types)
|
||||
{
|
||||
$this->morphToTypes = collect($types)->map(function ($display, $key) {
|
||||
return [
|
||||
'type' => is_numeric($key) ? $display : $key,
|
||||
'singularLabel' => is_numeric($key) ? $display::singularLabel() : $key::singularLabel(),
|
||||
'display' => (is_string($display) && is_numeric($key)) ? $display::singularLabel() : $display,
|
||||
'value' => is_numeric($key) ? $display::uriKey() : $key::uriKey(),
|
||||
];
|
||||
})->values()->all();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the column that should be displayed for the field.
|
||||
*
|
||||
* @param \Closure|array<class-string<\Laravel\Nova\Resource>, callable>|string $display
|
||||
* @return $this
|
||||
*/
|
||||
public function display($display)
|
||||
{
|
||||
if (is_array($display)) {
|
||||
$this->display = collect($display)->mapWithKeys(function ($display, $type) {
|
||||
return [$type => $this->ensureDisplayerIsClosure($display)];
|
||||
})->all();
|
||||
} else {
|
||||
$this->display = $this->ensureDisplayerIsClosure($display);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the given displayer is a Closure.
|
||||
*
|
||||
* @param \Closure|string $display
|
||||
* @return \Closure
|
||||
*/
|
||||
protected function ensureDisplayerIsClosure($display)
|
||||
{
|
||||
return $display instanceof Closure
|
||||
? $display
|
||||
: function ($resource) use ($display) {
|
||||
return $resource->{$display};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column that should be displayed for a given type.
|
||||
*
|
||||
* @param string $type
|
||||
* @return \Closure|null
|
||||
*/
|
||||
public function displayFor($type)
|
||||
{
|
||||
if (is_array($this->display) && $type) {
|
||||
return $this->display[$type] ?? null;
|
||||
}
|
||||
|
||||
return $this->display;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify if the related resource can be viewed.
|
||||
*
|
||||
* @param bool $value
|
||||
* @return $this
|
||||
*/
|
||||
public function viewable($value = true)
|
||||
{
|
||||
$this->viewable = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the attribute name of the inverse of the relationship.
|
||||
*
|
||||
* @param string $inverse
|
||||
* @return $this
|
||||
*/
|
||||
public function inverse($inverse)
|
||||
{
|
||||
$this->inverse = $inverse;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* hides the "With Trashed" option.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withoutTrashed()
|
||||
{
|
||||
$this->displaysWithTrashed = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default relation resource class to be selected.
|
||||
*
|
||||
* @param (\Closure(\Laravel\Nova\Http\Requests\NovaRequest):(class-string<\Laravel\Nova\Resource>))|class-string<\Laravel\Nova\Resource> $resourceClass
|
||||
* @return $this
|
||||
*/
|
||||
public function defaultResource($resourceClass)
|
||||
{
|
||||
$this->defaultResourceCallable = $resourceClass;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the default resource class for the field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return string|void
|
||||
*/
|
||||
protected function resolveDefaultResource(NovaRequest $request)
|
||||
{
|
||||
if ($request->isCreateOrAttachRequest() || $request->isResourceIndexRequest() || $request->isActionRequest()) {
|
||||
if (is_null($this->value) && $this->defaultResourceCallable instanceof Closure) {
|
||||
$class = call_user_func($this->defaultResourceCallable, $request);
|
||||
} else {
|
||||
$class = $this->defaultResourceCallable;
|
||||
}
|
||||
|
||||
if (! empty($class) && class_exists($class)) {
|
||||
return $class::uriKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter|null
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
return new MorphToFilter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define filterable attribute.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return string
|
||||
*/
|
||||
protected function filterableAttribute(NovaRequest $request)
|
||||
{
|
||||
return $this->morphToRelationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default filterable callback.
|
||||
*
|
||||
* @return callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):void
|
||||
*/
|
||||
protected function defaultFilterableCallback()
|
||||
{
|
||||
$morphToTypes = collect($this->morphToTypes)
|
||||
->pluck('type')
|
||||
->mapWithKeys(function ($type) {
|
||||
return [$type => $type::newModel()->getMorphClass()];
|
||||
})->all();
|
||||
|
||||
return function (NovaRequest $request, $query, $value, $attribute) use ($morphToTypes) {
|
||||
$query->whereHasMorph(
|
||||
$attribute,
|
||||
! empty($value) && isset($morphToTypes[$value]) ? $morphToTypes[$value] : $morphToTypes
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return transform($this->jsonSerialize(), function ($field) {
|
||||
return [
|
||||
'resourceName' => $field['resourceName'],
|
||||
'morphToTypes' => $field['morphToTypes'],
|
||||
'uniqueKey' => $field['uniqueKey'],
|
||||
'relationshipType' => $field['relationshipType'],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$resourceClass = $this->resourceClass;
|
||||
|
||||
return with(app(NovaRequest::class), function ($request) use ($resourceClass) {
|
||||
$viewable = ! is_null($this->viewable)
|
||||
? $this->viewable
|
||||
: (! is_null($resourceClass) ? $resourceClass::authorizedToViewAny($request) : true);
|
||||
|
||||
return array_merge([
|
||||
'debounce' => $this->debounce,
|
||||
'morphToRelationship' => $this->morphToRelationship,
|
||||
'relationshipType' => $this->relationshipType(),
|
||||
'morphToType' => $this->morphToType,
|
||||
'morphToId' => $this->morphToId,
|
||||
'morphToTypes' => $this->morphToTypes,
|
||||
'peekable' => $this->isPeekable($request),
|
||||
'hasFieldsToPeekAt' => $this->hasFieldsToPeekAt($request),
|
||||
'resourceLabel' => $resourceClass ? $resourceClass::singularLabel() : null,
|
||||
'resourceName' => $this->resourceName,
|
||||
'reverse' => $this->isReverseRelation($request),
|
||||
'searchable' => $this->searchable,
|
||||
'withSubtitles' => $this->withSubtitles,
|
||||
'showCreateRelationButton' => $this->createRelationShouldBeShown($request),
|
||||
'displaysWithTrashed' => $this->displaysWithTrashed,
|
||||
'viewable' => $viewable,
|
||||
'defaultResource' => $this->resolveDefaultResource($request),
|
||||
], parent::jsonSerialize());
|
||||
});
|
||||
}
|
||||
}
|
||||
48
nova/src/Fields/MorphToActionTarget.php
Normal file
48
nova/src/Fields/MorphToActionTarget.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class MorphToActionTarget extends MorphTo
|
||||
{
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'morph-to-action-target-field';
|
||||
|
||||
/**
|
||||
* Determine if the field is not redundant.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function isNotRedundant(NovaRequest $request)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function resolve($resource, $attribute = null)
|
||||
{
|
||||
parent::resolve($resource, $attribute);
|
||||
|
||||
if (empty($this->value)) {
|
||||
$morphToType = $resource->getAttribute("{$this->attribute}_type");
|
||||
$morphToId = $resource->getAttribute("{$this->attribute}_id");
|
||||
|
||||
$this->morphToType = Relation::getMorphedModel($morphToType) ?? $morphToType;
|
||||
$this->morphToId = $this->value = (string) $morphToId;
|
||||
$this->viewable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
351
nova/src/Fields/MorphToMany.php
Normal file
351
nova/src/Fields/MorphToMany.php
Normal file
@@ -0,0 +1,351 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Contracts\Deletable as DeletableContract;
|
||||
use Laravel\Nova\Contracts\ListableField;
|
||||
use Laravel\Nova\Contracts\PivotableField;
|
||||
use Laravel\Nova\Contracts\QueryBuilder;
|
||||
use Laravel\Nova\Contracts\RelatableField;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Panel;
|
||||
use Laravel\Nova\Rules\RelatableAttachment;
|
||||
use Laravel\Nova\TrashedStatus;
|
||||
|
||||
/**
|
||||
* @method static static make(mixed $name, string|null $attribute = null, string|null $resource = null)
|
||||
*/
|
||||
class MorphToMany extends Field implements DeletableContract, ListableField, PivotableField, RelatableField
|
||||
{
|
||||
use AttachableRelation;
|
||||
use Collapsable;
|
||||
use Deletable;
|
||||
use DetachesPivotModels;
|
||||
use DeterminesIfCreateRelationCanBeShown;
|
||||
use FormatsRelatableDisplayValues;
|
||||
use ManyToManyCreationRules;
|
||||
use Searchable;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'morph-to-many-field';
|
||||
|
||||
/**
|
||||
* The class name of the related resource.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Resource>
|
||||
*/
|
||||
public $resourceClass;
|
||||
|
||||
/**
|
||||
* The URI key of the related resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $resourceName;
|
||||
|
||||
/**
|
||||
* The name of the Eloquent "morph to many" relationship.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $manyToManyRelationship;
|
||||
|
||||
/**
|
||||
* The callback that should be used to resolve the pivot fields.
|
||||
*
|
||||
* @var callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Model):array<int, \Laravel\Nova\Fields\Field>
|
||||
*/
|
||||
public $fieldsCallback;
|
||||
|
||||
/**
|
||||
* The callback that should be used to resolve the pivot actions.
|
||||
*
|
||||
* @var callable(\Laravel\Nova\Http\Requests\NovaRequest):array<int, \Laravel\Nova\Actions\Action>
|
||||
*/
|
||||
public $actionsCallback;
|
||||
|
||||
/**
|
||||
* The displayable name that should be used to refer to the pivot class.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $pivotName;
|
||||
|
||||
/**
|
||||
* The displayable singular label of the relation.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $singularLabel;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @param class-string<\Laravel\Nova\Resource>|null $resource
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, $resource = null)
|
||||
{
|
||||
parent::__construct($name, $attribute);
|
||||
|
||||
$resource = $resource ?? ResourceRelationshipGuesser::guessResource($name);
|
||||
|
||||
$this->resourceClass = $resource;
|
||||
$this->resourceName = $resource::uriKey();
|
||||
$this->manyToManyRelationship = $this->attribute;
|
||||
$this->deleteCallback = $this->detachmentCallback();
|
||||
|
||||
$this->fieldsCallback = function () {
|
||||
return [];
|
||||
};
|
||||
|
||||
$this->actionsCallback = function () {
|
||||
return [];
|
||||
};
|
||||
|
||||
$this->noDuplicateRelations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipName()
|
||||
{
|
||||
return $this->manyToManyRelationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function relationshipType()
|
||||
{
|
||||
return 'morphToMany';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field should be displayed for the given request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(Request $request)
|
||||
{
|
||||
return call_user_func(
|
||||
[$this->resourceClass, 'authorizedToViewAny'], $request
|
||||
) && parent::authorize($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the field's value.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string|null $attribute
|
||||
* @return void
|
||||
*/
|
||||
public function resolve($resource, $attribute = null)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array
|
||||
*/
|
||||
public function getRules(NovaRequest $request)
|
||||
{
|
||||
$withTrashed = $request->{$this->attribute.'_trashed'} === 'true';
|
||||
|
||||
return array_merge_recursive(parent::getRules($request), [
|
||||
$this->attribute => array_filter([
|
||||
'required', new RelatableAttachment($request, $this->buildAttachableQuery($request, $withTrashed)->toBase()),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the creation rules for this field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array<string, array<int, string|\Illuminate\Validation\Rule|\Illuminate\Contracts\Validation\Rule|callable>>
|
||||
*/
|
||||
public function getCreationRules(NovaRequest $request)
|
||||
{
|
||||
return array_merge_recursive(parent::getCreationRules($request), [
|
||||
$this->attribute => array_filter($this->getManyToManyCreationRules($request)),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an attachable query for the field.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param bool $withTrashed
|
||||
* @return \Laravel\Nova\Contracts\QueryBuilder
|
||||
*/
|
||||
public function buildAttachableQuery(NovaRequest $request, $withTrashed = false)
|
||||
{
|
||||
$model = forward_static_call([$resourceClass = $this->resourceClass, 'newModel']);
|
||||
|
||||
$query = app()->make(QueryBuilder::class, [$resourceClass]);
|
||||
|
||||
$request->first === 'true'
|
||||
? $query->whereKey($model->newQueryWithoutScopes(), $request->current)
|
||||
: $query->search(
|
||||
$request, $model->newQuery(), $request->search,
|
||||
[], [], TrashedStatus::fromBoolean($withTrashed)
|
||||
);
|
||||
|
||||
return $query->tap(function ($query) use ($request, $model) {
|
||||
forward_static_call($this->attachableQueryCallable($request, $model), $request, $query, $this);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachable query method name.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return array
|
||||
*/
|
||||
protected function attachableQueryCallable(NovaRequest $request, $model)
|
||||
{
|
||||
return ($method = $this->attachableQueryMethod($request, $model))
|
||||
? [$request->resource(), $method]
|
||||
: [$this->resourceClass, 'relatableQuery'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachable query method name.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return string|null
|
||||
*/
|
||||
protected function attachableQueryMethod(NovaRequest $request, $model)
|
||||
{
|
||||
$method = 'relatable'.Str::plural(class_basename($model));
|
||||
|
||||
if (method_exists($request->resource(), $method)) {
|
||||
return $method;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given attachable resource.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return array
|
||||
*/
|
||||
public function formatAttachableResource(NovaRequest $request, $resource)
|
||||
{
|
||||
return array_filter([
|
||||
'avatar' => $resource->resolveAvatarUrl($request),
|
||||
'display' => $this->formatDisplayValue($resource),
|
||||
'value' => optional(ID::forResource($resource))->value ?? $resource->getKey(),
|
||||
'subtitle' => $resource->subtitle(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback to be executed to retrieve the pivot fields.
|
||||
*
|
||||
* @param callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Model):array<int, \Laravel\Nova\Fields\Field> $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function fields($callback)
|
||||
{
|
||||
$this->fieldsCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback to be executed to retrieve the pivot actions.
|
||||
*
|
||||
* @param callable(\Laravel\Nova\Http\Requests\NovaRequest):array<int, \Laravel\Nova\Actions\Action> $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function actions($callback)
|
||||
{
|
||||
$this->actionsCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the displayable name that should be used to refer to the pivot class.
|
||||
*
|
||||
* @param string $pivotName
|
||||
* @return $this
|
||||
*/
|
||||
public function referToPivotAs($pivotName)
|
||||
{
|
||||
$this->pivotName = $pivotName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the displayable singular label of the resource.
|
||||
*
|
||||
* @param string $singularLabel
|
||||
* @return $this
|
||||
*/
|
||||
public function singularLabel($singularLabel)
|
||||
{
|
||||
$this->singularLabel = $singularLabel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make current field behaves as panel.
|
||||
*
|
||||
* @return \Laravel\Nova\Panel
|
||||
*/
|
||||
public function asPanel()
|
||||
{
|
||||
return Panel::make($this->name, [$this])
|
||||
->withMeta([
|
||||
'prefixComponent' => true,
|
||||
])->withComponent('relationship-panel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge([
|
||||
'collapsable' => $this->collapsable,
|
||||
'collapsedByDefault' => $this->collapsedByDefault,
|
||||
'debounce' => $this->debounce,
|
||||
'relatable' => true,
|
||||
'morphToManyRelationship' => $this->manyToManyRelationship,
|
||||
'relationshipType' => $this->relationshipType(),
|
||||
'perPage' => $this->resourceClass::$perPageViaRelationship,
|
||||
'resourceName' => $this->resourceName,
|
||||
'searchable' => $this->searchable,
|
||||
'withSubtitles' => $this->withSubtitles,
|
||||
'singularLabel' => $this->singularLabel ?? $this->resourceClass::singularLabel(),
|
||||
'showCreateRelationButton' => $this->createRelationShouldBeShown(app(NovaRequest::class)),
|
||||
], parent::jsonSerialize());
|
||||
}
|
||||
}
|
||||
8
nova/src/Fields/MorphedByMany.php
Normal file
8
nova/src/Fields/MorphedByMany.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
class MorphedByMany extends MorphToMany
|
||||
{
|
||||
//
|
||||
}
|
||||
173
nova/src/Fields/MultiSelect.php
Normal file
173
nova/src/Fields/MultiSelect.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Nova\Contracts\FilterableField;
|
||||
use Laravel\Nova\Fields\Filters\MultiSelectFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Util;
|
||||
|
||||
/**
|
||||
* @phpstan-type TOptionLabel \Stringable|string|array{label: string, group?: string}
|
||||
* @phpstan-type TOptionValue string|int
|
||||
* @phpstan-type TOption iterable<TOptionValue, TOptionLabel>
|
||||
*/
|
||||
class MultiSelect extends Field implements FilterableField
|
||||
{
|
||||
use FieldFilterable;
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'multi-select-field';
|
||||
|
||||
/**
|
||||
* The field's options callback.
|
||||
*
|
||||
* @var array<string|int, array<string, mixed>|string>|\Closure|callable|\Illuminate\Support\Collection|null
|
||||
*
|
||||
* @phpstan-var TOption|(callable(): (TOption))|(\Closure(): (TOption))|null
|
||||
*/
|
||||
public $optionsCallback;
|
||||
|
||||
/**
|
||||
* Set display using label for the field.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $displayUsingLabel = false;
|
||||
|
||||
/**
|
||||
* Set the options for the select menu.
|
||||
*
|
||||
* @param array<string|int, array<string, mixed>|string>|\Closure|callable|\Illuminate\Support\Collection $options
|
||||
* @return $this
|
||||
*
|
||||
* @phpstan-param TOption|(callable(): (TOption))|(\Closure(): (TOption)) $options
|
||||
*/
|
||||
public function options($options)
|
||||
{
|
||||
$this->optionsCallback = $options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display values using their corresponding specified labels.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function displayUsingLabels()
|
||||
{
|
||||
$this->displayUsingLabel = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
if ($request->exists($requestAttribute)) {
|
||||
$value = $request[$requestAttribute];
|
||||
|
||||
$model->{$attribute} = $this->isValidNullValue($value) ? null : json_decode($value, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
return new MultiSelectFilter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default filterable callback.
|
||||
*
|
||||
* @return callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):\Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function defaultFilterableCallback()
|
||||
{
|
||||
return function (NovaRequest $request, $query, $value, $attribute) {
|
||||
return $query->whereJsonContains($attribute, $value);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return transform($this->jsonSerialize(), function ($field) {
|
||||
return Arr::only($field, [
|
||||
'uniqueKey',
|
||||
'name',
|
||||
'attribute',
|
||||
'options',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize options for the field.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*
|
||||
* @phpstan-return array<int, array{group: string, label: string, value: TOptionValue}>
|
||||
*/
|
||||
protected function serializeOptions()
|
||||
{
|
||||
/** @var TOption $options */
|
||||
$options = value($this->optionsCallback);
|
||||
|
||||
if (is_callable($options)) {
|
||||
$options = $options();
|
||||
}
|
||||
|
||||
return collect($options ?? [])->map(function ($label, $value) {
|
||||
$value = Util::safeInt($value);
|
||||
|
||||
return is_array($label) ? $label + ['value' => $value] : ['label' => $label, 'value' => $value];
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$this->withMeta([
|
||||
'options' => $options = $this->serializeOptions(),
|
||||
]);
|
||||
|
||||
if ($this->displayUsingLabel === true) {
|
||||
$this->displayUsing(function ($value) use ($options) {
|
||||
return collect($options)
|
||||
->where('value', $value)
|
||||
->first()['label'] ?? $value;
|
||||
});
|
||||
}
|
||||
|
||||
return parent::jsonSerialize();
|
||||
}
|
||||
}
|
||||
159
nova/src/Fields/Number.php
Normal file
159
nova/src/Fields/Number.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Nova\Fields\Filters\NumberFilter;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class Number extends Text
|
||||
{
|
||||
/**
|
||||
* The minimum value that can be assigned to the field.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $min;
|
||||
|
||||
/**
|
||||
* The maximum value that can be assigned to the field.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $max;
|
||||
|
||||
/**
|
||||
* The step size the field will increment and decrement by.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $step;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|\Closure|callable|object|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, callable $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback);
|
||||
|
||||
$this->textAlign(Field::RIGHT_ALIGN)
|
||||
->withMeta(['type' => 'number'])
|
||||
->displayUsing(function ($value) {
|
||||
return ! $this->isValidNullValue($value) ? (string) $value : null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum value that can be assigned to the field.
|
||||
*
|
||||
* @param mixed $min
|
||||
* @return $this
|
||||
*/
|
||||
public function min($min)
|
||||
{
|
||||
$this->min = $min;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum value that can be assigned to the field.
|
||||
*
|
||||
* @param mixed $max
|
||||
* @return $this
|
||||
*/
|
||||
public function max($max)
|
||||
{
|
||||
$this->max = $max;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The step size the field will increment and decrement by.
|
||||
*
|
||||
* @param mixed $step
|
||||
* @return $this
|
||||
*/
|
||||
public function step($step)
|
||||
{
|
||||
$this->step = $step;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the field filter.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Laravel\Nova\Fields\Filters\Filter
|
||||
*/
|
||||
protected function makeFilter(NovaRequest $request)
|
||||
{
|
||||
return new NumberFilter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default filterable callback.
|
||||
*
|
||||
* @return callable(\Laravel\Nova\Http\Requests\NovaRequest, \Illuminate\Database\Eloquent\Builder, mixed, string):\Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function defaultFilterableCallback()
|
||||
{
|
||||
return function (NovaRequest $request, $query, $value, $attribute) {
|
||||
[$min, $max] = $value;
|
||||
|
||||
if (! is_null($min) && ! is_null($max)) {
|
||||
return $query->whereBetween($attribute, [$min, $max]);
|
||||
} elseif (! is_null($min)) {
|
||||
return $query->where($attribute, '>=', $min);
|
||||
}
|
||||
|
||||
return $query->where($attribute, '<=', $max);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function serializeForFilter()
|
||||
{
|
||||
return transform($this->jsonSerialize(), function ($field) {
|
||||
return Arr::only($field, [
|
||||
'uniqueKey',
|
||||
'name',
|
||||
'attribute',
|
||||
'type',
|
||||
'min',
|
||||
'max',
|
||||
'step',
|
||||
'pattern',
|
||||
'placeholder',
|
||||
'extraAttributes',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the element for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), collect([
|
||||
'min' => $this->min,
|
||||
'max' => $this->max,
|
||||
'step' => $this->step,
|
||||
])->reject(function ($value) {
|
||||
return is_null($value) || (empty($value) && $value !== 0);
|
||||
})->all());
|
||||
}
|
||||
}
|
||||
47
nova/src/Fields/Password.php
Normal file
47
nova/src/Fields/Password.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class Password extends Field
|
||||
{
|
||||
use SupportsDependentFields;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'password-field';
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
if (! empty($request[$requestAttribute])) {
|
||||
$model->{$attribute} = Hash::make($request[$requestAttribute]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(
|
||||
parent::jsonSerialize(),
|
||||
['value' => '']
|
||||
);
|
||||
}
|
||||
}
|
||||
44
nova/src/Fields/PasswordConfirmation.php
Normal file
44
nova/src/Fields/PasswordConfirmation.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
class PasswordConfirmation extends Password
|
||||
{
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'password-field';
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, callable $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback);
|
||||
|
||||
$this->onlyOnForms();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
protected function fillAttribute(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
89
nova/src/Fields/Peekable.php
Normal file
89
nova/src/Fields/Peekable.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait Peekable
|
||||
{
|
||||
/**
|
||||
* Indicates if the related resource can be peeked at.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool|null
|
||||
*/
|
||||
public $peekable = true;
|
||||
|
||||
/**
|
||||
* Specify if the related resource can be peeked at.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function peekable($callback = true)
|
||||
{
|
||||
$this->peekable = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent the user from peeking at the related resource.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function noPeeking()
|
||||
{
|
||||
$this->peekable = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve whether the relation is able to be peeked at.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function isPeekable(NovaRequest $request)
|
||||
{
|
||||
if (is_callable($this->peekable)) {
|
||||
$this->peekable = call_user_func($this->peekable, $request);
|
||||
}
|
||||
|
||||
return $this->peekable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the relation has fields that can be peeked at.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function hasFieldsToPeekAt(NovaRequest $request)
|
||||
{
|
||||
if (! $request->isPresentationRequest() && ! $request->isResourcePreviewRequest()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_null($relatedResource = $this->relatedResource())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $relatedResource->peekableFieldsCount($request) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the appropriate related Resource for the field.
|
||||
*
|
||||
* @return \Laravel\Nova\Resource|null
|
||||
*/
|
||||
protected function relatedResource()
|
||||
{
|
||||
if ($this instanceof MorphTo) {
|
||||
return $this->morphToResource;
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
return $this->belongsToResource;
|
||||
}
|
||||
}
|
||||
43
nova/src/Fields/PeekableFields.php
Normal file
43
nova/src/Fields/PeekableFields.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait PeekableFields
|
||||
{
|
||||
/**
|
||||
* Indicates whether to show the field in the modal preview.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool
|
||||
*/
|
||||
public $showWhenPeeking = false;
|
||||
|
||||
/**
|
||||
* Show the field in the modal preview.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function showWhenPeeking($callback = true)
|
||||
{
|
||||
$this->showWhenPeeking = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field is to be shown in the preview modal.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return bool
|
||||
*/
|
||||
public function isShownWhenPeeking(NovaRequest $request): bool
|
||||
{
|
||||
if (is_callable($this->showWhenPeeking)) {
|
||||
$this->showWhenPeeking = call_user_func($this->showWhenPeeking, $request);
|
||||
}
|
||||
|
||||
return $this->showWhenPeeking;
|
||||
}
|
||||
}
|
||||
185
nova/src/Fields/Place.php
Normal file
185
nova/src/Fields/Place.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
/**
|
||||
* @deprecated Places API will stop functioning on May 31st, 2022
|
||||
*/
|
||||
class Place extends Text
|
||||
{
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'place-field';
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|\Closure|callable|object|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):(mixed))|null $resolveCallback
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name, $attribute = null, callable $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback);
|
||||
|
||||
$this->secondAddressLine('address_line_2')
|
||||
->city('city')
|
||||
->state('state')
|
||||
->postalCode('postal_code')
|
||||
->suburb('suburb')
|
||||
->country('country')
|
||||
->latitude('latitude')
|
||||
->longitude('longitude');
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the field to only display cities in its results.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onlyCities()
|
||||
{
|
||||
return $this->type('city');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the place type.
|
||||
*
|
||||
* @param string $type
|
||||
* @return $this
|
||||
*/
|
||||
public function type($type)
|
||||
{
|
||||
if ($type == 'city') {
|
||||
$this->secondAddressLine(null)->city(null)->postalCode(null);
|
||||
}
|
||||
|
||||
return $this->withMeta(['placeType' => $type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the countries to search within.
|
||||
*
|
||||
* @param array $countries
|
||||
* @return $this
|
||||
*/
|
||||
public function countries(array $countries)
|
||||
{
|
||||
return $this->withMeta(['countries' => $countries]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the field that contains the second address line.
|
||||
*
|
||||
* @param string|null $field
|
||||
* @return $this
|
||||
*/
|
||||
public function secondAddressLine($field)
|
||||
{
|
||||
return $this->withMeta([__FUNCTION__ => $field]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the field that contains the city.
|
||||
*
|
||||
* @param string|null $field
|
||||
* @return $this
|
||||
*/
|
||||
public function city($field)
|
||||
{
|
||||
return $this->withMeta([__FUNCTION__ => $field]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the field that contains the state.
|
||||
*
|
||||
* @param string $field
|
||||
* @return $this
|
||||
*/
|
||||
public function state($field)
|
||||
{
|
||||
return $this->withMeta([__FUNCTION__ => $field]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the field that contains the postal code.
|
||||
*
|
||||
* @param string|null $field
|
||||
* @return $this
|
||||
*/
|
||||
public function postalCode($field)
|
||||
{
|
||||
return $this->withMeta([__FUNCTION__ => $field]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the field that contains the suburb.
|
||||
*
|
||||
* @param string $field
|
||||
* @return $this
|
||||
*/
|
||||
public function suburb($field)
|
||||
{
|
||||
return $this->withMeta([__FUNCTION__ => $field]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the field that contains the country.
|
||||
*
|
||||
* @param string $field
|
||||
* @return $this
|
||||
*/
|
||||
public function country($field)
|
||||
{
|
||||
return $this->withMeta([__FUNCTION__ => $field]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the field that contains the latitude.
|
||||
*
|
||||
* @param string $field
|
||||
* @return $this
|
||||
*/
|
||||
public function latitude($field)
|
||||
{
|
||||
return $this->withMeta([__FUNCTION__ => $field]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the language that places.js should use.
|
||||
*
|
||||
* @param string $language
|
||||
* @return $this
|
||||
*/
|
||||
public function language($language)
|
||||
{
|
||||
return $this->withMeta([__FUNCTION__ => $language]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the field that contains the longitude.
|
||||
*
|
||||
* @param string $field
|
||||
* @return $this
|
||||
*/
|
||||
public function longitude($field)
|
||||
{
|
||||
return $this->withMeta([__FUNCTION__ => $field]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register depends on to a field.
|
||||
*
|
||||
* @param string|array $attributes
|
||||
* @param callable|string $mixin
|
||||
* @return $this
|
||||
*/
|
||||
public function dependsOn($attributes, $mixin)
|
||||
{
|
||||
throw new \Exception('The `dependsOn` option is not available on Place fields.');
|
||||
}
|
||||
}
|
||||
42
nova/src/Fields/PresentsAudio.php
Normal file
42
nova/src/Fields/PresentsAudio.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait PresentsAudio
|
||||
{
|
||||
/**
|
||||
* The "preload" attribute callback.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):string)|string|null
|
||||
*/
|
||||
public $preloadAudioCallback;
|
||||
|
||||
/**
|
||||
* Set "preload" option for the field.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):string)|string $preloadAudioCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function preload($preloadAudioCallback)
|
||||
{
|
||||
$this->preloadAudioCallback = $preloadAudioCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the attributes to present the image with.
|
||||
*
|
||||
* @return array{preload: string|null}
|
||||
*/
|
||||
public function audioAttributes()
|
||||
{
|
||||
$request = app(NovaRequest::class);
|
||||
|
||||
return [
|
||||
'preload' => value($this->preloadAudioCallback, $request),
|
||||
];
|
||||
}
|
||||
}
|
||||
153
nova/src/Fields/PresentsImages.php
Normal file
153
nova/src/Fields/PresentsImages.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
trait PresentsImages
|
||||
{
|
||||
/**
|
||||
* The maximum width of the component.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $maxWidth = null;
|
||||
|
||||
/**
|
||||
* The width of the component when presenting the field on the index view.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $indexWidth = 32;
|
||||
|
||||
/**
|
||||
* The width of the component when presenting the field on the detail view.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $detailWidth = 128;
|
||||
|
||||
/**
|
||||
* Indicates whether the image should be fully rounded or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $rounded = false;
|
||||
|
||||
/**
|
||||
* Indicates the aspect ratio class the image should be displayed with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $aspect = 'aspect-auto';
|
||||
|
||||
/**
|
||||
* Set the maximum width of the component.
|
||||
*
|
||||
* @param int $maxWidth
|
||||
* @return $this
|
||||
*/
|
||||
public function maxWidth($maxWidth)
|
||||
{
|
||||
$this->maxWidth = $maxWidth;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the width of the image on the index view.
|
||||
*
|
||||
* @param int $width
|
||||
* @return $this
|
||||
*/
|
||||
public function indexWidth($width)
|
||||
{
|
||||
$this->indexWidth = $width;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the width of the image on the detail view.
|
||||
*
|
||||
* @param int $detailWidth
|
||||
* @return $this
|
||||
*/
|
||||
public function detailWidth($detailWidth)
|
||||
{
|
||||
$this->detailWidth = $detailWidth;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the image thumbnail with full-rounded edges.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function rounded()
|
||||
{
|
||||
$this->rounded = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the image thumbnail with square edges.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function squared()
|
||||
{
|
||||
$this->rounded = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the image thumbnail with square edges.
|
||||
*
|
||||
* @param string $aspect
|
||||
* @return $this
|
||||
*/
|
||||
public function aspect($aspect)
|
||||
{
|
||||
$this->aspect = $aspect;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the field should have rounded corners.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isRounded()
|
||||
{
|
||||
return $this->rounded == true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the field should have squared corners.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSquared()
|
||||
{
|
||||
return $this->rounded == false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the attributes to present the image with.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function imageAttributes()
|
||||
{
|
||||
return [
|
||||
'indexWidth' => $this->indexWidth,
|
||||
'detailWidth' => $this->detailWidth,
|
||||
'maxWidth' => $this->maxWidth,
|
||||
'rounded' => $this->isRounded(),
|
||||
'aspect' => $this->aspect,
|
||||
];
|
||||
}
|
||||
}
|
||||
60
nova/src/Fields/PreviewableFields.php
Normal file
60
nova/src/Fields/PreviewableFields.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait PreviewableFields
|
||||
{
|
||||
/**
|
||||
* Indicates whether to show the field in the modal preview.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool
|
||||
*/
|
||||
public $showOnPreview = false;
|
||||
|
||||
/**
|
||||
* Show the field in the modal preview.
|
||||
*
|
||||
* @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(bool))|bool $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function showOnPreview($callback = true)
|
||||
{
|
||||
$this->showOnPreview = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should only be shown on the preview modal.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onlyOnPreview()
|
||||
{
|
||||
$this->showOnIndex = false;
|
||||
$this->showOnDetail = false;
|
||||
$this->showOnCreation = false;
|
||||
$this->showOnUpdate = false;
|
||||
$this->showOnPreview = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field is to be shown in the preview modal.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param mixed $resource
|
||||
* @return bool
|
||||
*/
|
||||
public function isShownOnPreview(NovaRequest $request, $resource): bool
|
||||
{
|
||||
if (is_callable($this->showOnPreview)) {
|
||||
$this->showOnPreview = call_user_func($this->showOnPreview, $request, $resource);
|
||||
}
|
||||
|
||||
return $this->showOnPreview;
|
||||
}
|
||||
}
|
||||
277
nova/src/Fields/Repeater.php
Normal file
277
nova/src/Fields/Repeater.php
Normal file
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields;
|
||||
|
||||
use Laravel\Nova\Exceptions\NovaException;
|
||||
use Laravel\Nova\Fields\Repeater\Presets\HasMany;
|
||||
use Laravel\Nova\Fields\Repeater\Presets\JSON;
|
||||
use Laravel\Nova\Fields\Repeater\Presets\Preset;
|
||||
use Laravel\Nova\Fields\Repeater\Repeatable;
|
||||
use Laravel\Nova\Fields\Repeater\RepeatableCollection;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type TFieldValidationRules from \Laravel\Nova\Fields\Field
|
||||
*/
|
||||
class Repeater extends Field
|
||||
{
|
||||
/**
|
||||
* The resource class for the repeater.
|
||||
*
|
||||
* @var class-string<\Laravel\Nova\Resource>|null
|
||||
*/
|
||||
public $resourceClass;
|
||||
|
||||
/**
|
||||
* The resource name for the repeater.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $resourceName;
|
||||
|
||||
/**
|
||||
* The field's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'repeater-field';
|
||||
|
||||
/**
|
||||
* Indicates if the field label and form element should sit on top of each other.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $stacked = false;
|
||||
|
||||
/**
|
||||
* Indicates whether the field should use all available white-space.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $fullWidth = false;
|
||||
|
||||
/**
|
||||
* The repeatable types used for the Repeater.
|
||||
*
|
||||
* @var \Laravel\Nova\Fields\Repeater\RepeatableCollection
|
||||
*/
|
||||
public $repeatables;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $sortable = true;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $uniqueField;
|
||||
|
||||
/**
|
||||
* The preset used for the field.
|
||||
*
|
||||
* @var \Laravel\Nova\Fields\Repeater\Presets\Preset|null
|
||||
*/
|
||||
public $preset;
|
||||
|
||||
/**
|
||||
* Create a new field.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $attribute
|
||||
* @param (callable(mixed, mixed, ?string):mixed)|null $resolveCallback
|
||||
*/
|
||||
public function __construct($name, $attribute = null, callable $resolveCallback = null)
|
||||
{
|
||||
parent::__construct($name, $attribute, $resolveCallback);
|
||||
|
||||
$this->onlyOnForms();
|
||||
$this->repeatables = RepeatableCollection::make();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the callback to be executed to retrieve the pivot fields.
|
||||
*
|
||||
* @param array<int, \Laravel\Nova\Fields\Repeater\Repeatable> $repeatables
|
||||
* @return $this
|
||||
*/
|
||||
public function repeatables(array $repeatables)
|
||||
{
|
||||
foreach ($repeatables as $repeatable) {
|
||||
$this->repeatables->push($repeatable);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the preset used for the field.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function preset(Preset $preset)
|
||||
{
|
||||
$this->preset = $preset;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the JSON preset for the field.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function asJson()
|
||||
{
|
||||
return $this->preset(new JSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the HasMany preset for the field.
|
||||
*
|
||||
* @param class-string<\Laravel\Nova\Resource>|null $resourceClass
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Laravel\Nova\Exceptions\NovaException
|
||||
*/
|
||||
public function asHasMany($resourceClass = null)
|
||||
{
|
||||
/** @var class-string<\Laravel\Nova\Resource>|null $resource */
|
||||
$resource = $resourceClass ?? ResourceRelationshipGuesser::guessResource($this->name);
|
||||
|
||||
if ($resource) {
|
||||
$this->resourceClass = $resource;
|
||||
$this->resourceName = $resource::uriKey();
|
||||
|
||||
return $this->preset(new HasMany);
|
||||
}
|
||||
|
||||
throw NovaException::missingResourceForRepeater($this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the preset instance for the field.
|
||||
*
|
||||
* @return \Laravel\Nova\Fields\Repeater\Presets\Preset
|
||||
*/
|
||||
public function getPreset()
|
||||
{
|
||||
return $this->preset ?? new JSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the given attribute from the given resource.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @param string $attribute
|
||||
* @return mixed
|
||||
*/
|
||||
protected function resolveAttribute($resource, $attribute)
|
||||
{
|
||||
$request = app(NovaRequest::class);
|
||||
|
||||
return $this->getPreset()->get($request, $resource, $attribute, $this->repeatables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the field collection contains an ID field.
|
||||
*/
|
||||
protected function fieldsContainsIDField(FieldCollection $fields): bool
|
||||
{
|
||||
return $fields->contains(function (Field $field) {
|
||||
return $field instanceof ID && $field->attribute === $this->uniqueField
|
||||
|| $field instanceof Hidden && $field->attribute === $this->uniqueField;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the given attribute on the model based on the incoming request.
|
||||
*
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model
|
||||
* @param string $attribute
|
||||
* @return \Closure
|
||||
*/
|
||||
protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute)
|
||||
{
|
||||
return $this->getPreset()->set($request, $requestAttribute, $model, $attribute, $this->repeatables, $this->uniqueField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the creation rules for this field.
|
||||
*
|
||||
* @return array<array-key, mixed>
|
||||
*
|
||||
* @phpstan-return array<string, TFieldValidationRules>
|
||||
*/
|
||||
public function getCreationRules(NovaRequest $request)
|
||||
{
|
||||
return array_merge_recursive(parent::getCreationRules($request), $this->formatRules());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the update rules for this field.
|
||||
*
|
||||
* @return array<array-key, mixed>
|
||||
*
|
||||
* @phpstan-return array<string, TFieldValidationRules>
|
||||
*/
|
||||
public function getUpdateRules(NovaRequest $request)
|
||||
{
|
||||
return array_merge_recursive(parent::getUpdateRules($request), $this->formatRules());
|
||||
}
|
||||
|
||||
/**
|
||||
* Format available rules.
|
||||
*
|
||||
* @return array<array-key, mixed>
|
||||
*
|
||||
* @phpstan-return array<string, TFieldValidationRules>
|
||||
*/
|
||||
protected function formatRules()
|
||||
{
|
||||
$request = app(NovaRequest::class);
|
||||
|
||||
if ($request->method() === 'GET') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect($request->{$this->validationKey()})
|
||||
->map(function ($item) {
|
||||
return $this->repeatables->findByKey($item['type']);
|
||||
})
|
||||
->flatMap(function (Repeatable $repeatable, $index) use ($request) {
|
||||
return FieldCollection::make($repeatable->fields($request))
|
||||
->mapWithKeys(function (Field $field) use ($index) {
|
||||
return ["{$this->validationKey()}.{$index}.fields.{$field->attribute}" => $field->rules];
|
||||
});
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the unique database column to use when attempting upserts.
|
||||
*
|
||||
* @param string|null $key
|
||||
* @return $this
|
||||
*/
|
||||
public function uniqueField($key)
|
||||
{
|
||||
$this->uniqueField = $key;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the field for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge([
|
||||
'repeatables' => $this->repeatables,
|
||||
'sortable' => $this->sortable,
|
||||
], parent::jsonSerialize());
|
||||
}
|
||||
}
|
||||
138
nova/src/Fields/Repeater/Presets/HasMany.php
Normal file
138
nova/src/Fields/Repeater/Presets/HasMany.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Repeater\Presets;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany as EloquentHasMany;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Nova\Fields\Field;
|
||||
use Laravel\Nova\Fields\FieldCollection;
|
||||
use Laravel\Nova\Fields\Repeater\RepeatableCollection;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Support\Fluent;
|
||||
|
||||
class HasMany implements Preset
|
||||
{
|
||||
/**
|
||||
* Save the field value to permanent storage.
|
||||
*
|
||||
* @param string|null $uniqueField
|
||||
* @return \Closure|void
|
||||
*/
|
||||
public function set(
|
||||
NovaRequest $request,
|
||||
string $requestAttribute,
|
||||
Model $model,
|
||||
string $attribute,
|
||||
RepeatableCollection $repeatables,
|
||||
$uniqueField
|
||||
) {
|
||||
return function () use ($request, $requestAttribute, $model, $attribute, $repeatables, $uniqueField) {
|
||||
$repeaterItems = collect($request->input($requestAttribute));
|
||||
|
||||
if (! $uniqueField) {
|
||||
$model->{$attribute}()->delete();
|
||||
} else {
|
||||
$this->deleteMissingRelations($attribute, $model, $repeaterItems, $uniqueField);
|
||||
}
|
||||
|
||||
$repeaterItems->transform(function ($item, $blockKey) use ($request, $requestAttribute, $repeatables) {
|
||||
$block = $repeatables->findByKey($item['type']);
|
||||
$fields = FieldCollection::make($block->fields($request));
|
||||
$data = Fluent::make();
|
||||
|
||||
$callbacks = $fields
|
||||
->withoutUnfillable()
|
||||
->withoutMissingValues()
|
||||
->map(function (Field $field) use ($request, $requestAttribute, $data, $blockKey) {
|
||||
return $field->fillInto($request, $data, $field->attribute, "{$requestAttribute}.{$blockKey}.fields.{$field->attribute}");
|
||||
})
|
||||
->filter(function ($callback) {
|
||||
return is_callable($callback);
|
||||
})->toBase();
|
||||
|
||||
return [$data, $callbacks, $item];
|
||||
})->each(function ($tuple) use ($model, $attribute, $uniqueField) {
|
||||
[$data, $callbacks, $row] = $tuple;
|
||||
|
||||
if ($uniqueField) {
|
||||
$this->upsertRelation($model, $data, $row, $uniqueField, $model->{$attribute}());
|
||||
} else {
|
||||
$model->{$attribute}()->forceCreate($data->getAttributes());
|
||||
}
|
||||
|
||||
$callbacks->each->__invoke();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value from storage and hydrate the field's value.
|
||||
*
|
||||
* @param mixed $model
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function get(NovaRequest $request, $model, string $attribute, RepeatableCollection $repeatables)
|
||||
{
|
||||
return RepeatableCollection::make($model->{$attribute})
|
||||
->map(function ($block) use ($repeatables) {
|
||||
return $repeatables->newRepeatableByModel($block);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete missing relations.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param \Illuminate\Support\Collection $repeaterItems
|
||||
* @param mixed $uniqueField
|
||||
* @return void
|
||||
*/
|
||||
public function deleteMissingRelations(string $attribute, Model $model, Collection $repeaterItems, $uniqueField): void
|
||||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Relations\HasMany $relation */
|
||||
$relation = $model->{$attribute}();
|
||||
|
||||
$availableItems = $repeaterItems->map(function ($item) use ($uniqueField) {
|
||||
return $item['fields'][$uniqueField];
|
||||
})->all();
|
||||
|
||||
$deletableIds = $relation->pluck($uniqueField)
|
||||
->reject(function ($id) use ($availableItems) {
|
||||
return in_array($id, $availableItems);
|
||||
});
|
||||
|
||||
if ($deletableIds->isNotEmpty()) {
|
||||
$model->{$attribute}()->whereIn($uniqueField, $deletableIds)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert relation.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param \Laravel\Nova\Support\Fluent $data
|
||||
* @param array $row
|
||||
* @param mixed $uniqueField
|
||||
* @param \Illuminate\Database\Eloquent\Relations\HasMany $relation
|
||||
* @return void
|
||||
*/
|
||||
public function upsertRelation(Model $model, Fluent $data, array $row, $uniqueField, EloquentHasMany $relation): void
|
||||
{
|
||||
$model->unguarded(function () use ($data, $row, $uniqueField, $relation) {
|
||||
$uniqueValue = $row['fields'][$uniqueField];
|
||||
|
||||
$attributes = Arr::except($data->getAttributes(), $uniqueField);
|
||||
|
||||
if (empty($uniqueValue)) {
|
||||
$relation->create($attributes);
|
||||
} else {
|
||||
$relation->updateOrCreate(
|
||||
[$uniqueField => $uniqueValue], $attributes
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
81
nova/src/Fields/Repeater/Presets/JSON.php
Normal file
81
nova/src/Fields/Repeater/Presets/JSON.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Repeater\Presets;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Laravel\Nova\Fields\Field;
|
||||
use Laravel\Nova\Fields\FieldCollection;
|
||||
use Laravel\Nova\Fields\Repeater\RepeatableCollection;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Support\Fluent;
|
||||
|
||||
class JSON implements Preset
|
||||
{
|
||||
/**
|
||||
* Save the field value to permanent storage.
|
||||
*
|
||||
* @param string|null $uniqueField
|
||||
* @return \Closure
|
||||
*/
|
||||
public function set(
|
||||
NovaRequest $request,
|
||||
string $requestAttribute,
|
||||
Model $model,
|
||||
string $attribute,
|
||||
RepeatableCollection $repeatables,
|
||||
$uniqueField
|
||||
) {
|
||||
// Reset the field attribute in case it's filled already
|
||||
$model->setAttribute($attribute, null);
|
||||
|
||||
$fieldCallbacks = collect($request->input($requestAttribute))
|
||||
->map(function ($item, $blockKey) use ($request, $requestAttribute, $model, $attribute, $repeatables) {
|
||||
$data = new Fluent();
|
||||
|
||||
$block = $repeatables->findByKey($item['type']);
|
||||
$fields = FieldCollection::make($block->fields($request));
|
||||
|
||||
// For each field collection, return the callbacks and set the data on the model, and then return a function
|
||||
// that invokes all of the callbacks;
|
||||
$callbacks = $fields
|
||||
->withoutUnfillable()
|
||||
->withoutMissingValues()
|
||||
->map(function (Field $field) use ($request, $requestAttribute, $data, $blockKey) {
|
||||
return $field->fillInto($request, $data, $field->attribute, "{$requestAttribute}.{$blockKey}.fields.{$field->attribute}");
|
||||
})
|
||||
->filter(function ($callback) {
|
||||
return is_callable($callback);
|
||||
});
|
||||
|
||||
// Set the block type on the data object
|
||||
$model->setAttribute("{$attribute}->{$blockKey}->type", $block->key());
|
||||
|
||||
// Set the data on the model
|
||||
foreach ($data->getAttributes() as $k => $v) {
|
||||
$model->setAttribute("{$attribute}->{$blockKey}->fields->{$k}", $v);
|
||||
}
|
||||
|
||||
// Return a function that calls the callbacks from the fields
|
||||
return function () use ($callbacks) {
|
||||
return $callbacks->each->__invoke();
|
||||
};
|
||||
});
|
||||
|
||||
return function () use ($fieldCallbacks) {
|
||||
return collect($fieldCallbacks)->each->__invoke();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value from storage and hydrate the field's value.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function get(NovaRequest $request, Model $model, string $attribute, RepeatableCollection $repeatables)
|
||||
{
|
||||
return RepeatableCollection::make($model->{$attribute})
|
||||
->map(function ($block) use ($repeatables) {
|
||||
return $repeatables->newRepeatableByKey($block['type'], $block['fields']);
|
||||
});
|
||||
}
|
||||
}
|
||||
34
nova/src/Fields/Repeater/Presets/Preset.php
Normal file
34
nova/src/Fields/Repeater/Presets/Preset.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Fields\Repeater\Presets;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Laravel\Nova\Fields\Repeater\RepeatableCollection;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
interface Preset
|
||||
{
|
||||
/**
|
||||
* Save the field value to permanent storage.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param string $requestAttribute
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @param \Laravel\Nova\Fields\Repeater\RepeatableCollection $repeatables
|
||||
* @param string|null $uniqueField
|
||||
* @return \Closure|void
|
||||
*/
|
||||
public function set(NovaRequest $request, string $requestAttribute, Model $model, string $attribute, RepeatableCollection $repeatables, $uniqueField);
|
||||
|
||||
/**
|
||||
* Retrieve the value from storage and hydrate the field's value.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @param \Laravel\Nova\Fields\Repeater\RepeatableCollection $repeatables
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function get(NovaRequest $request, Model $model, string $attribute, RepeatableCollection $repeatables);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user