From 9985f56fc1ed0e5633aa8b1a1ec8aeb19aaf1e36 Mon Sep 17 00:00:00 2001 From: mkucmus Date: Fri, 3 Nov 2023 13:02:28 +0100 Subject: [PATCH 01/32] feat(vue-demo-store): customized product example --- packages/composables/src/index.ts | 1 + packages/composables/src/useCart.ts | 4 +- packages/composables/src/useCartItem.ts | 7 + ...useProductCustomizedProductConfigurator.ts | 174 ++++++++++++++++++ .../models/content/product/Product.d.ts | 2 +- .../components/checkout/CheckoutCartItem.vue | 3 +- .../ProductCustomizedProductConfigurator.vue | 115 ++++++++++++ .../components/product/ProductStatic.vue | 1 + 8 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 packages/composables/src/useProductCustomizedProductConfigurator.ts create mode 100644 templates/vue-demo-store/components/product/ProductCustomizedProductConfigurator.vue diff --git a/packages/composables/src/index.ts b/packages/composables/src/index.ts index a7d569a4b..60f5ee3df 100644 --- a/packages/composables/src/index.ts +++ b/packages/composables/src/index.ts @@ -46,6 +46,7 @@ export * from "./usePrice"; export * from "./useProduct"; export * from "./useProductAssociations"; export * from "./useProductConfigurator"; +export * from "./useProductCustomizedProductConfigurator"; export * from "./useProductPrice"; export * from "./useProductReviews"; export * from "./useProductSearch"; diff --git a/packages/composables/src/useCart.ts b/packages/composables/src/useCart.ts index 958b96451..fe2c7f39e 100644 --- a/packages/composables/src/useCart.ts +++ b/packages/composables/src/useCart.ts @@ -189,9 +189,7 @@ export function useCartFunction(): UseCartReturn { const count = computed(() => { return cartItems.value.reduce( (accumulator: number, lineItem: LineItem) => - lineItem.type === "product" - ? lineItem.quantity + accumulator - : accumulator, + lineItem.good === true ? lineItem.quantity + accumulator : accumulator, 0, ); }); diff --git a/packages/composables/src/useCartItem.ts b/packages/composables/src/useCartItem.ts index ef0836573..57419e011 100644 --- a/packages/composables/src/useCartItem.ts +++ b/packages/composables/src/useCartItem.ts @@ -46,6 +46,10 @@ export type UseCartItemReturn = { * Determines if the current item is a promotion */ isPromotion: ComputedRef; + /** + * Determines if the current item can be removed from cart + */ + isRemovable: ComputedRef; /** * Stock information for the current item */ @@ -115,6 +119,8 @@ export function useCartItem(cartItem: Ref): UseCartItemReturn { const isPromotion = computed(() => cartItem.value.type === "promotion"); + const isRemovable = computed(() => cartItem.value.removable); + async function removeItem() { const newCart = await removeCartItem(cartItem.value.id, apiInstance); await refreshCart(newCart); @@ -174,5 +180,6 @@ export function useCartItem(cartItem: Ref): UseCartItemReturn { itemImageThumbnailUrl, isProduct, isPromotion, + isRemovable, }; } diff --git a/packages/composables/src/useProductCustomizedProductConfigurator.ts b/packages/composables/src/useProductCustomizedProductConfigurator.ts new file mode 100644 index 000000000..0406d340e --- /dev/null +++ b/packages/composables/src/useProductCustomizedProductConfigurator.ts @@ -0,0 +1,174 @@ +import { ref, computed, reactive } from "vue"; +import type { Ref, ComputedRef, UnwrapNestedRefs } from "vue"; +import type { Media, Product } from "@shopware-pwa/types"; +import type { Price } from "@shopware-pwa/types/shopware-6-client/models/framework/pricing/Price"; +import { useAddToCart, useCart, useProduct, useShopwareContext } from "."; + +export type UseProductCustomizedProductConfiguratorReturn = { + customizedProduct: ComputedRef; + state: Ref<{ + [key: string]: string | { media: { filename: string; id: string } }; + }>; + addToCart: () => void; +}; + +export type CustomizedProductOptionValue = { + versionId: string; + translated: { + displayName: string; + }; + createdAt: string; + updatedAt: null | string; + oneTimeSurcharge: boolean; + relativeSurcharge: boolean; + advancedSurcharge: boolean; + taxId: string; + tax: null | unknown; + price: Price[]; + percentageSurcharge: number; + prices: []; + id: string; + templateOptionId: string; + value: { + _value: string; + }; + displayName: string; + itemNumber: null | number; + default: boolean; + position: number; + templateOption: null | unknown; + translations: null | unknown; + templateExclusionConditions: null | unknown; + templateOptionVersionId: string; + apiAlias: "swag_customized_products_template_option_value"; +}; + +export type CustomizedProductOption = { + translated: { + displayName: "Outer leather"; + description: null; + placeholder: null; + }; + createdAt: "2020-08-06T06:26:55.533+00:00"; + updatedAt: null; + oneTimeSurcharge: false; + relativeSurcharge: false; + advancedSurcharge: false; + taxId: null; + tax: null; + calculatedPrice: null; + percentageSurcharge: 0; + price: Price[]; + prices: []; + id: string; + type: "select" | "colorselect" | "imageupload" | "textfield" | "imageselect"; + displayName: string; + description: null | string; + placeholder: null | string; + templateId: string; + typeProperties: { + isMultiSelect: boolean; + }; + itemNumber: null | number; + required: boolean; + position: number; + translations: null | unknown; + template: null | unknown; + values: CustomizedProductOptionValue[]; +}; + +export type SwagCustomizedProductsTemplate = { + versionId: string; + translated: { + displayName: string; + description: string; + }; + createdAt: string; + updatedAt: null | string; + internalName: string; + displayName: string; + description: string; + mediaId: null | string; + active: boolean; + stepByStep: boolean; + confirmInput: boolean; + optionsAutoCollapse: boolean; + decisionTree: unknown[]; + translations: null | unknown; + media: null | Media; + products: null | Product[]; + exclusions: unknown[]; + configurations: null | unknown; + id: string; + parentVersionId: string; + options: CustomizedProductOption[]; + apiAlias: "swag_customized_products_template"; +}; + +export type ProductExtensionsExtended = Product & { + extensions: { + swagCustomizedProductsTemplate: SwagCustomizedProductsTemplate; + }; +}; + +/** + * Composable to change product variant. + * @public + * @category Product + */ +export function useProductCustomizedProductConfigurator(): UseProductCustomizedProductConfiguratorReturn { + const { apiInstance } = useShopwareContext(); + const { configurator, product } = useProduct(); + const { refreshCart } = useCart(); + + const state = ref<{ + [key: string]: string | { media: { filename: string; id: string } }; + }>({}); + + const customizedProduct = computed( + () => + (product.value as ProductExtensionsExtended).extensions + ?.swagCustomizedProductsTemplate, + ); + + const addToCart = async () => { + const payload = { + "customized-products-template": { + id: customizedProduct.value.id, + options: Object.assign( + {}, + ...Object.entries(state.value).map(([id, value]) => ({ + [id]: (value as any).media + ? { + media: { + [(value as any).media.filename]: (value as any).media, + }, + } + : { value }, + })), + ), + }, + lineItems: { + [product.value.id]: { + id: product.value.id, + type: "product", + referencedId: product.value.id, + quantity: 1, + stackable: true, + removable: true, + }, + }, + }; + await apiInstance.invoke.post( + "/store-api/customized-products/add-to-cart", + payload, + ); + refreshCart(); + }; + + return { + customizedProduct, + state, + addToCart, + }; +} diff --git a/packages/types/shopware-6-client/models/content/product/Product.d.ts b/packages/types/shopware-6-client/models/content/product/Product.d.ts index 0617e3f25..bdd09c2ff 100644 --- a/packages/types/shopware-6-client/models/content/product/Product.d.ts +++ b/packages/types/shopware-6-client/models/content/product/Product.d.ts @@ -87,7 +87,7 @@ export type Product = { displayGroup: string; downloads: any; ean: string | null; - extensions: []; + extensions: unknown; height: number | null; id: string; isCloseout: boolean | null; diff --git a/templates/vue-demo-store/components/checkout/CheckoutCartItem.vue b/templates/vue-demo-store/components/checkout/CheckoutCartItem.vue index ec8ee35a3..070c7325e 100644 --- a/templates/vue-demo-store/components/checkout/CheckoutCartItem.vue +++ b/templates/vue-demo-store/components/checkout/CheckoutCartItem.vue @@ -26,6 +26,7 @@ const { itemTotalPrice, itemQuantity, isPromotion, + isRemovable, changeItemQuantity, } = useCartItem(cartItem); @@ -122,7 +123,7 @@ const removeCartItem = async () => { />
From a285a954aae49be80d0ce0a3e59f8f1ddeee677a Mon Sep 17 00:00:00 2001 From: mkucmus Date: Mon, 6 Nov 2023 16:13:21 +0100 Subject: [PATCH 02/32] fix: shared state --- ...useProductCustomizedProductConfigurator.ts | 50 ++++- .../components/product/ProductAddToCart.vue | 13 +- .../components/product/ProductCard.vue | 17 +- .../ProductCustomizedProductConfigurator.vue | 171 ++++++++++-------- 4 files changed, 163 insertions(+), 88 deletions(-) diff --git a/packages/composables/src/useProductCustomizedProductConfigurator.ts b/packages/composables/src/useProductCustomizedProductConfigurator.ts index 0406d340e..2f4e99f6b 100644 --- a/packages/composables/src/useProductCustomizedProductConfigurator.ts +++ b/packages/composables/src/useProductCustomizedProductConfigurator.ts @@ -1,15 +1,17 @@ -import { ref, computed, reactive } from "vue"; -import type { Ref, ComputedRef, UnwrapNestedRefs } from "vue"; +import { ref, computed } from "vue"; +import type { Ref, ComputedRef } from "vue"; import type { Media, Product } from "@shopware-pwa/types"; import type { Price } from "@shopware-pwa/types/shopware-6-client/models/framework/pricing/Price"; -import { useAddToCart, useCart, useProduct, useShopwareContext } from "."; +import { useCart, useProduct, useShopwareContext } from "."; export type UseProductCustomizedProductConfiguratorReturn = { customizedProduct: ComputedRef; state: Ref<{ [key: string]: string | { media: { filename: string; id: string } }; }>; + isActive: ComputedRef; addToCart: () => void; + handleFileUpload: (event: Event, optionId: string) => Promise; }; export type CustomizedProductOptionValue = { @@ -111,6 +113,12 @@ export type ProductExtensionsExtended = Product & { }; }; +const productsState = ref<{ + [productId: string]: { + [key: string]: string | { media: { filename: string; id: string } }; + }; +}>({}); + /** * Composable to change product variant. * @public @@ -118,19 +126,20 @@ export type ProductExtensionsExtended = Product & { */ export function useProductCustomizedProductConfigurator(): UseProductCustomizedProductConfiguratorReturn { const { apiInstance } = useShopwareContext(); - const { configurator, product } = useProduct(); + const { product } = useProduct(); const { refreshCart } = useCart(); - - const state = ref<{ - [key: string]: string | { media: { filename: string; id: string } }; - }>({}); - + if (!productsState.value[product.value.id]) { + productsState.value[product.value.id] = {}; + } const customizedProduct = computed( () => (product.value as ProductExtensionsExtended).extensions ?.swagCustomizedProductsTemplate, ); + const isActive = computed(() => customizedProduct.value?.active); + const state = computed(() => productsState.value[product.value.id]); + const addToCart = async () => { const payload = { "customized-products-template": { @@ -166,9 +175,32 @@ export function useProductCustomizedProductConfigurator(): UseProductCustomizedP refreshCart(); }; + const handleFileUpload = async (event: Event, optionId: string) => { + const file = (event.target as EventTarget & { files: FileList }).files[0]; + const formData = new FormData(); + formData.append("file", file); + formData.append("optionId", optionId); + const headers = { "Content-Type": "multipart/form-data" }; + const addedMediaResponse = await apiInstance.invoke.post<{ + mediaId: string; + fileName: string; + }>(`/store-api/customized-products/upload`, formData, { + headers, + }); + + state.value[optionId] = { + media: { + id: addedMediaResponse?.data?.mediaId, + filename: addedMediaResponse?.data?.fileName, + }, + }; + }; + return { + isActive, customizedProduct, state, addToCart, + handleFileUpload, }; } diff --git a/templates/vue-demo-store/components/product/ProductAddToCart.vue b/templates/vue-demo-store/components/product/ProductAddToCart.vue index 03f8cb406..f418c2f8c 100644 --- a/templates/vue-demo-store/components/product/ProductAddToCart.vue +++ b/templates/vue-demo-store/components/product/ProductAddToCart.vue @@ -1,5 +1,6 @@ - From d248f49611c3083887e331df33aec4f326c425d0 Mon Sep 17 00:00:00 2001 From: mkucmus Date: Mon, 6 Nov 2023 21:57:11 +0100 Subject: [PATCH 03/32] chore: cleanup --- packages/composables/src/index.ts | 1 - .../components/product/ProductAddToCart.vue | 3 -- .../components/product/ProductCard.vue | 1 - .../ProductCustomizedProductConfigurator.vue | 2 +- ...useProductCustomizedProductConfigurator.ts | 38 +++++++++++++++++-- 5 files changed, 35 insertions(+), 10 deletions(-) rename {packages/composables/src => templates/vue-demo-store/composables}/useProductCustomizedProductConfigurator.ts (87%) diff --git a/packages/composables/src/index.ts b/packages/composables/src/index.ts index 60f5ee3df..a7d569a4b 100644 --- a/packages/composables/src/index.ts +++ b/packages/composables/src/index.ts @@ -46,7 +46,6 @@ export * from "./usePrice"; export * from "./useProduct"; export * from "./useProductAssociations"; export * from "./useProductConfigurator"; -export * from "./useProductCustomizedProductConfigurator"; export * from "./useProductPrice"; export * from "./useProductReviews"; export * from "./useProductSearch"; diff --git a/templates/vue-demo-store/components/product/ProductAddToCart.vue b/templates/vue-demo-store/components/product/ProductAddToCart.vue index f418c2f8c..ccb6cfa06 100644 --- a/templates/vue-demo-store/components/product/ProductAddToCart.vue +++ b/templates/vue-demo-store/components/product/ProductAddToCart.vue @@ -1,6 +1,5 @@