Initial commit

This commit is contained in:
Developer
2025-04-21 16:03:20 +02:00
commit 2832896157
22874 changed files with 3092801 additions and 0 deletions

767
nova/src/Actions/Action.php Normal file
View File

@@ -0,0 +1,767 @@
<?php
namespace Laravel\Nova\Actions;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use JsonSerializable;
use Laravel\Nova\AuthorizedToSee;
use Laravel\Nova\Exceptions\MissingActionHandlerException;
use Laravel\Nova\Fields\ActionFields;
use Laravel\Nova\Http\Requests\ActionRequest;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Makeable;
use Laravel\Nova\Metable;
use Laravel\Nova\Nova;
use Laravel\Nova\ProxiesCanSeeToGate;
use ReflectionClass;
class Action implements JsonSerializable
{
use Metable, AuthorizedToSee, ProxiesCanSeeToGate, Makeable;
/**
* The displayable name of the action.
*
* @var string
*/
public $name;
/**
* The action's component.
*
* @var string
*/
public $component = 'confirm-action-modal';
/**
* Indicates if need to skip log action events for models.
*
* @var bool
*/
public $withoutActionEvents = false;
/**
* Indicates if this action is available to run against the entire resource.
*
* @var bool
*/
public $availableForEntireResource = false;
/**
* Determine where the action redirection should be without confirmation.
*
* @var bool
*/
public $withoutConfirmation = false;
/**
* Indicates if this action is only available on the resource index view.
*
* @var bool
*/
public $onlyOnIndex = false;
/**
* Indicates if this action is only available on the resource detail view.
*
* @var bool
*/
public $onlyOnDetail = false;
/**
* Indicates if this action is available on the resource index view.
*
* @var bool
*/
public $showOnIndex = true;
/**
* Indicates if this action is available on the resource detail view.
*
* @var bool
*/
public $showOnDetail = true;
/**
* Indicates if this action is available on the resource's table row.
*
* @var bool
*/
public $showOnTableRow = false;
/**
* The current batch ID being handled by the action.
*
* @var string|null
*/
public $batchId;
/**
* The callback used to authorize running the action.
*
* @var \Closure|null
*/
public $runCallback;
/**
* The number of models that should be included in each chunk.
*
* @var int
*/
public static $chunkCount = 200;
/**
* The text to be used for the action's confirm button.
*
* @var string
*/
public $confirmButtonText = 'Run Action';
/**
* The text to be used for the action's cancel button.
*
* @var string
*/
public $cancelButtonText = 'Cancel';
/**
* The text to be used for the action's confirmation text.
*
* @var string
*/
public $confirmText = 'Are you sure you want to run this action?';
/**
* Indicates if the action can be run without any models.
*
* @var bool
*/
public $standalone = false;
/**
* Determine if the action is executable for the given request.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Model $model
* @return bool
*/
public function authorizedToRun(Request $request, $model)
{
return $this->runCallback ? call_user_func($this->runCallback, $request, $model) : true;
}
/**
* Return a message response from the action.
*
* @param string $message
* @return array
*/
public static function message($message)
{
return ['message' => $message];
}
/**
* Return a dangerous message response from the action.
*
* @param string $message
* @return array
*/
public static function danger($message)
{
return ['danger' => $message];
}
/**
* Return a delete response from the action.
*
* @return array
*/
public static function deleted()
{
return ['deleted' => true];
}
/**
* Return a redirect response from the action.
*
* @param string $url
* @return array
*/
public static function redirect($url)
{
return ['redirect' => $url];
}
/**
* Return a Vue router response from the action.
*
* @param string $path
* @param array $query
* @return array
*/
public static function push($path, $query = [])
{
return [
'push' => [
'path' => $path,
'query' => $query,
],
];
}
/**
* Return an open new tab response from the action.
*
* @param string $url
* @return array
*/
public static function openInNewTab($url)
{
return ['openInNewTab' => $url];
}
/**
* Return a download response from the action.
*
* @param string $url
* @param string $name
* @return array
*/
public static function download($url, $name)
{
return ['download' => $url, 'name' => $name];
}
/**
* Return an action modal response from the action.
*
* @param string $modal
* @param array $data
* @return array
*/
public static function modal($modal, $data)
{
return array_merge(['modal' => $modal], $data);
}
/**
* Execute the action for the given request.
*
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
* @return mixed
* @throws MissingActionHandlerException
*/
public function handleRequest(ActionRequest $request)
{
$method = ActionMethod::determine($this, $request->targetModel());
if (! method_exists($this, $method)) {
throw MissingActionHandlerException::make($this, $method);
}
$wasExecuted = false;
$fields = $request->resolveFields();
if ($this->standalone) {
$wasExecuted = true;
$results = [
DispatchAction::forModels(
$request, $this, $method, $results = collect([]), $fields
),
];
} else {
$results = $request->chunks(
static::$chunkCount, function ($models) use ($fields, $request, $method, &$wasExecuted) {
$models = $models->filterForExecution($request);
if (count($models) > 0) {
$wasExecuted = true;
}
return DispatchAction::forModels(
$request, $this, $method, $models, $fields
);
}
);
}
if (! $wasExecuted) {
return static::danger(__('Sorry! You are not authorized to perform this action.'));
}
return $this->handleResult($fields, $results);
}
/**
* Handle chunk results.
*
* @param \Laravel\Nova\Fields\ActionFields $fields
* @param array $results
*
* @return mixed
*/
public function handleResult(ActionFields $fields, $results)
{
return count($results) ? end($results) : null;
}
/**
* Handle any post-validation processing.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param \Illuminate\Contracts\Validation\Validator $validator
* @return void
*/
protected function afterValidation(NovaRequest $request, $validator)
{
//
}
/**
* Mark the action event record for the model as finished.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return int
*/
protected function markAsFinished($model)
{
return $this->batchId ? Nova::actionEvent()->markAsFinished($this->batchId, $model) : 0;
}
/**
* Mark the action event record for the model as failed.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param \Throwable|string $e
* @return int
*/
protected function markAsFailed($model, $e = null)
{
return $this->batchId ? Nova::actionEvent()->markAsFailed($this->batchId, $model, $e) : 0;
}
/**
* Get the fields available on the action.
*
* @return array
*/
public function fields()
{
return [];
}
/**
* Validate the given request.
*
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
* @return array
*
* @throws \Illuminate\Validation\ValidationException
*/
public function validateFields(ActionRequest $request)
{
$fields = collect($this->fields());
return Validator::make(
$request->all(),
$fields->mapWithKeys(function ($field) use ($request) {
return $field->getCreationRules($request);
})->all(),
[],
$fields->reject(function ($field) {
return empty($field->name);
})->mapWithKeys(function ($field) {
return [$field->attribute => $field->name];
})->all()
)->after(function ($validator) use ($request) {
$this->afterValidation($request, $validator);
})->validate();
}
/**
* Indicate that this action can be run for the entire resource at once.
*
* @param bool $value
* @return $this
*
* @deprecated
*/
public function availableForEntireResource($value = true)
{
$this->availableForEntireResource = $value;
return $this;
}
/**
* Indicate that this action is only available on the resource index view.
*
* @param bool $value
* @return $this
*/
public function onlyOnIndex($value = true)
{
$this->onlyOnIndex = $value;
$this->showOnIndex = $value;
$this->showOnDetail = ! $value;
$this->showOnTableRow = ! $value;
return $this;
}
/**
* Indicate that this action is available except on the resource index view.
*
* @return $this
*/
public function exceptOnIndex()
{
$this->showOnDetail = true;
$this->showOnTableRow = true;
$this->showOnIndex = false;
return $this;
}
/**
* Indicate that this action is only available on the resource detail view.
*
* @param bool $value
* @return $this
*/
public function onlyOnDetail($value = true)
{
$this->onlyOnDetail = $value;
$this->showOnDetail = $value;
$this->showOnIndex = ! $value;
$this->showOnTableRow = ! $value;
return $this;
}
/**
* Indicate that this action is available except on the resource detail view.
*
* @return $this
*/
public function exceptOnDetail()
{
$this->showOnIndex = true;
$this->showOnDetail = false;
$this->showOnTableRow = true;
return $this;
}
/**
* Indicate that this action is only available on the resource's table row.
*
* @param bool $value
* @return $this
*/
public function onlyOnTableRow($value = true)
{
$this->showOnTableRow = $value;
$this->showOnIndex = ! $value;
$this->showOnDetail = ! $value;
return $this;
}
/**
* Indicate that this action is available except on the resource's table row.
*
* @return $this
*/
public function exceptOnTableRow()
{
$this->showOnTableRow = false;
$this->showOnIndex = true;
$this->showOnDetail = true;
return $this;
}
/**
* Show the action on the index view.
*
* @return $this
*/
public function showOnIndex()
{
$this->showOnIndex = true;
return $this;
}
/**
* Show the action on the detail view.
*
* @return $this
*/
public function showOnDetail()
{
$this->showOnDetail = true;
return $this;
}
/**
* Show the action on the table row.
*
* @return $this
*/
public function showOnTableRow()
{
$this->showOnTableRow = true;
return $this;
}
/**
* Set the current batch ID being handled by the action.
*
* @param string $batchId
* @return $this
*/
public function withBatchId($batchId)
{
$this->batchId = $batchId;
return $this;
}
/**
* Set the callback to be run to authorize running the action.
*
* @param \Closure $callback
* @return $this
*/
public function canRun(Closure $callback)
{
$this->runCallback = $callback;
return $this;
}
/**
* Get the component name for the action.
*
* @return string
*/
public function component()
{
return $this->component;
}
/**
* Get the displayable name of the action.
*
* @return string
*/
public function name()
{
return $this->name ?: Nova::humanize($this);
}
/**
* Get the URI key for the action.
*
* @return string
*/
public function uriKey()
{
return Str::slug($this->name(), '-', null);
}
/**
* Set the action to execute instantly.
*
* @return $this
*/
public function withoutConfirmation()
{
$this->withoutConfirmation = true;
return $this;
}
/**
* Set the action to skip action events for models.
*
* @return $this
*/
public function withoutActionEvents()
{
$this->withoutActionEvents = true;
return $this;
}
/**
* Determine if the action is to be shown on the index view.
*
* @return bool
*/
public function shownOnIndex()
{
if ($this->onlyOnIndex == true) {
return true;
}
if ($this->onlyOnDetail) {
return false;
}
return $this->showOnIndex;
}
/**
* Determine if the action is to be shown on the detail view.
*
* @return bool
*/
public function shownOnDetail()
{
if ($this->onlyOnDetail) {
return true;
}
if ($this->onlyOnIndex) {
return false;
}
return $this->showOnDetail;
}
/**
* Determine if the action is to be sown on the table row.
*
* @return bool
*/
public function shownOnTableRow()
{
return $this->showOnTableRow;
}
/**
* Set the text for the action's confirmation button.
*
* @param string $text
* @return $this
*/
public function confirmButtonText($text)
{
$this->confirmButtonText = $text;
return $this;
}
/**
* Set the text for the action's cancel button.
*
* @param string $text
* @return $this
*/
public function cancelButtonText($text)
{
$this->cancelButtonText = $text;
return $this;
}
/**
* Set the text for the action's confirmation message.
*
* @param string $text
* @return $this
*/
public function confirmText($text)
{
$this->confirmText = $text;
return $this;
}
/**
* Return the CSS classes for the Action.
*
* @return string
*/
public function actionClass()
{
return $this instanceof DestructiveAction
? 'btn-danger'
: 'btn-primary';
}
/**
* Mark the action as a standalone action.
*
* @return $this
*/
public function standalone()
{
$this->standalone = true;
return $this;
}
/**
* Determine if the action is a standalone action.
*
* @return bool
*/
public function isStandalone()
{
return $this->standalone;
}
/**
* Prepare the action for JSON serialization.
*
* @return array
*/
public function jsonSerialize()
{
$request = app(NovaRequest::class);
return array_merge([
'cancelButtonText' => __($this->cancelButtonText),
'component' => $this->component(),
'confirmButtonText' => __($this->confirmButtonText),
'class' => $this->actionClass(),
'confirmText' => __($this->confirmText),
'destructive' => $this instanceof DestructiveAction,
'name' => $this->name(),
'uriKey' => $this->uriKey(),
'fields' => collect($this->fields())->each->resolveForAction($request)->all(),
'availableForEntireResource' => $this->availableForEntireResource,
'showOnDetail' => $this->shownOnDetail(),
'showOnIndex' => $this->shownOnIndex(),
'showOnTableRow' => $this->shownOnTableRow(),
'standalone' => $this->isStandalone(),
'withoutConfirmation' => $this->withoutConfirmation,
], $this->meta());
}
/**
* Prepare the instance for serialization.
*
* @return array
*/
public function __sleep()
{
$properties = (new ReflectionClass($this))->getProperties();
return array_values(array_filter(array_map(function ($p) {
return ($p->isStatic() || in_array($name = $p->getName(), ['runCallback', 'seeCallback'])) ? null : $name;
}, $properties)));
}
}

