diff --git a/addons/portal_rating/views/rating_templates.xml b/addons/portal_rating/views/rating_templates.xml
index 50759e876d96f..f12377b632312 100644
--- a/addons/portal_rating/views/rating_templates.xml
+++ b/addons/portal_rating/views/rating_templates.xml
@@ -33,7 +33,7 @@
-
+
()
diff --git a/addons/test_website/__manifest__.py b/addons/test_website/__manifest__.py
index 1238a057f6625..2c213f55b8740 100644
--- a/addons/test_website/__manifest__.py
+++ b/addons/test_website/__manifest__.py
@@ -37,6 +37,7 @@
],
'web.assets_frontend': [
'test_website/static/src/interactions/**/*',
+ 'test_website/static/src/snippets/**/*.xml',
],
'website.website_builder_assets': [
'test_website/static/src/website_builder/**/*',
diff --git a/addons/test_website/models/model.py b/addons/test_website/models/model.py
index b9d3e646ca9ef..78a51d02a8f1e 100644
--- a/addons/test_website/models/model.py
+++ b/addons/test_website/models/model.py
@@ -45,6 +45,8 @@ def _search_get_detail(self, website, order, options):
},
'icon': 'fa-check-square-o',
'order': 'name asc, id desc',
+ 'template': 'test_website.search_result_item_test_model',
+ 'group_name': self.env._("Test Models"),
}
def _get_search_highlight_handlers(self):
diff --git a/addons/test_website/static/src/snippets/s_searchbar/000.xml b/addons/test_website/static/src/snippets/s_searchbar/000.xml
new file mode 100644
index 0000000000000..2420b57bd0fb3
--- /dev/null
+++ b/addons/test_website/static/src/snippets/s_searchbar/000.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/test_website/tests/test_fuzzy.py b/addons/test_website/tests/test_fuzzy.py
index ef0d2b925c445..e087885b525d5 100644
--- a/addons/test_website/tests/test_fuzzy.py
+++ b/addons/test_website/tests/test_fuzzy.py
@@ -30,9 +30,7 @@ def _autocomplete(self, term, expected_count, expected_fuzzy_term, search_type="
def _autocomplete_page(self, term, expected_count, expected_fuzzy_term):
self._autocomplete(term, expected_count, expected_fuzzy_term, search_type="pages", options={
- 'displayDescription': False, 'displayDetail': False,
- 'displayExtraDetail': False, 'displayExtraLink': False,
- 'displayImage': False, 'allowFuzzy': True
+ 'allowFuzzy': True
})
def test_01_many_records(self):
@@ -98,9 +96,7 @@ def test_02_pages_search(self):
# ORM level, not in SQL, due to how `inherits` works.
self.env['website'].browse(1)._search_with_fuzzy(
'pages', 'test', limit=5, order='name asc, website_id desc, id', options={
- 'displayDescription': False, 'displayDetail': False,
- 'displayExtraDetail': False, 'displayExtraLink': False,
- 'displayImage': False, 'allowFuzzy': True
+ 'allowFuzzy': True
}
)
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index c6a26d1345dd9..bfcef0173a5c2 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -587,7 +587,7 @@ def _get_search_order(self, order):
return 'is_published desc, %s, id desc' % order
@http.route('/website/snippet/autocomplete', type='jsonrpc', auth='public', website=True, readonly=True)
- def autocomplete(self, search_type=None, term=None, order=None, limit=5, max_nb_chars=999, options=None):
+ def autocomplete(self, search_type=None, term=None, order=None, limit=6, max_nb_chars=999, options=None):
"""
Returns list of results according to the term and options
@@ -612,54 +612,64 @@ def autocomplete(self, search_type=None, term=None, order=None, limit=5, max_nb_
order = self._get_search_order(order)
options = options or {}
results_count, search_results, fuzzy_term = request.website._search_with_fuzzy(search_type, term, limit, order, options)
+ # Sort result based in sequence for ordered results.
+ search_results.sort(key=lambda d: d.get('sequence', float('inf')))
if not results_count:
return {
- 'results': [],
+ 'results': {},
'results_count': 0,
'parts': {},
}
term = fuzzy_term or term
- search_results = request.website._search_render_results(search_results, limit)
+ search_results = request.website.sudo()._search_render_results(search_results, limit)
mappings = []
- results_data = []
+ result = {}
for search_result in search_results:
- for result in search_result['results_data']:
- result['model'] = search_result['model']
- results_data.append(result)
+ if not len(search_result['results_data']):
+ continue
+ search_result['results_data'].sort(key=lambda r: r.get('name', ''), reverse='name desc' in order)
mappings.append(search_result['mapping'])
- if search_type == 'all':
- # Only supported order for 'all' is on name
- results_data.sort(key=lambda r: r.get('name', ''), reverse='name desc' in order)
- results_data = results_data[:limit]
- result = []
- for record in results_data:
- mapping = record['_mapping']
- model = request.env[record['model']]
- mapped = {
- '_fa': record.get('_fa'),
- }
- for mapped_name, field_meta in mapping.items():
- value = record.get(field_meta.get('name'))
- if not value:
- mapped[mapped_name] = ''
- continue
- field_type = field_meta.get('type')
- if field_type == 'text' and field_meta.get('truncate', True):
- value = self._shorten_around_match(value, term, max_nb_chars)
-
- if field_meta.get('match'):
- skip_field, value, field_type = model._search_highlight_field(field_meta, value, term)
- if skip_field:
+ group_name = search_result.get("group_name")
+ group_key = '_'.join(group_name.lower().split())
+ result_data = []
+ for record in search_result['results_data']:
+ model = request.env[search_result['model']]
+ mapping = record['_mapping']
+ mapped = {
+ '_fa': record.get('_fa'),
+ }
+ model = request.env[search_result['model']]
+ for mapped_name, field_meta in mapping.items():
+ value = record.get(field_meta.get('name'))
+ if not value:
+ mapped[mapped_name] = ''
continue
-
- if field_type not in ('image', 'binary') and ('ir.qweb.field.%s' % field_type) in request.env:
- opt = {}
- if field_type == 'monetary':
- opt['display_currency'] = options['display_currency']
- value = request.env[('ir.qweb.field.%s' % field_type)].value_to_html(value, opt)
- mapped[mapped_name] = escape(value)
- result.append(mapped)
+ field_type = field_meta.get('type')
+ if field_type == 'text' and field_meta.get('truncate', True):
+ value = self._shorten_around_match(value, term, max_nb_chars)
+
+ if field_meta.get('match'):
+ skip_field, value, field_type = model._search_highlight_field(field_meta, value, term)
+ if skip_field:
+ continue
+
+ if field_type not in ('image', 'binary') and ('ir.qweb.field.%s' % field_type) in request.env:
+ opt = {}
+ if field_type == 'monetary':
+ opt['display_currency'] = options['display_currency']
+ elif field_type == 'float':
+ opt['precision'] = field_meta.get('precision', 2)
+ value = request.env[('ir.qweb.field.%s' % field_type)].value_to_html(value, opt)
+ mapped[mapped_name] = escape(value)
+ result_data.append(mapped)
+
+ result[group_key] = {
+ "groupName": group_name,
+ "templateKey": search_result.get("template_key"),
+ "search_count": search_result.get('count'),
+ "data": result_data,
+ }
return {
'results': result,
@@ -670,11 +680,6 @@ def autocomplete(self, search_type=None, term=None, order=None, limit=5, max_nb_
def _get_page_search_options(self, **post):
return {
- 'displayDescription': False,
- 'displayDetail': False,
- 'displayExtraDetail': False,
- 'displayExtraLink': False,
- 'displayImage': False,
'allowFuzzy': not post.get('noFuzzy'),
}
@@ -708,44 +713,26 @@ def pages_list(self, page=1, search='', **kw):
def _get_hybrid_search_options(self, **post):
return {
- 'displayDescription': True,
- 'displayDetail': True,
- 'displayExtraDetail': True,
- 'displayExtraLink': True,
- 'displayImage': True,
'allowFuzzy': not post.get('noFuzzy'),
}
@http.route([
'/website/search',
- '/website/search/page/',
'/website/search/',
- '/website/search//page/',
], type='http', auth="public", website=True, sitemap=False, readonly=True)
- def hybrid_list(self, page=1, search='', search_type='all', **kw):
+ def hybrid_list(self, search='', search_type='all', **kw):
if not search:
return request.render("website.list_hybrid")
options = self._get_hybrid_search_options(**kw)
- data = self.autocomplete(search_type=search_type, term=search, order='name asc', limit=500, max_nb_chars=200, options=options)
+ data = self.autocomplete(search_type=search_type, term=search, order='name asc', limit=100, max_nb_chars=200, options=options)
results = data.get('results', [])
- search_count = len(results)
+ search_count = data.get('results_count', 0)
parts = data.get('parts', {})
- step = 50
- pager = portal_pager(
- url="/website/search/%s" % search_type,
- url_args={'search': search},
- total=search_count,
- page=page,
- step=step
- )
-
- results = results[(page - 1) * step:page * step]
-
+ # TODO: Implement pagination or load more button for more records
values = {
- 'pager': pager,
'results': results,
'parts': parts,
'search': search,
diff --git a/addons/website/models/mixins.py b/addons/website/models/mixins.py
index c47ae137d31f0..c3f2ffb116a93 100644
--- a/addons/website/models/mixins.py
+++ b/addons/website/models/mixins.py
@@ -118,6 +118,15 @@ def _get_background(self, height=None, width=None):
img = img[:-1] + suffix + ')'
return img
+ def _get_image_url(self):
+ self.ensure_one()
+ properties = json_safe.loads(self.cover_properties)
+ img = properties.get('background-image', None)
+ if not img:
+ return None
+ match = re.search(r"url\(\s*(['\"]?)(.*?)\1\s*\)", img)
+ return match.group(2) if match else img
+
def write(self, vals):
if 'cover_properties' not in vals:
return super().write(vals)
@@ -694,7 +703,8 @@ def _search_fetch(self, search_detail, search, limit, order):
return results, count
def _search_render_results(self, fetch_fields, mapping, icon, limit):
- results_data = self.read(fetch_fields)[:limit]
+ # Some fields are not avaiable in public user group - require sudo to complete result
+ results_data = self.sudo().read(fetch_fields)[:limit]
for result in results_data:
result['_fa'] = icon
result['_mapping'] = mapping
diff --git a/addons/website/models/website_page.py b/addons/website/models/website_page.py
index afb39a6d278ac..57255e9780743 100644
--- a/addons/website/models/website_page.py
+++ b/addons/website/models/website_page.py
@@ -206,7 +206,6 @@ def get_website_meta(self):
@api.model
def _search_get_detail(self, website, order, options):
- with_description = options['displayDescription']
# Read access on website.page requires sudo.
requires_sudo = True
domain = [website.website_domain()]
@@ -224,16 +223,13 @@ def _search_get_detail(self, website, order, options):
[('group_ids', '=', False)], [('group_ids', 'in', self.env.user.group_ids.ids)]
]))
- search_fields = ['name', 'url']
- fetch_fields = ['id', 'name', 'url']
+ search_fields = ['name', 'url', 'arch_db']
+ fetch_fields = ['id', 'name', 'url', 'arch']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
'website_url': {'name': 'url', 'type': 'text', 'truncate': False},
+ 'description': {'name': 'arch', 'type': 'text', 'html': True, 'match': True}
}
- if with_description:
- search_fields.append('arch_db')
- fetch_fields.append('arch')
- mapping['description'] = {'name': 'arch', 'type': 'text', 'html': True, 'match': True}
return {
'model': 'website.page',
'base_domain': domain,
@@ -242,6 +238,9 @@ def _search_get_detail(self, website, order, options):
'fetch_fields': fetch_fields,
'mapping': mapping,
'icon': 'fa-file-o',
+ 'template_key': 'website.search_items_page',
+ 'group_name': self.env._("Pages"),
+ 'sequence': 10,
}
@api.model
diff --git a/addons/website/static/src/@types/plugins.d.ts b/addons/website/static/src/@types/plugins.d.ts
index 380fdea2338f8..13c11b9fc39ca 100644
--- a/addons/website/static/src/@types/plugins.d.ts
+++ b/addons/website/static/src/@types/plugins.d.ts
@@ -23,7 +23,7 @@ declare module "plugins" {
import { MegaMenuOptionShared } from "@website/builder/plugins/options/mega_menu_option_plugin";
import { NavTabsStyleOptionShared } from "@website/builder/plugins/options/navtabs_style_option_plugin";
import { WebsiteParallaxShared } from "@website/builder/plugins/options/parallax_option_plugin";
- import { searchbar_option_display_items, searchbar_option_order_by_items } from "@website/builder/plugins/options/searchbar_option_plugin";
+ import { searchbar_option_order_by_items } from "@website/builder/plugins/options/searchbar_option_plugin";
import { SocialMediaOptionShared } from "@website/builder/plugins/options/social_media_option_plugin";
import { visibility_selector_parameters } from "@website/builder/plugins/options/visibility_option_plugin";
import { WebsitePageConfigOptionShared } from "@website/builder/plugins/options/website_page_config_option_plugin";
@@ -92,7 +92,6 @@ declare module "plugins" {
footer_templates_providers: footer_templates_providers;
// Data
- searchbar_option_display_items: searchbar_option_display_items;
searchbar_option_order_by_items: searchbar_option_order_by_items;
theme_options: theme_options;
visibility_selector_parameters: visibility_selector_parameters;
diff --git a/addons/website/static/src/builder/plugins/options/searchbar_option.js b/addons/website/static/src/builder/plugins/options/searchbar_option.js
index ea14e1ecdad18..37b925b10c8d0 100644
--- a/addons/website/static/src/builder/plugins/options/searchbar_option.js
+++ b/addons/website/static/src/builder/plugins/options/searchbar_option.js
@@ -10,6 +10,5 @@ export class SearchbarOption extends BaseOptionComponent {
this.getItemValue = useGetItemValue();
this.orderByItems = this.getResource("searchbar_option_order_by_items");
- this.displayItems = this.getResource("searchbar_option_display_items");
}
}
diff --git a/addons/website/static/src/builder/plugins/options/searchbar_option.xml b/addons/website/static/src/builder/plugins/options/searchbar_option.xml
index 11dc6e49e2e98..eb630eea81210 100644
--- a/addons/website/static/src/builder/plugins/options/searchbar_option.xml
+++ b/addons/website/static/src/builder/plugins/options/searchbar_option.xml
@@ -29,11 +29,6 @@
results
-
-
-
-
-
Default Input Style
diff --git a/addons/website/static/src/builder/plugins/options/searchbar_option_plugin.js b/addons/website/static/src/builder/plugins/options/searchbar_option_plugin.js
index ff6ce169fbd91..9df9e8303ccc4 100644
--- a/addons/website/static/src/builder/plugins/options/searchbar_option_plugin.js
+++ b/addons/website/static/src/builder/plugins/options/searchbar_option_plugin.js
@@ -31,27 +31,6 @@ import { BuilderAction } from "@html_builder/core/builder_action";
* },
* };
*/
-/**
- * @typedef {{
- * label: LazyTranslatedString;
- * dataAttribute: string;
- * dependency: string;
- * }[]} searchbar_option_display_items
- *
- * Register display options for the website searchbar.
- * `dataAttribute` is the attribute which will be used to display the data.
- * `dependency` takes an id of another builder option.
- *
- * Example:
- *
- * resources: {
- * searchbar_option_display_items: {
- * label: _t("Description"),
- * dataAttribute: "displayDescription",
- * dependency: "search_all_opt",
- * },
- * };
- */
class SearchbarOptionPlugin extends Plugin {
static id = "searchbarOption";
@@ -62,12 +41,6 @@ class SearchbarOptionPlugin extends Plugin {
SetSearchTypeAction,
SetOrderByAction,
SetSearchbarStyleAction,
- // This resets the data attribute to an empty string on clean.
- // TODO: modify the Python `_search_get_detail()` (grep
- // `with_description = options['displayDescription']`) so we can use
- // the default `dataAttributeAction`. The python should not need a
- // value if it doesn't exist.
- SetNonEmptyDataAttributeAction,
},
so_content_addition_selector: [".s_searchbar_input"],
searchbar_option_order_by_items: {
@@ -75,33 +48,6 @@ class SearchbarOptionPlugin extends Plugin {
orderBy: "name asc",
id: "order_name_asc_opt",
},
- searchbar_option_display_items: [
- {
- label: _t("Description"),
- dataAttribute: "displayDescription",
- dependency: "search_all_opt",
- },
- {
- label: _t("Content"),
- dataAttribute: "displayDescription",
- dependency: "search_pages_opt",
- },
- {
- label: _t("Extra Link"),
- dataAttribute: "displayExtraLink",
- dependency: "search_all_opt",
- },
- {
- label: _t("Detail"),
- dataAttribute: "displayDetail",
- dependency: "search_all_opt",
- },
- {
- label: _t("Image"),
- dataAttribute: "displayImage",
- dependency: "search_all_opt",
- },
- ],
// input group should not be contenteditable, while all other children
// beside the input are contenteditable
content_not_editable_selectors: [".input-group:has( > input)"],
@@ -144,21 +90,6 @@ export class SetSearchTypeAction extends BaseSearchBarAction {
editingElement.dataset.orderBy = this.defaultSearchType;
searchOrderByInputEl.value = this.defaultSearchType;
}
-
- // Reset display options. Has to be done in 2 steps, because
- // the same option may be on 2 dependencies, and we don't
- // want the 1st to add it and the 2nd to delete it.
- const displayDataAttributes = new Set();
- for (const item of this.getResource("searchbar_option_display_items")) {
- if (isDependencyActive(item.dependency)) {
- displayDataAttributes.add(item.dataAttribute);
- } else {
- delete editingElement.dataset[item.dataAttribute];
- }
- }
- for (const dataAttribute of displayDataAttributes) {
- editingElement.dataset[dataAttribute] = "true";
- }
}
}
export class SetOrderByAction extends BaseSearchBarAction {
@@ -190,29 +121,5 @@ export class SetSearchbarStyleAction extends BaseSearchBarAction {
searchButtonEl?.classList.toggle("btn-primary", !isLight);
}
}
-// This resets the data attribute to an empty string on clean.
-// TODO: modify the Python `_search_get_detail()` (grep
-// `with_description = options['displayDescription']`) so we can use
-// the default `dataAttributeAction`. The python should not need a
-// value if it doesn't exist.
-export class SetNonEmptyDataAttributeAction extends BuilderAction {
- static id = "setNonEmptyDataAttribute";
- getValue({ editingElement, params: { mainParam: attributeName } = {} }) {
- return editingElement.dataset[attributeName];
- }
- isApplied({ editingElement, params: { mainParam: attributeName } = {}, value = "" }) {
- return editingElement.dataset[attributeName] === value;
- }
- apply({ editingElement, params: { mainParam: attributeName } = {}, value }) {
- if (value) {
- editingElement.dataset[attributeName] = value;
- } else {
- delete editingElement.dataset[attributeName];
- }
- }
- clean({ editingElement, params: { mainParam: attributeName } = {} }) {
- editingElement.dataset[attributeName] = "";
- }
-}
registry.category("website-plugins").add(SearchbarOptionPlugin.id, SearchbarOptionPlugin);
diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss
index 9209bce823d8f..750404841a3b9 100644
--- a/addons/website/static/src/scss/website.scss
+++ b/addons/website/static/src/scss/website.scss
@@ -2348,11 +2348,19 @@ $ribbon-padding: 100px;
}
// Search results
-.o_search_result_item_detail {
- flex: 1;
+.o_search_result_item {
+ min-height: inherit;
- button:disabled {
- color: inherit;
+ .o_search_result_item_content {
+ flex: 1;
+ button:disabled {
+ color: inherit;
+ }
+ }
+ a {
+ &:focus, &:hover {
+ background: var(--tertiary-bg) !important;
+ }
}
}
diff --git a/addons/website/static/src/snippets/s_searchbar/000.xml b/addons/website/static/src/snippets/s_searchbar/000.xml
index ff23cff9f8991..e9533e1df95bb 100644
--- a/addons/website/static/src/snippets/s_searchbar/000.xml
+++ b/addons/website/static/src/snippets/s_searchbar/000.xml
@@ -1,55 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
-