Sulu CMS - Visiblity condition from underlying value of resource single selection - php

I have three entities Provision, Gatekeeper and Data.
class Provision {
private Gatekeeper $keeper; // ManyToOne
private Collection $points; // ManyToMany
}
class Gatekeeper {
private int $id;
private string $name;
private bool $allowData;
}
class Data {
private int $value;
}
All entities have REST-CRUD logic implemented. In order to edit the Provision entity I've added configurations to the sulu_admin.yaml:
sulu_admin:
field_type_options:
selection:
data_selection:
default_type: 'list_overlay'
resource_key: 'data'
types:
list_overlay:
adapter: 'table'
list_key: 'data'
display_properties: ['value']
icon: 'su-plus'
label: 'app.data'
overlay_title: 'app.action.select.data'
single_selection:
single_gatekeeper_selection:
default_type: 'single_select'
resource_key: 'gatekeepers'
types:
single_select:
display_property: 'name'
id_property: 'id'
overlay_title: 'app.action.select.gatekeeper'
Now, in the form configuration file for Provision entities, I want to include a visibleCondition for the data list:
<form xmlns="http://schemas.sulu.io/template/template"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/form-1.0.xsd">
<key>provision_details</key>
<properties>
<property name="gatekeeper" type="single_gatekeeper_selection" mandatory="true">
<meta>
<title>app.gatekeeper</title>
</meta>
</property>
<property name="data" type="data_selection" visibleCondition="!!gatekeeper && gatekeeper.allowData">
<meta>
<title>app.data</title>
</meta>
</property>
</properties>
</form>
Is there any possibility to implement this behaviour? I've already tried adding a custom ConditionDataProvider, but the main problem is that the visibleCondition is evaluated at Field-level while the only place I could pull the required data from would be the ResourceListStore in the SingleSelect.

This is not possible you only have access to the raw form data. For a single selection the gatekeeper value contains only the id of the selected element. Not any data of the element behind this element.
All data you have access to it, you see in save (POST/PUT) request of the form.
The only thing which is possible would be using resource_store_properties_to_request to filter the result of your data_selection by the gatekeeper id, this requires that your data api need to keep the gatekeeper parameter in mind to filter by it:
<properties>
<property name="gatekeeper" type="single_gatekeeper_selection" mandatory="true">
<meta>
<title>app.gatekeeper</title>
</meta>
</property>
<property name="data" type="data_selection">
<meta>
<title>app.data</title>
</meta>
<params>
<param name="resource_store_properties_to_request" type="collection">
<param name="gatekeeper" value="gatekeeper"/>
</param>
</params>
</property>
</properties>

Related

sulu CMS admin ui not showing data

i'm trying display the list from the controller, all the routes are loading but the data is not displaying
#[Route(path: '/admin/api/products', methods: ['GET'], name: 'app.get_products')]
public function index(ManagerRegistry $doctrine): Response
{
$products = $doctrine->getRepository(Product::class)->findAll();
if (!$products) {
return $this->json('No product found');
}
$data = [
'_embedded' => [
'products' => $products
]
];
return $this->json($data);
}
enter image description here
<?xml version="1.0" ?>
<list xmlns="http://schemas.sulu.io/list-builder/list">
<key>products</key>
<properties>
<property name="id" visibility="no" translation="sulu_admin.id">
<field-name>id</field-name>
<entity-name>App\Entity\Product</entity-name>
</property>
<property name="Product" visibility="always" searchability="yes" type="string">
<field-name>Product</field-name>
<entity-name>App\Entity\Product</entity-name>
</property>
<property name="item_number" visibility="always" type="string">
<field-name>ItemNumber</field-name>
<entity-name>App\Entity\Product</entity-name>
</property>
<property name="quantity" visibility="always" type="integer">
<field-name>quantity</field-name>
<entity-name>App\Entity\Product</entity-name>
</property>
</properties>
</list>
I tried to use DoctrineRepresentational factory but shows undefined.
Is there something that I'm missing?
You might want to check the Sulu Workshop repository and step through the different Pull Requests to follow along. That helped me when developing my first custom entities: https://github.com/sulu/sulu-workshop/pulls
For example, the Sulu people use a custom DoctrineListRepresentationFactory class which returns a PaginatedRepresentation via a DoctrineListBuilder. I would assume your code does not work because the JSON response does not match what Sulu Admin is expecting.

