diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 84c2e57..0000000 --- a/.babelrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "env": { - "test": { - "presets": [ - [ - "@babel/preset-env", - { - "targets": { - "node": "current" - } - } - ] - ] - } - } -} diff --git a/.lintstagedrc b/.lintstagedrc deleted file mode 100644 index ceb5029..0000000 --- a/.lintstagedrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "*.{js,jsx}": [ - "eslint --fix" - ], - "*.{ts,tsx}": [ - "eslint --fix" - ], - "*.{vue}": [ - "eslint --fix" - ], -} diff --git a/.yarnrc.yml b/.yarnrc.yml deleted file mode 100644 index 3186f3f..0000000 --- a/.yarnrc.yml +++ /dev/null @@ -1 +0,0 @@ -nodeLinker: node-modules diff --git a/README.md b/README.md index c5247cf..4542b3a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ # Magento 2.x theme + +[![All Contributors](https://img.shields.io/badge/all_contributors-21-orange.svg?style=flat-square)](#contributors-) + + + ### Vue Storefront 2 integration with Magento ### Requirements: @@ -91,6 +96,10 @@ Thanks go to these wonderful people 🙌:
Jonathan Ribas

💻
Ali Ghanei

💻
Maya Shavin

📖 +
Alexander Devitsky

💻 + + +
Diego Alba

