Merge pull request #716 from mirumee/feature/variants-reorder

Add variants reordering possibility
This commit is contained in:
Dawid Tarasiuk 2020-09-21 10:58:28 +02:00 committed by GitHub
commit 734c9c0dc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1594 additions and 371 deletions

View file

@ -41,6 +41,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Restyle side menu - #697 by @dominik-zeglen - Restyle side menu - #697 by @dominik-zeglen
- Add error info when fetching taxes - #701 by @dominik-zeglen - Add error info when fetching taxes - #701 by @dominik-zeglen
- Fix return to previous page on screen size change - #710 by @orzechdev - Fix return to previous page on screen size change - #710 by @orzechdev
- Add variants reordering possibility - #716 by @orzechdev
## 2.10.1 ## 2.10.1

View file

@ -5897,6 +5897,10 @@
"context": "webhook events", "context": "webhook events",
"string": "Expand or restrict webhooks permissions to register certain events in Saleor system." "string": "Expand or restrict webhooks permissions to register certain events in Saleor system."
}, },
"src_dot_webhooks_dot_components_dot_WebhookEvents_dot_3316426878": {
"context": "event",
"string": "Product updated"
},
"src_dot_webhooks_dot_components_dot_WebhookEvents_dot_3345061702": { "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_3345061702": {
"context": "event", "context": "event",
"string": "Order fully paid" "string": "Order fully paid"

View file

@ -2640,6 +2640,7 @@ type Mutation {
productClearPrivateMetadata(id: ID!, input: MetaPath!): ProductClearPrivateMeta @deprecated(reason: "Use the `deletePrivateMetadata` mutation instead. This field will be removed after 2020-07-31.") productClearPrivateMetadata(id: ID!, input: MetaPath!): ProductClearPrivateMeta @deprecated(reason: "Use the `deletePrivateMetadata` mutation instead. This field will be removed after 2020-07-31.")
productSetAvailabilityForPurchase(isAvailable: Boolean!, productId: ID!, startDate: Date): ProductSetAvailabilityForPurchase productSetAvailabilityForPurchase(isAvailable: Boolean!, productId: ID!, startDate: Date): ProductSetAvailabilityForPurchase
productImageCreate(input: ProductImageCreateInput!): ProductImageCreate productImageCreate(input: ProductImageCreateInput!): ProductImageCreate
productVariantReorder(moves: [ReorderInput]!, productId: ID!): ProductVariantReorder
productImageDelete(id: ID!): ProductImageDelete productImageDelete(id: ID!): ProductImageDelete
productImageBulkDelete(ids: [ID]!): ProductImageBulkDelete productImageBulkDelete(ids: [ID]!): ProductImageBulkDelete
productImageReorder(imagesIds: [ID]!, productId: ID!): ProductImageReorder productImageReorder(imagesIds: [ID]!, productId: ID!): ProductImageReorder
@ -2999,6 +3000,7 @@ enum OrderErrorCode {
REQUIRED REQUIRED
SHIPPING_METHOD_NOT_APPLICABLE SHIPPING_METHOD_NOT_APPLICABLE
SHIPPING_METHOD_REQUIRED SHIPPING_METHOD_REQUIRED
TAX_ERROR
UNIQUE UNIQUE
VOID_INACTIVE_PAYMENT VOID_INACTIVE_PAYMENT
ZERO_QUANTITY ZERO_QUANTITY
@ -4174,6 +4176,12 @@ input ProductVariantInput {
weight: WeightScalar weight: WeightScalar
} }
type ProductVariantReorder {
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
product: Product
productErrors: [ProductError!]!
}
type ProductVariantStocksCreate { type ProductVariantStocksCreate {
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
productVariant: ProductVariant productVariant: ProductVariant
@ -5600,6 +5608,7 @@ enum WebhookEventTypeEnum {
INVOICE_SENT INVOICE_SENT
CUSTOMER_CREATED CUSTOMER_CREATED
PRODUCT_CREATED PRODUCT_CREATED
PRODUCT_UPDATED
CHECKOUT_QUANTITY_CHANGED CHECKOUT_QUANTITY_CHANGED
CHECKOUT_CREATED CHECKOUT_CREATED
CHECKOUT_UPDATED CHECKOUT_UPDATED
@ -5622,6 +5631,7 @@ enum WebhookSampleEventTypeEnum {
INVOICE_SENT INVOICE_SENT
CUSTOMER_CREATED CUSTOMER_CREATED
PRODUCT_CREATED PRODUCT_CREATED
PRODUCT_UPDATED
CHECKOUT_QUANTITY_CHANGED CHECKOUT_QUANTITY_CHANGED
CHECKOUT_CREATED CHECKOUT_CREATED
CHECKOUT_UPDATED CHECKOUT_UPDATED

View file

@ -15,6 +15,7 @@ interface ImageUploadProps {
isActiveClassName?: string; isActiveClassName?: string;
iconContainerClassName?: string; iconContainerClassName?: string;
iconContainerActiveClassName?: string; iconContainerActiveClassName?: string;
hideUploadIcon?: boolean;
onImageUpload: (file: FileList) => void; onImageUpload: (file: FileList) => void;
} }
@ -66,6 +67,7 @@ export const ImageUpload: React.FC<ImageUploadProps> = props => {
iconContainerActiveClassName, iconContainerActiveClassName,
iconContainerClassName, iconContainerClassName,
isActiveClassName, isActiveClassName,
hideUploadIcon,
onImageUpload onImageUpload
} = props; } = props;
@ -82,24 +84,26 @@ export const ImageUpload: React.FC<ImageUploadProps> = props => {
[isActiveClassName]: isDragActive [isActiveClassName]: isDragActive
})} })}
> >
<div {!hideUploadIcon && (
className={classNames(iconContainerClassName, { <div
[iconContainerActiveClassName]: isDragActive className={classNames(iconContainerClassName, {
})} [iconContainerActiveClassName]: isDragActive
> })}
<input >
{...getInputProps()} <input
className={classes.fileField} {...getInputProps()}
accept="image/*" className={classes.fileField}
/> accept="image/*"
<ImageIcon className={classes.photosIcon} />
<Typography className={classes.uploadText}>
<FormattedMessage
defaultMessage="Drop here to upload"
description="image upload"
/> />
</Typography> <ImageIcon className={classes.photosIcon} />
</div> <Typography className={classes.uploadText}>
<FormattedMessage
defaultMessage="Drop here to upload"
description="image upload"
/>
</Typography>
</div>
)}
</div> </div>
{children && children({ isDragActive })} {children && children({ isDragActive })}
</> </>

