diff --git a/demo/app/Sharp/Dashboard/PostDashboard.php b/demo/app/Sharp/Dashboard/PostDashboard.php new file mode 100644 index 000000000..4852162df --- /dev/null +++ b/demo/app/Sharp/Dashboard/PostDashboard.php @@ -0,0 +1,87 @@ +addWidget( + SharpLineGraphWidget::make('visits_line') + ->setTitle('Visits') + ->setHeight(200) + ->setShowLegend(false) + ->setDisplayHorizontalAxisAsTimeline() + ) + ->addWidget( + SharpFigureWidget::make('visit_count') + ->setTitle('Total visits'), + ) + ->addWidget( + SharpFigureWidget::make('page_count') + ->setTitle('Total pageviews'), + ); + } + + protected function buildDashboardLayout(DashboardLayout $dashboardLayout): void + { + $dashboardLayout + ->addFullWidthWidget('visits_line') + ->addRow(fn (DashboardLayoutRow $row) => $row + ->addWidget(6, 'visit_count') + ->addWidget(6, 'page_count') + ); + } + + public function getFilters(): ?array + { + return [ + PeriodRequiredFilter::class, + HiddenFilter::make('post'), + ]; + } + + protected function buildWidgetsData(): void + { + $visitCount = $this->getStartDate()->diffInDays($this->getEndDate()) * rand(10, 100); + + $this + ->setFigureData('visit_count', figure: $visitCount) + ->setFigureData('page_count', figure: $visitCount * rand(2, 10)) + ->addGraphDataSet( + 'visits_line', + SharpGraphWidgetDataSet::make(collect(CarbonPeriod::create($this->getStartDate(), $this->getEndDate())) + ->mapWithKeys(fn (Carbon $day, $k) => [ + $day->isoFormat('L') => rand(10, 100), + ])) + ->setLabel('Visits') + ->setColor('#274754'), + ); + } + + protected function getStartDate(): Carbon + { + return $this->queryParams->filterFor(PeriodRequiredFilter::class)->getStart(); + } + + protected function getEndDate(): Carbon + { + return min( + $this->queryParams->filterFor(PeriodRequiredFilter::class)->getEnd(), + today()->subDay(), + ); + } +} diff --git a/demo/app/Sharp/Entities/PostDashboardEntity.php b/demo/app/Sharp/Entities/PostDashboardEntity.php new file mode 100644 index 000000000..ee868abdb --- /dev/null +++ b/demo/app/Sharp/Entities/PostDashboardEntity.php @@ -0,0 +1,23 @@ +isAdmin(); + } + }; + } +} diff --git a/demo/app/Sharp/Posts/PostShow.php b/demo/app/Sharp/Posts/PostShow.php index a2bcbc759..af8e10dbe 100644 --- a/demo/app/Sharp/Posts/PostShow.php +++ b/demo/app/Sharp/Posts/PostShow.php @@ -5,6 +5,7 @@ use App\Models\Post; use App\Sharp\Entities\AuthorEntity; use App\Sharp\Entities\PostBlockEntity; +use App\Sharp\Entities\PostDashboardEntity; use App\Sharp\Entities\PostEntity; use App\Sharp\Posts\Commands\EvaluateDraftPostWizardCommand; use App\Sharp\Posts\Commands\PreviewPostCommand; @@ -14,6 +15,7 @@ use App\Sharp\Utils\Embeds\TableOfContentsEmbed; use App\Sharp\Utils\Filters\CategoryFilter; use Code16\Sharp\Form\Eloquent\Uploads\Transformers\SharpUploadModelFormAttributeTransformer; +use Code16\Sharp\Show\Fields\SharpShowDashboardField; use Code16\Sharp\Show\Fields\SharpShowEntityListField; use Code16\Sharp\Show\Fields\SharpShowFileField; use Code16\Sharp\Show\Fields\SharpShowListField; @@ -64,6 +66,11 @@ protected function buildShowFields(FieldsContainer $showFields): void ->setLabel('File') ) ) + ->addField( + SharpShowDashboardField::make(PostDashboardEntity::class) + ->setLabel('Analytics') + ->hideFilterWithValue('post', fn ($instanceId) => $instanceId) + ) ->addField( SharpShowEntityListField::make(PostBlockEntity::class) ->setLabel('Blocks') @@ -94,6 +101,7 @@ protected function buildShowLayout(ShowLayout $showLayout): void $column->withField('content'); }); }) + ->addDashboardSection(PostDashboardEntity::class) ->addEntityListSection(PostBlockEntity::class); } diff --git a/docs/.vitepress/sidebar.ts b/docs/.vitepress/sidebar.ts index a8dae5976..855dfc670 100644 --- a/docs/.vitepress/sidebar.ts +++ b/docs/.vitepress/sidebar.ts @@ -57,7 +57,8 @@ export function sidebar(): DefaultTheme.SidebarItem[] { { text: 'Picture', link: '/guide/show-fields/picture.md' }, { text: 'List', link: '/guide/show-fields/list.md' }, { text: 'File', link: '/guide/show-fields/file.md' }, - { text: 'Embedded EntityList', link: '/guide/show-fields/embedded-entity-list.md' }, + { text: 'Entity List', link: '/guide/show-fields/entity-list.md' }, + { text: 'Dashboard', link: '/guide/show-fields/dashboard.md' }, // { text: 'Custom show field', link: '/guide/custom-show-fields.md' } ] }, diff --git a/docs/guide/building-show-page.md b/docs/guide/building-show-page.md index bef0cef03..987c63845 100644 --- a/docs/guide/building-show-page.md +++ b/docs/guide/building-show-page.md @@ -56,7 +56,7 @@ class MyShow extends SharpShow Each available Show field is detailed below; here are the attributes they all share : -- `setShowIfEmpty(bool $show = true): self`: by default, an empty field (meaning: with null or empty data) is not displayed at all in the Show UI. You can change this behaviour with this attribute. This method has no impact for [embedded EntityList](show-fields/embedded-entity-list.md). +- `setShowIfEmpty(bool $show = true): self`: by default, an empty field (meaning: with null or empty data) is not displayed at all in the Show UI. You can change this behaviour with this attribute. This method has no impact for the [Entity List field](show-fields/entity-list.md). #### Available simple Show fields @@ -95,9 +95,9 @@ Notice that you have three possibilities for the actual code of this Entity List As always with Sharp, implementation is up to you. -The next thing to do is to scope the data of the embedded Entity List. In our case, we want to display and interact only with the products for this order... For this and more on personalization, refer to the detailed documentation of this field: +The next thing to do is to scope the data of the Entity List field. In our case, we want to display and interact only with the products for this order... For this and more on personalization, refer to the detailed documentation of this field: -- [embedded EntityList](show-fields/embedded-entity-list.md) +- [Entity List field](show-fields/entity-list.md) ### `buildShowLayout(ShowLayout $showLayout): void` diff --git a/docs/guide/entity-class.md b/docs/guide/entity-class.md index ef5fa3f6a..60be6b019 100644 --- a/docs/guide/entity-class.md +++ b/docs/guide/entity-class.md @@ -202,5 +202,5 @@ You must remove all `->declareEntity()` calls in order to use `->declareEntityRe ::: ::: warning -If you are using a custom entity resolver, you won’t be able to use the `SharpEntity` classes in the [menu](building-menu.md), or in [`LinkTo` links](link-to.md), or for [embedded entity lists](show-fields/embedded-entity-list.md): you will have to use the entity key instead. For instance: `LinkToForm::make('products', $id)`. +If you are using a custom entity resolver, you won’t be able to use the `SharpEntity` classes in the [menu](building-menu.md), or in [`LinkTo` links](link-to.md), or for [Entity List fields](show-fields/entity-list.md): you will have to use the entity key instead. For instance: `LinkToForm::make('products', $id)`. ::: diff --git a/docs/guide/show-fields/dashboard.md b/docs/guide/show-fields/dashboard.md new file mode 100644 index 000000000..da25ea443 --- /dev/null +++ b/docs/guide/show-fields/dashboard.md @@ -0,0 +1,124 @@ +# Dashboard + +Class: `Code16\Sharp\Show\Fields\SharpShowDashboardField`. + +The field allows you to integrate a [Dashboard](../building-dashboard.md) into your Show Page. + +## Constructor + +This field needs, as first parameter, either the entity key or the `SharpDashboardEntity` class that declares the dashboard which will be included in the Show Page. + +For instance: + +```php +SharpShowDashboardField::make('posts_dashboard') +``` + +or + +```php +SharpShowDashboardField::make(PostDashboardEntity::class) +``` +::: warning +This last syntax is better in terms of DX (since it allows using the IDE to navigate to the Entity List implementation), but it won’t work in two specific cases: if you use a custom `SharpEntityResolver` or if you your Entity is declared with multiple keys. +::: + +## Configuration + +### `hideFilterWithValue(string $filterName, $value)` +This is the most important method of the field, since it will not only hide a filter but also set its value. The purpose is to allow to **scope the data to the instance** of the Show Page. For example, let’s say we display a Post and that we want to embed a dashboard with the post's statistics: + + +```php +class PostShow extends SharpShow +{ + // ... + + public function buildShowFields(FieldsContainer $showFields): void + { + $showFields->addField( + SharpShowDashboardField::make(PostDashboardEntity::class) + ->hideFilterWithValue(PostFilter::class, 64) + ); + } +} +``` + +Here we're scoping the `PostDashboard` declared in the `PostDashboardEntity` to the instance of the `Post` with id 64. + + +You can pass a closure as the value, and it will contain the current Show instance id. In most cases, you'll have to write this: + +```php +SharpShowDashboardField::make(PostDashboardEntity::class) + ->hideFilterWithValue(PostFilter::class, fn ($instanceId) => $instanceId); +``` + +**One final note**: sometimes the linked filter is really just a scope, never displayed to the user. In this case, it can be tedious to write a full implementation in the Dashboard. In this situation, you can use the `HiddenFiler` class for the filter, passing a key: + +```php +class PostShow extends SharpShow +{ + // ... + + public function buildShowFields(FieldsContainer $showFields): void + { + $showFields->addField( + SharpShowDashboardField::make(PostDashboardEntity::class) + ->hideFilterWithValue('post', fn ($instanceId) => $instanceId); + ); + } +} +``` + +```php +use \Code16\Sharp\EntityList\Filters\HiddenFilter; + +class PostDashboard extends SharpDashboard +{ + // ... + + protected function getFilters(): ?array + { + return [ + HiddenFilter::make('post') + ]; + } + + protected function buildWidgetsData(): void + { + return $this->setFigureData('visit_count', + figure: Post::query() + ->findOrFail($this->queryParams->filterFor('post')) + ->get()?->visit_count + ); + } +} +``` + +### `hideDashboardCommand(array|string $commands): self` + +Use it to hide any dashboard command in this particular Dashboard (useful when reusing a Dashboard). This will apply before looking at authorizations. + +## Display in layout + +To display your dashboard in your show page's layout, you have to use the `addDashboardSection()` method in your Show Page's `buildShowLayout()` method. + +```php +protected function buildShowLayout(ShowLayout $showLayout): void + { + $showLayout + ->addSection(function (ShowLayoutSection $section) { + $section + ->addColumn(7, function (ShowLayoutColumn $column) { + $column + ->withFields(categories: 5, author: 7) + // ... + }) + ->addColumn(5, function (ShowLayoutColumn $column) { + // ... + }); + }) + ->addDashboardSection(PostDashboardEntity::class); + } +``` diff --git a/docs/guide/show-fields/embedded-entity-list.md b/docs/guide/show-fields/entity-list.md similarity index 52% rename from docs/guide/show-fields/embedded-entity-list.md rename to docs/guide/show-fields/entity-list.md index 89151f153..85b5e75e6 100644 --- a/docs/guide/show-fields/embedded-entity-list.md +++ b/docs/guide/show-fields/entity-list.md @@ -1,10 +1,12 @@ -# Embedded EntityList +# Entity List Class: `Code16\Sharp\Show\Fields\SharpShowEntityListField` +The field allows you to integrate an [Entity List](../building-entity-list.md) into your Show Page. + ## Constructor -This field needs, as first parameter, either the entity key or the `SharpEntity` class that declares the Entity List which will be embedded. +This field needs, as first parameter, either the entity key or the `SharpEntity` class that declares the Entity List which will be included in the Show Page. For instance: @@ -19,26 +21,26 @@ SharpShowEntityListField::make(ProductEntity::class) ``` ::: warning -This last syntax is better in terms of DX (since it allows to use the IDE to navigate to the Entity List implementation), but it won’t work in two specific cases: if you use a custom `SharpEntityResolver` or if you your Entity is declared with multiple keys. +This last syntax is better in terms of DX (since it allows using the IDE to navigate to the Entity List implementation), but it won’t work in two specific cases: if you use a custom `SharpEntityResolver` or if you your Entity is declared with multiple keys. ::: -To handle special (rare) cases you can provide a second string argument: in this case the first argument is the field key (as referred in the layout), and the second argument is the related Entity List key: +To handle special cases, you can provide a second string argument: the first argument is the field key (as referred in the layout), and the second argument is the related Entity List key: ```php SharpShowEntityListField::make('order_products', 'products') ``` ::: tip -Note that unlike every other Show Field, the `instance` of the Show don't have to expose an attribute named like that, since the EntityList data is gathered with a dedicated request. +Note that unlike every other Show Field, the `instance` of the Show don't have to expose an attribute named like that, since the Entity List data is gathered with a dedicated request. ::: -Embedded Entity List are really just regular Entity List presented in a Show page: we therefore need a full Entity List implementation, declared in an Entity. To scope the data to the `instance` of the Show, you can use `hideFilterWithValue()` (see below). +Entity List fields really are just regular Entity List presented in a Show page: we therefore need a full Entity List implementation, declared in an Entity. To scope the data to the `instance` of the Show, you can use `hideFilterWithValue()` (see below). ## Configuration ### `hideFilterWithValue(string $filterName, $value)` -This is the most important method of the field, since it will not only hide a filter, but also set its value. The purpose is to allow to **scope the data to the instance** of the Show Page. For example let’s say we display an Order and that we want to embed a list of its products: +This is the most important method of the field, since it will not only hide a filter but also set its value. The purpose is to allow to **scope the data to the instance** of the Show Page. For example, let’s say we display an Order and that we want to embed a list of its products: ```php class OrderShow extends SharpShow @@ -55,7 +57,7 @@ class OrderShow extends SharpShow } ``` -We defined here that we want to embed the Entity List defined in the `ProductEntity`, with its `OrderFilter` filter (which must be declared as usual in the Entity List implementation) hidden AND valued to `64` when gathering the data. In short: we want the products for the order of id `64`. +We defined here that we want to display the Entity List defined in the `ProductEntity`, with its `OrderFilter` filter (which must be declared as usual in the Entity List implementation) hidden AND valued to `64` when gathering the data. In short: we want the products for the order of id `64`. ::: tip Note on the filter name: passing its full classname will always work, but you can also directly pass its `key`, in case you defined one. @@ -112,11 +114,11 @@ class OrderProductList extends SharpEntityList ### `hideEntityCommand(array|string $commands): self` -Use it to hide any entity command in this particular embedded Entity List (useful when reusing an EntityList). This will apply before looking at authorizations. +Use it to hide any entity command in this particular Entity List (useful when reusing an Entity List displayed elsewhere). This will apply before looking at authorizations. ### `hideInstanceCommand(array|string $commands): self` -Use it to hide any instance command in this particular embedded Entity List (useful when reusing an Entity List). This will apply before looking at authorizations. +Use it to hide any instance command in this particular Entity List (useful when reusing an Entity List). This will apply before looking at authorizations. ### `showEntityState(bool $showEntityState = true): self` @@ -124,20 +126,43 @@ Use it to show or hide the EntityState label and command (useful when reusing an ### `showCreateButton(bool $showCreateButton = true): self` -Use it to show or hide the create button in this particular embedded Entity List (useful when reusing an Entity List). This will apply before looking at authorizations. +Use it to show or hide the "create" button in this particular Entity List (useful when reusing an Entity List). This will apply before looking at authorizations. ### `showReorderButton(bool $showReorderButton = true): self` -Use it to show or hide the reorder button in this particular embedded Entity List (useful when reusing an Entity List). This will apply before looking at authorizations. +Use it to show or hide the reorder button in this particular Entity List (useful when reusing an Entity List). This will apply before looking at authorizations. ### `showSearchField(bool $showSearchField = true): self` -Use it to show or hide the search field in this particular embedded Entity List (useful when reusing an Entity List). +Use it to show or hide the search field in this particular Entity List (useful when reusing an Entity List). ### `showCount(bool $showCount = true): self` -Use it to show or hide the count of items in the embedded Entity List. +Use it to show or hide the count of items in the Entity List. + +## Display in layout + +To display your entity list in your show page's layout, you have to use the `addEntityListSection()` method in your Show Page's `buildShowLayout()` method. + +```php +protected function buildShowLayout(ShowLayout $showLayout): void + { + $showLayout + ->addSection(function (ShowLayoutSection $section) { + $section + ->addColumn(7, function (ShowLayoutColumn $column) { + $column + ->withFields(categories: 5, author: 7) + // ... + }) + ->addColumn(5, function (ShowLayoutColumn $column) { + // ... + }); + }) + ->addEntityListSection(ProductEntity::class); + } +``` ## Transformer -There is no transformer, since Sharp will NOT look for an attribute in the instance sent. The data of the Entity List is brought by a distinct XHR call, the same Sharp will use for a non embedded Entity List. +There is no transformer, since Sharp will NOT look for an attribute in the instance sent. The data of the Entity List is brought by a distinct XHR call, the same Sharp will use for any Entity List. diff --git a/resources/js/Pages/Dashboard/Dashboard.vue b/resources/js/Pages/Dashboard/Dashboard.vue index 93a8b9caf..d2b919134 100644 --- a/resources/js/Pages/Dashboard/Dashboard.vue +++ b/resources/js/Pages/Dashboard/Dashboard.vue @@ -1,35 +1,17 @@ - -
-
- - - - -
- -
-
+
+
+
- +
diff --git a/resources/js/Pages/Show/Show.vue b/resources/js/Pages/Show/Show.vue index cd0030dcf..176ecc339 100644 --- a/resources/js/Pages/Show/Show.vue +++ b/resources/js/Pages/Show/Show.vue @@ -1,6 +1,6 @@ + + diff --git a/resources/js/show/Show.ts b/resources/js/show/Show.ts index 64ef8c05e..f2b21a20d 100644 --- a/resources/js/show/Show.ts +++ b/resources/js/show/Show.ts @@ -108,8 +108,8 @@ export class Show implements ShowData { return config('app.debug'); } - if(field.type === 'entityList') { - return true; + if(field.type === 'entityList' || field.type === 'dashboard') { + return field.authorizations.view; } if(field.emptyVisible) { diff --git a/resources/js/show/components/fields/Dashboard.vue b/resources/js/show/components/fields/Dashboard.vue new file mode 100644 index 000000000..d33bbb728 --- /dev/null +++ b/resources/js/show/components/fields/Dashboard.vue @@ -0,0 +1,155 @@ + + + diff --git a/resources/js/show/components/fields/entity-list/EntityList.vue b/resources/js/show/components/fields/EntityList.vue similarity index 100% rename from resources/js/show/components/fields/entity-list/EntityList.vue rename to resources/js/show/components/fields/EntityList.vue diff --git a/resources/js/types/generated.d.ts b/resources/js/types/generated.d.ts index fa6b53717..c161ea09c 100644 --- a/resources/js/types/generated.d.ts +++ b/resources/js/types/generated.d.ts @@ -90,6 +90,9 @@ export type DashboardData = { data: { [key: string]: any }; filterValues: FilterValuesData; pageAlert: PageAlertData | null; + query: { + [filterKey: string]: string; + }; }; export type DashboardLayoutData = { sections: Array; @@ -137,6 +140,9 @@ export type EmbedFormData = { fields: { [key: string]: FormFieldData }; layout: FormLayoutData | null; }; +export type EmbeddedFieldAuthorizationsData = { + view: boolean; +}; export type EntityListAuthorizationsData = { create: boolean; reorder: boolean; @@ -797,6 +803,18 @@ export type ShowCustomFieldData = { type: string; emptyVisible: boolean; }; +export type ShowDashboardFieldData = { + value?: null; + key: string; + type: "dashboard"; + emptyVisible: boolean; + dashboardKey: string; + hiddenCommands: Array; + endpointUrl: string; + authorizations: EmbeddedFieldAuthorizationsData; + label: string | null; + hiddenFilters: { [key: string]: any } | null; +}; export type ShowData = { title: string | { [locale: string]: string } | null; authorizations: InstanceAuthorizationsData; @@ -820,10 +838,12 @@ export type ShowEntityListFieldData = { showSearchField: boolean; showCount: boolean; endpointUrl: string; + authorizations: EmbeddedFieldAuthorizationsData; label: string | null; hiddenFilters: { [key: string]: any } | null; }; export type ShowFieldData = + | ShowDashboardFieldData | ShowEntityListFieldData | ShowFileFieldData | ShowListFieldData @@ -835,7 +855,8 @@ export type ShowFieldType = | "list" | "picture" | "text" - | "entityList"; + | "entityList" + | "dashboard"; export type ShowFileFieldData = { value?: { disk: string; diff --git a/resources/js/types/routes.d.ts b/resources/js/types/routes.d.ts index 4081fe3d2..9a405a17c 100644 --- a/resources/js/types/routes.d.ts +++ b/resources/js/types/routes.d.ts @@ -212,6 +212,16 @@ declare module 'ziggy-js' { "name": "instanceId" } ], + "code16.sharp.api.dashboard": [ + { + "name": "dashboardKey" + } + ], + "code16.sharp.api.dashboard.filters.store": [ + { + "name": "dashboardKey" + } + ], "code16.sharp.api.show.command.instance": [ { "name": "entityKey" diff --git a/src/Data/Dashboard/DashboardData.php b/src/Data/Dashboard/DashboardData.php index 444a7a622..4439de46d 100644 --- a/src/Data/Dashboard/DashboardData.php +++ b/src/Data/Dashboard/DashboardData.php @@ -6,6 +6,7 @@ use Code16\Sharp\Data\Data; use Code16\Sharp\Data\Filters\FilterValuesData; use Code16\Sharp\Data\PageAlertData; +use Spatie\TypeScriptTransformer\Attributes\LiteralTypeScriptType; /** * @internal @@ -21,6 +22,10 @@ public function __construct( public array $data, public FilterValuesData $filterValues, public ?PageAlertData $pageAlert = null, + #[LiteralTypeScriptType('{ + [filterKey: string]: string, + }')] + public ?array $query = null, ) {} public static function from(array $dashboard): self @@ -32,6 +37,7 @@ public static function from(array $dashboard): self data: $dashboard['data'], filterValues: FilterValuesData::from($dashboard['filterValues']), pageAlert: PageAlertData::optional($dashboard['pageAlert'] ?? null), + query: $dashboard['query'], ); } } diff --git a/src/Data/Show/Fields/ShowDashboardFieldData.php b/src/Data/Show/Fields/ShowDashboardFieldData.php new file mode 100644 index 000000000..b9c0580f6 --- /dev/null +++ b/src/Data/Show/Fields/ShowDashboardFieldData.php @@ -0,0 +1,40 @@ +value.'"')] + public ShowFieldType $type, + public bool $emptyVisible, + public string $dashboardKey, + /** @var string[] */ + public array $hiddenCommands, + public string $endpointUrl, + public ShowFieldAuthorizationsData $authorizations, + public ?string $label = null, + /** @var array */ + public ?array $hiddenFilters = null, + ) {} + + public static function from(array $field): self + { + $field['authorizations'] = ShowFieldAuthorizationsData::from($field['authorizations']); + + return new self(...$field); + } +} diff --git a/src/Data/Show/Fields/ShowEntityListFieldData.php b/src/Data/Show/Fields/ShowEntityListFieldData.php index 677abe976..44230ed3e 100644 --- a/src/Data/Show/Fields/ShowEntityListFieldData.php +++ b/src/Data/Show/Fields/ShowEntityListFieldData.php @@ -3,6 +3,7 @@ namespace Code16\Sharp\Data\Show\Fields; use Code16\Sharp\Data\Data; +use Code16\Sharp\Data\Show\ShowFieldAuthorizationsData; use Code16\Sharp\Enums\ShowFieldType; use Spatie\TypeScriptTransformer\Attributes\LiteralTypeScriptType; use Spatie\TypeScriptTransformer\Attributes\Optional; @@ -33,6 +34,7 @@ public function __construct( public bool $showSearchField, public bool $showCount, public string $endpointUrl, + public ShowFieldAuthorizationsData $authorizations, public ?string $label = null, /** @var array */ public ?array $hiddenFilters = null, @@ -40,6 +42,8 @@ public function __construct( public static function from(array $field): self { + $field['authorizations'] = ShowFieldAuthorizationsData::from($field['authorizations']); + return new self(...$field); } } diff --git a/src/Data/Show/Fields/ShowFieldData.php b/src/Data/Show/Fields/ShowFieldData.php index d98cff116..28b4f178a 100644 --- a/src/Data/Show/Fields/ShowFieldData.php +++ b/src/Data/Show/Fields/ShowFieldData.php @@ -7,7 +7,8 @@ use Spatie\TypeScriptTransformer\Attributes\TypeScriptType; #[TypeScriptType( - ShowEntityListFieldData::class + ShowDashboardFieldData::class + .'|'.ShowEntityListFieldData::class // .'|'.ShowCustomFieldData::class .'|'.ShowFileFieldData::class .'|'.ShowListFieldData::class @@ -26,6 +27,7 @@ public static function from(array $field): Data $field['type'] = ShowFieldType::tryFrom($field['type']) ?? $field['type']; return match ($field['type']) { + ShowFieldType::Dashboard => ShowDashboardFieldData::from($field), ShowFieldType::EntityList => ShowEntityListFieldData::from($field), ShowFieldType::File => ShowFileFieldData::from($field), ShowFieldType::List => ShowListFieldData::from($field), diff --git a/src/Data/Show/ShowFieldAuthorizationsData.php b/src/Data/Show/ShowFieldAuthorizationsData.php new file mode 100644 index 000000000..f23c7fef8 --- /dev/null +++ b/src/Data/Show/ShowFieldAuthorizationsData.php @@ -0,0 +1,20 @@ +authorizationManager->check('entity', $dashboardKey); + + $dashboard = $this->getDashboardInstance($dashboardKey); + $dashboard->buildDashboardConfig(); + + $dashboard->filterContainer() + ->putRetainedFilterValuesInSession( + collect(request()->input('filterValues', [])) + ->diffKeys(request()->input('hiddenFilters') ?? []) + ->toArray() + ); + + return redirect()->route('code16.sharp.api.dashboard', [ + 'dashboardKey' => $dashboardKey, + ...(request()->input('query') ?? []), + ...$dashboard->filterContainer()->getQueryParamsFromFilterValues([ + ...request()->input('filterValues', []), + ...(request()->input('hiddenFilters') ?? []), + ]), + ]); + } +} diff --git a/src/Http/Controllers/DashboardController.php b/src/Http/Controllers/DashboardController.php index 51c88b6ee..7c328dd87 100644 --- a/src/Http/Controllers/DashboardController.php +++ b/src/Http/Controllers/DashboardController.php @@ -24,8 +24,14 @@ public function show(string $dashboardKey) 'data' => $dashboardData, 'pageAlert' => $dashboard->pageAlert($dashboardData), 'filterValues' => $dashboard->filterContainer()->getCurrentFilterValuesForFront(request()->all()), + 'query' => count(request()->query()) ? request()->query() : null, ]; + if (request()->routeIs('code16.sharp.api.dashboard')) { + // EmbeddedDashboard case, need to return JSON + return response()->json(DashboardData::from($data)->toArray()); + } + return Inertia::render('Dashboard/Dashboard', [ 'dashboard' => DashboardData::from($data), 'breadcrumb' => BreadcrumbData::from([ diff --git a/src/Http/Controllers/PreloadsShowEntityLists.php b/src/Http/Controllers/PreloadsShowFields.php similarity index 78% rename from src/Http/Controllers/PreloadsShowEntityLists.php rename to src/Http/Controllers/PreloadsShowFields.php index f70131d92..dfd31a9b9 100644 --- a/src/Http/Controllers/PreloadsShowEntityLists.php +++ b/src/Http/Controllers/PreloadsShowFields.php @@ -2,25 +2,27 @@ namespace Code16\Sharp\Http\Controllers; +use Code16\Sharp\Data\Show\Fields\ShowDashboardFieldData; use Code16\Sharp\Data\Show\Fields\ShowEntityListFieldData; use Code16\Sharp\Data\Show\ShowData; use Code16\Sharp\Data\Show\ShowLayoutColumnData; use Code16\Sharp\Data\Show\ShowLayoutSectionData; use Code16\Sharp\Http\Middleware\AddLinkHeadersForPreloadedRequests; -trait PreloadsShowEntityLists +trait PreloadsShowFields { protected function addPreloadHeadersForShowEntityLists(ShowData $payload): void { collect($payload->fields)->each(function ($field) use ($payload) { - if ($field instanceof ShowEntityListFieldData) { + if ($field instanceof ShowEntityListFieldData || $field instanceof ShowDashboardFieldData) { $section = collect($payload->layout->sections) ->firstWhere(fn (ShowLayoutSectionData $section) => collect($section->columns) ->map(fn (ShowLayoutColumnData $column) => $column->fields) ->flatten(2) ->firstWhere('key', $field->key) ); - if ($section && ! $section->collapsable) { + + if ($section && ! $section->collapsable && $field->authorizations->view) { app(AddLinkHeadersForPreloadedRequests::class)->preload($field->endpointUrl); } } diff --git a/src/Http/Controllers/ShowController.php b/src/Http/Controllers/ShowController.php index 5b237e825..b10f81ea2 100644 --- a/src/Http/Controllers/ShowController.php +++ b/src/Http/Controllers/ShowController.php @@ -12,7 +12,7 @@ class ShowController extends SharpProtectedController { use HandlesSharpNotificationsInRequest; - use PreloadsShowEntityLists; + use PreloadsShowFields; public function show(string $parentUri, EntityKey $entityKey, string $instanceId) { diff --git a/src/Http/Controllers/SingleShowController.php b/src/Http/Controllers/SingleShowController.php index 35666f6f4..9484ffba6 100644 --- a/src/Http/Controllers/SingleShowController.php +++ b/src/Http/Controllers/SingleShowController.php @@ -11,7 +11,7 @@ class SingleShowController extends SharpProtectedController { use HandlesSharpNotificationsInRequest; - use PreloadsShowEntityLists; + use PreloadsShowFields; public function show(EntityKey $entityKey) { diff --git a/src/Show/Fields/SharpShowDashboardField.php b/src/Show/Fields/SharpShowDashboardField.php new file mode 100644 index 000000000..f384d0185 --- /dev/null +++ b/src/Show/Fields/SharpShowDashboardField.php @@ -0,0 +1,110 @@ +entityKeyFor($key); + } + + return tap( + new static($key, static::FIELD_TYPE), + fn ($instance) => $instance->dashboardKey = $dashboardKey ?: $key + ); + } + + public function hideFilterWithValue(string $filterFullClassNameOrKey, $value): self + { + if (class_exists($filterFullClassNameOrKey)) { + $key = tap( + app($filterFullClassNameOrKey), function (Filter $filter) { + $filter->buildFilterConfig(); + }) + ->getKey(); + } else { + $key = $filterFullClassNameOrKey; + } + + $this->hiddenFilters[$key] = $value; + + return $this; + } + + public function hideDashboardCommand(array|string $commands): self + { + foreach ((array) $commands as $command) { + if (class_exists($command)) { + $command = app($command)->getCommandKey(); + } + + $this->hiddenCommands[] = $command; + } + + return $this; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + /** + * Create the property array for the field, using parent::buildArray(). + */ + public function toArray(): array + { + return tap( + parent::buildArray([ + 'label' => $this->label, + 'dashboardKey' => $this->dashboardKey, + 'hiddenCommands' => $this->hiddenCommands, + 'hiddenFilters' => count($this->hiddenFilters) + ? collect($this->hiddenFilters) + ->map(fn ($value) => is_callable($value) + ? $value(sharp()->context()->instanceId()) + : $value + ) + ->all() + : null, + 'authorizations' => [ + 'view' => app(SharpAuthorizationManager::class)->isAllowed('entity', $this->dashboardKey), + ], + ]), + function (array &$options) { + $options['endpointUrl'] = route('code16.sharp.api.dashboard', [ + 'dashboardKey' => $this->dashboardKey, + 'current_page_url' => request()->url(), + ...app(SharpEntityManager::class) + ->entityFor($this->dashboardKey) + ->getViewOrFail() + ->filterContainer() + ->getQueryParamsFromFilterValues($options['hiddenFilters'] ?? []), + ]); + }); + } + + protected function validationRules(): array + { + return [ + 'dashboardKey' => 'required', + 'hiddenCommands' => 'array', + 'hiddenFilters' => 'array', + ]; + } +} diff --git a/src/Show/Fields/SharpShowEntityListField.php b/src/Show/Fields/SharpShowEntityListField.php index cc12f3d12..8b6fa64a1 100644 --- a/src/Show/Fields/SharpShowEntityListField.php +++ b/src/Show/Fields/SharpShowEntityListField.php @@ -2,6 +2,7 @@ namespace Code16\Sharp\Show\Fields; +use Code16\Sharp\Auth\SharpAuthorizationManager; use Code16\Sharp\Filters\Filter; use Code16\Sharp\Utils\Entities\SharpEntityManager; @@ -144,17 +145,15 @@ public function toArray(): array 'hiddenCommands' => $this->hiddenCommands, 'hiddenFilters' => count($this->hiddenFilters) ? collect($this->hiddenFilters) - ->map(function ($value) { - // Filter value can be a Closure - if (is_callable($value)) { - // Call it with current instanceId - return $value(sharp()->context()->instanceId()); - } - - return $value; - }) + ->map(fn ($value) => is_callable($value) + ? $value(sharp()->context()->instanceId()) + : $value + ) ->all() : null, + 'authorizations' => [ + 'view' => app(SharpAuthorizationManager::class)->isAllowed('entity', $this->entityListKey), + ], ]), function (array &$options) { $options['endpointUrl'] = route('code16.sharp.api.list', [ diff --git a/src/Show/Layout/ShowLayout.php b/src/Show/Layout/ShowLayout.php index bd244407a..8d0be6734 100644 --- a/src/Show/Layout/ShowLayout.php +++ b/src/Show/Layout/ShowLayout.php @@ -51,4 +51,13 @@ public function toArray(): array ->all(), ]; } + + final public function addDashboardSection(string $dashboardKey, ?bool $collapsable = null): self + { + $this->sections[] = (new ShowLayoutSection('')) + ->addColumn(12, fn ($column) => $column->withField($dashboardKey)) + ->when($collapsable !== null, fn ($section) => $section->setCollapsable($collapsable)); + + return $this; + } } diff --git a/src/routes/api.php b/src/routes/api.php index 16ddff1c4..d5546e5b3 100644 --- a/src/routes/api.php +++ b/src/routes/api.php @@ -1,5 +1,6 @@ name('code16.sharp.api.list.command.quick-creation-form.store'); - // EEL + // EmbeddedEntityLists Route::get('/list/{entityKey}', [EntityListController::class, 'show']) ->name('code16.sharp.api.list') ->middleware('cache.headers:no_store'); @@ -65,6 +67,14 @@ Route::get('/list/{entityKey}/command/{commandKey}/{instanceId}/form', [ApiEntityListInstanceCommandController::class, 'show']) ->name('code16.sharp.api.list.command.instance.form'); + // EmbeddedDashboards + Route::get('/dashboard/{dashboardKey}', [DashboardController::class, 'show']) + ->name('code16.sharp.api.dashboard') + ->middleware('cache.headers:no_store'); + + Route::post('/dashboard/{dashboardKey}/filters', [ApiDashboardFiltersController::class, 'store']) + ->name('code16.sharp.api.dashboard.filters.store'); + Route::post('/show/{entityKey}/command/{commandKey}/{instanceId?}', [ApiShowInstanceCommandController::class, 'update']) ->name('code16.sharp.api.show.command.instance'); diff --git a/tests/Http/Api/ApiDashboardControllerTest.php b/tests/Http/Api/ApiDashboardControllerTest.php new file mode 100644 index 000000000..788c0738d --- /dev/null +++ b/tests/Http/Api/ApiDashboardControllerTest.php @@ -0,0 +1,70 @@ +config()->declareEntity(DashboardEntity::class); + login(); +}); + +it('gets dashboard data as JSON in an EmbeddedDashboard case', function () { + fakeDashboardFor(DashboardEntity::class, new class() extends SharpDashboard + { + protected function buildWidgets(WidgetsContainer $widgetsContainer): void + { + $widgetsContainer + ->addWidget( + SharpPanelWidget::make('panel') + ->setTemplate('') + ) + ->addWidget( + SharpFigureWidget::make('figure') + ); + } + + protected function buildDashboardLayout(DashboardLayout $dashboardLayout): void + { + $dashboardLayout + ->addSection('section', function (DashboardLayoutSection $section) { + $section + ->addRow(function (DashboardLayoutRow $row) { + $row + ->addWidget(.3, 'panel') + ->addWidget(.7, 'figure'); + }); + }); + } + + protected function buildWidgetsData(): void + { + $this + ->setPanelData('panel', ['name' => 'Albert Einstein']) + ->setFigureData('figure', 200, '€', '+3%'); + } + }); + + $this->getJson('/sharp/api/dashboard/'.DashboardEntity::$entityKey, headers: [ + SharpBreadcrumb::CURRENT_PAGE_URL_HEADER => url('/sharp/s-list/person/s-show/person/1'), + ]) + ->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->has('data.panel.data', fn (AssertableJson $json) => $json + ->where('name', 'Albert Einstein') + ->etc() + ) + ->has('data.figure.data', fn (AssertableJson $json) => $json + ->where('figure', '200') + ->etc() + ) + ->etc() + ); +}); diff --git a/tests/Http/Auth/PolicyAuthorizationsTest.php b/tests/Http/Auth/PolicyAuthorizationsTest.php index a085bcd89..11b1b1952 100644 --- a/tests/Http/Auth/PolicyAuthorizationsTest.php +++ b/tests/Http/Auth/PolicyAuthorizationsTest.php @@ -1,11 +1,16 @@ config()->declareEntity(PersonPhysicistEntity::class); + + fakeShowFor(PersonEntity::$entityKey, new class() extends PersonShow + { + public function buildShowFields(FieldsContainer $showFields): void + { + $showFields->addField(SharpShowEntityListField::make(PersonPhysicistEntity::class)); + } + }); + + fakePolicyFor(PersonPhysicistEntity::$entityKey, new class() extends SharpEntityPolicy + { + public function entity($user): bool + { + return $user->name == 'admin'; + } + }); + + $this + ->actingAs(new User(['name' => 'bob'])) + ->get('/sharp/s-list/person/s-show/person/1') + ->assertInertia(fn (Assert $page) => $page + ->where('show.fields.'.PersonPhysicistEntity::class.'.authorizations', [ + 'view' => false, + ]) + ); + + $this + ->actingAs(new User(['name' => 'admin'])) + ->get('/sharp/s-list/person/s-show/person/1') + ->assertInertia(fn (Assert $page) => $page + ->where('show.fields.'.PersonPhysicistEntity::class.'.authorizations', [ + 'view' => true, + ]) + ); +}); + +it('returns policies with a SharpShowDashboardField on a show get request', function () { + sharp()->config()->declareEntity(DashboardEntity::class); + + fakeShowFor(PersonEntity::$entityKey, new class() extends PersonShow + { + public function buildShowFields(FieldsContainer $showFields): void + { + $showFields->addField(SharpShowDashboardField::make(DashboardEntity::class)); + } + }); + + fakePolicyFor(DashboardEntity::$entityKey, new class() extends SharpEntityPolicy + { + public function entity($user): bool + { + return $user->name == 'admin'; + } + }); + + $this + ->actingAs(new User(['name' => 'bob'])) + ->get('/sharp/s-list/person/s-show/person/1') + ->assertInertia(fn (Assert $page) => $page + ->where('show.fields.'.DashboardEntity::class.'.authorizations', [ + 'view' => false, + ]) + ); + + $this + ->actingAs(new User(['name' => 'admin'])) + ->get('/sharp/s-list/person/s-show/person/1') + ->assertInertia(fn (Assert $page) => $page + ->where('show.fields.'.DashboardEntity::class.'.authorizations', [ + 'view' => true, + ]) + ); +}); + it('overrides policies with global authorizations', function () { app(SharpEntityManager::class) ->entityFor('person') @@ -178,9 +259,9 @@ public function entity($user): bool }); it('allows to set dashboard view policy to handle whole dashboard visibility', function () { - sharp()->config()->addEntity('dashboard', DashboardEntity::class); + sharp()->config()->declareEntity(DashboardEntity::class); - fakePolicyFor('dashboard', new class() extends SharpEntityPolicy + fakePolicyFor(DashboardEntity::$entityKey, new class() extends SharpEntityPolicy { public function entity($user): bool { @@ -190,7 +271,7 @@ public function entity($user): bool login(new User(['name' => 'unauthorized-user'])); - $this->get('/sharp/s-dashboard/dashboard')->assertForbidden(); + $this->get('/sharp/s-dashboard/'.DashboardEntity::$entityKey)->assertForbidden(); }); it('does not check view, update and delete policies on create case', function () { diff --git a/tests/Http/DashboardControllerTest.php b/tests/Http/DashboardControllerTest.php index 17fe3f775..10311bacd 100644 --- a/tests/Http/DashboardControllerTest.php +++ b/tests/Http/DashboardControllerTest.php @@ -14,12 +14,12 @@ use Inertia\Testing\AssertableInertia as Assert; beforeEach(function () { - sharp()->config()->addEntity('stats', DashboardEntity::class); + sharp()->config()->declareEntity(DashboardEntity::class); login(); }); it('gets dashboard widgets, layout and data', function () { - fakeShowFor('stats', new class() extends SharpDashboard + fakeDashboardFor(DashboardEntity::class, new class() extends SharpDashboard { protected function buildWidgets(WidgetsContainer $widgetsContainer): void { @@ -56,7 +56,7 @@ protected function buildWidgetsData(): void $this->withoutExceptionHandling(); - $this->get('/sharp/s-dashboard/stats') + $this->get('/sharp/s-dashboard/'.DashboardEntity::$entityKey) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->has('dashboard', fn (Assert $dashboard) => $dashboard @@ -79,7 +79,7 @@ protected function buildWidgetsData(): void it('allows to configure a page alert', function () { $this->withoutExceptionHandling(); - fakeShowFor('stats', new class() extends TestDashboard + fakeDashboardFor(DashboardEntity::class, new class() extends TestDashboard { public function buildPageAlert(PageAlert $pageAlert): void { @@ -90,7 +90,7 @@ public function buildPageAlert(PageAlert $pageAlert): void } }); - $this->get('/sharp/s-dashboard/stats') + $this->get('/sharp/s-dashboard/'.DashboardEntity::$entityKey) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->where('dashboard.pageAlert', [ @@ -104,7 +104,7 @@ public function buildPageAlert(PageAlert $pageAlert): void }); it('allows to configure a page alert with a closure as content', function () { - fakeShowFor('stats', new class() extends TestDashboard + fakeDashboardFor(DashboardEntity::class, new class() extends TestDashboard { public function buildPageAlert(PageAlert $pageAlert): void { @@ -127,7 +127,7 @@ protected function buildWidgetsData(): void } }); - $this->get('/sharp/s-dashboard/stats') + $this->get('/sharp/s-dashboard/'.DashboardEntity::$entityKey) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->where('dashboard.pageAlert', [ diff --git a/tests/Pest.php b/tests/Pest.php index e5341e7a8..00e9ba055 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -115,6 +115,11 @@ class_exists($entityKeyOrClass) return test(); } +function fakeDashboardFor(string $entityKeyOrClass, $fakeImplementation) +{ + return fakeShowFor($entityKeyOrClass, $fakeImplementation); +} + function fakePolicyFor(string $entityKeyOrClass, $fakeImplementation) { app(SharpEntityManager::class) diff --git a/tests/Unit/Show/Fields/SharpShowDashboardFieldTest.php b/tests/Unit/Show/Fields/SharpShowDashboardFieldTest.php new file mode 100644 index 000000000..6cfbf5856 --- /dev/null +++ b/tests/Unit/Show/Fields/SharpShowDashboardFieldTest.php @@ -0,0 +1,94 @@ +config()->declareEntity(DashboardEntity::class); +}); + +it('allows to define a SharpShowDashboardField', function () { + $field = SharpShowDashboardField::make('dashboardField', DashboardEntity::$entityKey); + + expect($field->toArray()) + ->key->toBe('dashboardField') + ->type->toBe('dashboard') + ->dashboardKey->toBe(DashboardEntity::$entityKey) + ->hiddenCommands->toEqual([]) + ->endpointUrl->toStartWith(route('code16.sharp.api.dashboard', [DashboardEntity::$entityKey])) + ->authorizations->toEqual(['view' => true]); +}); + +it('allows to define a SharpShowDashboardField with default key', function () { + $field = SharpShowDashboardField::make(DashboardEntity::$entityKey); + + expect($field->toArray()) + ->key->toBe(DashboardEntity::$entityKey) + ->type->toBe('dashboard') + ->dashboardKey->toBe(DashboardEntity::$entityKey) + ->endpointUrl->toStartWith(route('code16.sharp.api.dashboard', [DashboardEntity::$entityKey])); +}); + +it('allows to define a SharpShowDashboardField with the entity className', function () { + $field = SharpShowDashboardField::make(DashboardEntity::class); + + expect($field->toArray()) + ->key->toBe(DashboardEntity::class) + ->type->toBe('dashboard') + ->dashboardKey->toBe(DashboardEntity::$entityKey) + ->endpointUrl->toStartWith(route('code16.sharp.api.dashboard', [DashboardEntity::$entityKey])); +}); + +it('handles hideFilterWithValue', function () { + $field = SharpShowDashboardField::make(DashboardEntity::$entityKey) + ->hideFilterWithValue('f1', 'value1'); + + expect($field->toArray()['hiddenFilters'])->toEqual([ + 'f1' => 'value1', + ]); +}); + +it('handles hideFilterWithValue with a callable', function () { + $field = SharpShowDashboardField::make(DashboardEntity::$entityKey) + ->hideFilterWithValue('f1', fn () => 'computed'); + + expect($field->toArray()['hiddenFilters'])->toEqual([ + 'f1' => 'computed', + ]); +}); + +it('handles hideDashboardCommand', function () { + $field = SharpShowDashboardField::make(DashboardEntity::$entityKey) + ->hideDashboardCommand(['c1', 'c2']); + + expect($field->toArray()['hiddenCommands'])->toEqual( + ['c1', 'c2'], + ); + + $field->hideDashboardCommand('c3'); + + expect($field->toArray()['hiddenCommands'])->toEqual( + ['c1', 'c2', 'c3'] + ); +}); + +it('handles authorizations', function () { + $field = SharpShowDashboardField::make(DashboardEntity::$entityKey); + + app()->bind(SharpAuthorizationManager::class, fn () => new class() extends SharpAuthorizationManager + { + public function __construct() {} + + public function isAllowed(string $ability, string $entityKey, ?string $instanceId = null): bool + { + if ($ability == 'entity' && $entityKey == DashboardEntity::$entityKey) { + return false; + } + + return true; + } + }); + + expect($field->toArray()['authorizations']['view'])->toBeFalse(); +}); diff --git a/tests/Unit/Show/Fields/SharpShowEntityListFieldTest.php b/tests/Unit/Show/Fields/SharpShowEntityListFieldTest.php index 605cc2f1e..102d3f3dc 100644 --- a/tests/Unit/Show/Fields/SharpShowEntityListFieldTest.php +++ b/tests/Unit/Show/Fields/SharpShowEntityListFieldTest.php @@ -1,19 +1,20 @@ config()->addEntity('entityKey', PersonEntity::class); + sharp()->config()->declareEntity(PersonEntity::class); }); it('allows to define a EEL field', function () { - $field = SharpShowEntityListField::make('entityListField', 'entityKey'); + $field = SharpShowEntityListField::make('entityListField', PersonEntity::$entityKey); expect($field->toArray()) ->key->toBe('entityListField') ->type->toBe('entityList') - ->entityListKey->toBe('entityKey') + ->entityListKey->toBe(PersonEntity::$entityKey) ->showEntityState->toBeTrue() ->showCreateButton->toBeTrue() ->showReorderButton->toBeTrue() @@ -21,16 +22,16 @@ ->emptyVisible->toBeFalse() ->showCount->toBeFalse() ->hiddenCommands->toEqual(['entity' => [], 'instance' => []]) - ->endpointUrl->toStartWith(route('code16.sharp.api.list', ['entityKey'])); + ->endpointUrl->toStartWith(route('code16.sharp.api.list', [PersonEntity::$entityKey])) + ->authorizations->toEqual(['view' => true]); }); it('allows to define EEL field with default key', function () { - sharp()->config()->addEntity('instances', PersonEntity::class); - $field = SharpShowEntityListField::make('instances'); + $field = SharpShowEntityListField::make(PersonEntity::$entityKey); - expect($field->toArray())->key->toBe('instances') + expect($field->toArray())->key->toBe(PersonEntity::$entityKey) ->type->toBe('entityList') - ->entityListKey->toBe('instances') + ->entityListKey->toBe(PersonEntity::$entityKey) ->showEntityState->toBeTrue() ->showCreateButton->toBeTrue() ->showReorderButton->toBeTrue() @@ -38,7 +39,7 @@ ->emptyVisible->toBeFalse() ->showCount->toBeFalse() ->hiddenCommands->toEqual(['entity' => [], 'instance' => []]) - ->endpointUrl->toStartWith(route('code16.sharp.api.list', ['instances'])); + ->endpointUrl->toStartWith(route('code16.sharp.api.list', [PersonEntity::$entityKey])); }); it('allows to define EEL field with the entity className', function () { @@ -46,12 +47,12 @@ expect($field->toArray())->key->toBe(PersonEntity::class) ->type->toBe('entityList') - ->entityListKey->toBe('entityKey') - ->endpointUrl->toStartWith(route('code16.sharp.api.list', ['entityKey'])); + ->entityListKey->toBe(PersonEntity::$entityKey) + ->endpointUrl->toStartWith(route('code16.sharp.api.list', [PersonEntity::$entityKey])); }); it('handles hideFilterWithValue', function () { - $field = SharpShowEntityListField::make('entityListField', 'entityKey') + $field = SharpShowEntityListField::make(PersonEntity::$entityKey) ->hideFilterWithValue('f1', 'value1'); expect($field->toArray()['hiddenFilters'])->toEqual([ @@ -60,7 +61,7 @@ }); it('handles hideFilterWithValue with a callable', function () { - $field = SharpShowEntityListField::make('entityListField', 'entityKey') + $field = SharpShowEntityListField::make(PersonEntity::$entityKey) ->hideFilterWithValue('f1', fn () => 'computed'); expect($field->toArray()['hiddenFilters'])->toEqual([ @@ -69,42 +70,42 @@ }); it('handles showEntityState', function () { - $field = SharpShowEntityListField::make('entityListField', 'entityKey') + $field = SharpShowEntityListField::make(PersonEntity::$entityKey) ->showEntityState(false); expect($field->toArray()['showEntityState'])->toBeFalse(); }); it('handles showReorderButton', function () { - $field = SharpShowEntityListField::make('entityListField', 'entityKey') + $field = SharpShowEntityListField::make(PersonEntity::$entityKey) ->showReorderButton(false); expect($field->toArray()['showReorderButton'])->toBeFalse(); }); it('handles showCreateButton', function () { - $field = SharpShowEntityListField::make('entityListField', 'entityKey') + $field = SharpShowEntityListField::make(PersonEntity::$entityKey) ->showCreateButton(false); expect($field->toArray()['showCreateButton'])->toBeFalse(); }); it('handles showSearchField', function () { - $field = SharpShowEntityListField::make('entityListField', 'entityKey') + $field = SharpShowEntityListField::make(PersonEntity::$entityKey) ->showSearchField(false); expect($field->toArray()['showSearchField'])->toBeFalse(); }); it('handles showCount', function () { - $field = SharpShowEntityListField::make('entityListField', 'entityKey') + $field = SharpShowEntityListField::make(PersonEntity::$entityKey) ->showCount(); expect($field->toArray()['showCount'])->toBeTrue(); }); it('handles hideEntityCommands', function () { - $field = SharpShowEntityListField::make('entityListField', 'entityKey') + $field = SharpShowEntityListField::make(PersonEntity::$entityKey) ->hideEntityCommand(['c1', 'c2']); expect($field->toArray()['hiddenCommands']['entity'])->toEqual( @@ -119,7 +120,7 @@ }); it('handles hideInstanceCommands', function () { - $field = SharpShowEntityListField::make('entityListField', 'entityKey') + $field = SharpShowEntityListField::make(PersonEntity::$entityKey) ->hideInstanceCommand(['c1', 'c2']); expect($field->toArray()['hiddenCommands']['instance'])->toEqual( @@ -132,3 +133,23 @@ ['c1', 'c2', 'c3'], ); }); + +it('handles authorizations', function () { + $field = SharpShowEntityListField::make(PersonEntity::$entityKey); + + app()->bind(SharpAuthorizationManager::class, fn () => new class() extends SharpAuthorizationManager + { + public function __construct() {} + + public function isAllowed(string $ability, string $entityKey, ?string $instanceId = null): bool + { + if ($ability == 'entity' && $entityKey == PersonEntity::$entityKey) { + return false; + } + + return true; + } + }); + + expect($field->toArray()['authorizations']['view'])->toBeFalse(); +});