Product autocomplete input on module (Prestashop) - prestashop

I'm developing a prestashop module that has to make lists of existing products.
For the configuration panel of the module, using renderForm() and getContent(), I'm trying to replicate the "accesories" capability, where you start writing some info of a product on an input, and it shows the products that are a match. When selecting that product, it gets added on a list. Like this:
This a screenshot of Catalog / Products / Associations tab.
I'm trying with PS 1.6.0.14 and PS1.6.1.0RC3. How would I replicate this functionality to get lists of products on a module configuration panel?
I tried looking here Prestashop AdminProductsController.php but I don't really understand where half of that info is coming from.

There is an autocomplete plugin in prestashop you got to use that for this. Its in js->jquery->plugins you got to add this plugin into your module to make it work.

I think that to achieve that functionality, the renderForm() function won't be enough since you have to bind some javascript and some custom html.
The process of writing a fully functional module is a bit long but by taking the accessories functionality as a starting point it wont be so hard and you will always have a reference on "how-to-do-it".
I would go with this:
1) first create your
getContent()
function to be able to show the custom template and the product associated by your module so we will have something along:
public function getContent(){
//post process part to save the associations
if(Tools::isSubmit('saveMyAssociations'){
... //we will see it later
}
$my_associations = MyModule::getAssociationsLight($this->context->language->id,Tools::getValue('id_product')); //function that will retrieve the array of all the product associated on my module table.
$this->context->smarty->assign(array(
'my_associations' => $my_associations,
'product_id' => (int)Tools::getValue('id_product')
));
return $this->display(__FILE__, 'views/templates/admin/admintemplate.tpl'); //custome template to create the autocomplete
}
//our little function to get the already saved list, for each product we will retrieve id, name and reference with a join on the product/product_lang tables.
public static function getAssociationsLight($id_lang, $id_product, Context $context = null)
{
if (!$context)
$context = Context::getContext();
$sql = 'SELECT p.`id_product`, p.`reference`, pl.`name`
FROM `'._DB_PREFIX_.'my_associations`
LEFT JOIN `'._DB_PREFIX_.'product` p ON (p.`id_product`= `id_product_2`)
'.Shop::addSqlAssociation('product', 'p').'
LEFT JOIN `'._DB_PREFIX_.'product_lang` pl ON (
p.`id_product` = pl.`id_product`
AND pl.`id_lang` = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl').'
)
WHERE `id_product_1` = '.(int)$id_product;
return Db::getInstance()->executeS($sql);
}
2) create a template that will be able to show the automplete and the list.
Here we will loop trough the saved associations to create our autocomplete list, and we will do it with some hidden field to keep track of the ids/name and also a visible list were we will have a delete button for each row.
<input type="hidden" name="inputMyAssociations" id="inputMyAssociations" value="{foreach from=$my_associations item=accessory}{$accessory.id_product}-{/foreach}" />
<input type="hidden" name="nameMyAssociations" id="nameMyAssociations" value="{foreach from=$my_associations item=accessory}{$accessory.name|escape:'html':'UTF-8'}¤{/foreach}" />
<div id="ajax_choose_product_association">
<div class="input-group">
<input type="text" id="product_autocomplete_input_association" name="product_autocomplete_input_association" />
<span class="input-group-addon"><i class="icon-search"></i></span>
</div>
</div>
<div id="divMyAssociations">
{foreach from=$my_associations item=accessory}
<div class="form-control-static">
<button type="button" class="btn btn-default delAssociation" name="{$accessory.id_product}">
<i class="icon-remove text-danger"></i>
</button>
{$accessory.name|escape:'html':'UTF-8'}{if !empty($accessory.reference)}{$accessory.reference}{/if}
</div>
{/foreach}
</div>
<input type="submit" name="submitMyAssociations" id="submitMyAssociations" value="Send"/>
<input type="hidden" name="productId" id="productId" value="{$product_id|escape:'html'}"/>
3) Now we can add the javascript to bind an autocomplete on the main input and perform all the logic for each action
$(document).ready(function(){
//our function wrapper.
var initMyAssociationsAutocomplete = function (){
//initialize the autocomplete that will point to the default ajax_products_list page (it returns the products by id+name)
$('#product_autocomplete_input_association')
.autocomplete('ajax_products_list.php', {
minChars: 1,
autoFill: true,
max:20,
matchContains: true,
mustMatch:true,
scroll:false,
cacheLength:0,
formatItem: function(item) {
return item[1]+' - '+item[0];
}
}).result(addAssociation);
//as an option we will add a function to exclude a product if it's already in the list
$('#product_autocomplete_input_association').setOptions({
extraParams: {
excludeIds : getAssociationsIds()
}
});
};
//function to exclude a product if it exists in the list
var getAssociationsIds = function()
{
if ($('#inputMyAssociations').val() === undefined)
return '';
return $('#inputMyAssociations').val().replace(/\-/g,',');
}
//function to add a new association, adds it in the hidden input and also as a visible div, with a button to delete the association any time.
var addAssociation = function(event, data, formatted)
{
if (data == null)
return false;
var productId = data[1];
var productName = data[0];
var $divAccessories = $('#divCrossSellers');
var $inputAccessories = $('#inputMyAssociations');
var $nameAccessories = $('#nameMyAssociations');
/* delete product from select + add product line to the div, input_name, input_ids elements */
$divAccessories.html($divAccessories.html() + '<div class="form-control-static"><button type="button" class="delAssociation btn btn-default" name="' + productId + '"><i class="icon-remove text-danger"></i></button> '+ productName +'</div>');
$nameAccessories.val($nameAccessories.val() + productName + '¤');
$inputAccessories.val($inputAccessories.val() + productId + '-');
$('#product_autocomplete_input_association').val('');
$('#product_autocomplete_input_association').setOptions({
extraParams: {excludeIds : getAssociationsIds()}
});
};
//the function to delete an associations, delete it from both the hidden inputs and the visible div list.
var delAssociations = function(id)
{
var div = getE('divMyAssociations');
var input = getE('inputMyAssociations');
var name = getE('nameMyAssociations');
// Cut hidden fields in array
var inputCut = input.value.split('-');
var nameCut = name.value.split('¤');
if (inputCut.length != nameCut.length)
return alert('Bad size');
// Reset all hidden fields
input.value = '';
name.value = '';
div.innerHTML = '';
for (i in inputCut)
{
// If empty, error, next
if (!inputCut[i] || !nameCut[i])
continue ;
// Add to hidden fields no selected products OR add to select field selected product
if (inputCut[i] != id)
{
input.value += inputCut[i] + '-';
name.value += nameCut[i] + '¤';
div.innerHTML += '<div class="form-control-static"><button type="button" class="delAssociation btn btn-default" name="' + inputCut[i] +'"><i class="icon-remove text-danger"></i></button> ' + nameCut[i] + '</div>';
}
else
$('#selectAssociation').append('<option selected="selected" value="' + inputCut[i] + '-' + nameCut[i] + '">' + inputCut[i] + ' - ' + nameCut[i] + '</option>');
}
$('#product_autocomplete_input_association').setOptions({
extraParams: {excludeIds : getAssociationsIds()}
});
};
//finally initialize the function we have written above and create all the binds.
initMyAssociationsAutocomplete();
//live delegation of the deletion button to our delete function, this will allow us to delete also any element added after the dom creation with the ajax autocomplete.
$('#divMyAssociations').delegate('.delAssociation', 'click', function(){
delAssociations($(this).attr('name'));
});
});
4) now you just need to save the associations made by your module autocomplete, and i suggest to perform it by first deleting any association made on a given product and then saving all of them. so you don't have to care about inserting or updating an entry
public function getContent(){
//post process part
if(Tools::isSubmit('saveMyAssociations'){
$product_id = (int)Tools::getValue('productId');
// see the function below, a simple query to delete all the associations on a product
$this->deleteMyAssociations($product_id);
if ($associations = Tools::getValue('inputMyAssociations'))
{
$associations_id = array_unique(explode('-', $associations));
if (count($associations_id))
{
array_pop($associations_id);
//insert all the association we have made.
$this->changeMyAssociations($associations_id, $product_id);
}
}
}
}
protected function deleteMyAssociations($product_id){
return Db::getInstance()->execute('DELETE FROM `'._DB_PREFIX_.'my_associations` WHERE `id_product_1` = '.(int)$product_id);
}
protected function changeMyAssociations($associations_id, $product_id){
foreach ($associations_id as $id_product_2)
Db::getInstance()->insert('my_associations', array(
'id_product_1' => (int)$product_id,
'id_product_2' => (int)$id_product_2
));
}
I hope it can help you to go through all of this.

Related

Select Option (for dropdown) Laravel

I make a dropdown for a form, I will show the code below. However, when I click the submit button, there is an error saying,
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'brand' cannot be null (SQL: insert into supplier_details.
The data that I chose from the dropdown is actually null. Actually, I'm new to Laravel.
I don't want to make a dropdown list from a database, I just want to display the option and the option will be inserted into the database when the user clicks the submit button after filling in the form.
<div class="form-group row">
<label style="font-size: 16px;" for="id" class = "col-sm-2">Item Brand </label>
<label for="supp_name" class = "col-sm-1">:</label>
<div class="col-sm-7">
<select name="brand" class="form-control js-example-basic-single" required>
<option >Please select item brand</option>
<option value="machine1"> Item Brand 1 </option>
<option value="machine1"> Item Brand 2 </option>
<option value="machine1"> Tem Brand 3 </option>
</select>
</div>
</div>
Controller
public function createsupplierdetails()
{
return view ('frontend.praiBarcode.getweight');
}
public function supplierdetails(Request $r)
{
$details = new SupplierDetail;
$getuserPO = Supplier::where('PO',$r->PO)->first();
$details->brand = $getuserPO->brand;
$details->container_no = $getuserPO->container_no;
$details->date_received = $getuserPO->date_received;
$details->gross_weight = $getuserPO->gross_weight;
$details->tare_weight = $getuserPO->tare_weight;
$details->net_weight = $getuserPO->net_weight;
$details->save();
return view ('frontend.praiBarcode.viewsupplierdetails')
->with('details',$details);
}
This to check to verify if it is working:
Make sure you are submitting the correct form.
Try doing dd on your controller dd($request->all())
If data is reaching the controller and not inserted into the database, check on your model, if it is added to fillable or if there is only id in the guarded array. You can know about more here in https://laravel.com/docs/9.x/eloquent#mass-assignment
Error should be fixed, as soon as you fix it.
Controller
use Validator;
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'brand' => 'required',
]);
if ($validator->fails()) {
return redirect()->back()->with('error', $validator->errors()->first());
}
$details = new SupplierDetail();
$details->brand = $request->brand;
$details->container_no = $request->container_no;
$details->date_received = $request->date_received;
$details->gross_weight = $request->gross_weight;
$details->tare_weight = $request->tare_weight;
$details->net_weight = $request->net_weight;
$details->save();
if ($trending) {
return redirect(route('details.index'))->with('success', 'Field added successfully');
} else {
return redirect()->back()->with('error', 'Field has been not added successfully');
}
}