View file

@ -83,9 +83,6 @@ const useStyles = makeStyles(
imageUploadActive: { imageUploadActive: {
zIndex: 1 zIndex: 1
}, },
imageUploadIcon: {
display: "none"
},
imageUploadIconActive: { imageUploadIconActive: {
display: "block" display: "block"
}, },
@ -253,7 +250,7 @@ const ProductImages: React.FC<ProductImagesProps> = props => {
className={classes.imageUpload} className={classes.imageUpload}
isActiveClassName={classes.imageUploadActive} isActiveClassName={classes.imageUploadActive}
disableClick={true} disableClick={true}
iconContainerClassName={classes.imageUploadIcon} hideUploadIcon={true}
iconContainerActiveClassName={classes.imageUploadIconActive} iconContainerActiveClassName={classes.imageUploadIconActive}
onImageUpload={handleImageUpload} onImageUpload={handleImageUpload}
> >

View file

@ -18,7 +18,7 @@ import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories"; import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories";
import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections"; import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections";
import { FetchMoreProps, ListActions } from "@saleor/types"; import { FetchMoreProps, ListActions, ReorderAction } from "@saleor/types";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
@ -73,6 +73,7 @@ export interface ProductUpdatePageProps extends ListActions {
fetchCollections: (query: string) => void; fetchCollections: (query: string) => void;
onVariantsAdd: () => void; onVariantsAdd: () => void;
onVariantShow: (id: string) => () => void; onVariantShow: (id: string) => () => void;
onVariantReorder: ReorderAction;
onImageDelete: (id: string) => () => void; onImageDelete: (id: string) => () => void;
onBack?(); onBack?();
onDelete(); onDelete();
@ -120,6 +121,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
onVariantAdd, onVariantAdd,
onVariantsAdd, onVariantsAdd,
onVariantShow, onVariantShow,
onVariantReorder,
isChecked, isChecked,
selected, selected,
toggle, toggle,
@ -302,6 +304,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
onRowClick={onVariantShow} onRowClick={onVariantShow}
onVariantAdd={onVariantAdd} onVariantAdd={onVariantAdd}
onVariantsAdd={onVariantsAdd} onVariantsAdd={onVariantsAdd}
onVariantReorder={onVariantReorder}
toolbar={toolbar} toolbar={toolbar}
isChecked={isChecked} isChecked={isChecked}
selected={selected} selected={selected}

View file

@ -14,6 +14,7 @@ import useFormset, {
} from "@saleor/hooks/useFormset"; } from "@saleor/hooks/useFormset";
import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data"; import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { ReorderAction } from "@saleor/types";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -56,6 +57,7 @@ interface ProductVariantCreatePageProps {
onBack: () => void; onBack: () => void;
onSubmit: (data: ProductVariantCreatePageSubmitData) => void; onSubmit: (data: ProductVariantCreatePageSubmitData) => void;
onVariantClick: (variantId: string) => void; onVariantClick: (variantId: string) => void;
onVariantReorder: ReorderAction;
} }
const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
@ -69,7 +71,8 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
weightUnit, weightUnit,
onBack, onBack,
onSubmit, onSubmit,
onVariantClick onVariantClick,
onVariantReorder
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const attributeInput = React.useMemo( const attributeInput = React.useMemo(
@ -131,6 +134,7 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
return onVariantClick(variantId); return onVariantClick(variantId);
} }
}} }}
onReorder={onVariantReorder}
/> />
</div> </div>
<div> <div>

View file