View File

@@ -0,0 +1,451 @@
<?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;
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
*/
protected $casts = [
'original' => 'array',
'changes' => '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.
*/
public function user()
{
$provider = config('auth.guards.'.(config('nova.guard') ?? 'web').'.provider');
return $this->belongsTo(
config('auth.providers.'.$provider.'.model'), 'user_id'
);
}
/**
* Get the target of the action for user interface linking.
*/
public function target()
{
return $this->morphTo('target', 'target_type', 'target_id')->withTrashed();
}
/**
* 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' => $model->attributesToArray(),
'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' => '',
'original' => array_intersect_key($model->getRawOriginal(), $model->getDirty()),
'changes' => $model->getDirty(),
'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' => $request->user()->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' => $pivot->attributesToArray(),
'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' => $request->user()->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' => '',
'original' => array_intersect_key($pivot->getRawOriginal(), $pivot->getDirty()),
'changes' => $pivot->getDirty(),
'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
*/
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, $alias) use ($pivotClass) {
return $model === $pivotClass;
})->keys()->first() ?? $pivotClass;
} else {
$modelType = $request->actionableModel()->getMorphClass();
}
return [
'batch_id' => $batchId,
'user_id' => $request->user()->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
*/
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';
}
}

View File

@@ -0,0 +1,22 @@
<?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)
{
$method = 'handleFor'.Str::plural(class_basename($model));
return method_exists($action, $method) ? $method : 'handle';
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Laravel\Nova\Actions;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Laravel\Nova\Http\Requests\ActionRequest;
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();
if (! $request->isPivotAction()) {
$models = $this->filterByResourceAuthorization($request);
} else {
$models = $this;
}
return static::make($models->filter(function ($model) use ($request, $action) {
return $action->authorizedToRun($request, $model);
}));
}
/**
* Remove models the user does not have permission to execute the action against.
*
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
* @return \Illuminate\Support\Collection
*/
protected function filterByResourceAuthorization(ActionRequest $request)
{
if ($request->action()->runCallback) {
$models = $this->mapInto($request->resource())->map->resource;
} else {
$models = $this->mapInto($request->resource())
->filter->authorizedToUpdate($request)->map->resource;
}
$action = $request->action();
if ($action instanceof DestructiveAction) {
$models = $this->mapInto($request->resource())
->filter->authorizedToDelete($request)->map->resource;
}
return $models;
}
}

View File

@@ -0,0 +1,165 @@
<?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\Resource;
class ActionResource extends Resource
{
/**
* The model the resource corresponds to.
*
* @var string
*/
public static $model = ActionEvent::class;
/**
* 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 delete resources.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function authorizedToDelete(Request $request)
{
return false;
}
/**
* Get the fields displayed by the resource.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function fields(Request $request)
{
return [
ID::make('ID', 'id'),
Text::make(__('Action Name'), 'name', function ($value) {
return __($value);
}),
Text::make(__('Action Initiated By'), function () {
return $this->user->name ?? $this->user->email ?? __('Nova User');
}),
MorphToActionTarget::make(__('Action Target'), 'target'),
Status::make(__('Action Status'), 'status', function ($value) {
return __(ucfirst($value));
})->loadingWhen([__('Waiting'), __('Running')])->failedWhen([__('Failed')]),
$this->when(isset($this->original), function () {
return KeyValue::make(__('Original'), 'original');
}),
$this->when(isset($this->changes), function () {
return KeyValue::make(__('Changes'), 'changes');
}),
Textarea::make(__('Exception'), 'exception'),
DateTime::make(__('Action Happened At'), 'created_at')->exceptOnForms(),
];
}
/**
* 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 __('Actions');
}
/**
* Get the displayable singular label of the resource.
*
* @return string
*/
public static function singularLabel()
{
return __('Action');
}
/**
* Get the URI key for the resource.
*
* @return string
*/
public static function uriKey()
{
return 'action-events';
}
}

View 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');
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Laravel\Nova\Actions;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Laravel\Nova\Fields\ActionFields;
use Laravel\Nova\Nova;
class CallQueuedAction
{
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 $batchId
* @return void
*/
public function __construct(Action $action, $method, ActionFields $fields, Collection $models, $batchId)
{
$this->action = $action;
$this->method = $method;
$this->fields = $fields;
$this->models = $models;
$this->batchId = $batchId;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
return $this->callAction(function ($action) {
return $action->withBatchId($this->batchId)->{$this->method}($this->fields, $this->models);
});
}
/**
* Call the failed method on the job instance.
*
* @param \Exception $e
* @return void
*/
public function failed($e)
{
Nova::actionEvent()->markBatchAsFailed($this->batchId, $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()));
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Laravel\Nova\Actions;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Laravel\Nova\Nova;
trait CallsQueuedActions
{
use InteractsWithQueue, 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 $batchId;
/**
* Call the action using the given callback.
*
* @param callable $callback
* @return void
*/
protected function callAction($callback)
{
Nova::actionEvent()->markBatchAsRunning($this->batchId);
$action = $this->setJobInstanceIfNecessary($this->action);
$callback($action);
if (! $this->job->hasFailed() && ! $this->job->isReleased()) {
Nova::actionEvent()->markBatchAsFinished($this->batchId);
}
}
/**
* 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);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Laravel\Nova\Actions;
class DestructiveAction extends Action
{
//
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Laravel\Nova\Actions;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Queue;
use Laravel\Nova\Fields\ActionFields;
use Laravel\Nova\Http\Requests\ActionRequest;
use Laravel\Nova\Nova;
class DispatchAction
{
/**
* Dispatch the given action.
*
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
* @param \Laravel\Nova\Actions\Action $action
* @param string $method
* @param \Illuminate\Support\Collection $models
* @param \Laravel\Nova\Fields\ActionFields $fields
* @return mixed
*
* @throws \Throwable
*/
public static function forModels(
ActionRequest $request,
Action $action,
$method,
Collection $models,
ActionFields $fields
) {
if (! $action->isStandalone() && $models->isEmpty()) {
return;
}
if ($action instanceof ShouldQueue) {
return static::queueForModels($request, $action, $method, $models);
}
return Transaction::run(function ($batchId) use ($fields, $request, $action, $method, $models) {
if (! $action->withoutActionEvents) {
Nova::actionEvent()->createForModels($request, $action, $batchId, $models);
}
return $action->withBatchId($batchId)->{$method}($fields, $models);
}, function ($batchId) {
Nova::actionEvent()->markBatchAsFinished($batchId);
});
}
/**
* Dispatch the given action in the background.
*
* @param \Laravel\Nova\Http\Requests\ActionRequest $request
* @param \Laravel\Nova\Actions\Action $action
* @param string $method
* @param \Illuminate\Support\Collection $models
* @return void
*
* @throws \Throwable
*/
protected static function queueForModels(ActionRequest $request, Action $action, $method, Collection $models)
{
return Transaction::run(function ($batchId) use ($request, $action, $method, $models) {
if (! $action->withoutActionEvents) {
Nova::actionEvent()->createForModels($request, $action, $batchId, $models, 'waiting');
}
Queue::connection(static::connection($action))->pushOn(
static::queue($action),
new CallQueuedAction(
$action, $method, $request->resolveFields(), $models, $batchId
)
);
});
}
/**
* Extract the queue connection for the action.
*
* @param \Laravel\Nova\Actions\Action $action
* @return string|null
*/
protected static function connection($action)
{
return property_exists($action, 'connection') ? $action->connection : null;
}
/**
* Extract the queue name for the action.
*
* @param \Laravel\Nova\Actions\Action $action
* @return string|null
*/
protected static function queue($action)
{
return property_exists($action, 'queue') ? $action->queue : null;
}
}

View 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 $callback
* @param callable|null $finished
* @return mixed
*
* @throws \Throwable
*/
public static function run($callback, $finished = null)
{
try {
DB::beginTransaction();
$batchId = (string) Str::orderedUuid();
return tap($callback($batchId), function ($response) use ($finished, $batchId) {
if ($finished) {
$finished($batchId);
}
DB::commit();
});
} catch (Throwable $e) {
DB::rollBack();
throw $e;
}
}
}

295
nova/src/Authorizable.php Normal file
View File