💻 @@ -100,3 +109,4 @@ Thanks go to these wonderful people 🙌: This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! + diff --git a/app/router.scrollBehavior.js b/app/router.scrollBehavior.js index 4e0816d..64be137 100644 --- a/app/router.scrollBehavior.js +++ b/app/router.scrollBehavior.js @@ -1,6 +1,6 @@ -export default function (_to, _from, savedPosition) { +export default function scrollBehavior(_to, _from, savedPosition) { return savedPosition || { x: 0, y: 0, }; -}; +} diff --git a/components/AddToWishlist.vue b/components/AddToWishlist.vue index f7137a1..f9e88c2 100644 --- a/components/AddToWishlist.vue +++ b/components/AddToWishlist.vue @@ -17,7 +17,7 @@ - diff --git a/components/CartSidebar.vue b/components/CartSidebar.vue index 292189e..8cfbae8 100644 --- a/components/CartSidebar.vue +++ b/components/CartSidebar.vue @@ -202,15 +202,37 @@
+ + + + +
+ + @@ -238,7 +260,7 @@
- diff --git a/components/CurrencySelector.vue b/components/CurrencySelector.vue index f7c9c6e..ebbc34a 100644 --- a/components/CurrencySelector.vue +++ b/components/CurrencySelector.vue @@ -14,7 +14,7 @@ /> - diff --git a/components/TopBar/useTopBar.ts b/components/TopBar/useTopBar.ts index 6c5f113..37ee4ae 100644 --- a/components/TopBar/useTopBar.ts +++ b/components/TopBar/useTopBar.ts @@ -1,20 +1,25 @@ import { ref, onMounted } from '@nuxtjs/composition-api'; +import type { Currency, StoreConfig } from '~/modules/GraphQL/types'; -import useApi from '~/composables/useApi'; +import { useApi } from '~/composables/useApi'; import checkStoresAndCurrencyQuery from './checkStoresAndCurrency.gql'; +type StoresAndCurrencyQueryResponse = { + availableStores: Pick[], + currency: Pick, +}; + export const useTopBar = () => { const { query } = useApi(); - const hasStoresToSelect = ref(null); - const hasCurrencyToSelect = ref(null); + const hasStoresToSelect = ref(null); + const hasCurrencyToSelect = ref(null); onMounted(() => { - query(checkStoresAndCurrencyQuery) + query(checkStoresAndCurrencyQuery) + // eslint-disable-next-line promise/always-return .then((data) => { - hasStoresToSelect.value = data?.availableStores?.length; - hasCurrencyToSelect.value = data?.currency?.available_currency_codes?.length > 1; - - return data; + hasStoresToSelect.value = data.availableStores.length > 1; + hasCurrencyToSelect.value = data.currency.available_currency_codes.length > 1; }) .catch(() => { hasStoresToSelect.value = false; diff --git a/components/UserAddressDetails.vue b/components/UserAddressDetails.vue index beea8b0..1be1d10 100644 --- a/components/UserAddressDetails.vue +++ b/components/UserAddressDetails.vue @@ -37,15 +37,17 @@ - + diff --git a/modules/catalog/category/components/breadcrumbs/__tests__/CategoryBreadcrumbs.spec.ts b/modules/catalog/category/components/breadcrumbs/__tests__/CategoryBreadcrumbs.spec.ts new file mode 100644 index 0000000..98408f4 --- /dev/null +++ b/modules/catalog/category/components/breadcrumbs/__tests__/CategoryBreadcrumbs.spec.ts @@ -0,0 +1,67 @@ +import { mount } from '@vue/test-utils'; +import * as composables from '@nuxtjs/composition-api'; +import { + categoryAncestorsFirstLevelMock, + categoryAncestorsSecondLevelMock, + categoryAncestorsThirdLevelMock, + useTraverseCategoryMock, +} from '~/test-utils/mocks/useTraverseCategoryMock'; +import { useUiHelpers } from '~/composables'; +import { useTraverseCategory } from '~/modules/catalog/category/helpers/useTraverseCategory'; +import CategoryBreadcrumbs from '~/modules/catalog/category/components/breadcrumbs/CategoryBreadcrumbs.vue'; +import type { CategoryTree } from '~/modules/GraphQL/types'; + +jest.mock('@nuxtjs/composition-api', () => { + const originalComposables = jest.requireActual('@nuxtjs/composition-api'); + return { + ...originalComposables, + useContext: jest.fn(), + }; +}); +jest.mock('~/composables'); +jest.mock('~/modules/catalog/category/helpers/useTraverseCategory'); + +(composables.useContext as jest.Mock).mockReturnValue({ + localePath: jest.fn((path: string) => path), +}); +(useUiHelpers as jest.Mock).mockReturnValue({ + getCatLink: jest.fn( + (category: CategoryTree): string => `/c/${category.url_path}${category.url_suffix || ''}`, + ), +}); + +describe('CategoryBreadcrumbs.vue', () => { + it('Breadcrumbs should not render if there is only a first level category', () => { + (useTraverseCategory as jest.Mock).mockReturnValue(useTraverseCategoryMock(categoryAncestorsFirstLevelMock)); + const wrapper = mount(CategoryBreadcrumbs); + expect(wrapper).toBeDefined(); + expect(wrapper.find('li').exists()).toBeFalsy(); + }); + + it('Breadcrumbs must have one item for the second level category', () => { + (useTraverseCategory as jest.Mock).mockReturnValue(useTraverseCategoryMock(categoryAncestorsSecondLevelMock)); + const wrapper = mount(CategoryBreadcrumbs); + expect(wrapper.findAll('li')).toHaveLength(1); + }); + + it('Breadcrumbs must have two item for the third level category', () => { + (useTraverseCategory as jest.Mock).mockReturnValue(useTraverseCategoryMock(categoryAncestorsThirdLevelMock)); + const wrapper = mount(CategoryBreadcrumbs); + expect(wrapper.findAll('li')).toHaveLength(2); + }); + + it('Breadcrumb must have link that should have href attribute', () => { + (useTraverseCategory as jest.Mock).mockReturnValue(useTraverseCategoryMock(categoryAncestorsSecondLevelMock)); + const wrapper = mount(CategoryBreadcrumbs); + const link = wrapper.find('a'); + expect(link).toBeDefined(); + expect(link.attributes('href')).toBeDefined(); + }); + + it('The last breadcrumb must have \'current\' class', () => { + (useTraverseCategory as jest.Mock).mockReturnValue(useTraverseCategoryMock(categoryAncestorsThirdLevelMock)); + const wrapper = mount(CategoryBreadcrumbs); + const links = wrapper.findAll('a'); + expect(links.at(-1).classes()).toContain('current'); + }); +}); diff --git a/modules/catalog/category/components/cms/__tests__/CmsContent.spec.ts b/modules/catalog/category/components/cms/__tests__/CmsContent.spec.ts new file mode 100644 index 0000000..9203bc2 --- /dev/null +++ b/modules/catalog/category/components/cms/__tests__/CmsContent.spec.ts @@ -0,0 +1,6 @@ +import { cmsContentTest } from '~/test-utils/tests/cmsContent'; +import CmsContent from '~/modules/catalog/category/components/cms/CmsContent.vue'; + +describe('CmsContent.vue', () => { + cmsContentTest(CmsContent); +}); diff --git a/modules/catalog/category/components/cms/categoryContent.gql.ts b/modules/catalog/category/components/cms/categoryContent.gql.ts index 0cfc6c0..d0413f3 100644 --- a/modules/catalog/category/components/cms/categoryContent.gql.ts +++ b/modules/catalog/category/components/cms/categoryContent.gql.ts @@ -1,9 +1,9 @@ import { gql } from 'graphql-request'; export default gql` - query category($id: Int!) { - category(id: $id) { - id + query getCategoryContentData($filters: CategoryFilterInput) { + categoryList(filters: $filters) { + uid display_mode landing_page cms_block { diff --git a/modules/catalog/category/components/cms/useCategoryContent.ts b/modules/catalog/category/components/cms/useCategoryContent.ts index bc062e0..dbe633a 100644 --- a/modules/catalog/category/components/cms/useCategoryContent.ts +++ b/modules/catalog/category/components/cms/useCategoryContent.ts @@ -21,10 +21,11 @@ interface CmsContentInterface { export const useCategoryContent = () => { const { query } = useApi(); - const getContentData = async (id: number): Promise => { - const data = await query(categoryContentQuery, { id }); - const cmsBlock = data?.category?.cms_block ?? { cmsBlock: { content: '' } }; - const displayMode = data?.category?.display_mode ?? displayModesEnum.PRODUCTS; + const getContentData = async (uid: string): Promise => { + const data = await query(categoryContentQuery, { filters: { category_uid: { eq: uid } } }); + const category = data.categoryList[0] ?? {}; + const cmsBlock = category?.cms_block ?? { cmsBlock: { content: '' } }; + const displayMode = category?.display_mode ?? displayModesEnum.PRODUCTS; const isShowCms = displayMode === displayModesEnum.PAGE || displayMode === displayModesEnum.PRODUCTS_AND_PAGE; const isShowProducts = displayMode === displayModesEnum.PRODUCTS || displayMode === displayModesEnum.PRODUCTS_AND_PAGE; diff --git a/modules/catalog/category/components/filters/CategoryFilters.vue b/modules/catalog/category/components/filters/CategoryFilters.vue index 1e54535..6494e8a 100644 --- a/modules/catalog/category/components/filters/CategoryFilters.vue +++ b/modules/catalog/category/components/filters/CategoryFilters.vue @@ -3,7 +3,10 @@

{{ $t('Filters') }}

-
+
{{ $t('Apply filters') }} {{ $t('Clear all') }} @@ -67,6 +73,7 @@ :visible="isVisible" class="sidebar-filters smartphone-only" title="Filters" + data-testid="mobile-sidebar" @close="$emit('close')" > @@ -112,7 +119,7 @@ diff --git a/modules/catalog/category/components/views/CategoryProductList.vue b/modules/catalog/category/components/views/CategoryProductList.vue index e6f0634..ab8d168 100644 --- a/modules/catalog/category/components/views/CategoryProductList.vue +++ b/modules/catalog/category/components/views/CategoryProductList.vue @@ -42,6 +42,9 @@ value="white" /> + - + + diff --git a/modules/customer/pages/AddressesDetails.vue b/modules/customer/pages/MyAccount/AddressesDetails/AddressesDetails.vue similarity index 55% rename from modules/customer/pages/AddressesDetails.vue rename to modules/customer/pages/MyAccount/AddressesDetails/AddressesDetails.vue index fc7ed35..ee7d709 100644 --- a/modules/customer/pages/AddressesDetails.vue +++ b/modules/customer/pages/MyAccount/AddressesDetails/AddressesDetails.vue @@ -1,38 +1,52 @@ - + + diff --git a/modules/customer/pages/MyNewsletter.vue b/modules/customer/pages/MyAccount/MyNewsletter.vue similarity index 91% rename from modules/customer/pages/MyNewsletter.vue rename to modules/customer/pages/MyAccount/MyNewsletter.vue index 0154f53..ec7e355 100644 --- a/modules/customer/pages/MyNewsletter.vue +++ b/modules/customer/pages/MyAccount/MyNewsletter.vue @@ -1,8 +1,5 @@ - diff --git a/modules/customer/pages/MyAccount/OrderHistory/SingleOrder/OrderSummaryRow.vue b/modules/customer/pages/MyAccount/OrderHistory/SingleOrder/OrderSummaryRow.vue new file mode 100644 index 0000000..cd4f306 --- /dev/null +++ b/modules/customer/pages/MyAccount/OrderHistory/SingleOrder/OrderSummaryRow.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/modules/customer/pages/MyAccount/OrderHistory/SingleOrder/SingleOrder.vue b/modules/customer/pages/MyAccount/OrderHistory/SingleOrder/SingleOrder.vue new file mode 100644 index 0000000..0155d28 --- /dev/null +++ b/modules/customer/pages/MyAccount/OrderHistory/SingleOrder/SingleOrder.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/modules/customer/pages/MyAccount/useSidebarLinkGroups.ts b/modules/customer/pages/MyAccount/useSidebarLinkGroups.ts new file mode 100644 index 0000000..7f3032b --- /dev/null +++ b/modules/customer/pages/MyAccount/useSidebarLinkGroups.ts @@ -0,0 +1,62 @@ +import type { RawLocation } from 'vue-router'; +import { useRouter, useContext } from '@nuxtjs/composition-api'; +import { useUser } from '~/modules/customer/composables/useUser'; +import { useCart } from '~/modules/checkout/composables/useCart'; + +type LinkGroup = { title: string, items: LinkGroupItem[] }; +type LinkGroupItem = { label: string, link?: RawLocation, listeners?: Record (Promise | void)> }; + +export const useSidebarLinkGroups = () => { + const { localeRoute } = useContext(); + const { logout } = useUser(); + const { clear } = useCart(); + + const router = useRouter(); + const sidebarLinkGroups : LinkGroup[] = [ + { + title: 'Personal details', + items: [ + { + label: 'My profile', + link: { name: 'customer-my-profile' }, + }, + { + label: 'Addresses details', + link: { name: 'customer-addresses-details' }, + }, + { + label: 'My newsletter', + link: { name: 'customer-my-newsletter' }, + }, + { + label: 'My wishlist', + link: { name: 'customer-my-wishlist' }, + }, + ], + }, + { + title: 'Order details', + items: [ + { + label: 'Order history', + link: { name: 'customer-order-history' }, + }, + { + label: 'My reviews', + link: { name: 'customer-my-reviews' }, + }, + { + label: 'Log out', + listeners: { + click: async () => { + await Promise.all([logout({}), clear({})]); + await router.push(localeRoute({ name: 'home' })); + }, + }, + }, + ], + }, + ]; + + return { sidebarLinkGroups }; +}; diff --git a/stores/customer.ts b/modules/customer/stores/customer.ts similarity index 56% rename from stores/customer.ts rename to modules/customer/stores/customer.ts index d7331b9..f6ebbe9 100644 --- a/stores/customer.ts +++ b/modules/customer/stores/customer.ts @@ -1,19 +1,14 @@ import { defineStore } from 'pinia'; -import type { Cart } from '~/modules/GraphQL/types'; -import type { User } from '~/modules/customer/composables/useUser/useUser'; +import type { Customer } from '~/modules/GraphQL/types'; interface CustomerState { - cart: Cart, - user: User | null, + user: Customer | null, isLoggedIn: boolean, } export const useCustomerStore = defineStore('customer', { state: (): CustomerState => ({ user: null, - cart: { - id: '', is_virtual: false, total_quantity: 0, shipping_addresses: [], - }, isLoggedIn: false, }), actions: { diff --git a/modules/customer/types/form.ts b/modules/customer/types/form.ts new file mode 100644 index 0000000..5dd8422 --- /dev/null +++ b/modules/customer/types/form.ts @@ -0,0 +1,8 @@ +export type OnFormComplete = () => void; +export type OnFormError = (message: string) => void; + +export type SubmitEventPayload = { + form: TForm, + onComplete: OnFormComplete, + onError: OnFormError, +}; diff --git a/modules/magento/defaultConfig.ts b/modules/magento/defaultConfig.ts index c73080c..8a30f32 100644 --- a/modules/magento/defaultConfig.ts +++ b/modules/magento/defaultConfig.ts @@ -8,7 +8,7 @@ export const defaultConfig = { storeCookieName: 'vsf-store', messageCookieName: 'vsf-message', }, - locale: undefined, - country: undefined, - currency: undefined, + locale: undefined as undefined, + country: undefined as undefined, + currency: undefined as undefined, } as const; diff --git a/modules/magento/helpers/index.ts b/modules/magento/helpers/index.ts index d629284..361ee4c 100644 --- a/modules/magento/helpers/index.ts +++ b/modules/magento/helpers/index.ts @@ -1,6 +1,7 @@ +import { NuxtAppOptions } from '@nuxt/types'; import { defaultConfig } from '~/modules/magento/defaultConfig'; -export const getLocaleSettings = (app, moduleOptions, additionalProperties) => { +export const getLocaleSettings = (app: NuxtAppOptions, moduleOptions: Record, additionalProperties: any) => { const localeSettings = moduleOptions.cookies ? { currency: additionalProperties.state.getCurrency(), @@ -16,7 +17,8 @@ export const getLocaleSettings = (app, moduleOptions, additionalProperties) => { }; }; -export const mapConfigToSetupObject = ({ app, moduleOptions, additionalProperties = {} }) => ({ +export const mapConfigToSetupObject = ({ app, moduleOptions, additionalProperties = {} } : +{ app: NuxtAppOptions, moduleOptions: Record, additionalProperties: Record }) => ({ ...defaultConfig, ...moduleOptions, ...additionalProperties, diff --git a/modules/magento/index.ts b/modules/magento/index.ts index 015feef..8e1deaa 100644 --- a/modules/magento/index.ts +++ b/modules/magento/index.ts @@ -3,7 +3,7 @@ import { Module } from '@nuxt/types'; /* eslint-disable unicorn/prefer-module */ const path = require('path'); -const nuxtModule : Module = function (options) { +const nuxtModule : Module = function magentoModule(options) { this.extendBuild((config) => { // eslint-disable-next-line no-param-reassign config.resolve.alias['@vue-storefront/magento-api$'] = require.resolve('@vue-storefront/magento-api'); diff --git a/modules/magento/plugin.ts b/modules/magento/plugin.ts index c8916d7..65c15ec 100644 --- a/modules/magento/plugin.ts +++ b/modules/magento/plugin.ts @@ -7,7 +7,7 @@ import { defaultConfig } from '~/modules/magento/defaultConfig'; const moduleOptions = JSON.parse('<%= JSON.stringify(options) %>'); export default integrationPlugin((plugin) => { - const getCookieName = (property: string) : string => moduleOptions.cookies?.[property] ?? defaultConfig.cookies[property]; + const getCookieName = (property: keyof typeof defaultConfig['cookies']) : string => moduleOptions.cookies?.[property] ?? defaultConfig.cookies[property]; const cookieNames = { cart: getCookieName('cartCookieName'), diff --git a/modules/theme/index.ts b/modules/theme/index.ts deleted file mode 100644 index a5c7dab..0000000 --- a/modules/theme/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Module } from '@nuxt/types'; - -const nuxtModule : Module = function themeModule() {}; - -export default nuxtModule; diff --git a/modules/wishlist/components/EmptyWishlist.vue b/modules/wishlist/components/EmptyWishlist.vue new file mode 100644 index 0000000..c7e5245 --- /dev/null +++ b/modules/wishlist/components/EmptyWishlist.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/modules/theme/components/wishlist/WishlistSidebar.vue b/modules/wishlist/components/WishlistSidebar.vue similarity index 76% rename from modules/theme/components/wishlist/WishlistSidebar.vue rename to modules/wishlist/components/WishlistSidebar.vue index a81d847..dc355ed 100644 --- a/modules/theme/components/wishlist/WishlistSidebar.vue +++ b/modules/wishlist/components/WishlistSidebar.vue @@ -48,14 +48,7 @@ :regular-price=" $fc(getItemPrice(wishlistItem).regular) " - :link=" - localePath( - `/p/${wishlistItem.product.sku}${productGetters.getSlug( - wishlistItem.product, - wishlistItem.product.categories[0] - )}` - ) - " + :link="getItemLink(wishlistItem)" :special-price="getItemPrice(wishlistItem).special && $fc(getItemPrice(wishlistItem).special)" :stock="99999" class="collected-product" @@ -65,16 +58,7 @@