How to display formatted HTML in GridField DataColumn in Silverstripe 4 - silverstripe

We have an object which has two fields - one is Text, the other is HTMLText:
private static $db = [
'Question' => 'Varchar(255)',
'Answer' => 'HTMLText'
];
We are referencing this object in a Gridfield using DataColumns:
$questionsGrid = GridField::create(
'Questions', 'Questions',
$this->Questions(),
GridFieldConfig_RelationEditor::create()
);
$dataColumns = $questionsGrid->
getConfig()->getComponentByType(GridFieldDataColumns::class);
$dataColumns->setDisplayFields([
'Question' => 'Question',
'Answer' => 'Answer'
]);
$dataColumns->setFieldCasting([
'Question' => 'Text',
'Answer' => 'HTMLText'
]);
Yet the Answer column is displayed as raw HTML - with visible tags & no formatting.
<p>The answer to life the universe & everything is 42.</p><p>A second paragraph for good measure.</p>
How do we display the Answer column as formatted HTML?

You can use 'HTMLFragment->RAW' for that column
$dataColumns->setFieldCasting([
'Question' => 'Text',
'Answer' => 'HTMLFragment->RAW'
]);

If you want to modify a method on a DataObject subclass being rendered as a row in a GridField to achieve the same thing, you simply cast it as HTMLText:
/**
* #return HTMLText
*/
public function ImageNice(): \HTMLText
{
$image = '<img src="/path/to/foo.png" />';
return \DBField::create_field(\HTMLText::class, $image);
}

Related

Silverstripe - Repeating / Reusable grouped fields