@@ -0,0 +1,295 @@
<?php
namespace Laravel\Nova;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use Laravel\Nova\Http\Requests\NovaRequest;
trait Authorizable
{
/**
* Determine if the given resource is authorizable.
*
* @return bool
*/
public static function authorizable()
{
return ! is_null(Gate::getPolicyFor(static::newModel()));
}
/**
* Determine if the resource should be available for the given request.
*
* @param \Illuminate\Http\Request $request
* @return void
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function authorizeToViewAny(Request $request)
{
if (! static::authorizable()) {
return;
}
$gate = Gate::getPolicyFor(static::newModel());
if (! is_null($gate) && method_exists($gate, 'viewAny')) {
$this->authorizeTo($request, 'viewAny');
}
}
/**
* Determine if the resource should be available for the given request.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public static function authorizedToViewAny(Request $request)
{
if (! static::authorizable()) {
return true;
}
$gate = Gate::getPolicyFor(static::newModel());
return ! is_null($gate) && method_exists($gate, 'viewAny')
? Gate::check('viewAny', get_class(static::newModel()))
: true;
}
/**
* Determine if the current user can view the given resource or throw an exception.
*
* @param \Illuminate\Http\Request $request
* @return void
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function authorizeToView(Request $request)
{
return $this->authorizeTo($request, 'view') && $this->authorizeToViewAny($request);
}
/**
* Determine if the current user can view the given resource.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function authorizedToView(Request $request)
{
return $this->authorizedTo($request, 'view') && $this->authorizedToViewAny($request);
}
/**
* Determine if the current user can create new resources or throw an exception.
*
* @param \Illuminate\Http\Request $request
* @return void
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public static function authorizeToCreate(Request $request)
{
throw_unless(static::authorizedToCreate($request), AuthorizationException::class);
}
/**
* Determine if the current user can create new resources.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public static function authorizedToCreate(Request $request)
{
if (static::authorizable()) {
return Gate::check('create', get_class(static::newModel()));
}
return true;
}
/**
* Determine if the current user can update the given resource or throw an exception.
*
* @param \Illuminate\Http\Request $request
* @return void
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function authorizeToUpdate(Request $request)
{
return $this->authorizeTo($request, 'update');
}
/**
* Determine if the current user can update the given resource.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function authorizedToUpdate(Request $request)
{
return $this->authorizedTo($request, 'update');
}
/**
* Determine if the current user can delete the given resource or throw an exception.
*
* @param \Illuminate\Http\Request $request
* @return void
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function authorizeToDelete(Request $request)
{
return $this->authorizeTo($request, 'delete');
}
/**
* Determine if the current user can delete the given resource.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function authorizedToDelete(Request $request)
{
return $this->authorizedTo($request, 'delete');
}
/**
* Determine if the current user can restore the given resource.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function authorizedToRestore(Request $request)
{
return $this->authorizedTo($request, 'restore');
}
/**
* Determine if the current user can force delete the given resource.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function authorizedToForceDelete(Request $request)
{
return $this->authorizedTo($request, 'forceDelete');
}
/**
* Determine if the user can add / associate models of the given type to the resource.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param \Illuminate\Database\Eloquent\Model|string $model
* @return bool
*/
public function authorizedToAdd(NovaRequest $request, $model)
{
if (! static::authorizable()) {
return true;
}
$gate = Gate::getPolicyFor($this->model());
$method = 'add'.class_basename($model);
return ! is_null($gate) && method_exists($gate, $method)
? Gate::check($method, $this->model())
: true;
}
/**
* Determine if the user can attach any models of the given type to the resource.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param \Illuminate\Database\Eloquent\Model|string $model
* @return bool
*/
public function authorizedToAttachAny(NovaRequest $request, $model)
{
if (! static::authorizable()) {
return true;
}
$gate = Gate::getPolicyFor($this->model());
$method = 'attachAny'.Str::singular(class_basename($model));
return ! is_null($gate) && method_exists($gate, $method)
? Gate::check($method, [$this->model()])
: true;
}
/**
* Determine if the user can attach models of the given type to the resource.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param \Illuminate\Database\Eloquent\Model|string $model
* @return bool
*/
public function authorizedToAttach(NovaRequest $request, $model)
{
if (! static::authorizable()) {
return true;
}
$gate = Gate::getPolicyFor($this->model());
$method = 'attach'.Str::singular(class_basename($model));
return ! is_null($gate) && method_exists($gate, $method)
? Gate::check($method, [$this->model(), $model])
: true;
}
/**
* Determine if the user can detach models of the given type to the resource.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param \Illuminate\Database\Eloquent\Model|string $model
* @param string $relationship
* @return bool
*/
public function authorizedToDetach(NovaRequest $request, $model, $relationship)
{
if (! static::authorizable()) {
return true;
}
$gate = Gate::getPolicyFor($this->model());
$method = 'detach'.Str::singular(class_basename($model));
return ! is_null($gate) && method_exists($gate, $method)
? Gate::check($method, [$this->model(), $model])
: true;
}
/**
* Determine if the current user has a given ability.
*
* @param \Illuminate\Http\Request $request
* @param string $ability
* @return void
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function authorizeTo(Request $request, $ability)
{
throw_unless($this->authorizedTo($request, $ability), AuthorizationException::class);
}
/**
* Determine if the current user can view the given resource.
*
* @param \Illuminate\Http\Request $request
* @param string $ability
* @return bool
*/
public function authorizedTo(Request $request, $ability)
{
return static::authorizable() ? Gate::check($ability, $this->resource) : true;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Laravel\Nova;
use Closure;
use Illuminate\Http\Request;
trait AuthorizedToSee
{
/**
* The callback used to authorize viewing the filter or action.
*
* @var \Closure|null
*/
public $seeCallback;
/**
* Determine if the filter or action should be available for the given request.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public function authorizedToSee(Request $request)
{
return $this->seeCallback ? call_user_func($this->seeCallback, $request) : true;
}
/**
* Set the callback to be run to authorize viewing the filter or action.
*
* @param \Closure $callback
* @return $this
*/
public function canSee(Closure $callback)
{
$this->seeCallback = $callback;
return $this;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Laravel\Nova;
trait AuthorizesRequests
{
/**
* The callback that should be used to authenticate Nova users.
*
* @var \Closure
*/
public static $authUsing;
/**
* Register the Nova authentication callback.
*
* @param \Closure $callback
* @return static
*/
public static function auth($callback)
{
static::$authUsing = $callback;
return new static;
}
/**
* Determine if the given request can access the Nova dashboard.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
public static function check($request)
{
return (static::$authUsing ?: function () {
return app()->environment('local');
})($request);
}
}

38
nova/src/Card.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
namespace Laravel\Nova;
abstract class Card extends Element
{
/**
* The width of the card (1/3, 1/2, or full).
*
* @var string
*/
public $width = '1/3';
/**
* Set the width of the card.
*
* @param string $width
* @return $this
*/
public function width($width)
{
$this->width = $width;
return $this;
}
/**
* Prepare the element for JSON serialization.
*
* @return array
*/
public function jsonSerialize()
{
return array_merge([
'width' => $this->width,
], parent::jsonSerialize());
}
}

25
nova/src/Cards/Help.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
namespace Laravel\Nova\Cards;
use Laravel\Nova\Card;
class Help extends Card
{
/**
* The width of the card (1/3, 1/2, or full).
*
* @var string
*/
public $width = 'full';
/**
* Get the component name for the element.
*
* @return string
*/
public function component()
{
return 'help';
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Laravel\Nova\Concerns;
use Illuminate\Support\Facades\Event;
use Laravel\Nova\Events\NovaServiceProviderRegistered;
use Laravel\Nova\Events\ServingNova;
trait InteractsWithEvents
{
/**
* Register an event listener for the Nova "booted" event.
*
* @param \Closure|string $callback
* @return void
*/
public static function booted($callback)
{
Event::listen(NovaServiceProviderRegistered::class, $callback);
}
/**
* Register an event listener for the Nova "serving" event.
*
* @param \Closure|string $callback
* @return void
*/
public static function serving($callback)
{
Event::listen(ServingNova::class, $callback);
}
/**
* Flush the persistent Nova state.
*
* @return void
*/
public static function flushState()
{
static::$cards = [];
static::$createUserCallback = null;
static::$createUserCommandCallback = null;
static::$dashboards = [];
static::$defaultDashboardCards = [];
static::$jsonVariables = [];
static::$reportCallback = null;
static::$resetsPasswords = false;
static::$resources = [];
static::$resourcesByModel = [];
static::$scripts = [];
static::$styles = [];
static::$themes = [];
static::$tools = [];
static::$userTimezoneCallback = null;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\GeneratorCommand;
use Symfony\Component\Console\Input\InputOption;
class ActionCommand extends GeneratorCommand
{
use ResolvesStubPath;
/**
* The console command name.
*
* @var string
*/
protected $name = 'nova:action';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new action class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Action';
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
if ($this->option('destructive')) {
return $this->resolveStubPath('/stubs/nova/destructive-action.stub');
}
return $this->resolveStubPath('/stubs/nova/action.stub');
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Nova\Actions';
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['destructive', null, InputOption::VALUE_NONE, 'Indicate that the action deletes / destroys resources'],
];
}
}

View File

@@ -0,0 +1,278 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Laravel\Nova\Console\Concerns\AcceptsNameAndVendor;
use Symfony\Component\Process\Process;
class AssetCommand extends Command
{
use AcceptsNameAndVendor, RenamesStubs;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:asset {name}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new asset';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
if (! $this->hasValidNameArgument()) {
return;
}
(new Filesystem)->copyDirectory(
__DIR__.'/asset-stubs',
$this->assetPath()
);
// AssetServiceProvider.php replacements...
$this->replace('{{ namespace }}', $this->assetNamespace(), $this->assetPath().'/src/AssetServiceProvider.stub');
$this->replace('{{ component }}', $this->assetName(), $this->assetPath().'/src/AssetServiceProvider.stub');
$this->replace('{{ name }}', $this->assetName(), $this->assetPath().'/src/AssetServiceProvider.stub');
// Asset composer.json replacements...
$this->replace('{{ name }}', $this->argument('name'), $this->assetPath().'/composer.json');
$this->replace('{{ escapedNamespace }}', $this->escapedAssetNamespace(), $this->assetPath().'/composer.json');
// Rename the stubs with the proper file extensions...
$this->renameStubs();
// Register the asset...
$this->addAssetRepositoryToRootComposer();
$this->addAssetPackageToRootComposer();
$this->addScriptsToNpmPackage();
if ($this->confirm("Would you like to install the asset's NPM dependencies?", true)) {
$this->installNpmDependencies();
$this->output->newLine();
}
if ($this->confirm("Would you like to compile the asset's assets?", true)) {
$this->compile();
$this->output->newLine();
}
if ($this->confirm('Would you like to update your Composer packages?', true)) {
$this->composerUpdate();
}
}
/**
* Get the array of stubs that need PHP file extensions.
*
* @return array
*/
protected function stubsToRename()
{
return [
$this->assetPath().'/src/AssetServiceProvider.stub',
];
}
/**
* Add a path repository for the asset to the application's composer.json file.
*
* @return void
*/
protected function addAssetRepositoryToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['repositories'][] = [
'type' => 'path',
'url' => './'.$this->relativeAssetPath(),
];
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Add a package entry for the asset to the application's composer.json file.
*
* @return void
*/
protected function addAssetPackageToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['require'][$this->argument('name')] = '*';
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
}
/**
* Add a path repository for the asset to the application's composer.json file.
*
* @return void
*/
protected function addScriptsToNpmPackage()
{
$package = json_decode(file_get_contents(base_path('package.json')), true);
$package['scripts']['build-'.$this->assetName()] = 'cd '.$this->relativeAssetPath().' && npm run dev';
$package['scripts']['build-'.$this->assetName().'-prod'] = 'cd '.$this->relativeAssetPath().' && npm run prod';
file_put_contents(
base_path('package.json'),
json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Install the asset's NPM dependencies.
*
* @return void
*/
protected function installNpmDependencies()
{
$this->executeCommand('npm set progress=false && npm install', $this->assetPath());
}
/**
* Compile the asset's assets.
*
* @return void
*/
protected function compile()
{
$this->executeCommand('npm run dev', $this->assetPath());
}
/**
* Update the project's composer dependencies.
*
* @return void
*/
protected function composerUpdate()
{
$this->executeCommand('composer update', getcwd());
}
/**
* Run the given command as a process.
*
* @param string $command
* @param string $path
* @return void
*/
protected function executeCommand($command, $path)
{
$process = (Process::fromShellCommandline($command, $path))->setTimeout(null);
if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
$process->setTty(true);
}
$process->run(function ($type, $line) {
$this->output->write($line);
});
}
/**
* Replace the given string in the given file.
*
* @param string $search
* @param string $replace
* @param string $path
* @return void
*/
protected function replace($search, $replace, $path)
{
file_put_contents($path, str_replace($search, $replace, file_get_contents($path)));
}
/**
* Get the path to the asset.
*
* @return string
*/
protected function assetPath()
{
return base_path('nova-components/'.$this->assetClass());
}
/**
* Get the relative path to the asset.
*
* @return string
*/
protected function relativeAssetPath()
{
return 'nova-components/'.$this->assetClass();
}
/**
* Get the asset's namespace.
*
* @return string
*/
protected function assetNamespace()
{
return Str::studly($this->assetVendor()).'\\'.$this->assetClass();
}
/**
* Get the asset's escaped namespace.
*
* @return string
*/
protected function escapedAssetNamespace()
{
return str_replace('\\', '\\\\', $this->assetNamespace());
}
/**
* Get the asset's class name.
*
* @return string
*/
protected function assetClass()
{
return Str::studly($this->assetName());
}
/**
* Get the asset's vendor.
*
* @return string
*/
protected function assetVendor()
{
return explode('/', $this->argument('name'))[0];
}
/**
* Get the asset's base name.
*
* @return string
*/
protected function assetName()
{
return explode('/', $this->argument('name'))[1];
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\GeneratorCommand;
class BaseResourceCommand extends GeneratorCommand
{
use ResolvesStubPath;
/**
* The console command name.
*
* @var string
*/
protected $name = 'nova:base-resource';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new base resource class';
/**
* Indicates whether the command should be shown in the Artisan command list.
*
* @var bool
*/
protected $hidden = true;
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Resource';
/**
* Execute the console command.
*
* @return bool|null
*/
public function handle()
{
parent::handle();
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return $this->resolveStubPath('/stubs/nova/base-resource.stub');
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Nova';
}
}

View File

@@ -0,0 +1,303 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Laravel\Nova\Console\Concerns\AcceptsNameAndVendor;
use Symfony\Component\Process\Process;
class CardCommand extends Command
{
use AcceptsNameAndVendor, RenamesStubs;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:card {name}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new card';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
if (! $this->hasValidNameArgument()) {
return;
}
(new Filesystem)->copyDirectory(
__DIR__.'/card-stubs',
$this->cardPath()
);
// Card.js replacements...
$this->replace('{{ title }}', $this->cardTitle(), $this->cardPath().'/resources/js/components/Card.vue');
$this->replace('{{ component }}', $this->cardName(), $this->cardPath().'/resources/js/card.js');
// Card.php replacements...
$this->replace('{{ namespace }}', $this->cardNamespace(), $this->cardPath().'/src/Card.stub');
$this->replace('{{ class }}', $this->cardClass(), $this->cardPath().'/src/Card.stub');
$this->replace('{{ component }}', $this->cardName(), $this->cardPath().'/src/Card.stub');
(new Filesystem)->move(
$this->cardPath().'/src/Card.stub',
$this->cardPath().'/src/'.$this->cardClass().'.php'
);
// CardServiceProvider.php replacements...
$this->replace('{{ namespace }}', $this->cardNamespace(), $this->cardPath().'/src/CardServiceProvider.stub');
$this->replace('{{ component }}', $this->cardName(), $this->cardPath().'/src/CardServiceProvider.stub');
$this->replace('{{ name }}', $this->cardName(), $this->cardPath().'/src/CardServiceProvider.stub');
// Card composer.json replacements...
$this->replace('{{ name }}', $this->argument('name'), $this->cardPath().'/composer.json');
$this->replace('{{ escapedNamespace }}', $this->escapedCardNamespace(), $this->cardPath().'/composer.json');
// Rename the stubs with the proper file extensions...
$this->renameStubs();
// Register the card...
$this->addCardRepositoryToRootComposer();
$this->addCardPackageToRootComposer();
$this->addScriptsToNpmPackage();
if ($this->confirm("Would you like to install the card's NPM dependencies?", true)) {
$this->installNpmDependencies();
$this->output->newLine();
}
if ($this->confirm("Would you like to compile the card's assets?", true)) {
$this->compile();
$this->output->newLine();
}
if ($this->confirm('Would you like to update your Composer packages?', true)) {
$this->composerUpdate();
}
}
/**
* Get the array of stubs that need PHP file extensions.
*
* @return array
*/
protected function stubsToRename()
{
return [
$this->cardPath().'/src/CardServiceProvider.stub',
$this->cardPath().'/routes/api.stub',
];
}
/**
* Add a path repository for the card to the application's composer.json file.
*
* @return void
*/
protected function addCardRepositoryToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['repositories'][] = [
'type' => 'path',
'url' => './'.$this->relativeCardPath(),
];
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Add a package entry for the card to the application's composer.json file.
*
* @return void
*/
protected function addCardPackageToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['require'][$this->argument('name')] = '*';
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Add a path repository for the card to the application's composer.json file.
*
* @return void
*/
protected function addScriptsToNpmPackage()
{
$package = json_decode(file_get_contents(base_path('package.json')), true);
$package['scripts']['build-'.$this->cardName()] = 'cd '.$this->relativeCardPath().' && npm run dev';
$package['scripts']['build-'.$this->cardName().'-prod'] = 'cd '.$this->relativeCardPath().' && npm run prod';
file_put_contents(
base_path('package.json'),
json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Install the card's NPM dependencies.
*
* @return void
*/
protected function installNpmDependencies()
{
$this->executeCommand('npm set progress=false && npm install', $this->cardPath());
}
/**
* Compile the card's assets.
*
* @return void
*/
protected function compile()
{
$this->executeCommand('npm run dev', $this->cardPath());
}
/**
* Update the project's composer dependencies.
*
* @return void
*/
protected function composerUpdate()
{
$this->executeCommand('composer update', getcwd());
}
/**
* Run the given command as a process.
*
* @param string $command
* @param string $path
* @return void
*/
protected function executeCommand($command, $path)
{
$process = (Process::fromShellCommandline($command, $path))->setTimeout(null);
if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
$process->setTty(true);
}
$process->run(function ($type, $line) {
$this->output->write($line);
});
}
/**
* Replace the given string in the given file.
*
* @param string $search
* @param string $replace
* @param string $path
* @return void
*/
protected function replace($search, $replace, $path)
{
file_put_contents($path, str_replace($search, $replace, file_get_contents($path)));
}
/**
* Get the path to the card.
*
* @return string
*/
protected function cardPath()
{
return base_path('nova-components/'.$this->cardClass());
}
/**
* Get the relative path to the card.
*
* @return string
*/
protected function relativeCardPath()
{
return 'nova-components/'.$this->cardClass();
}
/**
* Get the card's namespace.
*
* @return string
*/
protected function cardNamespace()
{
return Str::studly($this->cardVendor()).'\\'.$this->cardClass();
}
/**
* Get the card's escaped namespace.
*
* @return string
*/
protected function escapedCardNamespace()
{
return str_replace('\\', '\\\\', $this->cardNamespace());
}
/**
* Get the card's class name.
*
* @return string
*/
protected function cardClass()
{
return Str::studly($this->cardName());
}
/**
* Get the card's vendor.
*
* @return string
*/
protected function cardVendor()
{
return explode('/', $this->argument('name'))[0];
}
/**
* Get the "title" name of the card.
*
* @return string
*/
protected function cardTitle()
{
return Str::title(str_replace('-', ' ', $this->cardName()));
}
/**
* Get the card's base name.
*
* @return string
*/
protected function cardName()
{
return explode('/', $this->argument('name'))[1];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Laravel\Nova\Console\Concerns;
use Illuminate\Support\Str;
trait AcceptsNameAndVendor
{
/**
* Determine if the name argument is valid.
*
* @return bool
*/
public function hasValidNameArgument()
{
$name = $this->argument('name');
if (! Str::contains($name, '/')) {
$this->error("The name argument expects a vendor and name in 'Composer' format. Here's an example: `vendor/name`.");
return false;
}
return true;
}
}

View File

@@ -0,0 +1,321 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Laravel\Nova\Console\Concerns\AcceptsNameAndVendor;
use Symfony\Component\Process\Process;
class CustomFilterCommand extends Command
{
use AcceptsNameAndVendor, RenamesStubs;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:custom-filter {name}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new custom filter';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
if (! $this->hasValidNameArgument()) {
return;
}
(new Filesystem)->copyDirectory(
__DIR__.'/filter-stubs',
$this->filterPath()
);
// Filter.js replacements...
$this->replace('{{ component }}', $this->filterName(), $this->filterPath().'/resources/js/filter.js');
// Filter.php replacements...
$this->replace('{{ namespace }}', $this->filterNamespace(), $this->filterPath().'/src/Filter.stub');
$this->replace('{{ class }}', $this->filterClass(), $this->filterPath().'/src/Filter.stub');
$this->replace('{{ component }}', $this->filterName(), $this->filterPath().'/src/Filter.stub');
(new Filesystem)->move(
$this->filterPath().'/src/Filter.stub',
$this->filterPath().'/src/'.$this->filterClass().'.php'
);
// FilterServiceProvider.php replacements...
$this->replace('{{ namespace }}', $this->filterNamespace(), $this->filterPath().'/src/FilterServiceProvider.stub');
$this->replace('{{ component }}', $this->filterName(), $this->filterPath().'/src/FilterServiceProvider.stub');
// Filter composer.json replacements...
$this->replace('{{ name }}', $this->argument('name'), $this->filterPath().'/composer.json');
$this->replace('{{ escapedNamespace }}', $this->escapedFilterNamespace(), $this->filterPath().'/composer.json');
// Rename the stubs with the proper file extensions...
$this->renameStubs();
// Register the filter...
$this->addFilterRepositoryToRootComposer();
$this->addFilterPackageToRootComposer();
$this->addScriptsToNpmPackage();
if ($this->confirm("Would you like to install the filter's NPM dependencies?", true)) {
$this->installNpmDependencies();
$this->output->newLine();
}
if ($this->confirm("Would you like to compile the filter's assets?", true)) {
$this->compile();
$this->output->newLine();
}
if ($this->confirm('Would you like to update your Composer packages?', true)) {
$this->composerUpdate();
}
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return __DIR__.'/stubs/filter.stub';
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Nova\Filters';
}
/**
* Get the array of stubs that need PHP file extensions.
*
* @return array
*/
protected function stubsToRename()
{
return [
$this->filterPath().'/src/FilterServiceProvider.stub',
];
}
/**
* Add a path repository for the filter to the application's composer.json file.
*
* @return void
*/
protected function addFilterRepositoryToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['repositories'][] = [
'type' => 'path',
'url' => './'.$this->relativeFilterPath(),
];
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Add a package entry for the filter to the application's composer.json file.
*
* @return void
*/
protected function addFilterPackageToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['require'][$this->argument('name')] = '*';
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Add a path repository for the filter to the application's composer.json file.
*
* @return void
*/
protected function addScriptsToNpmPackage()
{
$package = json_decode(file_get_contents(base_path('package.json')), true);
$package['scripts']['build-'.$this->filterName()] = 'cd '.$this->relativeFilterPath().' && npm run dev';
$package['scripts']['build-'.$this->filterName().'-prod'] = 'cd '.$this->relativeFilterPath().' && npm run prod';
file_put_contents(
base_path('package.json'),
json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Install the filter's NPM dependencies.
*
* @return void
*/
protected function installNpmDependencies()
{
$this->executeCommand('npm set progress=false && npm install', $this->filterPath());
}
/**
* Compile the filter's assets.
*
* @return void
*/
protected function compile()
{
$this->executeCommand('npm run dev', $this->filterPath());
}
/**
* Update the project's composer dependencies.
*
* @return void
*/
protected function composerUpdate()
{
$this->executeCommand('composer update', getcwd());
}
/**
* Run the given command as a process.
*
* @param string $command
* @param string $path
* @return void
*/
protected function executeCommand($command, $path)
{
$process = (Process::fromShellCommandline($command, $path))->setTimeout(null);
if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
$process->setTty(true);
}
$process->run(function ($type, $line) {
$this->output->write($line);
});
}
/**
* Replace the given string in the given file.
*
* @param string $search
* @param string $replace
* @param string $path
* @return void
*/
protected function replace($search, $replace, $path)
{
file_put_contents($path, str_replace($search, $replace, file_get_contents($path)));
}
/**
* Get the path to the filter.
*
* @return string
*/
protected function filterPath()
{
return base_path('nova-components/'.$this->filterClass());
}
/**
* Get the relative path to the filter.
*
* @return string
*/
protected function relativeFilterPath()
{
return 'nova-components/'.$this->filterClass();
}
/**
* Get the filter's namespace.
*
* @return string
*/
protected function filterNamespace()
{
return Str::studly($this->filterVendor()).'\\'.$this->filterClass();
}
/**
* Get the filter's escaped namespace.
*
* @return string
*/
protected function escapedFilterNamespace()
{
return str_replace('\\', '\\\\', $this->filterNamespace());
}
/**
* Get the filter's class name.
*
* @return string
*/
protected function filterClass()
{
return Str::studly($this->filterName());
}
/**
* Get the filter's vendor.
*
* @return string
*/
protected function filterVendor()
{
return explode('/', $this->argument('name'))[0];
}
/**
* Get the "title" name of the filter.
*
* @return string
*/
protected function filterTitle()
{
return Str::title(str_replace('-', ' ', $this->filterName()));
}
/**
* Get the filter's base name.
*
* @return string
*/
protected function filterName()
{
return explode('/', $this->argument('name'))[1];
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
class DashboardCommand extends GeneratorCommand
{
use ResolvesStubPath;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:dashboard {name}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new dashboard.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Dashboard';
/**
* Build the class with the given name.
*
* @param string $name
* @return string
*/
protected function buildClass($name)
{
$stub = parent::buildClass($name);
$stub = str_replace('uri-key', Str::snake($this->argument('name'), '-'), $stub);
return str_replace('dashboard-name', ucwords(Str::snake($this->argument('name'), ' ')), $stub);
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return $this->resolveStubPath('/stubs/nova/dashboard.stub');
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Nova\Dashboards';
}
}

View File

@@ -0,0 +1,280 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Laravel\Nova\Console\Concerns\AcceptsNameAndVendor;
use Symfony\Component\Process\Process;
class FieldCommand extends Command
{
use AcceptsNameAndVendor;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:field {name}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new field';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
if (! $this->hasValidNameArgument()) {
return;
}
(new Filesystem)->copyDirectory(
__DIR__.'/field-stubs',
$this->fieldPath()
);
// Field.js replacements...
$this->replace('{{ component }}', $this->fieldName(), $this->fieldPath().'/resources/js/field.js');
// Field.php replacements...
$this->replace('{{ namespace }}', $this->fieldNamespace(), $this->fieldPath().'/src/Field.stub');
$this->replace('{{ class }}', $this->fieldClass(), $this->fieldPath().'/src/Field.stub');
$this->replace('{{ component }}', $this->fieldName(), $this->fieldPath().'/src/Field.stub');
(new Filesystem)->move(
$this->fieldPath().'/src/Field.stub',
$this->fieldPath().'/src/'.$this->fieldClass().'.php'
);
// FieldServiceProvider.php replacements...
$this->replace('{{ namespace }}', $this->fieldNamespace(), $this->fieldPath().'/src/FieldServiceProvider.stub');
$this->replace('{{ component }}', $this->fieldName(), $this->fieldPath().'/src/FieldServiceProvider.stub');
(new Filesystem)->move(
$this->fieldPath().'/src/FieldServiceProvider.stub',
$this->fieldPath().'/src/FieldServiceProvider.php'
);
// Field composer.json replacements...
$this->replace('{{ name }}', $this->argument('name'), $this->fieldPath().'/composer.json');
$this->replace('{{ escapedNamespace }}', $this->escapedFieldNamespace(), $this->fieldPath().'/composer.json');
// Register the field...
$this->addFieldRepositoryToRootComposer();
$this->addFieldPackageToRootComposer();
$this->addScriptsToNpmPackage();
if ($this->confirm("Would you like to install the field's NPM dependencies?", true)) {
$this->installNpmDependencies();
$this->output->newLine();
}
if ($this->confirm("Would you like to compile the field's assets?", true)) {
$this->compile();
$this->output->newLine();
}
if ($this->confirm('Would you like to update your Composer packages?', true)) {
$this->composerUpdate();
}
}
/**
* Add a path repository for the field to the application's composer.json file.
*
* @return void
*/
protected function addFieldRepositoryToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['repositories'][] = [
'type' => 'path',
'url' => './'.$this->relativeFieldPath(),
];
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Add a package entry for the field to the application's composer.json file.
*
* @return void
*/
protected function addFieldPackageToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['require'][$this->argument('name')] = '*';
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Add a path repository for the field to the application's composer.json file.
*
* @return void
*/
protected function addScriptsToNpmPackage()
{
$package = json_decode(file_get_contents(base_path('package.json')), true);
$package['scripts']['build-'.$this->fieldName()] = 'cd '.$this->relativeFieldPath().' && npm run dev';
$package['scripts']['build-'.$this->fieldName().'-prod'] = 'cd '.$this->relativeFieldPath().' && npm run prod';
file_put_contents(
base_path('package.json'),
json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Install the field's NPM dependencies.
*
* @return void
*/
protected function installNpmDependencies()
{
$this->executeCommand('npm set progress=false && npm install', $this->fieldPath());
}
/**
* Compile the field's assets.
*
* @return void
*/
protected function compile()
{
$this->executeCommand('npm run dev', $this->fieldPath());
}
/**
* Update the project's composer dependencies.
*
* @return void
*/
protected function composerUpdate()
{
$this->executeCommand('composer update', getcwd());
}
/**
* Run the given command as a process.
*
* @param string $command
* @param string $path
* @return void
*/
protected function executeCommand($command, $path)
{
$process = (Process::fromShellCommandline($command, $path))->setTimeout(null);
if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
$process->setTty(true);
}
$process->run(function ($type, $line) {
$this->output->write($line);
});
}
/**
* Replace the given string in the given file.
*
* @param string $search
* @param string $replace
* @param string $path
* @return void
*/
protected function replace($search, $replace, $path)
{
file_put_contents($path, str_replace($search, $replace, file_get_contents($path)));
}
/**
* Get the path to the tool.
*
* @return string
*/
protected function fieldPath()
{
return base_path('nova-components/'.$this->fieldClass());
}
/**
* Get the relative path to the field.
*
* @return string
*/
protected function relativeFieldPath()
{
return 'nova-components/'.$this->fieldClass();
}
/**
* Get the field's namespace.
*
* @return string
*/
protected function fieldNamespace()
{
return Str::studly($this->fieldVendor()).'\\'.$this->fieldClass();
}
/**
* Get the field's escaped namespace.
*
* @return string
*/
protected function escapedFieldNamespace()
{
return str_replace('\\', '\\\\', $this->fieldNamespace());
}
/**
* Get the field's class name.
*
* @return string
*/
protected function fieldClass()
{
return Str::studly($this->fieldName());
}
/**
* Get the field's vendor.
*
* @return string
*/
protected function fieldVendor()
{
return explode('/', $this->argument('name'))[0];
}
/**
* Get the field's base name.
*
* @return string
*/
protected function fieldName()
{
return explode('/', $this->argument('name'))[1];
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\GeneratorCommand;
use Symfony\Component\Console\Input\InputOption;
class FilterCommand extends GeneratorCommand
{
use ResolvesStubPath;
/**
* The console command name.
*
* @var string
*/
protected $name = 'nova:filter';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new filter class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Filter';
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
if ($this->option('boolean')) {
return $this->resolveStubPath('/stubs/nova/boolean-filter.stub');
} elseif ($this->option('date')) {
return $this->resolveStubPath('/stubs/nova/date-filter.stub');
}
return $this->resolveStubPath('/stubs/nova/filter.stub');
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Nova\Filters';
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['boolean', null, InputOption::VALUE_NONE, 'Indicates if the generated filter should be a boolean filter'],
['date', null, InputOption::VALUE_NONE, 'Indicates if the generated filter should be a date filter'],
];
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
class InstallCommand extends Command
{
use ResolvesStubPath;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:install';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Install all of the Nova resources';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->comment('Publishing Nova Assets / Resources...');
$this->callSilent('nova:publish');
$this->comment('Publishing Nova Service Provider...');
$this->callSilent('vendor:publish', ['--tag' => 'nova-provider']);
$this->installNovaServiceProvider();
$this->comment('Generating User Resource...');
$this->callSilent('nova:resource', ['name' => 'User']);
copy($this->resolveStubPath('/stubs/nova/user-resource.stub'), app_path('Nova/User.php'));
if (file_exists(app_path('Models/User.php'))) {
file_put_contents(
app_path('Nova/User.php'),
str_replace('App\User::class', 'App\Models\User::class', file_get_contents(app_path('Nova/User.php')))
);
}
$this->setAppNamespace();
$this->info('Nova scaffolding installed successfully.');
}
/**
* Install the Nova service providers in the application configuration file.
*
* @return void
*/
protected function installNovaServiceProvider()
{
$namespace = Str::replaceLast('\\', '', $this->laravel->getNamespace());
if (! Str::contains($appConfig = file_get_contents(config_path('app.php')), "{$namespace}\\Providers\\NovaServiceProvider::class")) {
file_put_contents(config_path('app.php'), str_replace(
"{$namespace}\\Providers\EventServiceProvider::class,".PHP_EOL,
"{$namespace}\\Providers\EventServiceProvider::class,".PHP_EOL." {$namespace}\Providers\NovaServiceProvider::class,".PHP_EOL,
$appConfig
));
}
}
/**
* Set the proper application namespace on the installed files.
*
* @return void
*/
protected function setAppNamespace()
{
$namespace = $this->laravel->getNamespace();
$this->setAppNamespaceOn(app_path('Nova/User.php'), $namespace);
$this->setAppNamespaceOn(app_path('Providers/NovaServiceProvider.php'), $namespace);
}
/**
* Set the namespace on the given file.
*
* @param string $file
* @param string $namespace
* @return void
*/
protected function setAppNamespaceOn($file, $namespace)
{
file_put_contents($file, str_replace(
'App\\',
$namespace,
file_get_contents($file)
));
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
class LensCommand extends GeneratorCommand
{
use ResolvesStubPath;
/**
* The console command name.
*
* @var string
*/
protected $name = 'nova:lens';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new lens class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Lens';
/**
* Build the class with the given name.
*
* @param string $name
* @return string
*/
protected function buildClass($name)
{
$stub = parent::buildClass($name);
$key = preg_replace('/[^a-zA-Z0-9]+/', '', $this->argument('name'));
return str_replace('uri-key', Str::kebab($key), $stub);
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return $this->resolveStubPath('/stubs/nova/lens.stub');
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Nova\Lenses';
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
class PartitionCommand extends GeneratorCommand
{
use ResolvesStubPath;
/**
* The console command name.
*
* @var string
*/
protected $name = 'nova:partition';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new metric (partition) class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Metric';
/**
* Build the class with the given name.
*
* @param string $name
* @return string
*/
protected function buildClass($name)
{
$stub = parent::buildClass($name);
$key = preg_replace('/[^a-zA-Z0-9]+/', '', $this->argument('name'));
return str_replace('uri-key', Str::kebab($key), $stub);
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return $this->resolveStubPath('/stubs/nova/partition.stub');
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Nova\Metrics';
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\Command;
class PublishCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:publish {--force : Overwrite any existing files}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Publish all of the Nova resources';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->call('vendor:publish', [
'--tag' => 'nova-config',
'--force' => $this->option('force'),
]);
$this->call('vendor:publish', [
'--tag' => 'nova-assets',
'--force' => true,
]);
$this->call('vendor:publish', [
'--tag' => 'nova-lang',
'--force' => $this->option('force'),
]);
$this->call('vendor:publish', [
'--tag' => 'nova-views',
'--force' => $this->option('force'),
]);
$this->call('view:clear');
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Filesystem\Filesystem;
trait RenamesStubs
{
/**
* Rename the stubs with PHP file extensions.
*
* @return void
*/
protected function renameStubs()
{
foreach ($this->stubsToRename() as $stub) {
(new Filesystem)->move($stub, str_replace('.stub', '.php', $stub));
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Laravel\Nova\Console;
trait ResolvesStubPath
{
/**
* Resolve the fully-qualified path to the stub.
*
* @param string $stub
* @return string
*/
protected function resolveStubPath($stub)
{
return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
? $customPath
: __DIR__.str_replace('nova/', '', $stub);
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputOption;
class ResourceCommand extends GeneratorCommand
{
use ResolvesStubPath;
/**
* The console command name.
*
* @var string
*/
protected $name = 'nova:resource';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new resource class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Resource';
/**
* A list of resource names which are protected.
*
* @var array
*/
protected $protectedNames = [
'card',
'cards',
'dashboard',
'dashboards',
'metric',
'metrics',
'script',
'scripts',
'search',
'searches',
'style',
'styles',
];
/**
* Execute the console command.
*
* @return bool|null
*/
public function handle()
{
parent::handle();
$this->callSilent('nova:base-resource', [
'name' => 'Resource',
]);
}
/**
* Build the class with the given name.
*
* @param string $name
* @return string
*/
protected function buildClass($name)
{
$model = $this->option('model');
$modelNamespace = $this->getModelNamespace();
if (is_null($model)) {
$model = $modelNamespace.str_replace('/', '\\', $this->argument('name'));
} elseif (! Str::startsWith($model, [
$modelNamespace, '\\',
])) {
$model = $modelNamespace.$model;
}
$resourceName = $this->argument('name');
if (in_array(strtolower($resourceName), $this->protectedNames)) {
$this->warn("You *must* override the uriKey method for your {$resourceName} resource.");
}
$replace = [
'DummyFullModel' => $model,
'{{ namespacedModel }}' => $model,
'{{namespacedModel}}' => $model,
];
return str_replace(
array_keys($replace), array_values($replace), parent::buildClass($name)
);
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return $this->resolveStubPath('/stubs/nova/resource.stub');
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Nova';
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getModelNamespace()
{
$rootNamespace = $this->laravel->getNamespace();
return is_dir(app_path('Models')) ? $rootNamespace.'Models\\' : $rootNamespace;
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['model', 'm', InputOption::VALUE_REQUIRED, 'The model class being represented.'],
];
}
}

View File

@@ -0,0 +1,321 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Laravel\Nova\Console\Concerns\AcceptsNameAndVendor;
use Symfony\Component\Process\Process;
class ResourceToolCommand extends Command
{
use AcceptsNameAndVendor, RenamesStubs;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:resource-tool {name}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new resource tool';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
if (! $this->hasValidNameArgument()) {
return;
}
(new Filesystem)->copyDirectory(
__DIR__.'/resource-tool-stubs',
$this->toolPath()
);
// Tool.js replacements...
$this->replace('{{ component }}', $this->toolName(), $this->toolPath().'/resources/js/tool.js');
// Tool.vue replacements...
$this->replace('{{ title }}', $this->toolTitle(), $this->toolPath().'/resources/js/components/Tool.vue');
// Tool.php replacements...
$this->replace('{{ namespace }}', $this->toolNamespace(), $this->toolPath().'/src/Tool.stub');
$this->replace('{{ class }}', $this->toolClass(), $this->toolPath().'/src/Tool.stub');
$this->replace('{{ component }}', $this->toolName(), $this->toolPath().'/src/Tool.stub');
$this->replace('{{ title }}', $this->toolTitle(), $this->toolPath().'/src/Tool.stub');
(new Filesystem)->move(
$this->toolPath().'/src/Tool.stub',
$this->toolPath().'/src/'.$this->toolClass().'.php'
);
// ToolServiceProvider.php replacements...
$this->replace('{{ namespace }}', $this->toolNamespace(), $this->toolPath().'/src/ToolServiceProvider.stub');
$this->replace('{{ component }}', $this->toolName(), $this->toolPath().'/src/ToolServiceProvider.stub');
$this->replace('{{ name }}', $this->toolName(), $this->toolPath().'/src/ToolServiceProvider.stub');
// Tool composer.json replacements...
$this->replace('{{ name }}', $this->argument('name'), $this->toolPath().'/composer.json');
$this->replace('{{ escapedNamespace }}', $this->escapedToolNamespace(), $this->toolPath().'/composer.json');
// Rename the stubs with the proper file extensions...
$this->renameStubs();
// Register the tool...
$this->addToolRepositoryToRootComposer();
$this->addToolPackageToRootComposer();
if ($this->hasPackageFile()) {
$this->addScriptsToNpmPackage();
} else {
$this->warn('Please create a package.json to the root of your project.');
}
if ($this->confirm("Would you like to install the tool's NPM dependencies?", true)) {
$this->installNpmDependencies();
$this->output->newLine();
}
if ($this->confirm("Would you like to compile the tool's assets?", true)) {
$this->compile();
$this->output->newLine();
}
if ($this->confirm('Would you like to update your Composer packages?', true)) {
$this->composerUpdate();
}
}
/**
* Get the array of stubs that need PHP file extensions.
*
* @return array
*/
protected function stubsToRename()
{
return [
$this->toolPath().'/src/ToolServiceProvider.stub',
$this->toolPath().'/routes/api.stub',
];
}
/**
* Add a path repository for the tool to the application's composer.json file.
*
* @return void
*/
protected function addToolRepositoryToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['repositories'][] = [
'type' => 'path',
'url' => './'.$this->relativeToolPath(),
];
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Add a package entry for the tool to the application's composer.json file.
*
* @return void
*/
protected function addToolPackageToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['require'][$this->argument('name')] = '*';
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Add a path repository for the tool to the application's composer.json file.
*
* @return void
*/
protected function addScriptsToNpmPackage()
{
$package = json_decode(file_get_contents(base_path('package.json')), true);
$package['scripts']['build-'.$this->toolName()] = 'cd '.$this->relativeToolPath().' && npm run dev';
$package['scripts']['build-'.$this->toolName().'-prod'] = 'cd '.$this->relativeToolPath().' && npm run prod';
file_put_contents(
base_path('package.json'),
json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Install the tool's NPM dependencies.
*
* @return void
*/
protected function installNpmDependencies()
{
$this->executeCommand('npm set progress=false && npm install', $this->toolPath());
}
/**
* Compile the tool's assets.
*
* @return void
*/
protected function compile()
{
$this->executeCommand('npm run dev', $this->toolPath());
}
/**
* Update the project's composer dependencies.
*
* @return void
*/
protected function composerUpdate()
{
$this->executeCommand('composer update', getcwd());
}
/**
* Run the given command as a process.
*
* @param string $command
* @param string $path
* @return void
*/
protected function executeCommand($command, $path)
{
$process = (Process::fromShellCommandline($command, $path))->setTimeout(null);
if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
$process->setTty(true);
}
$process->run(function ($type, $line) {
$this->output->write($line);
});
}
/**
* Replace the given string in the given file.
*
* @param string $search
* @param string $replace
* @param string $path
* @return void
*/
protected function replace($search, $replace, $path)
{
file_put_contents($path, str_replace($search, $replace, file_get_contents($path)));
}
/**
* Get the path to the tool.
*
* @return string
*/
protected function toolPath()
{
return base_path('nova-components/'.$this->toolClass());
}
/**
* Get the relative path to the tool.
*
* @return string
*/
protected function relativeToolPath()
{
return 'nova-components/'.$this->toolClass();
}
/**
* Get the tool's namespace.
*
* @return string
*/
protected function toolNamespace()
{
return Str::studly($this->toolVendor()).'\\'.$this->toolClass();
}
/**
* Get the tool's escaped namespace.
*
* @return string
*/
protected function escapedToolNamespace()
{
return str_replace('\\', '\\\\', $this->toolNamespace());
}
/**
* Get the tool's class name.
*
* @return string
*/
protected function toolClass()
{
return Str::studly($this->toolName());
}
/**
* Get the tool's vendor.
*
* @return string
*/
protected function toolVendor()
{
return explode('/', $this->argument('name'))[0];
}
/**
* Get the "title" name of the tool.
*
* @return string
*/
protected function toolTitle()
{
return Str::title(str_replace('-', ' ', $this->toolName()));
}
/**
* Get the tool's base name.
*
* @return string
*/
protected function toolName()
{
return explode('/', $this->argument('name'))[1];
}
/**
* Determine whether we have a package file.
*
* @return bool
*/
protected function hasPackageFile()
{
return file_exists(base_path('package.json'));
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
class StubPublishCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:stubs {--force : Overwrite any existing files}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Publish all stubs that are available for customization';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
if (! is_dir($stubsPath = $this->laravel->basePath('stubs/nova'))) {
(new Filesystem)->makeDirectory($stubsPath, 0755, true);
}
$files = [
__DIR__.'/stubs/action.stub' => $stubsPath.'/action.stub',
__DIR__.'/stubs/base-resource.stub' => $stubsPath.'/base-resource.stub',
__DIR__.'/stubs/boolean-filter.stub' => $stubsPath.'/boolean-filter.stub',
__DIR__.'/stubs/dashboard.stub' => $stubsPath.'/dashboard.stub',
__DIR__.'/stubs/date-filter.stub' => $stubsPath.'/date-filter.stub',
__DIR__.'/stubs/destructive-action.stub' => $stubsPath.'/destructive-action.stub',
__DIR__.'/stubs/filter.stub' => $stubsPath.'/filter.stub',
__DIR__.'/stubs/lens.stub' => $stubsPath.'/lens.stub',
__DIR__.'/stubs/partition.stub' => $stubsPath.'/partition.stub',
__DIR__.'/stubs/resource.stub' => $stubsPath.'/resource.stub',
__DIR__.'/stubs/trend.stub' => $stubsPath.'/trend.stub',
__DIR__.'/stubs/user-resource.stub' => $stubsPath.'/user-resource.stub',
__DIR__.'/stubs/value.stub' => $stubsPath.'/value.stub',
];
foreach ($files as $from => $to) {
if (! file_exists($to) || $this->option('force')) {
file_put_contents($to, file_get_contents($from));
}
}
$this->info('Nova stubs published successfully.');
}
}

View File

@@ -0,0 +1,225 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Laravel\Nova\Console\Concerns\AcceptsNameAndVendor;
use Symfony\Component\Process\Process;
class ThemeCommand extends Command
{
use AcceptsNameAndVendor, RenamesStubs;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:theme {name}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new theme';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
if (! $this->hasValidNameArgument()) {
return;
}
(new Filesystem)->copyDirectory(__DIR__.'/theme-stubs', $this->themePath());
// ThemeServiceProvider.php replacements...
$this->replace('{{ namespace }}', $this->themeNamespace(), $this->themePath().'/src/ThemeServiceProvider.stub');
$this->replace('{{ component }}', $this->themeName(), $this->themePath().'/src/ThemeServiceProvider.stub');
$this->replace('{{ name }}', $this->themeName(), $this->themePath().'/src/ThemeServiceProvider.stub');
$this->replace('{{ vendor }}', $this->argument('name'), $this->themePath().'/src/ThemeServiceProvider.stub');
// Theme composer.json replacements...
$this->replace('{{ name }}', $this->argument('name'), $this->themePath().'/composer.json');
$this->replace('{{ escapedNamespace }}', $this->escapedThemeNamespace(), $this->themePath().'/composer.json');
// Rename the stubs with the proper file extensions...
$this->renameStubs();
// Register the theme...
$this->addThemeRepositoryToRootComposer();
$this->addThemePackageToRootComposer();
if ($this->confirm('Would you like to update your Composer packages?', true)) {
$this->composerUpdate();
}
}
/**
* Get the array of stubs that need PHP file extensions.
*
* @return array
*/
protected function stubsToRename()
{
return [
$this->themePath().'/src/ThemeServiceProvider.stub',
];
}
/**
* Add a path repository for the theme to the application's composer.json file.
*
* @return void
*/
protected function addThemeRepositoryToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['repositories'][] = [
'type' => 'path',
'url' => './'.$this->relativeThemePath(),
];
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Add a package entry for the theme to the application's composer.json file.
*
* @return void
*/
protected function addThemePackageToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['require'][$this->argument('name')] = '*';
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Update the project's composer dependencies.
*
* @return void
*/
protected function composerUpdate()
{
$this->executeCommand('composer update', getcwd());
}
/**
* Run the given command as a process.
*
* @param string $command
* @param string $path
* @return void
*/
protected function executeCommand($command, $path)
{
$process = (Process::fromShellCommandline($command, $path))->setTimeout(null);
if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
$process->setTty(true);
}
$process->run(function ($type, $line) {
$this->output->write($line);
});
}
/**
* Replace the given string in the given file.
*
* @param string $search
* @param string $replace
* @param string $path
* @return void
*/
protected function replace($search, $replace, $path)
{
file_put_contents($path, str_replace($search, $replace, file_get_contents($path)));
}
/**
* Get the path to the theme.
*
* @return string
*/
protected function themePath()
{
return base_path('nova-components/'.$this->themeClass());
}
/**
* Get the relative path to the theme.
*
* @return string
*/
protected function relativeThemePath()
{
return 'nova-components/'.$this->themeClass();
}
/**
* Get the theme's namespace.
*
* @return string
*/
protected function themeNamespace()
{
return Str::studly($this->themeVendor()).'\\'.$this->themeClass();
}
/**
* Get the theme's escaped namespace.
*
* @return string
*/
protected function escapedThemeNamespace()
{
return str_replace('\\', '\\\\', $this->themeNamespace());
}
/**
* Get the theme's class name.
*
* @return string
*/
protected function themeClass()
{
return Str::studly($this->themeName());
}
/**
* Get the theme's vendor.
*
* @return string
*/
protected function themeVendor()
{
return explode('/', $this->argument('name'))[0];
}
/**
* Get the theme's base name.
*
* @return string
*/
protected function themeName()
{
return explode('/', $this->argument('name'))[1];
}
}

View File

@@ -0,0 +1,317 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Laravel\Nova\Console\Concerns\AcceptsNameAndVendor;
use Symfony\Component\Process\Process;
class ToolCommand extends Command
{
use AcceptsNameAndVendor, RenamesStubs;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:tool {name}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new tool';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
if (! $this->hasValidNameArgument()) {
return;
}
$noInteraction = $this->option('no-interaction');
(new Filesystem)->copyDirectory(
__DIR__.'/tool-stubs',
$this->toolPath()
);
// Tool.js replacements...
$this->replace('{{ component }}', $this->toolName(), $this->toolPath().'/resources/js/tool.js');
// Tool.vue replacements...
$this->replace('{{ title }}', $this->toolTitle(), $this->toolPath().'/resources/js/components/Tool.vue');
$this->replace('{{ class }}', $this->toolClass(), $this->toolPath().'/resources/js/components/Tool.vue');
// Tool.php replacements...
$this->replace('{{ namespace }}', $this->toolNamespace(), $this->toolPath().'/src/Tool.stub');
$this->replace('{{ class }}', $this->toolClass(), $this->toolPath().'/src/Tool.stub');
$this->replace('{{ component }}', $this->toolName(), $this->toolPath().'/src/Tool.stub');
(new Filesystem)->move(
$this->toolPath().'/src/Tool.stub',
$this->toolPath().'/src/'.$this->toolClass().'.php'
);
// ToolServiceProvider.php replacements...
$this->replace('{{ namespace }}', $this->toolNamespace(), $this->toolPath().'/src/ToolServiceProvider.stub');
$this->replace('{{ component }}', $this->toolName(), $this->toolPath().'/src/ToolServiceProvider.stub');
$this->replace('{{ name }}', $this->toolName(), $this->toolPath().'/src/ToolServiceProvider.stub');
// Authorize.php replacements...
$this->replace('{{ namespace }}', $this->toolNamespace(), $this->toolPath().'/src/Http/Middleware/Authorize.stub');
$this->replace('{{ class }}', $this->toolClass(), $this->toolPath().'/src/Http/Middleware/Authorize.stub');
// Navigation replacements...
$this->replace('{{ title }}', $this->toolTitle(), $this->toolPath().'/resources/views/navigation.blade.php');
$this->replace('{{ component }}', $this->toolName(), $this->toolPath().'/resources/views/navigation.blade.php');
// Tool composer.json replacements...
$this->replace('{{ name }}', $this->argument('name'), $this->toolPath().'/composer.json');
$this->replace('{{ escapedNamespace }}', $this->escapedToolNamespace(), $this->toolPath().'/composer.json');
// Rename the stubs with the proper file extensions...
$this->renameStubs();
// Register the tool...
$this->addToolRepositoryToRootComposer();
$this->addToolPackageToRootComposer();
$this->addScriptsToNpmPackage();
if ($noInteraction || $this->confirm("Would you like to install the tool's NPM dependencies?", true)) {
$this->installNpmDependencies();
$this->output->newLine();
}
if ($noInteraction || $this->confirm("Would you like to compile the tool's assets?", true)) {
$this->compile();
$this->output->newLine();
}
if ($noInteraction || $this->confirm('Would you like to update your Composer packages?', true)) {
$this->composerUpdate();
}
}
/**
* Get the array of stubs that need PHP file extensions.
*
* @return array
*/
protected function stubsToRename()
{
return [
$this->toolPath().'/src/ToolServiceProvider.stub',
$this->toolPath().'/src/Http/Middleware/Authorize.stub',
$this->toolPath().'/routes/api.stub',
];
}
/**
* Add a path repository for the tool to the application's composer.json file.
*
* @return void
*/
protected function addToolRepositoryToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['repositories'][] = [
'type' => 'path',
'url' => './'.$this->relativeToolPath(),
];
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Add a package entry for the tool to the application's composer.json file.
*
* @return void
*/
protected function addToolPackageToRootComposer()
{
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
$composer['require'][$this->argument('name')] = '*';
file_put_contents(
base_path('composer.json'),
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Add a path repository for the tool to the application's composer.json file.
*
* @return void
*/
protected function addScriptsToNpmPackage()
{
$package = json_decode(file_get_contents(base_path('package.json')), true);
$package['scripts']['build-'.$this->toolName()] = 'cd '.$this->relativeToolPath().' && npm run dev';
$package['scripts']['build-'.$this->toolName().'-prod'] = 'cd '.$this->relativeToolPath().' && npm run prod';
file_put_contents(
base_path('package.json'),
json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Install the tool's NPM dependencies.
*
* @return void
*/
protected function installNpmDependencies()
{
$this->executeCommand('npm set progress=false && npm install', $this->toolPath());
}
/**
* Compile the tool's assets.
*
* @return void
*/
protected function compile()
{
$this->executeCommand('npm run dev', $this->toolPath());
}
/**
* Update the project's composer dependencies.
*
* @return void
*/
protected function composerUpdate()
{
$this->executeCommand('composer update', getcwd());
}
/**
* Run the given command as a process.
*
* @param string $command
* @param string $path
* @return void
*/
protected function executeCommand($command, $path)
{
$process = (Process::fromShellCommandline($command, $path))->setTimeout(null);
if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
$process->setTty(true);
}
$process->run(function ($type, $line) {
$this->output->write($line);
});
}
/**
* Replace the given string in the given file.
*
* @param string $search
* @param string $replace
* @param string $path
* @return void
*/
protected function replace($search, $replace, $path)
{
file_put_contents($path, str_replace($search, $replace, file_get_contents($path)));
}
/**
* Get the path to the tool.
*
* @return string
*/
protected function toolPath()
{
return base_path('nova-components/'.$this->toolClass());
}
/**
* Get the relative path to the tool.
*
* @return string
*/
protected function relativeToolPath()
{
return 'nova-components/'.$this->toolClass();
}
/**
* Get the tool's namespace.
*
* @return string
*/
protected function toolNamespace()
{
return Str::studly($this->toolVendor()).'\\'.$this->toolClass();
}
/**
* Get the tool's escaped namespace.
*
* @return string
*/
protected function escapedToolNamespace()
{
return str_replace('\\', '\\\\', $this->toolNamespace());
}
/**
* Get the tool's class name.
*
* @return string
*/
protected function toolClass()
{
return Str::studly($this->toolName());
}
/**
* Get the tool's vendor.
*
* @return string
*/
protected function toolVendor()
{
return explode('/', $this->argument('name'))[0];
}
/**
* Get the "title" name of the tool.
*
* @return string
*/
protected function toolTitle()
{
return Str::title(str_replace('-', ' ', $this->toolName()));
}
/**
* Get the tool's base name.
*
* @return string
*/
protected function toolName()
{
return explode('/', $this->argument('name'))[1];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
class TranslateCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:translate
{language}
{--force : Overwrite any existing files}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create translation files for Nova';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$language = $this->argument('language');
$jsonLanguageFile = resource_path("lang/vendor/nova/{$language}.json");
if (! File::exists($jsonLanguageFile) || $this->option('force')) {
File::copy(__DIR__.'/../../resources/lang/en.json', $jsonLanguageFile);
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
class TrendCommand extends GeneratorCommand
{
use ResolvesStubPath;
/**
* The console command name.
*
* @var string
*/
protected $name = 'nova:trend';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new metric (trend) class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Metric';
/**
* Build the class with the given name.
*
* @param string $name
* @return string
*/
protected function buildClass($name)
{
$stub = parent::buildClass($name);
$key = preg_replace('/[^a-zA-Z0-9]+/', '', $this->argument('name'));
return str_replace('uri-key', Str::kebab($key), $stub);
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return $this->resolveStubPath('/stubs/nova/trend.stub');
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Nova\Metrics';
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\Command;
use Laravel\Nova\Nova;
class UserCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nova:user';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new user';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
Nova::createUser($this);
$this->info('User created successfully.');
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Laravel\Nova\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
class ValueCommand extends GeneratorCommand
{
use ResolvesStubPath;
/**
* The console command name.
*
* @var string
*/
protected $name = 'nova:value';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new metric (single value) class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Metric';
/**
* Build the class with the given name.
*
* @param string $name
* @return string
*/
protected function buildClass($name)
{
$stub = parent::buildClass($name);
$key = preg_replace('/[^a-zA-Z0-9]+/', '', $this->argument('name'));
return str_replace('uri-key', Str::kebab($key), $stub);
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return $this->resolveStubPath('/stubs/nova/value.stub');
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Nova\Metrics';
}
}

View File

@@ -0,0 +1,29 @@
{
"name": "{{ name }}",
"description": "A Laravel Nova asset.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": ">=7.1.0"
},
"autoload": {
"psr-4": {
"{{ escapedNamespace }}\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"{{ escapedNamespace }}\\AssetServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,19 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"cross-env": "^5.0.0",
"laravel-mix": "^1.0"
},
"dependencies": {
"vue": "^2.5.0"
}
}

View File

@@ -0,0 +1 @@
// Nova Asset JS

View File

@@ -0,0 +1 @@
// Nova Asset CSS

View File

@@ -0,0 +1,33 @@
<?php
namespace {{ namespace }};
use Illuminate\Support\ServiceProvider;
use Laravel\Nova\Events\ServingNova;
use Laravel\Nova\Nova;
class AssetServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Nova::serving(function (ServingNova $event) {
Nova::script('{{ component }}', __DIR__.'/../dist/js/asset.js');
Nova::style('{{ component }}', __DIR__.'/../dist/css/asset.css');
});
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View File

@@ -0,0 +1,6 @@
let mix = require('laravel-mix')
mix
.setPublicPath('dist')
.js('resources/js/asset.js', 'js')
.sass('resources/sass/asset.scss', 'css')

View File

@@ -0,0 +1,29 @@
{
"name": "{{ name }}",
"description": "A Laravel Nova card.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": ">=7.1.0"
},
"autoload": {
"psr-4": {
"{{ escapedNamespace }}\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"{{ escapedNamespace }}\\CardServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,19 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"cross-env": "^5.0.0",
"laravel-mix": "^1.0"
},
"dependencies": {
"vue": "^2.5.0"
}
}

View File

@@ -0,0 +1,3 @@
Nova.booting((Vue, router, store) => {
Vue.component('{{ component }}', require('./components/Card'))
})

View File

@@ -0,0 +1,24 @@
<template>
<card class="flex flex-col items-center justify-center">
<div class="px-3 py-3">
<h1 class="text-center text-3xl text-80 font-light">{{ title }}</h1>
</div>
</card>
</template>
<script>
export default {
props: [
'card',
// The following props are only available on resource detail cards...
// 'resource',
// 'resourceId',
// 'resourceName',
],
mounted() {
//
},
}
</script>

View File

@@ -0,0 +1 @@
// Nova Card CSS

View File

@@ -0,0 +1,19 @@
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Card API Routes
|--------------------------------------------------------------------------
|
| Here is where you may register API routes for your card. These routes
| are loaded by the ServiceProvider of your card. You're free to add
| as many additional routes to this file as your card may require.
|
*/
// Route::get('/endpoint', function (Request $request) {
// //
// });

View File

@@ -0,0 +1,25 @@
<?php
namespace {{ namespace }};
use Laravel\Nova\Card;
class {{ class }} extends Card
{
/**
* The width of the card (1/3, 1/2, or full).
*
* @var string
*/
public $width = '1/3';
/**
* Get the component name for the element.
*
* @return string
*/
public function component()
{
return '{{ component }}';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace {{ namespace }};
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Laravel\Nova\Events\ServingNova;
use Laravel\Nova\Nova;
class CardServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
$this->app->booted(function () {
$this->routes();
});
Nova::serving(function (ServingNova $event) {
Nova::script('{{ component }}', __DIR__.'/../dist/js/card.js');
Nova::style('{{ component }}', __DIR__.'/../dist/css/card.css');
});
}
/**
* Register the card's routes.
*
* @return void
*/
protected function routes()
{
if ($this->app->routesAreCached()) {
return;
}
Route::middleware(['nova'])
->prefix('nova-vendor/{{ name }}')
->group(__DIR__.'/../routes/api.php');
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View File

@@ -0,0 +1,6 @@
let mix = require('laravel-mix')
mix
.setPublicPath('dist')
.js('resources/js/card.js', 'js')
.sass('resources/sass/card.scss', 'css')

View File

@@ -0,0 +1,29 @@
{
"name": "{{ name }}",
"description": "A Laravel Nova field.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": ">=7.1.0"
},
"autoload": {
"psr-4": {
"{{ escapedNamespace }}\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"{{ escapedNamespace }}\\FieldServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,20 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"cross-env": "^5.0.0",
"laravel-mix": "^1.0",
"laravel-nova": "^1.0"
},
"dependencies": {
"vue": "^2.5.0"
}
}

View File

@@ -0,0 +1,9 @@
<template>
<panel-item :field="field" />
</template>
<script>
export default {
props: ['resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,40 @@
<template>
<default-field :field="field" :errors="errors" :show-help-text="showHelpText">
<template slot="field">
<input
:id="field.name"
type="text"
class="w-full form-control form-input form-input-bordered"
:class="errorClasses"
:placeholder="field.name"
v-model="value"
/>
</template>
</default-field>
</template>
<script>
import { FormField, HandlesValidationErrors } from 'laravel-nova'
export default {
mixins: [FormField, HandlesValidationErrors],
props: ['resourceName', 'resourceId', 'field'],
methods: {
/*
* Set the initial, internal value for the field.
*/
setInitialValue() {
this.value = this.field.value || ''
},
/**
* Fill the given FormData object with the field's internal value.
*/
fill(formData) {
formData.append(this.field.attribute, this.value || '')
},
},
}
</script>

View File

@@ -0,0 +1,9 @@
<template>
<span>{{ field.value }}</span>
</template>
<script>
export default {
props: ['resourceName', 'field'],
}
</script>

View File

@@ -0,0 +1,5 @@
Nova.booting((Vue, router, store) => {
Vue.component('index-{{ component }}', require('./components/IndexField'))
Vue.component('detail-{{ component }}', require('./components/DetailField'))
Vue.component('form-{{ component }}', require('./components/FormField'))
})

View File

@@ -0,0 +1 @@
// Nova Tool CSS

View File

@@ -0,0 +1,15 @@
<?php
namespace {{ namespace }};
use Laravel\Nova\Fields\Field;
class {{ class }} extends Field
{
/**
* The field's component.
*
* @var string
*/
public $component = '{{ component }}';
}

View File

@@ -0,0 +1,33 @@
<?php
namespace {{ namespace }};
use Illuminate\Support\ServiceProvider;
use Laravel\Nova\Events\ServingNova;
use Laravel\Nova\Nova;
class FieldServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Nova::serving(function (ServingNova $event) {
Nova::script('{{ component }}', __DIR__.'/../dist/js/field.js');
Nova::style('{{ component }}', __DIR__.'/../dist/css/field.css');
});
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View File

@@ -0,0 +1,6 @@
let mix = require('laravel-mix')
mix
.setPublicPath('dist')
.js('resources/js/field.js', 'js')
.sass('resources/sass/field.scss', 'css')

View File

@@ -0,0 +1,29 @@
{
"name": "{{ name }}",
"description": "A Laravel Nova filter.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": ">=7.1.0"
},
"autoload": {
"psr-4": {
"{{ escapedNamespace }}\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"{{ escapedNamespace }}\\FilterServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,19 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"cross-env": "^5.0.0",
"laravel-mix": "^1.0"
},
"dependencies": {
"vue": "^2.5.0"
}
}

View File

@@ -0,0 +1,60 @@
<template>
<div>
<h3 class="text-sm uppercase tracking-wide text-80 bg-30 p-3">
{{ filter.name }}
</h3>
<div class="p-2">
<select
:dusk="filter.name + '-filter-select'"
class="block w-full form-control-sm form-select"
:value="value"
@change="handleChange"
>
<option value="" selected>&mdash;</option>
<option v-for="option in filter.options" :value="option.value">
{{ option.name }}
</option>
</select>
</div>
</div>
</template>
<script>
export default {
props: {
resourceName: {
type: String,
required: true,
},
filterKey: {
type: String,
required: true,
},
},
methods: {
handleChange(event) {
this.$store.commit(`${this.resourceName}/updateFilterState`, {
filterClass: this.filterKey,
value: event.target.value,
})
this.$emit('change')
},
},
computed: {
filter() {
return this.$store.getters[`${this.resourceName}/getFilter`](
this.filterKey
)
},
value() {
return this.filter.currentValue
},
},
}
</script>

View File

@@ -0,0 +1,3 @@
Nova.booting((Vue, router, store) => {
Vue.component('{{ component }}', require('./components/Filter'))
})

View File

@@ -0,0 +1 @@
// Nova Filter CSS

View File

@@ -0,0 +1,40 @@
<?php
namespace {{ namespace }};
use Illuminate\Http\Request;
use Laravel\Nova\Filters\Filter;
class {{ class }} extends Filter
{
/**
* The filter's component.
*
* @var string
*/
public $component = '{{ component }}';
/**
* Apply the filter to the given query.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Builder
*/
public function apply(Request $request, $query, $value)
{
return $query;
}
/**
* Get the filter's available options.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function options(Request $request)
{
return [];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace {{ namespace }};
use Illuminate\Support\ServiceProvider;
use Laravel\Nova\Events\ServingNova;
use Laravel\Nova\Nova;
class FilterServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Nova::serving(function (ServingNova $event) {
Nova::script('{{ component }}', __DIR__.'/../dist/js/filter.js');
Nova::style('{{ component }}', __DIR__.'/../dist/css/filter.css');
});
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View File

@@ -0,0 +1,6 @@
let mix = require('laravel-mix')
mix
.setPublicPath('dist')
.js('resources/js/filter.js', 'js')
.sass('resources/sass/filter.scss', 'css')

View File

@@ -0,0 +1,29 @@
{
"name": "{{ name }}",
"description": "A Laravel Nova resource tool.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": ">=7.1.0"
},
"autoload": {
"psr-4": {
"{{ escapedNamespace }}\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"{{ escapedNamespace }}\\ToolServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,19 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"cross-env": "^5.0.0",
"laravel-mix": "^1.0"
},
"dependencies": {
"vue": "^2.5.0"
}
}

View File

@@ -0,0 +1,13 @@
<template>
<div>{{ title }}</div>
</template>
<script>
export default {
props: ['resourceName', 'resourceId', 'panel'],
mounted() {
//
},
}
</script>

View File

@@ -0,0 +1,3 @@
Nova.booting((Vue, router, store) => {
Vue.component('{{ component }}', require('./components/Tool'))
})

View File

@@ -0,0 +1 @@
// Nova Tool CSS

View File

@@ -0,0 +1,19 @@
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Tool API Routes
|--------------------------------------------------------------------------
|
| Here is where you may register API routes for your tool. These routes
| are loaded by the ServiceProvider of your tool. You're free to add
| as many additional routes to this file as your tool may require.
|
*/
// Route::get('/endpoint', function (Request $request) {
// //
// });

View File

@@ -0,0 +1,28 @@
<?php
namespace {{ namespace }};
use Laravel\Nova\ResourceTool;
class {{ class }} extends ResourceTool
{
/**
* Get the displayable name of the resource tool.
*
* @return string
*/
public function name()
{
return '{{ title }}';
}
/**
* Get the component name for the resource tool.
*
* @return string
*/
public function component()
{
return '{{ component }}';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace {{ namespace }};
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Laravel\Nova\Events\ServingNova;
use Laravel\Nova\Nova;
class ToolServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
$this->app->booted(function () {
$this->routes();
});
Nova::serving(function (ServingNova $event) {
Nova::script('{{ component }}', __DIR__.'/../dist/js/tool.js');
Nova::style('{{ component }}', __DIR__.'/../dist/css/tool.css');
});
}
/**
* Register the tool's routes.
*
* @return void
*/
protected function routes()
{
if ($this->app->routesAreCached()) {
return;
}
Route::middleware(['nova'])
->prefix('nova-vendor/{{ name }}')
->group(__DIR__.'/../routes/api.php');
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View File

@@ -0,0 +1,6 @@
let mix = require('laravel-mix')
mix
.setPublicPath('dist')
.js('resources/js/tool.js', 'js')
.sass('resources/sass/tool.scss', 'css')

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Laravel\Nova\Cards\Help;
use Laravel\Nova\Nova;
use Laravel\Nova\NovaApplicationServiceProvider;
class NovaServiceProvider extends NovaApplicationServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
parent::boot();
}
/**
* Register the Nova routes.
*
* @return void
*/
protected function routes()
{
Nova::routes()
->withAuthenticationRoutes()
->withPasswordResetRoutes()
->register();
}
/**
* Register the Nova gate.
*
* This gate determines who can access Nova in non-local environments.
*
* @return void
*/
protected function gate()
{
Gate::define('viewNova', function ($user) {
return in_array($user->email, [
//
]);
});
}
/**
* Get the cards that should be displayed on the default Nova dashboard.
*
* @return array
*/
protected function cards()
{
return [
new Help,
];
}
/**
* Get the extra dashboards that should be displayed on the Nova dashboard.
*
* @return array
*/
protected function dashboards()
{
return [];
}
/**
* Get the tools that should be listed in the Nova sidebar.
*
* @return array
*/
public function tools()
{
return [];
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace {{ namespace }};
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Collection;
use Laravel\Nova\Actions\Action;
use Laravel\Nova\Fields\ActionFields;
class {{ class }} extends Action
{
use InteractsWithQueue, Queueable;
/**
* Perform the action on the given models.
*
* @param \Laravel\Nova\Fields\ActionFields $fields
* @param \Illuminate\Support\Collection $models
* @return mixed
*/
public function handle(ActionFields $fields, Collection $models)
{
//
}
/**
* Get the fields available on the action.
*
* @return array
*/
public function fields()
{
return [];
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace {{ namespace }};
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Resource as NovaResource;
abstract class Resource extends NovaResource
{
/**
* 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;
}
/**
* Build a Scout search query for the given resource.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param \Laravel\Scout\Builder $query
* @return \Laravel\Scout\Builder
*/
public static function scoutQuery(NovaRequest $request, $query)
{
return $query;
}
/**
* Build a "detail" 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 detailQuery(NovaRequest $request, $query)
{
return parent::detailQuery($request, $query);
}
/**
* Build a "relatable" query for the given resource.
*
* This query determines which instances of the model may be attached to other resources.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public static function relatableQuery(NovaRequest $request, $query)
{
return parent::relatableQuery($request, $query);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace {{ namespace }};
use Illuminate\Http\Request;
use Laravel\Nova\Filters\BooleanFilter;
class {{ class }} extends BooleanFilter
{
/**
* Apply the filter to the given query.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Builder
*/
public function apply(Request $request, $query, $value)
{
return $query;
}
/**
* Get the filter's available options.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function options(Request $request)
{
return [];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace {{ namespace }};
use Laravel\Nova\Dashboard;
class {{ class }} extends Dashboard
{
/**
* Get the cards for the dashboard.
*
* @return array
*/
public function cards()
{
return [
//
];
}
/**
* Get the URI key for the dashboard.
*
* @return string
*/
public static function uriKey()
{
return 'uri-key';
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace {{ namespace }};
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Laravel\Nova\Filters\DateFilter;
class {{ class }} extends DateFilter
{
/**
* Apply the filter to the given query.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Builder
*/
public function apply(Request $request, $query, $value)
{
$value = Carbon::parse($value);
return $query;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace {{ namespace }};
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Laravel\Nova\Actions\DestructiveAction;
use Laravel\Nova\Fields\ActionFields;
class {{ class }} extends DestructiveAction
{
use InteractsWithQueue, Queueable, SerializesModels;
/**
* Perform the action on the given models.
*
* @param \Laravel\Nova\Fields\ActionFields $fields
* @param \Illuminate\Support\Collection $models
* @return mixed
*/
public function handle(ActionFields $fields, Collection $models)
{
//
}
/**
* Get the fields available on the action.
*
* @return array
*/
public function fields()
{
return [];
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace {{ namespace }};
use Illuminate\Http\Request;
use Laravel\Nova\Filters\Filter;
class {{ class }} extends Filter
{
/**
* The filter's component.
*
* @var string
*/
public $component = 'select-filter';
/**
* Apply the filter to the given query.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param mixed $value
* @return \Illuminate\Database\Eloquent\Builder
*/
public function apply(Request $request, $query, $value)
{
return $query;
}
/**
* Get the filter's available options.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function options(Request $request)
{
return [];
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace {{ namespace }};
use Illuminate\Http\Request;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\LensRequest;
use Laravel\Nova\Lenses\Lens;
class {{ class }} extends Lens
{
/**
* Get the query builder / paginator for the lens.
*
* @param \Laravel\Nova\Http\Requests\LensRequest $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @return mixed
*/
public static function query(LensRequest $request, $query)
{
return $request->withOrdering($request->withFilters(
$query
));
}
/**
* Get the fields available to the lens.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function fields(Request $request)
{
return [
ID::make(__('ID'), 'id')->sortable(),
];
}
/**
* Get the cards available on the lens.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function cards(Request $request)
{
return [];
}
/**
* Get the filters available for the lens.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function filters(Request $request)
{
return [];
}
/**
* Get the actions available on the lens.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function actions(Request $request)
{
return parent::actions($request);
}
/**
* Get the URI key for the lens.
*
* @return string
*/
public function uriKey()
{
return 'uri-key';
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace {{ namespace }};
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Metrics\Partition;
class {{ class }} extends Partition
{
/**
* Calculate the value of the metric.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @return mixed
*/
public function calculate(NovaRequest $request)
{
return $this->count($request, Model::class, 'groupByColumn');
}
/**
* Determine for how many minutes the metric should be cached.
*
* @return \DateTimeInterface|\DateInterval|float|int
*/
public function cacheFor()
{
// return now()->addMinutes(5);
}
/**
* Get the URI key for the metric.
*
* @return string
*/
public function uriKey()
{
return 'uri-key';
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace {{ namespace }};
use Illuminate\Http\Request;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Http\Requests\NovaRequest;
class {{ class }} extends Resource
{
/**
* The model the resource corresponds to.
*
* @var string
*/
public static $model = \{{ namespacedModel }}::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'id';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'id',
];
/**
* Get the fields displayed by the resource.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function fields(Request $request)
{
return [
ID::make(__('ID'), 'id')->sortable(),
];
}
/**
* Get the cards available for the request.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function cards(Request $request)
{
return [];
}
/**
* Get the filters available for the resource.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function filters(Request $request)
{
return [];
}
/**
* Get the lenses available for the resource.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function lenses(Request $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function actions(Request $request)
{
return [];
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace {{ namespace }};
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Metrics\Trend;
class {{ class }} extends Trend
{
/**
* Calculate the value of the metric.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @return mixed
*/
public function calculate(NovaRequest $request)
{
return $this->countByDays($request, Model::class);
}
/**
* Get the ranges available for the metric.
*
* @return array
*/
public function ranges()
{
return [
30 => __('30 Days'),
60 => __('60 Days'),
90 => __('90 Days'),
];
}
/**
* Determine for how many minutes the metric should be cached.
*
* @return \DateTimeInterface|\DateInterval|float|int
*/
public function cacheFor()
{
// return now()->addMinutes(5);
}
/**
* Get the URI key for the metric.
*
* @return string
*/
public function uriKey()
{
return 'uri-key';
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace App\Nova;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\Gravatar;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Password;
use Laravel\Nova\Fields\Text;
class User extends Resource
{
/**
* The model the resource corresponds to.
*
* @var string
*/
public static $model = \App\User::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = 'name';
/**
* The columns that should be searched.
*
* @var array
*/
public static $search = [
'id', 'name', 'email',
];
/**
* Get the fields displayed by the resource.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Gravatar::make()->maxWidth(50),
Text::make('Name')
->sortable()
->rules('required', 'max:255'),
Text::make('Email')
->sortable()
->rules('required', 'email', 'max:254')
->creationRules('unique:users,email')
->updateRules('unique:users,email,{{resourceId}}'),
Password::make('Password')
->onlyOnForms()
->creationRules('required', 'string', 'min:8')
->updateRules('nullable', 'string', 'min:8'),
];
}
/**
* Get the cards available for the request.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function cards(Request $request)
{
return [];
}
/**
* Get the filters available for the resource.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function filters(Request $request)
{
return [];
}
/**
* Get the lenses available for the resource.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function lenses(Request $request)
{
return [];
}
/**
* Get the actions available for the resource.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function actions(Request $request)
{
return [];
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace {{ namespace }};
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Metrics\Value;
class {{ class }} extends Value
{
/**
* Calculate the value of the metric.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @return mixed
*/
public function calculate(NovaRequest $request)
{
return $this->count($request, Model::class);
}
/**
* Get the ranges available for the metric.
*
* @return array
*/
public function ranges()
{
return [
30 => __('30 Days'),
60 => __('60 Days'),
365 => __('365 Days'),
'TODAY' => __('Today'),
'MTD' => __('Month To Date'),
'QTD' => __('Quarter To Date'),
'YTD' => __('Year To Date'),
];
}
/**
* Determine for how many minutes the metric should be cached.
*
* @return \DateTimeInterface|\DateInterval|float|int
*/
public function cacheFor()
{
// return now()->addMinutes(5);
}
/**
* Get the URI key for the metric.
*
* @return string
*/
public function uriKey()
{
return 'uri-key';
}
}

View File

@@ -0,0 +1,29 @@
{
"name": "{{ name }}",
"description": "A Laravel Nova theme.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": ">=7.1.0"
},
"autoload": {
"psr-4": {
"{{ escapedNamespace }}\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"{{ escapedNamespace }}\\ThemeServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,26 @@
:root {
--primary: rgb(254, 1, 129, 1);
--primary-dark: rgb(209, 0, 105, 1);
--primary-70: rgba(254, 1, 129, 0.7);
--primary-50: rgba(254, 1, 129, 0.5);
--primary-30: rgba(254, 1, 129, 0.3);
--primary-10: rgba(254, 1, 129, 0.1);
--logo: #430051;
--sidebar-icon: #db34de;
}
.bg-grad-sidebar {
background-image: -webkit-gradient(
linear,
left bottom,
left top,
from(rgb(254, 1, 129, 1)),
to(#3c4655)
);
background-image: linear-gradient(
0deg,
rgb(254, 1, 129, 1),
rgb(100, 5, 113)
);
}

View File

@@ -0,0 +1,35 @@
<?php
namespace {{ namespace }};
use Illuminate\Support\ServiceProvider;
use Laravel\Nova\Nova;
class ThemeServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Nova::booted(function () {
Nova::theme(asset('/{{ vendor }}/theme.css'));
});
$this->publishes([
__DIR__.'/../resources/css' => public_path('{{ vendor }}'),
], 'public');
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

Some files were not shown because too many files have changed in this diff Show More