diff --git a/packages/composables/src/composables/useReview/index.ts b/packages/composables/src/composables/useReview/index.ts index 2ee3abe5d..f020125b0 100644 --- a/packages/composables/src/composables/useReview/index.ts +++ b/packages/composables/src/composables/useReview/index.ts @@ -1,4 +1,8 @@ /* istanbul ignore file */ + +/** + * @deprecated since version 1.0.0 + */ import { ComposableFunctionArgs, Context, diff --git a/packages/composables/src/factories/useReviewFactory.ts b/packages/composables/src/factories/useReviewFactory.ts index 09d6310eb..bf4896af5 100644 --- a/packages/composables/src/factories/useReviewFactory.ts +++ b/packages/composables/src/factories/useReviewFactory.ts @@ -1,3 +1,6 @@ +/** + * @deprecated since version 1.0.0 + */ import { Ref, computed } from '@vue/composition-api'; import { ComposableFunctionArgs, diff --git a/packages/theme/components/ProductAddReviewForm.vue b/packages/theme/components/ProductAddReviewForm.vue index 393ac2ba1..4d8156747 100644 --- a/packages/theme/components/ProductAddReviewForm.vue +++ b/packages/theme/components/ProductAddReviewForm.vue @@ -116,14 +116,13 @@ import { useRoute, useContext, } from '@nuxtjs/composition-api'; -import { useReview } from '@vue-storefront/magento'; import { extend, ValidationObserver, ValidationProvider } from 'vee-validate'; import { min, oneOf, required } from 'vee-validate/dist/rules'; import { SfInput, SfButton, SfSelect, SfTextarea, } from '@storefront-ui/vue'; import { reviewGetters, userGetters } from '~/getters'; -import { useUser } from '~/composables'; +import { useUser, useReview } from '~/composables'; extend('required', { ...required, @@ -168,16 +167,16 @@ export default defineComponent({ typeof $recaptcha !== 'undefined' && $config.isRecaptcha, ); const { - loading, loadReviewMetadata, metadata, error, - } = useReview( - `productReviews-${id}`, - ); + loading, loadReviewMetadata, error, + } = useReview(); const { isAuthenticated, user } = useUser(); const reviewSent = ref(false); const form = ref(BASE_FORM(id)); + const metadata = ref([]); + const ratingMetadata = computed(() => reviewGetters.getReviewMetadata([...metadata.value])); const formSubmitValue = computed(() => { @@ -234,7 +233,7 @@ export default defineComponent({ }; onBeforeMount(async () => { - await loadReviewMetadata(); + metadata.value = await loadReviewMetadata(); }); return { diff --git a/packages/theme/components/__tests__/ProductAddReviewForm.spec.js b/packages/theme/components/__tests__/ProductAddReviewForm.spec.js index 9269b2ea2..ed057e20b 100644 --- a/packages/theme/components/__tests__/ProductAddReviewForm.spec.js +++ b/packages/theme/components/__tests__/ProductAddReviewForm.spec.js @@ -2,13 +2,13 @@ import { waitFor } from '@testing-library/vue'; import userEvent from '@testing-library/user-event'; import { useRoute } from '@nuxtjs/composition-api'; -import { useUser, useReview } from '@vue-storefront/magento'; import { render, useUserMock, useReviewMock, } from '~/test-utils'; +import { useReview, useUser } from '~/composables'; import ProductAddReviewForm from '../ProductAddReviewForm'; jest.mock('@vue-storefront/magento', () => { @@ -16,7 +16,6 @@ jest.mock('@vue-storefront/magento', () => { return { ...originalModule, useUser: jest.fn(), - useReview: jest.fn(), }; }); @@ -30,6 +29,14 @@ jest.mock('@nuxtjs/composition-api', () => { }; }); +jest.mock('~/composables/useReview', () => { + const originalModule = jest.requireActual('~/composables/useReview'); + return { + ...originalModule, + useReview: jest.fn(), + }; +}); + describe.skip('', () => { it('Form fields are rendered and validated', async () => { useUser.mockReturnValue(useUserMock()); diff --git a/packages/theme/composables/context.d.ts b/packages/theme/composables/context.d.ts new file mode 100644 index 000000000..fa267d60a --- /dev/null +++ b/packages/theme/composables/context.d.ts @@ -0,0 +1,8 @@ +import { ApiClientMethods, IntegrationContext } from '@vue-storefront/core'; +import { ClientInstance, Config, MagentoApiMethods } from '@vue-storefront/magento-api'; + +declare module '@vue-storefront/core' { + export interface Context { + $magento: IntegrationContext>; + } +} diff --git a/packages/theme/composables/index.ts b/packages/theme/composables/index.ts index 90bb0f03a..57859973e 100644 --- a/packages/theme/composables/index.ts +++ b/packages/theme/composables/index.ts @@ -16,6 +16,7 @@ export { default as useCart } from './useCart'; export { default as useContent } from './useContent'; export { default as useCategorySearch } from './useCategorySearch'; export { default as useProduct } from './useProduct'; +export { default as useReview } from './useReview'; export { default as useShipping } from './useShipping'; export { default as useShippingProvider } from './useShippingProvider'; export { default as useRelatedProducts } from './useRelatedProducts'; diff --git a/packages/theme/composables/useCart/useCart.d.ts b/packages/theme/composables/useCart/useCart.d.ts index 920cae894..d0e45d708 100644 --- a/packages/theme/composables/useCart/useCart.d.ts +++ b/packages/theme/composables/useCart/useCart.d.ts @@ -17,7 +17,7 @@ export interface UseCartInterface { clear: (params: ComposableFunctionArgs<{ realCart?: boolean; }>) => Promise; applyCoupon: (params: ComposableFunctionArgs<{ couponCode: string; }>) => Promise; removeCoupon: (params: ComposableFunctionArgs<{}>) => Promise; - isInCart: (context: Context, params: { currentCart: CART; product: PRODUCT }) => boolean; + isInCart: (params: { currentCart: CART; product: PRODUCT }) => boolean; setCart: (newCart: CART) => void; cart: ComputedRef; loading: Ref; diff --git a/packages/theme/composables/useProduct/commands/getProductDetailsCommand.ts b/packages/theme/composables/useProduct/commands/getProductDetailsCommand.ts index b46fcaf1b..e5a35ffd1 100644 --- a/packages/theme/composables/useProduct/commands/getProductDetailsCommand.ts +++ b/packages/theme/composables/useProduct/commands/getProductDetailsCommand.ts @@ -10,6 +10,6 @@ export const getProductDetailsCommand = { ...searchParams, } as GetProductSearchParams, customQuery); - return result.data?.products ?? []; + return result.data?.products; }, }; diff --git a/packages/theme/composables/useProduct/commands/getProductListCommand.ts b/packages/theme/composables/useProduct/commands/getProductListCommand.ts index 9873677b9..287c6b2d6 100644 --- a/packages/theme/composables/useProduct/commands/getProductListCommand.ts +++ b/packages/theme/composables/useProduct/commands/getProductListCommand.ts @@ -8,6 +8,6 @@ export const getProductListCommand = { .api .products(searchParams as GetProductSearchParams, customQuery); - return result.data?.products ?? []; + return result.data?.products; }, }; diff --git a/packages/theme/composables/useProduct/index.ts b/packages/theme/composables/useProduct/index.ts index 3e990d3a9..5cc4883be 100644 --- a/packages/theme/composables/useProduct/index.ts +++ b/packages/theme/composables/useProduct/index.ts @@ -5,6 +5,7 @@ import { import { ref, useContext } from '@nuxtjs/composition-api'; import { getProductListCommand } from '~/composables/useProduct/commands/getProductListCommand'; import { getProductDetailsCommand } from '~/composables/useProduct/commands/getProductDetailsCommand'; +import { ProductsListQuery } from '~/modules/GraphQL/types'; export const useProduct = (id: string) => { const loading = ref(false); @@ -17,7 +18,7 @@ export const useProduct = (id: string) => { const getProductList = async (searchParams) => { Logger.debug(`useProduct/${id}/getProductList`, searchParams); - let products = []; + let products : ProductsListQuery['products'] = null; try { loading.value = true; @@ -35,7 +36,7 @@ export const useProduct = (id: string) => { const getProductDetails = async (searchParams) => { Logger.debug(`useProduct/${id}/getProductDetails`, searchParams); - let products = []; + let products : ProductsListQuery['products'] = null; try { loading.value = true; diff --git a/packages/theme/composables/useReview/commands/addReviewCommand.ts b/packages/theme/composables/useReview/commands/addReviewCommand.ts new file mode 100644 index 000000000..0d3912a98 --- /dev/null +++ b/packages/theme/composables/useReview/commands/addReviewCommand.ts @@ -0,0 +1,20 @@ +import { Context, Logger } from '@vue-storefront/core'; +import {CreateProductReviewInput } from '~/modules/GraphQL/types'; +import { ComposableFunctionArgs } from '~/composables/types'; + +export const addReviewCommand = { + execute: async (context: Context, params: ComposableFunctionArgs) => { + Logger.debug('[Magento] add review params input:', JSON.stringify(params, null, 2)); + + const { + customQuery, + ...input + } = params; + + const { data } = await context.$magento.api.createProductReview(input); + + Logger.debug('[Result]:', { data }); + + return data?.createProductReview?.review ?? {}; + }, +}; diff --git a/packages/theme/composables/useReview/commands/loadCustomerReviewsCommand.ts b/packages/theme/composables/useReview/commands/loadCustomerReviewsCommand.ts new file mode 100644 index 000000000..d8ea60761 --- /dev/null +++ b/packages/theme/composables/useReview/commands/loadCustomerReviewsCommand.ts @@ -0,0 +1,14 @@ +import { ComposableFunctionArgs, Context, Logger } from '@vue-storefront/core'; +import { CustomerProductReviewParams } from '@vue-storefront/magento-api'; + +export const loadCustomerReviewsCommand = { + execute: async (context: Context, params?: ComposableFunctionArgs) => { + Logger.debug('[Magento] load customer review based on:', { params }); + + const { data } = await context.$magento.api.customerProductReview(params); + + Logger.debug('[Result]:', { data }); + + return data?.customer ?? {}; + }, +}; diff --git a/packages/theme/composables/useReview/commands/loadReviewMetadataCommand.ts b/packages/theme/composables/useReview/commands/loadReviewMetadataCommand.ts new file mode 100644 index 000000000..63d77ac35 --- /dev/null +++ b/packages/theme/composables/useReview/commands/loadReviewMetadataCommand.ts @@ -0,0 +1,13 @@ +import { Context, Logger } from '@vue-storefront/core'; + +export const loadReviewMetadataCommand = { + execute: async (context: Context) => { + Logger.debug('[Magento] load review metadata'); + + const { data } = await context.$magento.api.productReviewRatingsMetadata(); + + Logger.debug('[Result]:', { data }); + + return data?.productReviewRatingsMetadata?.items ?? []; + }, +}; diff --git a/packages/theme/composables/useReview/commands/searchReviewsCommand.ts b/packages/theme/composables/useReview/commands/searchReviewsCommand.ts new file mode 100644 index 000000000..e6a20bf71 --- /dev/null +++ b/packages/theme/composables/useReview/commands/searchReviewsCommand.ts @@ -0,0 +1,20 @@ +import { Context, Logger } from '@vue-storefront/core'; +import { ComposableFunctionArgs } from '~/composables/types'; +import { GetProductSearchParams } from '~/composables/useProduct/useProduct'; + +export const searchReviewsCommand = { + execute: async (context: Context, params?: ComposableFunctionArgs) => { + Logger.debug('[Magento] search review params input:', JSON.stringify(params, null, 2)); + + const { + customQuery, + ...input + } = params; + + const { data } = await context.$magento.api.productReview(input as GetProductSearchParams); + + Logger.debug('[Result]:', { data }); + + return data?.products?.items ?? []; + }, +}; diff --git a/packages/theme/composables/useReview/index.ts b/packages/theme/composables/useReview/index.ts new file mode 100644 index 000000000..d0135f761 --- /dev/null +++ b/packages/theme/composables/useReview/index.ts @@ -0,0 +1,93 @@ +/* eslint-disable consistent-return */ +import { ref, useContext } from '@nuxtjs/composition-api'; +import { ComposableFunctionArgs, Context, Logger } from '@vue-storefront/core'; +import { CreateProductReviewInput } from '~/modules/GraphQL/types'; +import { UseReviewErrors } from './useReview'; +import { addReviewCommand } from './commands/addReviewCommand'; +import { loadCustomerReviewsCommand } from './commands/loadCustomerReviewsCommand'; +import { loadReviewMetadataCommand } from './commands/loadReviewMetadataCommand'; +import { searchReviewsCommand } from './commands/searchReviewsCommand'; +import { GetProductSearchParams } from '../useProduct/useProduct'; + +export const useReview = () => { + const loading = ref(false); + const error = ref({ + search: null, + addReview: null, + loadReviewMetadata: null, + loadCustomerReviews: null, + }); + + const { app } = useContext(); + const context = app.$vsf as Context; + + const search = async (searchParams: ComposableFunctionArgs) => { + Logger.debug('useReview/search', searchParams); + + try { + loading.value = true; + error.value.search = null; + return await searchReviewsCommand.execute(context, searchParams); + } catch (err) { + error.value.search = err; + Logger.error('useReview/search', err); + } finally { + loading.value = false; + } + }; + + const loadCustomerReviews = async () => { + Logger.debug('useReview/loadCustomerReviews'); + + try { + loading.value = true; + error.value.loadCustomerReviews = null; + return await loadCustomerReviewsCommand.execute(context); + } catch (err) { + error.value.loadCustomerReviews = err; + Logger.error('useReview/loadCustomerReviews', err); + } finally { + loading.value = false; + } + }; + + const loadReviewMetadata = async () => { + Logger.debug('useReview/loadReviewMetadata'); + + try { + loading.value = true; + error.value.loadReviewMetadata = null; + return await loadReviewMetadataCommand.execute(context); + } catch (err) { + error.value.loadReviewMetadata = err; + Logger.error('useReview/loadReviewMetadata', err); + } finally { + loading.value = false; + } + }; + + const addReview = async (params: ComposableFunctionArgs) => { + Logger.debug('useReview/addReview', params); + try { + loading.value = true; + error.value.addReview = null; + return await addReviewCommand.execute(context, params); + } catch (err) { + error.value.addReview = err; + Logger.error('useReview/addReview', err); + } finally { + loading.value = false; + } + }; + + return { + search, + addReview, + loadReviewMetadata, + loadCustomerReviews, + loading, + error, + }; +}; + +export default useReview; diff --git a/packages/theme/composables/useReview/useReview.d.ts b/packages/theme/composables/useReview/useReview.d.ts new file mode 100644 index 000000000..57f19c248 --- /dev/null +++ b/packages/theme/composables/useReview/useReview.d.ts @@ -0,0 +1,6 @@ +export interface UseReviewErrors { + search: Error; + addReview: Error; + loadReviewMetadata: Error; + loadCustomerReviews: Error; +} diff --git a/packages/theme/pages/MyAccount/MyReviews.vue b/packages/theme/pages/MyAccount/MyReviews.vue index 09810e736..5c8b0b6e5 100644 --- a/packages/theme/pages/MyAccount/MyReviews.vue +++ b/packages/theme/pages/MyAccount/MyReviews.vue @@ -55,9 +55,11 @@ import { SfTabs, SfLoader, SfReview, SfRating, } from '@storefront-ui/vue'; -import { useReview } from '@vue-storefront/magento'; import { reviewGetters } from '~/getters'; -import { computed, defineComponent, onMounted } from '@nuxtjs/composition-api'; +import { + computed, defineComponent, onMounted, ref, +} from '@nuxtjs/composition-api'; +import { useReview } from '~/composables'; export default defineComponent({ name: 'MyReviews', @@ -68,14 +70,13 @@ export default defineComponent({ SfRating, }, setup() { - const { reviews, loading, loadCustomerReviews } = useReview( - 'productReviews-my-reviews', - ); + const { loading, loadCustomerReviews } = useReview(); + const reviews = ref([]); const userReviews = computed(() => reviewGetters.getItems(reviews.value)); onMounted(async () => { - await loadCustomerReviews(); + reviews.value = await loadCustomerReviews(); }); return { diff --git a/packages/theme/pages/Product.vue b/packages/theme/pages/Product.vue index 6ced7da86..bde1ca7d8 100644 --- a/packages/theme/pages/Product.vue +++ b/packages/theme/pages/Product.vue @@ -270,7 +270,6 @@ import { SfSelect, SfTabs, } from '@storefront-ui/vue'; -import { useReview } from '@vue-storefront/magento'; import { ref, computed, @@ -282,7 +281,7 @@ import { import { useCache, CacheTagPrefix } from '@vue-storefront/cache'; import { productGetters, reviewGetters } from '~/getters'; import { - useProduct, useCart, useWishlist, useUser, + useProduct, useCart, useWishlist, useUser, useReview, } from '~/composables'; import { productData } from '~/helpers/product/productData'; import cacheControl from '~/helpers/cacheControl'; @@ -341,11 +340,11 @@ export default defineComponent({ const { getProductDetails, loading: productLoading } = useProduct(); const { addItem, loading } = useCart(); const { - reviews: productReviews, search: searchReviews, loading: reviewsLoading, addReview, - } = useReview(`productReviews-${id}`); + } = useReview(); + const productReviews = ref([]); const { isAuthenticated } = useUser(); const { addItem: addItemToWishlist, isInWishlist } = useWishlist(); const { error: nuxtError, app } = useContext(); @@ -482,7 +481,7 @@ export default defineComponent({ if (product?.value?.length === 0) nuxtError({ statusCode: 404 }); - await searchReviews(baseSearchQuery); + productReviews.value = await searchReviews(baseSearchQuery); const tags = [ {