Product variants bulk update (#3032)

This commit is contained in:
Paweł Chyła 2023-02-15 14:31:45 +01:00 committed by GitHub
parent 63cf983ee8
commit b040a0deb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 5634 additions and 820 deletions

View file

@ -163,7 +163,7 @@ describe("As an admin I should be able to create variant", () => {
}); });
getProductVariants(createdProduct.id, defaultChannel.slug); getProductVariants(createdProduct.id, defaultChannel.slug);
}) })
.then(([firstVariant, secondVariant]) => { .then(([secondVariant, firstVariant]) => {
expect(firstVariant).to.have.property("price", variants[0].price); expect(firstVariant).to.have.property("price", variants[0].price);
expect(firstVariant).to.have.property("name", "value"); expect(firstVariant).to.have.property("name", "value");
expect(firstVariant).to.have.property("currency", "USD"); expect(firstVariant).to.have.property("currency", "USD");

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -117,6 +117,17 @@ export const bulkProductErrorFragment = gql`
message message
} }
`; `;
export const productVariantBulkError = gql`
fragment ProductVariantBulkError on ProductVariantBulkError {
field
code
message
attributes
values
warehouses
channels
}
`;
export const bulkStockErrorFragment = gql` export const bulkStockErrorFragment = gql`
fragment BulkStockError on BulkStockError { fragment BulkStockError on BulkStockError {
code code

View file

@ -79,6 +79,7 @@ export const channelListingProductFragment = gql`
export const channelListingProductVariantFragment = gql` export const channelListingProductVariantFragment = gql`
fragment ChannelListingProductVariant on ProductVariantChannelListing { fragment ChannelListingProductVariant on ProductVariantChannelListing {
id
channel { channel {
id id
name name

View file

@ -98,6 +98,7 @@
"ProductVariantDeleted", "ProductVariantDeleted",
"ProductVariantMetadataUpdated", "ProductVariantMetadataUpdated",
"ProductVariantOutOfStock", "ProductVariantOutOfStock",
"ProductVariantStockUpdated",
"ProductVariantUpdated", "ProductVariantUpdated",
"SaleCreated", "SaleCreated",
"SaleDeleted", "SaleDeleted",
@ -224,6 +225,7 @@
"Webhook" "Webhook"
], ],
"ObjectWithMetadata": [ "ObjectWithMetadata": [
"Address",
"App", "App",
"Attribute", "Attribute",
"Category", "Category",

View file

@ -682,6 +682,17 @@ export const BulkProductErrorFragmentDoc = gql`
message message
} }
`; `;
export const ProductVariantBulkErrorFragmentDoc = gql`
fragment ProductVariantBulkError on ProductVariantBulkError {
field
code
message
attributes
values
warehouses
channels
}
`;
export const BulkStockErrorFragmentDoc = gql` export const BulkStockErrorFragmentDoc = gql`
fragment BulkStockError on BulkStockError { fragment BulkStockError on BulkStockError {
code code
@ -2031,6 +2042,7 @@ export const PreorderFragmentDoc = gql`
`; `;
export const ChannelListingProductVariantFragmentDoc = gql` export const ChannelListingProductVariantFragmentDoc = gql`
fragment ChannelListingProductVariant on ProductVariantChannelListing { fragment ChannelListingProductVariant on ProductVariantChannelListing {
id
channel { channel {
id id
name name
@ -11765,121 +11777,6 @@ export function useVariantDeleteMutation(baseOptions?: ApolloReactHooks.Mutation
export type VariantDeleteMutationHookResult = ReturnType<typeof useVariantDeleteMutation>; export type VariantDeleteMutationHookResult = ReturnType<typeof useVariantDeleteMutation>;
export type VariantDeleteMutationResult = Apollo.MutationResult<Types.VariantDeleteMutation>; export type VariantDeleteMutationResult = Apollo.MutationResult<Types.VariantDeleteMutation>;
export type VariantDeleteMutationOptions = Apollo.BaseMutationOptions<Types.VariantDeleteMutation, Types.VariantDeleteMutationVariables>; export type VariantDeleteMutationOptions = Apollo.BaseMutationOptions<Types.VariantDeleteMutation, Types.VariantDeleteMutationVariables>;
export const VariantDatagridUpdateDocument = gql`
mutation VariantDatagridUpdate($id: ID!, $input: ProductVariantInput!) {
productVariantUpdate(id: $id, input: $input) {
errors {
...ProductErrorWithAttributes
}
}
}
${ProductErrorWithAttributesFragmentDoc}`;
export type VariantDatagridUpdateMutationFn = Apollo.MutationFunction<Types.VariantDatagridUpdateMutation, Types.VariantDatagridUpdateMutationVariables>;
/**
* __useVariantDatagridUpdateMutation__
*
* To run a mutation, you first call `useVariantDatagridUpdateMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useVariantDatagridUpdateMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [variantDatagridUpdateMutation, { data, loading, error }] = useVariantDatagridUpdateMutation({
* variables: {
* id: // value for 'id'
* input: // value for 'input'
* },
* });
*/
export function useVariantDatagridUpdateMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<Types.VariantDatagridUpdateMutation, Types.VariantDatagridUpdateMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useMutation<Types.VariantDatagridUpdateMutation, Types.VariantDatagridUpdateMutationVariables>(VariantDatagridUpdateDocument, options);
}
export type VariantDatagridUpdateMutationHookResult = ReturnType<typeof useVariantDatagridUpdateMutation>;
export type VariantDatagridUpdateMutationResult = Apollo.MutationResult<Types.VariantDatagridUpdateMutation>;
export type VariantDatagridUpdateMutationOptions = Apollo.BaseMutationOptions<Types.VariantDatagridUpdateMutation, Types.VariantDatagridUpdateMutationVariables>;
export const VariantDatagridStockUpdateDocument = gql`
mutation VariantDatagridStockUpdate($stocks: [StockInput!]!, $removeStocks: [ID!]!, $id: ID!) {
productVariantStocksDelete(warehouseIds: $removeStocks, variantId: $id) {
errors {
...ProductVariantStocksDeleteError
}
}
productVariantStocksUpdate(stocks: $stocks, variantId: $id) {
errors {
...BulkStockError
}
}
}
${ProductVariantStocksDeleteErrorFragmentDoc}
${BulkStockErrorFragmentDoc}`;
export type VariantDatagridStockUpdateMutationFn = Apollo.MutationFunction<Types.VariantDatagridStockUpdateMutation, Types.VariantDatagridStockUpdateMutationVariables>;
/**
* __useVariantDatagridStockUpdateMutation__
*
* To run a mutation, you first call `useVariantDatagridStockUpdateMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useVariantDatagridStockUpdateMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [variantDatagridStockUpdateMutation, { data, loading, error }] = useVariantDatagridStockUpdateMutation({
* variables: {
* stocks: // value for 'stocks'
* removeStocks: // value for 'removeStocks'
* id: // value for 'id'
* },
* });
*/
export function useVariantDatagridStockUpdateMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<Types.VariantDatagridStockUpdateMutation, Types.VariantDatagridStockUpdateMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useMutation<Types.VariantDatagridStockUpdateMutation, Types.VariantDatagridStockUpdateMutationVariables>(VariantDatagridStockUpdateDocument, options);
}
export type VariantDatagridStockUpdateMutationHookResult = ReturnType<typeof useVariantDatagridStockUpdateMutation>;
export type VariantDatagridStockUpdateMutationResult = Apollo.MutationResult<Types.VariantDatagridStockUpdateMutation>;
export type VariantDatagridStockUpdateMutationOptions = Apollo.BaseMutationOptions<Types.VariantDatagridStockUpdateMutation, Types.VariantDatagridStockUpdateMutationVariables>;
export const VariantDatagridChannelListingUpdateDocument = gql`
mutation VariantDatagridChannelListingUpdate($id: ID!, $input: [ProductVariantChannelListingAddInput!]!) {
productVariantChannelListingUpdate(id: $id, input: $input) {
errors {
...ProductChannelListingError
}
}
}
${ProductChannelListingErrorFragmentDoc}`;
export type VariantDatagridChannelListingUpdateMutationFn = Apollo.MutationFunction<Types.VariantDatagridChannelListingUpdateMutation, Types.VariantDatagridChannelListingUpdateMutationVariables>;
/**
* __useVariantDatagridChannelListingUpdateMutation__
*
* To run a mutation, you first call `useVariantDatagridChannelListingUpdateMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useVariantDatagridChannelListingUpdateMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [variantDatagridChannelListingUpdateMutation, { data, loading, error }] = useVariantDatagridChannelListingUpdateMutation({
* variables: {
* id: // value for 'id'
* input: // value for 'input'
* },
* });
*/
export function useVariantDatagridChannelListingUpdateMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<Types.VariantDatagridChannelListingUpdateMutation, Types.VariantDatagridChannelListingUpdateMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useMutation<Types.VariantDatagridChannelListingUpdateMutation, Types.VariantDatagridChannelListingUpdateMutationVariables>(VariantDatagridChannelListingUpdateDocument, options);
}
export type VariantDatagridChannelListingUpdateMutationHookResult = ReturnType<typeof useVariantDatagridChannelListingUpdateMutation>;
export type VariantDatagridChannelListingUpdateMutationResult = Apollo.MutationResult<Types.VariantDatagridChannelListingUpdateMutation>;
export type VariantDatagridChannelListingUpdateMutationOptions = Apollo.BaseMutationOptions<Types.VariantDatagridChannelListingUpdateMutation, Types.VariantDatagridChannelListingUpdateMutationVariables>;
export const VariantUpdateDocument = gql` export const VariantUpdateDocument = gql`
mutation VariantUpdate($addStocks: [StockInput!]!, $removeStocks: [ID!]!, $id: ID!, $attributes: [AttributeValueInput!], $sku: String, $quantityLimitPerCustomer: Int, $trackInventory: Boolean!, $stocks: [StockInput!]!, $preorder: PreorderSettingsInput, $weight: WeightScalar, $firstValues: Int, $afterValues: String, $lastValues: Int, $beforeValues: String, $name: String!) { mutation VariantUpdate($addStocks: [StockInput!]!, $removeStocks: [ID!]!, $id: ID!, $attributes: [AttributeValueInput!], $sku: String, $quantityLimitPerCustomer: Int, $trackInventory: Boolean!, $stocks: [StockInput!]!, $preorder: PreorderSettingsInput, $weight: WeightScalar, $firstValues: Int, $afterValues: String, $lastValues: Int, $beforeValues: String, $name: String!) {
productVariantStocksDelete(warehouseIds: $removeStocks, variantId: $id) { productVariantStocksDelete(warehouseIds: $removeStocks, variantId: $id) {
@ -12528,6 +12425,52 @@ export function useProductVariantPreorderDeactivateMutation(baseOptions?: Apollo
export type ProductVariantPreorderDeactivateMutationHookResult = ReturnType<typeof useProductVariantPreorderDeactivateMutation>; export type ProductVariantPreorderDeactivateMutationHookResult = ReturnType<typeof useProductVariantPreorderDeactivateMutation>;
export type ProductVariantPreorderDeactivateMutationResult = Apollo.MutationResult<Types.ProductVariantPreorderDeactivateMutation>; export type ProductVariantPreorderDeactivateMutationResult = Apollo.MutationResult<Types.ProductVariantPreorderDeactivateMutation>;
export type ProductVariantPreorderDeactivateMutationOptions = Apollo.BaseMutationOptions<Types.ProductVariantPreorderDeactivateMutation, Types.ProductVariantPreorderDeactivateMutationVariables>; export type ProductVariantPreorderDeactivateMutationOptions = Apollo.BaseMutationOptions<Types.ProductVariantPreorderDeactivateMutation, Types.ProductVariantPreorderDeactivateMutationVariables>;
export const ProductVariantBulkUpdateDocument = gql`
mutation ProductVariantBulkUpdate($product: ID!, $input: [ProductVariantBulkUpdateInput!]!, $errorPolicy: ErrorPolicyEnum) {
productVariantBulkUpdate(
errorPolicy: $errorPolicy
product: $product
variants: $input
) {
errors {
...ProductVariantBulkError
}
results {
errors {
...ProductVariantBulkError
}
}
}
}
${ProductVariantBulkErrorFragmentDoc}`;
export type ProductVariantBulkUpdateMutationFn = Apollo.MutationFunction<Types.ProductVariantBulkUpdateMutation, Types.ProductVariantBulkUpdateMutationVariables>;
/**
* __useProductVariantBulkUpdateMutation__
*
* To run a mutation, you first call `useProductVariantBulkUpdateMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useProductVariantBulkUpdateMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [productVariantBulkUpdateMutation, { data, loading, error }] = useProductVariantBulkUpdateMutation({
* variables: {
* product: // value for 'product'
* input: // value for 'input'
* errorPolicy: // value for 'errorPolicy'
* },
* });
*/
export function useProductVariantBulkUpdateMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<Types.ProductVariantBulkUpdateMutation, Types.ProductVariantBulkUpdateMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useMutation<Types.ProductVariantBulkUpdateMutation, Types.ProductVariantBulkUpdateMutationVariables>(ProductVariantBulkUpdateDocument, options);
}
export type ProductVariantBulkUpdateMutationHookResult = ReturnType<typeof useProductVariantBulkUpdateMutation>;
export type ProductVariantBulkUpdateMutationResult = Apollo.MutationResult<Types.ProductVariantBulkUpdateMutation>;
export type ProductVariantBulkUpdateMutationOptions = Apollo.BaseMutationOptions<Types.ProductVariantBulkUpdateMutation, Types.ProductVariantBulkUpdateMutationVariables>;
export const InitialProductFilterAttributesDocument = gql` export const InitialProductFilterAttributesDocument = gql`
query InitialProductFilterAttributes { query InitialProductFilterAttributes {
attributes(first: 100, filter: {type: PRODUCT_TYPE}) { attributes(first: 100, filter: {type: PRODUCT_TYPE}) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -277,7 +277,11 @@ export const getMutationErrors = <
return [] as TErrors; return [] as TErrors;
} }
return Object.values(result.data).reduce( return Object.values(result.data).reduce(
(acc: TErrors[], mut: TData) => [...acc, ...(mut.errors || [])], (acc: TErrors[], mut: TData) => [
...acc,
...(mut.errors || []),
...(mut?.results?.flatMap(res => res.errors) || []),
],
[] as TErrors[], [] as TErrors[],
) as TErrors; ) as TErrors;
}; };

View file

@ -2,7 +2,10 @@ import placeholderImage from "@assets/images/placeholder255x255.png";
import { channelsList } from "@dashboard/channels/fixtures"; import { channelsList } from "@dashboard/channels/fixtures";
import { collections } from "@dashboard/collections/fixtures"; import { collections } from "@dashboard/collections/fixtures";
import { fetchMoreProps, limits, limitsReached } from "@dashboard/fixtures"; import { fetchMoreProps, limits, limitsReached } from "@dashboard/fixtures";
import { ProductErrorCode } from "@dashboard/graphql"; import {
ProductErrorCode,
ProductVariantBulkErrorCode,
} from "@dashboard/graphql";
import Decorator from "@dashboard/storybook/Decorator"; import Decorator from "@dashboard/storybook/Decorator";
import { taxClasses } from "@dashboard/taxes/fixtures"; import { taxClasses } from "@dashboard/taxes/fixtures";
import { warehouseList } from "@dashboard/warehouses/fixtures"; import { warehouseList } from "@dashboard/warehouses/fixtures";
@ -23,7 +26,7 @@ const props: ProductUpdatePageProps = {
variantId: product.variants[0].id, variantId: product.variants[0].id,
type: "channel", type: "channel",
channelIds: [channelsList[1].id], channelIds: [channelsList[1].id],
error: ProductErrorCode.ALREADY_EXISTS, error: ProductVariantBulkErrorCode.PRODUCT_NOT_ASSIGNED_TO_CHANNEL,
}, },
], ],
productId: "123", productId: "123",
@ -154,18 +157,20 @@ storiesOf("Products / Product edit", module)
.add("form errors", () => ( .add("form errors", () => (
<ProductUpdatePage <ProductUpdatePage
{...props} {...props}
errors={([ errors={(
"attributes", [
"category", "attributes",
"chargeTaxes", "category",
"collections", "chargeTaxes",
"name", "collections",
"publicationDate", "name",
"seoDescription", "publicationDate",
"seoTitle", "seoDescription",
"sku", "seoTitle",
"stockQuantity", "sku",
] as Array<keyof ProductUpdateFormData | "attributes">).map(field => ({ "stockQuantity",
] as Array<keyof ProductUpdateFormData | "attributes">
).map(field => ({
__typename: "ProductError", __typename: "ProductError",
attributes: attributes:
field === "attributes" ? [product.attributes[0].attribute.id] : null, field === "attributes" ? [product.attributes[0].attribute.id] : null,

View file

@ -309,10 +309,11 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
const listings = data.channels.updateChannels.map<ChannelData>( const listings = data.channels.updateChannels.map<ChannelData>(
listing => { listing => {
const channel = channels?.find(ac => ac.id === listing.channelId); const channel = channels?.find(ac => ac.id === listing.channelId);
return { return {
id: listing.channelId,
...channel, ...channel,
...listing, ...listing,
id: listing.channelId,
availableForPurchase: listing.availableForPurchaseDate, availableForPurchase: listing.availableForPurchaseDate,
currency: channel.currencyCode, currency: channel.currencyCode,
}; };

View file

@ -1,62 +0,0 @@
import {
DatagridChange,
DatagridChangeOpts,
} from "@dashboard/components/Datagrid/useDatagridChange";
import { getColumnChannel, getColumnChannelAvailability } from "./columnData";
const byCurrentRowByIndex = (index: number, data: DatagridChangeOpts) => (
change: DatagridChange,
) => {
const totalRemoved = data.removed.filter(r => r <= index).length;
return change.row === index + totalRemoved;
};
const byChannelColumn = (change: DatagridChange) =>
getColumnChannel(change.column);
const availabilityToChannelColumn = (change: DatagridChange) => {
const availabilityChannelId = getColumnChannelAvailability(change.column);
if (availabilityChannelId) {
return {
data: {
value: change.data ? 0 : null,
},
column: `channel:${availabilityChannelId}`,
};
}
return change;
};
const byColumn = (prev: DatagridChange[], change: DatagridChange) => {
const index = prev.findIndex(p => p.column === change.column);
if (index > -1) {
prev[index] = change;
return prev;
}
return prev.concat(change);
};
const dataGridChangeToFlatChannel = (change: DatagridChange) => ({
channelId: getColumnChannel(change.column),
price: change.data.value,
});
const byNotNullPrice = (
change: ReturnType<typeof dataGridChangeToFlatChannel>,
) => change.price !== null;
export function getVariantChannelsInputs(
data: DatagridChangeOpts,
index: number,
) {
return data.updates
.filter(byCurrentRowByIndex(index, data))
.map(availabilityToChannelColumn)
.filter(byChannelColumn)
.reduce(byColumn, [])
.map(dataGridChangeToFlatChannel)
.filter(byNotNullPrice);
}

View file

@ -9,17 +9,11 @@ import {
import { emptyDropdownCellValue } from "@dashboard/components/Datagrid/DropdownCell"; import { emptyDropdownCellValue } from "@dashboard/components/Datagrid/DropdownCell";
import { numberCellEmptyValue } from "@dashboard/components/Datagrid/NumberCell"; import { numberCellEmptyValue } from "@dashboard/components/Datagrid/NumberCell";
import { AvailableColumn } from "@dashboard/components/Datagrid/types"; import { AvailableColumn } from "@dashboard/components/Datagrid/types";
import { import { DatagridChange } from "@dashboard/components/Datagrid/useDatagridChange";
DatagridChange,
DatagridChangeOpts,
} from "@dashboard/components/Datagrid/useDatagridChange";
import { Choice } from "@dashboard/components/SingleSelectField"; import { Choice } from "@dashboard/components/SingleSelectField";
import { import {
ProductDetailsVariantFragment, ProductDetailsVariantFragment,
ProductFragment, ProductFragment,
VariantDatagridChannelListingUpdateMutationVariables,
VariantDatagridStockUpdateMutationVariables,
VariantDatagridUpdateMutationVariables,
WarehouseFragment, WarehouseFragment,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import { ProductVariantListError } from "@dashboard/products/views/ProductUpdate/handlers/errors"; import { ProductVariantListError } from "@dashboard/products/views/ProductUpdate/handlers/errors";
@ -32,117 +26,11 @@ import {
getColumnAttribute, getColumnAttribute,
getColumnChannel, getColumnChannel,
getColumnChannelAvailability, getColumnChannelAvailability,
getColumnName,
getColumnStock, getColumnStock,
} from "./datagrid/columnData"; } from "../../utils/datagrid";
import { getVariantChannelsInputs } from "./datagrid/getVariantChannelsInputs";
import messages from "./messages"; import messages from "./messages";
export function getVariantInput(data: DatagridChangeOpts, index: number) {
const attributes = data.updates
.filter(
change =>
getColumnAttribute(change.column) &&
change.row === index + data.removed.filter(r => r <= index).length,
)
.map(change => {
const attributeId = getColumnAttribute(change.column);
return {
id: attributeId,
values: [change.data.value.value],
};
});
const sku = data.updates.find(
change =>
change.column === "sku" &&
change.row === index + data.removed.filter(r => r <= index).length,
)?.data;
const name = data.updates.find(
change =>
change.column === "name" &&
change.row === index + data.removed.filter(r => r <= index).length,
)?.data;
return {
attributes,
sku,
name,
};
}
export function getVariantInputs(
variants: ProductFragment["variants"],
data: DatagridChangeOpts,
): VariantDatagridUpdateMutationVariables[] {
return variants
.map(
(variant, variantIndex): VariantDatagridUpdateMutationVariables => ({
id: variant.id,
input: getVariantInput(data, variantIndex),
}),
)
.filter(
variables =>
variables.input.sku ||
variables.input.name ||
variables.input.attributes.length > 0,
);
}
export function getStockInputs(data: DatagridChangeOpts, index: number) {
const stockChanges = data.updates.filter(change =>
getColumnStock(change.column),
);
const variantChanges = stockChanges
.filter(
change =>
change.row === index + data.removed.filter(r => r <= index).length,
)
.map(change => ({
warehouse: getColumnStock(change.column),
quantity: change.data.value,
}));
return {
stocks: variantChanges.filter(
change => change.quantity !== numberCellEmptyValue,
),
removeStocks: variantChanges
.filter(change => change.quantity === numberCellEmptyValue)
.map(({ warehouse }) => warehouse),
};
}
export function getStocks(
variants: ProductFragment["variants"],
data: DatagridChangeOpts,
): VariantDatagridStockUpdateMutationVariables[] {
return variants
.map((variant, variantIndex) => ({
id: variant.id,
...getStockInputs(data, variantIndex),
}))
.filter(
variables =>
variables.removeStocks.length > 0 || variables.stocks.length > 0,
);
}
export function getVariantChannels(
variants: ProductFragment["variants"],
data: DatagridChangeOpts,
): VariantDatagridChannelListingUpdateMutationVariables[] {
return variants
.map((variant, variantIndex) => ({
id: variant.id,
input: getVariantChannelsInputs(data, variantIndex),
}))
.filter(({ input }) => input.length > 0);
}
function errorMatchesColumn( function errorMatchesColumn(
error: ProductVariantListError, error: ProductVariantListError,
columnId: string, columnId: string,
@ -162,7 +50,8 @@ function errorMatchesColumn(
if (error.attributes?.length > 0) { if (error.attributes?.length > 0) {
return error.attributes.includes(getColumnAttribute(columnId)); return error.attributes.includes(getColumnAttribute(columnId));
} }
return columnId === "sku";
return error?.field?.includes(getColumnName(columnId)) ?? false;
} }
} }
@ -265,8 +154,9 @@ export function getData({
}; };
} }
const currency = channels.find(channel => channelId === channel.id) const currency = channels.find(
?.currency; channel => channelId === channel.id,
)?.currency;
const value = change?.value ?? listing?.price?.amount ?? 0; const value = change?.value ?? listing?.price?.amount ?? 0;
return moneyCell(value, currency); return moneyCell(value, currency);

View file

@ -635,8 +635,7 @@ export const product: (
quantity: 272, quantity: 272,
quantityAllocated: 0, quantityAllocated: 0,
warehouse: { warehouse: {
id: id: "V2FyZWhvdXNlOjI2NDNiNmIwLWExMTQtNGRiMC1hM2U4LTFkZGY3ZGM3NDVkMg==",
"V2FyZWhvdXNlOjI2NDNiNmIwLWExMTQtNGRiMC1hM2U4LTFkZGY3ZGM3NDVkMg==",
name: "Europe for click and collect", name: "Europe for click and collect",
__typename: "Warehouse", __typename: "Warehouse",
}, },
@ -647,8 +646,7 @@ export const product: (
quantity: 272, quantity: 272,
quantityAllocated: 0, quantityAllocated: 0,
warehouse: { warehouse: {
id: id: "V2FyZWhvdXNlOmFmZDA4YjY4LWQwYmMtNGQ1My1iNjJkLTg1YWMxOWI3MjliYg==",
"V2FyZWhvdXNlOmFmZDA4YjY4LWQwYmMtNGQ1My1iNjJkLTg1YWMxOWI3MjliYg==",
name: "Europe", name: "Europe",
__typename: "Warehouse", __typename: "Warehouse",
}, },
@ -659,8 +657,7 @@ export const product: (
quantity: 274, quantity: 274,
quantityAllocated: 2, quantityAllocated: 2,
warehouse: { warehouse: {
id: id: "V2FyZWhvdXNlOjMxOTRjNjY5LTY1YjItNDBjYy04ZDI5LWI3M2Q0YTUwODBmMQ==",
"V2FyZWhvdXNlOjMxOTRjNjY5LTY1YjItNDBjYy04ZDI5LWI3M2Q0YTUwODBmMQ==",
name: "Asia", name: "Asia",
__typename: "Warehouse", __typename: "Warehouse",
}, },
@ -671,8 +668,7 @@ export const product: (
quantity: 272, quantity: 272,
quantityAllocated: 0, quantityAllocated: 0,
warehouse: { warehouse: {
id: id: "V2FyZWhvdXNlOjI5YzBlYmYwLWVkNzktNDlmOS1hYmQ0LWQwNDBlOGNlZmI3Mg==",
"V2FyZWhvdXNlOjI5YzBlYmYwLWVkNzktNDlmOS1hYmQ0LWQwNDBlOGNlZmI3Mg==",
name: "Oceania", name: "Oceania",
__typename: "Warehouse", __typename: "Warehouse",
}, },
@ -683,8 +679,7 @@ export const product: (
quantity: 272, quantity: 272,
quantityAllocated: 0, quantityAllocated: 0,
warehouse: { warehouse: {
id: id: "V2FyZWhvdXNlOjRiNjc1ZmVlLTE3OWYtNGMwNS04YmJlLWE0ZDJjOTc0OWQzMA==",
"V2FyZWhvdXNlOjRiNjc1ZmVlLTE3OWYtNGMwNS04YmJlLWE0ZDJjOTc0OWQzMA==",
name: "Africa", name: "Africa",
__typename: "Warehouse", __typename: "Warehouse",
}, },
@ -695,8 +690,7 @@ export const product: (
quantity: 274, quantity: 274,
quantityAllocated: 2, quantityAllocated: 2,
warehouse: { warehouse: {
id: id: "V2FyZWhvdXNlOmQwODA2MzM5LTVhNjAtNDAxNi1hNGUwLTRjNDYxNTZlY2IzMQ==",
"V2FyZWhvdXNlOmQwODA2MzM5LTVhNjAtNDAxNi1hNGUwLTRjNDYxNTZlY2IzMQ==",
name: "Americas", name: "Americas",
__typename: "Warehouse", __typename: "Warehouse",
}, },
@ -707,6 +701,7 @@ export const product: (
preorder: null, preorder: null,
channelListings: [ channelListings: [
{ {
id: "UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNTM=",
channel: { channel: {
id: "Q2hhbm5lbDox", id: "Q2hhbm5lbDox",
name: "Channel-USD", name: "Channel-USD",
@ -731,6 +726,7 @@ export const product: (
__typename: "ProductVariantChannelListing", __typename: "ProductVariantChannelListing",
}, },
{ {
id: "UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNDM=",
channel: { channel: {
id: "Q2hhbm5lbDoy", id: "Q2hhbm5lbDoy",
name: "Channel-PLN", name: "Channel-PLN",
@ -793,6 +789,7 @@ export const product: (
preorder: null, preorder: null,
channelListings: [ channelListings: [
{ {
id: "UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNEM=",
channel: { channel: {
id: "Q2hhbm5lbDox", id: "Q2hhbm5lbDox",
name: "Channel-USD", name: "Channel-USD",
@ -817,6 +814,7 @@ export const product: (
__typename: "ProductVariantChannelListing", __typename: "ProductVariantChannelListing",
}, },
{ {
id: "UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoreNTM=",
channel: { channel: {
id: "Q2hhbm5lbDoy", id: "Q2hhbm5lbDoy",
name: "Channel-PLN", name: "Channel-PLN",
@ -880,8 +878,7 @@ export const product: (
quantity: 418, quantity: 418,
quantityAllocated: 0, quantityAllocated: 0,
warehouse: { warehouse: {
id: id: "V2FyZWhvdXNlOmFmZDA4YjY4LWQwYmMtNGQ1My1iNjJkLTg1YWMxOWI3MjliYg==",
"V2FyZWhvdXNlOmFmZDA4YjY4LWQwYmMtNGQ1My1iNjJkLTg1YWMxOWI3MjliYg==",
name: "Europe", name: "Europe",
__typename: "Warehouse", __typename: "Warehouse",
}, },
@ -892,8 +889,7 @@ export const product: (
quantity: 418, quantity: 418,
quantityAllocated: 0, quantityAllocated: 0,
warehouse: { warehouse: {
id: id: "V2FyZWhvdXNlOjMxOTRjNjY5LTY1YjItNDBjYy04ZDI5LWI3M2Q0YTUwODBmMQ==",
"V2FyZWhvdXNlOjMxOTRjNjY5LTY1YjItNDBjYy04ZDI5LWI3M2Q0YTUwODBmMQ==",
name: "Asia", name: "Asia",
__typename: "Warehouse", __typename: "Warehouse",
}, },
@ -904,8 +900,7 @@ export const product: (
quantity: 418, quantity: 418,
quantityAllocated: 0, quantityAllocated: 0,
warehouse: { warehouse: {
id: id: "V2FyZWhvdXNlOmQwODA2MzM5LTVhNjAtNDAxNi1hNGUwLTRjNDYxNTZlY2IzMQ==",
"V2FyZWhvdXNlOmQwODA2MzM5LTVhNjAtNDAxNi1hNGUwLTRjNDYxNTZlY2IzMQ==",
name: "Americas", name: "Americas",
__typename: "Warehouse", __typename: "Warehouse",
}, },
@ -916,8 +911,7 @@ export const product: (
quantity: 418, quantity: 418,
quantityAllocated: 0, quantityAllocated: 0,
warehouse: { warehouse: {
id: id: "V2FyZWhvdXNlOjI5YzBlYmYwLWVkNzktNDlmOS1hYmQ0LWQwNDBlOGNlZmI3Mg==",
"V2FyZWhvdXNlOjI5YzBlYmYwLWVkNzktNDlmOS1hYmQ0LWQwNDBlOGNlZmI3Mg==",
name: "Oceania", name: "Oceania",
__typename: "Warehouse", __typename: "Warehouse",
}, },
@ -928,8 +922,7 @@ export const product: (
quantity: 418, quantity: 418,
quantityAllocated: 0, quantityAllocated: 0,
warehouse: { warehouse: {
id: id: "V2FyZWhvdXNlOjI2NDNiNmIwLWExMTQtNGRiMC1hM2U4LTFkZGY3ZGM3NDVkMg==",
"V2FyZWhvdXNlOjI2NDNiNmIwLWExMTQtNGRiMC1hM2U4LTFkZGY3ZGM3NDVkMg==",
name: "Europe for click and collect", name: "Europe for click and collect",
__typename: "Warehouse", __typename: "Warehouse",
}, },
@ -940,8 +933,7 @@ export const product: (
quantity: 418, quantity: 418,
quantityAllocated: 0, quantityAllocated: 0,
warehouse: { warehouse: {
id: id: "V2FyZWhvdXNlOjRiNjc1ZmVlLTE3OWYtNGMwNS04YmJlLWE0ZDJjOTc0OWQzMA==",
"V2FyZWhvdXNlOjRiNjc1ZmVlLTE3OWYtNGMwNS04YmJlLWE0ZDJjOTc0OWQzMA==",
name: "Africa", name: "Africa",
__typename: "Warehouse", __typename: "Warehouse",
}, },
@ -952,6 +944,7 @@ export const product: (
preorder: null, preorder: null,
channelListings: [ channelListings: [
{ {
id: "UHJvZHVjdFZhcmlhbnRDaSAD3w2FubmVsTGlzdGluZzoyNTM=",
channel: { channel: {
id: "Q2hhbm5lbDox", id: "Q2hhbm5lbDox",
name: "Channel-USD", name: "Channel-USD",
@ -976,6 +969,7 @@ export const product: (
__typename: "ProductVariantChannelListing", __typename: "ProductVariantChannelListing",
}, },
{ {
id: "UHJvZHVjdFZhcmlhdwxDaGFubmVsTGlzdGluZzoyNTM=",
channel: { channel: {
id: "Q2hhbm5lbDoy", id: "Q2hhbm5lbDoy",
name: "Channel-PLN", name: "Channel-PLN",
@ -3215,6 +3209,7 @@ export const variant = (placeholderImage: string): ProductVariantFragment => ({
__typename: "ProductVariant", __typename: "ProductVariant",
channelListings: [ channelListings: [
{ {
id: "UHJvZHVjdFZhcasdasdASDDaGFubmVsTGlzdGluZzoyNTM=",
__typename: "ProductVariantChannelListing", __typename: "ProductVariantChannelListing",
channel: { channel: {
__typename: "Channel", __typename: "Channel",
@ -3239,6 +3234,7 @@ export const variant = (placeholderImage: string): ProductVariantFragment => ({
}, },
}, },
{ {
id: "UHJvZHVjdFZhcmlhbnRDaGGSAEdsWsTGlzdGluZzoyNTM=",
__typename: "ProductVariantChannelListing", __typename: "ProductVariantChannelListing",
channel: { channel: {
__typename: "Channel", __typename: "Channel",
@ -3377,8 +3373,8 @@ export const variant = (placeholderImage: string): ProductVariantFragment => ({
__typename: "Product" as "Product", __typename: "Product" as "Product",
channelListings: [ channelListings: [
{ {
id: "2",
__typename: "ProductChannelListing", __typename: "ProductChannelListing",
id: "11",
isPublished: false, isPublished: false,
publicationDate: null, publicationDate: null,
channel: { channel: {
@ -3389,8 +3385,8 @@ export const variant = (placeholderImage: string): ProductVariantFragment => ({
}, },
}, },
{ {
id: "2",
__typename: "ProductChannelListing", __typename: "ProductChannelListing",
id: "12",
isPublished: true, isPublished: true,
publicationDate: "2022-01-21", publicationDate: "2022-01-21",
channel: { channel: {
@ -3791,10 +3787,9 @@ export const variantProductImages = (placeholderImage: string) =>
export const variantSiblings = (placeholderImage: string) => export const variantSiblings = (placeholderImage: string) =>
variant(placeholderImage).product.variants; variant(placeholderImage).product.variants;
export const productTypesList: Array<Pick< export const productTypesList: Array<
ProductType, Pick<ProductType, "id" | "name" | "hasVariants">
"id" | "name" | "hasVariants" > = [
>> = [
{ {
hasVariants: true, hasVariants: true,
id: "UHJvZHVjdFR5cGU6Nw==", id: "UHJvZHVjdFR5cGU6Nw==",

View file

@ -117,48 +117,6 @@ export const variantDeleteMutation = gql`
} }
`; `;
export const variantDatagridUpdateMutation = gql`
mutation VariantDatagridUpdate($id: ID!, $input: ProductVariantInput!) {
productVariantUpdate(id: $id, input: $input) {
errors {
...ProductErrorWithAttributes
}
}
}
`;
export const variantDatagridStockUpdateMutation = gql`
mutation VariantDatagridStockUpdate(
$stocks: [StockInput!]!
$removeStocks: [ID!]!
$id: ID!
) {
productVariantStocksDelete(warehouseIds: $removeStocks, variantId: $id) {
errors {
...ProductVariantStocksDeleteError
}
}
productVariantStocksUpdate(stocks: $stocks, variantId: $id) {
errors {
...BulkStockError
}
}
}
`;
export const variantDatagridChannelListingUpdateMutation = gql`
mutation VariantDatagridChannelListingUpdate(
$id: ID!
$input: [ProductVariantChannelListingAddInput!]!
) {
productVariantChannelListingUpdate(id: $id, input: $input) {
errors {
...ProductChannelListingError
}
}
}
`;
export const variantUpdateMutation = gql` export const variantUpdateMutation = gql`
mutation VariantUpdate( mutation VariantUpdate(
$addStocks: [StockInput!]! $addStocks: [StockInput!]!
@ -458,3 +416,26 @@ export const ProductVariantPreorderDeactivateMutation = gql`
} }
} }
`; `;
export const ProductVariantBulkUpdateMutation = gql`
mutation ProductVariantBulkUpdate(
$product: ID!
$input: [ProductVariantBulkUpdateInput!]!
$errorPolicy: ErrorPolicyEnum
) {
productVariantBulkUpdate(
errorPolicy: $errorPolicy
product: $product
variants: $input
) {
errors {
...ProductVariantBulkError
}
results {
errors {
...ProductVariantBulkError
}
}
}
}
`;

View file

@ -0,0 +1,63 @@
import { isCurrentRow } from "./datagrid";
describe("isCurrentRow", () => {
test("should return true when variant index is equal to datagrid row index and no removed rows", () => {
// Arrange & Act
const datagridChangeRowIndex = 1;
const variantIndex = 1;
const datagridRemoveRowsIds = [];
// Assert
expect(
isCurrentRow(datagridChangeRowIndex, variantIndex, datagridRemoveRowsIds),
).toEqual(true);
});
test("should return true when variant index is equal to datagrid row index and removed rows contain higher rows ids", () => {
// Arrange & Act
const datagridChangeRowIndex = 1;
const variantIndex = 1;
const datagridRemoveRowsIds = [4, 5, 6];
// Assert
expect(
isCurrentRow(datagridChangeRowIndex, variantIndex, datagridRemoveRowsIds),
).toEqual(true);
});
test("should return false when variant index is not equal to datagrid row index and removed rows contains prev row id", () => {
// Arrange & Act
const datagridChangeRowIndex = 2;
const variantIndex = 1;
const datagridRemoveRowsIds = [1];
// Assert
expect(
isCurrentRow(datagridChangeRowIndex, variantIndex, datagridRemoveRowsIds),
).toEqual(true);
});
test("should return false when variant index is not equal to datagrid row index ", () => {
// Arrange & Act
const datagridChangeRowIndex = 1;
const variantIndex = 2;
const datagridRemoveRowsIds = [];
// Assert
expect(
isCurrentRow(datagridChangeRowIndex, variantIndex, datagridRemoveRowsIds),
).toEqual(false);
});
test("should return false when variant index is equal to datagrid row index and removed rows contains prev row id", () => {
// Arrange & Act
const datagridChangeRowIndex = 2;
const variantIndex = 2;
const datagridRemoveRowsIds = [1];
// Assert
expect(
isCurrentRow(datagridChangeRowIndex, variantIndex, datagridRemoveRowsIds),
).toEqual(false);
});
});

View file

@ -16,3 +16,17 @@ export const getColumnChannelAvailability = makeGetColumnData(
/^availableInChannel:(.*)/, /^availableInChannel:(.*)/,
); );
export const getColumnStock = makeGetColumnData(/^stock:(.*)/); export const getColumnStock = makeGetColumnData(/^stock:(.*)/);
export const getColumnName = (column: string) => {
const splited = column.split(":");
return splited[0];
};
export const isCurrentRow = (
datagridChangeIndex: number,
variantIndex: number,
datagridRemovedRowsIds: number[],
) =>
datagridChangeIndex ===
variantIndex +
datagridRemovedRowsIds.filter(index => index <= variantIndex).length;

View file

@ -0,0 +1,50 @@
import { DatagridChange } from "@dashboard/components/Datagrid/useDatagridChange";
import { getAttributeData } from "./attributes";
describe("getAttributeData", () => {
test("should filter and map data to attribute format", () => {
// Arrage
const changeData: DatagridChange[] = [
{ column: "attribute:1", row: 1, data: { value: { value: "test" } } },
{ column: "attribute:2", row: 1, data: { value: { value: "test2" } } },
];
// Act
const attributes = getAttributeData(changeData, 1, []);
// Assert
expect(attributes).toEqual([
{ id: "1", values: ["test"] },
{ id: "2", values: ["test2"] },
]);
});
test("should return empty array when no changes for given row", () => {
// Arrage
const changeData: DatagridChange[] = [
{ column: "attribute:1", row: 1, data: { value: { value: "test" } } },
{ column: "attribute:2", row: 1, data: { value: { value: "test2" } } },
];
// Act
const attributes = getAttributeData(changeData, 2, []);
// Assert
expect(attributes).toEqual([]);
});
test("should return empty array when no changes for attributes column", () => {
// Arrage
const changeData: DatagridChange[] = [
{ column: "channel:1", row: 1, data: { value: { value: "test" } } },
{ column: "channel:2", row: 1, data: { value: { value: "test2" } } },
];
// Act
const attributes = getAttributeData(changeData, 1, []);
// Assert
expect(attributes).toEqual([]);
});
});

View file

@ -0,0 +1,29 @@
import { DatagridChange } from "@dashboard/components/Datagrid/useDatagridChange";
import {
getColumnAttribute,
isCurrentRow,
} from "@dashboard/products/utils/datagrid";
export function getAttributeData(
data: DatagridChange[],
currentIndex: number,
removedIds: number[],
) {
return data
.filter(change => isCurrentRow(change.row, currentIndex, removedIds))
.filter(byHavingAnyAttribute)
.map(toAttributeData);
}
function byHavingAnyAttribute(change: DatagridChange): boolean {
return !!getColumnAttribute(change.column);
}
function toAttributeData(change: DatagridChange) {
const attributeId = getColumnAttribute(change.column);
return {
id: attributeId,
values: change.data.value.value ? [change.data.value.value] : [],
};
}

View file

@ -0,0 +1,301 @@
import { DatagridChangeOpts } from "@dashboard/components/Datagrid/useDatagridChange";
import { ProductFragment } from "@dashboard/graphql";
import {
getUpdateVariantChannelInputs,
getVariantChannelsInputs,
} from "./channel";
const channelListings = [
{
id: "UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNjA=",
channel: {
id: "Q2hhbm5lbDox",
},
},
{
id: "UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNjD=",
channel: {
id: "Q2hhbm5lbDot",
},
},
];
describe("getUpdateVariantChannelInputs", () => {
test("should handle updated channels", () => {
// Arrange
const changeData: DatagridChangeOpts = {
added: [],
removed: [],
updates: [
{
column: "channel:Q2hhbm5lbDox",
row: 1,
data: {
kind: "money-cell",
value: 43343,
currency: "USD",
},
},
{
column: "channel:Q2hhbm5lbDot",
row: 1,
data: {
kind: "money-cell",
value: 123,
currency: "PLN",
},
},
],
};
// Act
const channels = getUpdateVariantChannelInputs(changeData, 1, {
channelListings,
} as ProductFragment["variants"][number]);
// Assert
expect(channels).toEqual({
create: [],
remove: [],
update: [
{
channelListing: "UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNjA=",
price: 43343,
},
{
channelListing: "UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNjD=",
price: 123,
},
],
});
});
test("should handle removed channels", () => {
// Arrange
const changeData: DatagridChangeOpts = {
added: [],
removed: [],
updates: [
{
data: false,
column: "availableInChannel:Q2hhbm5lbDox",
row: 1,
},
{
data: false,
column: "availableInChannel:Q2hhbm5lbDot",
row: 1,
},
],
};
// Act
const channels = getUpdateVariantChannelInputs(changeData, 1, {
channelListings,
} as ProductFragment["variants"][number]);
// Assert
expect(channels).toEqual({
create: [],
remove: [
"UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNjA=",
"UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNjD=",
],
update: [],
});
});
test("should handle created channels", () => {
// Arrange
const changeData: DatagridChangeOpts = {
added: [],
removed: [],
updates: [
{
data: true,
column: "availableInChannel:Q2hhbm5lbDod",
row: 1,
},
{
data: {
kind: "money-cell",
value: 3434,
currency: "USD",
},
column: "channel:Q2hhbm5lbDod",
row: 1,
},
],
};
// Act
const channels = getUpdateVariantChannelInputs(changeData, 1, {
channelListings,
} as ProductFragment["variants"][number]);
// Assert
expect(channels).toEqual({
create: [
{
channelId: "Q2hhbm5lbDod",
price: 3434,
},
],
remove: [],
update: [],
});
});
test("should return empty arrays when no changes for given row", () => {
// Arrange
const changeData: DatagridChangeOpts = {
added: [],
removed: [],
updates: [
{
column: "channel:Q2hhbm5lbDox",
row: 11,
data: {
kind: "money-cell",
value: 43343,
currency: "USD",
},
},
],
};
// Act
const channels = getUpdateVariantChannelInputs(changeData, 1, {
channelListings,
} as ProductFragment["variants"][number]);
// Assert
expect(channels).toEqual({
create: [],
remove: [],
update: [],
});
});
test("should return empty arrays when no changes for given column", () => {
// Arrange
const changeData: DatagridChangeOpts = {
added: [],
removed: [],
updates: [
{ column: "attribute:1", row: 1, data: { value: { value: "test" } } },
{ column: "attribute:2", row: 1, data: { value: { value: "test2" } } },
],
};
// Act
const channels = getUpdateVariantChannelInputs(changeData, 1, {
channelListings,
} as ProductFragment["variants"][number]);
// Assert
expect(channels).toEqual({
create: [],
remove: [],
update: [],
});
});
});
describe("getVariantChannelsInputs", () => {
test("should filter and map change data to channel format", () => {
// Arrange
const changeData: DatagridChangeOpts = {
added: [],
removed: [],
updates: [
{
column: "channel:Q2hhbm5lbDox",
row: 1,
data: {
kind: "money-cell",
value: 43343,
currency: "USD",
},
},
],
};
// Act
const channels = getVariantChannelsInputs(changeData, 1);
// Assert
expect(channels).toEqual([
{
channelId: "Q2hhbm5lbDox",
price: 43343,
},
]);
});
test("should filter out changes with null prices", () => {
// Arrange
const changeData: DatagridChangeOpts = {
added: [],
removed: [],
updates: [
{
data: false,
column: "availableInChannel:Q2hhbm5lbDox",
row: 1,
},
],
};
// Act
const channels = getVariantChannelsInputs(changeData, 1);
// Assert
expect(channels).toEqual([]);
});
test("should return empty arrays when no changes for given row", () => {
// Arrange
const changeData: DatagridChangeOpts = {
added: [],
removed: [],
updates: [
{
column: "channel:Q2hhbm5lbDox",
row: 11,
data: {
kind: "money-cell",
value: 43343,
currency: "USD",
},
},
],
};
// Act
const channels = getVariantChannelsInputs(changeData, 1);
// Assert
expect(channels).toEqual([]);
});
test("should return empty arrays when no changes for given column", () => {
// Arrange
const changeData: DatagridChangeOpts = {
added: [],
removed: [],
updates: [
{ column: "attribute:1", row: 1, data: { value: { value: "test" } } },
{ column: "attribute:2", row: 1, data: { value: { value: "test2" } } },
],
};
// Act
const channels = getVariantChannelsInputs(changeData, 1);
// Assert
expect(channels).toEqual([]);
});
});

View file

@ -0,0 +1,129 @@
import {
DatagridChange,
DatagridChangeOpts,
} from "@dashboard/components/Datagrid/useDatagridChange";
import {
ProductFragment,
ProductVariantChannelListingAddInput,
ProductVariantChannelListingUpdateInput,
} from "@dashboard/graphql";
import {
getColumnChannel,
getColumnChannelAvailability,
} from "@dashboard/products/utils/datagrid";
export function getUpdateVariantChannelInputs(
data: DatagridChangeOpts,
index: number,
variant: ProductFragment["variants"][number],
): ProductVariantChannelListingUpdateInput {
return data.updates
.filter(byCurrentRowByIndex(index, data))
.map(availabilityToChannelColumn)
.filter(byChannelColumn)
.reduce(byColumn, [])
.map(dataGridChangeToFlatChannel)
.reduce<ProductVariantChannelListingUpdateInput>(
toUpdateChannelData(variant),
{
create: [],
remove: [],
update: [],
},
);
}
export function getVariantChannelsInputs(
data: DatagridChangeOpts,
index: number,
): ProductVariantChannelListingAddInput[] {
return data.updates
.filter(byCurrentRowByIndex(index, data))
.map(availabilityToChannelColumn)
.filter(byChannelColumn)
.reduce(byColumn, [])
.map(dataGridChangeToFlatChannel)
.filter(byNotNullPrice);
}
function byCurrentRowByIndex(index: number, data: DatagridChangeOpts) {
return (change: DatagridChange) => {
const totalRemoved = data.removed.filter(r => r <= index).length;
return change.row === index + totalRemoved;
};
}
function byChannelColumn(change: DatagridChange) {
return getColumnChannel(change.column);
}
function availabilityToChannelColumn(change: DatagridChange) {
const availabilityChannelId = getColumnChannelAvailability(change.column);
if (availabilityChannelId) {
return {
data: {
value: change.data ? 0 : null,
},
column: `channel:${availabilityChannelId}`,
};
}
return change;
}
function byColumn(prev: DatagridChange[], change: DatagridChange) {
const index = prev.findIndex(p => p.column === change.column);
if (index > -1) {
prev[index] = change;
return prev;
}
return prev.concat(change);
}
function dataGridChangeToFlatChannel(change: DatagridChange) {
return {
channelId: getColumnChannel(change.column),
price: change.data.value,
};
}
function byNotNullPrice(
change: ReturnType<typeof dataGridChangeToFlatChannel>,
) {
return change.price !== null;
}
function toUpdateChannelData(variant: ProductFragment["variants"][number]) {
return (
acc: ProductVariantChannelListingUpdateInput,
channel: ReturnType<typeof dataGridChangeToFlatChannel>,
) => {
const variantChannel = variant.channelListings.find(
c => c.channel.id === channel.channelId,
);
if (channel.price === null) {
if (variantChannel) {
acc.remove.push(variantChannel.id);
}
return acc;
}
if (variantChannel) {
acc.update.push({
channelListing: variantChannel.id,
price: channel.price,
});
} else {
acc.create.push({
channelId: channel.channelId,
price: channel.price,
});
}
return acc;
};
}

View file

@ -0,0 +1,45 @@
import { DatagridChange } from "@dashboard/components/Datagrid/useDatagridChange";
import { getNameData } from "./name";
describe("getNameData", () => {
test("should return name data", () => {
// Arrange
const changeData: DatagridChange[] = [
{ column: "name", row: 1, data: "Joe" },
{ column: "attribute:2", row: 1, data: { value: { value: "test2" } } },
];
// Act
const name = getNameData(changeData, 1, []);
// Assert
expect(name).toEqual("Joe");
});
test("should return undefined when no changes for given row", () => {
// Arrange
const changeData: DatagridChange[] = [
{ column: "attribute:2", row: 1, data: { value: { value: "test2" } } },
];
// Act
const name = getNameData(changeData, 1, []);
// Assert
expect(name).toEqual(undefined);
});
test("should return undefined when no name column for given row", () => {
// Arrange
const changeData: DatagridChange[] = [
{ column: "name", row: 2, data: "Joe" },
];
// Act
const name = getNameData(changeData, 1, []);
// Assert
expect(name).toEqual(undefined);
});
});

View file

@ -0,0 +1,14 @@
import { DatagridChange } from "@dashboard/components/Datagrid/useDatagridChange";
import { isCurrentRow } from "@dashboard/products/utils/datagrid";
export function getNameData(
data: DatagridChange[],
currentIndex: number,
removedIds: number[],
): string | undefined {
return data.find(
change =>
change.column === "name" &&
isCurrentRow(change.row, currentIndex, removedIds),
)?.data;
}

View file

@ -0,0 +1,45 @@
import { DatagridChange } from "@dashboard/components/Datagrid/useDatagridChange";
import { getSkuData } from "./sku";
describe("getSkuData", () => {
test("should return name data", () => {
// Arrange
const changeData: DatagridChange[] = [
{ column: "sku", row: 1, data: "123" },
{ column: "attribute:2", row: 1, data: { value: { value: "test2" } } },
];
// Act
const name = getSkuData(changeData, 1, []);
// Assert
expect(name).toEqual("123");
});
test("should return undefined when no changes for given row", () => {
// Arrange
const changeData: DatagridChange[] = [
{ column: "attribute:2", row: 1, data: { value: { value: "test2" } } },
];
// Act
const name = getSkuData(changeData, 1, []);
// Assert
expect(name).toEqual(undefined);
});
test("should return undefined when no name column for given row", () => {
// Arrange
const changeData: DatagridChange[] = [
{ column: "sku", row: 2, data: "Joe" },
];
// Act
const name = getSkuData(changeData, 1, []);
// Assert
expect(name).toEqual(undefined);
});
});

View file

@ -0,0 +1,14 @@
import { DatagridChange } from "@dashboard/components/Datagrid/useDatagridChange";
import { isCurrentRow } from "@dashboard/products/utils/datagrid";
export function getSkuData(
data: DatagridChange[],
currentIndex: number,
removedIds: number[],
): string | undefined {
return data.find(
change =>
change.column === "sku" &&
isCurrentRow(change.row, currentIndex, removedIds),
)?.data;
}

View file

@ -0,0 +1,88 @@
import { numberCellEmptyValue } from "@dashboard/components/Datagrid/NumberCell";
import { DatagridChange } from "@dashboard/components/Datagrid/useDatagridChange";
import {
ProductFragment,
ProductVariantStocksUpdateInput,
} from "@dashboard/graphql";
import {
getColumnStock,
isCurrentRow,
} from "@dashboard/products/utils/datagrid";
export function getStockData(
data: DatagridChange[],
currentIndex: number,
removedIds: number[],
) {
return data
.filter(change => byHavingStockColumn(change, currentIndex, removedIds))
.map(toStockData)
.filter(byStockWithQuantity);
}
export function getVaraintUpdateStockData(
data: DatagridChange[],
currentIndex: number,
removedIds: number[],
variant: ProductFragment["variants"][number],
) {
return data
.filter(change => byHavingStockColumn(change, currentIndex, removedIds))
.map(toStockData)
.reduce<ProductVariantStocksUpdateInput>(toUpdateStockData(variant), {
create: [],
update: [],
remove: [],
});
}
function toUpdateStockData(variant: ProductFragment["variants"][number]) {
return (
acc: ProductVariantStocksUpdateInput,
stock: ReturnType<typeof toStockData>,
) => {
const variantStock = variant.stocks.find(
variantStock => variantStock.warehouse.id === stock.warehouse,
);
if (stock.quantity === numberCellEmptyValue) {
if (variantStock) {
acc.remove.push(variantStock.id);
}
return acc;
}
if (variantStock) {
acc.update.push({
quantity: stock.quantity as number,
stock: variantStock.id,
});
} else {
acc.create.push(stock);
}
return acc;
};
}
function toStockData(change: DatagridChange) {
return {
warehouse: getColumnStock(change.column),
quantity: change.data.value,
};
}
function byStockWithQuantity(stock: { quantity: unknown }) {
return stock.quantity !== numberCellEmptyValue;
}
function byHavingStockColumn(
change: DatagridChange,
currentIndex: number,
removedIds: number[],
) {
return (
getColumnStock(change.column) &&
isCurrentRow(change.row, currentIndex, removedIds)
);
}

View file

@ -0,0 +1,198 @@
import { numberCellEmptyValue } from "@dashboard/components/Datagrid/NumberCell";
import { DatagridChange } from "@dashboard/components/Datagrid/useDatagridChange";
import { ProductFragment } from "@dashboard/graphql";
import { getStockData, getVaraintUpdateStockData } from "./stock";
describe("getStockData", () => {
test("should filter and map to stock format", () => {
// Arrange
const changeData: DatagridChange[] = [
{
data: false,
column: "availableInChannel:Q2hhbm5lbDox",
row: 1,
},
{ column: "attribute:2", row: 2, data: { value: { value: "test2" } } },
{ column: "stock:Q2hhbm5lbDox", row: 1, data: { value: "12345" } },
{ column: "stock:Q2hhbm5lbDot", row: 1, data: { value: "5666" } },
];
// Act
const stocks = getStockData(changeData, 1, []);
// Assert
expect(stocks).toEqual([
{
warehouse: "Q2hhbm5lbDox",
quantity: "12345",
},
{
warehouse: "Q2hhbm5lbDot",
quantity: "5666",
},
]);
});
test("should return empty array when no changes for given row", () => {
// Arrange
const changeData: DatagridChange[] = [
{ column: "attribute:2", row: 1, data: { value: { value: "test2" } } },
];
// Act
const stocks = getStockData(changeData, 1, []);
// Assert
expect(stocks).toEqual([]);
});
test("should return empty string when no name column for given row", () => {
// Arrange
const changeData: DatagridChange[] = [
{ column: "name", row: 2, data: "Joe" },
];
// Act
const stocks = getStockData(changeData, 1, []);
// Assert
expect(stocks).toEqual([]);
});
});
describe("getVaraintUpdateStockData", () => {
const stocks = [
{
id: "UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNjA=",
warehouse: {
id: "Q2hhbm5lbDox",
},
},
{
id: "UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNjD=",
warehouse: {
id: "Q2hhbm5lbDot",
},
},
];
test("should handle update stocks", () => {
// Arrange
const changeData: DatagridChange[] = [
{ column: "stock:Q2hhbm5lbDox", row: 1, data: { value: "12345" } },
{ column: "stock:Q2hhbm5lbDot", row: 1, data: { value: "5666" } },
];
// Act
const variantStocks = getVaraintUpdateStockData(changeData, 1, [], {
stocks,
} as ProductFragment["variants"][number]);
// Assert
expect(variantStocks).toEqual({
create: [],
remove: [],
update: [
{
stock: "UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNjA=",
quantity: "12345",
},
{
stock: "UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNjD=",
quantity: "5666",
},
],
});
});
test("should handle remove stocks", () => {
// Arrange
const changeData: DatagridChange[] = [
{
column: "stock:Q2hhbm5lbDox",
row: 1,
data: { value: numberCellEmptyValue },
},
{
column: "stock:Q2hhbm5lbDot",
row: 1,
data: { value: numberCellEmptyValue },
},
];
// Act
const variantStocks = getVaraintUpdateStockData(changeData, 1, [], {
stocks,
} as ProductFragment["variants"][number]);
// Assert
expect(variantStocks).toEqual({
create: [],
remove: [
"UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNjA=",
"UHJvZHVjdFZhcmlhbnRDaGFubmVsTGlzdGluZzoyNjD=",
],
update: [],
});
});
test("should handle create stocks", () => {
// Arrange
const changeData: DatagridChange[] = [
{ column: "stock:Q2hhbm5lbDof", row: 1, data: { value: "12345" } },
{ column: "stock:Q2hhbm5lbDod", row: 1, data: { value: "5666" } },
];
// Act
const variantStocks = getVaraintUpdateStockData(changeData, 1, [], {
stocks,
} as ProductFragment["variants"][number]);
// Assert
expect(variantStocks).toEqual({
create: [
{
warehouse: "Q2hhbm5lbDof",
quantity: "12345",
},
{
warehouse: "Q2hhbm5lbDod",
quantity: "5666",
},
],
remove: [],
update: [],
});
});
test("should return empty array when no changes for given row", () => {
// Arrange
const changeData: DatagridChange[] = [
{ column: "attribute:2", row: 1, data: { value: { value: "test2" } } },
];
// Act
const variantStocks = getVaraintUpdateStockData(changeData, 1, [], {
stocks,
} as ProductFragment["variants"][number]);
// Assert
expect(variantStocks).toEqual({ create: [], remove: [], update: [] });
});
test("should return empty string when no name column for given row", () => {
// Arrange
const changeData: DatagridChange[] = [
{ column: "name", row: 2, data: "Joe" },
];
// Act
const variantStocks = getVaraintUpdateStockData(changeData, 1, [], {
stocks,
} as ProductFragment["variants"][number]);
// Assert
expect(variantStocks).toEqual({ create: [], remove: [], update: [] });
});
});

View file

@ -1,24 +1,19 @@
import { FetchResult } from "@apollo/client"; import { FetchResult } from "@apollo/client";
import { import {
ProductChannelListingUpdateMutation,
ProductErrorCode, ProductErrorCode,
ProductVariantBulkCreateMutation, ProductVariantBulkCreateMutation,
ProductVariantChannelListingUpdateMutation, ProductVariantBulkErrorCode,
ProductVariantChannelListingUpdateMutationVariables, ProductVariantBulkErrorFragment,
StockInput, ProductVariantBulkUpdateMutation,
VariantDatagridStockUpdateMutation,
VariantDatagridStockUpdateMutationVariables,
VariantDatagridUpdateMutation,
VariantDatagridUpdateMutationVariables,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import { hasMutationErrors } from "@dashboard/misc";
export type ProductVariantListError = export type ProductVariantListError =
| { | {
__typename: "DatagridError"; __typename: "DatagridError";
attributes: string[] | null; attributes: string[] | null;
error: ProductErrorCode; error: ProductVariantBulkErrorCode;
variantId: string; variantId: string;
field?: string;
type: "variantData"; type: "variantData";
} }
| { | {
@ -29,7 +24,7 @@ export type ProductVariantListError =
} }
| { | {
__typename: "DatagridError"; __typename: "DatagridError";
error: ProductErrorCode; error: ProductVariantBulkErrorCode;
variantId: string; variantId: string;
channelIds: string[]; channelIds: string[];
type: "channel"; type: "channel";
@ -41,80 +36,101 @@ export type ProductVariantListError =
type: "create"; type: "create";
}; };
export function getProductVariantListErrors( export function getCreateVariantMutationError(
productChannelsUpdateResult: FetchResult<ProductChannelListingUpdateMutation>, result: FetchResult<ProductVariantBulkCreateMutation>,
variantMutationResults: FetchResult[],
): ProductVariantListError[] { ): ProductVariantListError[] {
return [productChannelsUpdateResult, ...variantMutationResults] return result.data.productVariantBulkCreate.errors.map<ProductVariantListError>(
.filter(hasMutationErrors) error => ({
.flatMap(result => { __typename: "DatagridError",
if (result.data.productVariantChannelListingUpdate) { type: "create",
const data = result.data as ProductVariantChannelListingUpdateMutation; index: error.index,
return data.productVariantChannelListingUpdate.errors.map< error: error.code,
ProductVariantListError }),
>(error => ({ );
__typename: "DatagridError", }
type: "channel",
error: error.code, export function getVariantUpdateMutationErrors(
variantId: (result.extensions mutationResult: FetchResult<ProductVariantBulkUpdateMutation>,
.variables as ProductVariantChannelListingUpdateMutationVariables) varaintsIds: string[],
.id, ): ProductVariantListError[] {
channelIds: error.channels, const { productVariantBulkUpdate } = mutationResult.data;
})); const generalErrors = productVariantBulkUpdate.errors;
} const variantsErrors = productVariantBulkUpdate.results.flatMap(
res => res.errors,
if (result.data.productVariantStocksUpdate) { );
const data = result.data as VariantDatagridStockUpdateMutation; const allErrors = [...generalErrors, ...variantsErrors];
const variables = result.extensions
.variables as VariantDatagridStockUpdateMutationVariables; return [
return [ ...getChannelErrors(allErrors, varaintsIds),
...data.productVariantStocksUpdate.errors.map< ...getStockErrors(allErrors, varaintsIds),
ProductVariantListError ...getRestOfErrors(allErrors, varaintsIds),
>(error => ({ ];
__typename: "DatagridError", }
type: "stock",
variantId: (variables as VariantDatagridStockUpdateMutationVariables) function getChannelErrors(
.id, errors: ProductVariantBulkErrorFragment[],
warehouseId: (variables.stocks as StockInput[])[error.index] varaintsIds: string[],
.warehouse, ) {
})), return errors.reduce<ProductVariantListError[]>((acc, error, index) => {
...data.productVariantStocksDelete.errors.map< if (error.channels?.length) {
ProductVariantListError const variantId = varaintsIds[index];
>(() => ({
__typename: "DatagridError", acc.push({
type: "stock", __typename: "DatagridError",
variantId: (variables as VariantDatagridStockUpdateMutationVariables) type: "channel",
.id, error: error.code,
warehouseId: null, variantId,
})), channelIds: error.channels,
]; });
} }
if (result.data.productVariantUpdate) { return acc;
const data = result.data as VariantDatagridUpdateMutation; }, []);
const variables = result.extensions }
.variables as VariantDatagridUpdateMutationVariables;
return data.productVariantUpdate.errors.map<ProductVariantListError>( function getStockErrors(
error => ({ errors: ProductVariantBulkErrorFragment[],
__typename: "DatagridError", varaintsIds: string[],
type: "variantData", ) {
variantId: (variables as VariantDatagridUpdateMutationVariables).id, return errors.reduce<ProductVariantListError[]>((acc, error, index) => {
error: error.code, if (error.warehouses?.length) {
attributes: error.attributes, const variantId = varaintsIds[index];
}),
); acc.push(
} ...error.warehouses.map(
warehouse =>
if (result.data.productVariantBulkCreate) { ({
const data = result.data as ProductVariantBulkCreateMutation; __typename: "DatagridError",
return data.productVariantBulkCreate.errors.map< variantId,
ProductVariantListError warehouseId: warehouse,
>(error => ({ type: "stock",
__typename: "DatagridError", } as const),
type: "create", ),
index: error.index, );
error: error.code, }
}));
} return acc;
}); }, []);
}
function getRestOfErrors(
errors: ProductVariantBulkErrorFragment[],
varaintsIds: string[],
) {
return errors.reduce<ProductVariantListError[]>((acc, error, index) => {
if (!error.warehouses?.length && !error.channels?.length) {
const variantId = varaintsIds[index];
acc.push({
__typename: "DatagridError",
type: "variantData",
variantId,
error: error.code,
attributes: error.attributes,
field: error.field,
});
}
return acc;
}, []);
} }

View file

@ -1,4 +1,3 @@
import { FetchResult } from "@apollo/client";
import { import {
mergeAttributeValueDeleteErrors, mergeAttributeValueDeleteErrors,
mergeFileUploadErrors, mergeFileUploadErrors,
@ -9,7 +8,7 @@ import {
} from "@dashboard/attributes/utils/handlers"; } from "@dashboard/attributes/utils/handlers";
import { import {
AttributeErrorFragment, AttributeErrorFragment,
BulkProductErrorFragment, ErrorPolicyEnum,
MetadataErrorFragment, MetadataErrorFragment,
ProductChannelListingErrorFragment, ProductChannelListingErrorFragment,
ProductErrorFragment, ProductErrorFragment,
@ -22,38 +21,34 @@ import {
useProductUpdateMutation, useProductUpdateMutation,
useProductVariantBulkCreateMutation, useProductVariantBulkCreateMutation,
useProductVariantBulkDeleteMutation, useProductVariantBulkDeleteMutation,
useProductVariantBulkUpdateMutation,
useUpdateMetadataMutation, useUpdateMetadataMutation,
useUpdatePrivateMetadataMutation, useUpdatePrivateMetadataMutation,
useVariantDatagridChannelListingUpdateMutation,
useVariantDatagridStockUpdateMutation,
useVariantDatagridUpdateMutation,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import useNotifier from "@dashboard/hooks/useNotifier"; import useNotifier from "@dashboard/hooks/useNotifier";
import { commonMessages } from "@dashboard/intl"; import { commonMessages } from "@dashboard/intl";
import { ProductUpdateSubmitData } from "@dashboard/products/components/ProductUpdatePage/types"; import { ProductUpdateSubmitData } from "@dashboard/products/components/ProductUpdatePage/types";
import { getVariantChannelsInputs } from "@dashboard/products/components/ProductVariants/datagrid/getVariantChannelsInputs";
import {
getStockInputs,
getStocks,
getVariantChannels,
getVariantInput,
getVariantInputs,
} from "@dashboard/products/components/ProductVariants/utils";
import { getProductErrorMessage } from "@dashboard/utils/errors"; import { getProductErrorMessage } from "@dashboard/utils/errors";
import createMetadataUpdateHandler from "@dashboard/utils/handlers/metadataUpdateHandler"; import createMetadataUpdateHandler from "@dashboard/utils/handlers/metadataUpdateHandler";
import { useState } from "react"; import { useState } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { getProductVariantListErrors, ProductVariantListError } from "./errors";
import { import {
getCreateVariantMutationError,
getVariantUpdateMutationErrors,
ProductVariantListError,
} from "./errors";
import {
getBulkVariantUpdateInputs,
getCreateVariantInput,
getProductChannelsUpdateVariables, getProductChannelsUpdateVariables,
getProductUpdateVariables, getProductUpdateVariables,
hasProductChannelsUpdate,
} from "./utils"; } from "./utils";
export type UseProductUpdateHandlerError = export type UseProductUpdateHandlerError =
| ProductErrorWithAttributesFragment | ProductErrorWithAttributesFragment
| ProductErrorFragment | ProductErrorFragment
| BulkProductErrorFragment
| AttributeErrorFragment | AttributeErrorFragment
| UploadErrorFragment | UploadErrorFragment
| ProductChannelListingErrorFragment | ProductChannelListingErrorFragment
@ -62,6 +57,7 @@ export type UseProductUpdateHandlerError =
type UseProductUpdateHandler = ( type UseProductUpdateHandler = (
data: ProductUpdateSubmitData, data: ProductUpdateSubmitData,
) => Promise<Array<UseProductUpdateHandlerError | MetadataErrorFragment>>; ) => Promise<Array<UseProductUpdateHandlerError | MetadataErrorFragment>>;
interface UseProductUpdateHandlerOpts { interface UseProductUpdateHandlerOpts {
called: boolean; called: boolean;
loading: boolean; loading: boolean;
@ -83,33 +79,26 @@ export function useProductUpdateHandler(
const [updateMetadata] = useUpdateMetadataMutation({}); const [updateMetadata] = useUpdateMetadataMutation({});
const [updatePrivateMetadata] = useUpdatePrivateMetadataMutation({}); const [updatePrivateMetadata] = useUpdatePrivateMetadataMutation({});
const [updateStocks] = useVariantDatagridStockUpdateMutation({}); const [updateVariants] = useProductVariantBulkUpdateMutation();
const [updateVariant] = useVariantDatagridUpdateMutation();
const [createVariants] = useProductVariantBulkCreateMutation(); const [createVariants] = useProductVariantBulkCreateMutation();
const [deleteVariants] = useProductVariantBulkDeleteMutation(); const [deleteVariants] = useProductVariantBulkDeleteMutation();
const [uploadFile] = useFileUploadMutation(); const [uploadFile] = useFileUploadMutation();
const [updateProduct, updateProductOpts] = useProductUpdateMutation(); const [updateProduct, updateProductOpts] = useProductUpdateMutation();
const [ const [updateChannels, updateChannelsOpts] =
updateChannels, useProductChannelListingUpdateMutation({
updateChannelsOpts, onCompleted: data => {
] = useProductChannelListingUpdateMutation({ if (!!data.productChannelListingUpdate.errors.length) {
onCompleted: data => { data.productChannelListingUpdate.errors.forEach(error =>
if (!!data.productChannelListingUpdate.errors.length) { notify({
data.productChannelListingUpdate.errors.forEach(error => status: "error",
notify({ text: getProductErrorMessage(error, intl),
status: "error", }),
text: getProductErrorMessage(error, intl), );
}), }
); },
} });
},
});
const [
updateVariantChannels,
] = useVariantDatagridChannelListingUpdateMutation();
const [deleteAttributeValue] = useAttributeValueDeleteMutation(); const [deleteAttributeValue] = useAttributeValueDeleteMutation();
@ -117,84 +106,103 @@ export function useProductUpdateHandler(
data: ProductUpdateSubmitData, data: ProductUpdateSubmitData,
): Promise<UseProductUpdateHandlerError[]> => { ): Promise<UseProductUpdateHandlerError[]> => {
let errors: UseProductUpdateHandlerError[] = []; let errors: UseProductUpdateHandlerError[] = [];
const variantErrors: ProductVariantListError[] = [];
const uploadFilesResult = await handleUploadMultipleFiles( const uploadFilesResult = await handleUploadMultipleFiles(
data.attributesWithNewFileValue, data.attributesWithNewFileValue,
variables => uploadFile({ variables }), variables => uploadFile({ variables }),
); );
const deleteAttributeValuesResult = await handleDeleteMultipleAttributeValues( const deleteAttributeValuesResult =
data.attributesWithNewFileValue, await handleDeleteMultipleAttributeValues(
product?.attributes, data.attributesWithNewFileValue,
variables => deleteAttributeValue({ variables }), product?.attributes,
variables => deleteAttributeValue({ variables }),
);
const updateProductChannelsData = getProductChannelsUpdateVariables(
product,
data,
); );
if (hasProductChannelsUpdate(updateProductChannelsData.input)) {
const updateChannelsResult = await updateChannels({
variables: updateProductChannelsData,
});
errors = [
...errors,
...updateChannelsResult.data.productChannelListingUpdate.errors,
];
}
if (data.variants.removed.length > 0) {
const deleteVaraintsResult = await deleteVariants({
variables: {
ids: data.variants.removed.map(index => product.variants[index].id),
},
});
errors = [
...errors,
...deleteVaraintsResult.data.productVariantBulkDelete.errors,
];
}
const updateProductResult = await updateProduct({
variables: getProductUpdateVariables(product, data, uploadFilesResult),
});
if (data.variants.added.length > 0) {
const createVariantsResults = await createVariants({
variables: {
id: product.id,
inputs: data.variants.added.map(index => ({
...getCreateVariantInput(data.variants, index),
})),
},
});
const createVariantsErrors = getCreateVariantMutationError(
createVariantsResults,
);
errors.push(...createVariantsErrors);
variantErrors.push(...createVariantsErrors);
}
if (data.variants.updates.length > 0) {
const updateInputdData = getBulkVariantUpdateInputs(
product.variants,
data.variants,
);
if (updateInputdData.length) {
const updateVariantsResults = await updateVariants({
variables: {
product: product.id,
input: updateInputdData,
errorPolicy: ErrorPolicyEnum.REJECT_FAILED_ROWS,
},
});
const updateVariantsErrors = getVariantUpdateMutationErrors(
updateVariantsResults,
updateInputdData.map(data => data.id),
);
variantErrors.push(...updateVariantsErrors);
errors.push(...updateVariantsErrors);
}
}
errors = [ errors = [
...errors, ...errors,
...mergeFileUploadErrors(uploadFilesResult), ...mergeFileUploadErrors(uploadFilesResult),
...mergeAttributeValueDeleteErrors(deleteAttributeValuesResult), ...mergeAttributeValueDeleteErrors(deleteAttributeValuesResult),
...updateProductResult.data.productUpdate.errors,
]; ];
if (data.variants.removed.length > 0) {
errors.push(
...(
await deleteVariants({
variables: {
ids: data.variants.removed.map(
index => product.variants[index].id,
),
},
})
).data.productVariantBulkDelete.errors,
);
}
const result = await updateProduct({
variables: getProductUpdateVariables(product, data, uploadFilesResult),
});
errors = [...errors, ...result.data.productUpdate.errors];
const productChannelsUpdateResult = await updateChannels({
variables: getProductChannelsUpdateVariables(product, data),
});
const mutations: Array<Promise<FetchResult>> = [
...getStocks(product.variants, data.variants).map(variables =>
updateStocks({ variables }),
),
...getVariantInputs(product.variants, data.variants).map(variables =>
updateVariant({ variables }),
),
...getVariantChannels(product.variants, data.variants).map(variables =>
updateVariantChannels({
variables,
}),
),
];
if (data.variants.added.length > 0) {
mutations.push(
createVariants({
variables: {
id: product.id,
inputs: data.variants.added.map(index => ({
...getVariantInput(data.variants, index),
channelListings: getVariantChannelsInputs(data.variants, index),
stocks: getStockInputs(data.variants, index).stocks,
})),
},
}),
);
}
const variantMutationResults = await Promise.all<FetchResult>(mutations);
const variantErrors = getProductVariantListErrors(
productChannelsUpdateResult,
variantMutationResults,
);
errors = [...errors, ...variantErrors];
setVariantListErrors(variantErrors); setVariantListErrors(variantErrors);
return errors; return errors;

View file

@ -1,7 +1,13 @@
import { DatagridChangeOpts } from "@dashboard/components/Datagrid/useDatagridChange";
import { ProductFragment } from "@dashboard/graphql"; import { ProductFragment } from "@dashboard/graphql";
import { ProductUpdateSubmitData } from "@dashboard/products/components/ProductUpdatePage/types"; import { ProductUpdateSubmitData } from "@dashboard/products/components/ProductUpdatePage/types";
import { inferProductChannelsAfterUpdate } from "./utils"; import { product } from "../../../fixtures";
import {
getBulkVariantUpdateInputs,
getCreateVariantInput,
inferProductChannelsAfterUpdate,
} from "./utils";
describe("Product update utils", () => { describe("Product update utils", () => {
it("should infer product channels after update with data", () => { it("should infer product channels after update with data", () => {
@ -74,3 +80,268 @@ describe("Product update utils", () => {
expect(result).toEqual(["1", "2", "3"]); expect(result).toEqual(["1", "2", "3"]);
}); });
}); });
describe("getCreateVariantInput", () => {
test("should return input data base on datagrid change data", () => {
// Arrange
const inputData: DatagridChangeOpts = {
updates: [
{
data: "new item",
column: "name",
row: 1,
},
{
data: "23423",
column: "sku",
row: 1,
},
{
data: true,
column: "availableInChannel:Q2hhbm5lbDoyMjQz",
row: 1,
},
{
data: {
kind: "money-cell",
value: 33434,
currency: "USD",
},
column: "channel:Q2hhbm5lbDoyMjQz",
row: 1,
},
{
data: true,
column: "availableInChannel:Q2hhbm5lbDoyNTQy",
row: 1,
},
{
data: {
kind: "money-cell",
value: 434,
currency: "BHD",
},
column: "channel:Q2hhbm5lbDoyNTQy",
row: 1,
},
{
data: {
kind: "number-cell",
value: 3223,
},
column:
"stock:V2FyZWhvdXNlOmQ0YzI0ODQxLTg2MDgtNGFiNC04MDkzLWUxNmQ4NWNlYjdkYQ==",
row: 1,
},
{
data: {
allowCustomValues: true,
emptyOption: true,
kind: "dropdown-cell",
value: {
label: "1l",
value: "1l",
},
},
column: "attribute:QXR0cmlidXRlOjE1",
row: 1,
},
],
removed: [],
added: [1],
};
// Act
const createDataInput = getCreateVariantInput(inputData, 1);
// Assert
expect(createDataInput).toEqual({
attributes: [
{
id: "QXR0cmlidXRlOjE1",
values: ["1l"],
},
],
sku: "23423",
name: "new item",
channelListings: [
{
channelId: "Q2hhbm5lbDoyMjQz",
price: 33434,
},
{
channelId: "Q2hhbm5lbDoyNTQy",
price: 434,
},
],
stocks: [
{
warehouse:
"V2FyZWhvdXNlOmQ0YzI0ODQxLTg2MDgtNGFiNC04MDkzLWUxNmQ4NWNlYjdkYQ==",
quantity: 3223,
},
],
});
});
test("should return only sku and name", () => {
// Arrange
const inputData: DatagridChangeOpts = {
updates: [
{
data: "new item",
column: "name",
row: 1,
},
{
data: "23423",
column: "sku",
row: 1,
},
],
removed: [],
added: [1],
};
// Act
const createDataInput = getCreateVariantInput(inputData, 1);
// Assert
expect(createDataInput).toEqual({
attributes: [],
sku: "23423",
name: "new item",
channelListings: [],
stocks: [],
});
});
});
describe("getBulkVariantUpdateInputs", () => {
test("should return input data base on datagrid change data for multiple variants", () => {
// Arrange
const variants: ProductFragment["variants"] =
product("http://google.com").variants;
const inputData: DatagridChangeOpts = {
updates: [
{
data: "item 1",
column: "name",
row: 1,
},
{
data: "new item",
column: "name",
row: 2,
},
{
data: "2345555",
column: "sku",
row: 2,
},
{
data: {
kind: "money-cell",
value: 234,
currency: "USD",
},
column: `channel:${variants[2].channelListings[0].channel.id}`,
row: 2,
},
{
data: {
kind: "money-cell",
value: 565,
currency: "BHD",
},
column: `channel:${variants[2].channelListings[1].channel.id}`,
row: 2,
},
{
data: {
kind: "number-cell",
value: 2344,
},
column: `stock:${variants[2].stocks[0].warehouse.id}`,
row: 2,
},
{
data: {
allowCustomValues: true,
emptyOption: true,
kind: "dropdown-cell",
value: {
label: "2l",
value: "2l",
},
},
column: `attribute:${variants[2].attributes[0].attribute.id}`,
row: 2,
},
],
removed: [],
added: [],
};
// Act
const bulkVariantUpdateInput = getBulkVariantUpdateInputs(
variants,
inputData,
);
// Assert
expect(bulkVariantUpdateInput).toEqual([
{
id: variants[1].id,
attributes: [],
name: "item 1",
stocks: {
create: [],
remove: [],
update: [],
},
channelListings: {
create: [],
remove: [],
update: [],
},
},
{
id: variants[2].id,
attributes: [
{
id: variants[2].attributes[0].attribute.id,
values: ["2l"],
},
],
sku: "2345555",
name: "new item",
stocks: {
update: [
{
stock: variants[2].stocks[0].id,
quantity: 2344,
},
],
create: [],
remove: [],
},
channelListings: {
update: [
{
channelListing: variants[2].channelListings[0].id,
price: 234,
},
{
channelListing: variants[2].channelListings[1].id,
price: 565,
},
],
remove: [],
create: [],
},
},
]);
});
});

View file

@ -1,20 +1,31 @@
import { FetchResult } from "@apollo/client"; import { FetchResult } from "@apollo/client";
import { getAttributesAfterFileAttributesUpdate } from "@dashboard/attributes/utils/data"; import { getAttributesAfterFileAttributesUpdate } from "@dashboard/attributes/utils/data";
import { prepareAttributesInput } from "@dashboard/attributes/utils/handlers"; import { prepareAttributesInput } from "@dashboard/attributes/utils/handlers";
import { DatagridChangeOpts } from "@dashboard/components/Datagrid/useDatagridChange";
import { VALUES_PAGINATE_BY } from "@dashboard/config"; import { VALUES_PAGINATE_BY } from "@dashboard/config";
import { import {
FileUploadMutation, FileUploadMutation,
ProductChannelListingAddInput, ProductChannelListingAddInput,
ProductChannelListingUpdateInput,
ProductChannelListingUpdateMutationVariables, ProductChannelListingUpdateMutationVariables,
ProductFragment, ProductFragment,
ProductVariantBulkUpdateInput,
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import { ProductUpdateSubmitData } from "@dashboard/products/components/ProductUpdatePage/types"; import { ProductUpdateSubmitData } from "@dashboard/products/components/ProductUpdatePage/types";
import { getColumnChannelAvailability } from "@dashboard/products/components/ProductVariants/datagrid/columnData";
import { getAttributeInputFromProduct } from "@dashboard/products/utils/data"; import { getAttributeInputFromProduct } from "@dashboard/products/utils/data";
import { getParsedDataForJsonStringField } from "@dashboard/utils/richText/misc"; import { getParsedDataForJsonStringField } from "@dashboard/utils/richText/misc";
import pick from "lodash/pick"; import pick from "lodash/pick";
import uniq from "lodash/uniq"; import uniq from "lodash/uniq";
import { getAttributeData } from "./data/attributes";
import {
getUpdateVariantChannelInputs,
getVariantChannelsInputs,
} from "./data/channel";
import { getNameData } from "./data/name";
import { getSkuData } from "./data/sku";
import { getStockData, getVaraintUpdateStockData } from "./data/stock";
export function getProductUpdateVariables( export function getProductUpdateVariables(
product: ProductFragment, product: ProductFragment,
data: ProductUpdateSubmitData, data: ProductUpdateSubmitData,
@ -49,34 +60,14 @@ export function getProductUpdateVariables(
}; };
} }
const hasChannel = ( export function getCreateVariantInput(data: DatagridChangeOpts, index: number) {
channelId: string, return {
variant?: ProductFragment["variants"][number], attributes: getAttributeData(data.updates, index, data.removed),
) => { sku: getSkuData(data.updates, index, data.removed),
if (!variant) { name: getNameData(data.updates, index, data.removed),
return false; channelListings: getVariantChannelsInputs(data, index),
} stocks: getStockData(data.updates, index, data.removed),
};
return variant.channelListings.some(c => c.channel.id === channelId);
};
export function inferProductChannelsAfterUpdate(
product: ProductFragment,
data: ProductUpdateSubmitData,
) {
const productChannelsIds = product.channelListings.map(
listing => listing.channel.id,
);
const updatedChannelsIds =
data.channels.updateChannels?.map(listing => listing.channelId) || [];
const removedChannelsIds = data.channels.removeChannels || [];
return uniq([
...productChannelsIds.filter(
channelId => !removedChannelsIds.includes(channelId),
),
...updatedChannelsIds,
]);
} }
export function getProductChannelsUpdateVariables( export function getProductChannelsUpdateVariables(
@ -105,41 +96,10 @@ export function getProductChannelsUpdateVariables(
) )
.forEach(listing => dataUpdated.set(listing.channelId, listing)); .forEach(listing => dataUpdated.set(listing.channelId, listing));
const variantsUpdates = new Map<string, ProductChannelListingAddInput>();
channels
.map(channelId => ({
channelId,
addVariants: data.variants.updates
.filter(
change =>
!data.variants.added.includes(change.row) &&
!hasChannel(channelId, product.variants[change.row]) &&
channelId === getColumnChannelAvailability(change.column) &&
change.data,
)
.map(change => product.variants[change.row].id),
removeVariants: data.variants.updates
.filter(
change =>
product.variants[change.row] &&
channelId === getColumnChannelAvailability(change.column) &&
!change.data,
)
.map(change => product.variants[change.row].id),
}))
.filter(
listing =>
listing.addVariants.length > 0 || listing.removeVariants.length > 0,
)
.forEach(listing => variantsUpdates.set(listing.channelId, listing));
const updateChannels = channels const updateChannels = channels
.filter( .filter(channelId => dataUpdated.has(channelId))
channelId => dataUpdated.has(channelId) || variantsUpdates.has(channelId),
)
.map(channelId => ({ .map(channelId => ({
...dataUpdated.get(channelId), ...dataUpdated.get(channelId),
...variantsUpdates.get(channelId),
})); }));
return { return {
@ -150,3 +110,63 @@ export function getProductChannelsUpdateVariables(
}, },
}; };
} }
export function hasProductChannelsUpdate(
data: ProductChannelListingUpdateInput,
) {
return data?.removeChannels?.length || data?.updateChannels?.length;
}
export function getBulkVariantUpdateInputs(
variants: ProductFragment["variants"],
data: DatagridChangeOpts,
): ProductVariantBulkUpdateInput[] {
const toUpdateInput = createToUpdateInput(data);
return variants.map(toUpdateInput).filter(byAvailability);
}
const createToUpdateInput =
(data: DatagridChangeOpts) =>
(variant, variantIndex): ProductVariantBulkUpdateInput => ({
id: variant.id,
attributes: getAttributeData(data.updates, variantIndex, data.removed),
sku: getSkuData(data.updates, variantIndex, data.removed),
name: getNameData(data.updates, variantIndex, data.removed),
stocks: getVaraintUpdateStockData(
data.updates,
variantIndex,
data.removed,
variant,
),
channelListings: getUpdateVariantChannelInputs(data, variantIndex, variant),
});
const byAvailability = (variant: ProductVariantBulkUpdateInput): boolean =>
variant.name !== undefined ||
variant.sku !== undefined ||
variant.attributes.length > 0 ||
variant.stocks.create.length > 0 ||
variant.stocks.update.length > 0 ||
variant.stocks.remove.length > 0 ||
variant.channelListings.update.length > 0 ||
variant.channelListings.remove.length > 0 ||
variant.channelListings.create.length > 0;
export function inferProductChannelsAfterUpdate(
product: ProductFragment,
data: ProductUpdateSubmitData,
) {
const productChannelsIds = product.channelListings.map(
listing => listing.channel.id,
);
const updatedChannelsIds =
data.channels.updateChannels?.map(listing => listing.channelId) || [];
const removedChannelsIds = data.channels.removeChannels || [];
return uniq([
...productChannelsIds.filter(
channelId => !removedChannelsIds.includes(channelId),
),
...updatedChannelsIds,
]);
}