Skip to content

Commit 7407431

Browse files
LSanthaherzog31
andauthored
CIF-2736 - Support custom sort keys in product listing components (#901)
* CIF-2736 - Support custom sort keys in product listing components * extended productcollectione dit dialog with configuration for default sorting * implemented support for default sorting configuration in productlist and searchresults components * updated unit tests * CIF-2736 - Support custom sort keys in product listing components * updated component readmes * CIF-2736 - Support custom sort keys in product listing components * added more unit tests * CIF-2736 - Support custom sort keys in product listing components * added javadoc * fixed imports * CIF-2736 - Support custom sort keys in product listing components * removed unneeded preset sort keys * improved unit tests and javadoc * CIF-2736 - Support custom sort keys in product listing components * addressing review feedback * improved unit tests coverage * CIF-2736 - Support custom sort keys in product listing components * minor fixes * CIF-2736 - Support custom sort keys in product listing components * use cloud icon for implicit sort field in productcollection edit dialog Co-authored-by: Mark J. Becker <[email protected]>
1 parent 54be0f0 commit 7407431

File tree

25 files changed

+590
-65
lines changed

25 files changed

+590
-65
lines changed

bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/productlist/ProductListImpl.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,13 @@ protected void initModel() {
147147
searchOptions.setAttributeFilters(searchFilters);
148148

149149
// configure sorting
150-
searchOptions.addSorterKey("price", "Price", Sorter.Order.ASC);
151-
searchOptions.addSorterKey("name", "Product Name", Sorter.Order.ASC);
150+
String defaultSortField = properties.get(PN_DEFAULT_SORT_FIELD, String.class);
151+
String defaultSortOrder = properties.get(PN_DEFAULT_SORT_ORDER, Sorter.Order.ASC.name());
152+
153+
if (StringUtils.isNotBlank(defaultSortField)) {
154+
Sorter.Order value = Sorter.Order.fromString(defaultSortOrder, Sorter.Order.ASC);
155+
searchOptions.setDefaultSorter(defaultSortField, value);
156+
}
152157
}
153158
}
154159

bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/searchresults/SearchResultsImpl.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,15 @@ protected void initModel() {
7474
searchOptions.setSearchQuery(searchTerm);
7575

7676
// configure sorting
77+
String defaultSortField = properties.get(PN_DEFAULT_SORT_FIELD, String.class);
78+
String defaultSortOrder = properties.get(PN_DEFAULT_SORT_ORDER, Sorter.Order.ASC.name());
79+
80+
if (StringUtils.isNotBlank(defaultSortField)) {
81+
Sorter.Order value = Sorter.Order.fromString(defaultSortOrder, Sorter.Order.ASC);
82+
searchOptions.setDefaultSorter(defaultSortField, value);
83+
}
84+
// relevance is not provided in the products search results, we add it manually
7785
searchOptions.addSorterKey("relevance", "Relevance", Sorter.Order.DESC);
78-
searchOptions.addSorterKey("price", "Price", Sorter.Order.ASC);
79-
searchOptions.addSorterKey("name", "Product Name", Sorter.Order.ASC);
8086
}
8187