Sulu CMF - Pass parameter to autocomplete field in FormOverlayList add form

This question is a followup.
I have two entities DataSet and DataGroup.
class DataSet {
string $name;
Collection $groups; // Collection<int, DataGroup>
}
class DataGroup {
string $name;
DataSet $dataSet;
?DataGroup $nextGroup; // Condition: $nextGroup !== $this && $nextGroup->dataSet === $this->dataSet
}
The property DataGroup::nextGroup may refer to any other DataGroup entity associated with the same DataSet.
I want to create a CRUD form where I can add, edit and remove DataSet entities. In this DataSet form, I also want to include a tab where I can CRUD DataGroup entities associated with the current DataSet.
I have created list metadata data_sets.xml and data_groups.xml, as well as form metadata data_set.xml and data_group.xml.
<!-- lists/data_sets.xml -->
<list xmlns="http://schemas.sulu.io/list-builder/list">
<key>data_sets</key>
<properties>
<property name="name" visibility="always" searchability="yes">
<field-name>name</field-name>
<entity-name>App\Entity\DataSet</entity-name>
</property>
</properties>
</list>
<!-- lists/data_groups.xml -->
<list xmlns="http://schemas.sulu.io/list-builder/list">
<key>data_groups</key>
<properties>
<property name="name" visibility="always" searchability="yes">
<field-name>name</field-name>
<entity-name>App\Entity\DataSet</entity-name>
</property>
<property name="dataSet" visibility="always">
<field-name>name</field-name>
<entity-name>App\Entity\DataSet</entity-name>
<joins>
<join>
<entity-name>App\Entity\DataSet</entity-name>
<field-name>App\Entity\DataGroup.dataSet</field-name>
</join>
</joins>
</property>
</properties>
</list>
<!-- forms/data_set.xml -->
<form xmlns="http://schemas.sulu.io/template/template"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.sulu.io/template/template
http://schemas.sulu.io/template/form-1.0.xsd"
>
<key>data_set</key>
<properties>
<property name="name" type="text_line" mandatory="true">
<params>
<param name="headline" value="true"/>
</params>
</property>
</properties>
</form>
<!-- forms/data_group.xml -->
<form xmlns="http://schemas.sulu.io/template/template"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.sulu.io/template/template
http://schemas.sulu.io/template/form-1.0.xsd"
>
<key>data_group</key>
<properties>
<property name="name" type="text_line" mandatory="true">
<params>
<param name="headline" value="true"/>
</params>
</property>
<property name="nextGroup" type="single_data_group_selection">
<params>
<param name="type" value="auto_complete"/>
<param name="resource_store_properties_to_request" type="collection">
<param name="dataSetId" value="id"/>
</param>
</params>
</property>
</properties>
</form>
I have also configured sulu_admin.yaml for REST routes and the custom single selection:
sulu_admin:
resources:
data_sets:
routes:
list: app.get_data_sets
detail: app.get_data_set
data_groups:
routes:
list: app.get_data_groups
details: app.get_data_group
field_type_options:
single_selection:
single_data_group_selection:
default_type: 'auto_complete'
resource_key: 'data_groups'
types:
auto_complete:
display_property: 'name'
search_properties:
- 'name'
I implemented two REST controllers for both entities. DataSetController is built as of the Docs, while DataGroupController has a small extension for the list route:
class DataGroupController implements ClassResourceInterface
{
public function cgetAction(int $dataSetId, Request $request): Response
{
// ... Init field descriptors and execute listBuilder
$list = new ListRepresentation(
$listResponse,
'data_groups',
\array_merge(['dataSetId' => $dataSetId], $request->query->all()), // add DataSet ID
$listBuilder->getCurrentPage(),
$listBuilder->getLimit(),
$listBuilder->count()
);
// ... handle view
}
// ...
}
Finally, I implemented a custom Admin class like explained in the Docs.
class DataAdmin extends Admin
{
public const DATA_SET_DETAILS = 'data_set_details';
public const DATA_SET_GROUPS = 'data_set_groups';
public const DATA_SET_LIST = 'app.data_sets_list';
public const DATA_SET_ADD_FORM = 'app.data_set_add_form';
public const DATA_SET_ADD_FORM_DETAILS = 'app.data_set_add_form.details';
public const DATA_SET_EDIT_FORM = 'app.data_set_edit_form';
public const DATA_SET_EDIT_FORM_DETAILS = 'app.data_set_edit_form.details';
public const DATA_SET_EDIT_FORM_GROUPS = 'app.data_set_edit_form.groups';
public function configureViews(ViewCollection $viewCollection): void
{
// Add DataSet list view
// Add DataSet add form view
// Add DataSet edit form view (details)
/*
* Custom second DataSet edit form tab for DataGroup CRUDding
*/
$groupsFormOverlayList = $this->viewBuilderFactory
->createFormOverlayListViewBuilder(self::DATA_SET_EDIT_FORM_GROUPS, '/groups')
->setResourceKey(self::DATA_SET_GROUPS)
->setListKey(self::DATA_SET_GROUPS)
->addListAdapters(['table'])
->addRouterAttributesToListRequest(['id' => 'dataSetId'])
->setFormKey(self::DATA_SET_GROUPS)
->addRouterAttributesToFormRequest(['id' => 'dataSetId'])
->setTabTitle('app.data_groups')
->addToolbarActions([
new ToolbarAction('sulu_admin.add'),
new ToolbarAction('sulu_admin.delete')
])
;
$viewCollection->add($groupsFormOverlayList->setParent(self::DATA_SET_EDIT_FORM));
}
}
As of now
I can perform all CRUD operations on DataSet entities.
I can list and remove DataGroup entities belonging to a DataSet.
When trying to add a new DataGroup, I can't perform autocomplete search for nextGroup because the dataSetId parameter is not passed to the field, forbidding the ResourceRequester to perform the REST request.
How do I pass dataSetId to fields of the "New DataGroup" form in order to search for matching entities only?
Thanks a lot for the detailed description! Unfortunately, I am afraid that it is not possible to implement what you are trying to do while using the built-in autocomplete component at the moment.
The resource_store_properties_to_request param reads the values from the data of the form that renders the autocomplete component. If you are creating a new DataGroup entity, the data of the form is empty (because there is now existing data for a new entity) and therefore the resource_store_properties_to_request param is not able to read a dataSetId value.
I am sorry to say, but I think you would need to implement a custom autocomplete field-type that reads the dataSetId value from the current url to achieve the desired functionality. If you are interested in doing this, I would recommend to have a look at the basic field-type example in the sulu-demo repository.

