chore: UI refresh for grid/list view and search

- enhanced UI for grid/list view, actions visible on hover only
- enhanced search UI
- Added indicator to show if item is in cart
- Moved search with view togglers
This commit is contained in:
marination
2021-07-12 03:28:33 +05:30
parent 9920747a26
commit cf88b517c8
14 changed files with 439 additions and 195 deletions

View File

@@ -61,7 +61,7 @@ def add_item_review(web_item, title, rating, comment=None):
doc.published_on = datetime.today().strftime("%d %B %Y")
doc.insert()
def get_customer():
def get_customer(silent=False):
user = frappe.session.user
contact_name = get_contact_name(user)
customer = None
@@ -75,5 +75,7 @@ def get_customer():
if customer:
return frappe.db.get_value("Customer", customer)
elif silent:
return None
else:
frappe.throw(_("You are not verified to write a review yet. Please contact us for verification."))

View File

@@ -310,7 +310,7 @@
{
"description": "Short Description for List View",
"fieldname": "short_description",
"fieldtype": "Data",
"fieldtype": "Small Text",
"label": "Short Website Description"
}
],
@@ -318,7 +318,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-07-08 19:25:15.115746",
"modified": "2021-07-11 20:49:45.415421",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Website Item",

View File

@@ -22,7 +22,7 @@ erpnext.ProductGrid = class {
this.items.forEach(item => {
let title = item.web_item_name || item.item_name || item.item_code || "";
title = title.length > 50 ? title.substr(0, 50) + "..." : title;
title = title.length > 90 ? title.substr(0, 90) + "..." : title;
html += `<div class="col-sm-4 item-card"><div class="card text-left">`;
html += me.get_image_html(item, title);
@@ -60,13 +60,20 @@ erpnext.ProductGrid = class {
get_card_body_html(item, title, settings) {
let body_html = `
<div class="card-body text-left" style="width:100%">
<div class="card-body text-left card-body-flex" style="width:100%">
<div style="margin-top: 16px; display: flex;">
`;
body_html += this.get_title_with_indicator(item, title, settings);
body_html += this.get_title(item, title);
// get floating elements
if (!item.has_variants) {
if (settings.enable_wishlist) {
body_html += this.get_wishlist_icon(item);
}
if (settings.enabled) {
body_html += this.get_cart_indicator(item);
}
if (!item.has_variants && settings.enable_wishlist) {
body_html += this.get_wishlist_icon(item);
}
body_html += `</div>`; // close div on line 50
@@ -76,29 +83,28 @@ erpnext.ProductGrid = class {
body_html += this.get_price_html(item);
}
body_html += this.get_stock_availability(item, settings);
body_html += this.get_primary_button(item, settings);
body_html += `</div>`; // close div on line 49
return body_html;
}
get_title_with_indicator(item, title, settings) {
get_title(item, title) {
let title_html = `
<a href="/${ item.route || '#' }">
<div class="product-title">
${ title || '' }
</div>
</a>
`;
if (item.in_stock && settings.show_stock_availability) {
title_html += `<span class="indicator ${ item.in_stock } card-indicator"></span>`;
}
title_html += `</div></a>`;
return title_html;
}
get_wishlist_icon(item) {
let icon_class = item.wished ? "wished" : "not-wished";
return `
<div class="like-action"
<div class="like-action ${ item.wished ? "like-action-wished" : ''}"
data-item-code="${ item.item_code }"
data-price="${ item.price || '' }"
data-formatted-price="${ item.formatted_price || '' }">
@@ -109,6 +115,14 @@ erpnext.ProductGrid = class {
`;
}
get_cart_indicator(item) {
return `
<div class="cart-indicator ${item.in_cart ? '' : 'hidden'}" data-item-code="${ item.item_code }">
1
</div>
`;
}
get_price_html(item) {
let price_html = `
<div class="product-price">
@@ -117,10 +131,10 @@ erpnext.ProductGrid = class {
if (item.formatted_mrp) {
price_html += `
<small class="ml-1 text-muted">
<s>${ item.formatted_mrp }</s>
<small class="striked-price">
<s>${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, "") : "" }</s>
</small>
<small class="ml-1" style="color: #F47A7A; font-weight: 500;">
<small class="ml-1 product-info-green">
${ item.discount } OFF
</small>
`;
@@ -129,6 +143,13 @@ erpnext.ProductGrid = class {
return price_html;
}
get_stock_availability(item, settings) {
if (!item.has_variants && !item.in_stock && settings.show_stock_availability) {
return `<span class="out-of-stock">Out of stock</span>`;
}
return ``;
}
get_primary_button(item, settings) {
if (item.has_variants) {
return `
@@ -138,13 +159,29 @@ erpnext.ProductGrid = class {
</div>
</a>
`;
} else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock !== "red")) {
} else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock)) {
return `
<div id="${ item.name }" class="btn
btn-sm btn-add-to-cart-list not-added w-100 mt-4"
btn-sm btn-primary btn-add-to-cart-list
w-100 mt-4 ${ item.in_cart ? 'hidden' : '' }"
data-item-code="${ item.item_code }">
<span class="mr-2">
<svg class="icon icon-md">
<use href="#icon-assets"></use>
</svg>
</span>
${ __('Add to Cart') }
</div>
<a href="/cart">
<div id="${ item.name }" class="btn
btn-sm btn-primary btn-add-to-cart-list
w-100 mt-4 go-to-cart-grid
${ item.in_cart ? '' : 'hidden' }"
data-item-code="${ item.item_code }">
${ __('Go to Cart') }
</div>
</a>
`;
} else {
return ``;

View File

@@ -24,8 +24,8 @@ erpnext.ProductList = class {
let title = item.web_item_name || item.item_name || item.item_code || "";
title = title.length > 200 ? title.substr(0, 200) + "..." : title;
html += `<div class='row mt-6 w-100' style="border-bottom: 1px solid var(--table-border-color); padding-bottom: 1rem;">`;
html += me.get_image_html(item, title);
html += `<div class='row list-row w-100'>`;
html += me.get_image_html(item, title, me.settings);
html += me.get_row_body_html(item, title, me.settings);
html += `</div>`;
});
@@ -34,20 +34,23 @@ erpnext.ProductList = class {
$product_wrapper.append(html);
}
get_image_html(item, title) {
get_image_html(item, title, settings) {
let image = item.website_image || item.image;
let wishlist_enabled = !item.has_variants && settings.enable_wishlist;
let image_html = ``;
if (image) {
return `
image_html += `
<div class="col-2 border text-center rounded list-image">
<a class="product-link product-list-link" href="/${ item.route || '#' }">
<img itemprop="image" class="website-image h-100 w-100" alt="${ title }"
src="${ image }">
</a>
${ wishlist_enabled ? this.get_wishlist_icon(item): '' }
</div>
`;
} else {
return `
image_html += `
<div class="col-2 border text-center rounded list-image">
<a class="product-link product-list-link" href="/${ item.route || '#' }"
style="text-decoration: none">
@@ -55,13 +58,16 @@ erpnext.ProductList = class {
${ frappe.get_abbr(title) }
</div>
</a>
${ wishlist_enabled ? this.get_wishlist_icon(item): '' }
</div>
`;
}
return image_html;
}
get_row_body_html(item, title, settings) {
let body_html = `<div class='col-9 text-left'>`;
let body_html = `<div class='col-10 text-left'>`;
body_html += this.get_title_html(item, title, settings);
body_html += this.get_item_details(item, settings);
body_html += `</div>`;
@@ -76,18 +82,11 @@ erpnext.ProductList = class {
style="color: var(--gray-800); font-weight: 500;">
${ title }
</a>
</div>
`;
if (item.in_stock && settings.show_stock_availability) {
title_html += `<span class="indicator ${ item.in_stock } card-indicator"></span>`;
}
title_html += `</div>`;
if (settings.enable_wishlist || settings.enabled) {
if (settings.enabled) {
title_html += `<div class="col-4" style="display:flex">`;
if (!item.has_variants && settings.enable_wishlist) {
title_html += this.get_wishlist_icon(item);
}
title_html += this.get_primary_button(item, settings);
title_html += `</div>`;
}
@@ -96,12 +95,12 @@ erpnext.ProductList = class {
return title_html;
}
get_item_details(item) {
get_item_details(item, settings) {
let details = `
<p class="product-code">
Item Code : ${ item.item_code }
${ item.item_group } | Item Code : ${ item.item_code }
</p>
<div class="text-muted mt-2">
<div class="mt-2" style="color: var(--gray-600) !important; font-size: 13px;">
${ item.short_description || '' }
</div>
<div class="product-price">
@@ -110,24 +109,33 @@ erpnext.ProductList = class {
if (item.formatted_mrp) {
details += `
<small class="ml-1 text-muted">
<s>${ item.formatted_mrp }</s>
<small class="striked-price">
<s>${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, "") : "" }</s>
</small>
<small class="ml-1" style="color: #F47A7A; font-weight: 500;">
<small class="ml-1 product-info-green">
${ item.discount } OFF
</small>
`;
}
details += this.get_stock_availability(item, settings);
details += `</div>`;
return details;
}
get_stock_availability(item, settings) {
if (!item.has_variants && !item.in_stock && settings.show_stock_availability) {
return `<br><span class="out-of-stock mt-2">Out of stock</span>`;
}
return ``;
}
get_wishlist_icon(item) {
let icon_class = item.wished ? "wished" : "not-wished";
return `
<div class="like-action mr-4"
<div class="like-action-list ${ item.wished ? "like-action-wished" : ''}"
data-item-code="${ item.item_code }"
data-price="${ item.price || '' }"
data-formatted-price="${ item.formatted_price || '' }">
@@ -142,19 +150,43 @@ erpnext.ProductList = class {
if (item.has_variants) {
return `
<a href="/${ item.route || '#' }">
<div class="btn btn-sm btn-explore-variants" style="margin-bottom: 0; margin-top: 4px; max-height: 30px;">
<div class="btn btn-sm btn-explore-variants btn"
style="margin-bottom: 0; max-height: 30px; float: right;
padding: 0.25rem 1rem; min-width: 135px;">
${ __('Explore') }
</div>
</a>
`;
} else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock !== "red")) {
} else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock)) {
return `
<div id="${ item.name }" class="btn
btn-sm btn-add-to-cart-list not-added"
btn-sm btn-primary btn-add-to-cart-list
${ item.in_cart ? 'hidden' : '' }"
data-item-code="${ item.item_code }"
style="margin-bottom: 0; margin-top: 0px; max-height: 30px;">
style="margin-bottom: 0; margin-top: 0px !important; max-height: 30px; float: right;
padding: 0.25rem 1rem; min-width: 135px;">
<span class="mr-2">
<svg class="icon icon-md">
<use href="#icon-assets"></use>
</svg>
</span>
${ __('Add to Cart') }
</div>
<div class="cart-indicator ${item.in_cart ? '' : 'hidden'}" style="position: unset;">
1
</div>
<a href="/cart">
<div id="${ item.name }" class="btn
btn-sm btn-primary btn-add-to-cart-list
ml-4 go-to-cart
${ item.in_cart ? '' : 'hidden' }"
data-item-code="${ item.item_code }"
style="padding: 0.25rem 1rem; min-width: 135px;">
${ __('Go to Cart') }
</div>
</a>
`;
} else {
return ``;

View File

@@ -3,6 +3,7 @@
import frappe
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
from erpnext.e_commerce.doctype.item_review.item_review import get_customer
from frappe.utils import flt
class ProductQuery:
@@ -38,7 +39,7 @@ class ProductQuery:
"""
# track if discounts included in field filters
self.filter_with_discount = bool(fields.get("discount"))
result, discount_list, website_item_groups, count = [], [], [], 0
result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0
website_item_groups = self.get_website_item_group_results(item_group, website_item_groups)
@@ -59,7 +60,11 @@ class ProductQuery:
# sort combined results by ranking
result = sorted(result, key=lambda x: x.get("ranking"), reverse=True)
result, discount_list = self.add_display_details(result, discount_list)
if self.settings.enabled:
cart_items = self.get_cart_items()
result, discount_list = self.add_display_details(result, discount_list, cart_items)
discounts = []
if discount_list:
@@ -191,7 +196,7 @@ class ProductQuery:
)
return website_item_groups
def add_display_details(self, result, discount_list):
def add_display_details(self, result, discount_list, cart_items):
"""Add price and availability details in result."""
for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
@@ -203,6 +208,8 @@ class ProductQuery:
if self.settings.show_stock_availability:
self.get_stock_availability(item)
item.in_cart = item.item_code in cart_items
item.wished = False
if frappe.db.exists("Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user}):
item.wished = True
@@ -225,13 +232,31 @@ class ProductQuery:
def get_stock_availability(self, item):
"""Modify item object and add stock details."""
item.in_stock = False
if item.get("website_warehouse"):
stock_qty = frappe.utils.flt(
frappe.db.get_value("Bin", {"item_code": item.item_code, "warehouse": item.get("website_warehouse")},
"actual_qty"))
item.in_stock = "green" if stock_qty else "red"
elif not frappe.db.get_value("Item", item.item_code, "is_stock_item"):
item.in_stock = "green" # non-stock item will always be available
item.in_stock = bool(stock_qty)
def get_cart_items(self):
customer = get_customer(silent=True)
if customer:
quotation = frappe.get_all("Quotation", fields=["name"], filters=
{"party_name": customer, "order_type": "Shopping Cart", "docstatus": 0},
order_by="modified desc", limit_page_length=1)
if quotation:
items = frappe.get_all(
"Quotation Item",
fields=["item_code"],
filters={
"parent": quotation[0].get("name")
})
items = [row.item_code for row in items]
return items
return []
def combine_web_item_group_results(self, item_group, result, website_item_groups):
"""Combine results with context of website item groups into item results."""

View File

@@ -10,8 +10,6 @@ erpnext.ProductSearch = class {
setupSearchDropDown() {
this.search_area = $("#dropdownMenuSearch");
this.setupSearchResultContainer();
this.setupProductsContainer();
this.setupCategoryRecentsContainer();
this.populateRecentSearches();
}
@@ -82,60 +80,46 @@ erpnext.ProductSearch = class {
<div class="overflow-hidden shadow dropdown-menu w-100 hidden"
id="search-results-container"
aria-labelledby="dropdownMenuSearch"
style="display: flex;">
style="display: flex; flex-direction: column;">
</div>
`).find("#search-results-container");
this.setupCategoryContainer()
this.setupProductsContainer();
this.setupRecentsContainer();
}
setupProductsContainer() {
let $products_section = this.search_dropdown.append(`
<div class="col-7 mr-2 mt-1"
id="product-results"
style="border-right: 1px solid var(--gray-200);">
</div>
`).find("#product-results");
this.products_container = $products_section.append(`
<div id="product-scroll" style="overflow: scroll; max-height: 300px">
<div class="mt-6 w-100 text-muted" style="font-weight: 400; text-align: center;">
${ __("Type something ...") }
this.products_container = this.search_dropdown.append(`
<div id="product-results mt-2">
<div id="product-scroll" style="overflow: scroll; max-height: 300px">
</div>
</div>
`).find("#product-scroll");
}
setupCategoryRecentsContainer() {
let $category_recents_section = $("#search-results-container").append(`
<div id="category-recents-container"
class="col-5 mt-2 h-100"
style="margin-left: -15px;">
</div>
`).find("#category-recents-container");
this.category_container = $category_recents_section.append(`
<div class="category-container">
<div class="mb-2"
style="border-bottom: 1px solid var(--gray-200);">
${ __("Categories") }
</div>
<div class="categories">
<span class="text-muted" style="font-weight: 400;"> ${ __('No results') } <span>
setupCategoryContainer() {
this.category_container = this.search_dropdown.append(`
<div class="category-container mt-2 mb-1">
<div class="category-chips">
</div>
</div>
`).find(".categories");
`).find(".category-chips");
}
let $recents_section = $("#category-recents-container").append(`
<div class="mb-2 mt-4 recent-searches">
<div style="border-bottom: 1px solid var(--gray-200);">
${ __("Recent") }
setupRecentsContainer() {
let $recents_section = this.search_dropdown.append(`
<div class="mb-2 mt-2 recent-searches">
<div>
<b>${ __("Recent") }</b>
</div>
</div>
`).find(".recent-searches");
this.recents_container = $recents_section.append(`
<div id="recent-chips" style="padding: 1rem 0;">
<div id="recents" style="padding: .25rem 0 1rem 0;">
</div>
`).find("#recent-chips");
`).find("#recents");
}
getRecentSearches() {
@@ -144,12 +128,12 @@ erpnext.ProductSearch = class {
attachEventListenersToChips() {
let me = this;
const chips = $(".recent-chip");
const chips = $(".recent-search");
window.chips = chips;
for (let chip of chips) {
chip.addEventListener("click", () => {
me.searchBox[0].value = chip.innerText;
me.searchBox[0].value = chip.innerText.trim();
// Start search with `recent query`
me.searchBox.trigger("input");
@@ -179,15 +163,22 @@ erpnext.ProductSearch = class {
let recents = this.getRecentSearches();
if (!recents.length) {
this.recents_container.html(`<span class=""text-muted">No searches yet.</span>`);
return;
}
let html = "";
recents.forEach((key) => {
html += `
<button class="btn btn-sm recent-chip mr-1 mb-2" style="font-size: 13px">
<div class="recent-search mr-1" style="font-size: 13px">
<span class="mr-2">
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="var(--gray-500)"" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.00027 5.20947V8.00017L10 10" stroke="var(--gray-500)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
${ key }
</button>
</div>
`;
});
@@ -197,11 +188,7 @@ erpnext.ProductSearch = class {
populateResults(data) {
if (data.length === 0 || data.message.results.length === 0) {
let empty_html = `
<div class="mt-6 w-100 text-muted" style="font-weight: 400; text-align: center;">
${ __('No results') }
</div>
`;
let empty_html = ``;
this.products_container.html(empty_html);
return;
}
@@ -228,21 +215,27 @@ erpnext.ProductSearch = class {
populateCategoriesList(data) {
if (data.length === 0 || data.message.results.length === 0) {
let empty_html = `
<span class="text-muted" style="font-weight: 400;">
${__('No results')}
</span>
<div class="category-container mt-2">
<div class="category-chips">
</div>
</div>
`;
this.category_container.html(empty_html);
return;
}
let html = "";
let html = `
<div class="mb-2">
<b>${ __("Categories") }</b>
</div>
`;
let search_results = data.message.results;
search_results.forEach((category) => {
html += `
<div class="mb-2" style="font-weight: 400;">
<a href="/${category.route}">${category.name}</a>
</div>
<a href="/${category.route}" class="btn btn-sm category-chip mr-2 mb-2"
style="font-size: 13px" role="button">
${ category.name }
</button>
`;
});

View File

@@ -12,11 +12,25 @@ erpnext.ProductView = class {
make(from_filters=false) {
this.products_section.empty();
this.prepare_view_toggler();
this.prepare_toolbar();
this.get_item_filter_data(from_filters);
}
prepare_toolbar() {
this.products_section.append(`
<div class="toolbar d-flex">
</div>
`);
this.prepare_search();
this.prepare_view_toggler();
frappe.require('/assets/js/e-commerce.min.js', function() {
new erpnext.ProductSearch();
});
}
prepare_view_toggler() {
if (!$("#list").length || !$("#image-view").length) {
this.render_view_toggler();
this.bind_view_toggler_actions();
@@ -173,19 +187,45 @@ erpnext.ProductView = class {
}
}
prepare_search() {
$(".toolbar").append(`
<div class="input-group col-6">
<div class="dropdown w-100" id="dropdownMenuSearch">
<input type="search" name="query" id="search-box" class="form-control font-md"
placeholder="Search for Products"
aria-label="Product" aria-describedby="button-addon2">
<div class="search-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-search">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<!-- Results dropdown rendered in product_search.js -->
</div>
</div>
`);
}
render_view_toggler() {
$(".toolbar").append(`<div class="toggle-container col-6"></div>`);
["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>`);
$(".toggle-container").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>
`);
});
}
@@ -448,9 +488,6 @@ erpnext.ProductView = class {
render_item_sub_categories(categories) {
if (categories && categories.length) {
let sub_group_html = `
<div class="sub-category-container">
<div class="heading"> ${ __('Sub Categories') } </div>
</div>
<div class="sub-category-container scroll-categories">
`;