diff --git a/packages/theme/components/BottomNavigation.vue b/packages/theme/components/BottomNavigation.vue
index d3511a36c..d2e0369b7 100644
--- a/packages/theme/components/BottomNavigation.vue
+++ b/packages/theme/components/BottomNavigation.vue
@@ -18,7 +18,7 @@
-
+
@@ -80,14 +80,17 @@
import { SfBottomNavigation, SfCircleIcon } from '@storefront-ui/vue';
import { defineComponent, useRouter, useContext } from '@nuxtjs/composition-api';
import { useUiState, useUser } from '~/composables';
-import MobileMenuSidebar from '~/components/MobileMenuSidebar.vue';
import SvgImage from '~/components/General/SvgImage.vue';
+import { useCategoryStore } from '~/stores/category';
+import { useApi } from '~/composables/useApi';
+
+const MobileCategorySidebar = () => import('~/modules/catalog/category/components/sidebar/MobileCategorySidebar/MobileCategorySidebar.vue');
export default defineComponent({
components: {
SfBottomNavigation,
SfCircleIcon,
- MobileMenuSidebar,
+ MobileCategorySidebar,
SvgImage,
},
setup() {
@@ -101,6 +104,7 @@ export default defineComponent({
const { isAuthenticated } = useUser();
const router = useRouter();
const { app } = useContext();
+ const api = useApi();
const handleAccountClick = async () => {
if (isAuthenticated.value) {
await router.push(`${app.localePath('/my-account')}`);
@@ -109,12 +113,21 @@ export default defineComponent({
}
};
+ const loadCategoryMenu = async () => {
+ const categories = useCategoryStore(api);
+ if (categories.categories === null) {
+ await categories.load();
+ }
+ toggleMobileMenu();
+ };
+
return {
isAuthenticated,
isMobileMenuOpen,
toggleWishlistSidebar,
toggleCartSidebar,
toggleMobileMenu,
+ loadCategoryMenu,
handleAccountClick,
app,
};
diff --git a/packages/theme/lang/de.js b/packages/theme/lang/de.js
index 588305771..00acd35c7 100644
--- a/packages/theme/lang/de.js
+++ b/packages/theme/lang/de.js
@@ -253,4 +253,5 @@ export default {
"There was some error while trying to fetch shipping methods. We are sorry, please try with different shipping details.": "Beim Versuch, Versandarten abzurufen, ist ein Fehler aufgetreten. Es tut uns leid, bitte versuchen Sie es mit anderen Versanddetails oder später.",
"There was some error while trying to select this shipping method. We are sorry, please try with different shipping method.": "Beim Versuch, diese Versandart auszuwählen, ist ein Fehler aufgetreten. Es tut uns leid, bitte versuchen Sie es mit einer anderen Versandart.",
"We can't find products matching the selection.":"Wir können keine Produkte finden, die der Auswahl entsprechen.",
+ "AllProductsFromCategory": "Alle {categoryName}"
};
diff --git a/packages/theme/lang/en.js b/packages/theme/lang/en.js
index 653e84d9e..05335d853 100644
--- a/packages/theme/lang/en.js
+++ b/packages/theme/lang/en.js
@@ -251,4 +251,5 @@ export default {
"There was some error while trying to fetch shipping methods. We are sorry, please try with different shipping details.": "There was some error while trying to fetch shipping methods. We are sorry, please try with different shipping details.",
"There was some error while trying to select this shipping method. We are sorry, please try with different shipping method.": "There was some error while trying to select this shipping method. We are sorry, please try with different shipping method.",
"We can't find products matching the selection.":"We can't find products matching the selection.",
+ "AllProductsFromCategory": "All {categoryName}"
};
diff --git a/packages/theme/modules/catalog/category/components/sidebar/CategorySidebar.vue b/packages/theme/modules/catalog/category/components/sidebar/CategorySidebar.vue
index b06b3f881..801ba9e61 100644
--- a/packages/theme/modules/catalog/category/components/sidebar/CategorySidebar.vue
+++ b/packages/theme/modules/catalog/category/components/sidebar/CategorySidebar.vue
@@ -4,17 +4,17 @@
height="500px"
>
@@ -54,23 +54,25 @@
-
+
+
diff --git a/packages/theme/modules/catalog/category/components/sidebar/MobileCategorySidebar/__tests__/logic.spec.ts b/packages/theme/modules/catalog/category/components/sidebar/MobileCategorySidebar/__tests__/logic.spec.ts
new file mode 100644
index 000000000..a15ec57c0
--- /dev/null
+++ b/packages/theme/modules/catalog/category/components/sidebar/MobileCategorySidebar/__tests__/logic.spec.ts
@@ -0,0 +1,41 @@
+import { CategoryTreeInterface } from '~/modules/catalog/category/types';
+import { useMobileCategoryTree } from '../logic';
+
+const createCategoryItem = (label: string): CategoryTreeInterface => ({
+ label, items: [], isCurrent: false, count: 10,
+});
+
+describe('categoryTreeLogic', () => {
+ it('can go down down a category', () => {
+ const itemFirst = createCategoryItem('Itemless1');
+ const { history, current, onGoCategoryDown } = useMobileCategoryTree();
+ onGoCategoryDown(itemFirst);
+ expect(current.value.label).toBe(itemFirst.label);
+ expect(history.value).toHaveLength(1);
+ });
+
+ it('can go up a category', () => {
+ const itemFirst = createCategoryItem('Itemless1');
+ const itemSecond = createCategoryItem('Itemless2');
+
+ const { current, onGoCategoryDown, onGoCategoryUp } = useMobileCategoryTree();
+
+ onGoCategoryDown(itemFirst);
+ onGoCategoryDown(itemSecond);
+ onGoCategoryUp();
+
+ expect(current.value.label).toBe(itemFirst.label);
+ });
+
+ it('current item is last in history', () => {
+ const itemFirst = createCategoryItem('Itemless1');
+ const itemSecond = createCategoryItem('Itemless2');
+ const { history, current, onGoCategoryDown } = useMobileCategoryTree();
+
+ onGoCategoryDown(itemFirst);
+ onGoCategoryDown(itemSecond);
+
+ expect(current.value.label).toBe(itemSecond.label);
+ expect(history.value).toHaveLength(2);
+ });
+});
diff --git a/packages/theme/modules/catalog/category/components/sidebar/MobileCategorySidebar/logic.ts b/packages/theme/modules/catalog/category/components/sidebar/MobileCategorySidebar/logic.ts
new file mode 100644
index 000000000..b2faa9056
--- /dev/null
+++ b/packages/theme/modules/catalog/category/components/sidebar/MobileCategorySidebar/logic.ts
@@ -0,0 +1,20 @@
+import { computed, ref } from '@nuxtjs/composition-api';
+import { CategoryTreeInterface } from '~/modules/catalog/category/types';
+
+export const useMobileCategoryTree = (initialHistory: CategoryTreeInterface[] = []) => {
+ const history = ref(initialHistory);
+ const current = computed(() => history.value.at(-1) ?? null);
+ const currentItems = computed(() => current.value?.items);
+ const onGoCategoryDown = (category: CategoryTreeInterface) => {
+ history.value.push(category);
+ };
+ const onGoCategoryUp = () => history.value.pop();
+
+ return {
+ history,
+ current,
+ currentItems,
+ onGoCategoryUp,
+ onGoCategoryDown,
+ };
+};
diff --git a/packages/theme/modules/catalog/category/components/sidebar/command/categoryList.gql.ts b/packages/theme/modules/catalog/category/components/sidebar/command/categoryList.gql.ts
index d0f35b66c..2360a5f38 100644
--- a/packages/theme/modules/catalog/category/components/sidebar/command/categoryList.gql.ts
+++ b/packages/theme/modules/catalog/category/components/sidebar/command/categoryList.gql.ts
@@ -1,40 +1,33 @@
import { gql } from 'graphql-request';
+const fragmentCategory = gql`
+ fragment CategoryFields on CategoryTree {
+ is_anchor
+ name
+ position
+ product_count
+ uid
+ url_path
+ url_suffix
+ }
+`;
+
export default gql`
query categoryList {
categories {
items {
+ ...CategoryFields
children {
- is_anchor
- name
- position
- product_count
- uid
- url_path
- url_suffix
+ ...CategoryFields
children {
- is_anchor
- name
- position
- product_count
- uid
- url_path
- url_suffix
+ ...CategoryFields
children {
- is_anchor
- name
- position
- product_count
- uid
- url_path
- url_suffix
+ ...CategoryFields
}
}
}
- product_count
- name
- uid
}
}
}
+ ${fragmentCategory}
`;
diff --git a/packages/theme/modules/catalog/category/components/sidebar/useSidebar.ts b/packages/theme/modules/catalog/category/components/sidebar/useSidebar.ts
deleted file mode 100644
index 80507729b..000000000
--- a/packages/theme/modules/catalog/category/components/sidebar/useSidebar.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import findDeep from 'deepdash/findDeep';
-import { getCategoryTree } from '~/modules/catalog/category/getters/categoryGetters';
-import { CategoryTreeInterface } from '~/modules/catalog/category/types';
-import loadCategoriesCommand from '~/modules/catalog/category/components/sidebar/command/loadCategoriesCommand';
-
-export const useSidebar = () => {
- const loadCategoryTree = async (customQuery?: string): Promise => {
- const categoryTree = await loadCategoriesCommand.execute(customQuery);
-
- return getCategoryTree(categoryTree?.[0]);
- };
-
- const findActiveCategory = (categoryTree: CategoryTreeInterface, path: string = '') => {
- const categories = categoryTree?.items ?? false;
- if (!categories) {
- return '';
- }
-
- let categoryLabel = '';
- const parent = findDeep(categories, (value: string, key, parentValue, _deepCtx) => {
- if (key === 'slug' && path.includes(value)) {
- // eslint-disable-next-line no-underscore-dangle
- categoryLabel = _deepCtx.obj[_deepCtx._item.path[0]].label;
- }
-
- return key === 'slug' && path.includes(value);
- });
-
- return categoryLabel || parent?.category?.label || categories[0]?.label;
- };
-
- return {
- loadCategoryTree,
- findActiveCategory,
- };
-};
-
-export default useSidebar;
diff --git a/packages/theme/modules/catalog/category/getters/categoryGetters.ts b/packages/theme/modules/catalog/category/getters/categoryGetters.ts
index 0bbe68a2a..8d21b8460 100644
--- a/packages/theme/modules/catalog/category/getters/categoryGetters.ts
+++ b/packages/theme/modules/catalog/category/getters/categoryGetters.ts
@@ -1,7 +1,7 @@
import {
CategoryGetters, CategoryTreeInterface, AgnosticBreadcrumb, Category,
} from '~/modules/catalog/category/types';
-import { buildCategoryTree } from '~/modules/catalog/category/helpers/buildCategoryTree';
+import { buildCategoryTree } from '~/modules/catalog/category/helpers';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const getTree = (category: Category): CategoryTreeInterface | null => {
diff --git a/packages/theme/modules/catalog/category/helpers/buildCategoryTree.ts b/packages/theme/modules/catalog/category/helpers/buildCategoryTree.ts
deleted file mode 100644
index 6b69268eb..000000000
--- a/packages/theme/modules/catalog/category/helpers/buildCategoryTree.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { CategoryTreeInterface } from '~/modules/catalog/category/types';
-
-export const buildCategoryTree = (rootCategory: any, currentCategory: string, withProducts = false): CategoryTreeInterface => {
- const hasChildren = Array.isArray(rootCategory.children) && rootCategory.children.length > 0;
- const isCurrent = rootCategory.uid === currentCategory;
- const label = rootCategory.name;
- const slug = `/${rootCategory.url_path}${rootCategory.url_suffix || ''}`;
-
- const childrenUid = hasChildren
- ? rootCategory
- .children
- .reduce((acc, curr) => [...acc, curr.uid], [])
- : [];
-
- const childProductCount = hasChildren
- ? rootCategory
- .children
- .reduce((acc, curr) => acc + curr.product_count, 0)
- : 0;
-
- const items = hasChildren
- ? rootCategory
- .children
- .filter((c) => (withProducts ? c.product_count > 0 : true))
- .map((c) => buildCategoryTree(c, currentCategory))
- : [];
-
- return {
- label,
- slug,
- uid: [rootCategory.uid, ...childrenUid],
- items: items.filter((c) => c.count > 0),
- count: childProductCount || rootCategory.product_count,
- isCurrent,
- };
-};
diff --git a/packages/theme/modules/catalog/category/helpers/index.ts b/packages/theme/modules/catalog/category/helpers/index.ts
new file mode 100644
index 000000000..c12492046
--- /dev/null
+++ b/packages/theme/modules/catalog/category/helpers/index.ts
@@ -0,0 +1,65 @@
+import findDeep from 'deepdash/findDeep';
+import { CategoryTreeInterface } from '~/modules/catalog/category/types';
+
+export const buildCategoryTree = (rootCategory: any, currentCategory: string, withProducts = false): CategoryTreeInterface => {
+ const hasChildren = Array.isArray(rootCategory.children) && rootCategory.children.length > 0;
+ const isCurrent = rootCategory.uid === currentCategory;
+ const label = rootCategory.name;
+ const slug = `/${rootCategory.url_path}${rootCategory.url_suffix || ''}`;
+
+ const childrenUid = hasChildren
+ ? rootCategory
+ .children
+ .reduce((acc, curr) => [...acc, curr.uid], [])
+ : [];
+
+ const childProductCount = hasChildren
+ ? rootCategory
+ .children
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
+ .reduce((acc, curr) => acc + curr.product_count, 0)
+ : 0;
+
+ const items = hasChildren
+ ? rootCategory
+ .children
+ .filter((c) => (withProducts ? c.product_count > 0 : true))
+ .map((c) => buildCategoryTree(c, currentCategory))
+ : [];
+
+ return {
+ label,
+ slug,
+ uid: [rootCategory.uid, ...childrenUid],
+ items: items.filter((c) => c.count > 0),
+ count: childProductCount || rootCategory.product_count,
+ isCurrent,
+ };
+};
+
+export const findActiveCategory = (categoryTree: CategoryTreeInterface, slugToFind: string): CategoryTreeInterface | null => {
+ const categories = categoryTree?.items;
+ return categories
+ ? findDeep(categories, (value: unknown, key: string) => key === 'slug' && slugToFind.includes(value as string))?.parent ?? null
+ : null;
+};
+
+/*
+ * Finds each category preceding `toFind` in the category tree
+ * Uses a modified Breadth First Search algorithm to tell you if the toFind node exists how to get to it
+ * @returns Flat array of categories ([level1Obj, level2Obj, level3Obj])
+ */
+export const findCategoryAncestors = (node: CategoryTreeInterface, toFind: CategoryTreeInterface, startingArray = [])
+: CategoryTreeInterface[] | null => {
+ if (node === toFind) {
+ return startingArray;
+ }
+ if (node.items && node.items.length > 0) {
+ for (let i = 0; i < node.items.length; i += 1) {
+ const subnode = node.items[i];
+ const result = findCategoryAncestors(subnode, toFind, [...startingArray, subnode]);
+ if (result) return result;
+ }
+ }
+ return null;
+};
diff --git a/packages/theme/stores/category.ts b/packages/theme/stores/category.ts
new file mode 100644
index 000000000..f8ed96542
--- /dev/null
+++ b/packages/theme/stores/category.ts
@@ -0,0 +1,33 @@
+import { defineStore } from 'pinia';
+import categoryListGql from '~/modules/catalog/category/components/sidebar/command/categoryList.gql';
+import { buildCategoryTree } from '~/modules/catalog/category/helpers';
+import type { useApi } from '~/composables/useApi';
+import type { Pinia } from 'pinia';
+import { CategoryTreeInterface } from '../modules/catalog/category/types';
+
+interface CategoryState {
+ rawCategories: { categories: { items: CategoryTreeInterface[] } } | null
+}
+
+export const useCategoryStore = (api: ReturnType, $pinia?: Pinia) => defineStore('category', {
+ state: (): CategoryState => ({
+ rawCategories: null,
+ }),
+ actions: {
+ async load() {
+ this.rawCategories = await api.query(categoryListGql);
+ },
+ },
+ getters: {
+ categories(state) {
+ if (state.rawCategories === null) {
+ return null;
+ }
+ const rootCategory = state.rawCategories?.categories.items[0];
+ const shouldIncludeProductCounts = true;
+ const currentCategory = '';
+ const agnosticCategoryTree = buildCategoryTree(rootCategory, currentCategory, shouldIncludeProductCounts);
+ return agnosticCategoryTree;
+ },
+ },
+})($pinia);
diff --git a/packages/theme/tests/e2e/tsconfig.json b/packages/theme/tests/e2e/tsconfig.json
new file mode 100644
index 000000000..f87a578b6
--- /dev/null
+++ b/packages/theme/tests/e2e/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["es5", "dom"],
+ "types": ["cypress"]
+ },
+ "include": ["**/*.ts"]
+}
diff --git a/packages/theme/tsconfig.json b/packages/theme/tsconfig.json
index 659ec37eb..ff682f2c7 100644
--- a/packages/theme/tsconfig.json
+++ b/packages/theme/tsconfig.json
@@ -3,7 +3,7 @@
"declaration": true,
"target": "ES2019",
"module": "ES2015",
- "moduleResolution": "Node",
+ "moduleResolution": "node",
"lib": [
"ES2019",
"DOM"
@@ -23,18 +23,18 @@
]
},
"types": [
- "@types/node",
+ "node",
"@nuxt/types",
"nuxt-i18n",
- "@nuxt/image"
+ "@nuxt/image",
+ "jest"
],
+ "typeRoots": ["../../node_modules/@types"],
"resolveJsonModule": true,
"rootDir": "./",
"declarationDir": "./lib",
"importHelpers": true,
"allowSyntheticDefaultImports": true
},
- "exclude": [
- "node_modules"
- ]
+ "exclude": ["tests/e2e/**"]
}