I would like to implement a layout in which a column is repeated several times and can be specified as dynamic content in the CMS. However, I cannot find an object type that would fit for this.
Do I have to specify the input fields individually for each column?
private static $db = [
'Intro_Headline' => 'Varchar',
'Intro_Subheadline' => 'Varchar',
'Intro_Text' => 'HTMLText',
'Intro_Headline2' => 'Varchar',
'Intro_Subheadline2' => 'Varchar',
'Intro_Text2' => 'HTMLText',
...
];
--
$fields = parent::getCMSFields();
//Intro Field 1
$fields->addFieldToTab('Root.Intro', TextField::create('Intro_Headline', 'Headline'), 'Content');
$fields->addFieldtoTab('Root.Intro', TextField::create('Intro_Subheadline', 'Subheadline'), 'Content');
$fields->addFieldToTab('Root.Intro', HTMLEditorField::create('Intro_Text', 'Text'), 'Content');
//Intro Field 2
$fields->addFieldToTab('Root.Intro', TextField::create('Intro_Headline2', 'Headline'), 'Content');
$fields->addFieldtoTab('Root.Intro', TextField::create('Intro_Subheadline2', 'Subheadline'), 'Content');
$fields->addFieldToTab('Root.Intro', HTMLEditorField::create('Intro_Text2', 'Text'), 'Content');
...
Or can someone tell me which field type I can't find?
Update:
Now I have a $has_many variable in addition to my $db variable in my page-model:
private static $has_many = [
'Intro_Columns' => IntroColumn::class,
];
In the getCMSFields() function I add them like this:
$fields->addFieldToTab('Root.Columns', GridField::create('Intro_Columns', 'Columns', IntroColumn::get()), 'Content');
And my data object looks like this:
class IntroColumn extends DataObject
{
private static $db = [
'img_url' => 'Text',
'headline' => 'Varchar',
'subheadline' => 'Varchar',
'text' => 'Text',
'link' => 'Text'
];
}
But the fields are not yet displayed in the CMS. How do I output the data fields from a data object?
For things to be repeatable, you will have to put them into a different object, and then link multiple of those objects to your current object/page.
GridFIeld
The default way of doing this in SilverStripe 4 is using the built in Database Relation ($has_many or $many_many instead of $db) and GridField` as the form field.
I'd recommend you go through this tutorial: https://docs.silverstripe.org/en/4/developer_guides/model/relations/
In paticular the section about $has_many will apply to your usecase. (Example where 1 Team has multiple Players or 1 Company multiple People)
$has_many/$many_many is a very generic option and can be used for any number of possible database relation (linking Categories, Images, Pages, ...)
elemental module
Another option is an officially supported module called elemental. This is very specifically built for repeatable content.
https://github.com/silverstripe/silverstripe-elemental
serialized-dataobject module
Probably not ideal for your usecase, but I maintain a module that provides an alternative to GridField, but it's more suitable for small form fields. The HTMLEditor is to large to be useful in this module.
https://github.com/Zauberfisch/silverstripe-serialized-dataobject
PS: regardless of what way you go, I highly recommend you go through the tutorial from (1.). It's a pretty important fundamental functionality of SilverStripe.
EDIT: response to your updated question:
If you are using GridField, I'd recommend the following:
class Page extends SiteTree {
private static $has_many = [
'Intro_Columns' => IntroColumn::class,
];
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Columns', new GridField(
'Intro_Columns',
'My Columns',
$this->Intro_Columns(),
new GridFieldConfig_RecordEditor()
), 'Content');
return $fields;
}
}
class IntroColumn extends DataObject {
private static $db = [
'headline' => 'Varchar',
'subheadline' => 'Varchar',
'text' => 'Text',
'link' => 'Text'
];
private static $has_one = [
'Image' => 'Image',
]
public function getCMSFields() {
$fields = new FieldList();
$fields->push(new TextField('headline', 'My Headline'));
$fields->push(new TextField('subheadline', 'My Subheadline'));
// ... and so on
$fields->push(new UploadField('Image', 'Upload an image'));
return $fields;
}
}
Note that I am using $this->Intro_Columns() as the Value for the GridField and not IntroColumn::get(). Because $this->Intro_Columns() is a automatically generated Method that returns all IntroColumn objects linked to the current page. But $this->Intro_Columns() would return all IntroColumns from all Pages
In the template, you cal also access this automatically generated method:
<!-- Page.ss -->
<h1>Page Title: $Title</h1>
<div class="intro-columns">
<% loop $Intro_Columns %>
<div class="intro-column">
<!-- here we are scoped into a single IntroColumn, so you can use all DB fields and methods of that object -->
<h2>$headline <br> $subheadline</h2>
$Image.URL <br>
...
</div>
<% end_loop %>
</div>

Sonata admin export fields with collection fields

I'm trying to make custom columns for export, but I can't access children. Is there any possibility to do that ?
My code at this moment looks like this:
public function getExportFields()
{
return [
'ID' => 'id',
'Transaction number' => 'transactionNumber',
'Loan account' => 'loan',
'Loan name' => 'loan.name',
'Amount' => 'amount',
//'Amount ($)' => '',
'Transaction type' => 'transactionCategory',
'Reference' => 'transactionAssociation.cashTransaction.transactionNumber',
'Date' => 'date'
];
}
I can't find out a solution. I was thinking to use PropertyAccess, but I don't know how to integrate it here.
I'm using Symfony 3.X with Sonata.
To get the collection records in export you cannot directly do this by specifying the property with association, A workaround for to achieve this you can define a new unmapped property in your entity with a getter function which will get all the collection details like in your main entity define new property as
protected $cashTransactionNumber;
public function getCashTransactionNumber()
{
$cashTransactionNumber = array();
$i = 1;
foreach ($this->getTransactionAssociation() as $key => $transactionAssociation) {
$cashTransactionNumber [] = $i .
') No.:' . $transactionAssociation->somemethod()->__toString()() .
/** Other properties */;
$i++;
}
return $this->cashTransactionNumber = join(' , ', $cashTransactionNumber );
}
then in your getExportFields() method call this property
public function getExportFields(){
return array(
'Reference'=>'cashTransactionNumber ',
....// Other properties
);
}
Reference: Exporting one to many relationship on sonata admin

Does silverstripe have recursive data relationships?

Does silverstripe have recursive data relationships? I tried to implement it and it gives no errors but the page is blank on modeladmin.
Example has_one recursive relation on Product to itself:
class Product extends DataObject {
private static $db = array(
'Name' => 'Varchar',
'ProductCode' => 'Varchar',
'Price' => 'Currency'
);
private static $has_one = array(
'Product' => 'Product'
);
}
Yes, this is possible.
There can be problems when doing this with Many_Many relationships, though.
My answer would be "no" for this done in this way. Where I've need this in the past I've created the "has one" as an "int" in the db array...
class Product extends DataObject {
private static $db = array(
'Name' => 'Varchar',
'MyProductID' => 'Int',
);
}
this means I've had to add casting for the summary field, custom scaffolding for the search fields and in the getCMSFields to replaceField the int field for a DropdownField to select a product.

Change formatted address order no longer working

Something appears to have changed in the new version of WooCommerce, this snippet to change the order of the items in a formatted address used to work fine....
add_filter( 'woocommerce_order_formatted_billing_address' , 'woo_custom_order_formatted_billing_address' );
/**
* woo_custom_order_formatted_billing_address
*
* #access public
* #since 1.0
* #return void
*/
function woo_custom_order_formatted_billing_address() {
$address = array(
'first_name' => $this->billing_first_name,
'last_name' => $this->billing_last_name,
'company' => $this->billing_company,
'address_1' => $this->billing_address_1,
'address_2' => $this->billing_address_2,
'city' => $this->billing_city,
'state' => $this->billing_state,
'postcode' => $this->billing_postcode,
'country' => $this->billing_country
);
return $address;
}
But now it returns the following error...
Fatal error: Using $this when not in object context in
Can anyone point me in the right direction of either what is going wrong or an alternative way of achieving it?
I'm seeing this example on the internet, but I don't know if it ever worked. First of because in 'woocommerce_order_formatted_billing_address' filter, that is applied in class-wc-order.php file, two arguments are provided, $address ARRAY, and a reference to current WC_Order OBJECT. But your definition of the filter function does not provide any arguments. Second, error that you receive describes the problem very accurately, pseudo-variable $this is available from within an object context, but your global function is not part of any object.
Enough with technicalities, the definition should look like this:
add_filter( 'woocommerce_order_formatted_billing_address' , 'woo_custom_order_formatted_billing_address', 10, 2 );
function woo_custom_order_formatted_billing_address( $address, $wc_order ) {
// make the changes to $address array here
// use for example, $wc_order->billing_first_name, instead of $this->billing_first_name
return $address;
}
#Eolis' answer doens't work for me, because Woocommerce applies WC()->countries->get_formatted_address( $address ) and this functions erases my new fields added to array on add_filter, so my solution is add field to previous field (first_name):
add_filter('woocommerce_order_formatted_billing_address', 'my_order_formatted_billing_address', 10, 2);
function my_order_formatted_billing_address($address, $wc_order) {
$billing_last_name_2 = get_post_meta( $wc_order->id, '_billing_last_name_2', true );
$address = array(
'postcode' => $wc_order->billing_postcode,
'last_name' => $wc_order->billing_last_name,
//\n force break line
'first_name' => $wc_order->billing_first_name . "\n" . $billing_last_name_2,
'address_1' => $wc_order->billing_address_1,
'address_2' => $wc_order->billing_address_2,
'city' => $wc_order->billing_city,
'state' => $wc_order->billing_state,
'country' => $wc_order->billing_country
);
return $address;
}

Symfony2 - Give a default filter in a list of elements of Sonata Admin

I have a list of elements of type Vehicle and I show these elements with Sonata Admin. I allow to filter these elements by the "status" field, but I want that, when the list is showed, only the active vehicles are showed, and if somebody wants to see the inactive vehicles, uses the filter and select the inactive status. I would like to know if somebody Knows the way to apply filters by default for a list of elements using Sonata Admin.
Here is my code:
public function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('name')
->add('status')
;
}
protected function configureDatagridFilters(DatagridMapper $mapper)
{
$mapper
->add('name')
->add('status')
;
}
Is there any option that can be added to the status field in configureDatagridFilters() to achieve this goal? Other options?
Thanks in advance.
You have to override $datagridValues property as following (for status > 0 if it's an integer) :
/**
* Default Datagrid values
*
* #var array
*/
protected $datagridValues = array (
'status' => array ('type' => 2, 'value' => 0), // type 2 : >
'_page' => 1, // Display the first page (default = 1)
'_sort_order' => 'DESC', // Descendant ordering (default = 'ASC')
'_sort_by' => 'id' // name of the ordered field (default = the model id field, if any)
// the '_sort_by' key can be of the form 'mySubModel.mySubSubModel.myField'.
);
source: Configure the default page and ordering in the list view
You can also use this method
public function getFilterParameters()
{
$this->datagridValues = array_merge(
array(
'status' => array (
'type' => 1,
'value' => 0
),
// exemple with date range
'updatedAt' => array(
'type' => 1,
'value' => array(
'start' => array(
'day' => date('j'),
'month' => date('m'),
'year' => date('Y')
),
'end' => array(
'day' => date('j'),
'month' => date('m'),
'year' => date('Y')
)
),
)
),
$this->datagridValues
);
return parent::getFilterParameters();
}
Using both above suggested approaches will break the filters "reset" behaviour since we are always forcing the filter to filter by a default value. To me, i think the best approach is to use the getFilterParameters function (since we can add logic in there instead of statically add the value) and check if the user clicked the "Reset button"
/**
* {#inheritdoc}
*/
public function getFilterParameters()
{
// build the values array
if ($this->hasRequest()) {
$reset = $this->request->query->get('filters') === 'reset';
if (!$reset) {
$this->datagridValues = array_merge(array(
'status' => array (
'type' => 1,
'value' => 0
),
),
$this->datagridValues
);
}
}
return parent::getFilterParameters();
}
Since sonata-admin 4.0, the function getFilterParameters() is tagged as final and the $datagridValues doesn't exist anymore.
So you need to override the configureDefaultFilterValues() function
protected function configureDefaultFilterValues(array &$filterValues): void
{
$filterValues['foo'] = [
'type' => ContainsOperatorType::TYPE_CONTAINS,
'value' => 'bar',
];
}
More details: https://symfony.com/bundles/SonataAdminBundle/current/reference/action_list.html#default-filters
Another approach is to use createQuery and getPersistentParameters to enforce invisible filter. This approach is best to have fully customizable filters. See my articles here:
http://www.theodo.fr/blog/2016/09/sonata-for-symfony-hide-your-filters/

Resources