Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: configurable products open and change #1478

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e92adc2
feat: configurable products open and change
ivan-kalachikov Nov 26, 2024
b7d0653
feat: configurable products open and change
ivan-kalachikov Nov 26, 2024
253a4b9
feat: configurable products open and change
ivan-kalachikov Nov 26, 2024
5cb7789
feat: configurable products open and change
ivan-kalachikov Nov 27, 2024
80a7cc5
fix: tests
ivan-kalachikov Nov 27, 2024
2d9e467
feat: configurable products open and change
ivan-kalachikov Nov 27, 2024
b202894
feat: configurable products open and change
ivan-kalachikov Nov 27, 2024
e5600f9
feat: configurable products open and change
ivan-kalachikov Nov 27, 2024
c61aea5
feat: configurable products open and change
ivan-kalachikov Nov 27, 2024
c220c89
feat: configurable products open and change
ivan-kalachikov Nov 27, 2024
3961163
feat: configurable products open and change
ivan-kalachikov Nov 27, 2024
8054ce2
feat: configurable products open and change
ivan-kalachikov Nov 27, 2024
e6fecf7
feat: update tests
ivan-kalachikov Nov 29, 2024
9d7b135
Merge branch 'dev' into feat/VCST-2093-configurable-products-opening-…
ivan-kalachikov Nov 29, 2024
ee5036a
fix: resolve comment - mark useAllGlobalVariables as depricated
ivan-kalachikov Nov 29, 2024
2707233
fix: resolve comment - modify conditions
ivan-kalachikov Nov 29, 2024
2ae47cd
fix: remove no more used property
ivan-kalachikov Nov 29, 2024
caa3fd1
fix: refactor to complience with sonar code complexity requirements
ivan-kalachikov Nov 29, 2024
fc9c203
fix: refactor to complience with sonar code complexity requirements
ivan-kalachikov Nov 29, 2024
633f0e5
Merge branch 'dev' into feat/VCST-2093-configurable-products-opening-…
ivan-kalachikov Nov 29, 2024
2a7ca16
fix: omit configurable products section type
ivan-kalachikov Dec 2, 2024
08d691d
fix: remove unused import
ivan-kalachikov Dec 2, 2024
1992569
Merge branch 'fix/VCST-2223-omit-configurable-products-section-type' …
ivan-kalachikov Dec 2, 2024
3622d9e
fix: router
ivan-kalachikov Dec 2, 2024
8b5a962
feat: use configurationItems query instead of fullCart query
ivan-kalachikov Dec 2, 2024
ea01ac2
feat: update tests
ivan-kalachikov Dec 2, 2024
4cc8203
feat: minor refactor
ivan-kalachikov Dec 2, 2024
e440688
Merge branch 'dev' into feat/VCST-2093-configurable-products-opening-…
ivan-kalachikov Dec 3, 2024
533c7d1
Merge branch 'dev' into feat/VCST-2093-configurable-products-opening-…
ivan-kalachikov Dec 4, 2024
78d442c
fix: resolve comments - use simple function to get usr search parameter
ivan-kalachikov Dec 4, 2024
0ee55ff
fix: resolve comments - not update cart items qty if there is no ente…
ivan-kalachikov Dec 4, 2024
87b394d
fix: tests to adjust new getting url search param function
ivan-kalachikov Dec 4, 2024
9045984
fix: resolve comments - create a constant for lineItemId url search p…
ivan-kalachikov Dec 4, 2024
e6c5c11
Merge branch 'dev' into feat/VCST-2093-configurable-products-opening-…
ivan-kalachikov Dec 4, 2024
197fe40
fix: configurable products button (#1484)
ivan-kalachikov Dec 4, 2024
19322f6
fix: collapse non first sections by default
ivan-kalachikov Dec 5, 2024
65cb23a
Merge branch 'feat/VCST-2093-configurable-products-opening-updating' …
ivan-kalachikov Dec 5, 2024
b905284
Merge branch 'dev' into feat/VCST-2093-configurable-products-opening-…
ivan-kalachikov Dec 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ fragment fullLineItemProduct on Product {
availabilityData {
...availabilityData
}
isConfigurable
}
1 change: 1 addition & 0 deletions client-app/core/api/graphql/cart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from "./mutations/addItemsCart";
export * from "./mutations/addOrUpdateCartPayment";
export * from "./mutations/addOrUpdateCartShipment";
export * from "./mutations/changeCartComment";
export * from "./mutations/changeCartConfiguredItem";
export * from "./mutations/changeCartItemQuantity";
export * from "./mutations/changeFullCartItemQuantity";
export * from "./mutations/changeFullCartItemsQuantity";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation ChangeCartConfiguredItem($command: InputChangeCartConfiguredItemType!) {
changeCartConfiguredItem(command: $command) {
...fullCart
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useMutation } from "@/core/api/graphql/composables";
import { ChangeCartConfiguredItemDocument } from "@/core/api/graphql/types";
import { globals } from "@/core/globals";
import type { ConfigurationSectionInput } from "@/core/api/graphql/types";

type MutationVariablesType = {
lineItemId: string;
configurationSections: ConfigurationSectionInput[];
quantity: number;
};

export function useChangeCartConfiguredItemMutation() {
const { storeId, cultureName, currencyCode, userId } = globals;

const { mutate: _mutate, ...rest } = useMutation(ChangeCartConfiguredItemDocument);

async function mutate({ lineItemId, configurationSections, quantity }: MutationVariablesType) {
return await _mutate({
command: {
storeId,
cultureName,
currencyCode,
userId,
lineItemId,
configurationSections,
quantity,
},
});
}

return { mutate, ...rest };
}
1 change: 1 addition & 0 deletions client-app/core/api/graphql/catalog/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./mutations/createConfiguredLineItem";
export * from "./queries/childCategories";
export * from "./queries/getCategory";
export * from "./queries/getConfigurationItems";
export * from "./queries/getProduct";
export * from "./queries/getProductConfigurations";
export * from "./queries/getProductWishlists";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
query GetConfigurationItems(
$lineItemId: String!
$storeId: String!
$currencyCode: String!
$cultureName: String
$cartId: String
) {
configurationItems(
lineItemId: $lineItemId
storeId: $storeId
currencyCode: $currencyCode
cultureName: $cultureName
cartId: $cartId
) {
configurationItems {
id
name
quantity
productId
sectionId
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { graphqlClient } from "@/core/api/graphql/client";
import { globals } from "@/core/globals";
import getConfigurationItemsQueryDocument from "./getConfigurationItems.graphql";
import type { GetConfigurationItemsQuery, GetConfigurationItemsQueryVariables } from "@/core/api/graphql/types";
import type { ApolloQueryResult } from "@apollo/client/core";

export async function getConfigurationItems(
lineItemId: string,
cartId?: string,
): Promise<GetConfigurationItemsQuery["configurationItems"] | undefined> {
const { storeId, cultureName, currencyCode } = globals;

const result: ApolloQueryResult<GetConfigurationItemsQuery> = await graphqlClient.query<
GetConfigurationItemsQuery,
GetConfigurationItemsQueryVariables
>({
query: getConfigurationItemsQueryDocument,
variables: {
storeId,
cultureName,
currencyCode,
lineItemId,
cartId,
},
});

return result.data.configurationItems;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ query GetProductConfigurations(
id
name
description
type
isRequired
options {
id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ query GetRecentlyBrowsed($storeId: String!, $currencyCode: String, $cultureName:
name
code
hasVariations
isConfigurable
variations {
id
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ query SearchRelatedProducts(
name
}
hasVariations
isConfigurable
variations {
id
}
Expand Down
1 change: 1 addition & 0 deletions client-app/core/api/graphql/composables/useVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface IAllGlobalVariables {
}

/**
* @deprecated Use {@link globals} directly instead.
* Returns all global variables.
* Now these variables are just strings, because we reload the page on sign in / sign out,
* but in future we should update them without page reload and they will become reactive.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ fragment wishlistLineItemFields on LineItemType {
id
code
slug
hasVariations
isConfigurable
outline
minQuantity
maxQuantity
Expand Down Expand Up @@ -58,5 +60,8 @@ fragment wishlistLineItemFields on LineItemType {
formattedAmount
}
}
variations {
id
}
}
}
122 changes: 72 additions & 50 deletions client-app/core/api/graphql/types.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions client-app/core/constants/line-items.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const LINE_ITEM_QUANTITY_LIMIT = 999999;
export const LINE_ITEM_ID_URL_SEARCH_PARAM = "lineItemId";
15 changes: 14 additions & 1 deletion client-app/core/types/line-items.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { AvailabilityData, CommonVendor, MoneyType, Product, Property } from "@/core/api/graphql/types";
import type {
AvailabilityData,
CommonVendor,
MoneyType,
Product,
Property,
VariationType,
} from "@/core/api/graphql/types";
import type { RouteLocationRaw } from "vue-router";

export type AnyLineItemType = {
Expand All @@ -24,6 +31,9 @@ export type AnyLineItemType = {
price: MoneyType;
quantity: number;
};
isConfigurable?: boolean;
hasVariations?: boolean;
variations?: VariationType[];
configurationItems?: {
id: string;
name?: string;
Expand Down Expand Up @@ -57,6 +67,9 @@ export type PreparedLineItemType = {
sku?: string;
productId?: string;
countInCart?: number;
isConfigurable?: boolean;
hasVariations?: boolean;
variations?: VariationType[];
configurationItems?: {
id: string;
name?: string;
Expand Down
5 changes: 5 additions & 0 deletions client-app/core/utilities/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,8 @@ export type UniqByLastIterateeType<T> = keyof T | ((item: T) => unknown);
export function uniqByLast<T>(arr: T[], iteratee: UniqByLastIterateeType<T>): T[] {
return uniqBy(arr.slice().reverse(), iteratee).reverse();
}

export function getUrlSearchParam(param: string): string | null {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(param);
}
3 changes: 3 additions & 0 deletions client-app/core/utilities/line-items/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export function prepareLineItem(item: AnyLineItemType, countInCart?: number): Pr
minQuantity: item.product?.minQuantity,
maxQuantity: item.product?.maxQuantity ?? item.inStockQuantity ?? item.product?.availabilityData?.availableQuantity,
packSize: item.product?.packSize,
isConfigurable: item.product?.isConfigurable,
hasVariations: item.product?.hasVariations,
variations: item.product?.variations,
configurationItems: "configurationItems" in item ? item.configurationItems : undefined,
};
}
Expand Down
115 changes: 65 additions & 50 deletions client-app/shared/cart/components/add-to-cart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
:loading="loading"
show-empty-details
validate-on-mount
:is-add-only="isConfigurable"
@update:model-value="onInput"
@update:cart-item-quantity="onChange"
@update:validation="onValidationUpdate"
Expand All @@ -30,21 +29,15 @@ import { clone } from "lodash";
import { computed, ref, toRef } from "vue";
import { useI18n } from "vue-i18n";
import { useErrorsTranslator, useGoogleAnalytics, useHistoricalEvents } from "@/core/composables";
import { LINE_ITEM_QUANTITY_LIMIT } from "@/core/constants";
import { LINE_ITEM_ID_URL_SEARCH_PARAM, LINE_ITEM_QUANTITY_LIMIT } from "@/core/constants";
import { ValidationErrorObjectType } from "@/core/enums";
import { globals } from "@/core/globals";
import { Logger } from "@/core/utilities";
import { getUrlSearchParam, Logger } from "@/core/utilities";
import { useShortCart } from "@/shared/cart/composables";
import { useConfigurableProduct } from "@/shared/catalog";
import { useConfigurableProduct } from "@/shared/catalog/composables";
import { useNotifications } from "@/shared/notification";
import { AddToCartModeType } from "@/ui-kit/enums";
import type {
Product,
ShortCartFragment,
ShortLineItemFragment,
VariationType,
ValidationErrorType,
} from "@/core/api/graphql/types";
import type { Product, ShortLineItemFragment, VariationType, ValidationErrorType } from "@/core/api/graphql/types";

const emit = defineEmits<IEmits>();

Expand All @@ -63,10 +56,11 @@ interface IProps {
}

const product = toRef(props, "product");
const { cart, addToCart, changeItemQuantity } = useShortCart();
const { cart, addToCart, changeItemQuantity, changeCartConfiguredItem } = useShortCart();
const { t } = useI18n();
const ga = useGoogleAnalytics();
const { translate } = useErrorsTranslator<ValidationErrorType>("validation_error");
const configurableLineItemId = getUrlSearchParam(LINE_ITEM_ID_URL_SEARCH_PARAM);
const { selectedConfigurationInput } = useConfigurableProduct(product.value.id);

const loading = ref(false);
Expand Down Expand Up @@ -101,66 +95,87 @@ function onInput(value: number): void {
async function onChange() {
loading.value = true;

let lineItem = getLineItem(cart.value?.items);
try {
const lineItem = getLineItem(cart.value?.items);
const mode = lineItem ? AddToCartModeType.Update : AddToCartModeType.Add;
const updatedCart = await updateOrAddToCart(lineItem, mode);

let updatedCart: ShortCartFragment | undefined;
if (isConfigurable.value && mode === AddToCartModeType.Add) {
loading.value = false;
return;
}

const isAlreadyExistsInTheCart = !!lineItem;
const mode = isAlreadyExistsInTheCart && !isConfigurable.value ? AddToCartModeType.Update : AddToCartModeType.Add;
const updatedLineItem = getLineItem(updatedCart?.items);
handleUpdateResult(updatedLineItem, mode);
} finally {
loading.value = false;
}
}

if (mode === AddToCartModeType.Update) {
updatedCart = await changeItemQuantity(lineItem!.id, enteredQuantity.value || 0);
} else {
const inputQuantity = enteredQuantity.value || minQty.value;
const configurationSections = isConfigurable.value ? selectedConfigurationInput.value : undefined;
updatedCart = await addToCart(product.value.id, inputQuantity, configurationSections);

/**
* Send Google Analytics event for an item added to cart.
*/
ga.addItemToCart(product.value, inputQuantity);
void pushHistoricalEvent({
eventType: "addToCart",
sessionId: cart.value?.id,
productId: product.value.id,
storeId: globals.storeId,
});
async function updateOrAddToCart(lineItem: ShortLineItemFragment | undefined, mode: AddToCartModeType) {
if (mode === AddToCartModeType.Update && !enteredQuantity.value) {
return cart.value;
}
if (mode === AddToCartModeType.Update && !!lineItem && enteredQuantity.value) {
return isConfigurable.value
? await changeCartConfiguredItem(lineItem.id, enteredQuantity.value, selectedConfigurationInput.value)
: await changeItemQuantity(lineItem.id, enteredQuantity.value);
}

const quantity = enteredQuantity.value || minQty.value;
const config = isConfigurable.value ? selectedConfigurationInput.value : undefined;
const updatedCart = await addToCart(product.value.id, quantity, config);

trackAddToCart(quantity);
return updatedCart;
}

lineItem = clone(getLineItem(updatedCart?.items));
function trackAddToCart(quantity: number) {
ga.addItemToCart(product.value, quantity);
void pushHistoricalEvent({
eventType: "addToCart",
sessionId: cart.value?.id,
productId: product.value.id,
storeId: globals.storeId,
});
}

function handleUpdateResult(lineItem: ShortLineItemFragment | undefined, mode: AddToCartModeType) {
if (!lineItem) {
Logger.error(onChange.name, 'The variable "lineItem" must be defined');
notifications.error({
text: t(
mode === AddToCartModeType.Update
? "common.messages.fail_to_change_quantity_in_cart"
: "common.messages.fail_add_product_to_cart",
{
reason: updatedCart?.validationErrors
?.filter(
(validationError) =>
validationError.objectId === product.value.id &&
validationError.objectType === ValidationErrorObjectType.CatalogProduct,
)
.map((el) => {
return translate(el);
})
.join(" "),
},
{ reason: getValidationErrors() },
),
duration: 4000,
single: true,
});
} else {
emit("update:lineItem", lineItem);
return;
}

loading.value = false;
emit("update:lineItem", clone(lineItem));
}

function getValidationErrors(): string {
return (
cart.value?.validationErrors
?.filter(
(error) => error.objectId === product.value.id && error.objectType === ValidationErrorObjectType.CatalogProduct,
)
.map(translate)
.join(" ") || ""
);
}

function getLineItem(items?: ShortLineItemFragment[]): ShortLineItemFragment | undefined {
return items?.find((item) => item.productId === product.value.id);
if (isConfigurable.value) {
return configurableLineItemId ? items?.find((item) => item.id === configurableLineItemId) : undefined;
} else {
return items?.find((item) => item.productId === product.value.id);
}
}

function onValidationUpdate(validation: { isValid: true } | { isValid: false; errorMessage: string }) {
Expand Down
Loading
Loading