@ -1,13 +1,17 @@
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import ResponsiveTable from "@saleor/components/ResponsiveTable"; import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import {
SortableTableBody,
SortableTableRow
} from "@saleor/components/SortableTable";
import TableCellAvatar from "@saleor/components/TableCellAvatar"; import TableCellAvatar from "@saleor/components/TableCellAvatar";
import { ReorderAction } from "@saleor/types";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -26,16 +30,18 @@ const useStyles = makeStyles(
cursor: "pointer" cursor: "pointer"
}, },
tabActive: { tabActive: {
"&:before": { "& > td:first-child": {
background: theme.palette.primary.main, "&:before": {
content: '""', background: theme.palette.primary.main,
height: "100%", content: '""',
left: 0, height: "100%",
position: "absolute", left: 0,
top: 0, position: "absolute",
width: 2 top: 0,
}, width: 2
position: "relative" },
position: "relative"
}
} }
}), }),
{ name: "ProductVariantNavigation" } { name: "ProductVariantNavigation" }
@ -49,10 +55,18 @@ interface ProductVariantNavigationProps {
| ProductVariantCreateData_product_variants[]; | ProductVariantCreateData_product_variants[];
onAdd?: () => void; onAdd?: () => void;
onRowClick: (variantId: string) => void; onRowClick: (variantId: string) => void;
onReorder: ReorderAction;
} }
const ProductVariantNavigation: React.FC<ProductVariantNavigationProps> = props => { const ProductVariantNavigation: React.FC<ProductVariantNavigationProps> = props => {
const { current, fallbackThumbnail, variants, onAdd, onRowClick } = props; const {
current,
fallbackThumbnail,
variants,
onAdd,
onRowClick,
onReorder
} = props;
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
@ -66,18 +80,18 @@ const ProductVariantNavigation: React.FC<ProductVariantNavigationProps> = props
})} })}
/> />
<ResponsiveTable> <ResponsiveTable>
<TableBody> <SortableTableBody onSortEnd={onReorder}>
{renderCollection(variants, variant => ( {renderCollection(variants, (variant, variantIndex) => (
<TableRow <SortableTableRow
hover={!!variant} hover={!!variant}
key={variant ? variant.id : "skeleton"} key={variant ? variant.id : "skeleton"}
className={classes.link} index={variantIndex}
className={classNames(classes.link, {
[classes.tabActive]: variant && variant.id === current
})}
onClick={variant ? () => onRowClick(variant.id) : undefined} onClick={variant ? () => onRowClick(variant.id) : undefined}
> >
<TableCellAvatar <TableCellAvatar
className={classNames({
[classes.tabActive]: variant && variant.id === current
})}
thumbnail={maybe( thumbnail={maybe(
() => variant.images[0].url, () => variant.images[0].url,
fallbackThumbnail fallbackThumbnail
@ -86,11 +100,11 @@ const ProductVariantNavigation: React.FC<ProductVariantNavigationProps> = props
<TableCell className={classes.colName}> <TableCell className={classes.colName}>
{variant ? variant.name || variant.sku : <Skeleton />} {variant ? variant.name || variant.sku : <Skeleton />}
</TableCell> </TableCell>
</TableRow> </SortableTableRow>
))} ))}
{onAdd ? ( {onAdd ? (
<TableRow> <TableRow>
<TableCell colSpan={2}> <TableCell colSpan={3}>
<Button color="primary" onClick={onAdd}> <Button color="primary" onClick={onAdd}>
<FormattedMessage <FormattedMessage
defaultMessage="Add variant" defaultMessage="Add variant"
@ -102,7 +116,7 @@ const ProductVariantNavigation: React.FC<ProductVariantNavigationProps> = props
) : ( ) : (
<TableRow> <TableRow>
<TableCellAvatar className={classes.tabActive} thumbnail={null} /> <TableCellAvatar className={classes.tabActive} thumbnail={null} />
<TableCell className={classes.colName}> <TableCell className={classes.colName} colSpan={2}>
<FormattedMessage <FormattedMessage
defaultMessage="New Variant" defaultMessage="New Variant"
description="variant name" description="variant name"
@ -110,7 +124,7 @@ const ProductVariantNavigation: React.FC<ProductVariantNavigationProps> = props
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
</TableBody> </SortableTableBody>
</ResponsiveTable> </ResponsiveTable>
</Card> </Card>
); );

View file

@ -19,6 +19,7 @@ import {
getAttributeInputFromVariant, getAttributeInputFromVariant,
getStockInputFromVariant getStockInputFromVariant
} from "@saleor/products/utils/data"; } from "@saleor/products/utils/data";
import { ReorderAction } from "@saleor/types";
import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { diff } from "fast-array-diff"; import { diff } from "fast-array-diff";
@ -60,6 +61,7 @@ interface ProductVariantPageProps {
placeholderImage?: string; placeholderImage?: string;
header: string; header: string;
warehouses: WarehouseFragment[]; warehouses: WarehouseFragment[];
onVariantReorder: ReorderAction;
onAdd(); onAdd();
onBack(); onBack();
onDelete(); onDelete();
@ -82,7 +84,8 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
onDelete, onDelete,
onImageSelect, onImageSelect,
onSubmit, onSubmit,
onVariantClick onVariantClick,
onVariantReorder
}) => { }) => {
const attributeInput = React.useMemo( const attributeInput = React.useMemo(
() => getAttributeInputFromVariant(variant), () => getAttributeInputFromVariant(variant),
@ -188,6 +191,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
return onVariantClick(variantId); return onVariantClick(variantId);
} }
}} }}
onReorder={onVariantReorder}
/> />
</div> </div>
<div> <div>

