Sorting New Grid Column in Sylius Error - sylius

I'm trying to get sorting to work on my new column in the Sylius admin 'Products' page, on the product grid. The column is mainTaxon, and I've gotten it to display correctly by adding a section to src/Sylius/Bundle/AdminBundle/Resources/config/grids/product.yml
under 'fields':
mainTaxon:
type: twig
label: "Main Taxon"
sortable: ~
options:
template: #SyliusAdmin/Product/Grid/Field/mainTaxon.html.twig
from the mainTaxon tempate I then render the main taxon if there is one, or else just a space character.
{% if data %}
<a href="/admin/taxons/{{ data.id }}/edit">
{{ data.name }}
</a>
{% else %}
{% endif %}
This all works great for displaying the column with the main taxons. Unfortunately, as soon as I click on sort I get the following error:
An exception has been thrown during the rendering of a template
("Notice: Undefined index: mainTaxon") in
SyliusAdminBundle:Grid:_default.html.twig at line 41. 500 Internal
Server Error - Twig_Error_Runtime
The line in the file that the error references is:
{% for row in data %}
This is the row loop that generates the entire grid for the product page. It seems to be having trouble before it ever gets to rendering the cell (td) that contains the mainTaxon, presumably because there is an array somewhere that I don't know about, which is missing the index for the extra category? Just a guess, insight appreciate!
EDIT:
I've done some further digging into this. Documentation on grid editing has been committed to Sylius since I asked this question: http://docs.sylius.org/en/latest/customization/grid.html, though unfortunately it still does not address adding new fields.
I've taken a look at the Product.orm.xml file that maps the product ORM, and see that the mainTaxon field is mapped as follows:
<many-to-one field="mainTaxon" target-entity="Sylius\Component\Taxonomy\Model\TaxonInterface">
<join-column name="main_taxon_id" referenced-column-name="id" nullable="true" />
</many-to-one>
I think the issue must be that when a sort is performed a new query must be run to pull the product data from the database again, resorted. Clearly this mapping on the main_taxon_id column is entering the sort query incorrectly, presumably because it is a foreign key. I'm uncertain if foreign keys are not supported by the sorting functionality yet and this should be planned for the future, or whether there's something further I need to do to get this working.

Related

Shopify/Liquid, add product variant of specific size to cart

im probably having this issue due to my lack of my experience with the liquid and the shopify platform in general.
Here's my issue:
In the current theme im working on, when a product is added to card it is always adding the smallest available size. Im not 100 % confident this is the code that is actually doing it but im fairly positive it is.
onclick=" Shopify.addItem('{{ product.variants.first.id}}', 1);
Currently when this code is fired the products smallest size (assuming its the first) is added to the cart.
I tried to take a look at the shopify API documentation for product.variants but it seems the theme or store is not using the latest version since I cant find any references to this first property.
What I would like if for there to be control in terms of product size when the add to cart action is triggered. Instead of adding the smallest size always it can persay add product.variants.{SIZE}.id instead (Not actual code just a guess)
If anyone could point me in the right direction of proper documentation for something like this or if you've had a similar question I would greatly appreciate it!
this is easy :)
yes this is the code sending the product to the cart and this theme don't support product variants. You should add the variants support.
some theory:
Variants is an array.
variants.first is a liquid function for easily take the first element of the array, check the liquid docs for more details.
The important part is, to add an item you need to send the variant id. always is the variant id.
now the magic:
you need to iterate on product.variants, keep on someplace the variant id and finally pass the value to the function.
onclick=" Shopify.addItem('{{ product.variants.first.id}}', 1);
by
onclick=" Shopify.addItem( {{ variant.id }}, 1);
you can use wherever you want, you can use select, or even got for a pure js / ajax solution using the cart API
extra detail the 1 is the qty, you can set it dinamic too.
I don't know your desing, so on psedo-code with multiple buttons you can do:
{% for variant in product.variants %}
<button onclick="Shopify.addItem('{{ variant.id }}', 1)">
{{ variant.title }}
</button>
{% endfor %}
Almost all themes, free or paid, have proper functioning code to handle changes in variations. When a customer selects a variant, via a change in a select element or perhaps the click of a radio button, the existing code should update the currently selected variant properly. Your example of an onclick set to the first variant is probably one the most primitive modes of operation, and as you see, less than satisfactory.
As a beginner, your best bet is to download the Debut theme from Shopify and study the basics of how variant selection work from the position of working code. Only then could you hope to be able to mod your own themes.

Show custom fields on the collection page in Shopify