Dynamically Tally Child Elements by Classname in Vue

I have a page that allows a user to drag/drop images into pre-defined DIVs, then I tally up the total value of the images based on their class name. What I am trying to do is get vue to read the values from each outer div.answer and get the class names of the child images.
My source code is:
<div
is="box-answers"
v-for="box in boxes.slice().reverse()"
v-bind:key="box.id"
v-bind:level="box.level"
v-bind:hint="box.hint"
></div>
<script>
Vue.component('box-answers', {
props: ['level','hint'],
template: '<div class="droppable answer :id="level" :title="hint"></div>'
});
new Vue({
el: '#mainapp',
data: {
boxes: [
{ id: 1, level: 'baselevel-1', hint: 'x 1' },
{ id: 2, level: 'baselevel-2', hint: 'x 20' },
{ id: 3, level: 'baselevel-3', hint: 'x 400' },
{ id: 4, level: 'baselevel-4', hint: 'x 8,000' },
{ id: 5, level: 'baselevel-5', hint: 'x 160,000' }
]
}
</script>
This converts to the follow HTML (the nested DIVs and SPANs are user-possible entries by dragging):
<div id="baselevel-5" class="droppable answer" title="x 160,000">
<div><img src="images/line.gif" alt="Five" class="imgfive"></div>
<span><img src="images/dot.gif" alt="One" class="imgone"></span>
</div>
...
<div id="baselevel-1" class="droppable answer" title="x 1">
<span><img src="images/line.gif" alt="One" class="imgone"></span>
</div>
Currently, I have jQuery/JavaScript calculating the point values using the following:
$(function(j) {
var arAnswers = Array(1);
count = 0; //
j("div.answer").each(function( idx ) {
currentId = j(this).attr('id');
ones = 0;
fives = 0;
if ( j("#" + currentId).children().length > 0 ) {
ones = j("#" + currentId).children().find("img.imgone").length * 1;
fives = j("#" + currentId).children().find("img.imgfive").length * 5;
arAnswers[count] = ones + fives; //Tally box value
count++;
}
});
});
I would like Vue to perform similar iteration and addition to return total value of ones and fives found based on the image classname.
Currently, you are approaching this problem as a pure-play DOM operation. If that is what you need then you can simply use $refs:
<!-- NOTICE ref -->
<div ref="boxAnswers"
is="box-answers"
v-for="box in boxes.slice().reverse()"
v-bind:key="box.id"
v-bind:level="box.level"
v-bind:hint="box.hint">
</div>
Inside your high-level component, you will have a function like:
function calculate() {
// NOTICE $refs
const arAnswers = this.$refs.boxAnswers.map((x) => {
// $el is the DOM element
const once = x.$el.querySelectorAll('img.imgone').length * 1;
const fives = x.$el.querySelectorAll('img.imgfive').length * 5;
return once + fives
});
return arAnswers;
}
But this is not the correct Vue way of doing things. You have to think in terms of events and data model (MVVM - don't touch DOM. DOM is just a representation of your data model). Since, you have a drag-n-drop based application, you have to listen for drag, dragstart, dragend and other drag events. For example:
<!-- NOTICE drop event -->
<div #drop="onDropEnd(box, $event)"
is="box-answers"
v-for="box in boxes.slice().reverse()"
v-bind:key="box.id"
v-bind:level="box.level"
v-bind:hint="box.hint">
</div>
Your onDropEnd event handler will look like:
function onDrop(box, $event) {
// box - on which box drop is happening
// $event.data - which image is being dropped
// Verify $event.data is actually the image you are intending
if ($event.data === 'some-type-image') {
// Do the counting manipulations here
// ... remaining code
}
}
This is not a complete code as I don't know other components. But it should help you with the required direction.

prestashop 1.7 add a field to checkout process (and save it)

Good day, I've created a module that adds a new field (ask for invoice) during the checkout, I've added it in the payment selection hook.
How I save the field value (it is a checkbox) once the page is submitted? I mean after a payment is been selected and the submit button is pressed?
public function hookDisplayPaymentTop()
{
/*
echo"<pre>";
print_r($this->context->cart);
echo"</pre>";
*/
$sql = 'SELECT vat_number FROM ' . _DB_PREFIX_ . 'address WHERE `id_address` = '.$this->context->cart->id_address_invoice;
$vat_number = Db::getInstance()->getValue($sql);
if ($vat_number == false) {
$message = $this->l(' your VAT number is invalid or ');
$vat_status = 0;
}else{
$message = $vat_number;
$vat_status = 1;
}
$this->context->smarty->assign([
'foo' => 'bar',
'usrId' => $this->context->customer->id,
'vat' => $vat_number,
'vat_status' => $vat_status,
'cartId' => $this->context->cart->id
]);
return $this->display(__FILE__, '/views/templates/front/doyouinvoice.tpl');
}
and the TPL is like:
<h5>Do you need an invoice?</h5>
<div class="ggAskInvoiceError {if $vat_status == 1}hideThisMessage{/if}">
{l s="You need to add your VAT number in the billing address." m="ggaskinvoice"}
</div>
<div class="ggAskContainer">
<label><input type="checkbox" name="askInvoice" id="askInvoice" {if $vat_status == 0}disabled='disabled'{/if} data-cartid="{$cartId}" > {l s="I need an invoice for this order" m="ggaskinvoice"}</label>
</div>
Maybe try to use Tools::isSubmit
I'm not sure if this can work for you,
Try to create a controller in your module like this: in yourModule/controller/controllerName,
class yourModuleNameModuleFrontController extends ModuleFrontController
{
public function initContent(){
parent::initContent();
$this>setTemplate('module:yourModuleName//views/templates/front/doyouinvoice.tpl');
if(Tools::isSubmit('askInvoice')){ //askInvoice is your name button
//Your php code
}
}
}
And for the DisplayHook i think you only need to do this
return $this->display(__FILE__, '/views/templates/front/doyouinvoice.tpl');

Method not receiving attributes from shortcode call, general OOP problems

The method OpenMods that you see below, is supposed to take an array generated by an fgetcsv function, and put it into an HTML table. __construct is supposed to, as is typically the case, define the attributes for the class, and shortcode is supposed to take two attributes from the shortcode, and if mods comes back, it is supposed to call another function in the class.
OpenMods did function when it was outside of a class, without the class attribute calls, so I'm fairly certain that isn't the source of my problem. My problem most likely lies within __construct and shortcode; However please don't overlook OpenMods as it may contain errors that are contributing to the problem, I'm just giving my estimation which isn't worth much since I'm having to ask to for help.
This is an example of the shortcode I'm trying to make work:
[priceguide file=’test.csv’ type=’mods’]
class CsvImporter
{
private $parse_header;
private $header;
private $delimiter;
private $length;
//--------------------------------------------------------------------
function __construct($parse_header=false, $delimiter="\t", $length=8000)
{
add_shortcode( 'priceguide', array( $this, 'shortcode' ) );
$this->parse_header = $parse_header;
$this->delimiter = $delimiter;
$this->length = $length;
}
//--------------------------------------------------------------------
public function shortcode($atts) {
$attributes = extract( shortcode_atts( array(
'file' => '',
'type' => '',
), $atts ));
if ($attributes['mods'])
{
$this->OpenMods($attributes['file']);
}
}
//--------------------------------------------------------------------
function OpenMods($file) {
ob_start();
$fp = fopen(plugin_dir_path( __FILE__ ) . $file , "r" );
if ($this->parse_header)
{
$header = fgetcsv($fp, $this->length, $this->delimiter);
}
// table header and search html
echo('<input type="text" class="search" id="search" placeholder="Search">');
echo('<br>');
echo('<table id="table"> <tr class="hidden">
<th><b>
Name</b>
</th>
<th><b>
Cheese</b>
</th>
<th><b>
Price</b>
</th>
<th><b>Vote</b>
</th>
</tr>
<tbody>');
// integer for drop down/price submit
$a = 1;
// set values for table data
while ($header !== FALSE) {
$name = $header[0];
$quanid = $header[2];
$table = $header[3];
unset($header[2]);
unset($header[3]);
$cssId = 'row-'.$a;
$a++;
//generate HTML
echo('<tr>');
foreach ($header as $index=>$val) {
echo('<td>');
echo htmlentities($val, ENT_QUOTES);
echo('</td>');
}
// query to get item prices
$sql = "SELECT ItemID, Price
FROM {$table}
WHERE ItemID = %d
GROUP BY Price
ORDER BY COUNT(*) DESC LIMIT 1";
global $wpdb;
$results = $wpdb->get_var( $wpdb->prepare( $sql, $quanid));
// put the results in the table
echo('<td>');
print_r($results);
echo('</td>');
// HTML for hidden row/price submission
echo('<td>
<button class="toggler" data-prod-cat="' . $cssId . '">Vote</button>
</td>');
echo('</tr>');
echo('<tr class="cat' . $cssId . ' hidden" style="display:none">');
echo('<td colspan="4" style="white-space: nowrap">Enter ' . $name . ' Price:
<form action="" name="form' . $quanid . '" method="post"><input type="text" id="' . $quanid . '" maxlength="4" name="' . $quanid . '" value="price_input" class="input" />
<button id="submit" name="submit" class="submit" type="submit" value="Submit">Submit</button></form>
<?php
?>
</td>
</tr>');
wp_nonce_field('price_input');
}
echo("</table>");
fclose($fp);
return ob_get_clean();
}
}
Based on OP comment, the problem is, the object can not created and in this case, __constructor() will not run, and add_shortcode( 'priceguide', array( $this, 'shortcode' ) ); will never triggered.
There are two solutions. One, if you are make the shortcode method to static, and add this in your functions.php file:
add_shortcode( 'priceguide', 'CsvImporter::shortcode' ) );
The second option, if you do not want to make it static if you instantiate an object from your class, before anythings happens. In your functions.php
add_action('init', 'my_init');
global $CsvImporter;
function my_init() {
global $CsvImporter;
$CsvImporter = new CsvImporter();
}
In this case, when no output send to the buffer, you create a new CsvImporter object, so the __construct() will run, so shortcode will registered.

select a record from a list-box using text in Protractor

I want to select the record from the list box using text. how can i use the filter function to select the particular record. I will be having many options but i want to select the value which i want by checking the text (e.g Spanish). I dont want to select value by index becoz if i do that i wont be able to verify test, moreover list gets updated. kindly help. below r my html code.
<ul class="addList">
<li ng-repeat="skill in availableSkills" ng-click="addSkillFunc(skill, $index)" class="ng-binding ng-scope">Mandarin</li>
<li ng-repeat="skill in availableSkills" ng-click="addSkillFunc(skill, $index)" class="ng-binding ng-scope">English</li>
<li ng-repeat="skill in availableSkills" ng-click="addSkillFunc(skill, $index)" class="ng-binding ng-scope">Spanish</li>
</ul>
Yea i can select the record by index. but i want something like selectbyvisibleText which is available in Selenium.
Finally got the solution. created a function SelectRowByCellValue and used to call it where ever i want by
SelectRowByCellValue(AGP.SkillList, Data.SkillSelect);
SkillList = element.all(by.css('Ul.addList li'));
SkillSelect = Value that u want to select. (Spanish)
this.SelectRowByCellValue = function (Elem, Texts) {
Elem.filter(function (element) {
return element.getText().then(function (text) {
if (text == Texts) {
element.click();
return false;
}
});
}).then(function (filteredElements) {
});
};