View file

@ -3,9 +3,7 @@ import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import Hidden from "@material-ui/core/Hidden"; import Hidden from "@material-ui/core/Hidden";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Checkbox from "@saleor/components/Checkbox"; import Checkbox from "@saleor/components/Checkbox";
@ -14,13 +12,17 @@ import Money from "@saleor/components/Money";
import ResponsiveTable from "@saleor/components/ResponsiveTable"; import ResponsiveTable from "@saleor/components/ResponsiveTable";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import {
SortableTableBody,
SortableTableRow
} from "@saleor/components/SortableTable";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import { ProductVariant_costPrice } from "@saleor/fragments/types/ProductVariant"; import { ProductVariant_costPrice } from "@saleor/fragments/types/ProductVariant";
import React from "react"; import React from "react";
import { FormattedMessage, IntlShape, useIntl } from "react-intl"; import { FormattedMessage, IntlShape, useIntl } from "react-intl";
import { maybe, renderCollection } from "../../../misc"; import { maybe, renderCollection } from "../../../misc";
import { ListActions } from "../../../types"; import { ListActions, ReorderAction } from "../../../types";
import { import {
ProductDetails_product_variants, ProductDetails_product_variants,
ProductDetails_product_variants_stocks_warehouse ProductDetails_product_variants_stocks_warehouse
@ -171,12 +173,13 @@ interface ProductVariantsProps extends ListActions {
disabled: boolean; disabled: boolean;
variants: ProductDetails_product_variants[]; variants: ProductDetails_product_variants[];
fallbackPrice?: ProductVariant_costPrice; fallbackPrice?: ProductVariant_costPrice;
onVariantReorder: ReorderAction;
onRowClick: (id: string) => () => void; onRowClick: (id: string) => () => void;
onVariantAdd?(); onVariantAdd?();
onVariantsAdd?(); onVariantsAdd?();
} }
const numberOfColumns = 5; const numberOfColumns = 6;
export const ProductVariants: React.FC<ProductVariantsProps> = props => { export const ProductVariants: React.FC<ProductVariantsProps> = props => {
const { const {
@ -186,6 +189,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
onRowClick, onRowClick,
onVariantAdd, onVariantAdd,
onVariantsAdd, onVariantsAdd,
onVariantReorder,
isChecked, isChecked,
selected, selected,
toggle, toggle,
@ -266,6 +270,7 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
items={variants} items={variants}
toggleAll={toggleAll} toggleAll={toggleAll}
toolbar={toolbar} toolbar={toolbar}
dragRows
> >
<TableCell className={classes.colName}> <TableCell className={classes.colName}>
<FormattedMessage <FormattedMessage
@ -291,8 +296,8 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
/> />
</TableCell> </TableCell>
</TableHead> </TableHead>
<TableBody> <SortableTableBody onSortEnd={onVariantReorder}>
{renderCollection(variants, variant => { {renderCollection(variants, (variant, variantIndex) => {
const isSelected = variant ? isChecked(variant.id) : false; const isSelected = variant ? isChecked(variant.id) : false;
const numAvailable = const numAvailable =
variant && variant.stocks variant && variant.stocks
@ -303,11 +308,12 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
: null; : null;
return ( return (
<TableRow <SortableTableRow
selected={isSelected} selected={isSelected}
hover={!!variant} hover={!!variant}
onClick={onRowClick(variant.id)} onClick={onRowClick(variant.id)}
key={variant ? variant.id : "skeleton"} key={variant ? variant.id : "skeleton"}
index={variantIndex || 0}
className={classes.link} className={classes.link}
> >
<TableCell padding="checkbox"> <TableCell padding="checkbox">
@ -354,10 +360,10 @@ export const ProductVariants: React.FC<ProductVariantsProps> = props => {
) )
)} )}
</TableCell> </TableCell>
</TableRow> </SortableTableRow>
); );
})} })}
</TableBody> </SortableTableBody>
</ResponsiveTable> </ResponsiveTable>
)} )}
</Card> </Card>

View file