8288
protected Map<String, String> createFilterMap(final Map<String, String[]> parameterMap) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2+
~ Copyright 2022 Adobe
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
16+
package com.adobe.cq.commerce.core.components.internal.servlets;
17+
18+
import java.util.ArrayList;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
22+
import javax.servlet.Servlet;
23+
24+
import org.apache.sling.api.SlingHttpServletRequest;
25+
import org.apache.sling.api.SlingHttpServletResponse;
26+
import org.apache.sling.api.resource.Resource;
27+
import org.apache.sling.api.resource.ResourceMetadata;
28+
import org.apache.sling.api.resource.ResourceResolver;
29+
import org.apache.sling.api.resource.ValueMap;
30+
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
31+
import org.apache.sling.api.wrappers.ValueMapDecorator;
32+
import org.osgi.service.component.annotations.Component;
33+
34+
import com.adobe.cq.commerce.core.components.client.MagentoGraphqlClient;
35+
import com.adobe.cq.commerce.magento.graphql.SortField;
36+
import com.adobe.cq.commerce.magento.graphql.SortFields;
37+
import com.adobe.granite.ui.components.ds.DataSource;
38+
import com.adobe.granite.ui.components.ds.SimpleDataSource;
39+
import com.adobe.granite.ui.components.ds.ValueMapResource;
40+
import com.day.cq.commons.jcr.JcrConstants;
41+
42+
/**
43+
* A {@link DataSource} implementation for the available product sort fields.
44+
*/
45+
@Component(
46+
immediate = true,
47+
service = Servlet.class,
48+
property = { ProductSortFieldsDataSourceServlet.RT_PRODUCTCOLLECTION_SORTFIELDS })
49+
public class ProductSortFieldsDataSourceServlet extends SlingSafeMethodsServlet {
50+
static final String RT_PRODUCTCOLLECTION_SORTFIELDS = "sling.servlet.resourceTypes=core/cif/components/commerce/productcollection/sortfields";
51+
static final String RT_SEARCHRESULTS = "core/cif/components/commerce/searchresults/v2/searchresults";
52+
static final String ICON_IMPLICIT_SORTFIELD = "CloudOutline";
53+
54+
@Override
55+
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) {
56+
Resource suffixResource = request.getRequestPathInfo().getSuffixResource();
57+
if (suffixResource == null) {
58+
return;
59+
}
60+
61+
MagentoGraphqlClient magentoGraphqlClient = suffixResource.adaptTo(MagentoGraphqlClient.class);
62+
if (magentoGraphqlClient == null) {
63+
return;
64+
}
65+
66+
List<Resource> values = new ArrayList<>();
67+
68+
String query = "{products(filter:{}) {sort_fields {default options {label value}}}}";
69+
SortFields sortFields = magentoGraphqlClient.execute(query).getData().getProducts().getSortFields();
70+
71+
String defaultSortField = sortFields.getDefault();
72+
ResourceResolver resourceResolver = request.getResourceResolver();
73+
74+
for (SortField sortField : sortFields.getOptions()) {
75+
ValueMap vm = new ValueMapDecorator(new HashMap<>());
76+
vm.put("value", sortField.getValue());
77+
vm.put("text", sortField.getLabel());
78+
if (defaultSortField != null && defaultSortField.equals(sortField.getValue())) {
79+
vm.put("icon", ICON_IMPLICIT_SORTFIELD);
80+
81+
}
82+
values.add(new ValueMapResource(resourceResolver, new ResourceMetadata(), JcrConstants.NT_UNSTRUCTURED, vm));
83+
}
84+
85+
// relevance is not provided in the products search results, we add it manually
86+
if (suffixResource.isResourceType(RT_SEARCHRESULTS)) {
87+
if (values.stream().noneMatch(res -> "relevance".equals(res.getValueMap().get("value", String.class)))) {
88+
ValueMap vm = new ValueMapDecorator(new HashMap<>());
89+
vm.put("value", "relevance");
90+
vm.put("text", "Relevance");
91+
values.add(new ValueMapResource(resourceResolver, new ResourceMetadata(), JcrConstants.NT_UNSTRUCTURED, vm));
92+
}
93+
}
94+
95+
DataSource ds = new SimpleDataSource(values.iterator());
96+
request.setAttribute(DataSource.class.getName(), ds);
97+
}
98+
}

bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/productcollection/ProductCollection.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ public interface ProductCollection {
5151
*/
5252
String PN_PAGINATION_TYPE = "paginationType";
5353

54+
/**
55+
* Name of the String resource property for the default product sort field.
56+
*/
57+
String PN_DEFAULT_SORT_FIELD = "defaultSortField";
58+
59+
/**
60+
* Name of the String resource property for the default product sort order.
61+
*/
62+
String PN_DEFAULT_SORT_ORDER = "defaultSortOrder";
63+
5464
/**
5565
* Returns the product list's items collection, as {@link ProductListItem}s elements.
5666
*

bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/productcollection/package-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
~ See the License for the specific language governing permissions and
1414
~ limitations under the License.
1515
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
16-
@Version("2.1.0")
16+
@Version("2.2.0")
1717
package com.adobe.cq.commerce.core.components.models.productcollection;
1818

1919
import org.osgi.annotation.versioning.Version;

bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/productlist/package-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
~ See the License for the specific language governing permissions and
1414
~ limitations under the License.
1515
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
16-
@Version("4.3.0")
16+
@Version("4.4.0")
1717
package com.adobe.cq.commerce.core.components.models.productlist;
1818

1919
import org.osgi.annotation.versioning.Version;

bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/searchresults/package-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
~ See the License for the specific language governing permissions and
1414
~ limitations under the License.
1515
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
16-
@Version("4.1.0")
16+
@Version("4.2.0")
1717
package com.adobe.cq.commerce.core.components.models.searchresults;
1818

1919
import org.osgi.annotation.versioning.Version;

bundles/core/src/main/java/com/adobe/cq/commerce/core/search/internal/models/SearchOptionsImpl.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ public class SearchOptionsImpl implements SearchOptions {
4444

4545
List<SorterKey> sorterKeys = new ArrayList<>();
4646

47+
SorterKey defaultSorter;
48+
4749
public SearchOptionsImpl() {
4850
attributeFilters = new HashMap<>();
4951
}
@@ -64,6 +66,7 @@ public SearchOptionsImpl(SearchOptions searchOptions) {
6466
searchQuery = searchOptions.getSearchQuery().get();
6567
}
6668

69+
this.defaultSorter = searchOptions.getDefaultSorter();
6770
}
6871

6972
@Override
@@ -137,4 +140,16 @@ public void addSorterKey(String name, String label, Sorter.Order preferredOrder)
137140
public List<SorterKey> getSorterKeys() {
138141
return sorterKeys;
139142
}
143+
144+
@Override
145+
public void setDefaultSorter(String sortField, Sorter.Order sortOrder) {
146+
SorterKeyImpl defaultSorter = new SorterKeyImpl(sortField, sortField);
147+
defaultSorter.setOrder(sortOrder);
148+
this.defaultSorter = defaultSorter;
149+
}
150+
151+
@Override
152+
public SorterKey getDefaultSorter() {
153+
return defaultSorter;
154+
}
140155
}

bundles/core/src/main/java/com/adobe/cq/commerce/core/search/internal/services/SearchResultsServiceImpl.java

Lines changed: 74 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.ArrayList;
1919
import java.util.Collection;
2020
import java.util.Collections;
21+
import java.util.Comparator;
2122
import java.util.HashMap;
2223
import java.util.List;
2324
import java.util.Map;
@@ -76,6 +77,8 @@
7677
import com.adobe.cq.commerce.magento.graphql.Query;
7778
import com.adobe.cq.commerce.magento.graphql.QueryQuery;
7879
import com.adobe.cq.commerce.magento.graphql.SortEnum;
80+
import com.adobe.cq.commerce.magento.graphql.SortField;
81+
import com.adobe.cq.commerce.magento.graphql.SortFields;
7982
import com.adobe.cq.commerce.magento.graphql.gson.Error;
8083
import com.adobe.cq.wcm.core.components.util.ComponentUtils;
8184
import com.day.cq.wcm.api.Page;
@@ -178,7 +181,7 @@ public Pair<QueryQuery.CategoryListArgumentsDefinition, CategoryTreeQueryDefinit
178181
// We will use the search filter service to retrieve all of the potential available filters the commerce system
179182
// has available for querying against
180183
List<FilterAttributeMetadata> availableFilters = searchFilterService.retrieveCurrentlyAvailableCommerceFilters(request, page);
181-
SorterKey currentSorterKey = prepareSorting(mutableSearchOptions, searchResultsSet);
184+
SorterKey currentSorterKey = findSortKey(mutableSearchOptions);
182185

183186
String productsQueryString = generateProductsQueryString(mutableSearchOptions, availableFilters, productQueryHook,
184187
currentSorterKey);
@@ -198,6 +201,8 @@ public Pair<QueryQuery.CategoryListArgumentsDefinition, CategoryTreeQueryDefinit
198201
final List<ProductListItem> productListItems = extractProductsFromResponse(products.getItems(), productPage, request, resource,
199202
category);
200203

204+
prepareSortKeys(currentSorterKey, products.getSortFields(), mutableSearchOptions, searchResultsSet);
205+
201206
List<SearchAggregation> searchAggregations = extractSearchAggregationsFromResponse(products.getAggregations(),
202207
mutableSearchOptions.getAllFilters(), availableFilters);
203208

@@ -212,40 +217,56 @@ public Pair<QueryQuery.CategoryListArgumentsDefinition, CategoryTreeQueryDefinit
212217
return new ImmutablePair<>(category, searchResultsSet);
213218
}
214219

215-
private SorterKey prepareSorting(SearchOptions searchOptions, SearchResultsSetImpl searchResultsSet) {
216-
List<SorterKey> availableSorterKeys = searchOptions.getSorterKeys();
217-
if (CollectionUtils.isEmpty(availableSorterKeys)) {
218-
return null;
219-
}
220-
221-
SorterKey resultSorterKey = null;
222-
223-
SorterKey defaultSorterKey = availableSorterKeys.get(0);
220+
private SorterKey findSortKey(SearchOptionsImpl searchOptions) {
221+
SorterKey defaultSorterKey = searchOptions.getDefaultSorter();
224222
String sortKeyParam = searchOptions.getAllFilters().get(Sorter.PARAMETER_SORT_KEY);
225223
if (sortKeyParam == null) {
226-
sortKeyParam = defaultSorterKey.getName();
227-
}
228-
String sortOrderParam = searchOptions.getAllFilters().get(Sorter.PARAMETER_SORT_ORDER);
229-
Sorter.Order sortOrder;
230-
try {
231-
if (sortOrderParam != null) {
232-
sortOrderParam = Sorter.Order.valueOf(sortOrderParam.toUpperCase()).name();
224+
if (defaultSorterKey != null) {
225+
sortKeyParam = defaultSorterKey.getName();
226+
} else {
227+
return null;
233228
}
234-
} catch (RuntimeException x) {
235-
sortOrderParam = null;
236229
}
237-
if (sortOrderParam == null) {
238-
sortOrder = defaultSorterKey.getOrder();
239-
if (sortOrder == null) {
240-
sortOrder = Sorter.Order.ASC;
230+
231+
Sorter.Order defaultSortOrder;
232+
if (defaultSorterKey != null) {
233+
defaultSortOrder = defaultSorterKey.getOrder();
234+
if (defaultSortOrder == null) {
235+
defaultSortOrder = Sorter.Order.ASC;
241236
}
242237
} else {
243-
sortOrder = Sorter.Order.valueOf(sortOrderParam.toUpperCase());
238+
defaultSortOrder = Sorter.Order.ASC;
239+
}
240+
241+
String sortOrderParam = searchOptions.getAllFilters().get(Sorter.PARAMETER_SORT_ORDER);
242+
Sorter.Order sortOrder = Sorter.Order.fromString(sortOrderParam, defaultSortOrder);
243+
244+
SorterKeyImpl resultSorterKey = new SorterKeyImpl(sortKeyParam, sortKeyParam);
245+
resultSorterKey.setOrder(sortOrder);
246+
resultSorterKey.setSelected(true);
247+
248+
return resultSorterKey;
249+
}
250+
251+
private void prepareSortKeys(SorterKey currentSorterKey, SortFields sortFields, SearchOptions searchOptions,
252+
SearchResultsSetImpl searchResultsSet) {
253+
254+
String defaultSortField = null;
255+
if (sortFields != null) {
256+
for (SortField sortField : sortFields.getOptions()) {
257+
if (searchOptions.getSorterKeys().stream().noneMatch(sk -> sk.getName().equals(sortField.getValue()))) {
258+
searchOptions.addSorterKey(sortField.getValue(), sortField.getLabel(), Sorter.Order.ASC);
259+
}
260+
}
261+
262+
defaultSortField = sortFields.getDefault();
244263
}
245264

265+
List<SorterKey> availableSorterKeys = searchOptions.getSorterKeys();
266+
246267
SorterImpl sorter = searchResultsSet.getSorter();
247-
List<SorterKey> keys = new ArrayList<>();
248-
keys.addAll(availableSorterKeys);
268+
List<SorterKey> keys = new ArrayList<>(availableSorterKeys);
269+
keys.sort(Comparator.comparing(SorterKey::getLabel));
249270
sorter.setKeys(keys);
250271

251272
for (SorterKey key : keys) {
@@ -254,13 +275,19 @@ private SorterKey prepareSorting(SearchOptions searchOptions, SearchResultsSetIm
254275
Map<String, String> cParams = new HashMap<>(searchOptions.getAllFilters());
255276
cParams.put(Sorter.PARAMETER_SORT_KEY, key.getName());
256277
Sorter.Order keyOrder = keyImpl.getOrder();
257-
if (sortKeyParam.equals(key.getName())) {
258-
keyImpl.setSelected(true);
259-
sorter.setCurrentKey(key);
260-
keyOrder = sortOrder;
261-
resultSorterKey = keyImpl;
262-
} else if (keyOrder == null) {
263-
keyOrder = sortOrder;
278+
if (currentSorterKey == null) {
279+
if (defaultSortField != null && defaultSortField.equals(key.getName())) {
280+
keyImpl.setSelected(true);
281+
sorter.setCurrentKey(key);
282+
}
283+
} else {
284+
if (currentSorterKey.getName().equals(key.getName())) {
285+
keyImpl.setSelected(true);
286+
sorter.setCurrentKey(key);
287+
keyOrder = currentSorterKey.getOrder();
288+
} else if (keyOrder == null) {
289+
keyOrder = currentSorterKey.getOrder();
290+
}
264291
}
265292
keyImpl.setOrder(keyOrder);
266293
cParams.put(Sorter.PARAMETER_SORT_ORDER, keyOrder.name().toLowerCase());
@@ -271,8 +298,6 @@ private SorterKey prepareSorting(SearchOptions searchOptions, SearchResultsSetIm
271298
oParams.put(Sorter.PARAMETER_SORT_ORDER, keyOrder.opposite().name().toLowerCase());
272299
keyImpl.setOppositeOrderParameters(oParams);
273300
}
274-
275-
return resultSorterKey;
276301
}
277302

278303
private String generateProductsQueryString(
@@ -348,10 +373,21 @@ private String generateProductsQueryString(
348373
sort.setPosition(sortEnum);
349374
} else {
350375
validSortKey = false;
351-
LOGGER.warn("Unknown sort key: " + sortKey);
376+
LOGGER.debug("Unrecognized sort key: " + sortKey);
352377
}
353378
if (validSortKey) {
354379
productArguments.sort(sort);
380+
} else {
381+
// handle sort keys not supported in the current magento-graphql library
382+
productArguments.sort(new ProductAttributeSortInput() {
383+
@Override
384+
public void appendTo(StringBuilder _queryBuilder) {
385+
_queryBuilder.append('{');
386+
_queryBuilder.append(sortKey + ":");
387+
_queryBuilder.append(sortEnum.toString());
388+
_queryBuilder.append('}');
389+
}
390+
});
355391
}
356392
}
357393
};
@@ -366,7 +402,8 @@ private String generateProductsQueryString(
366402
.value())
367403
.attributeCode()
368404
.count()
369-
.label());
405+
.label())
406+
.sortFields(s -> s.options(sf -> sf.value().label()).defaultValue());
370407

371408
return Operations.query(query -> query.products(searchArgs, queryArgs)).toString();
372409
}

0 commit comments

Comments
 (0)