feat: (wip) Toggle Views
- Auto Height on Cards
- Title with ellipses on length exceed
- Changed namepaces
- Moved product card rendering to JS
- Added Image and List View Toggling buttons
- Kept basic filters rendering just as before
diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py
index c186a05..9e675e5 100644
--- a/erpnext/e_commerce/product_query.py
+++ b/erpnext/e_commerce/product_query.py
@@ -22,7 +22,7 @@
def __init__(self):
self.settings = frappe.get_doc("E Commerce Settings")
self.page_length = self.settings.products_per_page or 20
- self.fields = ['wi.name', 'wi.item_name', 'wi.item_code', 'wi.website_image', 'wi.variant_of',
+ self.fields = ['wi.web_item_name', 'wi.name', 'wi.item_name', 'wi.item_code', 'wi.website_image', 'wi.variant_of',
'wi.has_variants', 'wi.item_group', 'wi.image', 'wi.web_long_description', 'wi.description',
'wi.route', 'wi.website_warehouse']
self.conditions = ""
diff --git a/erpnext/e_commerce/product_view.js b/erpnext/e_commerce/product_view.js
new file mode 100644
index 0000000..660db66
--- /dev/null
+++ b/erpnext/e_commerce/product_view.js
@@ -0,0 +1,124 @@
+erpnext.ProductView = class {
+ /* Options: View Type */
+ constructor(options) {
+ Object.assign(this, options);
+ this.render_view_toggler();
+ this.get_item_filter_data();
+ this.render_list_view();
+ this.render_grid_view();
+ }
+
+ render_view_toggler() {
+ ["btn-list-view", "btn-grid-view"].forEach(view => {
+ let icon = view === "btn-list-view" ? "list" : "image-view";
+ this.products_section.append(`
+ <div class="form-group mb-0" id="toggle-view">
+ <button id="${icon}" class="btn ${view} mr-2">
+ <span>
+ <svg class="icon icon-md">
+ <use href="#icon-${icon}"></use>
+ </svg>
+ </span>
+ </button>
+ </div>`);
+ });
+
+ $("#list").click(function() {
+ let $btn = $(this);
+ $btn.removeClass('btn-primary');
+ $btn.addClass('btn-primary');
+ $(".btn-grid-view").removeClass('btn-primary');
+ })
+
+ $("#image-view").click(function() {
+ let $btn = $(this);
+ $btn.removeClass('btn-primary');
+ $btn.addClass('btn-primary');
+ $(".btn-list-view").removeClass('btn-primary');
+ });
+
+ this.products_area = this.products_section.append(`
+ <br><br>
+ <div id="products-area" class="row products-list mt-4"></div>
+ `);
+ }
+
+ get_item_filter_data() {
+ // Get Items and Discount Filters to render
+ let me = this;
+ const filters = frappe.utils.get_query_params();
+ let {field_filters, attribute_filters} = filters;
+
+ field_filters = field_filters ? JSON.parse(field_filters) : {};
+ attribute_filters = attribute_filters ? JSON.parse(attribute_filters) : {};
+
+ frappe.call({
+ method: 'erpnext.www.all-products.index.get_product_filter_data',
+ args: {
+ field_filters: field_filters,
+ attribute_filters: attribute_filters,
+ item_group: me.item_group
+ },
+ callback: function(result) {
+ if (!result.exc) {
+ me.render_filters(result.message[1]);
+
+ // Append pre-rendered products
+ // TODO: get products as is and style via js
+ me.products = result.message;
+ $("#products-area").append(result.message[0]);
+
+ } else {
+ $("#products-area").append(`
+ <div class="d-flex justify-content-center p-3 text-muted">
+ ${__('No products found')}
+ </div>`);
+
+ }
+ }
+ });
+ }
+
+ render_filters(filter_data) {
+ this.get_discount_filter_html(filter_data.discount_filters);
+ }
+
+ get_discount_filter_html(filter_data) {
+ if (filter_data) {
+ $("#product-filters").append(`
+ <div id="discount-filters" class="mb-4 filter-block pb-5">
+ <div class="filter-label mb-3">${__("Discounts")}</div>
+ </div>
+ `);
+
+ let html = `<div class="filter-options">`;
+ filter_data.forEach(filter => {
+ html += `
+ <div class="checkbox">
+ <label data-value="${filter[0]}">
+ <input type="radio" class="product-filter discount-filter"
+ name="discount" id="${filter[0]}"
+ data-filter-name="discount" data-filter-value="${filter[0]}"
+ >
+ <span class="label-area" for="${filter[0]}">
+ ${filter[1]}
+ </span>
+ </label>
+ </div>
+ `;
+ });
+ html += `</div>`;
+
+ $("#discount-filters").append(html);
+ }
+ }
+
+ render_list_view() {
+ // loop over data and add list html to it
+ }
+
+ render_grid_view() {
+ // loop over data and add grid html to it
+ }
+
+}
\ No newline at end of file
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 6fa3fb9..aa8ef6d 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -66,5 +66,8 @@
"js/hierarchy-chart.min.js": [
"public/js/hierarchy_chart/hierarchy_chart_desktop.js",
"public/js/hierarchy_chart/hierarchy_chart_mobile.js"
+ ],
+ "js/e-commerce.min.js": [
+ "e_commerce/product_view.js"
]
}
diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js
index b57862b..331d04e 100644
--- a/erpnext/public/js/shopping_cart.js
+++ b/erpnext/public/js/shopping_cart.js
@@ -2,8 +2,8 @@
// License: GNU General Public License v3. See license.txt
// shopping cart
-frappe.provide("erpnext.shopping_cart");
-var shopping_cart = erpnext.shopping_cart;
+frappe.provide("e_commerce.shopping_cart");
+var shopping_cart = e_commerce.shopping_cart;
var getParams = function (url) {
var params = [];
@@ -214,7 +214,7 @@
this.animate_add_to_cart($btn);
const item_code = $btn.data('item-code');
- erpnext.shopping_cart.update_cart({
+ e_commerce.shopping_cart.update_cart({
item_code,
qty: 1
});
diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js
index 6bcb6b1..11dae35 100644
--- a/erpnext/public/js/wishlist.js
+++ b/erpnext/public/js/wishlist.js
@@ -1,8 +1,8 @@
-frappe.provide("erpnext.wishlist");
-var wishlist = erpnext.wishlist;
+frappe.provide("e_commerce.wishlist");
+var wishlist = e_commerce.wishlist;
-frappe.provide("erpnext.shopping_cart");
-var shopping_cart = erpnext.shopping_cart;
+frappe.provide("e_commerce.shopping_cart");
+var shopping_cart = e_commerce.shopping_cart;
$.extend(wishlist, {
set_wishlist_count: function() {
@@ -79,7 +79,7 @@
let me = this;
let success_action = function() {
- erpnext.wishlist.set_wishlist_count();
+ e_commerce.wishlist.set_wishlist_count();
};
if ($wish_icon.hasClass('wished')) {
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 04bf983..acd97ad 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -68,7 +68,6 @@
.item-card-group-section {
.card {
- height: 400px;
align-items: center;
justify-content: center;
@@ -779,3 +778,7 @@
padding: 6px;
font-size: 14px;
}
+
+#toggle-view {
+ float: right;
+}
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index d944509..1823680 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -65,37 +65,14 @@
self.delete_child_item_groups_key()
def get_context(self, context):
- context.show_search=True
+ context.show_search = True
context.page_length = cint(frappe.db.get_single_value('E Commerce Settings', 'products_per_page')) or 6
- context.e_commerce_settings = frappe.get_cached_doc('E Commerce Settings', 'E Commerce Settings')
context.search_link = '/product_search'
- if frappe.form_dict:
- search = frappe.form_dict.search
- field_filters = frappe.parse_json(frappe.form_dict.field_filters)
- attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters)
- start = frappe.parse_json(frappe.form_dict.start)
- else:
- search = None
- attribute_filters = None
- field_filters = {}
- start = 0
-
- if not field_filters:
- field_filters = {}
-
- # Ensure the query remains within current item group
- field_filters['item_group'] = self.name
-
- engine = ProductQuery()
- context.items, discounts = engine.query(attribute_filters, field_filters, search, start)
-
filter_engine = ProductFiltersBuilder(self.name)
context.field_filters = filter_engine.get_field_filters()
context.attribute_filters = filter_engine.get_attribute_filters()
- if discounts:
- context.discount_filters = filter_engine.get_discount_filters(discounts)
context.update({
"parents": get_parent_item_groups(self.parent_item_group),
@@ -124,6 +101,7 @@
context.no_breadcrumbs = False
context.title = self.website_title or self.name
+ context.name = self.name
return context
diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html
index 1da4d15..d42453d 100644
--- a/erpnext/templates/generators/item/item_add_to_cart.html
+++ b/erpnext/templates/generators/item/item_add_to_cart.html
@@ -143,7 +143,7 @@
const $btn = $(e.currentTarget);
$btn.prop('disabled', true);
const item_code = $btn.data('item-code');
- erpnext.shopping_cart.update_cart({
+ e_commerce.shopping_cart.update_cart({
item_code,
qty: 1,
callback(r) {
@@ -170,11 +170,11 @@
};
let success_action = function() {
$btn.prop('disabled', false);
- erpnext.wishlist.set_wishlist_count();
+ e_commerce.wishlist.set_wishlist_count();
$('.btn-add-to-wishlist, .btn-view-in-wishlist').toggleClass('hidden');
};
- erpnext.wishlist.add_remove_from_wishlist("add", args, success_action, failure_action);
+ e_commerce.wishlist.add_remove_from_wishlist("add", args, success_action, failure_action);
});
$('.page_content').on('click', '.offer-details', (e) => {
diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js
index 5cb5d15..d7b8d32 100644
--- a/erpnext/templates/generators/item/item_configure.js
+++ b/erpnext/templates/generators/item/item_configure.js
@@ -247,7 +247,7 @@
const additional_notes = Object.keys(this.range_values || {}).map(attribute => {
return `${attribute}: ${this.range_values[attribute]}`;
}).join('\n');
- erpnext.shopping_cart.update_cart({
+ e_commerce.shopping_cart.update_cart({
item_code,
additional_notes,
qty: 1
diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html
index a27b566..3ae9a89 100644
--- a/erpnext/templates/generators/item_group.html
+++ b/erpnext/templates/generators/item_group.html
@@ -16,7 +16,8 @@
{% endblock %}
{% block page_content %}
-<div class="item-group-content" itemscope itemtype="http://schema.org/Product" data-item-group="{{ name }}">
+<div class="item-group-content" itemscope itemtype="http://schema.org/Product"
+ data-item-group="{{ name }}">
<div class="item-group-slideshow">
{% if slideshow %}<!-- slideshow -->
{{ web_block(
@@ -33,7 +34,7 @@
{% endif %}
</div>
<div class="row">
- <div class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
+ <div id="product-listing" class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
{% if sub_categories %}
<div class="sub-category-container">
<div class="heading"> {{ _('Sub Categories') }} </div>
@@ -48,15 +49,9 @@
{% endfor %}
</div>
{% endif %}
- <div class="row products-list">
- {% if items %}
- {% for item in items %}
- {% include "erpnext/www/all-products/item_row.html" %}
- {% endfor %}
- {% else %}
- {% include "erpnext/www/all-products/not_found.html" %}
- {% endif %}
- </div>
+
+ <!-- Products Rendered in all-products/index.js-->
+
</div>
<div class="col-12 order-1 col-md-3 order-md-1">
<div class="collapse d-md-block mr-4 filters-section" id="product-filters">
@@ -70,10 +65,6 @@
<!-- attribute filters -->
{{ attribute_filter_section(attribute_filters) }}
- <!-- discount filters -->
- {% if discount_filters %}
- {{ discount_range_filters(discount_filters) }}
- {% endif %}
</div>
<script>
diff --git a/erpnext/templates/includes/cart.js b/erpnext/templates/includes/cart.js
index 4de8ff7..f2b026c 100644
--- a/erpnext/templates/includes/cart.js
+++ b/erpnext/templates/includes/cart.js
@@ -4,8 +4,8 @@
// js inside blog page
// shopping cart
-frappe.provide("erpnext.shopping_cart");
-var shopping_cart = erpnext.shopping_cart;
+frappe.provide("e_commerce.shopping_cart");
+var shopping_cart = e_commerce.shopping_cart;
$.extend(shopping_cart, {
show_error: function(title, text) {
diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html
index d371352..d6d6629 100644
--- a/erpnext/templates/includes/macros.html
+++ b/erpnext/templates/includes/macros.html
@@ -66,7 +66,8 @@
'align-items-start': align == 'Left',
}) -%}
{%- set col_size = 3 if is_full_width else 4 -%}
-{%- set title = item.item_name or item.item_code -%}
+{%- set title = item.web_item_name or item.item_name or item.item_code -%}
+{%- set title = title[:50] + "..." if title|len > 50 else title -%}
{%- set image = item.website_image or item.image -%}
{%- set description = item.website_description or item.description-%}
@@ -120,11 +121,13 @@
<div class="card-body {{ align_class }}" style="width:100%">
<div style="margin-top: 16px; display: flex;">
<a href="/{{ item.route or '#' }}">
- <div class="product-title">{{ title or '' }}</div>
+ <div class="product-title">
+ {{ title or '' }}
+ {% if item.in_stock %}
+ <span class="indicator {{ item.in_stock }} card-indicator"></span>
+ {% endif %}
+ </div>
</a>
- {% if item.in_stock %}
- <span class="indicator {{ item.in_stock }} card-indicator"></span>
- {% endif %}
{% if not item.has_variants and settings.enable_wishlist %}
<div class="like-action"
data-item-code="{{ item.item_code }}"
@@ -363,7 +366,7 @@
<div class="filter-options">
{% for attr_value in attribute.item_attribute_values %}
<div class="checkbox">
- <label data-value="{{ value }}">
+ <label data-value="{{ attr_value }}">
<input type="checkbox"
class="product-filter attribute-filter"
id="{{attr_value.name}}"
@@ -381,24 +384,3 @@
</div>
{% endfor %}
{%- endmacro -%}
-
-{%- macro discount_range_filters(filters)-%}
-<div class="mb-4 filter-block pb-5">
- <div class="filter-label mb-3">{{ _("Discounts") }}</div>
- <div class="filter-options">
- {% for entry in filters %}
- <div class="checkbox">
- <label data-value="{{ entry[0] }}">
- <input type="radio" class="product-filter discount-filter"
- name="discount" id="{{ entry[0] }}"
- data-filter-name="discount" data-filter-value="{{ entry[0] }}"
- >
- <span class="label-area" for="{{ entry[0] }}">
- {{ entry[1] }}
- </span>
- </label>
- </div>
- {% endfor %}
- </div>
-</div>
-{%- endmacro -%}
diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html
index 1e9b482..ad40796 100644
--- a/erpnext/www/all-products/index.html
+++ b/erpnext/www/all-products/index.html
@@ -7,7 +7,8 @@
{% endblock header %}
{% block page_content %}
-<div class="row" style="display: none;">
+<!-- Old Search -->
+<!-- <div class="row" style="display: none;">
<div class="col-8">
<div class="input-group input-group-sm mb-3">
<input type="search" class="form-control" placeholder="{{_('Search')}}"
@@ -29,22 +30,16 @@
{{ _('Toggle Filters') }}
</button>
</div>
-</div>
+</div> -->
+<!-- Items section -->
<div class="row">
- <div class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
- <div class="row products-list">
- {% if items %}
- {% for item in items %}
- {% include "erpnext/www/all-products/item_row.html" %}
- {% endfor %}
- {% else %}
- {% include "erpnext/www/all-products/not_found.html" %}
- {% endif %}
- </div>
+ <div id="product-listing" class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
+ <!-- Rendered via JS -->
</div>
- <div class="col-12 order-1 col-md-3 order-md-1">
+ <!-- Filters Section -->
+ <div class="col-12 order-1 col-md-3 order-md-1">
{% if frappe.form_dict.start or frappe.form_dict.field_filters or frappe.form_dict.attribute_filters or frappe.form_dict.search %}
@@ -64,11 +59,6 @@
{% if attribute_filters %}
{{ attribute_filter_section(attribute_filters) }}
{% endif %}
-
- <!-- discount filters -->
- {% if discount_filters %}
- {{ discount_range_filters(discount_filters) }}
- {% endif %}
</div>
<script>
@@ -91,7 +81,10 @@
</script>
</div>
</div>
-<div class="row product-paging-area mt-5">
+
+<!-- TODO -->
+<!-- Paging Section -->
+<!-- <div class="row product-paging-area mt-5">
<div class="col-3">
</div>
<div class="col-9 text-right">
@@ -102,7 +95,7 @@
<button class="btn btn-default btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</button>
{% endif %}
</div>
-</div>
+</div> -->
<script>
frappe.ready(() => {
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js
index d2a3b19..94b4c6f 100644
--- a/erpnext/www/all-products/index.js
+++ b/erpnext/www/all-products/index.js
@@ -1,6 +1,17 @@
$(() => {
class ProductListing {
constructor() {
+ let is_item_group_page = $(".item-group-content").data("item-group");
+ let item_group = is_item_group_page || null;
+
+ // Render Products
+ frappe.require('assets/js/e-commerce.min.js', function() {
+ new erpnext.ProductView({
+ products_section: $('#product-listing'),
+ item_group: item_group
+ });
+ });
+
this.bind_filters();
this.bind_card_actions();
this.bind_search();
@@ -77,8 +88,8 @@
}
bind_card_actions() {
- erpnext.shopping_cart.bind_add_to_cart_action();
- erpnext.wishlist.bind_wishlist_action();
+ e_commerce.shopping_cart.bind_add_to_cart_action();
+ e_commerce.wishlist.bind_wishlist_action();
}
bind_search() {
diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py
index a4662bb..2472dad 100644
--- a/erpnext/www/all-products/index.py
+++ b/erpnext/www/all-products/index.py
@@ -6,33 +6,55 @@
sitemap = 1
def get_context(context):
+ # Add homepage as parent
+ context.parents = [{"name": frappe._("Home"), "route":"/"}]
+ filter_engine = ProductFiltersBuilder()
+ context.field_filters = filter_engine.get_field_filters()
+ context.attribute_filters = filter_engine.get_attribute_filters()
+
+ context.page_length = cint(frappe.db.get_single_value('E Commerce Settings', 'products_per_page'))or 20
+
+ context.no_cache = 1
+
+@frappe.whitelist(allow_guest=True)
+def get_product_filter_data():
+ """Get pre-rendered filtered products and discount filters on load."""
if frappe.form_dict:
search = frappe.form_dict.search
field_filters = frappe.parse_json(frappe.form_dict.field_filters)
attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters)
start = cint(frappe.parse_json(frappe.form_dict.start))
+ item_group = frappe.form_dict.item_group
else:
- search = field_filters = attribute_filters = None
+ search, attribute_filters, item_group = None, None, None
+ field_filters = {}
start = 0
+ if item_group:
+ field_filters['item_group'] = item_group
+
engine = ProductQuery()
- context.items, discounts = engine.query(attribute_filters, field_filters, search, start)
+ items, discounts = engine.query(attribute_filters, field_filters, search_term=search, start=start)
- # Add homepage as parent
- context.parents = [{"name": frappe._("Home"), "route":"/"}]
+ item_html = []
+ for item in items:
+ item_html.append(frappe.render_template('erpnext/www/all-products/item_row.html', {
+ 'item': item,
+ 'e_commerce_settings': engine.settings
+ }))
+ html = ''.join(item_html)
- filter_engine = ProductFiltersBuilder()
+ if not items:
+ html = frappe.render_template('erpnext/www/all-products/not_found.html', {})
- context.field_filters = filter_engine.get_field_filters()
- context.attribute_filters = filter_engine.get_attribute_filters()
+ # discount filter data
+ filters = {}
if discounts:
- context.discount_filters = filter_engine.get_discount_filters(discounts)
+ filter_engine = ProductFiltersBuilder()
+ filters["discount_filters"] = filter_engine.get_discount_filters(discounts)
- context.e_commerce_settings = engine.settings
- context.page_length = engine.settings.products_per_page or 20
-
- context.no_cache = 1
+ return html, filters
@frappe.whitelist(allow_guest=True)
def get_products_html_for_website(field_filters=None, attribute_filters=None):
@@ -47,7 +69,7 @@
for item in items:
item_html.append(frappe.render_template('erpnext/www/all-products/item_row.html', {
'item': item,
- 'e_commerce_settings': None
+ 'e_commerce_settings': engine.settings
}))
html = ''.join(item_html)