mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-31 18:59:08 +00:00
feat: Search UI
- Search UI with dropdown results - Client class to handle Product Search actions and results - Integrated Search bar into all-products and item group pages - Run db search without redisearch - Cleanup: [Search] change decorator names and variables - Sider fixes
This commit is contained in:
@@ -3,35 +3,19 @@
|
||||
|
||||
{% block title %}{{ _('Products') }}{% endblock %}
|
||||
{% block header %}
|
||||
<div class="mb-6">{{ _('Products') }}</div>
|
||||
<div class="row mb-6" style="width: 65vw">
|
||||
<div class="mb-6 col-4 order-1">{{ _('Products') }}</div>
|
||||
|
||||
<div class="input-group mb-6 col-8 order-2">
|
||||
<div class="dropdown w-100" id="dropdownMenuSearch">
|
||||
<input type="search" name="query" id="search-box" class="form-control" placeholder="Search for products..." aria-label="Product" aria-describedby="button-addon2">
|
||||
<!-- Results dropdown rendered in product_search.js -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock header %}
|
||||
|
||||
{% block page_content %}
|
||||
<!-- 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')}}"
|
||||
aria-label="{{_('Product Search')}}" aria-describedby="product-search"
|
||||
value="{{ frappe.sanitize_html(frappe.form_dict.search) or '' }}"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-4 pl-0">
|
||||
<button class="btn btn-light btn-sm btn-block d-md-none"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#product-filters"
|
||||
aria-expanded="false"
|
||||
aria-controls="product-filters"
|
||||
style="white-space: nowrap;"
|
||||
>
|
||||
{{ _('Toggle Filters') }}
|
||||
</button>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="row">
|
||||
<!-- Items section -->
|
||||
<div id="product-listing" class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
|
||||
@@ -40,11 +24,6 @@
|
||||
|
||||
<!-- 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 %}
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
<div class="collapse d-md-block mr-4 filters-section" id="product-filters">
|
||||
<div class="d-flex justify-content-between align-items-center mb-5 title-section">
|
||||
<div class="mb-4 filters-title" > {{ _('Filters') }} </div>
|
||||
|
||||
@@ -7,8 +7,10 @@ $(() => {
|
||||
|
||||
let view_type = "List View";
|
||||
|
||||
// Render Product Views and setup Filters
|
||||
// Render Product Views, Filters & Search
|
||||
frappe.require('/assets/js/e-commerce.min.js', function() {
|
||||
new erpnext.ProductSearch();
|
||||
|
||||
new erpnext.ProductView({
|
||||
view_type: view_type,
|
||||
products_section: $('#product-listing'),
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.item-thumb {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.brand-line {
|
||||
color: gray;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block title %}{{ _('Search') }}{% endblock %}
|
||||
|
||||
{%- block head_include %}
|
||||
<link rel="stylesheet" href="search.css">
|
||||
{% endblock -%}
|
||||
|
||||
{% block header %}
|
||||
<div class="mb-6">{{ _('Search Products') }}</div>
|
||||
{% endblock header %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" name="query" id="search-box" class="form-control" placeholder="Search for products..." aria-label="Product" aria-describedby="button-addon2">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" id="search-button">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- To show recent searches -->
|
||||
<div class="my-2" id="recent-search-chips"></div>
|
||||
|
||||
<div class="row mt-2">
|
||||
<!-- Search Results -->
|
||||
<div class="col-sm">
|
||||
<h2>Products</h2>
|
||||
<ul id="results" class="list-group"></ul>
|
||||
</div>
|
||||
|
||||
{% set show_categories = frappe.db.get_single_value('E Commerce Settings', 'show_categories_in_search_autocomplete') %}
|
||||
{% if show_categories %}
|
||||
<div id="categories" class="col-sm">
|
||||
<h2>Categories</h2>
|
||||
<ul id="category-suggestions">
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set show_brand_line = frappe.db.get_single_value('E Commerce Settings', 'show_brand_line') %}
|
||||
{% if show_brand_line %}
|
||||
<span id="show-brand-line"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,148 +0,0 @@
|
||||
let loading = false;
|
||||
|
||||
const MAX_RECENT_SEARCHES = 4;
|
||||
|
||||
const searchBox = document.getElementById("search-box");
|
||||
const searchButton = document.getElementById("search-button");
|
||||
const results = document.getElementById("results");
|
||||
const categoryList = document.getElementById("category-suggestions");
|
||||
const showBrandLine = document.getElementById("show-brand-line");
|
||||
const recentSearchArea = document.getElementById("recent-search-chips");
|
||||
|
||||
function getRecentSearches() {
|
||||
return JSON.parse(localStorage.getItem("recent_searches") || "[]");
|
||||
}
|
||||
|
||||
function attachEventListenersToChips() {
|
||||
const chips = document.getElementsByClassName("recent-chip");
|
||||
|
||||
for (let chip of chips) {
|
||||
chip.addEventListener("click", () => {
|
||||
searchBox.value = chip.innerText;
|
||||
|
||||
// Start search with `recent query`
|
||||
const event = new Event("input");
|
||||
searchBox.dispatchEvent(event);
|
||||
searchBox.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function populateRecentSearches() {
|
||||
let recents = getRecentSearches();
|
||||
|
||||
if (!recents.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
html = "Recent Searches: ";
|
||||
for (let query of recents) {
|
||||
html += `<button class="btn btn-secondary btn-sm recent-chip mr-1">${query}</button>`;
|
||||
}
|
||||
|
||||
recentSearchArea.innerHTML = html;
|
||||
attachEventListenersToChips();
|
||||
}
|
||||
|
||||
function populateResults(data) {
|
||||
if (!data.message.from_redisearch) {
|
||||
// Data not from redisearch
|
||||
}
|
||||
|
||||
if (data.message.results.length === 0) {
|
||||
results.innerHTML = 'No results';
|
||||
return;
|
||||
}
|
||||
|
||||
html = ""
|
||||
search_results = data.message.results
|
||||
for (let res of search_results) {
|
||||
html += `<li class="list-group-item list-group-item-action">
|
||||
<img class="item-thumb" src="${res.thumbnail || 'img/placeholder.png'}" />
|
||||
<a href="/${res.route}">${res.web_item_name} <span class="brand-line">${showBrandLine && res.brand ? "by " + res.brand : ""}</span></a>
|
||||
</li>`
|
||||
}
|
||||
results.innerHTML = html;
|
||||
}
|
||||
|
||||
function populateCategoriesList(data) {
|
||||
if (!data.message.from_redisearch) {
|
||||
// Data not from redisearch
|
||||
categoryList.innerHTML = "Install Redisearch to enable autocompletions.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.message.results.length === 0) {
|
||||
categoryList.innerHTML = 'No results';
|
||||
return;
|
||||
}
|
||||
|
||||
html = ""
|
||||
search_results = data.message.results
|
||||
for (let category of search_results) {
|
||||
html += `<li>${category}</li>`
|
||||
}
|
||||
|
||||
categoryList.innerHTML = html;
|
||||
}
|
||||
|
||||
function updateLoadingState() {
|
||||
if (loading) {
|
||||
results.innerHTML = `<div class="spinner-border"><span class="sr-only">loading...<span></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
searchBox.addEventListener("input", (e) => {
|
||||
loading = true;
|
||||
updateLoadingState();
|
||||
frappe.call({
|
||||
method: "erpnext.templates.pages.product_search.search",
|
||||
args: {
|
||||
query: e.target.value
|
||||
},
|
||||
callback: (data) => {
|
||||
populateResults(data);
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
// If there is a suggestion list node
|
||||
if (categoryList) {
|
||||
frappe.call({
|
||||
method: "erpnext.templates.pages.product_search.get_category_suggestions",
|
||||
args: {
|
||||
query: e.target.value
|
||||
},
|
||||
callback: (data) => {
|
||||
populateCategoriesList(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
searchButton.addEventListener("click", (e) => {
|
||||
let query = searchBox.value;
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
|
||||
let recents = getRecentSearches();
|
||||
|
||||
if (recents.length >= MAX_RECENT_SEARCHES) {
|
||||
// Remove the `First` query
|
||||
recents.splice(0, 1);
|
||||
}
|
||||
|
||||
if (recents.indexOf(query) >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
recents.push(query);
|
||||
|
||||
localStorage.setItem("recent_searches", JSON.stringify(recents));
|
||||
|
||||
// Refresh recent searches
|
||||
populateRecentSearches();
|
||||
});
|
||||
|
||||
populateRecentSearches();
|
||||
@@ -71,8 +71,12 @@ def get_category_records(categories):
|
||||
fields += ["image"]
|
||||
|
||||
categorical_data[category] = frappe.db.sql(f"""
|
||||
Select {",".join(fields)}
|
||||
from `tab{doctype}`""", as_dict=1)
|
||||
Select
|
||||
{",".join(fields)}
|
||||
from
|
||||
`tab{doctype}`""",
|
||||
as_dict=1
|
||||
)
|
||||
|
||||
return categorical_data
|
||||
|
||||
|
||||
Reference in New Issue
Block a user