I am using a plugin that filters products. This means that products are no longer shown in the traditional product loop. I think the plugin has switched to using javascript to show products on the collection page rather than liquid.
For every product on a collection page, I wish to show its colour. Each product has a colour associated with it using a custom field.
If I manually manually enter a product handle in the below code, the colour for the product handle that has been entered successfully displays for each product.
{{ all_products["MANUALLY ENTERED PRODUCT HANDLE"].metafields.custom_fields["colour"] }}
I am also able to dynamically able to get a products handle with {!productHandle!}
For some reason however when I put the two together like this:
{{ all_products["{!productHandle!}"].metafields.custom_fields["colour"] }}
The result is that nothing is shown.
My question is, how can I dynamically pull the product handle into the custom field? I have already tried
{{ all_products[product.handle].metafields.custom_fields["colour"] }}
and
{{ all_products[product-handle].metafields.custom_fields["colour"] }}
Try saving the handle as a string. Eg:
{% capture fizz %}{{product.handle}}{%endcapture%}
{{ all_products[fizz].metafields.custom_fields["colour"] }}
Note that all_products is also limited to just 20 products.

Shopify - Order Products By Custom Field

we have a simple Shopify store using the free "Meta Fields Editor" to add a simple custom field integer value to each of the products. Some products will have a meta value of 3, others 2 or 1. Many will have no meta value at all. We would like to be able to display the products in search results, by the meta field value entered for each product in descending order (weighted ordering). However, despite the ability to set meta values and display them in search results and collections pages, Shopify does not make it easy to Order or Sort products by a custom meta field. In the search results page I am able to call the custom meta value using
{{item.metafields.important.important}}
So far I have tried to order the products using this method
{% assign products = collection.products | sort:
'item.metafields.important.important' %}
{% for product in products %}
{{ product.title }}
{% endfor %}
But this results in null results.
Any insight anyone can give me in how to display products in order of a custom field would be much appreciated!
After attempting to find a Liquid solution I was finally able to solve this problem using custom metafields and jQuery. In the Shopify collection template, I added a data attribute to be included with each product like so:
<div id="product_list">
<div data-imp="{{product.metafields.important.important}}" class="product_block" /> PRODUCT #1 </div>
<div data-imp="{{product.metafields.important.important}}" class="product_block" /> PRODUCT #2 </div>
</div>
Then I used a simple jQuery function to sort the nodes by data attribute in descending order
var $wrapper = $('#product_list');
$wrapper.find('.product_block').sort(function (a, b) {
return +b.dataset.imp - +a.dataset.imp;
})
.appendTo( $wrapper );
}
Hope this helps somebody!
Nope. Doesn't work. Only fixed filters affect the sorting order. You can manually sort the products from the collection page in admin panel.

Shopify - Capturing a Metafield Value of the Last Added Collection with a Custom Template

I'm working on a theme where I've got several collections of clothing and I have created a custom collection template called 'seasonal', with the intent of using a custom background color for that specific type of collection.
I've achieved this by using metafields. It's working fine and I now have two seasonal collection pages - FW15 and SS16, with two different background colors,
I would now like to fetch the background color of the most recent 'seasonal' collection in another page of the theme.
This is where I'm stuck in. See the code below:
{% for collection in collections reversed %}
{% if collection.template_suffix contains 'seasonal' %}
{% assign seasonalCollectionColor = collection.metafields.c_f.Collection_Color | split: "|" %}
{{ seasonalCollectionColor[0] }}
{% endif %}
{% endear %}
This is outputting both seasonal collection colors:
#8DE5EB #FF7C1A
Instead of just the most recent one, which I'm trying to get by appending [0] to seasonalCollectionColor.
Any help on what am I doing wrong?
Thanks in advance!
You are in a loop. The loop has one condition you are triggering off of (contains seasonal). Hence you capture the result twice, and output it twice. You are not limiting your test to the condition of most recent.
If you were to try and test for the condition of most recent, what would that be? In your case, perhaps since you are traversing collections in a reverse order, the first occurrence is your most recent one.
So add a condition that if the condition is found, and you have set the seasonalCollectionColor to something, DO NOT set it again... just ignore any other values that match, and then you'll have only one value, the most recent one.

Shopify Product Replacement Parts