@ -53,6 +53,10 @@ import {
ProductVariantBulkDelete, ProductVariantBulkDelete,
ProductVariantBulkDeleteVariables ProductVariantBulkDeleteVariables
} from "./types/ProductVariantBulkDelete"; } from "./types/ProductVariantBulkDelete";
import {
ProductVariantReorder,
ProductVariantReorderVariables
} from "./types/ProductVariantReorder";
import { import {
SimpleProductUpdate, SimpleProductUpdate,
SimpleProductUpdateVariables SimpleProductUpdateVariables
@ -628,3 +632,22 @@ export const useProductSetAvailabilityForPurchase = makeMutation<
ProductSetAvailabilityForPurchase, ProductSetAvailabilityForPurchase,
ProductSetAvailabilityForPurchaseVariables ProductSetAvailabilityForPurchaseVariables
>(productSetAvailabilityForPurchase); >(productSetAvailabilityForPurchase);
const productVariantReorder = gql`
${productErrorFragment}
${productFragmentDetails}
mutation ProductVariantReorder($move: ReorderInput!, $productId: ID!) {
productVariantReorder(moves: [$move], productId: $productId) {
errors: productErrors {
...ProductErrorFragment
}
product {
...Product
}
}
}
`;
export const useProductVariantReorderMutation = makeMutation<
ProductVariantReorder,
ProductVariantReorderVariables
>(productVariantReorder);

View file

@ -0,0 +1,236 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ReorderInput, ProductErrorCode, AttributeInputTypeEnum, WeightUnitsEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: ProductVariantReorder
// ====================================================
export interface ProductVariantReorder_productVariantReorder_errors {
__typename: "ProductError";
code: ProductErrorCode;
field: string | null;
}
export interface ProductVariantReorder_productVariantReorder_product_attributes_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
}
export interface ProductVariantReorder_productVariantReorder_product_attributes_attribute {
__typename: "Attribute";
id: string;
slug: string | null;
name: string | null;
inputType: AttributeInputTypeEnum | null;
valueRequired: boolean;
values: (ProductVariantReorder_productVariantReorder_product_attributes_attribute_values | null)[] | null;
}
export interface ProductVariantReorder_productVariantReorder_product_attributes_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
}
export interface ProductVariantReorder_productVariantReorder_product_attributes {
__typename: "SelectedAttribute";
attribute: ProductVariantReorder_productVariantReorder_product_attributes_attribute;
values: (ProductVariantReorder_productVariantReorder_product_attributes_values | null)[];
}
export interface ProductVariantReorder_productVariantReorder_product_productType_variantAttributes_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
}
export interface ProductVariantReorder_productVariantReorder_product_productType_variantAttributes {
__typename: "Attribute";
id: string;
name: string | null;
values: (ProductVariantReorder_productVariantReorder_product_productType_variantAttributes_values | null)[] | null;
}
export interface ProductVariantReorder_productVariantReorder_product_productType {
__typename: "ProductType";
id: string;
variantAttributes: (ProductVariantReorder_productVariantReorder_product_productType_variantAttributes | null)[] | null;
name: string;
hasVariants: boolean;
}
export interface ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_start_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_start {
__typename: "TaxedMoney";
gross: ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_start_gross;
}
export interface ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_stop_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_stop {
__typename: "TaxedMoney";
gross: ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_stop_gross;
}
export interface ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted {
__typename: "TaxedMoneyRange";
start: ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_start | null;
stop: ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_stop | null;
}
export interface ProductVariantReorder_productVariantReorder_product_pricing {
__typename: "ProductPricingInfo";
priceRangeUndiscounted: ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted | null;
}
export interface ProductVariantReorder_productVariantReorder_product_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductVariantReorder_productVariantReorder_product_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductVariantReorder_productVariantReorder_product_category {
__typename: "Category";
id: string;
name: string;
}
export interface ProductVariantReorder_productVariantReorder_product_collections {
__typename: "Collection";
id: string;
name: string;
}
export interface ProductVariantReorder_productVariantReorder_product_margin {
__typename: "Margin";
start: number | null;
stop: number | null;
}
export interface ProductVariantReorder_productVariantReorder_product_purchaseCost_start {
__typename: "Money";
amount: number;
currency: string;
}
export interface ProductVariantReorder_productVariantReorder_product_purchaseCost_stop {
__typename: "Money";
amount: number;
currency: string;
}
export interface ProductVariantReorder_productVariantReorder_product_purchaseCost {
__typename: "MoneyRange";
start: ProductVariantReorder_productVariantReorder_product_purchaseCost_start | null;
stop: ProductVariantReorder_productVariantReorder_product_purchaseCost_stop | null;
}
export interface ProductVariantReorder_productVariantReorder_product_images {
__typename: "ProductImage";
id: string;
alt: string;
sortOrder: number | null;
url: string;
}
export interface ProductVariantReorder_productVariantReorder_product_variants_price {
__typename: "Money";
amount: number;
currency: string;
}
export interface ProductVariantReorder_productVariantReorder_product_variants_stocks_warehouse {
__typename: "Warehouse";
id: string;
name: string;
}
export interface ProductVariantReorder_productVariantReorder_product_variants_stocks {
__typename: "Stock";
id: string;
quantity: number;
quantityAllocated: number;
warehouse: ProductVariantReorder_productVariantReorder_product_variants_stocks_warehouse;
}
export interface ProductVariantReorder_productVariantReorder_product_variants {
__typename: "ProductVariant";
id: string;
sku: string;
name: string;
price: ProductVariantReorder_productVariantReorder_product_variants_price | null;
margin: number | null;
stocks: (ProductVariantReorder_productVariantReorder_product_variants_stocks | null)[] | null;
trackInventory: boolean;
}
export interface ProductVariantReorder_productVariantReorder_product_weight {
__typename: "Weight";
unit: WeightUnitsEnum;
value: number;
}
export interface ProductVariantReorder_productVariantReorder_product {
__typename: "Product";
id: string;
attributes: ProductVariantReorder_productVariantReorder_product_attributes[];
productType: ProductVariantReorder_productVariantReorder_product_productType;
pricing: ProductVariantReorder_productVariantReorder_product_pricing | null;
metadata: (ProductVariantReorder_productVariantReorder_product_metadata | null)[];
privateMetadata: (ProductVariantReorder_productVariantReorder_product_privateMetadata | null)[];
name: string;
descriptionJson: any;
seoTitle: string | null;
seoDescription: string | null;
category: ProductVariantReorder_productVariantReorder_product_category | null;
collections: (ProductVariantReorder_productVariantReorder_product_collections | null)[] | null;
margin: ProductVariantReorder_productVariantReorder_product_margin | null;
purchaseCost: ProductVariantReorder_productVariantReorder_product_purchaseCost | null;
isAvailableForPurchase: boolean | null;
isAvailable: boolean | null;
isPublished: boolean;
chargeTaxes: boolean;
publicationDate: any | null;
images: (ProductVariantReorder_productVariantReorder_product_images | null)[] | null;
variants: (ProductVariantReorder_productVariantReorder_product_variants | null)[] | null;
weight: ProductVariantReorder_productVariantReorder_product_weight | null;
availableForPurchase: any | null;
visibleInListings: boolean;
}
export interface ProductVariantReorder_productVariantReorder {
__typename: "ProductVariantReorder";
errors: ProductVariantReorder_productVariantReorder_errors[];
product: ProductVariantReorder_productVariantReorder_product | null;
}
export interface ProductVariantReorder {
productVariantReorder: ProductVariantReorder_productVariantReorder | null;
}
export interface ProductVariantReorderVariables {
move: ReorderInput;
productId: string;
}