Doctrine return null in place of EntityNotFoundException

I have broken FKs in my database and if I load an entity and ask for a related entity Doctrine will throw \Doctrine\ORM\EntityNotFoundException.
For the entity in question, I would prefer that where the FK is broken it would return NULL rather than throw an exception. This is because its within a Twig template that the exception occurs and I would prefer Twig to not have to have to handle the exception in this case.
The following is an example configuration.
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Foo\Click" table="clicks">
<id name="id" type="bigint" column="click_id">
<generator strategy="IDENTITY"/>
</id>
<!-- .. -->
<many-to-one field="visitor" target-entity="Foo\Visitor" fetch="LAZY">
<join-columns>
<join-column name="visitor_id" referenced-column-name="visitor_id"/>
</join-columns>
</many-to-one>
</entity>
<entity name="Foo\Visitor" table="visitors" read-only="true">
<id name="visitorId" type="integer" column="visitor_id">
<generator strategy="IDENTITY"/>
</id>
<!-- ... -->
<one-to-one field="firstClick" target-entity="Foo\Click" fetch="LAZY">
<join-columns>
<join-column name="click_id" referenced-column-name="click_id"/>
</join-columns>
</one-to-one>
</entity>
</doctrine-mapping>
The following is an example of expected results where the the click as a visitor ID, but the a visitor record does not exists with that ID. In this case, I would rather not have to wrap the logic in Try/Catch and instead have Click::getVisitor() return null;
<?php
$clickOne = $entityManager()->find(Foo\Click::class, 1);
$v = $clickOne->getVisitor();
if ($v !== null) {
echo $v->getId(); // may throw Doctrine\ORM\EntityNotFoundException
}
Is there a strategy for this with Doctrine?
Update: Added example configuration and code, and now I see the why this is not achievable with a simple Doctrine configuration.
EntityNotFoundException is thrown from Doctrine's proxy. So you can use EAGER loading method to get rid of proxies and get NULL instead of exception.
This is the strategy I have adopted based on the comment made by iainn.
<?php
class Parent
{
protected $child;
public function getChild()
{
if ($this->child instance of \Doctrine\ORM\Proxy\Proxy) {
try {
$this->child->__load();
} catch (\Doctrine\ORM\EntityNotFoundException $e) {
$this->child = null
}
}
return $this->child;
}
}
I am sure it is not recommended for your entities to interact directly with proxies. But I preferred this over calling a known method on the entity (which would also have the effect of loading it) because the intention of this code is clearer to the next developer who might read it.
I'm not sure if there are any side effects with interacting with the proxy like this.

