Skip to content

Commit

Permalink
feat: extending available filter api #1043 (#1068)
Browse files Browse the repository at this point in the history
* feat(helpers): change entity filter extracting

* feat(composables): change available filters source

* feat(default-theme): change the available source from page resolver to product-listing slot

* feat(helpers): handle other filter types and add extra data

* test(composables): listing filter types handling

* feat(helpers): aggregation to filters converter

* feat(composables): fetch the entire aggregations for given listing

* feat(composables): prevent resetting products on visiting product listing
  • Loading branch information
mkucmus authored Sep 7, 2020
1 parent 5f9bbc2 commit aa7c08b
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 43 deletions.
65 changes: 62 additions & 3 deletions packages/composables/__tests__/useProductListing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
EqualsFilter,
} from "@shopware-pwa/commons/interfaces/search/SearchFilter";

const mockedGetPage = shopwareClient as jest.Mocked<typeof shopwareClient>;
const mockedApiClient = shopwareClient as jest.Mocked<typeof shopwareClient>;

describe("Composables - useProductListing", () => {
const statePage: Ref<Object | null> = ref(null);
Expand Down Expand Up @@ -165,7 +165,9 @@ describe("Composables - useProductListing", () => {

//
it("should return default total and empty product listing when page resolver fails", async () => {
mockedGetPage.getCategoryProductsListing.mockResolvedValueOnce({} as any);
mockedApiClient.getCategoryProductsListing.mockResolvedValueOnce(
{} as any
);

const { products, search, pagination } = useProductListing(
rootContextMock
Expand All @@ -180,7 +182,7 @@ describe("Composables - useProductListing", () => {
});

it("should return products if exist", async () => {
mockedGetPage.getCategoryProductsListing.mockResolvedValueOnce({
mockedApiClient.getCategoryProductsListing.mockResolvedValueOnce({
elements: [
{
id: "123456",
Expand Down Expand Up @@ -296,5 +298,62 @@ describe("Composables - useProductListing", () => {
expect(productsTotal.value).toBeFalsy();
});
});

describe("availableFilters", () => {
it("should parse aggregations if the initial listing has any", () => {
const { availableFilters } = useProductListing(rootContextMock, {
aggregations: {
manufacturer: {
entities: [
{
name: "Dicki, Gerhold and Witting",
translated: {
name: "Dicki, Gerhold and Witting",
},
},
],
},
},
} as any);

expect(availableFilters.value).toStrictEqual([
{
name: "manufacturer",
options: [
{
color: undefined,
label: "Dicki, Gerhold and Witting",
value: undefined,
name: "Dicki, Gerhold and Witting",
translated: {
name: "Dicki, Gerhold and Witting",
},
},
],
type: "entity",
},
]);
});
it("should make another call if the response is narrowed down", async () => {
mockedApiClient.getCategoryProductsListing.mockResolvedValue({
aggregations: {
color: {
elements: [],
},
},
currentFilters: {
manufacturer: [{ name: "shopware ag" }],
properties: [{ name: "color" }],
},
} as any);

const { availableFilters, search } = useProductListing(
rootContextMock as any
);
await search();
expect(mockedApiClient.getCategoryProductsListing).toBeCalledTimes(2);
expect(availableFilters.value).toStrictEqual([]);
});
});
});
});
4 changes: 4 additions & 0 deletions packages/composables/__tests__/useProductSearch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ describe("Composables - useProductSearch", () => {
name: "manufacturer",
options: [
{
translated: {
name: "DivanteLtd",
},
id: "123456",
color: undefined,
label: "DivanteLtd",
value: "123456",
Expand Down
47 changes: 43 additions & 4 deletions packages/composables/src/hooks/useProductListing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getFilterSearchCriteria,
getSortingSearchCriteria,
exportUrlQuery,
getListingAvailableFilters,
} from "@shopware-pwa/helpers";
import {
useCms,
Expand Down Expand Up @@ -45,6 +46,7 @@ const sharedPagination = Vue.observable({

const sharedListing = Vue.observable({
products: [],
availableFilters: [],
} as any);

const selectedCriteria = Vue.observable({
Expand Down Expand Up @@ -79,10 +81,26 @@ export const useProductListing = (
const localListing = reactive(sharedListing);
const localCriteria = reactive(selectedCriteria);
const localPagination = reactive(sharedPagination);
const productListingResult: Ref<ProductListingResult | null> = ref(null);

// check whether the search result has some filters applied
/* istanbul ignore next */
const isBaseRequest = () =>
!productListingResult.value?.currentFilters?.manufacturer?.length &&
!productListingResult.value?.currentFilters?.properties?.length;

if (initialListing?.elements && initialListing.elements.length) {
sharedListing.products = initialListing.elements;
}

sharedListing.products = initialListing?.elements || [];
selectedCriteria.sorting = activeSorting.value;

if (initialListing?.aggregations) {
sharedListing.availableFilters = getListingAvailableFilters(
initialListing.aggregations
);
}

const resetFilters = () => {
selectedCriteria.filters = {};
selectedCriteria.manufacturer = [];
Expand Down Expand Up @@ -154,13 +172,32 @@ export const useProductListing = (
if (typeof history !== "undefined")
history.replaceState({}, null as any, location.pathname + "?" + search);

const result = await getCategoryProductsListing(
productListingResult.value = await getCategoryProductsListing(
categoryId.value,
searchCriteria,
apiInstance
);
sharedPagination.total = (result && result.total) || 0;
sharedListing.products = result?.elements || [];
sharedPagination.total =
(productListingResult.value && productListingResult.value.total) || 0;
sharedListing.products = productListingResult.value?.elements || [];

// base response has always all the aggregations
if (isBaseRequest()) {
sharedListing.availableFilters = getListingAvailableFilters(
productListingResult.value?.aggregations
);
} else {
// get the aggregations without narrowing down the results, so another api call is needed (using post-aggregation may fix it)
const productListingBaseResult = await getCategoryProductsListing(
categoryId.value,
{ pagination: { limit: 1 } },
apiInstance
);
sharedListing.availableFilters = getListingAvailableFilters(
productListingBaseResult.aggregations
);
}

initialListing = undefined;
loading.value = false;
};
Expand Down Expand Up @@ -194,6 +231,7 @@ export const useProductListing = (
]);
const selectedFilters = computed(() => localCriteria.filters);
const selectedSorting = computed(() => localCriteria.sorting);
const availableFilters = computed(() => localListing.availableFilters);

return {
search,
Expand All @@ -210,5 +248,6 @@ export const useProductListing = (
changeSorting,
selectedSorting,
categoryId,
availableFilters,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export default {
},
},
setup(props, { root }) {
const { availableFilters, availableSorting } = useCategoryFilters(root)
const { availableSorting } = useCategoryFilters(root)
const {
toggleFilter,
changeSorting,
Expand All @@ -136,6 +136,8 @@ export default {
selectedEntityFilters,
resetFilters,
productsTotal,
availableFilters,
changePagination,
} = useProductListing(root, null)
const { isOpen: isListView, switchState: switchToListView } = useUIState(
Expand All @@ -156,6 +158,7 @@ export default {
switchToListView,
availableFilters,
availableSorting,
changePagination,
}
},
data() {
Expand Down Expand Up @@ -201,11 +204,11 @@ export default {
},
async clearAllFilters() {
this.resetFilters()
await this.search()
await this.changePagination(1)
this.isFilterSidebarOpen = false
},
async submitFilters() {
await this.search()
await this.changePagination(1)
this.isFilterSidebarOpen = false
},
getSortLabel(sorting) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ export default {
},
},
setup({ content }, { root }) {
const { search } = useProductSearch(root)
const listing = content.data.listing || []
const {
products,
Expand Down
4 changes: 2 additions & 2 deletions packages/default-theme/components/listing/types/range.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default {
},
setup({ filter, selectedValues }, { emit }) {
const min = computed({
get: () => selectedValues.gt || filter.options.min,
get: () => selectedValues.gt || filter.min,
set: (value) =>
emit("toggle-filter-value", {
type: "range",
Expand All @@ -53,7 +53,7 @@ export default {
}),
})
const max = computed({
get: () => selectedValues.lt || filter.options.max,
get: () => selectedValues.lt || filter.max,
set: (value) =>
emit("toggle-filter-value", {
type: "range",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,56 +34,105 @@ describe("Shopware helpers - getListingAvailableFilters", () => {
options: [
{
color: undefined,
id: "2cc7db79c4214b448df87fb6642ebc57",
label: "Rolfson-Schuppe",
value: "2cc7db79c4214b448df87fb6642ebc57",
translated: {
name: "Rolfson-Schuppe",
},
},
],
type: "entity",
},
]);
});
it("should return transformed filter if properties property is resolved", () => {
const aggregations = {

it("should return an empty array if resolved aggregation has no values", () => {
const aggregation = {
manufacturer: {},
} as any;

const result = getListingAvailableFilters(aggregation);
expect(result).toEqual([]);
});

it("should treat properties aggregation differently", () => {
const aggregation = {
properties: {
entities: [
{
name: "color",
options: [
{
name: "mediumblue",
colorHexCode: "#ffc",
id: "white",
name: "white",
colorHexCode: "#fff",
translated: {
name: "mediumblue",
name: "white",
},
id: "e4af09b1c8ac463ca7a61fac99e71226",
},
],
id: "8c6aadfc5ed84a7db0b54935e3f60403",
},
],
},
} as any;
const result = getListingAvailableFilters(aggregations);
expect(result).toEqual([

const result = getListingAvailableFilters(aggregation);
expect(result).toStrictEqual([
{
name: "color",
options: [
{
color: "#ffc",
label: "mediumblue",
value: "e4af09b1c8ac463ca7a61fac99e71226",
colorHexCode: "#fff",
color: "#fff",
id: "white",
label: "white",
name: "white",
translated: { name: "white" },
value: "white",
},
],
type: "entity",
},
]);
});
it("should return an empty array if resolved aggregation has no values", () => {

it("should handle max filter type", () => {
const aggregation = {
manufacturer: {},
"shipping-free": {
max: "1",
},
} as any;

const result = getListingAvailableFilters(aggregation);
expect(result).toEqual([]);
expect(result).toEqual([
{
max: "1",
name: "shipping-free",
type: "max",
},
]);
});
it("should handle range filter type", () => {
const aggregation = {
price: {
min: "50",
max: "974",
avg: 770.5172413793103,
sum: 22345,
},
} as any;

const result = getListingAvailableFilters(aggregation);
expect(result).toEqual([
{
max: "974",
name: "price",
type: "range",
min: "50",
avg: 770.5172413793103,
sum: 22345,
},
]);
});
});
2 changes: 1 addition & 1 deletion packages/helpers/src/listing/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const getFilterSearchCriteria = (selectedFilters: any): any[] => {
try {
filters.push(extractFilter(filterName, selectedFilters[filterName]));
} catch (error) {
console.error("helpers:getFilterSearchCriteria:extractFilter", error);
console.warn("helpers:getFilterSearchCriteria:extractFilter", error);
}
}

Expand Down
Loading

1 comment on commit aa7c08b

@vercel
Copy link

@vercel vercel bot commented on aa7c08b Sep 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.