add nova
This commit is contained in:
1126
nova/src/Actions/Action.php
Normal file
1126
nova/src/Actions/Action.php
Normal file
File diff suppressed because it is too large
Load Diff
86
nova/src/Actions/ActionCollection.php
Normal file
86
nova/src/Actions/ActionCollection.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
/**
|
||||
* @template TKey of array-key
|
||||
* @template TValue of \Laravel\Nova\Actions\Action
|
||||
*
|
||||
* @extends \Illuminate\Support\Collection<TKey, TValue>
|
||||
*/
|
||||
class ActionCollection extends Collection
|
||||
{
|
||||
/**
|
||||
* Get the actions that are authorized for viewing on the index.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return static<TKey, TValue>
|
||||
*/
|
||||
public function authorizedToSeeOnIndex(NovaRequest $request)
|
||||
{
|
||||
return $this->filter->shownOnIndex()
|
||||
->filter(function ($action) use ($request) {
|
||||
if ($action->sole === true) {
|
||||
return ! $request->allResourcesSelected()
|
||||
&& $request->selectedResourceIds()->count() === 1
|
||||
&& $action->authorizedToSee($request);
|
||||
}
|
||||
|
||||
return $action->authorizedToSee($request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actions that are authorized for viewing on detail pages.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return static<TKey, TValue>
|
||||
*/
|
||||
public function authorizedToSeeOnDetail(NovaRequest $request)
|
||||
{
|
||||
return $this->filter->shownOnDetail()->filter->authorizedToSee($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actions that are authorized for viewing on table rows.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return static<TKey, TValue>
|
||||
*/
|
||||
public function authorizedToSeeOnTableRow(NovaRequest $request)
|
||||
{
|
||||
return $this->filter->shownOnTableRow()->filter->authorizedToSee($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the actions available for display can be executed.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return $this
|
||||
*/
|
||||
public function withAuthorizedToRun(NovaRequest $request, $model)
|
||||
{
|
||||
return $this->each(function (Action $action) use ($request, $model) {
|
||||
$action->authorizedToRun($request, $model);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return action counts by type on index.
|
||||
*
|
||||
* @return array{standalone: mixed, resource: mixed}
|
||||
*/
|
||||
public function countsByTypeOnIndex()
|
||||
{
|
||||
[$standalone, $resource] = $this->filter->shownOnIndex()->partition->isStandalone();
|
||||
|
||||
return [
|
||||
'standalone' => $standalone->count(),
|
||||
'resource' => $resource->count(),
|
||||
];
|
||||
}
|
||||
}
|
||||
495
nova/src/Actions/ActionEvent.php
Normal file
495
nova/src/Actions/ActionEvent.php
Normal file
@@ -0,0 +1,495 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use DateTime;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Http\Requests\ActionRequest;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Nova;
|
||||
use Laravel\Nova\Util;
|
||||
|
||||
/**
|
||||
* @property \Illuminate\Database\Eloquent\Model $target
|
||||
* @property \Illuminate\Foundation\Auth\User $user
|
||||
* @property array|null $changes
|
||||
* @property array|null $original
|
||||
*/
|
||||
class ActionEvent extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that aren't mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'changes' => 'array',
|
||||
'original' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* The storage format of the model's date columns.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dateFormat = 'Y-m-d H:i:s';
|
||||
|
||||
/**
|
||||
* Get the user that initiated the action.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(Util::userModel(), 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target of the action for user interface linking.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function target()
|
||||
{
|
||||
$queryWithTrashed = function ($query) {
|
||||
return $query->withTrashed();
|
||||
};
|
||||
|
||||
return $this->morphTo('target', 'target_type', 'target_id')
|
||||
->constrain(
|
||||
collect(Nova::$resources)
|
||||
->filter(function ($resource) {
|
||||
return $resource::softDeletes();
|
||||
})->mapWithKeys(function ($resource) use ($queryWithTrashed) {
|
||||
return [$resource::$model => $queryWithTrashed];
|
||||
})->all()
|
||||
)->when(true, function ($query) use ($queryWithTrashed) {
|
||||
return $query->hasMacro('withTrashed') ? $queryWithTrashed($query) : $query;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new action event instance for a resource creation.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return static
|
||||
*/
|
||||
public static function forResourceCreate($user, $model)
|
||||
{
|
||||
return new static([
|
||||
'batch_id' => (string) Str::orderedUuid(),
|
||||
'user_id' => $user->getAuthIdentifier(),
|
||||
'name' => 'Create',
|
||||
'actionable_type' => $model->getMorphClass(),
|
||||
'actionable_id' => $model->getKey(),
|
||||
'target_type' => $model->getMorphClass(),
|
||||
'target_id' => $model->getKey(),
|
||||
'model_type' => $model->getMorphClass(),
|
||||
'model_id' => $model->getKey(),
|
||||
'fields' => '',
|
||||
'original' => null,
|
||||
'changes' => array_diff_key($model->attributesToArray(), array_flip($model->getHidden())),
|
||||
'status' => 'finished',
|
||||
'exception' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new action event instance for a resource update.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return static
|
||||
*/
|
||||
public static function forResourceUpdate($user, $model)
|
||||
{
|
||||
return new static([
|
||||
'batch_id' => (string) Str::orderedUuid(),
|
||||
'user_id' => $user->getAuthIdentifier(),
|
||||
'name' => 'Update',
|
||||
'actionable_type' => $model->getMorphClass(),
|
||||
'actionable_id' => $model->getKey(),
|
||||
'target_type' => $model->getMorphClass(),
|
||||
'target_id' => $model->getKey(),
|
||||
'model_type' => $model->getMorphClass(),
|
||||
'model_id' => $model->getKey(),
|
||||
'fields' => '',
|
||||
'changes' => static::hydrateChangesPayload(
|
||||
$changes = array_diff_key($model->getDirty(), array_flip($model->getHidden()))
|
||||
),
|
||||
'original' => static::hydrateChangesPayload(
|
||||
array_intersect_key($model->newInstance()->setRawAttributes($model->getRawOriginal())->attributesToArray(), $changes)
|
||||
),
|
||||
'status' => 'finished',
|
||||
'exception' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new action event instance for an attached resource.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model $parent
|
||||
* @param \Illuminate\Database\Eloquent\Model $pivot
|
||||
* @return static
|
||||
*/
|
||||
public static function forAttachedResource(NovaRequest $request, $parent, $pivot)
|
||||
{
|
||||
return new static([
|
||||
'batch_id' => (string) Str::orderedUuid(),
|
||||
'user_id' => Nova::user($request)->getAuthIdentifier(),
|
||||
'name' => 'Attach',
|
||||
'actionable_type' => $parent->getMorphClass(),
|
||||
'actionable_id' => $parent->getKey(),
|
||||
'target_type' => Nova::modelInstanceForKey($request->relatedResource)->getMorphClass(),
|
||||
'target_id' => $request->input($request->relatedResource),
|
||||
'model_type' => $pivot->getMorphClass(),
|
||||
'model_id' => $pivot->getKey(),
|
||||
'fields' => '',
|
||||
'original' => null,
|
||||
'changes' => array_diff_key($pivot->attributesToArray(), $pivot->getHidden()),
|
||||
'status' => 'finished',
|
||||
'exception' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new action event instance for an attached resource update.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Model $parent
|
||||
* @param \Illuminate\Database\Eloquent\Model $pivot
|
||||
* @return static
|
||||
*/
|
||||
public static function forAttachedResourceUpdate(NovaRequest $request, $parent, $pivot)
|
||||
{
|
||||
return new static([
|
||||
'batch_id' => (string) Str::orderedUuid(),
|
||||
'user_id' => Nova::user($request)->getAuthIdentifier(),
|
||||
'name' => 'Update Attached',
|
||||
'actionable_type' => $parent->getMorphClass(),
|
||||
'actionable_id' => $parent->getKey(),
|
||||
'target_type' => Nova::modelInstanceForKey($request->relatedResource)->getMorphClass(),
|
||||
'target_id' => $request->relatedResourceId,
|
||||
'model_type' => $pivot->getMorphClass(),
|
||||
'model_id' => $pivot->getKey(),
|
||||
'fields' => '',
|
||||
'changes' => static::hydrateChangesPayload(
|
||||
$changes = array_diff_key($pivot->getDirty(), array_flip($pivot->getHidden()))
|
||||
),
|
||||
'original' => static::hydrateChangesPayload(
|
||||
array_intersect_key($pivot->newInstance()->setRawAttributes($pivot->getRawOriginal())->attributesToArray(), $changes)
|
||||
),
|
||||
'status' => 'finished',
|
||||
'exception' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new action event instances for resource deletes.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param \Illuminate\Support\Collection $models
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public static function forResourceDelete($user, Collection $models)
|
||||
{
|
||||
return static::forSoftDeleteAction('Delete', $user, $models);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new action event instances for resource restorations.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param \Illuminate\Support\Collection $models
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public static function forResourceRestore($user, Collection $models)
|
||||
{
|
||||
return static::forSoftDeleteAction('Restore', $user, $models);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new action event instances for resource soft deletions.
|
||||
*
|
||||
* @param string $action
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param \Illuminate\Support\Collection $models
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public static function forSoftDeleteAction($action, $user, Collection $models)
|
||||
{
|
||||
$batchId = (string) Str::orderedUuid();
|
||||
|
||||
return $models->map(function ($model) use ($action, $user, $batchId) {
|
||||
return new static([
|
||||
'batch_id' => $batchId,
|
||||
'user_id' => $user->getAuthIdentifier(),
|
||||
'name' => $action,
|
||||
'actionable_type' => $model->getMorphClass(),
|
||||
'actionable_id' => $model->getKey(),
|
||||
'target_type' => $model->getMorphClass(),
|
||||
'target_id' => $model->getKey(),
|
||||
'model_type' => $model->getMorphClass(),
|
||||
'model_id' => $model->getKey(),
|
||||
'fields' => '',
|
||||
'original' => null,
|
||||
'changes' => null,
|
||||
'status' => 'finished',
|
||||
'exception' => '',
|
||||
'created_at' => new DateTime,
|
||||
'updated_at' => new DateTime,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new action event instances for resource detachments.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param \Illuminate\Database\Eloquent\Model $parent
|
||||
* @param \Illuminate\Support\Collection $models
|
||||
* @param string $pivotClass
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public static function forResourceDetach($user, $parent, Collection $models, $pivotClass)
|
||||
{
|
||||
$batchId = (string) Str::orderedUuid();
|
||||
|
||||
return $models->map(function ($model) use ($user, $parent, $pivotClass, $batchId) {
|
||||
return new static([
|
||||
'batch_id' => $batchId,
|
||||
'user_id' => $user->getAuthIdentifier(),
|
||||
'name' => 'Detach',
|
||||
'actionable_type' => $parent->getMorphClass(),
|
||||
'actionable_id' => $parent->getKey(),
|
||||
'target_type' => $model->getMorphClass(),
|
||||
'target_id' => $model->getKey(),
|
||||
'model_type' => $pivotClass,
|
||||
'model_id' => null,
|
||||
'fields' => '',
|
||||
'original' => null,
|
||||
'changes' => null,
|
||||
'status' => 'finished',
|
||||
'exception' => '',
|
||||
'created_at' => new DateTime,
|
||||
'updated_at' => new DateTime,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the action records for the given models.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
|
||||
* @param \Laravel\Nova\Actions\Action $action
|
||||
* @param string $batchId
|
||||
* @param \Illuminate\Support\Collection $models
|
||||
* @param string $status
|
||||
* @return void
|
||||
*/
|
||||
public static function createForModels(ActionRequest $request, Action $action,
|
||||
$batchId, Collection $models, $status = 'running')
|
||||
{
|
||||
$models = $models->map(function ($model) use ($request, $action, $batchId, $status) {
|
||||
return array_merge(
|
||||
static::defaultAttributes($request, $action, $batchId, $status),
|
||||
[
|
||||
'actionable_id' => $request->actionableKey($model),
|
||||
'target_id' => $request->targetKey($model),
|
||||
'model_id' => $model->getKey(),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
$models->chunk(50)->each(function ($models) {
|
||||
static::insert($models->all());
|
||||
});
|
||||
|
||||
static::prune($models);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default attributes for creating a new action event.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
|
||||
* @param \Laravel\Nova\Actions\Action $action
|
||||
* @param string $batchId
|
||||
* @param string $status
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function defaultAttributes(ActionRequest $request, Action $action,
|
||||
$batchId, $status = 'running')
|
||||
{
|
||||
if ($request->isPivotAction()) {
|
||||
$pivotClass = $request->pivotRelation()->getPivotClass();
|
||||
|
||||
$modelType = collect(Relation::$morphMap)->filter(function ($model) use ($pivotClass) {
|
||||
return $model === $pivotClass;
|
||||
})->keys()->first() ?? $pivotClass;
|
||||
} else {
|
||||
$modelType = $request->actionableModel()->getMorphClass();
|
||||
}
|
||||
|
||||
return [
|
||||
'batch_id' => $batchId,
|
||||
'user_id' => Nova::user($request)->getAuthIdentifier(),
|
||||
'name' => $action->name(),
|
||||
'actionable_type' => $request->actionableModel()->getMorphClass(),
|
||||
'target_type' => $request->model()->getMorphClass(),
|
||||
'model_type' => $modelType,
|
||||
'fields' => serialize($request->resolveFieldsForStorage()),
|
||||
'original' => null,
|
||||
'changes' => null,
|
||||
'status' => $status,
|
||||
'exception' => '',
|
||||
'created_at' => new DateTime,
|
||||
'updated_at' => new DateTime,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune the action events for the given types.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $models
|
||||
* @param int $limit
|
||||
* @return void
|
||||
*/
|
||||
public static function prune($models, $limit = 25)
|
||||
{
|
||||
$models->each(function ($model) use ($limit) {
|
||||
static::where('actionable_id', $model['actionable_id'])
|
||||
->where('actionable_type', $model['actionable_type'])
|
||||
->whereNotIn('id', function ($query) use ($model, $limit) {
|
||||
$query->select('id')->fromSub(
|
||||
static::select('id')->orderBy('id', 'desc')
|
||||
->where('actionable_id', $model['actionable_id'])
|
||||
->where('actionable_type', $model['actionable_type'])
|
||||
->limit($limit)->toBase(),
|
||||
'action_events_temp'
|
||||
);
|
||||
})->delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given batch as running.
|
||||
*
|
||||
* @param string $batchId
|
||||
* @return int
|
||||
*/
|
||||
public static function markBatchAsRunning($batchId)
|
||||
{
|
||||
return static::where('batch_id', $batchId)
|
||||
->whereNotIn('status', ['finished', 'failed'])->update([
|
||||
'status' => 'running',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given batch as finished.
|
||||
*
|
||||
* @param string $batchId
|
||||
* @return int
|
||||
*/
|
||||
public static function markBatchAsFinished($batchId)
|
||||
{
|
||||
return static::where('batch_id', $batchId)
|
||||
->whereNotIn('status', ['finished', 'failed'])->update([
|
||||
'status' => 'finished',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a given action event record as finished.
|
||||
*
|
||||
* @param string $batchId
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return int
|
||||
*/
|
||||
public static function markAsFinished($batchId, $model)
|
||||
{
|
||||
return static::updateStatus($batchId, $model, 'finished');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given batch as failed.
|
||||
*
|
||||
* @param string $batchId
|
||||
* @param \Throwable $e
|
||||
* @return int
|
||||
*/
|
||||
public static function markBatchAsFailed($batchId, $e = null)
|
||||
{
|
||||
return static::where('batch_id', $batchId)
|
||||
->whereNotIn('status', ['finished', 'failed'])->update([
|
||||
'status' => 'failed',
|
||||
'exception' => $e ? (string) $e : '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a given action event record as failed.
|
||||
*
|
||||
* @param string $batchId
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param \Throwable|string $e
|
||||
* @return int
|
||||
*/
|
||||
public static function markAsFailed($batchId, $model, $e = null)
|
||||
{
|
||||
return static::updateStatus($batchId, $model, 'failed', $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status of a given action event.
|
||||
*
|
||||
* @param string $batchId
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $status
|
||||
* @param \Throwable|string $e
|
||||
* @return int
|
||||
*/
|
||||
public static function updateStatus($batchId, $model, $status, $e = null)
|
||||
{
|
||||
return static::where('batch_id', $batchId)
|
||||
->where('model_type', $model->getMorphClass())
|
||||
->where('model_id', $model->getKey())
|
||||
->update(['status' => $status, 'exception' => (string) $e]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table associated with the model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTable()
|
||||
{
|
||||
return 'action_events';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the changes payuload.
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return array
|
||||
*/
|
||||
protected static function hydrateChangesPayload(array $attributes)
|
||||
{
|
||||
return collect($attributes)
|
||||
->transform(function ($value) {
|
||||
return Util::hydrate($value);
|
||||
})->all();
|
||||
}
|
||||
}
|
||||
30
nova/src/Actions/ActionMethod.php
Normal file
30
nova/src/Actions/ActionMethod.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ActionMethod
|
||||
{
|
||||
/**
|
||||
* Determine the appropriate "handle" method for the given models.
|
||||
*
|
||||
* @param \Laravel\Nova\Actions\Action $action
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return string
|
||||
*/
|
||||
public static function determine(Action $action, $model)
|
||||
{
|
||||
if (! is_null($action->handleCallback)) {
|
||||
return 'handleUsingCallback';
|
||||
}
|
||||
|
||||
$method = 'handleFor'.Str::plural(class_basename($model));
|
||||
|
||||
if (method_exists($action, $method)) {
|
||||
return $method;
|
||||
}
|
||||
|
||||
return 'handle';
|
||||
}
|
||||
}
|
||||
48
nova/src/Actions/ActionModelCollection.php
Normal file
48
nova/src/Actions/ActionModelCollection.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Laravel\Nova\Http\Requests\ActionRequest;
|
||||
|
||||
/**
|
||||
* @template TKey of array-key
|
||||
* @template TModel of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Collection<TKey, TModel>
|
||||
*/
|
||||
class ActionModelCollection extends EloquentCollection
|
||||
{
|
||||
/**
|
||||
* Remove models the user does not have permission to execute the action against.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
|
||||
* @return static
|
||||
*/
|
||||
public function filterForExecution(ActionRequest $request)
|
||||
{
|
||||
$action = $request->action();
|
||||
$isPivotAction = $request->isPivotAction();
|
||||
|
||||
return new static($this->filter(function ($model) use ($request, $action, $isPivotAction) {
|
||||
if ($isPivotAction || $action->runCallback) {
|
||||
return $action->authorizedToRun($request, $model);
|
||||
}
|
||||
|
||||
return $action->authorizedToRun($request, $model) && $this->filterByResourceAuthorization($request, $request->newResourceWith($model), $action);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove models the user does not have permission to execute the action against.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
|
||||
* @param \Laravel\Nova\Resource $resource
|
||||
* @param \Laravel\Nova\Actions\Action|\Laravel\Nova\Actions\DestructiveAction $action
|
||||
* @return bool
|
||||
*/
|
||||
protected function filterByResourceAuthorization(ActionRequest $request, $resource, $action)
|
||||
{
|
||||
return $resource->authorizedToRunAction($request, $action);
|
||||
}
|
||||
}
|
||||
196
nova/src/Actions/ActionResource.php
Normal file
196
nova/src/Actions/ActionResource.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Nova\Fields\DateTime;
|
||||
use Laravel\Nova\Fields\ID;
|
||||
use Laravel\Nova\Fields\KeyValue;
|
||||
use Laravel\Nova\Fields\MorphToActionTarget;
|
||||
use Laravel\Nova\Fields\Status;
|
||||
use Laravel\Nova\Fields\Text;
|
||||
use Laravel\Nova\Fields\Textarea;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Nova;
|
||||
use Laravel\Nova\Resource;
|
||||
|
||||
/**
|
||||
* @template TActionModel of \Laravel\Nova\Actions\ActionEvent
|
||||
*
|
||||
* @extends \Laravel\Nova\Resource<TActionModel>
|
||||
*/
|
||||
class ActionResource extends Resource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
*
|
||||
* @var class-string<TActionModel>
|
||||
*/
|
||||
public static $model = ActionEvent::class;
|
||||
|
||||
/**
|
||||
* The single value that should be used to represent the resource when being displayed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $title = 'name';
|
||||
|
||||
/**
|
||||
* Indicates if the resource should be globally searchable.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $globallySearchable = false;
|
||||
|
||||
/**
|
||||
* Indicates whether the resource should automatically poll for new resources.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $polling = true;
|
||||
|
||||
/**
|
||||
* Determine if the current user can create new resources.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
public static function authorizedToCreate(Request $request)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current user can edit resources.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
public function authorizedToUpdate(Request $request)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current user can replicate the given resource.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
public function authorizedToReplicate(Request $request)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current user can delete resources.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
public function authorizedToDelete(Request $request)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fields displayed by the resource.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array
|
||||
*/
|
||||
public function fields(NovaRequest $request)
|
||||
{
|
||||
return [
|
||||
ID::make(Nova::__('ID'), 'id')->showOnPreview(),
|
||||
Text::make(Nova::__('Action Name'), 'name', function ($value) {
|
||||
return Nova::__($value);
|
||||
})->showOnPreview(),
|
||||
|
||||
Text::make(Nova::__('Action Initiated By'), function () {
|
||||
return $this->user->name ?? $this->user->email ?? __('Nova User');
|
||||
})->showOnPreview(),
|
||||
|
||||
MorphToActionTarget::make(Nova::__('Action Target'), 'target')->showOnPreview(),
|
||||
|
||||
Status::make(Nova::__('Action Status'), 'status', function ($value) {
|
||||
return Nova::__(ucfirst($value));
|
||||
})->loadingWhen([Nova::__('Waiting'), Nova::__('Running')])->failedWhen([Nova::__('Failed')]),
|
||||
|
||||
$this->when(isset($this->original), function () {
|
||||
return KeyValue::make(Nova::__('Original'), 'original')->showOnPreview();
|
||||
}),
|
||||
|
||||
$this->when(isset($this->changes), function () {
|
||||
return KeyValue::make(Nova::__('Changes'), 'changes')->showOnPreview();
|
||||
}),
|
||||
|
||||
Textarea::make(Nova::__('Exception'), 'exception')->showOnPreview(),
|
||||
|
||||
DateTime::make(Nova::__('Action Happened At'), 'created_at')->exceptOnForms()->showOnPreview(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an "index" query for the given resource.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public static function indexQuery(NovaRequest $request, $query)
|
||||
{
|
||||
return $query->with('user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this resource is available for navigation.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
public static function availableForNavigation(Request $request)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this resource is searchable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function searchable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the displayable label of the resource.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function label()
|
||||
{
|
||||
return Nova::__('Actions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the displayable singular label of the resource.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function singularLabel()
|
||||
{
|
||||
return Nova::__('Action');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URI key for the resource.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function uriKey()
|
||||
{
|
||||
return 'action-events';
|
||||
}
|
||||
}
|
||||
311
nova/src/Actions/ActionResponse.php
Normal file
311
nova/src/Actions/ActionResponse.php
Normal file
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use ArrayAccess;
|
||||
use JsonSerializable;
|
||||
|
||||
class ActionResponse implements ArrayAccess, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $danger;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $deleted;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $download;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $message;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $openInNewTab;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $redirect;
|
||||
|
||||
/**
|
||||
* @var array{path: string, options: array<string, mixed>}|null
|
||||
*/
|
||||
private $visit;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $modal;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $data = [];
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @return \Laravel\Nova\Actions\ActionResponse
|
||||
*/
|
||||
public static function message($message)
|
||||
{
|
||||
return tap(new static, function (self $response) use ($message) {
|
||||
$response->withMessage($message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @return $this
|
||||
*/
|
||||
public function withMessage($message)
|
||||
{
|
||||
$this->message = $message;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @return $this
|
||||
*/
|
||||
public function withDangerMessage($message)
|
||||
{
|
||||
$this->danger = $message;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @return \Laravel\Nova\Actions\ActionResponse
|
||||
*/
|
||||
public static function danger(string $message)
|
||||
{
|
||||
return tap(new static, function (self $response) use ($message) {
|
||||
$response->withDangerMessage($message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function withDeleted()
|
||||
{
|
||||
$this->deleted = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Laravel\Nova\Actions\ActionResponse
|
||||
*/
|
||||
public static function deleted()
|
||||
{
|
||||
return tap(new static, function (self $response) {
|
||||
$response->withDeleted();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return $this
|
||||
*/
|
||||
public function withRedirect($url)
|
||||
{
|
||||
$this->redirect = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return \Laravel\Nova\Actions\ActionResponse
|
||||
*/
|
||||
public static function redirect($url)
|
||||
{
|
||||
return tap(new static, function (self $response) use ($url) {
|
||||
$response->withRedirect($url);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return \Laravel\Nova\Actions\ActionResponse
|
||||
*/
|
||||
public static function openInNewTab($url)
|
||||
{
|
||||
return tap(new static, function (self $response) use ($url) {
|
||||
$response->usingNewTab($url);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|\Laravel\Nova\URL $path
|
||||
* @param array<string, mixed> $options
|
||||
* @return $this
|
||||
*/
|
||||
public function withVisitOptions($path, $options = [])
|
||||
{
|
||||
$this->visit = [
|
||||
'path' => '/'.ltrim($path, '/'),
|
||||
'options' => $options,
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|\Laravel\Nova\URL $path
|
||||
* @param array<string, mixed> $options
|
||||
* @return \Laravel\Nova\Actions\ActionResponse
|
||||
*/
|
||||
public static function visit($path, $options = [])
|
||||
{
|
||||
return tap(new static, function (self $response) use ($path, $options) {
|
||||
$response->withVisitOptions($path, $options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return $this
|
||||
*/
|
||||
private function usingNewTab($url)
|
||||
{
|
||||
$this->openInNewTab = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $url
|
||||
* @return $this
|
||||
*/
|
||||
public function withDownload($name, $url)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->download = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $url
|
||||
* @return \Laravel\Nova\Actions\ActionResponse
|
||||
*/
|
||||
public static function download(string $name, string $url)
|
||||
{
|
||||
return tap(new static, function (self $response) use ($name, $url) {
|
||||
$response->withDownload($name, $url);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $modal
|
||||
* @param array $data
|
||||
* @return $this
|
||||
*/
|
||||
public function withModal($modal, $data = [])
|
||||
{
|
||||
$this->modal = $modal;
|
||||
$this->data = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $modal
|
||||
* @param array $data
|
||||
* @return \Laravel\Nova\Actions\ActionResponse
|
||||
*/
|
||||
public static function modal(string $modal, $data)
|
||||
{
|
||||
return tap(new static, function (self $response) use ($data, $modal) {
|
||||
$response->withModal($modal, $data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given offset exists.
|
||||
*
|
||||
* @param string $offset
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->{$offset});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for a given offset.
|
||||
*
|
||||
* @param string $offset
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function offsetGet($offset): mixed
|
||||
{
|
||||
return $this->{$offset};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value at the given offset.
|
||||
*
|
||||
* @param string $offset
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
if (property_exists($this, $offset)) {
|
||||
$this->{$offset} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset the value at the given offset.
|
||||
*
|
||||
* @param string $offset
|
||||
* @return void
|
||||
*/
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->{$offset});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the element for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_filter(array_merge([
|
||||
'danger' => $this->danger,
|
||||
'deleted' => $this->deleted,
|
||||
'download' => $this->download,
|
||||
'modal' => $this->modal,
|
||||
'message' => $this->message,
|
||||
'name' => $this->name,
|
||||
'openInNewTab' => $this->openInNewTab,
|
||||
'redirect' => $this->redirect,
|
||||
'visit' => $this->visit,
|
||||
], $this->data));
|
||||
}
|
||||
}
|
||||
18
nova/src/Actions/Actionable.php
Normal file
18
nova/src/Actions/Actionable.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use Laravel\Nova\Nova;
|
||||
|
||||
trait Actionable
|
||||
{
|
||||
/**
|
||||
* Get all of the action events for the user.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function actions()
|
||||
{
|
||||
return $this->morphMany(Nova::actionEvent(), 'actionable');
|
||||
}
|
||||
}
|
||||
117
nova/src/Actions/CallQueuedAction.php
Normal file
117
nova/src/Actions/CallQueuedAction.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Contracts\BatchableAction;
|
||||
use Laravel\Nova\Fields\ActionFields;
|
||||
use Laravel\Nova\Nova;
|
||||
|
||||
#[\AllowDynamicProperties]
|
||||
class CallQueuedAction
|
||||
{
|
||||
use Batchable;
|
||||
use CallsQueuedActions;
|
||||
|
||||
/**
|
||||
* The Eloquent model/data collection.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Collection|\Illuminate\Support\Collection
|
||||
*/
|
||||
public $models;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param \Laravel\Nova\Actions\Action $action
|
||||
* @param string $method
|
||||
* @param \Laravel\Nova\Fields\ActionFields $fields
|
||||
* @param \Illuminate\Support\Collection $models
|
||||
* @param string $actionBatchId
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Action $action, $method, ActionFields $fields, Collection $models, $actionBatchId)
|
||||
{
|
||||
$this->action = $action;
|
||||
$this->method = $method;
|
||||
$this->fields = $fields;
|
||||
$this->models = $models;
|
||||
$this->actionBatchId = $actionBatchId;
|
||||
|
||||
if (property_exists($action, 'timeout')) {
|
||||
/** @phpstan-ignore-next-line */
|
||||
$this->timeout = $action->timeout;
|
||||
}
|
||||
|
||||
if (property_exists($action, 'tries')) {
|
||||
/** @phpstan-ignore-next-line */
|
||||
$this->tries = $action->tries;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->callAction(function ($action) {
|
||||
if ($action instanceof BatchableAction) {
|
||||
$action->withBatchId($this->batchId);
|
||||
}
|
||||
|
||||
return $action->withActionBatchId($this->actionBatchId)
|
||||
->{$this->method}($this->fields, $this->models);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the failed method on the job instance.
|
||||
*
|
||||
* @param \Exception $e
|
||||
* @return void
|
||||
*/
|
||||
public function failed($e)
|
||||
{
|
||||
Nova::usingActionEvent(function ($actionEvent) use ($e) {
|
||||
if (! $this->action->withoutActionEvents) {
|
||||
$actionEvent->markBatchAsFailed($this->actionBatchId, $e);
|
||||
}
|
||||
});
|
||||
|
||||
if ($method = $this->failedMethodName()) {
|
||||
call_user_func([$this->action, $method], $this->fields, $this->models, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the "failed" method that should be called for the action.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function failedMethodName()
|
||||
{
|
||||
if (($method = $this->failedMethodForModel()) &&
|
||||
method_exists($this->action, $method)) {
|
||||
return $method;
|
||||
}
|
||||
|
||||
return method_exists($this->action, 'failed')
|
||||
? 'failed' : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate "failed" method name for the action's model type.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function failedMethodForModel()
|
||||
{
|
||||
if ($this->models->isNotEmpty()) {
|
||||
return 'failedFor'.Str::plural(class_basename($this->models->first()));
|
||||
}
|
||||
}
|
||||
}
|
||||
97
nova/src/Actions/CallsQueuedActions.php
Normal file
97
nova/src/Actions/CallsQueuedActions.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Laravel\Nova\Nova;
|
||||
|
||||
trait CallsQueuedActions
|
||||
{
|
||||
use Batchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* The action class name.
|
||||
*
|
||||
* @var \Laravel\Nova\Actions\Action
|
||||
*/
|
||||
public $action;
|
||||
|
||||
/**
|
||||
* The method that should be called on the action.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $method;
|
||||
|
||||
/**
|
||||
* The resolved fields.
|
||||
*
|
||||
* @var \Laravel\Nova\Fields\ActionFields
|
||||
*/
|
||||
public $fields;
|
||||
|
||||
/**
|
||||
* The batch ID of the action event records.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $actionBatchId;
|
||||
|
||||
/**
|
||||
* Call the action using the given callback.
|
||||
*
|
||||
* @param callable(\Laravel\Nova\Actions\Action):void $callback
|
||||
* @return void
|
||||
*/
|
||||
protected function callAction($callback)
|
||||
{
|
||||
Nova::usingActionEvent(function ($actionEvent) {
|
||||
if (! $this->action->withoutActionEvents) {
|
||||
$actionEvent->markBatchAsRunning($this->actionBatchId);
|
||||
}
|
||||
});
|
||||
|
||||
$action = $this->setJobInstanceIfNecessary($this->action);
|
||||
|
||||
$callback($action);
|
||||
|
||||
if (! $this->job->hasFailed() && ! $this->job->isReleased()) {
|
||||
Nova::usingActionEvent(function ($actionEvent) {
|
||||
if (! $this->action->withoutActionEvents) {
|
||||
$actionEvent->markBatchAsFinished($this->actionBatchId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the job instance of the given class if necessary.
|
||||
*
|
||||
* @param mixed $instance
|
||||
* @return mixed
|
||||
*/
|
||||
protected function setJobInstanceIfNecessary($instance)
|
||||
{
|
||||
if (in_array(InteractsWithQueue::class, class_uses_recursive(get_class($instance)))) {
|
||||
$instance->setJob($this->job);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name for the queued job.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function displayName()
|
||||
{
|
||||
return get_class($this->action);
|
||||
}
|
||||
}
|
||||
8
nova/src/Actions/DestructiveAction.php
Normal file
8
nova/src/Actions/DestructiveAction.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
class DestructiveAction extends Action
|
||||
{
|
||||
//
|
||||
}
|
||||
317
nova/src/Actions/DispatchAction.php
Normal file
317
nova/src/Actions/DispatchAction.php
Normal file
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use Illuminate\Bus\PendingBatch;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Laravel\Nova\Contracts\BatchableAction;
|
||||
use Laravel\Nova\Fields\ActionFields;
|
||||
use Laravel\Nova\Http\Requests\ActionRequest;
|
||||
use Laravel\Nova\Nova;
|
||||
|
||||
class DispatchAction
|
||||
{
|
||||
/**
|
||||
* The request instance.
|
||||
*
|
||||
* @var \Laravel\Nova\Http\Requests\ActionRequest
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* The action instance.
|
||||
*
|
||||
* @var \Laravel\Nova\Actions\Action
|
||||
*/
|
||||
protected $action;
|
||||
|
||||
/**
|
||||
* The fields for action instance.
|
||||
*
|
||||
* @var \Laravel\Nova\Fields\ActionFields
|
||||
*/
|
||||
protected $fields;
|
||||
|
||||
/**
|
||||
* The pending batch instance (if the action implements BatchableAction).
|
||||
*
|
||||
* @var \Illuminate\Bus\PendingBatch|null
|
||||
*/
|
||||
protected $batchJob;
|
||||
|
||||
/**
|
||||
* Set dispatchable callback.
|
||||
*
|
||||
* @var (callable(\Laravel\Nova\Actions\Response):(mixed))|null
|
||||
*/
|
||||
protected $dispatchableCallback;
|
||||
|
||||
/**
|
||||
* Create a new action dispatcher instance.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
|
||||
* @param \Laravel\Nova\Actions\Action $action
|
||||
* @param \Laravel\Nova\Fields\ActionFields $fields
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(ActionRequest $request, Action $action, ActionFields $fields)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->action = $action;
|
||||
$this->fields = $fields;
|
||||
|
||||
if ($action instanceof BatchableAction) {
|
||||
$this->configureBatchJob($action, $fields);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the batch job for the action.
|
||||
*
|
||||
* @param \Laravel\Nova\Actions\Action $action
|
||||
* @param \Laravel\Nova\Fields\ActionFields $fields
|
||||
* @return void
|
||||
*/
|
||||
protected function configureBatchJob(Action $action, ActionFields $fields)
|
||||
{
|
||||
$this->batchJob = tap(Bus::batch([]), function (PendingBatch $batch) use ($action, $fields) {
|
||||
$batch->name($action->name());
|
||||
|
||||
if (! is_null($connection = $this->connection())) {
|
||||
$batch->onConnection($connection);
|
||||
}
|
||||
|
||||
if (! is_null($queue = $this->queue())) {
|
||||
$batch->onQueue($queue);
|
||||
}
|
||||
|
||||
$action->withBatch($fields, $batch);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the action.
|
||||
*
|
||||
* @return \Laravel\Nova\Actions\Response
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function dispatch()
|
||||
{
|
||||
if ($this->action instanceof ShouldQueue) {
|
||||
return tap(new Response(), function ($response) {
|
||||
with($response, $this->dispatchableCallback);
|
||||
|
||||
if (! is_null($this->batchJob)) {
|
||||
$this->batchJob->dispatch();
|
||||
}
|
||||
|
||||
return $response->successful();
|
||||
});
|
||||
}
|
||||
|
||||
return with(new Response(), $this->dispatchableCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the given action.
|
||||
*
|
||||
* @param string $method
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handleStandalone($method)
|
||||
{
|
||||
$this->dispatchableCallback = function (Response $response) use ($method) {
|
||||
if ($this->action instanceof ShouldQueue) {
|
||||
$this->addQueuedActionJob($method, collect());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return $response->successful([
|
||||
$this->dispatchSynchronouslyForCollection($method, collect()),
|
||||
]);
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the given action.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
|
||||
* @param string $method
|
||||
* @param int $chunkCount
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handleRequest(ActionRequest $request, $method, $chunkCount)
|
||||
{
|
||||
$this->dispatchableCallback = function (Response $response) use ($request, $method, $chunkCount) {
|
||||
if ($this->action instanceof ShouldQueue) {
|
||||
$request->chunks($chunkCount, function ($models) use ($request, $method) {
|
||||
$models = $models->filterForExecution($request);
|
||||
|
||||
return $this->forModels($method, $models);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$wasExecuted = false;
|
||||
|
||||
$results = $request->chunks(
|
||||
$chunkCount, function ($models) use ($request, $method, &$wasExecuted) {
|
||||
$models = $models->filterForExecution($request);
|
||||
|
||||
if (count($models) > 0) {
|
||||
$wasExecuted = true;
|
||||
}
|
||||
|
||||
return $this->forModels($method, $models);
|
||||
}
|
||||
);
|
||||
|
||||
return $wasExecuted ? $response->successful($results) : $response->failed();
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the given action using custom handler.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
|
||||
* @param \Closure(\Laravel\Nova\Http\Requests\ActionRequest, \Laravel\Nova\Actions\Response, \Laravel\Nova\Fields\ActionFields):\Laravel\Nova\Actions\Response $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function handleUsing(ActionRequest $request, $callback)
|
||||
{
|
||||
$this->dispatchableCallback = function (Response $response) use ($request, $callback) {
|
||||
return $callback($request, $response, $this->fields);
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the given action.
|
||||
*
|
||||
* @param string $method
|
||||
* @param \Illuminate\Support\Collection $models
|
||||
* @return mixed|void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function forModels($method, Collection $models)
|
||||
{
|
||||
if ($this->action->isStandalone() || $models->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->action instanceof ShouldQueue) {
|
||||
$this->addQueuedActionJob($method, $models);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->dispatchSynchronouslyForCollection($method, $models);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the given action synchronously for a model collection.
|
||||
*
|
||||
* @param string $method
|
||||
* @param \Illuminate\Support\Collection $models
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function dispatchSynchronouslyForCollection($method, Collection $models)
|
||||
{
|
||||
return Transaction::run(function ($batchId) use ($method, $models) {
|
||||
Nova::usingActionEvent(function ($actionEvent) use ($batchId, $models) {
|
||||
if (! $this->action->withoutActionEvents) {
|
||||
$actionEvent->createForModels(
|
||||
$this->request, $this->action, $batchId, $models
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return $this->action->withActionBatchId($batchId)->{$method}($this->fields, $models);
|
||||
}, function ($batchId) {
|
||||
Nova::usingActionEvent(function ($actionEvent) use ($batchId) {
|
||||
if (! $this->action->withoutActionEvents) {
|
||||
$actionEvent->markBatchAsFinished($batchId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the given action to the queue for a model collection.
|
||||
*
|
||||
* @param string $method
|
||||
* @param \Illuminate\Support\Collection $models
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function addQueuedActionJob($method, Collection $models)
|
||||
{
|
||||
return Transaction::run(function ($batchId) use ($method, $models) {
|
||||
Nova::usingActionEvent(function ($actionEvent) use ($batchId, $models) {
|
||||
if (! $this->action->withoutActionEvents) {
|
||||
$actionEvent->createForModels(
|
||||
$this->request, $this->action, $batchId, $models, 'waiting'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$job = new CallQueuedAction(
|
||||
$this->action, $method, $this->request->resolveFields(), $models, $batchId
|
||||
);
|
||||
|
||||
if ($this->action instanceof BatchableAction) {
|
||||
$this->batchJob->add([$job]);
|
||||
|
||||
$this->batchJob->options['resourceIds'] = array_values(array_unique(array_merge(
|
||||
$this->batchJob->options['resourceIds'] ?? [],
|
||||
$models->map(function ($model) {
|
||||
return $model->getKey();
|
||||
})->all()
|
||||
)));
|
||||
} else {
|
||||
Queue::connection($this->connection())->pushOn(
|
||||
$this->queue(), $job
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the queue connection for the action.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function connection()
|
||||
{
|
||||
return property_exists($this->action, 'connection') ? $this->action->connection : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the queue name for the action.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function queue()
|
||||
{
|
||||
return property_exists($this->action, 'queue') ? $this->action->queue : null;
|
||||
}
|
||||
}
|
||||
242
nova/src/Actions/ExportAsCsv.php
Normal file
242
nova/src/Actions/ExportAsCsv.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use InvalidArgumentException;
|
||||
use Laravel\Nova\Fields\ActionFields;
|
||||
use Laravel\Nova\Fields\Select;
|
||||
use Laravel\Nova\Fields\Text;
|
||||
use Laravel\Nova\Http\Requests\ActionRequest;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Nova;
|
||||
use Laravel\Nova\Rules\Filename;
|
||||
use Rap2hpoutre\FastExcel\FastExcel;
|
||||
use Symfony\Component\HttpFoundation\HeaderUtils;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class ExportAsCsv extends Action
|
||||
{
|
||||
/**
|
||||
* The XHR response type on executing the action.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $responseType = 'blob';
|
||||
|
||||
/**
|
||||
* All of the defined action fields.
|
||||
*
|
||||
* @var \Illuminate\Support\Collection
|
||||
*/
|
||||
public $actionFields;
|
||||
|
||||
/**
|
||||
* The custom query callback.
|
||||
*
|
||||
* @var (\Closure(\Illuminate\Database\Eloquent\Builder, \Laravel\Nova\Fields\ActionFields):(\Illuminate\Database\Eloquent\Builder))|null
|
||||
*/
|
||||
public $withQueryCallback;
|
||||
|
||||
/**
|
||||
* The custom field callback.
|
||||
*
|
||||
* @var (\Closure(\Laravel\Nova\Http\Requests\NovaRequest):(array<int, \Laravel\Nova\Fields\Field>))|null
|
||||
*/
|
||||
public $withFieldsCallback;
|
||||
|
||||
/**
|
||||
* The custom format callback.
|
||||
*
|
||||
* @var (\Closure(\Illuminate\Database\Eloquent\Model):(array<string, mixed>))|null
|
||||
*/
|
||||
public $withFormatCallback;
|
||||
|
||||
/**
|
||||
* Indicates action events should be logged for models.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $withoutActionEvents = true;
|
||||
|
||||
/**
|
||||
* Construct a new action instance.
|
||||
*
|
||||
* @param string|null $name
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($name = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->actionFields = collect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fields available on the action.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return array
|
||||
*/
|
||||
public function fields(NovaRequest $request)
|
||||
{
|
||||
if ($this->withFieldsCallback instanceof Closure) {
|
||||
$this->actionFields = $this->actionFields->merge(call_user_func($this->withFieldsCallback, $request));
|
||||
}
|
||||
|
||||
return $this->actionFields->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the action request using custom dispatch handler.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
|
||||
* @param \Laravel\Nova\Actions\Response $response
|
||||
* @param \Laravel\Nova\Fields\ActionFields $fields
|
||||
* @return \Laravel\Nova\Actions\Response
|
||||
*/
|
||||
protected function dispatchRequestUsing(ActionRequest $request, Response $response, ActionFields $fields)
|
||||
{
|
||||
$this->then(function ($results) {
|
||||
return $results->first();
|
||||
});
|
||||
|
||||
$query = $request->toSelectedResourceQuery();
|
||||
|
||||
$query->when($this->withQueryCallback instanceof Closure, function ($query) use ($fields) {
|
||||
return call_user_func($this->withQueryCallback, $query, $fields);
|
||||
});
|
||||
|
||||
$eloquentGenerator = function () use ($query) {
|
||||
foreach ($query->cursor() as $model) {
|
||||
yield $model;
|
||||
}
|
||||
};
|
||||
|
||||
$filename = $fields->get('filename') ?? sprintf('%s-%d.csv', $this->uriKey(), now()->format('YmdHis'));
|
||||
|
||||
$extension = 'csv';
|
||||
|
||||
if (Str::contains($filename, '.')) {
|
||||
[$filename, $extension] = explode('.', $filename);
|
||||
}
|
||||
|
||||
$exportFilename = sprintf(
|
||||
'%s.%s',
|
||||
$filename,
|
||||
$fields->get('writerType') ?? $extension
|
||||
);
|
||||
|
||||
return $response->successful([
|
||||
tap(
|
||||
(new FastExcel($eloquentGenerator()))->download($exportFilename, $this->withFormatCallback),
|
||||
function ($response) use ($exportFilename) {
|
||||
if ($response instanceof StreamedResponse && ! $response->headers->has('Content-Disposition')) {
|
||||
$response->headers->set(
|
||||
'Content-Disposition',
|
||||
HeaderUtils::makeDisposition(
|
||||
HeaderUtils::DISPOSITION_ATTACHMENT, $exportFilename, str_replace('%', '', Str::ascii($exportFilename))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a callback that modifies the query used to retrieve the selected models.
|
||||
*
|
||||
* @param (\Closure(\Illuminate\Database\Eloquent\Builder, \Laravel\Nova\Fields\ActionFields):(\Illuminate\Database\Eloquent\Builder))|null $withQueryCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function withQuery($withQueryCallback)
|
||||
{
|
||||
$this->withQueryCallback = $withQueryCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a callback that defines the fields that should be present within the generated file.
|
||||
*
|
||||
* @param (\Closure(\Laravel\Nova\Http\Requests\NovaRequest):(array<int, \Laravel\Nova\Fields\Field>))|null $withFieldsCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function withFields($withFieldsCallback)
|
||||
{
|
||||
$this->withFieldsCallback = $withFieldsCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a callback that defines the field formatting for the generated file.
|
||||
*
|
||||
* @param (\Closure(\Illuminate\Database\Eloquent\Model):(array<string, mixed>))|null $withFormatCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function withFormat($withFormatCallback)
|
||||
{
|
||||
$this->withFormatCallback = $withFormatCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Select field to the action that allows the selection of the generated file's type.
|
||||
*
|
||||
* @param (\Closure(\Laravel\Nova\Http\Requests\NovaRequest):(?string))|string|null $default
|
||||
* @return $this
|
||||
*/
|
||||
public function withTypeSelector($default = null)
|
||||
{
|
||||
$this->actionFields->push(
|
||||
Select::make(Nova::__('Type'), 'writerType')->options(function () {
|
||||
return [
|
||||
'csv' => Nova::__('CSV (.csv)'),
|
||||
'xlsx' => Nova::__('Excel (.xlsx)'),
|
||||
];
|
||||
})->default($default)->rules(['required', Rule::in(['csv', 'xlsx'])])
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Text field to the action to allow users to define the generated file's name.
|
||||
*
|
||||
* @param (\Closure(\Laravel\Nova\Http\Requests\NovaRequest):(?string))|string|null $default
|
||||
* @return $this
|
||||
*/
|
||||
public function nameable($default = null)
|
||||
{
|
||||
$this->actionFields->push(
|
||||
Text::make(Nova::__('Filename'), 'filename')->default($default)->rules(['required', 'min:1', new Filename()])
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the displayable name of the action.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name()
|
||||
{
|
||||
return $this->name ?: 'Export As CSV';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the action as a standalone action.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function standalone()
|
||||
{
|
||||
throw new InvalidArgumentException('The Export As CSV action may not be registered as a standalone action.');
|
||||
}
|
||||
}
|
||||
48
nova/src/Actions/Response.php
Normal file
48
nova/src/Actions/Response.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class Response
|
||||
{
|
||||
/**
|
||||
* Determine if action was executed.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $wasExecuted = false;
|
||||
|
||||
/**
|
||||
* List of action results.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $results = [];
|
||||
|
||||
/**
|
||||
* Mark response as successful.
|
||||
*
|
||||
* @param array|mixed|null $results
|
||||
* @return $this
|
||||
*/
|
||||
public function successful($results = null)
|
||||
{
|
||||
$this->wasExecuted = true;
|
||||
$this->results = Arr::wrap($results ?? []);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark response as failed.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function failed()
|
||||
{
|
||||
$this->wasExecuted = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
40
nova/src/Actions/Transaction.php
Normal file
40
nova/src/Actions/Transaction.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Actions;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
|
||||
class Transaction
|
||||
{
|
||||
/**
|
||||
* Perform the given callbacks within a batch transaction.
|
||||
*
|
||||
* @param callable(string):mixed $callback
|
||||
* @param (callable(string):(void))|null $finished
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public static function run($callback, $finished = null)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$actionBatchId = (string) Str::orderedUuid();
|
||||
|
||||
return tap($callback($actionBatchId), function ($response) use ($finished, $actionBatchId) {
|
||||
if ($finished) {
|
||||
$finished($actionBatchId);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
});
|
||||
} catch (Throwable $e) {
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user