Custom grid with modifications in backend of Magento 2

I’m newbie Magento2 developer.
Now I’m making one small module and I’m stuck in one place.
I built admin grid with founded example and here is my di.xml:
<preference for="Magento\Catalog\Model\Product" type="Vendor\Module\Model\Product" />
<virtualType name="Vendor\Module\Model\ResourceModel\Grid\Grid\Collection" type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult">
<arguments>
<argument name="mainTable" xsi:type="string">vendor_module</argument>
<argument name="resourceModel" xsi:type="string">Vendor\Module\Model\ResourceModel\Grid</argument>
</arguments>
</virtualType>
<type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
<arguments>
<argument name="collections" xsi:type="array">
<item name="grid_record_grid_list_data_source" xsi:type="string">Vendor\Module\Model\ResourceModel\Grid\Grid\Collection</item>
</argument>
</arguments>
</type>
And also I use layout XML file with hardcoded columns inside:
...
<column name="customer" >
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="filter" xsi:type="string">false</item>
<item name="label" xsi:type="string" translate="true">Customer</item>
</item>
</argument>
</column>
...
My table has columns like: product id, customer id, price, status
And my questions are:
How do I transform customer id to first+last name?
Column “status” has 3 different states (0, 1 and 2) - how do I convert them to human-readable words? (undefined, good, bad)
How to add to same grid another column for example $price + 10%?
Within the component XML you can define a UI class to assist in displaying custom/readable data within Magento 2. There are a number of examples within core, such as the thumbnail being displayed on the Catalog grid view.
Using that as an example, here's the column definition within catalog/view/adminhtml/ui_component/product_listing.xml:
<column name="thumbnail" class="Magento\Catalog\Ui\Component\Listing\Columns\Thumbnail">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="component" xsi:type="string">Magento_Ui/js/grid/columns/thumbnail</item>
<item name="add_field" xsi:type="boolean">true</item>
<item name="sortable" xsi:type="boolean">false</item>
<item name="altField" xsi:type="string">name</item>
<item name="has_preview" xsi:type="string">1</item>
<item name="label" xsi:type="string" translate="true">Thumbnail</item>
<item name="sortOrder" xsi:type="number">20</item>
</item>
</argument>
</column>
As you can see there are several arguments which can be passed to the column definition, including a component which depends on the type of data you're trying to display. In this case, it is a thumbnail. Reviewing that JS file reveals that it is logic to pull out the data being set in the below method to be displayed as the actual thumbnail. This is not necessarily a requirement.
Within the defined class on the column tag, you see Magento\Catalog\Ui\Component\Listing\Columns\Thumbnail. This is a class which defines helper methods for the way the data is to be displayed, as well as parsing the data to be displayed in such a way that the defined column component can properly render it.
Pay close attention to the method within that class, prepareDataSource:
/**
* Prepare Data Source
*
* #param array $dataSource
* #return array
*/
public function prepareDataSource(array $dataSource)
{
if (isset($dataSource['data']['items'])) {
$fieldName = $this->getData('name');
foreach ($dataSource['data']['items'] as & $item) {
$product = new \Magento\Framework\DataObject($item);
$imageHelper = $this->imageHelper->init($product, 'product_listing_thumbnail');
$item[$fieldName . '_src'] = $imageHelper->getUrl();
$item[$fieldName . '_alt'] = $this->getAlt($item) ?: $imageHelper->getLabel();
$item[$fieldName . '_link'] = $this->urlBuilder->getUrl(
'catalog/product/edit',
['id' => $product->getEntityId(), 'store' => $this->context->getRequestParam('store')]
);
$origImageHelper = $this->imageHelper->init($product, 'product_listing_thumbnail_preview');
$item[$fieldName . '_orig_src'] = $origImageHelper->getUrl();
}
}
return $dataSource;
}
You would use this method to format the data you're displaying into the format you need.
For instance, the way Price is displayed on the catalog grid (formatted to the proper currency) through their defined column class is:
public function prepareDataSource(array $dataSource)
{
if (isset($dataSource['data']['items'])) {
$store = $this->storeManager->getStore(
$this->context->getFilterParam('store_id', \Magento\Store\Model\Store::DEFAULT_STORE_ID)
);
$currency = $this->localeCurrency->getCurrency($store->getBaseCurrencyCode());
$fieldName = $this->getData('name');
foreach ($dataSource['data']['items'] as & $item) {
if (isset($item[$fieldName])) {
$item[$fieldName] = $currency->toCurrency(sprintf("%f", $item[$fieldName]));
}
}
}
return $dataSource;
}
I hope this helps make it clear how to format data in columns on grids.