Looking for some advice on an interesting situation using Shopify. I'm building a site for a client that has Products that have free replacement parts available. Each replacement part has variant color options.
So far I have had the users at the company add all the replacement parts as products in the store. I have filtered the search results and the catalog results so the replacment parts are not show.
The only place they want the replacement parts to show is when a user visits a PRODUCT, they can click a button that says order replacement parts. Then a screen will show with all the replacement parts for that product.
A single replacement part may belong to several products and may have different color variants.
So what I have done thus far was had the client tag all parts with at least two tags. A tag called "part" that identifies the product as a part. And one or more tags like "link:SKU123" that links the part to one or more products. Then on my Product page I was using liquid to loop all parts and display the ones that matched the products SKU. Then I found out that the for loop has a 50 item limit...
So I looked at the product API, which would be ok, except that it has no way to filter by tags. Tags seem so handy and yet I don't see many ways to use them... So ultimately I'm looking for a way to display all replacement parts for a particular product. It doesn't have to involve tags, although the client has already tagged all the parts and I would hate to tell them it was a waste of time. But really any thoughts on how to accomplish this would be greatly appreciated.
The only way I can think to do it, is to return all of the replacement parts and then filter through them on the page. I see the API is limited to 250 products, but I suppose I could make multiple calls until I get them all. I'm not sure how many replacment parts there are total, but I suspect there could be upwards of 1000. Seems like a waste of network resources to have to pull them all down and then filter through them...
P.S. - the replacement parts are free, can they be run through the checkout with a zero dollar amount?
Ok so I have a couple different answers to this question. Not sure which one I will use yet.
Method 1: Filter Collection by Tag using URL
The first one was provided by Shawn Rudolph on the Shopify forums. It involves filtering a collection by tag using the URL. Shawn's post here explains it well: https://ecommerce.shopify.com/c/shopify-discussion/t/product-replacement-parts-270174
Method 2: Use paginate to get all products from collection using the AJAX API
This method is pretty cool. Yes it is more work than method one, but this maybe useful in a lot of scenarios. Out of the box Shopify does not allow you to retrieve all products from a given collection using the AJAX API. It can be done with the admin API but not the AJAX one to my knowledge. However, you can access all products from a collection with a for loop, but the for loop only allows up to 50 items to be looped at a time. That's where the paginate trick comes in. Basically I adapted the technique outlined by davecap here: http://www.davecap.com/post/9675189741/infinite-scroll-for-shopify-collections
So first you need your HTML/Liquid layout:
{% paginate collections.mycollectionname.products by 50 %}
{% for product in collections.mycollectionname.products %}
<div class="clone-node" id="product-{{ forloop.index | plus:paginate.current_offset }}">
{{ product.title }}
</div>
{% endfor %}
{% if paginate.next %}
<div class="clone-node next" title="{{ paginate.next.url }}"></div>
{% endif %}
<div id="insertion-point"></div>
{% endpaginate %}
So let's break it down a bit. First we are paginating are products by 50. This is the max amount a for loop will allow, so that's what we are going to use:
{% paginate collections.mycollectionname.products by 50 %}
Next we begin to loop our products. Every product is given a wrapper div with a class of "clone-node" this is very important. I also assign the div a unique ID, which isn't necessary for this to work, but may come in handy when trying to identify the product for later operations.
{% for product in collections.mycollectionname.products %}
<div class="clone-node" id="product-{{ forloop.index | plus:paginate.current_offset }}">
{{ product.title }}
</div>
{% endfor %}
We have to make sure to include the paginate.next URL. We also give this a "clone-node" class and we add a "next" class. I assign the paginate.next.url to the title attribute, but you could assign it to any number of attributes. You just need to be able to fetch it with jQuery.
{% if paginate.next %}
<div class="clone-node next" title="{{ paginate.next.url }}"></div>
{% endif %}
Then lastly we assign an insertion point. This is where we want our next set of 50 products to be inserted once we fetch them:
<div id="insertion-point"></div>
OK so now let's look at the JS code:
<script>
var prevUrl = ""; //this helps us know when we are done receiving products
function getParts() {
//get the last instance of the .next node. This will give us the next URL to query
var nextNode= $(".next").last(),
url = nextNode.attr("title"); //nab the URL
//send a get request to our next URL
$.ajax({
type: 'GET',
url: url,
dataType:'text',
success: function (data) {
//use a dummy div to convert the text to HTML, then find all of our clone-nodes, including our new "next" div which contains our next URL
var cloneNodes = $("<div>").html(data).find(".clone-node");
//insert our new clone-nodes on the page
cloneNodes.insertBefore("#insertion-point");
//if the URL's don't match let's grab the next 50!
if (prevUrl != url) {
prevUrl = url;
getParts();
}
}
});
}
//Call getParts for the first time to get the party started.
getParts();
</script>
What this will basically do, is get the URL for the next page of products from the title attribute of the div that's holding the paginate.next.url. Then using the jQuery ajax function we call that URL and it returns a page of HTML to use formatted just like our existing page, with the same "clone-node" classes we assigned, only it has the next 50 products embedded in it.
In davecap's example he used a dataType of HTML on his Ajax call, but that gave me some troubles. So instead, I used dataType text and used a dummy div created by jQuery to convert the text into HTML. Then jQuery grabs all of the divs with the "clone-node" class on them and inserts them on the page before our insertion-point. Remember the clone-nodes now hold the next 50 products so we just added the next 50 products to our page.
Lastly, we check if the previous URL is not equal to the current one. If it's not equal, that means it's a new URL and thus there must be more products to fetch, so we recursively call our getParts() function, which starts the process over and grabs the next 50. This continues until finally the URLs match, which means no more products to fetch, and the process stops.
There you have it! Of course if you have to fetch thousands and thousands of products this may be less then ideal because you are calling them 50 at a time. But for smaller numbers (maybe hundreds and hundreds...) it should work just fine.
I would create the parts as standalone products. Then each part will have variants (colours). Then you create metafields for this parts (product) which have a field with a list of all product ids which are 'mother' for this parts. This way you don't need to have strange tags and you keep concepts more separated, & everything cleaner. As per the zero amount, yes, you can go to checkout with zero amount (if your shipping settings allow you to do so).