View file

@ -20,6 +20,7 @@ import {
useProductSetAvailabilityForPurchase, useProductSetAvailabilityForPurchase,
useProductUpdateMutation, useProductUpdateMutation,
useProductVariantBulkDeleteMutation, useProductVariantBulkDeleteMutation,
useProductVariantReorderMutation,
useSimpleProductUpdateMutation useSimpleProductUpdateMutation
} from "@saleor/products/mutations"; } from "@saleor/products/mutations";
import useCategorySearch from "@saleor/searches/useCategorySearch"; import useCategorySearch from "@saleor/searches/useCategorySearch";
@ -52,7 +53,8 @@ import {
import { import {
createImageReorderHandler, createImageReorderHandler,
createImageUploadHandler, createImageUploadHandler,
createUpdateHandler createUpdateHandler,
createVariantReorderHandler
} from "./handlers"; } from "./handlers";
interface ProductUpdateProps { interface ProductUpdateProps {
@ -232,12 +234,22 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
reorderProductImages({ variables }) reorderProductImages({ variables })
); );
const [
reorderProductVariants,
reorderProductVariantsOpts
] = useProductVariantReorderMutation({});
const handleVariantReorder = createVariantReorderHandler(product, variables =>
reorderProductVariants({ variables })
);
const disableFormSave = const disableFormSave =
createProductImageOpts.loading || createProductImageOpts.loading ||
deleteProductOpts.loading || deleteProductOpts.loading ||
reorderProductImagesOpts.loading || reorderProductImagesOpts.loading ||
updateProductOpts.loading || updateProductOpts.loading ||
productAvailabilityOpts.loading || productAvailabilityOpts.loading ||
reorderProductVariantsOpts.loading ||
loading; loading;
const formTransitionState = getMutationState( const formTransitionState = getMutationState(
@ -289,6 +301,7 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
onVariantsAdd={() => navigate(productVariantCreatorUrl(id))} onVariantsAdd={() => navigate(productVariantCreatorUrl(id))}
onVariantShow={variantId => () => onVariantShow={variantId => () =>
navigate(productVariantEditUrl(product.id, variantId))} navigate(productVariantEditUrl(product.id, variantId))}
onVariantReorder={handleVariantReorder}
onImageUpload={handleImageUpload} onImageUpload={handleImageUpload}
onImageEdit={handleImageEdit} onImageEdit={handleImageEdit}
onImageDelete={handleImageDelete} onImageDelete={handleImageDelete}

View file

@ -14,6 +14,9 @@ import {
ProductUpdate, ProductUpdate,
ProductUpdateVariables ProductUpdateVariables
} from "@saleor/products/types/ProductUpdate"; } from "@saleor/products/types/ProductUpdate";
import { ProductVariantCreateData_product } from "@saleor/products/types/ProductVariantCreateData";
import { ProductVariantDetails_productVariant_product } from "@saleor/products/types/ProductVariantDetails";
import { ProductVariantReorderVariables } from "@saleor/products/types/ProductVariantReorder";
import { import {
SimpleProductUpdate, SimpleProductUpdate,
SimpleProductUpdateVariables SimpleProductUpdateVariables
@ -134,3 +137,21 @@ export function createImageReorderHandler(
}); });
}; };
} }
export function createVariantReorderHandler(
product:
| ProductDetails_product
| ProductVariantDetails_productVariant_product
| ProductVariantCreateData_product,
reorderProductVariants: (variables: ProductVariantReorderVariables) => void
) {
return ({ newIndex, oldIndex }: ReorderEvent) => {
reorderProductVariants({
move: {
id: product.variants[oldIndex].id,
sortOrder: newIndex - oldIndex
},
productId: product.id
});
};
}