Symfony, serialize object with arrays as XML preserving attributes and structure

I am using Symfony2 and trying to serialize different collections of objects into XML. For the sake of brevity, let's assume I am trying to list and unlist entities and this is the XML I want to get as a result:
<?xml version="1.0" encoding="UTF-8"?>
<r someattribute="value">
<data_list>
<item id="9" type="a"><![CDATA[list data 1]></item>
<item id="10" type="a"><![CDATA[list data 2]></item>
<item id="11" type="b"><![CDATA[list data 3]></item>
</data_list>
<data_unlist>
<uitem id="9" type="a" />
</data_unlist>
</r>
Here are my classes: Item for the "item" nodes, Uitem for the "uitem" nodes
and Model, to contain them all:
class Item
{
private $data=array();
public function getData() {return $this->data;}
public function __construct($id, $type, $value)
{
$this->data["#id"]=$id;
$this->data["#type"]=$type;
//How do I put $value as the node value????
}
}
class UItem
{
private $data=array();
public function getData() {return $this->data;}
public function __construct($id, $type)
{
$this->data["#id"]=$id;
$this->data["#type"]=$type;
}
}
class Model
{
private $data_list=array();
private $data_unlist=array();
public function getDataList() {return $this->data_list;}
public function getDataUnlist() {return $this->data_unlist;}
public function __construct()
{
$this->data_list[]=new Item(9, 'a', 'list data 1');
$this->data_list[]=new Item(10, 'a', 'list data 2');
$this->data_list[]=new Item(11, 'b', 'list data 3');
$this->data_unlist[]=new UItem(9, 'a');
}
}
Save for the problem I left commented in the Item class (how to put the node value there) I think that should serialize correctly so...
$model=new Model();
$encoders=array(new XmlEncoder());
$normalizers=array(new GetSetMethodNormalizer());
$serializer=new Serializer($normalizers, $encoders);
$contents_xml=$serializer->serialize($model, 'xml');
This is the result I am getting:
<response>
<data_list>
<item id="9" type="a" />
</data_list>
<data_list>
<item id="11" type="b" />
</data_list>
<data_unlist>
<uitem id="9" type="a" />
</data_unlist>
</response>
As you can see, two separate nodes for "data_list" have been created instead of grouping them into one single node.
Here are my questions:
Can I put the two "item" into a single "data_list"?. If so, how?.
How can I specify the value of a item node (instead of its attributes only) preserving the desired structure?.
How do I alter the root node name and add attributes to it?.
For the record, I am using the vanilla serializer, no JMS here.
Thanks in advance.
Using Symfony XmlEncoder I am able to build an array and turn it into xml using the encode method. Any array items which are named as #something are turned into attributes of the wrapping element rather than child elements, so:
$encoder = new \Symfony\Component\Serializer\Encoder\XmlEncoder();
$data = [
'something' => [
'#foo' => 'bar',
'yoo' => 'bar',
]
];
$xml = $encoder->encode($data, 'xml');
Gives me:
<?xml version="1.0"?>
<response>
<something foo="bar">
<yoo>bar</yoo>
</something>
</response>
JMSSerializer was created for these purposes. And I think you should start to use it in your project. It has no overhead and is easy to use.
You can control your serializing options with annotations:
http://jmsyst.com/libs/serializer/master/reference/annotations

Categories