From 3d6aec36c5f76657c76af1e8f3e64e1bb1b75999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=2E=20Nagy=20Gerg=C5=91?= Date: Thu, 2 Nov 2023 10:01:32 +0100 Subject: [PATCH] merge columns into fields --- resources/views/columns/actions.blade.php | 3 - .../views/columns/cells/row-actions.blade.php | 18 -- .../views/columns/cells/row-select.blade.php | 5 - resources/views/columns/column.blade.php | 12 -- resources/views/columns/select-all.blade.php | 6 - .../cells => resources/table}/cell.blade.php | 0 .../views/resources/table/column.blade.php | 30 +++ .../views/resources/table/table.blade.php | 49 ++++- src/Actions/Action.php | 6 +- src/Actions/Actions.php | 21 +- src/Columns/Column.php | 193 ------------------ src/Columns/Columns.php | 55 ----- src/Columns/Relation.php | 52 ----- src/Columns/RowActions.php | 23 --- src/Columns/RowSelect.php | 23 --- src/Fields/BelongsToMany.php | 2 +- src/Fields/Dropdown.php | 4 +- src/Fields/Editor.php | 6 +- src/Fields/Field.php | 154 +++++++++++++- src/Fields/Fields.php | 46 ++++- src/Fields/Fieldset.php | 6 +- src/Fields/File.php | 4 +- src/{Columns => Fields}/ID.php | 8 +- src/Fields/Media.php | 4 +- src/Fields/Meta.php | 4 +- src/Fields/Relation.php | 50 ++++- src/Fields/Repeater.php | 8 +- src/Fields/Select.php | 4 +- src/Fields/Slug.php | 4 +- src/Filters/Filters.php | 8 + src/Filters/Search.php | 30 +-- src/Filters/Sort.php | 20 +- src/Resources/Resource.php | 21 +- src/Traits/ResolvesColumns.php | 66 ------ src/Traits/ResolvesFilters.php | 14 +- src/Traits/ResolvesModelValue.php | 81 -------- src/Traits/ResolvesVisibility.php | 58 ++++++ stubs/Resource.stub | 11 - stubs/UserResource.stub | 23 +-- 39 files changed, 476 insertions(+), 656 deletions(-) delete mode 100644 resources/views/columns/actions.blade.php delete mode 100644 resources/views/columns/cells/row-actions.blade.php delete mode 100644 resources/views/columns/cells/row-select.blade.php delete mode 100644 resources/views/columns/column.blade.php delete mode 100644 resources/views/columns/select-all.blade.php rename resources/views/{columns/cells => resources/table}/cell.blade.php (100%) create mode 100644 resources/views/resources/table/column.blade.php delete mode 100644 src/Columns/Column.php delete mode 100644 src/Columns/Columns.php delete mode 100644 src/Columns/Relation.php delete mode 100644 src/Columns/RowActions.php delete mode 100644 src/Columns/RowSelect.php rename src/{Columns => Fields}/ID.php (60%) delete mode 100644 src/Traits/ResolvesColumns.php delete mode 100644 src/Traits/ResolvesModelValue.php create mode 100644 src/Traits/ResolvesVisibility.php diff --git a/resources/views/columns/actions.blade.php b/resources/views/columns/actions.blade.php deleted file mode 100644 index 38d2ac524..000000000 --- a/resources/views/columns/actions.blade.php +++ /dev/null @@ -1,3 +0,0 @@ - - {{ $label }} - diff --git a/resources/views/columns/cells/row-actions.blade.php b/resources/views/columns/cells/row-actions.blade.php deleted file mode 100644 index 4383598c8..000000000 --- a/resources/views/columns/cells/row-actions.blade.php +++ /dev/null @@ -1,18 +0,0 @@ - -
- @can('update', $model) - - - - @endcan - @can('delete', $model) -
- @csrf - @method('DELETE') - -
- @endcan -
- diff --git a/resources/views/columns/cells/row-select.blade.php b/resources/views/columns/cells/row-select.blade.php deleted file mode 100644 index 9773c9b3b..000000000 --- a/resources/views/columns/cells/row-select.blade.php +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/resources/views/columns/column.blade.php b/resources/views/columns/column.blade.php deleted file mode 100644 index ac79028bf..000000000 --- a/resources/views/columns/column.blade.php +++ /dev/null @@ -1,12 +0,0 @@ - - @if($sortable) -
- {{ $label }} - - - -
- @else - {{ $label }} - @endif - diff --git a/resources/views/columns/select-all.blade.php b/resources/views/columns/select-all.blade.php deleted file mode 100644 index dd5a64283..000000000 --- a/resources/views/columns/select-all.blade.php +++ /dev/null @@ -1,6 +0,0 @@ - - {{ __('Select') }} - - diff --git a/resources/views/columns/cells/cell.blade.php b/resources/views/resources/table/cell.blade.php similarity index 100% rename from resources/views/columns/cells/cell.blade.php rename to resources/views/resources/table/cell.blade.php diff --git a/resources/views/resources/table/column.blade.php b/resources/views/resources/table/column.blade.php new file mode 100644 index 000000000..846d03bc4 --- /dev/null +++ b/resources/views/resources/table/column.blade.php @@ -0,0 +1,30 @@ + + @if($sortable) +
+ {{ $label }} + @if(Request::input('sort.by') !== $attribute || Request::input('sort.order', 'asc') === 'asc') + + @if(Request::input('sort.by') !== $attribute) + + @else + + @endif + + @else + + + + @endif +
+ @else + {{ $label }} + @endif + diff --git a/resources/views/resources/table/table.blade.php b/resources/views/resources/table/table.blade.php index c4da2becb..13aad74c6 100644 --- a/resources/views/resources/table/table.blade.php +++ b/resources/views/resources/table/table.blade.php @@ -11,17 +11,58 @@ - @foreach($columns as $column) - @include($column['template'], $column) + @if(! empty($actions)) + + @endif + @foreach($data[0]['fields'] as $column) + @include('root::resources.table.column', $column) @endforeach + @foreach($data as $row) - @foreach($row['cells'] as $cell) - @include($cell['template'], $cell) + @if(! empty($actions)) + + @endif + @foreach($row['fields'] as $cell) + @include('root::resources.table.cell', $cell) @endforeach + @endforeach diff --git a/src/Actions/Action.php b/src/Actions/Action.php index 28265af8f..55386ee7f 100644 --- a/src/Actions/Action.php +++ b/src/Actions/Action.php @@ -10,6 +10,7 @@ use Cone\Root\Traits\Authorizable; use Cone\Root\Traits\Makeable; use Cone\Root\Traits\RegistersRoutes; +use Cone\Root\Traits\ResolvesVisibility; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; @@ -28,6 +29,7 @@ abstract class Action implements Arrayable, Form, JsonSerializable use Authorizable; use HasAttributes; use Makeable; + use ResolvesVisibility; use RegistersRoutes { RegistersRoutes::registerRoutes as __registerRoutes; } @@ -219,12 +221,12 @@ public function toArray(): array /** * {@inheritdoc} */ - public function toTableComponent(Request $request): array + public function toForm(Request $request): array { return array_merge($this->toArray(), [ 'open' => $this->errors($request)->isNotEmpty(), 'fields' => $this->resolveFields($request) - ->mapToFormComponents($request, $this->query->getModel()), + ->mapToInputs($request, $this->query->getModel()), ]); } } diff --git a/src/Actions/Actions.php b/src/Actions/Actions.php index 98ef80f8e..37f8bb35f 100644 --- a/src/Actions/Actions.php +++ b/src/Actions/Actions.php @@ -3,6 +3,7 @@ namespace Cone\Root\Actions; use Cone\Root\Traits\RegistersRoutes; +use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; use Illuminate\Routing\Router; use Illuminate\Support\Arr; @@ -22,12 +23,28 @@ public function register(array|Action $actions): static return $this; } + /** + * Filter the actions that are available for the current request and model. + */ + public function authorized(Request $request, Model $model = null): static + { + return $this->filter->authorized($request, $model)->values(); + } + + /** + * Filter the actions that are visible in the given context. + */ + public function visible(string|array $context): static + { + return $this->filter->visible($context)->values(); + } + /** * Map the action to table components. */ - public function mapToTableComponents(Request $request): array + public function mapToForms(Request $request): array { - return $this->map->toTableComponent($request)->all(); + return $this->map->toForm($request)->all(); } /** diff --git a/src/Columns/Column.php b/src/Columns/Column.php deleted file mode 100644 index d78142eb2..000000000 --- a/src/Columns/Column.php +++ /dev/null @@ -1,193 +0,0 @@ -label = $label; - $this->modelAttribute = $modelAttribute ?: Str::of($label)->lower()->snake()->value(); - } - - /** - * Get the model attribute. - */ - public function getModelAttribute(): string - { - return $this->modelAttribute; - } - - /** - * Get the Blade template. - */ - public function getTemplate(): string - { - return $this->template; - } - - /** - * Set the sortable attribute. - */ - public function sortable(bool|Closure $value = true): static - { - $this->sortable = $value; - - return $this; - } - - /** - * Determine if the field is sortable. - */ - public function isSortable(): bool - { - if ($this->sortable instanceof Closure) { - return call_user_func($this->sortable); - } - - return $this->sortable; - } - - /** - * Get the sort URL. - */ - public function getSortUrl(Request $request): ?string - { - if (! $this->isSortable()) { - return null; - } - - return match ($request->input('sort.order', 'asc')) { - 'asc' => $request->fullUrlWithQuery(['sort' => ['order' => 'desc', 'sort' => $this->getModelAttribute()]]), - default => $request->fullUrlWithQuery(['sort' => ['order' => 'asc', 'by' => $this->getModelAttribute()]]), - }; - } - - /** - * Set the searachable attribute. - */ - public function searchable(bool|Closure $value = true): static - { - $this->searchable = $value; - - return $this; - } - - /** - * Determine if the field is searchable. - */ - public function isSearchable(): bool - { - if ($this->searchable instanceof Closure) { - return call_user_func($this->searchable); - } - - return $this->searchable; - } - - /** - * Set the search query resolver. - */ - public function searchWithQuery(Closure $callback): static - { - $this->searchQueryResolver = $callback; - - return $this; - } - - /** - * Resolve the search query. - */ - public function resolveSearchQuery(Request $request, Builder $query, mixed $value): Builder - { - return is_null($this->searchQueryResolver) - ? $query - : call_user_func_array($this->searchQueryResolver, [$request, $query, $value]); - } - - /** - * * Convert the column to a table head. - */ - public function toHead(Request $request): array - { - return [ - 'attribute' => $this->modelAttribute, - 'label' => $this->label, - 'sortable' => $this->isSortable(), - 'sortUrl' => $this->getSortUrl($request), - 'template' => 'root::columns.column', - ]; - } - - /** - * Convert the column to a cell. - */ - public function toCell(Request $request, Model $model): array - { - return [ - 'attrs' => $this->newAttributeBag(), - 'formattedValue' => $this->resolveFormat($request, $model), - 'model' => $model, - 'template' => $this->getTemplate(), - 'value' => $this->resolveValue($request, $model), - ]; - } -} diff --git a/src/Columns/Columns.php b/src/Columns/Columns.php deleted file mode 100644 index 47116efbb..000000000 --- a/src/Columns/Columns.php +++ /dev/null @@ -1,55 +0,0 @@ -push($column); - } - - return $this; - } - - /** - * Filter the searchable columns. - */ - public function searchable(): Collection - { - return $this->filter->isSearchable(); - } - - /** - * Filter the sortable columns. - */ - public function sortable(): Collection - { - return $this->filter->isSortable(); - } - - /** - * Map the columns to cells for the given model. - */ - public function mapToHeads(Request $request): array - { - return $this->map->toHead($request)->all(); - } - - /** - * Map the columns to cells for the given model. - */ - public function mapToCells(Request $request, Model $model): array - { - return $this->map->toCell($request, $model)->all(); - } -} diff --git a/src/Columns/Relation.php b/src/Columns/Relation.php deleted file mode 100644 index 251c4e3a7..000000000 --- a/src/Columns/Relation.php +++ /dev/null @@ -1,52 +0,0 @@ -searchableRelationAttributes = (array) $attributes; - - return $this; - } - - /** - * Set the sortable relation attribute. - */ - public function sortBy(string $attribute): static - { - $this->sortableRelationAttribute = $attribute; - - return $this; - } - - /** - * Get the serachable relation attributes. - */ - public function getSearchableRelationAttributes(): array - { - return $this->searchableRelationAttributes; - } - - /** - * Get the sortable relation attribute. - */ - public function getSortableRelationAttribute(): string - { - return $this->sortableRelationAttribute; - } -} diff --git a/src/Columns/RowActions.php b/src/Columns/RowActions.php deleted file mode 100644 index 9ed53713c..000000000 --- a/src/Columns/RowActions.php +++ /dev/null @@ -1,23 +0,0 @@ - 'root::columns.actions', - ]); - } -} diff --git a/src/Columns/RowSelect.php b/src/Columns/RowSelect.php deleted file mode 100644 index 5b91862ca..000000000 --- a/src/Columns/RowSelect.php +++ /dev/null @@ -1,23 +0,0 @@ - 'root::columns.select-all', - ]); - } -} diff --git a/src/Fields/BelongsToMany.php b/src/Fields/BelongsToMany.php index 13267f3c4..1d4e7efc8 100644 --- a/src/Fields/BelongsToMany.php +++ b/src/Fields/BelongsToMany.php @@ -162,7 +162,7 @@ public function toOption(Request $request, Model $model, Model $related): array $option['fields'] = is_null($this->pivotFieldsResolver) ? [] - : call_user_func_array($this->pivotFieldsResolver, [$request, $model, $related])->mapToFormComponents($request, $model); + : call_user_func_array($this->pivotFieldsResolver, [$request, $model, $related])->mapToInputs($request, $model); return $option; } diff --git a/src/Fields/Dropdown.php b/src/Fields/Dropdown.php index 458fc1bcc..e38adf6e1 100644 --- a/src/Fields/Dropdown.php +++ b/src/Fields/Dropdown.php @@ -27,9 +27,9 @@ public function __construct(string $label, string $modelAttribute = null) /** * {@inheritdoc} */ - public function toFormComponent(Request $request, Model $model): array + public function toInput(Request $request, Model $model): array { - $data = parent::toFormComponent($request, $model); + $data = parent::toInput($request, $model); return array_merge($data, [ 'options' => array_map(static function (array $option): array { diff --git a/src/Fields/Editor.php b/src/Fields/Editor.php index 3b1729043..0119d8f11 100644 --- a/src/Fields/Editor.php +++ b/src/Fields/Editor.php @@ -168,10 +168,10 @@ public function toArray(): array /** * {@inheritdoc} */ - public function toFormComponent(Request $request, Model $model): array + public function toInput(Request $request, Model $model): array { - return array_merge(parent::toFormComponent($request, $model), [ - 'media' => $this->media?->toFormComponent($request, $model), + return array_merge(parent::toInput($request, $model), [ + 'media' => $this->media?->toInput($request, $model), ]); } } diff --git a/src/Fields/Field.php b/src/Fields/Field.php index b6aeca16a..dce6f015f 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -3,11 +3,13 @@ namespace Cone\Root\Fields; use Closure; +use Cone\Root\Traits\Authorizable; use Cone\Root\Traits\HasAttributes; use Cone\Root\Traits\Makeable; -use Cone\Root\Traits\ResolvesModelValue; +use Cone\Root\Traits\ResolvesVisibility; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\MessageBag; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; use Illuminate\Support\Arr; @@ -18,10 +20,11 @@ abstract class Field implements Arrayable, JsonSerializable { + use Authorizable; use Conditionable; use HasAttributes; use Makeable; - use ResolvesModelValue; + use ResolvesVisibility; /** * The Blade template. @@ -33,6 +36,16 @@ abstract class Field implements Arrayable, JsonSerializable */ protected ?Closure $hydrateResolver = null; + /** + * The format resolver callback. + */ + protected ?Closure $formatResolver = null; + + /** + * The value resolver callback. + */ + protected ?Closure $valueResolver = null; + /** * The errors resolver callback. */ @@ -87,6 +100,21 @@ abstract class Field implements Arrayable, JsonSerializable */ protected bool $hydrated = false; + /** + * Indicates if the field is sortable. + */ + protected bool|Closure $sortable = false; + + /** + * Indicates if the field is searchable. + */ + protected bool|Closure $searchable = false; + + /** + * The search query resolver callback. + */ + protected ?Closure $searchQueryResolver = null; + /** * Create a new field instance. */ @@ -234,6 +262,80 @@ public function suffix(string $value): static return $this; } + /** + * Set the sortable attribute. + */ + public function sortable(bool|Closure $value = true): static + { + $this->sortable = $value; + + return $this; + } + + /** + * Determine if the field is sortable. + */ + public function isSortable(): bool + { + if ($this->sortable instanceof Closure) { + return call_user_func($this->sortable); + } + + return $this->sortable; + } + + /** + * Set the searachable attribute. + */ + public function searchable(bool|Closure $value = true): static + { + $this->searchable = $value; + + return $this; + } + + /** + * Determine if the field is searchable. + */ + public function isSearchable(): bool + { + if ($this->searchable instanceof Closure) { + return call_user_func($this->searchable); + } + + return $this->searchable; + } + + /** + * Set the search query resolver. + */ + public function searchWithQuery(Closure $callback): static + { + $this->searchQueryResolver = $callback; + + return $this; + } + + /** + * Resolve the serach query. + */ + public function resolveSearchQuery(Request $request, Builder $query, mixed $value): Builder + { + return is_null($this->searchQueryResolver) + ? $query + : call_user_func_array($this->searchQueryResolver, [$request, $query, $value]); + } + + /** + * Set the value resolver. + */ + public function value(Closure $callback): static + { + $this->valueResolver = $callback; + + return $this; + } + /** * Resolve the value. */ @@ -278,6 +380,38 @@ public function withoutOldValue(): mixed return $this->withOldValue(false); } + /** + * Get the default value from the model. + */ + public function getValue(Model $model): mixed + { + return $model->getAttribute($this->getModelAttribute()); + } + + /** + * Set the format resolver. + */ + public function format(Closure $callback): static + { + $this->formatResolver = $callback; + + return $this; + } + + /** + * Format the value. + */ + public function resolveFormat(Request $request, Model $model): mixed + { + $value = $this->resolveValue($request, $model); + + if (is_null($this->formatResolver)) { + return $value; + } + + return call_user_func_array($this->formatResolver, [$request, $model, $value]); + } + /** * Persist the request value on the model. */ @@ -405,21 +539,33 @@ public function toArray(): array 'prefix' => $this->prefix, 'suffix' => $this->suffix, 'template' => $this->getTemplate(), + 'searchable' => $this->isSearchable(), + 'sortable' => $this->isSortable(), ]; } /** * Get the form component data. */ - public function toFormComponent(Request $request, Model $model): array + public function toDisplay(Request $request, Model $model): array { return array_merge($this->toArray(), [ + 'value' => $this->resolveValue($request, $model), + 'formattedValue' => $this->resolveFormat($request, $model), + ]); + } + + /** + * Get the form component data. + */ + public function toInput(Request $request, Model $model): array + { + return array_merge($this->toDisplay($request, $model), [ 'attrs' => $this->newAttributeBag()->class([ 'form-control--invalid' => $this->invalid($request), ]), 'error' => $this->error($request), 'invalid' => $this->invalid($request), - 'value' => $this->resolveValue($request, $model), ]); } diff --git a/src/Fields/Fields.php b/src/Fields/Fields.php index f456d3357..c1398fc03 100644 --- a/src/Fields/Fields.php +++ b/src/Fields/Fields.php @@ -35,6 +35,38 @@ public function persist(Request $request, Model $model): void }); } + /** + * Filter the fields that are available for the current request and model. + */ + public function authorized(Request $request, Model $model = null): static + { + return $this->filter->authorized($request, $model)->values(); + } + + /** + * Filter the fields that are visible in the given context. + */ + public function visible(string|array $context): static + { + return $this->filter->visible($context)->values(); + } + + /** + * Filter the searchable fields. + */ + public function searchable(): static + { + return $this->filter->isSearchable(); + } + + /** + * Filter the sortable fields. + */ + public function sortable(): static + { + return $this->filter->isSortable(); + } + /** * Map the fields to validate. */ @@ -46,11 +78,19 @@ public function mapToValidate(Request $request, Model $model): array } /** - * Map the field to form components. + * Map the fields to displayable data. + */ + public function mapToDisplay(Request $request, Model $model): array + { + return $this->map->toDisplay($request, $model)->all(); + } + + /** + * Map the fields to form inputs. */ - public function mapToFormComponents(Request $request, Model $model): array + public function mapToInputs(Request $request, Model $model): array { - return $this->map->toFormComponent($request, $model)->all(); + return $this->map->toInput($request, $model)->all(); } /** diff --git a/src/Fields/Fieldset.php b/src/Fields/Fieldset.php index b5f37b0c1..985c5cf0e 100644 --- a/src/Fields/Fieldset.php +++ b/src/Fields/Fieldset.php @@ -77,10 +77,10 @@ public function invalid(Request $request): bool /** * {@inheritdoc} */ - public function toFormComponent(Request $request, Model $model): array + public function toInput(Request $request, Model $model): array { - return array_merge(parent::toFormComponent($request, $model), [ - 'fields' => $this->resolveFields($request)->mapToFormComponents($request, $model), + return array_merge(parent::toInput($request, $model), [ + 'fields' => $this->resolveFields($request)->mapToInputs($request, $model), ]); } diff --git a/src/Fields/File.php b/src/Fields/File.php index d8f847183..c6713fb97 100644 --- a/src/Fields/File.php +++ b/src/Fields/File.php @@ -247,9 +247,9 @@ public function toOption(Request $request, Model $model, Model $related): array /** * {@inheritdoc} */ - public function toFormComponent(Request $request, Model $model): array + public function toInput(Request $request, Model $model): array { - return array_merge(parent::toFormComponent($request, $model), [ + return array_merge(parent::toInput($request, $model), [ 'options' => $this->resolveOptions($request, $model), ]); } diff --git a/src/Columns/ID.php b/src/Fields/ID.php similarity index 60% rename from src/Columns/ID.php rename to src/Fields/ID.php index cf5a37d29..fee71343e 100644 --- a/src/Columns/ID.php +++ b/src/Fields/ID.php @@ -1,16 +1,14 @@ sortable(); } } diff --git a/src/Fields/Media.php b/src/Fields/Media.php index b15ac2273..85e936c4e 100644 --- a/src/Fields/Media.php +++ b/src/Fields/Media.php @@ -186,9 +186,9 @@ public function routes(Router $router): void /** * {@inheritdoc} */ - public function toFormComponent(Request $request, Model $model): array + public function toInput(Request $request, Model $model): array { - $data = parent::toFormComponent($request, $model); + $data = parent::toInput($request, $model); return array_merge($data, [ 'modalKey' => $this->getModalKey(), diff --git a/src/Fields/Meta.php b/src/Fields/Meta.php index 4e39ac09b..2d0e77034 100644 --- a/src/Fields/Meta.php +++ b/src/Fields/Meta.php @@ -252,9 +252,9 @@ public function toArray(): array /** * {@inheritdoc} */ - public function toFormComponent(Request $request, Model $model): array + public function toInput(Request $request, Model $model): array { - return $this->field->toFormComponent($request, $model); + return $this->field->toInput($request, $model); } /** diff --git a/src/Fields/Relation.php b/src/Fields/Relation.php index 5f88b7584..92749e839 100644 --- a/src/Fields/Relation.php +++ b/src/Fields/Relation.php @@ -17,6 +17,16 @@ abstract class Relation extends Field */ protected Closure|string $relation; + /** + * The searchable columns. + */ + protected array $searchableColumns = ['id']; + + /** + * The sortable column. + */ + protected string $sortableColumn = 'id'; + /** * Indicates if the field should be nullable. */ @@ -113,6 +123,42 @@ public function isNullable(): bool return $this->nullable; } + /** + * Set the searachable attribute. + */ + public function searchable(bool|Closure $value = true, array $columns = ['id']): static + { + $this->searchableColumns = $columns; + + return parent::searchable($value); + } + + /** + * Get the searchable columns. + */ + public function getSearchableColumns(): array + { + return $this->searchableColumns; + } + + /** + * Set the sortable attribute. + */ + public function sortable(bool|Closure $value = true, string $column = 'id'): static + { + $this->sortableColumn = $column; + + return parent::sortable($value); + } + + /** + * Get the sortable columns. + */ + public function getSortableColumn(): string + { + return $this->sortableColumn; + } + /** * Set the display resolver. */ @@ -242,9 +288,9 @@ public function toOption(Request $request, Model $model, Model $related): array /** * {@inheritdoc} */ - public function toFormComponent(Request $request, Model $model): array + public function toInput(Request $request, Model $model): array { - return array_merge(parent::toFormComponent($request, $model), [ + return array_merge(parent::toInput($request, $model), [ 'nullable' => $this->isNullable(), 'options' => $this->resolveOptions($request, $model), ]); diff --git a/src/Fields/Repeater.php b/src/Fields/Repeater.php index e40119a3f..cadb80082 100644 --- a/src/Fields/Repeater.php +++ b/src/Fields/Repeater.php @@ -173,7 +173,7 @@ public function buildOption(Request $request, Model $model): array { $option = $this->toOption($request, $model, $this->newTemporaryModel([])); - $option['fields'] = $option['fields']->mapToFormComponents($request, $model); + $option['fields'] = $option['fields']->mapToInputs($request, $model); $option['html'] = View::make('root::fields.repeater-option', $option)->render(); @@ -218,13 +218,13 @@ public function toOption(Request $request, Model $model, Model $tmpModel): array /** * {@inheritdoc} */ - public function toFormComponent(Request $request, Model $model): array + public function toInput(Request $request, Model $model): array { - return array_merge(parent::toFormComponent($request, $model), [ + return array_merge(parent::toInput($request, $model), [ 'addNewLabel' => $this->getAddNewOptionLabel(), 'max' => $this->max, 'options' => array_map(static function (array $option) use ($request, $model): array { - $option['fields'] = $option['fields']->mapToFormComponents($request, $model); + $option['fields'] = $option['fields']->mapToInputs($request, $model); return array_merge($option, [ 'html' => View::make('root::fields.repeater-option', $option)->render(), diff --git a/src/Fields/Select.php b/src/Fields/Select.php index 35e6f76bd..f52037d20 100644 --- a/src/Fields/Select.php +++ b/src/Fields/Select.php @@ -107,9 +107,9 @@ public function newOption(mixed $value, string $label): Option /** * {@inheritdoc} */ - public function toFormComponent(Request $request, Model $model): array + public function toInput(Request $request, Model $model): array { - return array_merge(parent::toFormComponent($request, $model), [ + return array_merge(parent::toInput($request, $model), [ 'nullable' => $this->isNullable(), 'options' => $this->resolveOptions($request, $model), ]); diff --git a/src/Fields/Slug.php b/src/Fields/Slug.php index c71084c88..ae9434bf9 100644 --- a/src/Fields/Slug.php +++ b/src/Fields/Slug.php @@ -185,9 +185,9 @@ static function (array $match): string { /** * {@inheritdoc} */ - public function toFormComponent(Request $request, Model $model): array + public function toInput(Request $request, Model $model): array { - return array_merge(parent::toFormComponent($request, $model), [ + return array_merge(parent::toInput($request, $model), [ 'help' => $this->help ?: __('Leave it empty for auto-generated slug.'), ]); } diff --git a/src/Filters/Filters.php b/src/Filters/Filters.php index 75fd7e9e3..7af8b743e 100644 --- a/src/Filters/Filters.php +++ b/src/Filters/Filters.php @@ -21,6 +21,14 @@ public function register(array|Filter $filters): static return $this; } + /** + * Filter the filters that are available for the given request. + */ + public function authorized(Request $request): static + { + return $this->filter->authorized($request)->values(); + } + /** * Apply the filters on the query. */ diff --git a/src/Filters/Search.php b/src/Filters/Search.php index 8d22a2b10..7e378d03d 100644 --- a/src/Filters/Search.php +++ b/src/Filters/Search.php @@ -2,25 +2,25 @@ namespace Cone\Root\Filters; -use Cone\Root\Columns\Column; -use Cone\Root\Columns\Columns; -use Cone\Root\Columns\Relation; +use Cone\Root\Fields\Field; +use Cone\Root\Fields\Fields; +use Cone\Root\Fields\Relation; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; class Search extends RenderableFilter { /** - * The searchable columns. + * The searchable fields. */ - protected Columns $columns; + protected Fields $fields; /** * Create a new filter instance. */ - public function __construct(Columns $columns) + public function __construct(Fields $fields) { - $this->columns = $columns; + $this->fields = $fields; } /** @@ -28,9 +28,9 @@ public function __construct(Columns $columns) */ public function apply(Request $request, Builder $query, mixed $value): Builder { - $attributes = $this->columns->mapWithKeys(static function (Column $column): array { + $attributes = $this->fields->mapWithKeys(static function (Field $field): array { return [ - $column->getModelAttribute() => $column instanceof Relation ? $column->getSearchableRelationAttributes() : null, + $field->getModelAttribute() => $field instanceof Relation ? $field->getSearchableColumns() : null, ]; })->all(); @@ -39,15 +39,15 @@ public function apply(Request $request, Builder $query, mixed $value): Builder } return $query->where(static function (Builder $query) use ($attributes, $value): void { - foreach ($attributes as $attribute => $columns) { + foreach ($attributes as $attribute => $fields) { $operator = array_key_first($attributes) === $attribute ? 'and' : 'or'; - if (is_array($columns)) { - $query->has($attribute, '>=', 1, $operator, static function (Builder $query) use ($columns, $value): Builder { - foreach ($columns as $column) { - $operator = $columns[0] === $column ? 'and' : 'or'; + if (is_array($fields)) { + $query->has($attribute, '>=', 1, $operator, static function (Builder $query) use ($fields, $value): Builder { + foreach ($fields as $field) { + $operator = $fields[0] === $field ? 'and' : 'or'; - $query->where($query->qualifyColumn($column), 'like', "%{$value}%", $operator); + $query->where($query->qualifyColumn($field), 'like', "%{$value}%", $operator); } return $query; diff --git a/src/Filters/Sort.php b/src/Filters/Sort.php index 810d09446..f895fd685 100644 --- a/src/Filters/Sort.php +++ b/src/Filters/Sort.php @@ -2,9 +2,9 @@ namespace Cone\Root\Filters; -use Cone\Root\Columns\Column; -use Cone\Root\Columns\Columns; -use Cone\Root\Columns\Relation; +use Cone\Root\Fields\Field; +use Cone\Root\Fields\Fields; +use Cone\Root\Fields\Relation; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\Relation as EloquentRelation; @@ -13,16 +13,16 @@ class Sort extends Filter { /** - * The sortable columns. + * The sortable fields. */ - protected Columns $columns; + protected Fields $fields; /** * Create a new filter instance. */ - public function __construct(Columns $columns) + public function __construct(Fields $fields) { - $this->columns = $columns; + $this->fields = $fields; } /** @@ -32,9 +32,9 @@ public function apply(Request $request, Builder $query, mixed $value): Builder { $value = array_replace(['by' => 'id', 'order' => 'desc'], (array) $value); - $attributes = $this->columns->mapWithKeys(static function (Column $column): array { + $attributes = $this->fields->mapWithKeys(static function (Field $field): array { return [ - $column->getModelAttribute() => $column instanceof Relation ? $column->getSortableRelationAttribute() : null, + $field->getModelAttribute() => $field instanceof Relation ? $field->getSortableColumn() : null, ]; })->all(); @@ -57,7 +57,7 @@ public function apply(Request $request, Builder $query, mixed $value): Builder ? $relation->getQualifiedOwnerKeyName() : $relation->getQualifiedParentKeyName(); - return $relation->whereColumn($relation->getQualifiedForeignKeyName(), '=', $key); + return $relation->whereField($relation->getQualifiedForeignKeyName(), '=', $key); }); return $query->orderBy( diff --git a/src/Resources/Resource.php b/src/Resources/Resource.php index 98cb8ad22..d6eccbde0 100644 --- a/src/Resources/Resource.php +++ b/src/Resources/Resource.php @@ -12,7 +12,6 @@ use Cone\Root\Traits\Authorizable; use Cone\Root\Traits\RegistersRoutes; use Cone\Root\Traits\ResolvesActions; -use Cone\Root\Traits\ResolvesColumns; use Cone\Root\Traits\ResolvesFilters; use Cone\Root\Traits\ResolvesWidgets; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -31,13 +30,12 @@ abstract class Resource implements Arrayable, Form, Table { use AsForm; use Authorizable; - use RegistersRoutes { - RegistersRoutes::registerRoutes as __registerRoutes; - } use ResolvesActions; - use ResolvesColumns; use ResolvesFilters; use ResolvesWidgets; + use RegistersRoutes { + RegistersRoutes::registerRoutes as __registerRoutes; + } /** * The model class. @@ -274,7 +272,9 @@ public function paginate(Request $request): LengthAwarePaginator ->through(function (Model $model) use ($request): array { return [ 'id' => $model->getKey(), - 'cells' => $this->resolveColumns($request)->mapToCells($request, $model), + 'url' => $this->modelUrl($model), + 'model' => $model, + 'fields' => $this->resolveFields($request)->mapToDisplay($request, $model), ]; }); } @@ -315,15 +315,14 @@ public function toIndex(Request $request): array { return array_merge($this->toArray(), [ 'title' => $this->getName(), - 'columns' => $this->resolveColumns($request)->mapToHeads($request), - 'actions' => $this->resolveActions($request)->mapToTableComponents($request), + 'actions' => $this->resolveActions($request)->mapToForms($request), 'data' => $this->paginate($request), 'widgets' => $this->resolveWidgets($request)->all(), 'perPageOptions' => $this->getPerPageOptions(), 'filters' => $this->resolveFilters($request) ->renderable() ->map(function (RenderableFilter $filter) use ($request): array { - return $filter->toField()->toFormComponent($request, $this->getModelInstance()); + return $filter->toField()->toInput($request, $this->getModelInstance()); }) ->all(), 'activeFilters' => $this->resolveFilters($request)->active($request)->count(), @@ -340,7 +339,7 @@ public function toCreate(Request $request): array 'model' => $model = $this->getModelInstance(), 'action' => $this->getUri(), 'method' => 'POST', - 'fields' => $this->resolveFields($request)->mapToFormComponents($request, $model), + 'fields' => $this->resolveFields($request)->mapToInputs($request, $model), ]); } @@ -354,7 +353,7 @@ public function toEdit(Request $request, Model $model): array 'model' => $model, 'action' => $this->modelUrl($model), 'method' => 'PATCH', - 'fields' => $this->resolveFields($request)->mapToFormComponents($request, $model), + 'fields' => $this->resolveFields($request)->mapToInputs($request, $model), ]); } } diff --git a/src/Traits/ResolvesColumns.php b/src/Traits/ResolvesColumns.php deleted file mode 100644 index 4f86ef6c5..000000000 --- a/src/Traits/ResolvesColumns.php +++ /dev/null @@ -1,66 +0,0 @@ -columns)) { - $this->columns = new Columns($this->columns($request)); - - if ($this->resolveActions($request)->isNotEmpty()) { - $this->columns->prepend( - RowSelect::make(__('Select'), 'id')->value(static function (Request $request, Model $model): string { - return $model->getKey(); - }) - ); - } - - $this->columns->push( - RowActions::make(__('Actions'), 'id')->value(function (Request $request, Model $model): string { - return $this->modelUrl($model); - }) - ); - - $this->columns->each(function (Column $column) use ($request): void { - $this->resolveColumn($request, $column); - }); - } - - return $this->columns; - } - - /** - * Handle the callback for the column resolution. - */ - protected function resolveColumn(Request $request, Column $column): void - { - // - } -} diff --git a/src/Traits/ResolvesFilters.php b/src/Traits/ResolvesFilters.php index 14b2311ab..c247b39fd 100644 --- a/src/Traits/ResolvesFilters.php +++ b/src/Traits/ResolvesFilters.php @@ -2,7 +2,7 @@ namespace Cone\Root\Traits; -use Cone\Root\Columns\Columns; +use Cone\Root\Fields\Fields; use Cone\Root\Filters\Filter; use Cone\Root\Filters\Filters; use Cone\Root\Filters\Search; @@ -36,16 +36,16 @@ public function resolveFilters(Request $request): Filters if (is_null($this->filters)) { $this->filters = new Filters($this->filters($request)); - $this->resolveColumns($request) + $this->resolveFields($request) ->searchable() - ->whenNotEmpty(function (Columns $columns): void { - $this->filters->prepend(new Search($columns)); + ->whenNotEmpty(function (Fields $fields): void { + $this->filters->prepend(new Search($fields)); }); - $this->resolveColumns($request) + $this->resolveFields($request) ->sortable() - ->whenNotEmpty(function (Columns $columns): void { - $this->filters->register(new Sort($columns)); + ->whenNotEmpty(function (Fields $fields): void { + $this->filters->register(new Sort($fields)); }); if (in_array(SoftDeletes::class, class_uses_recursive($this->getModel()))) { diff --git a/src/Traits/ResolvesModelValue.php b/src/Traits/ResolvesModelValue.php deleted file mode 100644 index 1b6f1d64f..000000000 --- a/src/Traits/ResolvesModelValue.php +++ /dev/null @@ -1,81 +0,0 @@ -valueResolver = $callback; - - return $this; - } - - /** - * Resolve the value. - */ - public function resolveValue(Request $request, Model $model): mixed - { - $value = $this->getValue($model); - - if (is_null($this->valueResolver)) { - return $value; - } - - return call_user_func_array($this->valueResolver, [$request, $model, $value]); - } - - /** - * Get the default value from the model. - */ - public function getValue(Model $model): mixed - { - return $model->getAttribute($this->getModelAttribute()); - } - - /** - * Set the format resolver. - */ - public function format(Closure $callback): static - { - $this->formatResolver = $callback; - - return $this; - } - - /** - * Format the value. - */ - public function resolveFormat(Request $request, Model $model): mixed - { - $value = $this->resolveValue($request, $model); - - if (is_null($this->formatResolver)) { - return $value; - } - - return call_user_func_array($this->formatResolver, [$request, $model, $value]); - } -} diff --git a/src/Traits/ResolvesVisibility.php b/src/Traits/ResolvesVisibility.php new file mode 100644 index 000000000..0126c6936 --- /dev/null +++ b/src/Traits/ResolvesVisibility.php @@ -0,0 +1,58 @@ +visibilityResolvers as $callback) { + if (! call_user_func_array($callback, [$context])) { + return false; + } + } + + return true; + } + + /** + * Set a custom visibility resolver. + */ + public function visibleOn(string|array|Closure $context): static + { + $this->visibilityResolvers[] = $context instanceof Closure + ? $context + : static function (string|array $currentContext) use ($context) { + return ! empty(array_intersect(Arr::wrap($currentContext), Arr::wrap($context))); + }; + + return $this; + } + + /** + * Set a custom hidden resolver. + */ + public function hiddenOn(string|array|Closure $context): static + { + $context = $context instanceof Closure + ? static function (array|string $currentContext) use ($context): bool { + return ! call_user_func_array($context, [$currentContext]); + } + : static function (string|array $currentContext) use ($context) { + return empty(array_intersect(Arr::wrap($currentContext), Arr::wrap($context))); + }; + + return $this->visibleOn($context); + } +} diff --git a/stubs/Resource.stub b/stubs/Resource.stub index db1122b25..0d0deb47f 100644 --- a/stubs/Resource.stub +++ b/stubs/Resource.stub @@ -5,7 +5,6 @@ namespace {{ namespace }}; use Cone\Root\Fields\Fields; use Cone\Root\Interfaces\Form; use Cone\Root\Resources\Resource; -use Cone\Root\Columns\Columns; use Cone\Root\Table\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; @@ -18,16 +17,6 @@ class {{ class }} extends Resource */ protected string $model = {{ model }}; - /** - * Define the columns. - */ - public function columns(Request $request): array - { - return array_merge(parent::columns($request), [ - // - ]); - } - /** * Define the fields. */ diff --git a/stubs/UserResource.stub b/stubs/UserResource.stub index 29c7a1437..d5e15ed0c 100644 --- a/stubs/UserResource.stub +++ b/stubs/UserResource.stub @@ -2,9 +2,8 @@ namespace App\Root\Resources; -use Cone\Root\Columns\Column; -use Cone\Root\Columns\ID; use Cone\Root\Fields\Email; +use Cone\Root\Fields\ID; use Cone\Root\Fields\Text; use Cone\Root\Resources\Resource; use Illuminate\Database\Eloquent\Model; @@ -18,30 +17,14 @@ class UserResource extends Resource */ protected string $icon = 'users'; - /** - * Define the columns. - */ - public function columns(Request $request): array - { - return array_merge(parent::columns($request), [ - ID::make(), - - Column::make(__('Name'), 'name') - ->searchable() - ->sortable(), - - Column::make(__('Email'), 'email') - ->searchable() - ->sortable(), - ]); - } - /** * Define the fields. */ public function fields(Request $request): array { return array_merge(parent::fields($request), [ + ID::make(), + Text::make(__('Name'), 'name') ->rules(['required', 'string', 'max:256']),
+ {{ __('Select') }} + + + {{ __('Actions') }} +
+ + +
+ @can('update', $row['model']) + + + + @endcan + @can('delete', $row['model']) +
+ @csrf + @method('DELETE') + +
+ @endcan +
+