diff --git a/database/migrations/2024_03_21_175513_create_root_events_table.php b/database/migrations/2024_03_21_175513_create_root_events_table.php index 3b71b871..c809bf44 100644 --- a/database/migrations/2024_03_21_175513_create_root_events_table.php +++ b/database/migrations/2024_03_21_175513_create_root_events_table.php @@ -15,11 +15,10 @@ public function up(): void Schema::create('root_events', static function (Blueprint $table): void { $table->id(); $table->foreignIdFor(User::getProxiedClass())->nullable()->constrained()->nullOnDelete(); - $table->morphs('target'); + $table->uuidMorphs('target'); $table->string('action'); - $table->string('label'); $table->json('payload')->nullable(); - $table->timestamp('created_at'); + $table->timestamps(); }); } diff --git a/src/Actions/Action.php b/src/Actions/Action.php index d0a6f3e4..2ec3e3af 100644 --- a/src/Actions/Action.php +++ b/src/Actions/Action.php @@ -12,6 +12,7 @@ use Cone\Root\Support\Alert; use Cone\Root\Traits\AsForm; use Cone\Root\Traits\Authorizable; +use Cone\Root\Traits\HasRootEvents; use Cone\Root\Traits\Makeable; use Cone\Root\Traits\RegistersRoutes; use Cone\Root\Traits\ResolvesVisibility; @@ -209,6 +210,15 @@ public function handleFormRequest(Request $request, Model $model): void }; $this->handle($request, $models); + + if (in_array(HasRootEvents::class, class_uses_recursive($model))) { + $models->each(static function (Model $model) use ($request): void { + $model->recordRootEvent( + Str::of(static::class)->classBasename()->headline()->value(), + $request->user() + ); + }); + } } /** diff --git a/src/Fields/Events.php b/src/Fields/Events.php new file mode 100644 index 00000000..98f0b815 --- /dev/null +++ b/src/Fields/Events.php @@ -0,0 +1,101 @@ +get('/', [RelationController::class, 'index']); + $router->get("/{{$this->getRouteKeyName()}}", [RelationController::class, 'show']); + } + + /** + * {@inheritdoc} + */ + public function mapRelationAbilities(Request $request, Model $model): array + { + return [ + 'viewAny' => $this->resolveAbility('viewAny', $request, $model), + 'create' => false, + ]; + } + + /** + * {@inheritdoc} + */ + public function mapRelatedAbilities(Request $request, Model $model, Model $related): array + { + return [ + 'view' => $this->resolveAbility('view', $request, $model, $related), + 'update' => false, + 'restore' => false, + 'delete' => false, + 'forceDelete' => false, + ]; + } + + /** + * {@inheritdoc} + */ + public function fields(Request $request): array + { + return [ + ID::make()->sortable(), + + Text::make(__('Action'), 'action')->sortable(), + + BelongsTo::make(__('User'), 'user') + ->display('name'), + + Date::make(__('Date'), 'created_at') + ->withTime(), + ]; + } +} diff --git a/src/Fields/Relation.php b/src/Fields/Relation.php index 749d595d..88da6971 100644 --- a/src/Fields/Relation.php +++ b/src/Fields/Relation.php @@ -14,6 +14,7 @@ use Cone\Root\Interfaces\Form; use Cone\Root\Root; use Cone\Root\Traits\AsForm; +use Cone\Root\Traits\HasRootEvents; use Cone\Root\Traits\RegistersRoutes; use Cone\Root\Traits\ResolvesActions; use Cone\Root\Traits\ResolvesFields; @@ -616,6 +617,13 @@ public function handleFormRequest(Request $request, Model $model): void $this->saved($request, $model); + if (in_array(HasRootEvents::class, class_uses_recursive($model))) { + $model->recordRootEvent( + $model->wasRecentlyCreated ? 'Created' : 'Updated', + $request->user() + ); + } + DB::commit(); } catch (Throwable $exception) { report($exception); diff --git a/src/Models/Event.php b/src/Models/Event.php index f802dad6..464d8e2e 100644 --- a/src/Models/Event.php +++ b/src/Models/Event.php @@ -6,6 +6,8 @@ use Cone\Root\Traits\InteractsWithProxy; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphTo; class Event extends Model implements Contract { @@ -21,6 +23,23 @@ class Event extends Model implements Contract 'payload' => 'json', ]; + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'action', + 'payload', + ]; + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'root_events'; + /** * Get the proxied interface. */ @@ -30,10 +49,18 @@ public static function getProxiedInterface(): string } /** - * Create a new method. + * Get the event target. + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::getProxiedClass()); + } + + /** + * Get the event target. */ - public function target(): mixed + public function target(): MorphTo { - // + return $this->morphTo(); } } diff --git a/src/Models/Meta.php b/src/Models/Meta.php index 3b59da0a..ba5bb8fe 100644 --- a/src/Models/Meta.php +++ b/src/Models/Meta.php @@ -17,22 +17,22 @@ class Meta extends Model implements Contract use InteractsWithProxy; /** - * The attributes that are mass assignable. + * The attributes that should be cast to native types. * - * @var array + * @var array */ - protected $fillable = [ - 'key', - 'value', + protected $casts = [ + 'value' => MetaValue::class, ]; /** - * The attributes that should be cast to native types. + * The attributes that are mass assignable. * - * @var array + * @var array */ - protected $casts = [ - 'value' => MetaValue::class, + protected $fillable = [ + 'key', + 'value', ]; /** diff --git a/src/Resources/Resource.php b/src/Resources/Resource.php index 4054fbb5..95dda295 100644 --- a/src/Resources/Resource.php +++ b/src/Resources/Resource.php @@ -4,7 +4,9 @@ use Cone\Root\Actions\Action; use Cone\Root\Exceptions\SaveFormDataException; +use Cone\Root\Fields\Events; use Cone\Root\Fields\Field; +use Cone\Root\Fields\Fields; use Cone\Root\Fields\Relation; use Cone\Root\Filters\Filter; use Cone\Root\Filters\RenderableFilter; @@ -16,6 +18,7 @@ use Cone\Root\Root; use Cone\Root\Traits\AsForm; use Cone\Root\Traits\Authorizable; +use Cone\Root\Traits\HasRootEvents; use Cone\Root\Traits\RegistersRoutes; use Cone\Root\Traits\ResolvesActions; use Cone\Root\Traits\ResolvesFilters; @@ -37,7 +40,9 @@ abstract class Resource implements Arrayable, Form { - use AsForm; + use AsForm { + AsForm::resolveFields as __resolveFields; + } use Authorizable; use RegistersRoutes { RegistersRoutes::registerRoutes as __registerRoutes; @@ -298,6 +303,22 @@ public function modelTitle(Model $model): string return $model->getKey(); } + /** + * Resolve the fields collection. + */ + public function resolveFields(Request $request): Fields + { + if (is_null($this->fields)) { + $this->withFields(function (): array { + return in_array(HasRootEvents::class, class_uses_recursive($this->getModel())) + ? [new Events()] + : []; + }); + } + + return $this->__resolveFields($request); + } + /** * Define the filters for the object. */ @@ -429,6 +450,13 @@ public function handleFormRequest(Request $request, Model $model): void $this->saved($request, $model); + if (in_array(HasRootEvents::class, class_uses_recursive($model))) { + $model->recordRootEvent( + $model->wasRecentlyCreated ? 'Created' : 'Updated', + $request->user() + ); + } + DB::commit(); } catch (Throwable $exception) { report($exception); diff --git a/src/Traits/HasRootEvents.php b/src/Traits/HasRootEvents.php new file mode 100644 index 00000000..20bbee7a --- /dev/null +++ b/src/Traits/HasRootEvents.php @@ -0,0 +1,33 @@ +morphMany(Event::getProxiedClass(), 'target'); + } + + /** + * Record a new root event for the model. + */ + public function recordRootEvent(string $action, ?User $user = null, ?array $payload = null): Event + { + $event = $this->rootEvents()->make([ + 'action' => $action, + 'payload' => $payload, + ]); + + $event->user()->associate($user)->save(); + + return $event; + } +}