diff --git a/demo/app/Sharp/Entities/PostBlockEntity.php b/demo/app/Sharp/Entities/PostBlockEntity.php index 2d5c3891a..65887937e 100644 --- a/demo/app/Sharp/Entities/PostBlockEntity.php +++ b/demo/app/Sharp/Entities/PostBlockEntity.php @@ -4,9 +4,6 @@ use App\Sharp\Posts\Blocks\PostBlockList; use App\Sharp\Posts\Blocks\PostBlockPolicy; -use App\Sharp\Posts\Blocks\PostBlockTextForm; -use App\Sharp\Posts\Blocks\PostBlockVideoForm; -use App\Sharp\Posts\Blocks\PostBlockVisualsForm; use Code16\Sharp\Utils\Entities\SharpEntity; class PostBlockEntity extends SharpEntity @@ -14,13 +11,4 @@ class PostBlockEntity extends SharpEntity protected ?string $list = PostBlockList::class; protected ?string $policy = PostBlockPolicy::class; protected string $label = 'Block'; - - public function getMultiforms(): array - { - return [ - 'text' => [PostBlockTextForm::class, 'Text block'], - 'visuals' => [PostBlockVisualsForm::class, 'Visuals block'], - 'video' => [PostBlockVideoForm::class, 'Video block'], - ]; - } } diff --git a/demo/app/Sharp/Entities/PostBlockTextEntity.php b/demo/app/Sharp/Entities/PostBlockTextEntity.php new file mode 100644 index 000000000..2dbde053e --- /dev/null +++ b/demo/app/Sharp/Entities/PostBlockTextEntity.php @@ -0,0 +1,11 @@ +configureMultiformAttribute('type') + $this + ->configureEntityMap( + attribute: 'type', + entities: EntityListEntities::make() + ->addEntity('text', PostBlockTextEntity::class) + ->addEntity('video', PostBlockVideoEntity::class) + ->addEntity('visuals', PostBlockVisualsEntity::class), + ) ->configureReorderable(new SimpleEloquentReorderHandler(PostBlock::class)) ->configureQuickCreationForm(); } diff --git a/demo/app/Sharp/Posts/Blocks/PostBlockVisualsShow.php b/demo/app/Sharp/Posts/Blocks/PostBlockVisualsShow.php new file mode 100644 index 000000000..18e06f07c --- /dev/null +++ b/demo/app/Sharp/Posts/Blocks/PostBlockVisualsShow.php @@ -0,0 +1,62 @@ +addField( + SharpShowListField::make('files') + ->setLabel('Visuals') + ->addItemField( + SharpShowFileField::make('file') + ) + ->addItemField( + SharpShowTextField::make('legend') + ->setLabel('Legend') + ) + ); + } + + protected function buildShowLayout(ShowLayout $showLayout): void + { + $showLayout + ->addSection(function (ShowLayoutSection $section) { + $section + ->addColumn(12, function (ShowLayoutColumn $column) { + $column + ->withListField('files', function (ShowLayoutColumn $item) { + $item->withFields('file') + ->withField('legend'); + }); + }); + }); + } + + public function find(mixed $id): array + { + $postBlock = PostBlock::findOrFail($id); + + return $this + ->setCustomTransformer('files', new SharpUploadModelFormAttributeTransformer()) + ->transform($postBlock); + } + + public function delete(mixed $id): void + { + PostBlock::findOrFail($id)->delete(); + } +} diff --git a/docs/.vitepress/sidebar.ts b/docs/.vitepress/sidebar.ts index b5e87562e..a8dae5976 100644 --- a/docs/.vitepress/sidebar.ts +++ b/docs/.vitepress/sidebar.ts @@ -1,4 +1,4 @@ -import type {DefaultTheme} from "vitepress"; +import type { DefaultTheme } from "vitepress"; export function sidebar(): DefaultTheme.SidebarItem[] { return [ @@ -26,7 +26,6 @@ export function sidebar(): DefaultTheme.SidebarItem[] { collapsed: true, items: [ { text: 'Create a Form', link: '/guide/building-form.md' }, - { text: 'Multi-Forms', link: '/guide/multiforms.md' }, { text: 'Using Single Form for unique resources', link: '/guide/single-form.md' }, { text: 'Quick creation form', link: '/guide/quick-creation-form.md' }, { text: 'Write an Embed for the Editor field', link: '/guide/form-editor-embeds.md' }, @@ -44,6 +43,7 @@ export function sidebar(): DefaultTheme.SidebarItem[] { { text: 'List', link: '/guide/form-fields/list.md' }, { text: 'AutocompleteList', link: '/guide/form-fields/autocomplete-list.md' }, { text: 'Geolocation', link: '/guide/form-fields/geolocation.md' }, + { text: 'Multi-Forms (deprecated)', link: '/guide/multiforms.md' }, // { text: 'Custom form field', link: '/guide/custom-form-fields.md' } ] }, diff --git a/docs/guide/building-entity-list.md b/docs/guide/building-entity-list.md index d54f6831c..bc606fe60 100644 --- a/docs/guide/building-entity-list.md +++ b/docs/guide/building-entity-list.md @@ -225,7 +225,9 @@ Here is the full list of available methods: - `configureDefaultSort(string $sortBy, string $sortDir = "asc")`: `EntityListQueryParams $queryParams` will be filled with this default value (see above) -- `configureMultiformAttribute(string $attribute)`: handle various types of entities; see [detailed doc](multiforms.md) +- `configureMultiformAttribute(string $attribute)`: :warning: This feature has been deprecated in version 9.6.0 and was replaced by the [Entity Map](entity-map.md) feature. You can still access to the [documentation](multiforms.md) for legacy usage. + +- `configureEntityMap(string $attribute, EntityListEntities $entities)`: configure an Entity Map to display multiple entities in a single Entity List; [see detailed section](#entity-map) above. - `configurePageAlert(string $template, string $alertLevel = null, string $fieldKey = null, bool $declareTemplateAsPath = false)`: display a dynamic message above the list; [see detailed doc](page-alerts.md) @@ -239,10 +241,65 @@ Here is the full list of available methods: - `configureCreateButtonLabel(string $label)` to set a custom "New..." button label. -## Configure the Entity List +## Declare the Entity List The Entity List must be declared in the correct entity class, as documented here: [Write an entity](entity-class.md)). After this we can access the Entity List at the following URL: **/sharp/s-list/products** (replace "products" by our entity key). To go ahead and learn how to add a link in the Sharp side menu, [look here](building-menu.md). + +## Entity Map + +::: info +This feature replaces the deprecated Multiforms functionality, which remains available for legacy use in version 9.x but will be removed in 10.x. +::: + +The Entity Map lets you display multiple entities within a single Entity List. This makes it possible to link different Show Pages or Forms based on a discriminating attribute. + +To set it up, declare the Entity Map in the `buildListConfig()` method by using `configureEntityMap()`: + +```php +class CarList extends SharpEntityList +{ + // ... + + public function buildListConfig(): void + { + $this + ->configureEntityMap( + attribute: 'engine', + entities: EntityListEntities::make() + ->addEntity('ice', InternalCombustionEngineCarEntity::class) + ->addEntity('ev', ElectricEngineCarEntity::class, 'lucide-plug', 'EV') + ); + } +} +``` + +The `attribute` parameter defines which attribute will be used to distinguish between entities. If needed, you can compute this value using a [custom transformer](./how-to-transform-data.md). + +The `entities` parameter expects an instance of `EntityListEntities`, which maps each discriminant value to a specific Entity. You can also specify the icon and label that will be displayed in the "Create" dropdown. + +Each mapped entity should be declared like any regular Entity, and can include a Show Page, a Form, a Policy, etc. + +::: tip +It’s not mandatory, but a good practice is to make your “sub-entities” extend the main Entity class to share policies or common logic. For example: +::: + +```php +class CarEntity extends SharpEntity +{ + protected ?string $list = CarList::class; + protected ?string $policy = CarPolicy::class; + protected string $label = 'Car'; +} +``` + +```php +class ElectricEngineCarEntity extends CarEntity +{ + protected ?string $form = ElectricEngineCarForm::class; + protected string $label = 'EV'; +} +``` diff --git a/docs/guide/entity-class.md b/docs/guide/entity-class.md index eddc90b38..ef5fa3f6a 100644 --- a/docs/guide/entity-class.md +++ b/docs/guide/entity-class.md @@ -84,11 +84,13 @@ class MyEntity extends SharpEntity ### Single shows and forms -When you need to configure a "unique" resource that does not fit into a List / Show schema, like for instance an account or a configuration item, you can use a Single Show or Form. This is a dedicated topic, [documented here](single-show.md). +When you need to configure a "unique" resource that does not fit into a List / Show schema, like an account or a configuration item, you can use a Single Show or Form. This is a dedicated topic, [documented here](single-show.md). ### Handle Multiforms -Multiforms allows to declare different forms for the same entity, to hanle variants. This is a dedicated topic, [documented here](multiforms.md). +::: info +This feature has been deprecated and was replaced in version 9.6.0 by the [Entity Map](./building-entity-list.md#entity-map) feature. +::: ## Declare the Entity in Sharp configuration diff --git a/docs/guide/multiforms.md b/docs/guide/multiforms.md index 739454fc0..12698f049 100644 --- a/docs/guide/multiforms.md +++ b/docs/guide/multiforms.md @@ -1,8 +1,12 @@ -# Multi-Forms +# Multi-Forms (deprecated) + +::: warning +Multi-Forms is a feature that has been deprecated in version 9.6.0 and was replaced by the [Entity Map](./building-entity-list.md#entity-map) feature. +::: Let's say you want to handle different variants for an entity in one Entity List. -For instance, maybe you want to display sold cars on an Entity List: easy enough, you create a `Car` entity, list and form. But you want the to handle different form fields for cars with an internal combustion engine and those with an electric engine; you can of course use a form and [conditional display](building-form.md#conditional-display) to achieve this, but in a case where there are many differences, the best option may be to split the Entity in two (or more) Forms. That's Multi-Form. +For instance, maybe you want to display sold cars on an Entity List: easy enough, you create a `Car` entity, list and form. But you want to handle different form fields for cars with an internal combustion engine and those with an electric engine; you can of course use a form and [conditional display](building-form.md#conditional-display) to achieve this, but in a case where there are many differences, the best option may be to split the Entity in two (or more) Forms. That's Multi-Form. ## Write the Form classes diff --git a/resources/js/Layouts/Layout.vue b/resources/js/Layouts/Layout.vue index 8b48edf9c..abb9c0dd4 100644 --- a/resources/js/Layouts/Layout.vue +++ b/resources/js/Layouts/Layout.vue @@ -219,8 +219,8 @@ - - @@ -233,7 +233,7 @@
- + {{ __('sharp::menu.logout_label') }}
diff --git a/resources/js/Pages/Show/Show.vue b/resources/js/Pages/Show/Show.vue index 88a06ee60..703498790 100644 --- a/resources/js/Pages/Show/Show.vue +++ b/resources/js/Pages/Show/Show.vue @@ -335,7 +335,7 @@ :collapsable="section.collapsable" :entity-key="entityKey" :instance-id="instanceId" - :is-right-col="columnIndex === section.columns.length - 1" + :is-right-col="columnIndex > 0 && columnIndex === section.columns.length - 1" :row="row" @reordering="onEntityListReordering(fieldLayout.key, $event)" /> diff --git a/resources/js/entity-list/EntityList.ts b/resources/js/entity-list/EntityList.ts index 14d9ecfbb..25f55f146 100644 --- a/resources/js/entity-list/EntityList.ts +++ b/resources/js/entity-list/EntityList.ts @@ -6,7 +6,6 @@ import { EntityStateValueData, FilterData, } from "@/types"; -import { getAppendableParentUri, route } from "@/utils/url"; import { EntityListInstance, InstanceId } from "./types"; export class EntityList implements EntityListData { @@ -14,7 +13,7 @@ export class EntityList implements EntityListData { config: EntityListData['config']; data: EntityListData['data']; fields: EntityListData['fields']; - forms: EntityListData['forms']; + entities: EntityListData['entities']; meta: EntityListData['meta']; pageAlert: EntityListData['pageAlert']; query: EntityListData['query']; @@ -110,31 +109,6 @@ export class EntityList implements EntityListData { return instance[this.config.instanceIdAttribute]; } - instanceUrl(instance: EntityListInstance): string | null { - const entityKey = this.entityKey; - const instanceId = this.instanceId(instance); - - if(!this.authorizations.view.includes(instanceId)) { - return null; - } - - const multiform = this.forms && Object.values(this.forms).find(form => form.instances.includes(instanceId)); - - if(this.config.hasShowPage) { - return route('code16.sharp.show.show', { - parentUri: getAppendableParentUri(), - entityKey: multiform ? `${entityKey}:${multiform.key}` : entityKey, - instanceId, - }); - } - - return route('code16.sharp.form.edit', { - parentUri: getAppendableParentUri(), - entityKey: multiform ? `${entityKey}:${multiform.key}` : entityKey, - instanceId, - }); - } - instanceState(instance: EntityListInstance): string | number | null { return this.config.state ? instance[this.config.state.attribute] @@ -153,10 +127,7 @@ export class EntityList implements EntityListData { } instanceCanDelete(instance: EntityListInstance): boolean { - if(Array.isArray(this.authorizations.delete)) { - return this.authorizations.delete?.includes(this.instanceId(instance)); - } - return !!this.authorizations.delete; + return instance._meta.authorizations.delete; } instanceCommands(instance: EntityListInstance): Array> | undefined { diff --git a/resources/js/entity-list/components/EntityList.vue b/resources/js/entity-list/components/EntityList.vue index 2c858ff8c..805f85d31 100644 --- a/resources/js/entity-list/components/EntityList.vue +++ b/resources/js/entity-list/components/EntityList.vue @@ -3,9 +3,9 @@ import { FilterManager } from "@/filters/FilterManager"; import { EntityList } from "../EntityList"; import { - CommandData, EntityListFieldData, EntityListMultiformData, EntityListQueryParamsData, + CommandData, EntityListFieldData, EntityListQueryParamsData, EntityStateValueData, - FilterData + FilterData, EntityListEntityData } from "@/types"; import WithCommands from "@/commands/components/WithCommands.vue"; import { CommandManager } from "@/commands/CommandManager"; @@ -68,6 +68,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { useElementVisibility } from "@vueuse/core"; import StateBadge from "@/components/ui/StateBadge.vue"; + import Icon from "@/components/ui/Icon.vue"; // import StateBadgeTest from "@/components/dev/StateBadgeTest.vue"; const props = withDefaults(defineProps<{ @@ -175,7 +176,7 @@ }); } - async function onCreate(event: MouseEvent, form?: EntityListMultiformData) { + async function onCreate(event: MouseEvent, listEntity?: EntityListEntityData) { if(event.metaKey || event.ctrlKey || event.shiftKey) { return; } @@ -186,19 +187,18 @@ if(props.entityList.config.quickCreationForm) { await props.commands.send({ hasForm: true } as CommandData, { postCommand: route('code16.sharp.api.list.command.quick-creation-form.store', { - entityKey: form ? `${entityKey}:${form.key}` : entityKey, + entityKey, + formEntityKey: listEntity ? listEntity.entityKey : entityKey, }), getForm: route('code16.sharp.api.list.command.quick-creation-form.create', { - entityKey: form ? `${entityKey}:${form.key}` : entityKey, + entityKey, + formEntityKey: listEntity ? listEntity.entityKey : entityKey, }), query: props.entityList.query, - entityKey: form ? `${entityKey}:${form.key}` : entityKey, + entityKey: listEntity ? listEntity.entityKey : entityKey, }); } else { - router.visit(route('code16.sharp.form.create', { - parentUri: getAppendableParentUri(), - entityKey: form ? `${entityKey}:${form.key}` : entityKey, - })); + router.visit(listEntity ? listEntity.formCreateUrl : props.entityList.config.formCreateUrl); } } @@ -469,7 +469,7 @@