View file

@ -21,6 +21,7 @@ import ProductVariantPage, {
ProductVariantPageSubmitData ProductVariantPageSubmitData
} from "../components/ProductVariantPage"; } from "../components/ProductVariantPage";
import { import {
useProductVariantReorderMutation,
useVariantDeleteMutation, useVariantDeleteMutation,
useVariantImageAssignMutation, useVariantImageAssignMutation,
useVariantImageUnassignMutation, useVariantImageUnassignMutation,
@ -36,6 +37,7 @@ import {
ProductVariantEditUrlQueryParams ProductVariantEditUrlQueryParams
} from "../urls"; } from "../urls";
import { mapFormsetStockToStockInput } from "../utils/data"; import { mapFormsetStockToStockInput } from "../utils/data";
import { createVariantReorderHandler } from "./ProductUpdate/handlers";
interface ProductUpdateProps { interface ProductUpdateProps {
variantId: string; variantId: string;
@ -120,12 +122,23 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
return <NotFoundPage onBack={handleBack} />; return <NotFoundPage onBack={handleBack} />;
} }
const [
reorderProductVariants,
reorderProductVariantsOpts
] = useProductVariantReorderMutation({});
const handleVariantReorder = createVariantReorderHandler(
variant?.product,
variables => reorderProductVariants({ variables })
);
const disableFormSave = const disableFormSave =
loading || loading ||
deleteVariantOpts.loading || deleteVariantOpts.loading ||
updateVariantOpts.loading || updateVariantOpts.loading ||
assignImageOpts.loading || assignImageOpts.loading ||
unassignImageOpts.loading; unassignImageOpts.loading ||
reorderProductVariantsOpts.loading;
const handleImageSelect = (id: string) => () => { const handleImageSelect = (id: string) => () => {
if (variant) { if (variant) {
@ -202,6 +215,7 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
onVariantClick={variantId => { onVariantClick={variantId => {
navigate(productVariantEditUrl(productId, variantId)); navigate(productVariantEditUrl(productId, variantId));
}} }}
onVariantReorder={handleVariantReorder}
/> />
<ProductVariantDeleteDialog <ProductVariantDeleteDialog
confirmButtonState={deleteVariantOpts.status} confirmButtonState={deleteVariantOpts.status}

View file

@ -17,9 +17,13 @@ import { decimal, weight } from "../../misc";
import ProductVariantCreatePage, { import ProductVariantCreatePage, {
ProductVariantCreatePageSubmitData ProductVariantCreatePageSubmitData
} from "../components/ProductVariantCreatePage"; } from "../components/ProductVariantCreatePage";
import { useVariantCreateMutation } from "../mutations"; import {
useProductVariantReorderMutation,
useVariantCreateMutation
} from "../mutations";
import { useProductVariantCreateQuery } from "../queries"; import { useProductVariantCreateQuery } from "../queries";
import { productListUrl, productUrl, productVariantEditUrl } from "../urls"; import { productListUrl, productUrl, productVariantEditUrl } from "../urls";
import { createVariantReorderHandler } from "./ProductUpdate/handlers";
interface ProductVariantCreateProps { interface ProductVariantCreateProps {
productId: string; productId: string;
@ -69,6 +73,15 @@ export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
return <NotFoundPage onBack={() => navigate(productListUrl())} />; return <NotFoundPage onBack={() => navigate(productListUrl())} />;
} }
const [
reorderProductVariants,
reorderProductVariantsOpts
] = useProductVariantReorderMutation({});
const handleVariantReorder = createVariantReorderHandler(product, variables =>
reorderProductVariants({ variables })
);
const handleBack = () => navigate(productUrl(productId)); const handleBack = () => navigate(productUrl(productId));
const handleCreate = async (formData: ProductVariantCreatePageSubmitData) => { const handleCreate = async (formData: ProductVariantCreatePageSubmitData) => {
const result = await variantCreate({ const result = await variantCreate({
@ -104,7 +117,10 @@ export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
const handleVariantClick = (id: string) => const handleVariantClick = (id: string) =>
navigate(productVariantEditUrl(productId, id)); navigate(productVariantEditUrl(productId, id));
const disableForm = productLoading || variantCreateResult.loading; const disableForm =
productLoading ||
variantCreateResult.loading ||
reorderProductVariantsOpts.loading;
return ( return (
<> <>
@ -126,6 +142,7 @@ export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
onBack={handleBack} onBack={handleBack}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onVariantClick={handleVariantClick} onVariantClick={handleVariantClick}
onVariantReorder={handleVariantReorder}
saveButtonBarState={variantCreateResult.status} saveButtonBarState={variantCreateResult.status}
warehouses={ warehouses={
warehouses.data?.warehouses.edges.map(edge => edge.node) || [] warehouses.data?.warehouses.edges.map(edge => edge.node) || []

File diff suppressed because it is too large Load diff

View file

@ -34,6 +34,7 @@ const props: ProductUpdatePageProps = {
onImageUpload: () => undefined, onImageUpload: () => undefined,
onSubmit: () => undefined, onSubmit: () => undefined,
onVariantAdd: () => undefined, onVariantAdd: () => undefined,
onVariantReorder: () => undefined,
onVariantShow: () => undefined, onVariantShow: () => undefined,
onVariantsAdd: () => undefined, onVariantsAdd: () => undefined,
placeholderImage, placeholderImage,

View file

@ -23,6 +23,7 @@ storiesOf("Views / Products / Create product variant", module)
onBack={() => undefined} onBack={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={undefined} onVariantClick={undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
/> />
@ -54,6 +55,7 @@ storiesOf("Views / Products / Create product variant", module)
onBack={() => undefined} onBack={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={undefined} onVariantClick={undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
/> />
@ -69,6 +71,7 @@ storiesOf("Views / Products / Create product variant", module)
onBack={() => undefined} onBack={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={undefined} onVariantClick={undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
/> />
@ -87,6 +90,7 @@ storiesOf("Views / Products / Create product variant", module)
onBack={() => undefined} onBack={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={undefined} onVariantClick={undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
/> />

View file

@ -24,6 +24,7 @@ storiesOf("Views / Products / Product variant details", module)
onImageSelect={() => undefined} onImageSelect={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={() => undefined} onVariantClick={() => undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
/> />
@ -41,6 +42,7 @@ storiesOf("Views / Products / Product variant details", module)
onImageSelect={() => undefined} onImageSelect={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={() => undefined} onVariantClick={() => undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
/> />
@ -56,6 +58,7 @@ storiesOf("Views / Products / Product variant details", module)
onImageSelect={() => undefined} onImageSelect={() => undefined}
onSubmit={() => undefined} onSubmit={() => undefined}
onVariantClick={() => undefined} onVariantClick={() => undefined}
onVariantReorder={() => undefined}
saveButtonBarState="default" saveButtonBarState="default"
errors={[ errors={[
{ {

View file

@ -548,6 +548,7 @@ export enum OrderErrorCode {
REQUIRED = "REQUIRED", REQUIRED = "REQUIRED",
SHIPPING_METHOD_NOT_APPLICABLE = "SHIPPING_METHOD_NOT_APPLICABLE", SHIPPING_METHOD_NOT_APPLICABLE = "SHIPPING_METHOD_NOT_APPLICABLE",
SHIPPING_METHOD_REQUIRED = "SHIPPING_METHOD_REQUIRED", SHIPPING_METHOD_REQUIRED = "SHIPPING_METHOD_REQUIRED",
TAX_ERROR = "TAX_ERROR",
UNIQUE = "UNIQUE", UNIQUE = "UNIQUE",
VOID_INACTIVE_PAYMENT = "VOID_INACTIVE_PAYMENT", VOID_INACTIVE_PAYMENT = "VOID_INACTIVE_PAYMENT",
ZERO_QUANTITY = "ZERO_QUANTITY", ZERO_QUANTITY = "ZERO_QUANTITY",
@ -905,6 +906,7 @@ export enum WebhookEventTypeEnum {
ORDER_FULLY_PAID = "ORDER_FULLY_PAID", ORDER_FULLY_PAID = "ORDER_FULLY_PAID",
ORDER_UPDATED = "ORDER_UPDATED", ORDER_UPDATED = "ORDER_UPDATED",
PRODUCT_CREATED = "PRODUCT_CREATED", PRODUCT_CREATED = "PRODUCT_CREATED",
PRODUCT_UPDATED = "PRODUCT_UPDATED",
} }
export enum WebhookSortField { export enum WebhookSortField {

View file

@ -68,6 +68,10 @@ const WebhookEvents: React.FC<WebhookEventsProps> = ({
defaultMessage: "Product created", defaultMessage: "Product created",
description: "event" description: "event"
}), }),
[WebhookEventTypeEnum.PRODUCT_UPDATED]: intl.formatMessage({
defaultMessage: "Product updated",
description: "event"
}),
[WebhookEventTypeEnum.CHECKOUT_QUANTITY_CHANGED]: intl.formatMessage({ [WebhookEventTypeEnum.CHECKOUT_QUANTITY_CHANGED]: intl.formatMessage({
defaultMessage: "Changed quantity in checkout", defaultMessage: "Changed quantity in checkout",
description: "event" description: "event"