Fix variant creator (#2085)
* Set channels for products without any variants * Fix duplicated_input_item error * Persist skus between steps * Cleanup code * Fix types
This commit is contained in:
parent
056b3501eb
commit
014ae0f362
21 changed files with 355 additions and 288 deletions
|
@ -1,4 +1,3 @@
|
||||||
import CannotDefineChannelsAvailabilityCard from "@saleor/channels/components/CannotDefineChannelsAvailabilityCard/CannotDefineChannelsAvailabilityCard";
|
|
||||||
import { ChannelData } from "@saleor/channels/utils";
|
import { ChannelData } from "@saleor/channels/utils";
|
||||||
import ChannelAvailabilityItemContent from "@saleor/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent";
|
import ChannelAvailabilityItemContent from "@saleor/components/ChannelsAvailabilityCard/Channel/ChannelAvailabilityItemContent";
|
||||||
import ChannelsAvailabilityCardWrapper, {
|
import ChannelsAvailabilityCardWrapper, {
|
||||||
|
@ -66,10 +65,6 @@ const ChannelsWithVariantsAvailabilityCard: React.FC<ChannelsWithVariantsAvailab
|
||||||
channelsWithVariantsData
|
channelsWithVariantsData
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!variants?.length) {
|
|
||||||
return <CannotDefineChannelsAvailabilityCard />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChannelsAvailabilityCardWrapper
|
<ChannelsAvailabilityCardWrapper
|
||||||
managePermissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
managePermissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||||
|
|
|
@ -151,7 +151,6 @@ const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
|
||||||
}}
|
}}
|
||||||
managePermissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
managePermissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||||
errors={channelsErrors}
|
errors={channelsErrors}
|
||||||
selectedChannelsCount={data.channelListings.length}
|
|
||||||
allChannelsCount={channelsCount}
|
allChannelsCount={channelsCount}
|
||||||
channels={data.channelListings}
|
channels={data.channelListings}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -141,7 +141,6 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
errors={channelsErrors}
|
errors={channelsErrors}
|
||||||
selectedChannelsCount={data.channelListings.length}
|
|
||||||
allChannelsCount={channelsCount}
|
allChannelsCount={channelsCount}
|
||||||
channels={data.channelListings}
|
channels={data.channelListings}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -38,8 +38,7 @@ const props: ChannelsAvailabilityCardProps = {
|
||||||
})),
|
})),
|
||||||
errors: [],
|
errors: [],
|
||||||
onChange: () => undefined,
|
onChange: () => undefined,
|
||||||
openModal: () => undefined,
|
openModal: () => undefined
|
||||||
selectedChannelsCount: 3
|
|
||||||
};
|
};
|
||||||
|
|
||||||
storiesOf("Generics / Channels availability card", module)
|
storiesOf("Generics / Channels availability card", module)
|
||||||
|
|
|
@ -17,7 +17,10 @@ import { ChannelOpts, ChannelsAvailabilityError, Messages } from "./types";
|
||||||
import { getChannelsAvailabilityMessages } from "./utils";
|
import { getChannelsAvailabilityMessages } from "./utils";
|
||||||
|
|
||||||
export interface ChannelsAvailability
|
export interface ChannelsAvailability
|
||||||
extends Omit<ChannelsAvailabilityWrapperProps, "children"> {
|
extends Omit<
|
||||||
|
ChannelsAvailabilityWrapperProps,
|
||||||
|
"children" | "selectedChannelsCount"
|
||||||
|
> {
|
||||||
channels: ChannelData[];
|
channels: ChannelData[];
|
||||||
channelsList: ChannelList[];
|
channelsList: ChannelList[];
|
||||||
errors?: ChannelsAvailabilityError[];
|
errors?: ChannelsAvailabilityError[];
|
||||||
|
@ -36,7 +39,6 @@ export const ChannelsAvailability: React.FC<ChannelsAvailabilityCardProps> = pro
|
||||||
const {
|
const {
|
||||||
channelsList,
|
channelsList,
|
||||||
errors = [],
|
errors = [],
|
||||||
selectedChannelsCount = 0,
|
|
||||||
allChannelsCount = 0,
|
allChannelsCount = 0,
|
||||||
channels,
|
channels,
|
||||||
messages,
|
messages,
|
||||||
|
@ -57,7 +59,7 @@ export const ChannelsAvailability: React.FC<ChannelsAvailabilityCardProps> = pro
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChannelsAvailabilityCardWrapper
|
<ChannelsAvailabilityCardWrapper
|
||||||
selectedChannelsCount={selectedChannelsCount}
|
selectedChannelsCount={(channels ?? channelsList).length ?? 0}
|
||||||
allChannelsCount={allChannelsCount}
|
allChannelsCount={allChannelsCount}
|
||||||
managePermissions={managePermissions}
|
managePermissions={managePermissions}
|
||||||
openModal={openModal}
|
openModal={openModal}
|
||||||
|
|
|
@ -143,7 +143,6 @@ const SaleCreatePage: React.FC<SaleCreatePageProps> = ({
|
||||||
<div>
|
<div>
|
||||||
<ChannelsAvailabilityCard
|
<ChannelsAvailabilityCard
|
||||||
managePermissions={[PermissionEnum.MANAGE_DISCOUNTS]}
|
managePermissions={[PermissionEnum.MANAGE_DISCOUNTS]}
|
||||||
selectedChannelsCount={data.channelListings.length}
|
|
||||||
allChannelsCount={allChannelsCount}
|
allChannelsCount={allChannelsCount}
|
||||||
channelsList={data.channelListings.map(channel => ({
|
channelsList={data.channelListings.map(channel => ({
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
|
|
|
@ -334,7 +334,6 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
|
||||||
<CardSpacer />
|
<CardSpacer />
|
||||||
<ChannelsAvailabilityCard
|
<ChannelsAvailabilityCard
|
||||||
managePermissions={[PermissionEnum.MANAGE_DISCOUNTS]}
|
managePermissions={[PermissionEnum.MANAGE_DISCOUNTS]}
|
||||||
selectedChannelsCount={data.channelListings.length}
|
|
||||||
allChannelsCount={allChannelsCount}
|
allChannelsCount={allChannelsCount}
|
||||||
channelsList={data.channelListings.map(channel => ({
|
channelsList={data.channelListings.map(channel => ({
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
|
|
|
@ -191,7 +191,6 @@ const VoucherCreatePage: React.FC<VoucherCreatePageProps> = ({
|
||||||
<div>
|
<div>
|
||||||
<ChannelsAvailabilityCard
|
<ChannelsAvailabilityCard
|
||||||
managePermissions={[PermissionEnum.MANAGE_DISCOUNTS]}
|
managePermissions={[PermissionEnum.MANAGE_DISCOUNTS]}
|
||||||
selectedChannelsCount={data.channelListings.length}
|
|
||||||
allChannelsCount={allChannelsCount}
|
allChannelsCount={allChannelsCount}
|
||||||
channelsList={data.channelListings.map(channel => ({
|
channelsList={data.channelListings.map(channel => ({
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
|
|
|
@ -402,7 +402,6 @@ const VoucherDetailsPage: React.FC<VoucherDetailsPageProps> = ({
|
||||||
<CardSpacer />
|
<CardSpacer />
|
||||||
<ChannelsAvailabilityCard
|
<ChannelsAvailabilityCard
|
||||||
managePermissions={[PermissionEnum.MANAGE_DISCOUNTS]}
|
managePermissions={[PermissionEnum.MANAGE_DISCOUNTS]}
|
||||||
selectedChannelsCount={data.channelListings.length}
|
|
||||||
allChannelsCount={allChannelsCount}
|
allChannelsCount={allChannelsCount}
|
||||||
channelsList={data.channelListings.map(channel => ({
|
channelsList={data.channelListings.map(channel => ({
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
|
|
|
@ -348,7 +348,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
errors={channelsErrors}
|
errors={channelsErrors}
|
||||||
selectedChannelsCount={data.channelListings?.length || 0}
|
|
||||||
allChannelsCount={allChannelsCount}
|
allChannelsCount={allChannelsCount}
|
||||||
channels={data.channelListings || []}
|
channels={data.channelListings || []}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
|
|
@ -290,260 +290,273 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
||||||
submit,
|
submit,
|
||||||
isSaveDisabled,
|
isSaveDisabled,
|
||||||
attributeRichTextGetters
|
attributeRichTextGetters
|
||||||
}) => (
|
}) => {
|
||||||
<>
|
const availabilityCommonProps = {
|
||||||
<Container>
|
managePermissions: [PermissionEnum.MANAGE_PRODUCTS],
|
||||||
<Backlink href={productListUrl()}>
|
messages: {
|
||||||
{intl.formatMessage(sectionNames.products)}
|
hiddenLabel: intl.formatMessage({
|
||||||
</Backlink>
|
id: "saKXY3",
|
||||||
<PageHeader title={header}>
|
defaultMessage: "Not published",
|
||||||
{extensionMenuItems.length > 0 && (
|
description: "product label"
|
||||||
<CardMenu menuItems={extensionMenuItems} data-test-id="menu" />
|
}),
|
||||||
)}
|
|
||||||
</PageHeader>
|
visibleLabel: intl.formatMessage({
|
||||||
<Grid richText>
|
id: "qJedl0",
|
||||||
<div>
|
defaultMessage: "Published",
|
||||||
<ProductDetailsForm
|
description: "product label"
|
||||||
data={data}
|
})
|
||||||
disabled={disabled}
|
},
|
||||||
errors={errors}
|
errors: channelsErrors,
|
||||||
onChange={change}
|
allChannelsCount,
|
||||||
/>
|
disabled,
|
||||||
<CardSpacer />
|
onChange: handlers.changeChannels,
|
||||||
<ProductMedia
|
openModal: openChannelsModal
|
||||||
media={media}
|
};
|
||||||
placeholderImage={placeholderImage}
|
|
||||||
onImageDelete={onImageDelete}
|
return (
|
||||||
onImageReorder={onImageReorder}
|
<>
|
||||||
onImageUpload={onImageUpload}
|
<Container>
|
||||||
openMediaUrlModal={() => setMediaUrlModalStatus(true)}
|
<Backlink href={productListUrl()}>
|
||||||
getImageEditUrl={imageId =>
|
{intl.formatMessage(sectionNames.products)}
|
||||||
productImageUrl(productId, imageId)
|
</Backlink>
|
||||||
|
<PageHeader title={header}>
|
||||||
|
{extensionMenuItems.length > 0 && (
|
||||||
|
<CardMenu
|
||||||
|
menuItems={extensionMenuItems}
|
||||||
|
data-test-id="menu"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PageHeader>
|
||||||
|
<Grid richText>
|
||||||
|
<div>
|
||||||
|
<ProductDetailsForm
|
||||||
|
data={data}
|
||||||
|
disabled={disabled}
|
||||||
|
errors={errors}
|
||||||
|
onChange={change}
|
||||||
|
/>
|
||||||
|
<CardSpacer />
|
||||||
|
<ProductMedia
|
||||||
|
media={media}
|
||||||
|
placeholderImage={placeholderImage}
|
||||||
|
onImageDelete={onImageDelete}
|
||||||
|
onImageReorder={onImageReorder}
|
||||||
|
onImageUpload={onImageUpload}
|
||||||
|
openMediaUrlModal={() => setMediaUrlModalStatus(true)}
|
||||||
|
getImageEditUrl={imageId =>
|
||||||
|
productImageUrl(productId, imageId)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<CardSpacer />
|
||||||
|
{data.attributes.length > 0 && (
|
||||||
|
<Attributes
|
||||||
|
attributes={data.attributes}
|
||||||
|
attributeValues={attributeValues}
|
||||||
|
errors={errors}
|
||||||
|
loading={disabled}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handlers.selectAttribute}
|
||||||
|
onMultiChange={handlers.selectAttributeMultiple}
|
||||||
|
onFileChange={handlers.selectAttributeFile}
|
||||||
|
onReferencesRemove={handlers.selectAttributeReference}
|
||||||
|
onReferencesAddClick={onAssignReferencesClick}
|
||||||
|
onReferencesReorder={handlers.reorderAttributeValue}
|
||||||
|
fetchAttributeValues={fetchAttributeValues}
|
||||||
|
fetchMoreAttributeValues={fetchMoreAttributeValues}
|
||||||
|
onAttributeSelectBlur={onAttributeSelectBlur}
|
||||||
|
richTextGetters={attributeRichTextGetters}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<CardSpacer />
|
||||||
|
{isSimpleProduct && (
|
||||||
|
<>
|
||||||
|
<ProductVariantPrice
|
||||||
|
ProductVariantChannelListings={data.channelListings}
|
||||||
|
errors={channelsErrors}
|
||||||
|
loading={disabled}
|
||||||
|
onChange={handlers.changeChannelPrice}
|
||||||
|
/>
|
||||||
|
<CardSpacer />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{hasVariants ? (
|
||||||
|
<ProductVariants
|
||||||
|
productId={productId}
|
||||||
|
disabled={disabled}
|
||||||
|
limits={limits}
|
||||||
|
variants={variants}
|
||||||
|
product={product}
|
||||||
|
onVariantsAdd={onVariantsAdd}
|
||||||
|
onVariantReorder={onVariantReorder}
|
||||||
|
onSetDefaultVariant={onSetDefaultVariant}
|
||||||
|
toolbar={toolbar}
|
||||||
|
isChecked={isChecked}
|
||||||
|
selected={selected}
|
||||||
|
selectedChannelId={selectedChannelId}
|
||||||
|
toggle={toggle}
|
||||||
|
toggleAll={toggleAll}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ProductShipping
|
||||||
|
data={data}
|
||||||
|
disabled={disabled}
|
||||||
|
errors={errors}
|
||||||
|
weightUnit={product?.weight?.unit || defaultWeightUnit}
|
||||||
|
onChange={change}
|
||||||
|
/>
|
||||||
|
<CardSpacer />
|
||||||
|
<ProductStocks
|
||||||
|
onVariantChannelListingChange={
|
||||||
|
handlers.changeChannelPreorder
|
||||||
|
}
|
||||||
|
productVariantChannelListings={data.channelListings}
|
||||||
|
onEndPreorderTrigger={
|
||||||
|
!!variants?.[0]?.preorder
|
||||||
|
? () => onVariantEndPreorderDialogOpen()
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
data={data}
|
||||||
|
disabled={disabled}
|
||||||
|
hasVariants={false}
|
||||||
|
errors={errors}
|
||||||
|
formErrors={formErrors}
|
||||||
|
stocks={data.stocks}
|
||||||
|
warehouses={warehouses}
|
||||||
|
onChange={handlers.changeStock}
|
||||||
|
onFormDataChange={change}
|
||||||
|
onChangePreorderEndDate={handlers.changePreorderEndDate}
|
||||||
|
onWarehouseStockAdd={handlers.addStock}
|
||||||
|
onWarehouseStockDelete={handlers.deleteStock}
|
||||||
|
onWarehouseConfigure={onWarehouseConfigure}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<CardSpacer />
|
||||||
|
<SeoForm
|
||||||
|
errors={errors}
|
||||||
|
title={data.seoTitle}
|
||||||
|
titlePlaceholder={data.name}
|
||||||
|
description={data.seoDescription}
|
||||||
|
descriptionPlaceholder={""} // TODO: cast description to string
|
||||||
|
slug={data.slug}
|
||||||
|
slugPlaceholder={data.name}
|
||||||
|
loading={disabled}
|
||||||
|
onClick={onSeoClick}
|
||||||
|
onChange={change}
|
||||||
|
helperText={intl.formatMessage({
|
||||||
|
id: "LKoIB1",
|
||||||
|
defaultMessage:
|
||||||
|
"Add search engine title and description to make this product easier to find"
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<CardSpacer />
|
||||||
|
<Metadata data={data} onChange={handlers.changeMetadata} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ProductOrganization
|
||||||
|
canChangeType={false}
|
||||||
|
categories={categories}
|
||||||
|
categoryInputDisplayValue={selectedCategory}
|
||||||
|
collections={collections}
|
||||||
|
collectionsInputDisplayValue={selectedCollections}
|
||||||
|
data={data}
|
||||||
|
disabled={disabled}
|
||||||
|
errors={errors}
|
||||||
|
fetchCategories={fetchCategories}
|
||||||
|
fetchCollections={fetchCollections}
|
||||||
|
fetchMoreCategories={fetchMoreCategories}
|
||||||
|
fetchMoreCollections={fetchMoreCollections}
|
||||||
|
productType={product?.productType}
|
||||||
|
onCategoryChange={handlers.selectCategory}
|
||||||
|
onCollectionChange={handlers.selectCollection}
|
||||||
|
/>
|
||||||
|
<CardSpacer />
|
||||||
|
{isSimpleProduct ? (
|
||||||
|
<ChannelsAvailabilityCard
|
||||||
|
{...availabilityCommonProps}
|
||||||
|
channels={data.channelListings}
|
||||||
|
/>
|
||||||
|
) : product?.variants.length === 0 ? (
|
||||||
|
<ChannelsAvailabilityCard
|
||||||
|
{...availabilityCommonProps}
|
||||||
|
channelsList={data.channelListings}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChannelsWithVariantsAvailabilityCard
|
||||||
|
messages={{
|
||||||
|
hiddenLabel: intl.formatMessage({
|
||||||
|
id: "saKXY3",
|
||||||
|
defaultMessage: "Not published",
|
||||||
|
description: "product label"
|
||||||
|
}),
|
||||||
|
|
||||||
|
visibleLabel: intl.formatMessage({
|
||||||
|
id: "qJedl0",
|
||||||
|
defaultMessage: "Published",
|
||||||
|
description: "product label"
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
errors={channelsErrors}
|
||||||
|
channels={data.channelsData}
|
||||||
|
channelsWithVariantsData={channelsWithVariantsData}
|
||||||
|
variants={variants}
|
||||||
|
onChange={handlers.changeChannels}
|
||||||
|
openModal={openChannelsModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<CardSpacer />
|
||||||
|
<ProductTaxes
|
||||||
|
data={data}
|
||||||
|
disabled={disabled}
|
||||||
|
selectedTaxTypeDisplayName={selectedTaxType}
|
||||||
|
taxTypes={taxTypes}
|
||||||
|
onChange={change}
|
||||||
|
onTaxTypeChange={handlers.selectTaxRate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
<Savebar
|
||||||
|
onCancel={() => navigate(productListUrl())}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onSubmit={submit}
|
||||||
|
state={saveButtonBarState}
|
||||||
|
disabled={isSaveDisabled}
|
||||||
|
/>
|
||||||
|
{canOpenAssignReferencesAttributeDialog && (
|
||||||
|
<AssignAttributeValueDialog
|
||||||
|
attributeValues={getAttributeValuesFromReferences(
|
||||||
|
assignReferencesAttributeId,
|
||||||
|
data.attributes,
|
||||||
|
referencePages,
|
||||||
|
referenceProducts
|
||||||
|
)}
|
||||||
|
hasMore={handlers.fetchMoreReferences?.hasMore}
|
||||||
|
open={canOpenAssignReferencesAttributeDialog}
|
||||||
|
onFetch={handlers.fetchReferences}
|
||||||
|
onFetchMore={handlers.fetchMoreReferences?.onFetchMore}
|
||||||
|
loading={handlers.fetchMoreReferences?.loading}
|
||||||
|
onClose={onCloseDialog}
|
||||||
|
onSubmit={attributeValues =>
|
||||||
|
handleAssignReferenceAttribute(
|
||||||
|
attributeValues,
|
||||||
|
data,
|
||||||
|
handlers
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<CardSpacer />
|
)}
|
||||||
{data.attributes.length > 0 && (
|
|
||||||
<Attributes
|
|
||||||
attributes={data.attributes}
|
|
||||||
attributeValues={attributeValues}
|
|
||||||
errors={errors}
|
|
||||||
loading={disabled}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={handlers.selectAttribute}
|
|
||||||
onMultiChange={handlers.selectAttributeMultiple}
|
|
||||||
onFileChange={handlers.selectAttributeFile}
|
|
||||||
onReferencesRemove={handlers.selectAttributeReference}
|
|
||||||
onReferencesAddClick={onAssignReferencesClick}
|
|
||||||
onReferencesReorder={handlers.reorderAttributeValue}
|
|
||||||
fetchAttributeValues={fetchAttributeValues}
|
|
||||||
fetchMoreAttributeValues={fetchMoreAttributeValues}
|
|
||||||
onAttributeSelectBlur={onAttributeSelectBlur}
|
|
||||||
richTextGetters={attributeRichTextGetters}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<CardSpacer />
|
|
||||||
{isSimpleProduct && (
|
|
||||||
<>
|
|
||||||
<ProductVariantPrice
|
|
||||||
ProductVariantChannelListings={data.channelListings}
|
|
||||||
errors={channelsErrors}
|
|
||||||
loading={disabled}
|
|
||||||
onChange={handlers.changeChannelPrice}
|
|
||||||
/>
|
|
||||||
<CardSpacer />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{hasVariants ? (
|
|
||||||
<ProductVariants
|
|
||||||
productId={productId}
|
|
||||||
disabled={disabled}
|
|
||||||
limits={limits}
|
|
||||||
variants={variants}
|
|
||||||
product={product}
|
|
||||||
onVariantsAdd={onVariantsAdd}
|
|
||||||
onVariantReorder={onVariantReorder}
|
|
||||||
onSetDefaultVariant={onSetDefaultVariant}
|
|
||||||
toolbar={toolbar}
|
|
||||||
isChecked={isChecked}
|
|
||||||
selected={selected}
|
|
||||||
selectedChannelId={selectedChannelId}
|
|
||||||
toggle={toggle}
|
|
||||||
toggleAll={toggleAll}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ProductShipping
|
|
||||||
data={data}
|
|
||||||
disabled={disabled}
|
|
||||||
errors={errors}
|
|
||||||
weightUnit={product?.weight?.unit || defaultWeightUnit}
|
|
||||||
onChange={change}
|
|
||||||
/>
|
|
||||||
<CardSpacer />
|
|
||||||
<ProductStocks
|
|
||||||
onVariantChannelListingChange={
|
|
||||||
handlers.changeChannelPreorder
|
|
||||||
}
|
|
||||||
productVariantChannelListings={data.channelListings}
|
|
||||||
onEndPreorderTrigger={
|
|
||||||
!!variants?.[0]?.preorder
|
|
||||||
? () => onVariantEndPreorderDialogOpen()
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
data={data}
|
|
||||||
disabled={disabled}
|
|
||||||
hasVariants={false}
|
|
||||||
errors={errors}
|
|
||||||
formErrors={formErrors}
|
|
||||||
stocks={data.stocks}
|
|
||||||
warehouses={warehouses}
|
|
||||||
onChange={handlers.changeStock}
|
|
||||||
onFormDataChange={change}
|
|
||||||
onChangePreorderEndDate={handlers.changePreorderEndDate}
|
|
||||||
onWarehouseStockAdd={handlers.addStock}
|
|
||||||
onWarehouseStockDelete={handlers.deleteStock}
|
|
||||||
onWarehouseConfigure={onWarehouseConfigure}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<CardSpacer />
|
|
||||||
<SeoForm
|
|
||||||
errors={errors}
|
|
||||||
title={data.seoTitle}
|
|
||||||
titlePlaceholder={data.name}
|
|
||||||
description={data.seoDescription}
|
|
||||||
descriptionPlaceholder={""} // TODO: cast description to string
|
|
||||||
slug={data.slug}
|
|
||||||
slugPlaceholder={data.name}
|
|
||||||
loading={disabled}
|
|
||||||
onClick={onSeoClick}
|
|
||||||
onChange={change}
|
|
||||||
helperText={intl.formatMessage({
|
|
||||||
id: "LKoIB1",
|
|
||||||
defaultMessage:
|
|
||||||
"Add search engine title and description to make this product easier to find"
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<CardSpacer />
|
|
||||||
<Metadata data={data} onChange={handlers.changeMetadata} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ProductOrganization
|
|
||||||
canChangeType={false}
|
|
||||||
categories={categories}
|
|
||||||
categoryInputDisplayValue={selectedCategory}
|
|
||||||
collections={collections}
|
|
||||||
collectionsInputDisplayValue={selectedCollections}
|
|
||||||
data={data}
|
|
||||||
disabled={disabled}
|
|
||||||
errors={errors}
|
|
||||||
fetchCategories={fetchCategories}
|
|
||||||
fetchCollections={fetchCollections}
|
|
||||||
fetchMoreCategories={fetchMoreCategories}
|
|
||||||
fetchMoreCollections={fetchMoreCollections}
|
|
||||||
productType={product?.productType}
|
|
||||||
onCategoryChange={handlers.selectCategory}
|
|
||||||
onCollectionChange={handlers.selectCollection}
|
|
||||||
/>
|
|
||||||
<CardSpacer />
|
|
||||||
{isSimpleProduct ? (
|
|
||||||
<ChannelsAvailabilityCard
|
|
||||||
managePermissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
|
||||||
messages={{
|
|
||||||
hiddenLabel: intl.formatMessage({
|
|
||||||
id: "saKXY3",
|
|
||||||
defaultMessage: "Not published",
|
|
||||||
description: "product label"
|
|
||||||
}),
|
|
||||||
|
|
||||||
visibleLabel: intl.formatMessage({
|
<ProductExternalMediaDialog
|
||||||
id: "qJedl0",
|
product={product}
|
||||||
defaultMessage: "Published",
|
onClose={() => setMediaUrlModalStatus(false)}
|
||||||
description: "product label"
|
open={mediaUrlModalStatus}
|
||||||
})
|
onSubmit={onMediaUrlUpload}
|
||||||
}}
|
|
||||||
errors={channelsErrors}
|
|
||||||
selectedChannelsCount={data.channelListings.length}
|
|
||||||
allChannelsCount={allChannelsCount}
|
|
||||||
channels={data.channelListings}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={handlers.changeChannels}
|
|
||||||
openModal={openChannelsModal}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ChannelsWithVariantsAvailabilityCard
|
|
||||||
messages={{
|
|
||||||
hiddenLabel: intl.formatMessage({
|
|
||||||
id: "saKXY3",
|
|
||||||
defaultMessage: "Not published",
|
|
||||||
description: "product label"
|
|
||||||
}),
|
|
||||||
|
|
||||||
visibleLabel: intl.formatMessage({
|
|
||||||
id: "qJedl0",
|
|
||||||
defaultMessage: "Published",
|
|
||||||
description: "product label"
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
errors={channelsErrors}
|
|
||||||
channels={data.channelsData}
|
|
||||||
channelsWithVariantsData={channelsWithVariantsData}
|
|
||||||
variants={variants}
|
|
||||||
onChange={handlers.changeChannels}
|
|
||||||
openModal={openChannelsModal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<CardSpacer />
|
|
||||||
<ProductTaxes
|
|
||||||
data={data}
|
|
||||||
disabled={disabled}
|
|
||||||
selectedTaxTypeDisplayName={selectedTaxType}
|
|
||||||
taxTypes={taxTypes}
|
|
||||||
onChange={change}
|
|
||||||
onTaxTypeChange={handlers.selectTaxRate}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Grid>
|
|
||||||
<Savebar
|
|
||||||
onCancel={() => navigate(productListUrl())}
|
|
||||||
onDelete={onDelete}
|
|
||||||
onSubmit={submit}
|
|
||||||
state={saveButtonBarState}
|
|
||||||
disabled={isSaveDisabled}
|
|
||||||
/>
|
|
||||||
{canOpenAssignReferencesAttributeDialog && (
|
|
||||||
<AssignAttributeValueDialog
|
|
||||||
attributeValues={getAttributeValuesFromReferences(
|
|
||||||
assignReferencesAttributeId,
|
|
||||||
data.attributes,
|
|
||||||
referencePages,
|
|
||||||
referenceProducts
|
|
||||||
)}
|
|
||||||
hasMore={handlers.fetchMoreReferences?.hasMore}
|
|
||||||
open={canOpenAssignReferencesAttributeDialog}
|
|
||||||
onFetch={handlers.fetchReferences}
|
|
||||||
onFetchMore={handlers.fetchMoreReferences?.onFetchMore}
|
|
||||||
loading={handlers.fetchMoreReferences?.loading}
|
|
||||||
onClose={onCloseDialog}
|
|
||||||
onSubmit={attributeValues =>
|
|
||||||
handleAssignReferenceAttribute(
|
|
||||||
attributeValues,
|
|
||||||
data,
|
|
||||||
handlers
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</Container>
|
||||||
|
</>
|
||||||
<ProductExternalMediaDialog
|
);
|
||||||
product={product}
|
}}
|
||||||
onClose={() => setMediaUrlModalStatus(false)}
|
|
||||||
open={mediaUrlModalStatus}
|
|
||||||
onSubmit={onMediaUrlUpload}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ProductUpdateForm>
|
</ProductUpdateForm>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,6 +24,7 @@ import reduceProductVariantCreateFormData, {
|
||||||
ProductVariantCreateReducerActionType
|
ProductVariantCreateReducerActionType
|
||||||
} from "./reducer";
|
} from "./reducer";
|
||||||
import { ProductVariantCreatorStep } from "./types";
|
import { ProductVariantCreatorStep } from "./types";
|
||||||
|
import { dedupeListings } from "./utils";
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
theme => ({
|
theme => ({
|
||||||
|
@ -176,7 +177,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = props
|
||||||
onTransition: (_, nextStep) => {
|
onTransition: (_, nextStep) => {
|
||||||
if (nextStep === ProductVariantCreatorStep.summary) {
|
if (nextStep === ProductVariantCreatorStep.summary) {
|
||||||
dispatchFormDataAction({
|
dispatchFormDataAction({
|
||||||
type: ProductVariantCreateReducerActionType.reload
|
type: ProductVariantCreateReducerActionType.rebuild
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -237,7 +238,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = props
|
||||||
className={classes.button}
|
className={classes.button}
|
||||||
disabled={!canHitNext(step, wizardData, variantsLeft)}
|
disabled={!canHitNext(step, wizardData, variantsLeft)}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => onSubmit(wizardData.variants)}
|
onClick={() => onSubmit(dedupeListings(wizardData).variants)}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="Q3j++G"
|
id="Q3j++G"
|
||||||
|
|
|
@ -102,7 +102,7 @@ const useStyles = makeStyles<ProductVariantCreatorSummaryProps, ClassKey>(
|
||||||
`minmax(180px, auto) repeat(${props.data.variants[0].channelListings
|
`minmax(180px, auto) repeat(${props.data.variants[0].channelListings
|
||||||
.length +
|
.length +
|
||||||
props.data.variants[0].stocks
|
props.data.variants[0].stocks
|
||||||
.length}, minmax(180px, auto)) 140px 64px`,
|
.length}, minmax(180px, auto)) 220px 64px`,
|
||||||
overflowX: "scroll",
|
overflowX: "scroll",
|
||||||
rowGap: theme.spacing(),
|
rowGap: theme.spacing(),
|
||||||
paddingBottom: 3
|
paddingBottom: 3
|
||||||
|
|
|
@ -152,14 +152,6 @@ function addAttributeToVariant(
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
function addVariantAttributeInput(
|
|
||||||
data: CreateVariantInput[],
|
|
||||||
attribute: Attribute
|
|
||||||
): CreateVariantInput[] {
|
|
||||||
return data
|
|
||||||
.map(variant => addAttributeToVariant(attribute, variant))
|
|
||||||
.reduce((acc, variantInput) => [...acc, ...variantInput]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createVariantFlatMatrixDimension(
|
export function createVariantFlatMatrixDimension(
|
||||||
variants: CreateVariantInput[],
|
variants: CreateVariantInput[],
|
||||||
|
@ -167,7 +159,9 @@ export function createVariantFlatMatrixDimension(
|
||||||
): CreateVariantInput[] {
|
): CreateVariantInput[] {
|
||||||
if (attributes.length > 0) {
|
if (attributes.length > 0) {
|
||||||
return createVariantFlatMatrixDimension(
|
return createVariantFlatMatrixDimension(
|
||||||
addVariantAttributeInput(variants, attributes[0]),
|
variants.flatMap(variant =>
|
||||||
|
addAttributeToVariant(attributes[0], variant)
|
||||||
|
),
|
||||||
attributes.slice(1)
|
attributes.slice(1)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { AttributeValueFragment, StockInput } from "@saleor/graphql";
|
import {
|
||||||
|
AttributeValueFragment,
|
||||||
|
ProductVariantBulkCreateInput,
|
||||||
|
StockInput
|
||||||
|
} from "@saleor/graphql";
|
||||||
import {
|
import {
|
||||||
add,
|
add,
|
||||||
remove,
|
remove,
|
||||||
|
@ -32,6 +36,7 @@ export enum ProductVariantCreateReducerActionType {
|
||||||
changeWarehouses,
|
changeWarehouses,
|
||||||
deleteVariant,
|
deleteVariant,
|
||||||
reload,
|
reload,
|
||||||
|
rebuild,
|
||||||
selectValue
|
selectValue
|
||||||
}
|
}
|
||||||
export interface ProductVariantCreateReducerAction {
|
export interface ProductVariantCreateReducerAction {
|
||||||
|
@ -80,6 +85,44 @@ export interface ProductVariantCreateReducerAction {
|
||||||
type: ProductVariantCreateReducerActionType;
|
type: ProductVariantCreateReducerActionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVariantId(variant: ProductVariantBulkCreateInput): string {
|
||||||
|
return variant.attributes
|
||||||
|
.map(attribute => attribute.values.join(":"))
|
||||||
|
.join("-");
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge(
|
||||||
|
prev: ProductVariantBulkCreateInput[],
|
||||||
|
update: ProductVariantBulkCreateInput[]
|
||||||
|
): ProductVariantBulkCreateInput[] {
|
||||||
|
const prevIds = prev.map(getVariantId);
|
||||||
|
const updateIds = update.map(getVariantId);
|
||||||
|
return [
|
||||||
|
...prev
|
||||||
|
.filter(variant => updateIds.includes(getVariantId(variant)))
|
||||||
|
.map(variant => {
|
||||||
|
const updatedVariant = update.find(
|
||||||
|
v => getVariantId(v) === getVariantId(variant)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedVariant,
|
||||||
|
sku: variant.sku
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
...update.filter(variant => !prevIds.includes(getVariantId(variant)))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuild(
|
||||||
|
state: ProductVariantCreateFormData
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
variants: merge(state.variants, createVariants(state))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function selectValue(
|
function selectValue(
|
||||||
prevState: ProductVariantCreateFormData,
|
prevState: ProductVariantCreateFormData,
|
||||||
attributeId: string,
|
attributeId: string,
|
||||||
|
@ -519,6 +562,8 @@ function reduceProductVariantCreateFormData(
|
||||||
return deleteVariant(prevState, action.deleteVariant.variantIndex);
|
return deleteVariant(prevState, action.deleteVariant.variantIndex);
|
||||||
case ProductVariantCreateReducerActionType.reload:
|
case ProductVariantCreateReducerActionType.reload:
|
||||||
return action.reload?.data || createVariantMatrix(prevState);
|
return action.reload?.data || createVariantMatrix(prevState);
|
||||||
|
case ProductVariantCreateReducerActionType.rebuild:
|
||||||
|
return rebuild(prevState);
|
||||||
default:
|
default:
|
||||||
return prevState;
|
return prevState;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
} from "@saleor/graphql";
|
} from "@saleor/graphql";
|
||||||
import { getById } from "@saleor/orders/components/OrderReturnPage/utils";
|
import { getById } from "@saleor/orders/components/OrderReturnPage/utils";
|
||||||
import { RelayToFlat } from "@saleor/types";
|
import { RelayToFlat } from "@saleor/types";
|
||||||
|
import uniqBy from "lodash/uniqBy";
|
||||||
|
|
||||||
import { AttributeValue, ProductVariantCreateFormData } from "./form";
|
import { AttributeValue, ProductVariantCreateFormData } from "./form";
|
||||||
|
|
||||||
|
@ -79,3 +80,15 @@ export const getBasicAttributeValue = (
|
||||||
attributeValues.find(getBySlug(attributeValue))
|
attributeValues.find(getBySlug(attributeValue))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function dedupeListings(
|
||||||
|
data: ProductVariantCreateFormData
|
||||||
|
): ProductVariantCreateFormData {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
variants: data.variants.map(variant => ({
|
||||||
|
...variant,
|
||||||
|
channelListings: uniqBy(variant.channelListings, "channelId")
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -530,7 +530,7 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
|
||||||
<>
|
<>
|
||||||
<WindowTitle title={data?.product?.name} />
|
<WindowTitle title={data?.product?.name} />
|
||||||
{!!allChannels?.length &&
|
{!!allChannels?.length &&
|
||||||
(isSimpleProduct ? (
|
(isSimpleProduct || product?.variants.length === 0 ? (
|
||||||
<ChannelsAvailabilityDialog
|
<ChannelsAvailabilityDialog
|
||||||
isSelected={isChannelSelected}
|
isSelected={isChannelSelected}
|
||||||
channels={allChannels}
|
channels={allChannels}
|
||||||
|
|
|
@ -142,7 +142,11 @@ export function createUpdateHandler(
|
||||||
const result = await updateProduct(productVariables);
|
const result = await updateProduct(productVariables);
|
||||||
errors = [...errors, ...result.data.productUpdate.errors];
|
errors = [...errors, ...result.data.productUpdate.errors];
|
||||||
|
|
||||||
await updateChannels(getChannelsVariables(product, allChannels, data));
|
if (product.variants.length === 0) {
|
||||||
|
await updateChannels(getSimpleChannelsVariables(data, product));
|
||||||
|
} else {
|
||||||
|
await updateChannels(getChannelsVariables(product, allChannels, data));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!product.variants.length) {
|
if (!product.variants.length) {
|
||||||
const productVariantResult = await productVariantCreate({
|
const productVariantResult = await productVariantCreate({
|
||||||
|
|
|
@ -204,7 +204,6 @@ export const ShippingZoneRatesCreatePage: React.FC<ShippingZoneRatesCreatePagePr
|
||||||
<ChannelsAvailabilityCard
|
<ChannelsAvailabilityCard
|
||||||
managePermissions={[PermissionEnum.MANAGE_SHIPPING]}
|
managePermissions={[PermissionEnum.MANAGE_SHIPPING]}
|
||||||
allChannelsCount={allChannelsCount}
|
allChannelsCount={allChannelsCount}
|
||||||
selectedChannelsCount={shippingChannels?.length}
|
|
||||||
channelsList={data.channelListings}
|
channelsList={data.channelListings}
|
||||||
openModal={openChannelsModal}
|
openModal={openChannelsModal}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -235,7 +235,6 @@ export const ShippingZoneRatesPage: React.FC<ShippingZoneRatesPageProps> = ({
|
||||||
<ChannelsAvailabilityCard
|
<ChannelsAvailabilityCard
|
||||||
managePermissions={[PermissionEnum.MANAGE_SHIPPING]}
|
managePermissions={[PermissionEnum.MANAGE_SHIPPING]}
|
||||||
allChannelsCount={allChannelsCount}
|
allChannelsCount={allChannelsCount}
|
||||||
selectedChannelsCount={shippingChannels?.length}
|
|
||||||
channelsList={data.channelListings.map(channel => ({
|
channelsList={data.channelListings.map(channel => ({
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
name: channel.name
|
name: channel.name
|
||||||
|
|
|
@ -4583,7 +4583,7 @@ exports[`Storyshots Generics / Channels availability card default 1`] = `
|
||||||
<div
|
<div
|
||||||
class="MuiTypography-root-id ChannelsAvailabilityCard-channelInfo-id MuiTypography-body1-id"
|
class="MuiTypography-root-id ChannelsAvailabilityCard-channelInfo-id MuiTypography-body1-id"
|
||||||
>
|
>
|
||||||
Available at 3 out of 4 channels
|
Available at 2 out of 4 channels
|
||||||
</div>
|
</div>
|
||||||
<hr
|
<hr
|
||||||
class="Hr-root-id ChannelsAvailabilityCard-hr-id"
|
class="Hr-root-id ChannelsAvailabilityCard-hr-id"
|
||||||
|
@ -4667,7 +4667,7 @@ exports[`Storyshots Generics / Channels availability card with onChange 1`] = `
|
||||||
<div
|
<div
|
||||||
class="MuiTypography-root-id ChannelsAvailabilityCard-channelInfo-id MuiTypography-body1-id"
|
class="MuiTypography-root-id ChannelsAvailabilityCard-channelInfo-id MuiTypography-body1-id"
|
||||||
>
|
>
|
||||||
Available at 3 out of 4 channels
|
Available at 2 out of 4 channels
|
||||||
</div>
|
</div>
|
||||||
<hr
|
<hr
|
||||||
class="Hr-root-id ChannelsAvailabilityCard-hr-id"
|
class="Hr-root-id ChannelsAvailabilityCard-hr-id"
|
||||||
|
@ -206300,11 +206300,21 @@ exports[`Storyshots Views / Products / Product edit when loading data 1`] = `
|
||||||
Availability
|
Availability
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiCardHeader-action-id"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="MuiCardContent-root-id"
|
class="MuiCardContent-root-id ChannelsAvailabilityCard-card-id"
|
||||||
>
|
>
|
||||||
You will be able to define availability of product after creating variants.
|
<div
|
||||||
|
class="MuiTypography-root-id ChannelsAvailabilityCard-channelInfo-id MuiTypography-body1-id"
|
||||||
|
>
|
||||||
|
Available at 1 out of 7 channels
|
||||||
|
</div>
|
||||||
|
<hr
|
||||||
|
class="Hr-root-id ChannelsAvailabilityCard-hr-id"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
Loading…
Reference in a new issue