Merge branch 'master' into SALEOR-1403-cannot-fulfill-order-when-product-quantity-is-one

This commit is contained in:
Dominik Żegleń 2020-11-03 12:15:39 +01:00 committed by GitHub
commit d16cf2c6da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 2381 additions and 1866 deletions

View file

@ -55,6 +55,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Add manage product types and attributes permission - #768 by @orzechdev - Add manage product types and attributes permission - #768 by @orzechdev
- Fix isPublished and isAvailable behaviour for products, collections and pages - #780 by @mmarkusik - Fix isPublished and isAvailable behaviour for products, collections and pages - #780 by @mmarkusik
- Add metadata editor to page views - #782 by @dominik-zeglen - Add metadata editor to page views - #782 by @dominik-zeglen
- Add missing infinite scroll to searches - #793 by @dominik-zeglen
- Fix for fulfilling order when allocated quantity equals total quantity - #788 by @GrzegorzDerdak - Fix for fulfilling order when allocated quantity equals total quantity - #788 by @GrzegorzDerdak
## 2.10.1 ## 2.10.1

View file

@ -1400,23 +1400,15 @@
"context": "section header", "context": "section header",
"string": "App Status" "string": "App Status"
}, },
"src_dot_components_dot_AssignCategoryDialog_dot_1305061437": { "src_dot_components_dot_AssignCategoryDialog_dot_3125506097": {
"string": "Search Categories"
},
"src_dot_components_dot_AssignCategoryDialog_dot_190977792": {
"context": "dialog header", "context": "dialog header",
"string": "Assign Categories" "string": "Assign Category"
}, },
"src_dot_components_dot_AssignCategoryDialog_dot_3690273268": { "src_dot_components_dot_AssignCategoryDialog_dot_3690273268": {
"string": "Search by category name, etc..." "string": "Search by category name, etc..."
}, },
"src_dot_components_dot_AssignCategoryDialog_dot_3973677075": { "src_dot_components_dot_AssignCategoryDialog_dot_3841025483": {
"context": "button", "string": "Search Category"
"string": "Assign categories"
},
"src_dot_components_dot_AssignCollectionDialog_dot_1035511604": {
"context": "button",
"string": "Assign collections"
}, },
"src_dot_components_dot_AssignCollectionDialog_dot_2605414502": { "src_dot_components_dot_AssignCollectionDialog_dot_2605414502": {
"string": "Search by collection name, etc..." "string": "Search by collection name, etc..."
@ -1428,6 +1420,10 @@
"src_dot_components_dot_AssignCollectionDialog_dot_4057224233": { "src_dot_components_dot_AssignCollectionDialog_dot_4057224233": {
"string": "Search Collection" "string": "Search Collection"
}, },
"src_dot_components_dot_AssignContainerDialog_dot_1731102929": {
"context": "button",
"string": "Assign"
},
"src_dot_components_dot_AssignProductDialog_dot_2100305525": { "src_dot_components_dot_AssignProductDialog_dot_2100305525": {
"context": "button", "context": "button",
"string": "Assign products" "string": "Assign products"

12
package-lock.json generated
View file

@ -9537,6 +9537,12 @@
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true "dev": true
}, },
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
},
"log-symbols": { "log-symbols": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
@ -16392,9 +16398,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.15", "version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
}, },
"lodash-es": { "lodash-es": {
"version": "4.17.15", "version": "4.17.15",

View file

@ -44,7 +44,7 @@
"is-url": "^1.2.4", "is-url": "^1.2.4",
"jss": "^9.8.7", "jss": "^9.8.7",
"keycode": "^2.2.0", "keycode": "^2.2.0",
"lodash": "^4.17.14", "lodash": "^4.17.20",
"lodash-es": "^4.17.14", "lodash-es": "^4.17.14",
"moment-timezone": "^0.5.26", "moment-timezone": "^0.5.26",
"qs": "^6.9.0", "qs": "^6.9.0",

View file

@ -10,6 +10,7 @@ import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo"; import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { PermissionEnum } from "@saleor/types/globalTypes"; import { PermissionEnum } from "@saleor/types/globalTypes";
import { getFormErrors } from "@saleor/utils/errors"; import { getFormErrors } from "@saleor/utils/errors";
@ -42,7 +43,7 @@ export interface CustomAppDetailsPageProps {
onTokenDelete: (id: string) => void; onTokenDelete: (id: string) => void;
onTokenClose: () => void; onTokenClose: () => void;
onTokenCreate: () => void; onTokenCreate: () => void;
onSubmit: (data: CustomAppDetailsPageFormData) => void; onSubmit: (data: CustomAppDetailsPageFormData) => SubmitPromise;
onWebhookCreate: () => void; onWebhookCreate: () => void;
onWebhookRemove: (id: string) => void; onWebhookRemove: (id: string) => void;
navigateToWebhookDetails: (id: string) => () => void; navigateToWebhookDetails: (id: string) => () => void;

View file

@ -130,8 +130,8 @@ export const CustomAppDetails: React.FC<OrderListProps> = ({
onCompleted: onTokenDelete onCompleted: onTokenDelete
}); });
const handleSubmit = (data: CustomAppDetailsPageFormData) => const handleSubmit = async (data: CustomAppDetailsPageFormData) => {
updateApp({ const result = await updateApp({
variables: { variables: {
id, id,
input: { input: {
@ -144,6 +144,9 @@ export const CustomAppDetails: React.FC<OrderListProps> = ({
} }
}); });
return result.data.appUpdate.errors;
};
const handleTokenCreate = (name: string) => const handleTokenCreate = (name: string) =>
createToken({ createToken({
variables: { variables: {

View file

@ -126,7 +126,7 @@ const AttributePage: React.FC<AttributePageProps> = ({
? data.privateMetadata ? data.privateMetadata
: undefined; : undefined;
onSubmit({ return onSubmit({
...data, ...data,
metadata, metadata,
privateMetadata, privateMetadata,

View file

@ -13,6 +13,7 @@ import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm"; import SeoForm from "@saleor/components/SeoForm";
import { Tab, TabContainer } from "@saleor/components/Tab"; import { Tab, TabContainer } from "@saleor/components/Tab";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
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";
@ -61,7 +62,7 @@ export interface CategoryUpdatePageProps
}; };
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
onImageDelete: () => void; onImageDelete: () => void;
onSubmit: (data: FormData) => void; onSubmit: (data: FormData) => SubmitPromise;
onImageUpload(file: File); onImageUpload(file: File);
onNextPage(); onNextPage();
onPreviousPage(); onPreviousPage();
@ -139,7 +140,7 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
? data.privateMetadata ? data.privateMetadata
: undefined; : undefined;
onSubmit({ return onSubmit({
...data, ...data,
metadata, metadata,
privateMetadata privateMetadata

View file

@ -81,7 +81,7 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
? data.privateMetadata ? data.privateMetadata
: undefined; : undefined;
onSubmit({ return onSubmit({
...data, ...data,
isPublished: data.isPublished || !!data.publicationDate, isPublished: data.isPublished || !!data.publicationDate,
metadata, metadata,

View file

@ -60,7 +60,7 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
); );
const paginate = usePaginator(); const paginate = usePaginator();
const intl = useIntl(); const intl = useIntl();
const { search, result } = useProductSearch({ const { search, loadMore, result } = useProductSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA variables: DEFAULT_INITIAL_SEARCH_DATA
}); });
const [updateMetadata] = useMetadataUpdate({}); const [updateMetadata] = useMetadataUpdate({});
@ -296,8 +296,10 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
/> />
<AssignProductDialog <AssignProductDialog
confirmButtonState={assignProductOpts.status} confirmButtonState={assignProductOpts.status}
hasMore={result.data?.search?.pageInfo.hasNextPage}
open={params.action === "assign"} open={params.action === "assign"}
onFetch={search} onFetch={search}
onFetchMore={loadMore}
loading={result.loading} loading={result.loading}
onClose={closeModal} onClose={closeModal}
onSubmit={products => onSubmit={products =>

View file

@ -1,7 +1,15 @@
import React from "react"; import React from "react";
const AppActionContext = React.createContext<React.RefObject<HTMLDivElement>>( interface AppAction {
undefined anchor: React.RefObject<HTMLDivElement>;
); docked: boolean;
setDocked: (docked: boolean) => void;
}
const AppActionContext = React.createContext<AppAction>({
anchor: undefined,
docked: true,
setDocked: () => undefined
});
export const useAppAction = () => React.useContext(AppActionContext);
export default AppActionContext; export default AppActionContext;

View file

@ -50,6 +50,9 @@ const useStyles = makeStyles(
position: "sticky", position: "sticky",
zIndex: 10 zIndex: 10
}, },
appActionDocked: {
position: "static"
},
appLoader: { appLoader: {
height: appLoaderHeight, height: appLoaderHeight,
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
@ -313,6 +316,7 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
const [appState, dispatchAppState] = useAppState(); const [appState, dispatchAppState] = useAppState();
const { location } = useRouter(); const { location } = useRouter();
const [isNavigatorVisible, setNavigatorVisibility] = React.useState(false); const [isNavigatorVisible, setNavigatorVisibility] = React.useState(false);
const [docked, setDocked] = React.useState(true);
const menuStructure = createMenuStructure(intl); const menuStructure = createMenuStructure(intl);
const configurationMenu = createConfigurationMenu(intl); const configurationMenu = createConfigurationMenu(intl);
@ -365,7 +369,13 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
setVisibility={setNavigatorVisibility} setVisibility={setNavigatorVisibility}
/> />
<AppHeaderContext.Provider value={appHeaderAnchor}> <AppHeaderContext.Provider value={appHeaderAnchor}>
<AppActionContext.Provider value={appActionAnchor}> <AppActionContext.Provider
value={{
anchor: appActionAnchor,
docked,
setDocked
}}
>
<div className={classes.root}> <div className={classes.root}>
<div className={classes.sideBar}> <div className={classes.sideBar}>
<ResponsiveDrawer <ResponsiveDrawer
@ -533,7 +543,12 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
: children} : children}
</main> </main>
</div> </div>
<div className={classes.appAction} ref={appActionAnchor} /> <div
className={classNames(classes.appAction, {
[classes.appActionDocked]: docked
})}
ref={appActionAnchor}
/>
</div> </div>
</div> </div>
</AppActionContext.Provider> </AppActionContext.Provider>

View file

@ -1,185 +1,41 @@
import Button from "@material-ui/core/Button";
import CircularProgress from "@material-ui/core/CircularProgress";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import FormSpacer from "@saleor/components/FormSpacer";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl";
import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories"; import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { useIntl } from "react-intl";
import Checkbox from "../Checkbox"; import AssignContainerDialog, {
AssignContainerDialogProps
} from "../AssignContainerDialog";
export interface FormData { interface AssignCategoryDialogProps
extends Omit<AssignContainerDialogProps, "containers" | "title" | "search"> {
categories: SearchCategories_search_edges_node[]; categories: SearchCategories_search_edges_node[];
query: string;
} }
const useStyles = makeStyles( const AssignCategoryDialog: React.FC<AssignCategoryDialogProps> = ({
{ categories,
avatar: { ...rest
"&:first-child": { }) => {
paddingLeft: 0
}
},
checkboxCell: {
paddingLeft: 0
},
overflow: {
overflowY: "visible"
},
wideCell: {
width: "100%"
}
},
{ name: "AssignCategoryDialog" }
);
interface AssignCategoriesDialogProps {
categories: SearchCategories_search_edges_node[];
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
loading: boolean;
onClose: () => void;
onFetch: (value: string) => void;
onSubmit: (data: SearchCategories_search_edges_node[]) => void;
}
function handleCategoryAssign(
product: SearchCategories_search_edges_node,
isSelected: boolean,
selectedCategories: SearchCategories_search_edges_node[],
setSelectedCategories: (data: SearchCategories_search_edges_node[]) => void
) {
if (isSelected) {
setSelectedCategories(
selectedCategories.filter(
selectedProduct => selectedProduct.id !== product.id
)
);
} else {
setSelectedCategories([...selectedCategories, product]);
}
}
const AssignCategoriesDialog: React.FC<AssignCategoriesDialogProps> = props => {
const {
confirmButtonState,
open,
loading,
categories: categories,
onClose,
onFetch,
onSubmit
} = props;
const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedCategories, setSelectedCategories] = React.useState<
SearchCategories_search_edges_node[]
>([]);
const handleSubmit = () => onSubmit(selectedCategories);
return ( return (
<Dialog <AssignContainerDialog
open={open} containers={categories}
onClose={onClose} search={{
classes={{ paper: classes.overflow }} label: intl.formatMessage({
fullWidth defaultMessage: "Search Category"
maxWidth="sm" }),
> placeholder: intl.formatMessage({
<DialogTitle> defaultMessage: "Search by category name, etc..."
<FormattedMessage })
defaultMessage="Assign Categories" }}
description="dialog header" title={intl.formatMessage({
/> defaultMessage: "Assign Category",
</DialogTitle> description: "dialog header"
<DialogContent className={classes.overflow}> })}
<TextField {...rest}
name="query" />
value={query}
onChange={onQueryChange}
label={intl.formatMessage({
defaultMessage: "Search Categories"
})}
placeholder={intl.formatMessage({
defaultMessage: "Search by category name, etc..."
})}
fullWidth
InputProps={{
autoComplete: "off",
endAdornment: loading && <CircularProgress size={16} />
}}
/>
<FormSpacer />
<ResponsiveTable>
<TableBody>
{categories &&
categories.map(category => {
const isSelected = !!selectedCategories.find(
selectedCategories => selectedCategories.id === category.id
);
return (
<TableRow key={category.id}>
<TableCell
padding="checkbox"
className={classes.checkboxCell}
>
<Checkbox
checked={isSelected}
onChange={() =>
handleCategoryAssign(
category,
isSelected,
selectedCategories,
setSelectedCategories
)
}
/>
</TableCell>
<TableCell className={classes.wideCell}>
{category.name}
</TableCell>
</TableRow>
);
})}
</TableBody>
</ResponsiveTable>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<ConfirmButton
transitionState={confirmButtonState}
color="primary"
variant="contained"
type="submit"
onClick={handleSubmit}
>
<FormattedMessage
defaultMessage="Assign categories"
description="button"
/>
</ConfirmButton>
</DialogActions>
</Dialog>
); );
}; };
AssignCategoriesDialog.displayName = "AssignCategoriesDialog";
export default AssignCategoriesDialog; AssignCategoryDialog.displayName = "AssignCategoryDialog";
export default AssignCategoryDialog;

View file

@ -1,185 +1,41 @@
import Button from "@material-ui/core/Button";
import CircularProgress from "@material-ui/core/CircularProgress";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl";
import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections"; import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { useIntl } from "react-intl";
import Checkbox from "../Checkbox"; import AssignContainerDialog, {
import ConfirmButton, { AssignContainerDialogProps
ConfirmButtonTransitionState } from "../AssignContainerDialog";
} from "../ConfirmButton/ConfirmButton";
import FormSpacer from "../FormSpacer";
export interface FormData { interface AssignCollectionDialogProps
extends Omit<AssignContainerDialogProps, "containers" | "title" | "search"> {
collections: SearchCollections_search_edges_node[]; collections: SearchCollections_search_edges_node[];
query: string;
} }
const useStyles = makeStyles( const AssignCollectionDialog: React.FC<AssignCollectionDialogProps> = ({
{ collections,
avatar: { ...rest
"&:first-child": { }) => {
paddingLeft: 0
}
},
checkboxCell: {
paddingLeft: 0
},
overflow: {
overflowY: "visible"
},
wideCell: {
width: "100%"
}
},
{ name: "AssignCollectionDialog" }
);
interface AssignCollectionDialogProps {
collections: SearchCollections_search_edges_node[];
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
loading: boolean;
onClose: () => void;
onFetch: (value: string) => void;
onSubmit: (data: SearchCollections_search_edges_node[]) => void;
}
function handleCollectionAssign(
product: SearchCollections_search_edges_node,
isSelected: boolean,
selectedCollections: SearchCollections_search_edges_node[],
setSelectedCollections: (data: SearchCollections_search_edges_node[]) => void
) {
if (isSelected) {
setSelectedCollections(
selectedCollections.filter(
selectedProduct => selectedProduct.id !== product.id
)
);
} else {
setSelectedCollections([...selectedCollections, product]);
}
}
const AssignCollectionDialog: React.FC<AssignCollectionDialogProps> = props => {
const {
confirmButtonState,
open,
loading,
collections,
onClose,
onFetch,
onSubmit
} = props;
const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedCollections, setSelectedCollections] = React.useState<
SearchCollections_search_edges_node[]
>([]);
const handleSubmit = () => onSubmit(selectedCollections);
return ( return (
<Dialog <AssignContainerDialog
onClose={onClose} containers={collections}
open={open} search={{
classes={{ paper: classes.overflow }} label: intl.formatMessage({
fullWidth defaultMessage: "Search Collection"
maxWidth="sm" }),
> placeholder: intl.formatMessage({
<DialogTitle> defaultMessage: "Search by collection name, etc..."
<FormattedMessage })
defaultMessage="Assign Collection" }}
description="dialog header" title={intl.formatMessage({
/> defaultMessage: "Assign Collection",
</DialogTitle> description: "dialog header"
<DialogContent className={classes.overflow}> })}
<TextField {...rest}
name="query" />
value={query}
onChange={onQueryChange}
label={intl.formatMessage({
defaultMessage: "Search Collection"
})}
placeholder={intl.formatMessage({
defaultMessage: "Search by collection name, etc..."
})}
fullWidth
InputProps={{
autoComplete: "off",
endAdornment: loading && <CircularProgress size={16} />
}}
/>
<FormSpacer />
<ResponsiveTable>
<TableBody>
{collections &&
collections.map(collection => {
const isSelected = !!selectedCollections.find(
selectedCollection => selectedCollection.id === collection.id
);
return (
<TableRow key={collection.id}>
<TableCell
padding="checkbox"
className={classes.checkboxCell}
>
<Checkbox
checked={isSelected}
onChange={() =>
handleCollectionAssign(
collection,
isSelected,
selectedCollections,
setSelectedCollections
)
}
/>
</TableCell>
<TableCell className={classes.wideCell}>
{collection.name}
</TableCell>
</TableRow>
);
})}
</TableBody>
</ResponsiveTable>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<ConfirmButton
transitionState={confirmButtonState}
color="primary"
variant="contained"
type="submit"
onClick={handleSubmit}
>
<FormattedMessage
defaultMessage="Assign collections"
description="button"
/>
</ConfirmButton>
</DialogActions>
</Dialog>
); );
}; };
AssignCollectionDialog.displayName = "AssignCollectionDialog"; AssignCollectionDialog.displayName = "AssignCollectionDialog";
export default AssignCollectionDialog; export default AssignCollectionDialog;

View file

@ -0,0 +1,204 @@
import Button from "@material-ui/core/Button";
import CircularProgress from "@material-ui/core/CircularProgress";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import { makeStyles } from "@material-ui/core/styles";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl";
import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle";
import { FetchMoreProps, Node } from "@saleor/types";
import React from "react";
import InfiniteScroll from "react-infinite-scroller";
import { FormattedMessage } from "react-intl";
import Checkbox from "../Checkbox";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "../ConfirmButton/ConfirmButton";
import FormSpacer from "../FormSpacer";
export interface FormData {
containers: string[];
query: string;
}
const useStyles = makeStyles(
{
avatar: {
"&:first-child": {
paddingLeft: 0
}
},
checkboxCell: {
paddingLeft: 0
},
wideCell: {
width: "100%"
}
},
{ name: "AssignContainerDialog" }
);
interface Container extends Node {
name: string;
}
export interface AssignContainerDialogProps extends FetchMoreProps {
confirmButtonState: ConfirmButtonTransitionState;
containers: Container[];
loading: boolean;
open: boolean;
search: Record<"label" | "placeholder", string>;
title: string;
onClose: () => void;
onFetch: (value: string) => void;
onSubmit: (data: string[]) => void;
}
function handleContainerAssign(
containerId: string,
isSelected: boolean,
selectedContainers: string[],
setSelectedContainers: (data: string[]) => void
) {
if (isSelected) {
setSelectedContainers(
selectedContainers.filter(
selectedContainer => selectedContainer !== containerId
)
);
} else {
setSelectedContainers([...selectedContainers, containerId]);
}
}
const AssignContainerDialog: React.FC<AssignContainerDialogProps> = props => {
const {
confirmButtonState,
containers,
hasMore,
loading,
open,
search,
title,
onClose,
onFetch,
onFetchMore,
onSubmit
} = props;
const classes = useStyles(props);
const scrollableDialogClasses = useScrollableDialogStyle({});
const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedContainers, setSelectedContainers] = React.useState<string[]>(
[]
);
const container = React.useRef<HTMLDivElement>();
const handleSubmit = () => onSubmit(selectedContainers);
const containerHeight = container.current?.scrollHeight - 130;
return (
<Dialog
onClose={onClose}
open={open}
classes={{ paper: scrollableDialogClasses.dialog }}
fullWidth
maxWidth="sm"
>
<DialogTitle>{title}</DialogTitle>
<DialogContent
className={scrollableDialogClasses.content}
ref={container}
>
<TextField
name="query"
value={query}
onChange={onQueryChange}
label={search.label}
placeholder={search.placeholder}
fullWidth
InputProps={{
autoComplete: "off",
endAdornment: loading && <CircularProgress size={16} />
}}
/>
<FormSpacer />
<div
className={scrollableDialogClasses.scrollArea}
style={{ height: containerHeight }}
>
<InfiniteScroll
pageStart={0}
loadMore={onFetchMore}
hasMore={hasMore}
useWindow={false}
loader={
<div className={scrollableDialogClasses.loadMoreLoaderContainer}>
<CircularProgress size={16} />
</div>
}
threshold={10}
>
<ResponsiveTable>
<TableBody>
{containers?.map(container => {
const isSelected = !!selectedContainers.find(
selectedContainer => selectedContainer === container.id
);
return (
<TableRow key={container.id}>
<TableCell
padding="checkbox"
className={classes.checkboxCell}
>
<Checkbox
checked={isSelected}
onChange={() =>
handleContainerAssign(
container.id,
isSelected,
selectedContainers,
setSelectedContainers
)
}
/>
</TableCell>
<TableCell className={classes.wideCell}>
{container.name}
</TableCell>
</TableRow>
);
})}
</TableBody>
</ResponsiveTable>
</InfiniteScroll>
</div>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<ConfirmButton
transitionState={confirmButtonState}
color="primary"
variant="contained"
type="submit"
onClick={handleSubmit}
>
<FormattedMessage defaultMessage="Assign" description="button" />
</ConfirmButton>
</DialogActions>
</Dialog>
);
};
AssignContainerDialog.displayName = "AssignContainerDialog";
export default AssignContainerDialog;

View file

@ -0,0 +1,2 @@
export * from "./AssignContainerDialog";
export { default } from "./AssignContainerDialog";

View file

@ -19,7 +19,10 @@ import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts"; import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts";
import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle";
import { FetchMoreProps } from "@saleor/types";
import React from "react"; import React from "react";
import InfiniteScroll from "react-infinite-scroller";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
@ -43,18 +46,12 @@ const useStyles = makeStyles(
}, },
colName: { colName: {
paddingLeft: 0 paddingLeft: 0
},
overflow: {
overflowY: "visible"
},
scrollArea: {
overflowY: "scroll"
} }
}, },
{ name: "AssignProductDialog" } { name: "AssignProductDialog" }
); );
export interface AssignProductDialogProps { export interface AssignProductDialogProps extends FetchMoreProps {
confirmButtonState: ConfirmButtonTransitionState; confirmButtonState: ConfirmButtonTransitionState;
open: boolean; open: boolean;
products: SearchProducts_search_edges_node[]; products: SearchProducts_search_edges_node[];
@ -84,28 +81,34 @@ function handleProductAssign(
const AssignProductDialog: React.FC<AssignProductDialogProps> = props => { const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
const { const {
confirmButtonState, confirmButtonState,
hasMore,
open, open,
loading, loading,
products, products,
onClose, onClose,
onFetch, onFetch,
onFetchMore,
onSubmit onSubmit
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
const scrollableDialogClasses = useScrollableDialogStyle({});
const intl = useIntl(); const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch); const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedProducts, setSelectedProducts] = React.useState< const [selectedProducts, setSelectedProducts] = React.useState<
SearchProducts_search_edges_node[] SearchProducts_search_edges_node[]
>([]); >([]);
const container = React.useRef<HTMLDivElement>();
const handleSubmit = () => onSubmit(selectedProducts); const handleSubmit = () => onSubmit(selectedProducts);
const containerHeight = container.current?.scrollHeight - 130;
return ( return (
<Dialog <Dialog
onClose={onClose} onClose={onClose}
open={open} open={open}
classes={{ paper: classes.overflow }} classes={{ paper: scrollableDialogClasses.dialog }}
fullWidth fullWidth
maxWidth="sm" maxWidth="sm"
> >
@ -115,7 +118,10 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
description="dialog header" description="dialog header"
/> />
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent
className={scrollableDialogClasses.content}
ref={container}
>
<TextField <TextField
name="query" name="query"
value={query} value={query}
@ -134,45 +140,61 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
}} }}
/> />
<FormSpacer /> <FormSpacer />
<div className={classes.scrollArea}> <div
<ResponsiveTable> className={scrollableDialogClasses.scrollArea}
<TableBody> style={{ height: containerHeight }}
{products && >
products.map(product => { <InfiniteScroll
const isSelected = selectedProducts.some( pageStart={0}
selectedProduct => selectedProduct.id === product.id loadMore={onFetchMore}
); hasMore={hasMore}
useWindow={false}
loader={
<div className={scrollableDialogClasses.loadMoreLoaderContainer}>
<CircularProgress size={16} />
</div>
}
threshold={10}
>
<ResponsiveTable key="table">
<TableBody>
{products &&
products.map(product => {
const isSelected = selectedProducts.some(
selectedProduct => selectedProduct.id === product.id
);
return ( return (
<TableRow key={product.id}> <TableRow key={product.id}>
<TableCellAvatar <TableCellAvatar
className={classes.avatar} className={classes.avatar}
thumbnail={maybe(() => product.thumbnail.url)} thumbnail={maybe(() => product.thumbnail.url)}
/>
<TableCell className={classes.colName}>
{product.name}
</TableCell>
<TableCell
padding="checkbox"
className={classes.checkboxCell}
>
<Checkbox
checked={isSelected}
onChange={() =>
handleProductAssign(
product,
isSelected,
selectedProducts,
setSelectedProducts
)
}
/> />
</TableCell> <TableCell className={classes.colName}>
</TableRow> {product.name}
); </TableCell>
})} <TableCell
</TableBody> padding="checkbox"
</ResponsiveTable> className={classes.checkboxCell}
>
<Checkbox
checked={isSelected}
onChange={() =>
handleProductAssign(
product,
isSelected,
selectedProducts,
setSelectedProducts
)
}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</ResponsiveTable>
</InfiniteScroll>
</div> </div>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>

View file

@ -7,6 +7,7 @@ import {
withStyles withStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import CheckIcon from "@material-ui/icons/Check"; import CheckIcon from "@material-ui/icons/Check";
import { DEFAULT_NOTIFICATION_SHOW_TIME } from "@saleor/config";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
@ -61,6 +62,7 @@ export interface ConfirmButtonProps
extends Omit<ButtonProps, "classes">, extends Omit<ButtonProps, "classes">,
WithStyles<typeof styles> { WithStyles<typeof styles> {
transitionState: ConfirmButtonTransitionState; transitionState: ConfirmButtonTransitionState;
onTransitionToDefault?: () => void;
} }
interface ConfirmButtonState { interface ConfirmButtonState {
@ -93,20 +95,21 @@ const ConfirmButton = withStyles(styles, { name: "ConfirmButton" })(
} }
componentDidUpdate(prevProps: ConfirmButtonProps) { componentDidUpdate(prevProps: ConfirmButtonProps) {
const { transitionState } = this.props; const { transitionState, onTransitionToDefault } = this.props;
if (prevProps.transitionState !== transitionState) { if (prevProps.transitionState !== transitionState) {
if ( if (
(["error", "success"] as ConfirmButtonTransitionState[]).includes( (["error", "success"] as ConfirmButtonTransitionState[]).includes(
transitionState transitionState
) )
) { ) {
this.timeout = setTimeout( this.timeout = setTimeout(() => {
() => this.setState({
this.setState({ displayCompletedActionState: false
displayCompletedActionState: false });
}), if (onTransitionToDefault) {
2000 onTransitionToDefault();
); }
}, DEFAULT_NOTIFICATION_SHOW_TIME);
} else if (transitionState === "loading") { } else if (transitionState === "loading") {
clearTimeout(this.timeout); clearTimeout(this.timeout);
} }
@ -125,6 +128,7 @@ const ConfirmButton = withStyles(styles, { name: "ConfirmButton" })(
disabled, disabled,
transitionState, transitionState,
onClick, onClick,
onTransitionToDefault: _,
...props ...props
} = this.props; } = this.props;
const { displayCompletedActionState } = this.state; const { displayCompletedActionState } = this.state;

View file

@ -1,4 +1,4 @@
import useForm, { UseFormResult } from "@saleor/hooks/useForm"; import useForm, { SubmitPromise, UseFormResult } from "@saleor/hooks/useForm";
import React from "react"; import React from "react";
export interface FormProps<T> { export interface FormProps<T> {
@ -6,7 +6,7 @@ export interface FormProps<T> {
confirmLeave?: boolean; confirmLeave?: boolean;
initial?: T; initial?: T;
resetOnSubmit?: boolean; resetOnSubmit?: boolean;
onSubmit?: (data: T) => void; onSubmit?: (data: T) => SubmitPromise | void;
} }
function Form<T>(props: FormProps<T>) { function Form<T>(props: FormProps<T>) {

View file

@ -8,8 +8,7 @@ import { buttonMessages } from "@saleor/intl";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { maybe } from "../../misc"; import { useAppAction } from "../AppLayout/AppActionContext";
import AppActionContext from "../AppLayout/AppActionContext";
import ConfirmButton, { import ConfirmButton, {
ConfirmButtonTransitionState ConfirmButtonTransitionState
} from "../ConfirmButton/ConfirmButton"; } from "../ConfirmButton/ConfirmButton";
@ -41,8 +40,12 @@ const useStyles = makeStyles(
backgroundColor: theme.palette.error.main, backgroundColor: theme.palette.error.main,
color: theme.palette.error.contrastText color: theme.palette.error.contrastText
}, },
paper: {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0
},
root: { root: {
height: 120 height: 70
}, },
spacer: { spacer: {
flex: "1" flex: "1"
@ -76,67 +79,63 @@ export const SaveButtonBar: React.FC<SaveButtonBarProps> = props => {
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
const appAction = useAppAction();
const intl = useIntl(); const intl = useIntl();
const scrollPosition = useWindowScroll(); const scrollPosition = useWindowScroll();
React.useEffect(() => {
if (!disabled && state !== "loading") {
appAction.setDocked(false);
}
}, [disabled]);
React.useEffect(() => () => appAction.setDocked(true), []);
const scrolledToBottom = const scrolledToBottom =
scrollPosition.y + window.innerHeight >= document.body.scrollHeight; scrollPosition.y + window.innerHeight >= document.body.scrollHeight;
const shouldDisplay = onDelete || !disabled;
return ( return appAction.anchor ? (
<AppActionContext.Consumer> <Portal container={appAction.anchor.current}>
{anchor => <div className={classes.root} {...rest}>
anchor ? ( <Container>
<Portal container={anchor.current}> <Card
{shouldDisplay && ( className={classes.paper}
<div className={classes.root} {...rest}> elevation={!(appAction.docked || scrolledToBottom) ? 16 : 0}
<Container> >
<Card elevation={scrolledToBottom ? 0 : 16}> <CardContent className={classes.content}>
<CardContent className={classes.content}> {!!onDelete && (
{!!onDelete && ( <Button
<Button variant="contained"
variant="contained" onClick={onDelete}
onClick={onDelete} className={classes.deleteButton}
className={classes.deleteButton} data-test="button-bar-delete"
data-test="button-bar-delete" >
> {labels?.delete || intl.formatMessage(buttonMessages.delete)}
{labels && labels.delete </Button>
? labels.delete )}
: intl.formatMessage(buttonMessages.delete)} <div className={classes.spacer} />
</Button> <Button
)} className={classes.cancelButton}
<div className={classes.spacer} /> variant="text"
<Button onClick={onCancel}
className={classes.cancelButton} data-test="button-bar-cancel"
variant="text" >
onClick={onCancel} {labels?.cancel || intl.formatMessage(buttonMessages.back)}
data-test="button-bar-cancel" </Button>
> <ConfirmButton
{maybe( disabled={disabled}
() => labels.cancel, onClick={onSave}
intl.formatMessage(buttonMessages.back) transitionState={state}
)} data-test="button-bar-confirm"
</Button> onTransitionToDefault={() => appAction.setDocked(true)}
<ConfirmButton >
disabled={disabled} {labels?.save || intl.formatMessage(buttonMessages.save)}
onClick={onSave} </ConfirmButton>
transitionState={state} </CardContent>
data-test="button-bar-confirm" </Card>
> </Container>
{maybe( </div>
() => labels.save, </Portal>
intl.formatMessage(buttonMessages.save) ) : null;
)}
</ConfirmButton>
</CardContent>
</Card>
</Container>
</div>
)}
</Portal>
) : null
}
</AppActionContext.Consumer>
);
}; };
SaveButtonBar.displayName = "SaveButtonBar"; SaveButtonBar.displayName = "SaveButtonBar";
export default SaveButtonBar; export default SaveButtonBar;

View file

@ -1,3 +1,4 @@
import { DEFAULT_NOTIFICATION_SHOW_TIME } from "@saleor/config";
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { TransitionGroup } from "react-transition-group"; import { TransitionGroup } from "react-transition-group";
@ -43,32 +44,35 @@ const MessageManagerProvider = ({ children }) => {
); );
}, []); }, []);
const show = useCallback((message = {}, timeout = 3000) => { const show = useCallback(
const id = Date.now(); (message = {}, timeout = DEFAULT_NOTIFICATION_SHOW_TIME) => {
const notification = { const id = Date.now();
close: () => remove(id), const notification = {
id, close: () => remove(id),
message, id,
timeout message,
}; timeout
if (timeout !== null) { };
const timeoutId = window.setTimeout(() => { if (timeout !== null) {
timerCallback(notification); const timeoutId = window.setTimeout(() => {
}, timeout); timerCallback(notification);
}, timeout);
timersArr.current.push({ timersArr.current.push({
id: notification.id, id: notification.id,
notification, notification,
remaining: timeout, remaining: timeout,
start: new Date().getTime(), start: new Date().getTime(),
timeoutId timeoutId
}); });
} }
setNotifications(state => [notification, ...state]); setNotifications(state => [notification, ...state]);
return notification; return notification;
}, []); },
[]
);
const getCurrentTimer = (notification: INotification) => { const getCurrentTimer = (notification: INotification) => {
const currentTimerIndex = timersArr.current.findIndex( const currentTimerIndex = timersArr.current.findIndex(

View file

@ -96,3 +96,5 @@ export const defaultListSettings: AppListViewSettings = {
export const APP_VERSION = packageInfo.version; export const APP_VERSION = packageInfo.version;
export const DEMO_MODE = process.env.DEMO_MODE === "true"; export const DEMO_MODE = process.env.DEMO_MODE === "true";
export const GTM_ID = process.env.GTM_ID; export const GTM_ID = process.env.GTM_ID;
export const DEFAULT_NOTIFICATION_SHOW_TIME = 3000;

View file

@ -76,7 +76,7 @@ const CustomerCreatePage: React.FC<CustomerCreatePageProps> = ({
const { const {
errors: validationErrors, errors: validationErrors,
submit: handleSubmitWithAddress submit: handleSubmitWithAddress
} = useAddressValidation<CustomerCreatePageFormData>(formData => } = useAddressValidation<CustomerCreatePageFormData, void>(formData =>
onSubmit({ onSubmit({
address: { address: {
city: formData.city, city: formData.city,

View file

@ -7,6 +7,7 @@ import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { AccountErrorFragment } from "@saleor/fragments/types/AccountErrorFragment"; import { AccountErrorFragment } from "@saleor/fragments/types/AccountErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -33,7 +34,7 @@ export interface CustomerDetailsPageProps {
errors: AccountErrorFragment[]; errors: AccountErrorFragment[];
saveButtonBar: ConfirmButtonTransitionState; saveButtonBar: ConfirmButtonTransitionState;
onBack: () => void; onBack: () => void;
onSubmit: (data: CustomerDetailsPageFormData) => void; onSubmit: (data: CustomerDetailsPageFormData) => SubmitPromise;
onViewAllOrdersClick: () => void; onViewAllOrdersClick: () => void;
onRowClick: (id: string) => void; onRowClick: (id: string) => void;
onAddressManageClick: () => void; onAddressManageClick: () => void;

View file

@ -10,7 +10,9 @@ import { FormattedMessage, useIntl } from "react-intl";
import { maybe } from "../../misc"; import { maybe } from "../../misc";
import { orderListUrl, orderUrl } from "../../orders/urls"; import { orderListUrl, orderUrl } from "../../orders/urls";
import CustomerDetailsPage from "../components/CustomerDetailsPage/CustomerDetailsPage"; import CustomerDetailsPage, {
CustomerDetailsPageFormData
} from "../components/CustomerDetailsPage/CustomerDetailsPage";
import { import {
TypedRemoveCustomerMutation, TypedRemoveCustomerMutation,
TypedUpdateCustomerMutation TypedUpdateCustomerMutation
@ -76,6 +78,25 @@ export const CustomerDetailsView: React.FC<CustomerDetailsViewProps> = ({
return <NotFoundPage onBack={handleBack} />; return <NotFoundPage onBack={handleBack} />;
} }
const handleSubmit = async (
data: CustomerDetailsPageFormData
) => {
const result = await updateCustomer({
variables: {
id,
input: {
email: data.email,
firstName: data.firstName,
isActive: data.isActive,
lastName: data.lastName,
note: data.note
}
}
});
return result.data.customerUpdate.errors;
};
return ( return (
<> <>
<WindowTitle <WindowTitle
@ -97,20 +118,7 @@ export const CustomerDetailsView: React.FC<CustomerDetailsViewProps> = ({
} }
onBack={handleBack} onBack={handleBack}
onRowClick={id => navigate(orderUrl(id))} onRowClick={id => navigate(orderUrl(id))}
onSubmit={formData => onSubmit={handleSubmit}
updateCustomer({
variables: {
id,
input: {
email: formData.email,
firstName: formData.firstName,
isActive: formData.isActive,
lastName: formData.lastName,
note: formData.note
}
}
})
}
onDelete={() => onDelete={() =>
navigate( navigate(
customerUrl(id, { customerUrl(id, {

View file

@ -25,7 +25,7 @@ import SaleSummary from "../SaleSummary";
import SaleType from "../SaleType"; import SaleType from "../SaleType";
import SaleValue from "../SaleValue"; import SaleValue from "../SaleValue";
export interface FormData { export interface SaleDetailsPageFormData {
endDate: string; endDate: string;
endTime: string; endTime: string;
hasEndDate: boolean; hasEndDate: boolean;
@ -70,7 +70,7 @@ export interface SaleDetailsPageProps
onProductUnassign: (id: string) => void; onProductUnassign: (id: string) => void;
onProductClick: (id: string) => () => void; onProductClick: (id: string) => () => void;
onRemove: () => void; onRemove: () => void;
onSubmit: (data: FormData) => void; onSubmit: (data: SaleDetailsPageFormData) => void;
onTabClick: (index: SaleDetailsPageTab) => void; onTabClick: (index: SaleDetailsPageTab) => void;
} }
@ -111,7 +111,7 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const initialForm: FormData = { const initialForm: SaleDetailsPageFormData = {
endDate: splitDateTime(maybe(() => sale.endDate, "")).date, endDate: splitDateTime(maybe(() => sale.endDate, "")).date,
endTime: splitDateTime(maybe(() => sale.endDate, "")).time, endTime: splitDateTime(maybe(() => sale.endDate, "")).time,
hasEndDate: maybe(() => !!sale.endDate), hasEndDate: maybe(() => !!sale.endDate),

View file

@ -9,10 +9,10 @@ import getDiscountErrorMessage from "@saleor/utils/errors/discounts";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { FormData } from "../SaleDetailsPage"; import { SaleDetailsPageFormData } from "../SaleDetailsPage";
export interface SaleInfoProps { export interface SaleInfoProps {
data: FormData; data: SaleDetailsPageFormData;
disabled: boolean; disabled: boolean;
errors: DiscountErrorFragment[]; errors: DiscountErrorFragment[];
onChange: (event: React.ChangeEvent<any>) => void; onChange: (event: React.ChangeEvent<any>) => void;
@ -38,7 +38,7 @@ const SaleInfo: React.FC<SaleInfoProps> = ({
disabled={disabled} disabled={disabled}
error={!!formErrors.name} error={!!formErrors.name}
helperText={getDiscountErrorMessage(formErrors.name, intl)} helperText={getDiscountErrorMessage(formErrors.name, intl)}
name={"name" as keyof FormData} name={"name" as keyof SaleDetailsPageFormData}
onChange={onChange} onChange={onChange}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Name", defaultMessage: "Name",

View file

@ -10,10 +10,10 @@ import { SaleType as SaleTypeEnum } from "@saleor/types/globalTypes";
import React from "react"; import React from "react";
import { IntlShape, useIntl } from "react-intl"; import { IntlShape, useIntl } from "react-intl";
import { FormData } from "../SaleDetailsPage"; import { SaleDetailsPageFormData } from "../SaleDetailsPage";
export interface SaleTypeProps { export interface SaleTypeProps {
data: FormData; data: SaleDetailsPageFormData;
disabled: boolean; disabled: boolean;
onChange: FormChange; onChange: FormChange;
} }
@ -70,7 +70,7 @@ const SaleType: React.FC<SaleTypeProps> = props => {
<RadioGroupField <RadioGroupField
choices={choices} choices={choices}
disabled={disabled} disabled={disabled}
name={"type" as keyof FormData} name={"type" as keyof SaleDetailsPageFormData}
value={data.type} value={data.type}
onChange={onChange} onChange={onChange}
/> />

View file

@ -10,11 +10,11 @@ import getDiscountErrorMessage from "@saleor/utils/errors/discounts";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { FormData } from "../SaleDetailsPage"; import { SaleDetailsPageFormData } from "../SaleDetailsPage";
export interface SaleValueProps { export interface SaleValueProps {
currencySymbol: string; currencySymbol: string;
data: FormData; data: SaleDetailsPageFormData;
disabled: boolean; disabled: boolean;
errors: DiscountErrorFragment[]; errors: DiscountErrorFragment[];
onChange: FormChange; onChange: FormChange;

View file

@ -11,10 +11,10 @@ import getDiscountErrorMessage from "@saleor/utils/errors/discounts";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { FormData } from "../VoucherDetailsPage"; import { VoucherDetailsPageFormData } from "../VoucherDetailsPage";
interface VoucherDatesProps { interface VoucherDatesProps {
data: FormData; data: VoucherDetailsPageFormData;
defaultCurrency: string; defaultCurrency: string;
disabled: boolean; disabled: boolean;
errors: DiscountErrorFragment[]; errors: DiscountErrorFragment[];
@ -45,7 +45,7 @@ const VoucherDates = ({
disabled={disabled} disabled={disabled}
error={!!formErrors.startDate} error={!!formErrors.startDate}
helperText={getDiscountErrorMessage(formErrors.startDate, intl)} helperText={getDiscountErrorMessage(formErrors.startDate, intl)}
name={"startDate" as keyof FormData} name={"startDate" as keyof VoucherDetailsPageFormData}
onChange={onChange} onChange={onChange}
label={intl.formatMessage(commonMessages.startDate)} label={intl.formatMessage(commonMessages.startDate)}
value={data.startDate} value={data.startDate}
@ -59,7 +59,7 @@ const VoucherDates = ({
disabled={disabled} disabled={disabled}
error={!!formErrors.startDate} error={!!formErrors.startDate}
helperText={getDiscountErrorMessage(formErrors.startDate, intl)} helperText={getDiscountErrorMessage(formErrors.startDate, intl)}
name={"startTime" as keyof FormData} name={"startTime" as keyof VoucherDetailsPageFormData}
onChange={onChange} onChange={onChange}
label={intl.formatMessage(commonMessages.startHour)} label={intl.formatMessage(commonMessages.startHour)}
value={data.startTime} value={data.startTime}
@ -76,7 +76,7 @@ const VoucherDates = ({
defaultMessage: "Set end date", defaultMessage: "Set end date",
description: "voucher end date, switch button" description: "voucher end date, switch button"
})} })}
name={"hasEndDate" as keyof FormData} name={"hasEndDate" as keyof VoucherDetailsPageFormData}
onChange={onChange} onChange={onChange}
/> />
{data.hasEndDate && ( {data.hasEndDate && (
@ -85,7 +85,7 @@ const VoucherDates = ({
disabled={disabled} disabled={disabled}
error={!!formErrors.endDate} error={!!formErrors.endDate}
helperText={getDiscountErrorMessage(formErrors.endDate, intl)} helperText={getDiscountErrorMessage(formErrors.endDate, intl)}
name={"endDate" as keyof FormData} name={"endDate" as keyof VoucherDetailsPageFormData}
onChange={onChange} onChange={onChange}
label={intl.formatMessage(commonMessages.endDate)} label={intl.formatMessage(commonMessages.endDate)}
value={data.endDate} value={data.endDate}
@ -99,7 +99,7 @@ const VoucherDates = ({
disabled={disabled} disabled={disabled}
error={!!formErrors.endDate} error={!!formErrors.endDate}
helperText={getDiscountErrorMessage(formErrors.endDate, intl)} helperText={getDiscountErrorMessage(formErrors.endDate, intl)}
name={"endTime" as keyof FormData} name={"endTime" as keyof VoucherDetailsPageFormData}
onChange={onChange} onChange={onChange}
label={intl.formatMessage(commonMessages.endHour)} label={intl.formatMessage(commonMessages.endHour)}
value={data.endTime} value={data.endTime}

View file

@ -47,7 +47,7 @@ export function voucherDetailsPageTab(tab: string): VoucherDetailsPageTab {
: VoucherDetailsPageTab.categories; : VoucherDetailsPageTab.categories;
} }
export interface FormData { export interface VoucherDetailsPageFormData {
applyOncePerCustomer: boolean; applyOncePerCustomer: boolean;
applyOncePerOrder: boolean; applyOncePerOrder: boolean;
code: string; code: string;
@ -89,7 +89,7 @@ export interface VoucherDetailsPageProps
onProductUnassign: (id: string) => void; onProductUnassign: (id: string) => void;
onProductClick: (id: string) => () => void; onProductClick: (id: string) => () => void;
onRemove: () => void; onRemove: () => void;
onSubmit: (data: FormData) => void; onSubmit: (data: VoucherDetailsPageFormData) => void;
onTabClick: (index: VoucherDetailsPageTab) => void; onTabClick: (index: VoucherDetailsPageTab) => void;
} }
@ -141,7 +141,7 @@ const VoucherDetailsPage: React.FC<VoucherDetailsPageProps> = ({
requirementsPickerInitValue = RequirementsPicker.NONE; requirementsPickerInitValue = RequirementsPicker.NONE;
} }
const initialForm: FormData = { const initialForm: VoucherDetailsPageFormData = {
applyOncePerCustomer: maybe(() => voucher.applyOncePerCustomer, false), applyOncePerCustomer: maybe(() => voucher.applyOncePerCustomer, false),
applyOncePerOrder: maybe(() => voucher.applyOncePerOrder, false), applyOncePerOrder: maybe(() => voucher.applyOncePerOrder, false),
code: maybe(() => voucher.code, ""), code: maybe(() => voucher.code, ""),

View file

@ -11,10 +11,10 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { generateCode } from "../../../misc"; import { generateCode } from "../../../misc";
import { FormData } from "../VoucherDetailsPage"; import { VoucherDetailsPageFormData } from "../VoucherDetailsPage";
interface VoucherInfoProps { interface VoucherInfoProps {
data: FormData; data: VoucherDetailsPageFormData;
errors: DiscountErrorFragment[]; errors: DiscountErrorFragment[];
disabled: boolean; disabled: boolean;
variant: "create" | "update"; variant: "create" | "update";
@ -61,7 +61,7 @@ const VoucherInfo = ({
error={!!formErrors.code} error={!!formErrors.code}
fullWidth fullWidth
helperText={getDiscountErrorMessage(formErrors.code, intl)} helperText={getDiscountErrorMessage(formErrors.code, intl)}
name={"code" as keyof FormData} name={"code" as keyof VoucherDetailsPageFormData}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Discount Code" defaultMessage: "Discount Code"
})} })}

View file

@ -9,10 +9,10 @@ import getDiscountErrorMessage from "@saleor/utils/errors/discounts";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { FormData } from "../VoucherDetailsPage"; import { VoucherDetailsPageFormData } from "../VoucherDetailsPage";
interface VoucherLimitsProps { interface VoucherLimitsProps {
data: FormData; data: VoucherDetailsPageFormData;
defaultCurrency: string; defaultCurrency: string;
disabled: boolean; disabled: boolean;
errors: DiscountErrorFragment[]; errors: DiscountErrorFragment[];
@ -44,7 +44,7 @@ const VoucherLimits = ({
defaultMessage: defaultMessage:
"Limit number of times this discount can be used in total" "Limit number of times this discount can be used in total"
})} })}
name={"hasUsageLimit" as keyof FormData} name={"hasUsageLimit" as keyof VoucherDetailsPageFormData}
onChange={onChange} onChange={onChange}
/> />
{data.hasUsageLimit && ( {data.hasUsageLimit && (
@ -56,7 +56,7 @@ const VoucherLimits = ({
defaultMessage: "Limit of Uses", defaultMessage: "Limit of Uses",
description: "voucher" description: "voucher"
})} })}
name={"usageLimit" as keyof FormData} name={"usageLimit" as keyof VoucherDetailsPageFormData}
value={data.usageLimit} value={data.usageLimit}
onChange={onChange} onChange={onChange}
type="number" type="number"
@ -72,7 +72,7 @@ const VoucherLimits = ({
defaultMessage: "Limit to one use per customer", defaultMessage: "Limit to one use per customer",
description: "limit voucher" description: "limit voucher"
})} })}
name={"applyOncePerCustomer" as keyof FormData} name={"applyOncePerCustomer" as keyof VoucherDetailsPageFormData}
onChange={onChange} onChange={onChange}
/> />
</CardContent> </CardContent>

View file

@ -11,10 +11,10 @@ import getDiscountErrorMessage from "@saleor/utils/errors/discounts";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { FormData } from "../VoucherDetailsPage"; import { VoucherDetailsPageFormData } from "../VoucherDetailsPage";
interface VoucherRequirementsProps { interface VoucherRequirementsProps {
data: FormData; data: VoucherDetailsPageFormData;
defaultCurrency: string; defaultCurrency: string;
disabled: boolean; disabled: boolean;
errors: DiscountErrorFragment[]; errors: DiscountErrorFragment[];
@ -73,7 +73,7 @@ const VoucherRequirements = ({
<RadioGroupField <RadioGroupField
choices={requirementsPickerChoices} choices={requirementsPickerChoices}
disabled={disabled} disabled={disabled}
name={"requirementsPicker" as keyof FormData} name={"requirementsPicker" as keyof VoucherDetailsPageFormData}
value={data.requirementsPicker} value={data.requirementsPicker}
onChange={onChange} onChange={onChange}
/> />
@ -86,7 +86,7 @@ const VoucherRequirements = ({
error={!!formErrors.minSpent} error={!!formErrors.minSpent}
helperText={getDiscountErrorMessage(formErrors.minSpent, intl)} helperText={getDiscountErrorMessage(formErrors.minSpent, intl)}
label={minimalOrderValueText} label={minimalOrderValueText}
name={"minSpent" as keyof FormData} name={"minSpent" as keyof VoucherDetailsPageFormData}
value={data.minSpent} value={data.minSpent}
onChange={onChange} onChange={onChange}
fullWidth fullWidth
@ -100,7 +100,9 @@ const VoucherRequirements = ({
intl intl
)} )}
label={minimalQuantityText} label={minimalQuantityText}
name={"minCheckoutItemsQuantity" as keyof FormData} name={
"minCheckoutItemsQuantity" as keyof VoucherDetailsPageFormData
}
value={data.minCheckoutItemsQuantity} value={data.minCheckoutItemsQuantity}
onChange={onChange} onChange={onChange}
fullWidth fullWidth

View file

@ -10,10 +10,10 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { DiscountValueTypeEnum } from "../../../types/globalTypes"; import { DiscountValueTypeEnum } from "../../../types/globalTypes";
import { FormData } from "../VoucherDetailsPage"; import { VoucherDetailsPageFormData } from "../VoucherDetailsPage";
interface VoucherTypesProps { interface VoucherTypesProps {
data: FormData; data: VoucherDetailsPageFormData;
errors: DiscountErrorFragment[]; errors: DiscountErrorFragment[];
disabled: boolean; disabled: boolean;
onChange: (event: React.ChangeEvent<any>) => void; onChange: (event: React.ChangeEvent<any>) => void;
@ -68,7 +68,7 @@ const VoucherTypes = ({
disabled={disabled} disabled={disabled}
error={!!formErrors.discountType} error={!!formErrors.discountType}
hint={getDiscountErrorMessage(formErrors.discountType, intl)} hint={getDiscountErrorMessage(formErrors.discountType, intl)}
name={"discountType" as keyof FormData} name={"discountType" as keyof VoucherDetailsPageFormData}
value={data.discountType} value={data.discountType}
onChange={onChange} onChange={onChange}
/> />

View file

@ -16,10 +16,10 @@ import { FormattedMessage, useIntl } from "react-intl";
import { DiscountValueTypeEnum } from "../../../types/globalTypes"; import { DiscountValueTypeEnum } from "../../../types/globalTypes";
import { translateVoucherTypes } from "../../translations"; import { translateVoucherTypes } from "../../translations";
import { FormData } from "../VoucherDetailsPage"; import { VoucherDetailsPageFormData } from "../VoucherDetailsPage";
interface VoucherValueProps { interface VoucherValueProps {
data: FormData; data: VoucherDetailsPageFormData;
defaultCurrency: string; defaultCurrency: string;
errors: DiscountErrorFragment[]; errors: DiscountErrorFragment[];
disabled: boolean; disabled: boolean;
@ -74,11 +74,11 @@ const VoucherValue: React.FC<VoucherValueProps> = props => {
data.discountType === DiscountValueTypeEnum.FIXED data.discountType === DiscountValueTypeEnum.FIXED
? defaultCurrency ? defaultCurrency
: "%", : "%",
name: "discountType" as keyof FormData, name: "discountType" as keyof VoucherDetailsPageFormData,
values: null values: null
}} }}
helperText={getDiscountErrorMessage(formErrors.discountValue, intl)} helperText={getDiscountErrorMessage(formErrors.discountValue, intl)}
name={"value" as keyof FormData} name={"value" as keyof VoucherDetailsPageFormData}
onChange={onChange} onChange={onChange}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Discount Value" defaultMessage: "Discount Value"
@ -102,7 +102,7 @@ const VoucherValue: React.FC<VoucherValueProps> = props => {
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Voucher Specific Information" defaultMessage: "Voucher Specific Information"
})} })}
name={"type" as keyof FormData} name={"type" as keyof VoucherDetailsPageFormData}
value={data.type} value={data.type}
onChange={onChange} onChange={onChange}
/> />
@ -111,7 +111,7 @@ const VoucherValue: React.FC<VoucherValueProps> = props => {
<Hr className={classes.hr} /> <Hr className={classes.hr} />
<FormSpacer /> <FormSpacer />
<ControlledCheckbox <ControlledCheckbox
name={"applyOncePerOrder" as keyof FormData} name={"applyOncePerOrder" as keyof VoucherDetailsPageFormData}
label={ label={
<> <>
<FormattedMessage <FormattedMessage

View file

@ -27,6 +27,7 @@ import { decimal, joinDateTime, maybe } from "../../misc";
import { productUrl } from "../../products/urls"; import { productUrl } from "../../products/urls";
import { DiscountValueTypeEnum, SaleType } from "../../types/globalTypes"; import { DiscountValueTypeEnum, SaleType } from "../../types/globalTypes";
import SaleDetailsPage, { import SaleDetailsPage, {
SaleDetailsPageFormData,
SaleDetailsPageTab SaleDetailsPageTab
} from "../components/SaleDetailsPage"; } from "../components/SaleDetailsPage";
import { import {
@ -68,18 +69,21 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
); );
const intl = useIntl(); const intl = useIntl();
const { const {
loadMore: loadMoreCategories,
search: searchCategories, search: searchCategories,
result: searchCategoriesOpts result: searchCategoriesOpts
} = useCategorySearch({ } = useCategorySearch({
variables: DEFAULT_INITIAL_SEARCH_DATA variables: DEFAULT_INITIAL_SEARCH_DATA
}); });
const { const {
loadMore: loadMoreCollections,
search: searchCollections, search: searchCollections,
result: searchCollectionsOpts result: searchCollectionsOpts
} = useCollectionSearch({ } = useCollectionSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA variables: DEFAULT_INITIAL_SEARCH_DATA
}); });
const { const {
loadMore: loadMoreProducts,
search: searchProducts, search: searchProducts,
result: searchProductsOpts result: searchProductsOpts
} = useProductSearch({ } = useProductSearch({
@ -192,6 +196,30 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
} }
}); });
const handleSubmit = async (
data: SaleDetailsPageFormData
) => {
const result = await saleUpdate({
variables: {
id,
input: {
endDate: data.hasEndDate
? joinDateTime(data.endDate, data.endTime)
: null,
name: data.name,
startDate: joinDateTime(
data.startDate,
data.startTime
),
type: discountValueTypeEnum(data.type),
value: decimal(data.value)
}
}
});
return result.data.saleUpdate.errors;
};
const { const {
loadNextPage, loadNextPage,
loadPreviousPage, loadPreviousPage,
@ -244,30 +272,7 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
activeTab={params.activeTab} activeTab={params.activeTab}
onBack={() => navigate(saleListUrl())} onBack={() => navigate(saleListUrl())}
onTabClick={changeTab} onTabClick={changeTab}
onSubmit={formData => onSubmit={handleSubmit}
saleUpdate({
variables: {
id,
input: {
endDate: formData.hasEndDate
? joinDateTime(
formData.endDate,
formData.endTime
)
: null,
name: formData.name,
startDate: joinDateTime(
formData.startDate,
formData.startTime
),
type: discountValueTypeEnum(
formData.type
),
value: decimal(formData.value)
}
}
})
}
onRemove={() => openModal("remove")} onRemove={() => openModal("remove")}
saveButtonBarState={saleUpdateOpts.status} saveButtonBarState={saleUpdateOpts.status}
categoryListToolbar={ categoryListToolbar={
@ -325,8 +330,13 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
/> />
<AssignProductDialog <AssignProductDialog
confirmButtonState={saleCataloguesAddOpts.status} confirmButtonState={saleCataloguesAddOpts.status}
hasMore={
searchProductsOpts.data?.search.pageInfo
.hasNextPage
}
open={params.action === "assign-product"} open={params.action === "assign-product"}
onFetch={searchProducts} onFetch={searchProducts}
onFetchMore={loadMoreProducts}
loading={searchProductsOpts.loading} loading={searchProductsOpts.loading}
onClose={closeModal} onClose={closeModal}
onSubmit={products => onSubmit={products =>
@ -359,8 +369,13 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
) )
)} )}
confirmButtonState={saleCataloguesAddOpts.status} confirmButtonState={saleCataloguesAddOpts.status}
hasMore={
searchCategoriesOpts.data?.search.pageInfo
.hasNextPage
}
open={params.action === "assign-category"} open={params.action === "assign-category"}
onFetch={searchCategories} onFetch={searchCategories}
onFetchMore={loadMoreCategories}
loading={searchCategoriesOpts.loading} loading={searchCategoriesOpts.loading}
onClose={closeModal} onClose={closeModal}
onSubmit={categories => onSubmit={categories =>
@ -369,9 +384,7 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
...paginationState, ...paginationState,
id, id,
input: { input: {
categories: categories.map( categories
product => product.id
)
} }
} }
}) })
@ -386,8 +399,13 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
) )
)} )}
confirmButtonState={saleCataloguesAddOpts.status} confirmButtonState={saleCataloguesAddOpts.status}
hasMore={
searchCollectionsOpts.data?.search.pageInfo
.hasNextPage
}
open={params.action === "assign-collection"} open={params.action === "assign-collection"}
onFetch={searchCollections} onFetch={searchCollections}
onFetchMore={loadMoreCollections}
loading={searchCollectionsOpts.loading} loading={searchCollectionsOpts.loading}
onClose={closeModal} onClose={closeModal}
onSubmit={collections => onSubmit={collections =>
@ -396,9 +414,7 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
...paginationState, ...paginationState,
id, id,
input: { input: {
collections: collections.map( collections
product => product.id
)
} }
} }
}) })

View file

@ -31,6 +31,7 @@ import {
} from "../../types/globalTypes"; } from "../../types/globalTypes";
import DiscountCountrySelectDialog from "../components/DiscountCountrySelectDialog"; import DiscountCountrySelectDialog from "../components/DiscountCountrySelectDialog";
import VoucherDetailsPage, { import VoucherDetailsPage, {
VoucherDetailsPageFormData,
VoucherDetailsPageTab VoucherDetailsPageTab
} from "../components/VoucherDetailsPage"; } from "../components/VoucherDetailsPage";
import { import {
@ -70,18 +71,21 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
); );
const intl = useIntl(); const intl = useIntl();
const { const {
loadMore: loadMoreCategories,
search: searchCategories, search: searchCategories,
result: searchCategoriesOpts result: searchCategoriesOpts
} = useCategorySearch({ } = useCategorySearch({
variables: DEFAULT_INITIAL_SEARCH_DATA variables: DEFAULT_INITIAL_SEARCH_DATA
}); });
const { const {
loadMore: loadMoreCollections,
search: searchCollections, search: searchCollections,
result: searchCollectionsOpts result: searchCollectionsOpts
} = useCollectionSearch({ } = useCollectionSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA variables: DEFAULT_INITIAL_SEARCH_DATA
}); });
const { const {
loadMore: loadMoreProducts,
search: searchProducts, search: searchProducts,
result: searchProductsOpts result: searchProductsOpts
} = useProductSearch({ } = useProductSearch({
@ -195,6 +199,54 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
} }
}); });
const handleSubmit = async (
data: VoucherDetailsPageFormData
) => {
const result = await voucherUpdate({
variables: {
id,
input: {
applyOncePerCustomer: data.applyOncePerCustomer,
applyOncePerOrder: data.applyOncePerOrder,
discountValue:
data.discountType.toString() === "SHIPPING"
? 100
: decimal(data.value),
discountValueType:
data.discountType.toString() === "SHIPPING"
? DiscountValueTypeEnum.PERCENTAGE
: data.discountType,
endDate: data.hasEndDate
? joinDateTime(data.endDate, data.endTime)
: null,
minAmountSpent:
data.requirementsPicker !==
RequirementsPicker.ORDER
? 0
: parseFloat(data.minSpent),
minCheckoutItemsQuantity:
data.requirementsPicker !==
RequirementsPicker.ITEM
? 0
: parseFloat(data.minCheckoutItemsQuantity),
startDate: joinDateTime(
data.startDate,
data.startTime
),
type:
data.discountType.toString() === "SHIPPING"
? VoucherTypeEnum.SHIPPING
: data.type,
usageLimit: data.hasUsageLimit
? parseInt(data.usageLimit, 10)
: null
}
}
});
return result.data.voucherUpdate.errors;
};
const { const {
loadNextPage, loadNextPage,
loadPreviousPage, loadPreviousPage,
@ -291,59 +343,7 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
activeTab={params.activeTab} activeTab={params.activeTab}
onBack={() => navigate(voucherListUrl())} onBack={() => navigate(voucherListUrl())}
onTabClick={changeTab} onTabClick={changeTab}
onSubmit={formData => onSubmit={handleSubmit}
voucherUpdate({
variables: {
id,
input: {
applyOncePerCustomer:
formData.applyOncePerCustomer,
applyOncePerOrder:
formData.applyOncePerOrder,
discountValue:
formData.discountType.toString() ===
"SHIPPING"
? 100
: decimal(formData.value),
discountValueType:
formData.discountType.toString() ===
"SHIPPING"
? DiscountValueTypeEnum.PERCENTAGE
: formData.discountType,
endDate: formData.hasEndDate
? joinDateTime(
formData.endDate,
formData.endTime
)
: null,
minAmountSpent:
formData.requirementsPicker !==
RequirementsPicker.ORDER
? 0
: parseFloat(formData.minSpent),
minCheckoutItemsQuantity:
formData.requirementsPicker !==
RequirementsPicker.ITEM
? 0
: parseFloat(
formData.minCheckoutItemsQuantity
),
startDate: joinDateTime(
formData.startDate,
formData.startTime
),
type:
formData.discountType.toString() ===
"SHIPPING"
? VoucherTypeEnum.SHIPPING
: formData.type,
usageLimit: formData.hasUsageLimit
? parseInt(formData.usageLimit, 10)
: null
}
}
})
}
onRemove={() => openModal("remove")} onRemove={() => openModal("remove")}
saveButtonBarState={voucherUpdateOpts.status} saveButtonBarState={voucherUpdateOpts.status}
categoryListToolbar={ categoryListToolbar={
@ -410,8 +410,13 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
confirmButtonState={ confirmButtonState={
voucherCataloguesAddOpts.status voucherCataloguesAddOpts.status
} }
hasMore={
searchCategoriesOpts.data?.search.pageInfo
.hasNextPage
}
open={params.action === "assign-category"} open={params.action === "assign-category"}
onFetch={searchCategories} onFetch={searchCategories}
onFetchMore={loadMoreCategories}
loading={searchCategoriesOpts.loading} loading={searchCategoriesOpts.loading}
onClose={closeModal} onClose={closeModal}
onSubmit={categories => onSubmit={categories =>
@ -420,9 +425,7 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
...paginationState, ...paginationState,
id, id,
input: { input: {
categories: categories.map( categories
product => product.id
)
} }
} }
}) })
@ -439,8 +442,13 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
confirmButtonState={ confirmButtonState={
voucherCataloguesAddOpts.status voucherCataloguesAddOpts.status
} }
hasMore={
searchCollectionsOpts.data?.search.pageInfo
.hasNextPage
}
open={params.action === "assign-collection"} open={params.action === "assign-collection"}
onFetch={searchCollections} onFetch={searchCollections}
onFetchMore={loadMoreCollections}
loading={searchCollectionsOpts.loading} loading={searchCollectionsOpts.loading}
onClose={closeModal} onClose={closeModal}
onSubmit={collections => onSubmit={collections =>
@ -449,9 +457,7 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
...paginationState, ...paginationState,
id, id,
input: { input: {
collections: collections.map( collections
product => product.id
)
} }
} }
}) })
@ -484,8 +490,13 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
confirmButtonState={ confirmButtonState={
voucherCataloguesAddOpts.status voucherCataloguesAddOpts.status
} }
hasMore={
searchProductsOpts.data?.search.pageInfo
.hasNextPage
}
open={params.action === "assign-product"} open={params.action === "assign-product"}
onFetch={searchProducts} onFetch={searchProducts}
onFetchMore={loadMoreProducts}
loading={searchProductsOpts.loading} loading={searchProductsOpts.loading}
onClose={closeModal} onClose={closeModal}
onSubmit={products => onSubmit={products =>

View file

@ -5,14 +5,14 @@ import { AccountErrorCode, AddressInput } from "@saleor/types/globalTypes";
import { add, remove } from "@saleor/utils/lists"; import { add, remove } from "@saleor/utils/lists";
import { useState } from "react"; import { useState } from "react";
interface UseAddressValidation<T> { interface UseAddressValidation<TInput, TOutput> {
errors: AccountErrorFragment[]; errors: AccountErrorFragment[];
submit: (data: T & AddressTypeInput) => void; submit: (data: TInput & AddressTypeInput) => TOutput;
} }
function useAddressValidation<T>( function useAddressValidation<TInput, TOutput>(
onSubmit: (address: T & AddressInput) => void onSubmit: (address: TInput & AddressInput) => TOutput
): UseAddressValidation<T> { ): UseAddressValidation<TInput, TOutput> {
const [validationErrors, setValidationErrors] = useState< const [validationErrors, setValidationErrors] = useState<
AccountErrorFragment[] AccountErrorFragment[]
>([]); >([]);
@ -25,7 +25,7 @@ function useAddressValidation<T>(
return { return {
errors: validationErrors, errors: validationErrors,
submit: (data: T & AddressTypeInput) => { submit: (data: TInput & AddressTypeInput) => {
try { try {
setValidationErrors( setValidationErrors(
remove( remove(
@ -34,7 +34,7 @@ function useAddressValidation<T>(
(a, b) => a.field === b.field (a, b) => a.field === b.field
) )
); );
onSubmit(transformFormToAddress(data)); return onSubmit(transformFormToAddress(data));
} catch { } catch {
setValidationErrors(add(countryRequiredError, validationErrors)); setValidationErrors(add(countryRequiredError, validationErrors));
} }

View file

@ -10,6 +10,7 @@ export interface ChangeEvent<TData = any> {
value: TData; value: TData;
}; };
} }
export type SubmitPromise = Promise<any[]>;
export type FormChange = (event: ChangeEvent, cb?: () => void) => void; export type FormChange = (event: ChangeEvent, cb?: () => void) => void;
@ -51,7 +52,7 @@ function handleRefresh<T extends FormData>(
function useForm<T extends FormData>( function useForm<T extends FormData>(
initial: T, initial: T,
onSubmit: (data: T) => void onSubmit?: (data: T) => SubmitPromise | void
): UseFormResult<T> { ): UseFormResult<T> {
const [hasChanged, setChanged] = useState(false); const [hasChanged, setChanged] = useState(false);
const [data, setData] = useStateFromProps(initial, { const [data, setData] = useStateFromProps(initial, {
@ -106,8 +107,16 @@ function useForm<T extends FormData>(
})); }));
} }
function submit() { async function submit() {
return onSubmit(data); if (typeof onSubmit === "function") {
const result = onSubmit(data);
if (result) {
const errors = await result;
if (errors.length === 0) {
setChanged(false);
}
}
}
} }
function triggerChange() { function triggerChange() {

View file

@ -7,6 +7,7 @@ import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { MenuErrorFragment } from "@saleor/fragments/types/MenuErrorFragment"; import { MenuErrorFragment } from "@saleor/fragments/types/MenuErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -36,7 +37,7 @@ export interface MenuDetailsPageProps {
onItemAdd: () => void; onItemAdd: () => void;
onItemClick: (id: string, type: MenuItemType) => void; onItemClick: (id: string, type: MenuItemType) => void;
onItemEdit: (id: string) => void; onItemEdit: (id: string) => void;
onSubmit: (data: MenuDetailsSubmitData) => Promise<boolean>; onSubmit: (data: MenuDetailsSubmitData) => SubmitPromise;
} }
const MenuDetailsPage: React.FC<MenuDetailsPageProps> = ({ const MenuDetailsPage: React.FC<MenuDetailsPageProps> = ({
@ -62,14 +63,16 @@ const MenuDetailsPage: React.FC<MenuDetailsPageProps> = ({
); );
const handleSubmit = async (data: MenuDetailsFormData) => { const handleSubmit = async (data: MenuDetailsFormData) => {
if ( const result = await onSubmit({
await onSubmit({ name: data.name,
name: data.name, operations: treeOperations
operations: treeOperations });
})
) { if (result) {
setTreeOperations([]); setTreeOperations([]);
} }
return result;
}; };
const handleChange = (operation: TreeOperation) => { const handleChange = (operation: TreeOperation) => {

View file

@ -141,28 +141,20 @@ const MenuDetails: React.FC<MenuDetailsProps> = ({ id, params }) => {
// that it should clean operation stack if mutations // that it should clean operation stack if mutations
// were successful // were successful
const handleSubmit = async (data: MenuDetailsSubmitData) => { const handleSubmit = async (data: MenuDetailsSubmitData) => {
try { const result = await menuUpdate({
const result = await menuUpdate({ variables: {
variables: { id,
id, moves: getMoves(data),
moves: getMoves(data), name: data.name,
name: data.name, removeIds: getRemoveIds(data)
removeIds: getRemoveIds(data)
}
});
if (result) {
if (
result.data.menuItemBulkDelete.errors.length > 0 ||
result.data.menuItemMove.errors.length > 0 ||
result.data.menuUpdate.errors.length > 0
) {
return false;
}
} }
return true; });
} catch {
return false; return [
} ...result.data.menuItemBulkDelete.errors,
...result.data.menuItemMove.errors,
...result.data.menuUpdate.errors
];
}; };
return ( return (

View file

@ -12,6 +12,7 @@ import Metadata, { MetadataFormData } from "@saleor/components/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { UserPermissionProps } from "@saleor/types"; import { UserPermissionProps } from "@saleor/types";
import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapMetadataItemToInput } from "@saleor/utils/maps";
@ -74,7 +75,7 @@ export interface OrderDetailsPageProps extends UserPermissionProps {
onInvoiceClick(invoiceId: string); onInvoiceClick(invoiceId: string);
onInvoiceGenerate(); onInvoiceGenerate();
onInvoiceSend(invoiceId: string); onInvoiceSend(invoiceId: string);
onSubmit(data: MetadataFormData); onSubmit(data: MetadataFormData): SubmitPromise;
} }
const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => { const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
@ -123,7 +124,7 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
? data.privateMetadata ? data.privateMetadata
: undefined; : undefined;
onSubmit({ return onSubmit({
metadata, metadata,
privateMetadata privateMetadata
}); });

View file

@ -122,6 +122,8 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
text: intl.formatMessage(commonMessages.savedChanges) text: intl.formatMessage(commonMessages.savedChanges)
}); });
} }
return result;
}; };
return ( return (

View file

@ -11,6 +11,7 @@ import SeoForm from "@saleor/components/SeoForm";
import VisibilityCard from "@saleor/components/VisibilityCard"; import VisibilityCard from "@saleor/components/VisibilityCard";
import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment";
import useDateLocalize from "@saleor/hooks/useDateLocalize"; import useDateLocalize from "@saleor/hooks/useDateLocalize";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
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";
@ -27,7 +28,7 @@ import { maybe } from "../../../misc";
import { PageDetails_page } from "../../types/PageDetails"; import { PageDetails_page } from "../../types/PageDetails";
import PageInfo from "../PageInfo"; import PageInfo from "../PageInfo";
export interface FormData extends MetadataFormData { export interface PageDetailsPageFormData extends MetadataFormData {
content: RawDraftContentState; content: RawDraftContentState;
isPublished: boolean; isPublished: boolean;
publicationDate: string; publicationDate: string;
@ -45,7 +46,7 @@ export interface PageDetailsPageProps {
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void; onBack: () => void;
onRemove: () => void; onRemove: () => void;
onSubmit: (data: FormData) => void; onSubmit: (data: PageDetailsPageFormData) => SubmitPromise;
} }
const PageDetailsPage: React.FC<PageDetailsPageProps> = ({ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
@ -67,7 +68,7 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
const pageExists = page !== null; const pageExists = page !== null;
const initialForm: FormData = { const initialForm: PageDetailsPageFormData = {
content: maybe( content: maybe(
() => JSON.parse(page.contentJson), () => JSON.parse(page.contentJson),
convertToRaw(ContentState.createFromText("")) convertToRaw(ContentState.createFromText(""))
@ -84,13 +85,13 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
title: page?.title || "" title: page?.title || ""
}; };
const handleSubmit = (data: FormData) => { const handleSubmit = (data: PageDetailsPageFormData) => {
const metadata = isMetadataModified ? data.metadata : undefined; const metadata = isMetadataModified ? data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified const privateMetadata = isPrivateMetadataModified
? data.privateMetadata ? data.privateMetadata
: undefined; : undefined;
onSubmit({ return onSubmit({
...data, ...data,
isPublished: data.isPublished || !!data.publicationDate, isPublished: data.isPublished || !!data.publicationDate,
metadata, metadata,

View file

@ -14,10 +14,10 @@ import { useIntl } from "react-intl";
import { maybe } from "../../../misc"; import { maybe } from "../../../misc";
import { PageDetails_page } from "../../types/PageDetails"; import { PageDetails_page } from "../../types/PageDetails";
import { FormData } from "../PageDetailsPage"; import { PageDetailsPageFormData } from "../PageDetailsPage";
export interface PageInfoProps { export interface PageInfoProps {
data: FormData; data: PageDetailsPageFormData;
disabled: boolean; disabled: boolean;
errors: PageErrorFragment[]; errors: PageErrorFragment[];
page: PageDetails_page; page: PageDetails_page;
@ -56,7 +56,7 @@ const PageInfo: React.FC<PageInfoProps> = props => {
defaultMessage: "Title", defaultMessage: "Title",
description: "page title" description: "page title"
})} })}
name={"title" as keyof FormData} name={"title" as keyof PageDetailsPageFormData}
value={data.title} value={data.title}
onChange={onChange} onChange={onChange}
/> />
@ -70,7 +70,7 @@ const PageInfo: React.FC<PageInfoProps> = props => {
defaultMessage: "Content", defaultMessage: "Content",
description: "page content" description: "page content"
})} })}
name={"content" as keyof FormData} name={"content" as keyof PageDetailsPageFormData}
onChange={onChange} onChange={onChange}
/> />
</CardContent> </CardContent>

View file

@ -9,7 +9,9 @@ import {
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import PageDetailsPage, { FormData } from "../components/PageDetailsPage"; import PageDetailsPage, {
PageDetailsPageFormData
} from "../components/PageDetailsPage";
import { TypedPageCreate } from "../mutations"; import { TypedPageCreate } from "../mutations";
import { PageCreate as PageCreateData } from "../types/PageCreate"; import { PageCreate as PageCreateData } from "../types/PageCreate";
import { pageListUrl, pageUrl } from "../urls"; import { pageListUrl, pageUrl } from "../urls";
@ -40,7 +42,7 @@ export const PageCreate: React.FC<PageCreateProps> = () => {
return ( return (
<TypedPageCreate onCompleted={handlePageCreate}> <TypedPageCreate onCompleted={handlePageCreate}>
{(pageCreate, pageCreateOpts) => { {(pageCreate, pageCreateOpts) => {
const handleCreate = async (formData: FormData) => { const handleCreate = async (formData: PageDetailsPageFormData) => {
const result = await pageCreate({ const result = await pageCreate({
variables: { variables: {
input: { input: {

View file

@ -14,7 +14,9 @@ import { FormattedMessage, useIntl } from "react-intl";
import { getStringOrPlaceholder, maybe } from "../../misc"; import { getStringOrPlaceholder, maybe } from "../../misc";
import { PageInput } from "../../types/globalTypes"; import { PageInput } from "../../types/globalTypes";
import PageDetailsPage, { FormData } from "../components/PageDetailsPage"; import PageDetailsPage, {
PageDetailsPageFormData
} from "../components/PageDetailsPage";
import { TypedPageRemove, TypedPageUpdate } from "../mutations"; import { TypedPageRemove, TypedPageUpdate } from "../mutations";
import { TypedPageDetailsQuery } from "../queries"; import { TypedPageDetailsQuery } from "../queries";
import { PageRemove } from "../types/PageRemove"; import { PageRemove } from "../types/PageRemove";
@ -25,7 +27,7 @@ export interface PageDetailsProps {
params: PageUrlQueryParams; params: PageUrlQueryParams;
} }
const createPageInput = (data: FormData): PageInput => ({ const createPageInput = (data: PageDetailsPageFormData): PageInput => ({
contentJson: JSON.stringify(data.content), contentJson: JSON.stringify(data.content),
isPublished: data.isPublished, isPublished: data.isPublished,
publicationDate: data.publicationDate, publicationDate: data.publicationDate,
@ -61,7 +63,7 @@ export const PageDetails: React.FC<PageDetailsProps> = ({ id, params }) => {
{(pageUpdate, pageUpdateOpts) => ( {(pageUpdate, pageUpdateOpts) => (
<TypedPageDetailsQuery variables={{ id }}> <TypedPageDetailsQuery variables={{ id }}>
{pageDetails => { {pageDetails => {
const handleUpdate = async (data: FormData) => { const handleUpdate = async (data: PageDetailsPageFormData) => {
const result = await pageUpdate({ const result = await pageUpdate({
variables: { variables: {
id, id,

View file

@ -9,6 +9,7 @@ import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo"; import { ShopInfo_shop_permissions } from "@saleor/components/Shop/types/ShopInfo";
import { PermissionGroupErrorFragment } from "@saleor/fragments/types/PermissionGroupErrorFragment"; import { PermissionGroupErrorFragment } from "@saleor/fragments/types/PermissionGroupErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { MembersListUrlSortField } from "@saleor/permissionGroups/urls"; import { MembersListUrlSortField } from "@saleor/permissionGroups/urls";
import { import {
@ -56,7 +57,7 @@ export interface PermissionGroupDetailsPageProps
onAssign: () => void; onAssign: () => void;
onBack: () => void; onBack: () => void;
onUnassign: (ids: string[]) => void; onUnassign: (ids: string[]) => void;
onSubmit(data: PermissionGroupDetailsPageFormData); onSubmit: (data: PermissionGroupDetailsPageFormData) => SubmitPromise;
} }
const PermissionGroupDetailsPage: React.FC<PermissionGroupDetailsPageProps> = ({ const PermissionGroupDetailsPage: React.FC<PermissionGroupDetailsPageProps> = ({

View file

@ -21,7 +21,9 @@ import React, { useState } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import AssignMembersDialog from "../../components/AssignMembersDialog"; import AssignMembersDialog from "../../components/AssignMembersDialog";
import PermissionGroupDetailsPage from "../../components/PermissionGroupDetailsPage"; import PermissionGroupDetailsPage, {
PermissionGroupDetailsPageFormData
} from "../../components/PermissionGroupDetailsPage";
import UnassignMembersDialog from "../../components/UnassignMembersDialog"; import UnassignMembersDialog from "../../components/UnassignMembersDialog";
import { usePermissionGroupUpdate } from "../../mutations"; import { usePermissionGroupUpdate } from "../../mutations";
import { usePermissionGroupDetailsQuery } from "../../queries"; import { usePermissionGroupDetailsQuery } from "../../queries";
@ -131,6 +133,21 @@ export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({
); );
const disabled = loading || !isGroupEditable || permissionsExceeded; const disabled = loading || !isGroupEditable || permissionsExceeded;
const handleSubmit = async (formData: PermissionGroupDetailsPageFormData) => {
const result = await permissionGroupUpdate({
variables: {
id,
input: {
name: formData.name,
...permissionsDiff(data?.permissionGroup, formData),
...usersDiff(data?.permissionGroup, formData)
}
}
});
return result.data.permissionGroupUpdate.errors;
};
return ( return (
<> <>
<PermissionGroupDetailsPage <PermissionGroupDetailsPage
@ -144,18 +161,7 @@ export const PermissionGroupDetails: React.FC<PermissionGroupDetailsProps> = ({
errors={ errors={
permissionGroupUpdateResult?.data?.permissionGroupUpdate.errors || [] permissionGroupUpdateResult?.data?.permissionGroupUpdate.errors || []
} }
onSubmit={formData => onSubmit={handleSubmit}
permissionGroupUpdate({
variables: {
id,
input: {
name: formData.name,
...permissionsDiff(data?.permissionGroup, formData),
...usersDiff(data?.permissionGroup, formData)
}
}
})
}
permissions={permissions} permissions={permissions}
saveButtonBarState={permissionGroupUpdateResult.status} saveButtonBarState={permissionGroupUpdateResult.status}
disabled={disabled} disabled={disabled}

View file

@ -13,10 +13,10 @@ import getPluginErrorMessage from "@saleor/utils/errors/plugins";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { FormData } from "../PluginsDetailsPage"; import { PluginDetailsPageFormData } from "../PluginsDetailsPage";
interface PluginInfoProps { interface PluginInfoProps {
data: FormData; data: PluginDetailsPageFormData;
description: string; description: string;
errors: PluginErrorFragment[]; errors: PluginErrorFragment[];
name: string; name: string;
@ -83,7 +83,7 @@ const PluginInfo: React.FC<PluginInfoProps> = ({
})} })}
</Typography> </Typography>
<ControlledCheckbox <ControlledCheckbox
name={"active" as keyof FormData} name={"active" as keyof PluginDetailsPageFormData}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Set plugin as Active" defaultMessage: "Set plugin as Active"
})} })}

View file

@ -14,10 +14,10 @@ import { getFieldError } from "@saleor/utils/errors";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { FormData } from "../PluginsDetailsPage"; import { PluginDetailsPageFormData } from "../PluginsDetailsPage";
interface PluginSettingsProps { interface PluginSettingsProps {
data: FormData; data: PluginDetailsPageFormData;
errors: UserError[]; errors: UserError[];
disabled: boolean; disabled: boolean;
onChange: (event: React.ChangeEvent<any>) => void; onChange: (event: React.ChangeEvent<any>) => void;

View file

@ -23,7 +23,7 @@ import PluginAuthorization from "../PluginAuthorization";
import PluginInfo from "../PluginInfo"; import PluginInfo from "../PluginInfo";
import PluginSettings from "../PluginSettings"; import PluginSettings from "../PluginSettings";
export interface FormData { export interface PluginDetailsPageFormData {
active: boolean; active: boolean;
configuration: ConfigurationItemInput[]; configuration: ConfigurationItemInput[];
} }
@ -36,7 +36,7 @@ export interface PluginsDetailsPageProps {
onBack: () => void; onBack: () => void;
onClear: (field: string) => void; onClear: (field: string) => void;
onEdit: (field: string) => void; onEdit: (field: string) => void;
onSubmit: (data: FormData) => void; onSubmit: (data: PluginDetailsPageFormData) => void;
} }
const useStyles = makeStyles( const useStyles = makeStyles(
@ -64,7 +64,7 @@ const PluginsDetailsPage: React.FC<PluginsDetailsPageProps> = props => {
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
const initialForm: FormData = { const initialForm: PluginDetailsPageFormData = {
active: plugin?.active || false, active: plugin?.active || false,
configuration: plugin?.configuration configuration: plugin?.configuration
?.filter(field => !isSecretField(plugin?.configuration || [], field.name)) ?.filter(field => !isSecretField(plugin?.configuration || [], field.name))

View file

@ -9,7 +9,9 @@ import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandl
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import PluginsDetailsPage from "../components/PluginsDetailsPage"; import PluginsDetailsPage, {
PluginDetailsPageFormData
} from "../components/PluginsDetailsPage";
import PluginSecretFieldDialog from "../components/PluginSecretFieldDialog"; import PluginSecretFieldDialog from "../components/PluginSecretFieldDialog";
import { TypedPluginUpdate } from "../mutations"; import { TypedPluginUpdate } from "../mutations";
import { TypedPluginsDetailsQuery } from "../queries"; import { TypedPluginsDetailsQuery } from "../queries";
@ -89,6 +91,25 @@ export const PluginsDetails: React.FC<PluginsDetailsProps> = ({
} }
}); });
const handleSubmit = async (
formData: PluginDetailsPageFormData
) => {
const result = await pluginUpdate({
variables: {
id,
input: {
active: formData.active,
configuration: getConfigurationInput(
pluginDetails.data.plugin.configuration,
formData.configuration
)
}
}
});
return result.data.pluginUpdate.errors;
};
return ( return (
<> <>
<WindowTitle title={pluginDetails.data?.plugin?.name} /> <WindowTitle title={pluginDetails.data?.plugin?.name} />
@ -110,20 +131,7 @@ export const PluginsDetails: React.FC<PluginsDetailsProps> = ({
id id
}) })
} }
onSubmit={formData => onSubmit={handleSubmit}
pluginUpdate({
variables: {
id,
input: {
active: formData.active,
configuration: getConfigurationInput(
pluginDetails.data.plugin.configuration,
formData.configuration
)
}
}
})
}
/> />
{pluginDetails.data?.plugin?.configuration && ( {pluginDetails.data?.plugin?.configuration && (
<> <>

View file

@ -9,7 +9,7 @@ import Metadata from "@saleor/components/Metadata/Metadata";
import { MetadataFormData } from "@saleor/components/Metadata/types"; import { MetadataFormData } from "@saleor/components/Metadata/types";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ChangeEvent, FormChange } from "@saleor/hooks/useForm"; import { ChangeEvent, FormChange, SubmitPromise } from "@saleor/hooks/useForm";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
@ -61,7 +61,7 @@ export interface ProductTypeDetailsPageProps {
onBack: () => void; onBack: () => void;
onDelete: () => void; onDelete: () => void;
onHasVariantsToggle: (hasVariants: boolean) => void; onHasVariantsToggle: (hasVariants: boolean) => void;
onSubmit: (data: ProductTypeForm) => void; onSubmit: (data: ProductTypeForm) => SubmitPromise;
} }
function handleTaxTypeChange( function handleTaxTypeChange(
@ -141,7 +141,7 @@ const ProductTypeDetailsPage: React.FC<ProductTypeDetailsPageProps> = ({
? data.privateMetadata ? data.privateMetadata
: undefined; : undefined;
onSubmit({ return onSubmit({
...data, ...data,
metadata, metadata,
privateMetadata privateMetadata

View file

@ -3,9 +3,8 @@ import AvailabilityCard from "@saleor/components/AvailabilityCard";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Metadata, { MetadataFormData } from "@saleor/components/Metadata"; import Metadata from "@saleor/components/Metadata";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
@ -13,70 +12,29 @@ import SeoForm from "@saleor/components/SeoForm";
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment"; import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
import { TaxTypeFragment } from "@saleor/fragments/types/TaxTypeFragment"; import { TaxTypeFragment } from "@saleor/fragments/types/TaxTypeFragment";
import useDateLocalize from "@saleor/hooks/useDateLocalize"; import useDateLocalize from "@saleor/hooks/useDateLocalize";
import useFormset from "@saleor/hooks/useFormset";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { import { getChoices } from "@saleor/products/utils/data";
getAttributeInputFromProductType,
getChoices,
ProductType
} from "@saleor/products/utils/data";
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 { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/searches/types/SearchProductTypes"; import { SearchProductTypes_search_edges_node } from "@saleor/searches/types/SearchProductTypes";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import { ContentState, convertToRaw } from "draft-js";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { ContentState, convertToRaw, RawDraftContentState } from "draft-js";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { FetchMoreProps } from "../../../types"; import { FetchMoreProps } from "../../../types";
import { import ProductAttributes from "../ProductAttributes";
createAttributeChangeHandler,
createAttributeMultiChangeHandler,
createProductTypeSelectHandler
} from "../../utils/handlers";
import ProductAttributes, {
ProductAttributeInput,
ProductAttributeInputData
} from "../ProductAttributes";
import ProductDetailsForm from "../ProductDetailsForm"; import ProductDetailsForm from "../ProductDetailsForm";
import ProductOrganization from "../ProductOrganization"; import ProductOrganization from "../ProductOrganization";
import ProductPricing from "../ProductPricing"; import ProductPricing from "../ProductPricing";
import ProductShipping from "../ProductShipping/ProductShipping"; import ProductShipping from "../ProductShipping/ProductShipping";
import ProductStocks, { ProductStockInput } from "../ProductStocks"; import ProductStocks from "../ProductStocks";
import ProductTaxes from "../ProductTaxes"; import ProductTaxes from "../ProductTaxes";
import ProductCreateForm, {
interface FormData extends MetadataFormData { ProductCreateData,
availableForPurchase: string; ProductCreateFormData
basePrice: number; } from "./form";
category: string;
changeTaxCode: boolean;
chargeTaxes: boolean;
collections: string[];
description: RawDraftContentState;
isAvailable: boolean;
isAvailableForPurchase: boolean;
isPublished: boolean;
name: string;
slug: string;
productType: string;
publicationDate: string;
seoDescription: string;
seoTitle: string;
sku: string;
stockQuantity: number;
taxCode: string;
trackInventory: boolean;
visibleInListings: boolean;
weight: string;
}
export interface ProductCreatePageSubmitData extends FormData {
attributes: ProductAttributeInput[];
stocks: ProductStockInput[];
}
interface ProductCreatePageProps { interface ProductCreatePageProps {
errors: ProductErrorWithAttributesFragment[]; errors: ProductErrorWithAttributesFragment[];
@ -87,13 +45,8 @@ interface ProductCreatePageProps {
fetchMoreCategories: FetchMoreProps; fetchMoreCategories: FetchMoreProps;
fetchMoreCollections: FetchMoreProps; fetchMoreCollections: FetchMoreProps;
fetchMoreProductTypes: FetchMoreProps; fetchMoreProductTypes: FetchMoreProps;
initial?: Partial<FormData>; initial?: Partial<ProductCreateFormData>;
productTypes?: Array<{ productTypes?: SearchProductTypes_search_edges_node[];
id: string;
name: string;
hasVariants: boolean;
productAttributes: SearchProductTypes_search_edges_node_productAttributes[];
}>;
header: string; header: string;
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
weightUnit: string; weightUnit: string;
@ -104,7 +57,7 @@ interface ProductCreatePageProps {
fetchProductTypes: (data: string) => void; fetchProductTypes: (data: string) => void;
onWarehouseConfigure: () => void; onWarehouseConfigure: () => void;
onBack?(); onBack?();
onSubmit?(data: ProductCreatePageSubmitData); onSubmit?(data: ProductCreateData);
} }
export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
@ -133,65 +86,12 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
const intl = useIntl(); const intl = useIntl();
const localizeDate = useDateLocalize(); const localizeDate = useDateLocalize();
const initialProductType = productTypeChoiceList?.find(
productType => initial?.productType === productType.id
);
// Form values
const {
change: changeAttributeData,
data: attributes,
set: setAttributeData
} = useFormset<ProductAttributeInputData>(
initial?.productType
? getAttributeInputFromProductType(initialProductType)
: []
);
const {
add: addStock,
change: changeStockData,
data: stocks,
remove: removeStock
} = useFormset<null, string>([]);
// Ensures that it will not change after component rerenders, because it // Ensures that it will not change after component rerenders, because it
// generates different block keys and it causes editor to lose its content. // generates different block keys and it causes editor to lose its content.
const initialDescription = React.useRef( const initialDescription = React.useRef(
convertToRaw(ContentState.createFromText("")) convertToRaw(ContentState.createFromText(""))
); );
const {
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const initialData: FormData = {
...(initial || {}),
availableForPurchase: "",
basePrice: 0,
category: "",
changeTaxCode: false,
chargeTaxes: false,
collections: [],
description: {} as any,
isAvailable: false,
isAvailableForPurchase: false,
isPublished: false,
metadata: [],
name: "",
privateMetadata: [],
productType: "",
publicationDate: "",
seoDescription: "",
seoTitle: "",
sku: null,
slug: "",
stockQuantity: null,
taxCode: null,
trackInventory: false,
visibleInListings: false,
weight: ""
};
// Display values // Display values
const [selectedCategory, setSelectedCategory] = useStateFromProps( const [selectedCategory, setSelectedCategory] = useStateFromProps(
initial?.category || "" initial?.category || ""
@ -201,9 +101,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
MultiAutocompleteChoiceType[] MultiAutocompleteChoiceType[]
>([]); >([]);
const [productType, setProductType] = useStateFromProps<ProductType>(
initialProductType || null
);
const [selectedTaxType, setSelectedTaxType] = useStateFromProps( const [selectedTaxType, setSelectedTaxType] = useStateFromProps(
initial?.taxCode || null initial?.taxCode || null
); );
@ -217,50 +114,23 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
value: taxType.taxCode value: taxType.taxCode
})) || []; })) || [];
const handleSubmit = (data: FormData) =>
onSubmit({
...data,
attributes,
stocks
});
return ( return (
<Form onSubmit={handleSubmit} initial={initialData} confirmLeave> <ProductCreateForm
{({ change, data, hasChanged, submit, triggerChange, toggleValue }) => { onSubmit={onSubmit}
const handleCollectionSelect = createMultiAutocompleteSelectHandler( initial={initial}
toggleValue, categories={categories}
setSelectedCollections, collections={collections}
selectedCollections, productTypes={productTypeChoiceList}
collections selectedCollections={selectedCollections}
); setSelectedCategory={setSelectedCategory}
const handleCategorySelect = createSingleAutocompleteSelectHandler( setSelectedCollections={setSelectedCollections}
change, setSelectedTaxType={setSelectedTaxType}
setSelectedCategory, taxTypes={taxTypeChoices}
categories warehouses={warehouses}
); >
const handleAttributeChange = createAttributeChangeHandler( {({ change, data, handlers, hasChanged, submit }) => {
changeAttributeData, // Comparing explicitly to false because `hasVariants` can be undefined
triggerChange const isSimpleProduct = data.productType?.hasVariants === false;
);
const handleAttributeMultiChange = createAttributeMultiChangeHandler(
changeAttributeData,
attributes,
triggerChange
);
const handleProductTypeSelect = createProductTypeSelectHandler(
change,
setAttributeData,
setProductType,
productTypeChoiceList
);
const handleTaxTypeSelect = createSingleAutocompleteSelectHandler(
change,
setSelectedTaxType,
taxTypeChoices
);
const changeMetadata = makeMetadataChangeHandler(change);
return ( return (
<Container> <Container>
@ -278,17 +148,17 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
onChange={change} onChange={change}
/> />
<CardSpacer /> <CardSpacer />
{attributes.length > 0 && ( {data.attributes.length > 0 && (
<ProductAttributes <ProductAttributes
attributes={attributes} attributes={data.attributes}
disabled={disabled} disabled={disabled}
errors={errors} errors={errors}
onChange={handleAttributeChange} onChange={handlers.selectAttribute}
onMultiChange={handleAttributeMultiChange} onMultiChange={handlers.selectAttributeMultiple}
/> />
)} )}
<CardSpacer /> <CardSpacer />
{!!productType && !productType.hasVariants && ( {isSimpleProduct && (
<> <>
<ProductShipping <ProductShipping
data={data} data={data}
@ -311,27 +181,11 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
hasVariants={false} hasVariants={false}
onFormDataChange={change} onFormDataChange={change}
errors={errors} errors={errors}
stocks={stocks} stocks={data.stocks}
warehouses={warehouses} warehouses={warehouses}
onChange={(id, value) => { onChange={handlers.changeStock}
triggerChange(); onWarehouseStockAdd={handlers.addStock}
changeStockData(id, value); onWarehouseStockDelete={handlers.deleteStock}
}}
onWarehouseStockAdd={id => {
triggerChange();
addStock({
data: null,
id,
label: warehouses.find(
warehouse => warehouse.id === id
).name,
value: "0"
});
}}
onWarehouseStockDelete={id => {
triggerChange();
removeStock(id);
}}
onWarehouseConfigure={onWarehouseConfigure} onWarehouseConfigure={onWarehouseConfigure}
/> />
<CardSpacer /> <CardSpacer />
@ -353,7 +207,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
onChange={change} onChange={change}
/> />
<CardSpacer /> <CardSpacer />
<Metadata data={data} onChange={changeMetadata} /> <Metadata data={data} onChange={handlers.changeMetadata} />
</div> </div>
<div> <div>
<ProductOrganization <ProductOrganization
@ -370,12 +224,12 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
fetchMoreCollections={fetchMoreCollections} fetchMoreCollections={fetchMoreCollections}
fetchMoreProductTypes={fetchMoreProductTypes} fetchMoreProductTypes={fetchMoreProductTypes}
fetchProductTypes={fetchProductTypes} fetchProductTypes={fetchProductTypes}
productType={productType} productType={data.productType}
productTypeInputDisplayValue={productType?.name || ""} productTypeInputDisplayValue={data.productType?.name || ""}
productTypes={productTypes} productTypes={productTypes}
onCategoryChange={handleCategorySelect} onCategoryChange={handlers.selectCategory}
onCollectionChange={handleCollectionSelect} onCollectionChange={handlers.selectCollection}
onProductTypeChange={handleProductTypeSelect} onProductTypeChange={handlers.selectProductType}
collectionsInputDisplayValue={selectedCollections} collectionsInputDisplayValue={selectedCollections}
/> />
<CardSpacer /> <CardSpacer />
@ -409,7 +263,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
data={data} data={data}
disabled={disabled} disabled={disabled}
onChange={change} onChange={change}
onTaxTypeChange={handleTaxTypeSelect} onTaxTypeChange={handlers.selectTaxRate}
selectedTaxTypeDisplayName={selectedTaxType} selectedTaxTypeDisplayName={selectedTaxType}
taxTypes={taxTypes} taxTypes={taxTypes}
/> />
@ -424,7 +278,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
</Container> </Container>
); );
}} }}
</Form> </ProductCreateForm>
); );
}; };
ProductCreatePage.displayName = "ProductCreatePage"; ProductCreatePage.displayName = "ProductCreatePage";

View file

@ -0,0 +1,254 @@
import { MetadataFormData } from "@saleor/components/Metadata";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import useForm, { FormChange } from "@saleor/hooks/useForm";
import useFormset, { FormsetChange } from "@saleor/hooks/useFormset";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import {
getAttributeInputFromProductType,
ProductType
} from "@saleor/products/utils/data";
import {
createAttributeChangeHandler,
createAttributeMultiChangeHandler,
createProductTypeSelectHandler
} from "@saleor/products/utils/handlers";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { RawDraftContentState } from "draft-js";
import React from "react";
import { SearchProductTypes_search_edges_node } from "../../../searches/types/SearchProductTypes";
import {
ProductAttributeInput,
ProductAttributeInputData
} from "../ProductAttributes";
import { ProductStockInput } from "../ProductStocks";
export interface ProductCreateFormData extends MetadataFormData {
availableForPurchase: string;
basePrice: number;
category: string;
changeTaxCode: boolean;
chargeTaxes: boolean;
collections: string[];
description: RawDraftContentState;
isAvailable: boolean;
isAvailableForPurchase: boolean;
isPublished: boolean;
name: string;
productType: ProductType;
publicationDate: string;
seoDescription: string;
seoTitle: string;
sku: string;
slug: string;
stockQuantity: number;
taxCode: string;
trackInventory: boolean;
visibleInListings: boolean;
weight: string;
}
export interface ProductCreateData extends ProductCreateFormData {
attributes: ProductAttributeInput[];
stocks: ProductStockInput[];
}
type ProductCreateHandlers = Record<
| "changeMetadata"
| "selectCategory"
| "selectCollection"
| "selectProductType"
| "selectTaxRate",
FormChange
> &
Record<
"changeStock" | "selectAttribute" | "selectAttributeMultiple",
FormsetChange<string>
> &
Record<"addStock" | "deleteStock", (id: string) => void>;
export interface UseProductCreateFormResult {
change: FormChange;
data: ProductCreateData;
handlers: ProductCreateHandlers;
hasChanged: boolean;
submit: () => Promise<boolean>;
}
export interface UseProductCreateFormOpts
extends Record<
"categories" | "collections" | "taxTypes",
SingleAutocompleteChoiceType[]
> {
setSelectedCategory: React.Dispatch<React.SetStateAction<string>>;
setSelectedCollections: React.Dispatch<
React.SetStateAction<MultiAutocompleteChoiceType[]>
>;
setSelectedTaxType: React.Dispatch<React.SetStateAction<string>>;
selectedCollections: MultiAutocompleteChoiceType[];
productTypes: SearchProductTypes_search_edges_node[];
warehouses: SearchWarehouses_search_edges_node[];
}
export interface ProductCreateFormProps extends UseProductCreateFormOpts {
children: (props: UseProductCreateFormResult) => React.ReactNode;
initial?: Partial<ProductCreateFormData>;
onSubmit: (data: ProductCreateData) => Promise<boolean>;
}
const defaultInitialFormData: ProductCreateFormData &
Record<"productType", string> = {
availableForPurchase: "",
basePrice: 0,
category: "",
changeTaxCode: false,
chargeTaxes: false,
collections: [],
description: {} as any,
isAvailable: false,
isAvailableForPurchase: false,
isPublished: false,
metadata: [],
name: "",
privateMetadata: [],
productType: null,
publicationDate: "",
seoDescription: "",
seoTitle: "",
sku: null,
slug: "",
stockQuantity: null,
taxCode: null,
trackInventory: false,
visibleInListings: false,
weight: ""
};
function useProductCreateForm(
initial: Partial<ProductCreateFormData>,
onSubmit: (data: ProductCreateData) => Promise<boolean>,
opts: UseProductCreateFormOpts
): UseProductCreateFormResult {
const initialProductType =
opts.productTypes?.find(
productType => initial?.productType?.id === productType.id
) || null;
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm({
...initial,
...defaultInitialFormData
});
const attributes = useFormset<ProductAttributeInputData>(
initial?.productType
? getAttributeInputFromProductType(initialProductType)
: []
);
const stocks = useFormset<null, string>([]);
const [productType, setProductType] = useStateFromProps<ProductType>(
initialProductType || null
);
const {
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const handleCollectionSelect = createMultiAutocompleteSelectHandler(
form.toggleValue,
opts.setSelectedCollections,
opts.selectedCollections,
opts.collections
);
const handleCategorySelect = createSingleAutocompleteSelectHandler(
handleChange,
opts.setSelectedCategory,
opts.categories
);
const handleAttributeChange = createAttributeChangeHandler(
attributes.change,
triggerChange
);
const handleAttributeMultiChange = createAttributeMultiChangeHandler(
attributes.change,
attributes.data,
triggerChange
);
const handleProductTypeSelect = createProductTypeSelectHandler(
attributes.set,
setProductType,
opts.productTypes,
triggerChange
);
const handleStockChange: FormsetChange<string> = (id, value) => {
triggerChange();
stocks.change(id, value);
};
const handleStockAdd = (id: string) => {
triggerChange();
stocks.add({
data: null,
id,
label: opts.warehouses.find(warehouse => warehouse.id === id).name,
value: "0"
});
};
const handleStockDelete = (id: string) => {
triggerChange();
stocks.remove(id);
};
const handleTaxTypeSelect = createSingleAutocompleteSelectHandler(
handleChange,
opts.setSelectedTaxType,
opts.taxTypes
);
const changeMetadata = makeMetadataChangeHandler(handleChange);
const data: ProductCreateData = {
...form.data,
attributes: attributes.data,
productType,
stocks: stocks.data
};
const submit = () => onSubmit(data);
return {
change: handleChange,
data,
handlers: {
addStock: handleStockAdd,
changeMetadata,
changeStock: handleStockChange,
deleteStock: handleStockDelete,
selectAttribute: handleAttributeChange,
selectAttributeMultiple: handleAttributeMultiChange,
selectCategory: handleCategorySelect,
selectCollection: handleCollectionSelect,
selectProductType: handleProductTypeSelect,
selectTaxRate: handleTaxTypeSelect
},
hasChanged: changed,
submit
};
}
const ProductCreateForm: React.FC<ProductCreateFormProps> = ({
children,
initial,
onSubmit,
...rest
}) => {
const props = useProductCreateForm(initial || {}, onSubmit, rest);
return <form onSubmit={props.submit}>{children(props)}</form>;
};
ProductCreateForm.displayName = "ProductCreateForm";
export default ProductCreateForm;

View file

@ -1,2 +1,3 @@
export { default } from "./ProductCreatePage"; export { default } from "./ProductCreatePage";
export * from "./ProductCreatePage"; export * from "./ProductCreatePage";
export * from "./form";

View file

@ -51,7 +51,7 @@ interface ProductOrganizationProps {
data: { data: {
category: string; category: string;
collections: string[]; collections: string[];
productType?: string; productType?: ProductType;
}; };
disabled: boolean; disabled: boolean;
errors: ProductErrorFragment[]; errors: ProductErrorFragment[];
@ -121,7 +121,7 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
defaultMessage: "Product Type" defaultMessage: "Product Type"
})} })}
choices={productTypes} choices={productTypes}
value={data.productType} value={data.productType?.id}
onChange={onProductTypeChange} onChange={onProductTypeChange}
fetchChoices={fetchProductTypes} fetchChoices={fetchProductTypes}
data-test="product-type" data-test="product-type"

View file

@ -127,9 +127,10 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
const anchor = React.useRef<HTMLDivElement>(); const anchor = React.useRef<HTMLDivElement>();
const [isExpanded, setExpansionState] = React.useState(false); const [isExpanded, setExpansionState] = React.useState(false);
const warehousesToAssign = warehouses.filter( const warehousesToAssign =
warehouse => !stocks.some(stock => stock.id === warehouse.id) warehouses?.filter(
); warehouse => !stocks.some(stock => stock.id === warehouse.id)
) || [];
const formErrors = getFormErrors(["sku"], errors); const formErrors = getFormErrors(["sku"], errors);
return ( return (
@ -187,7 +188,7 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
</span> </span>
</div> </div>
</Typography> </Typography>
{!warehouses.length && ( {!warehouses?.length && (
<Typography color="textSecondary" className={classes.noWarehouseInfo}> <Typography color="textSecondary" className={classes.noWarehouseInfo}>
{hasVariants ? ( {hasVariants ? (
<> <>
@ -219,7 +220,7 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
</Typography> </Typography>
)} )}
</CardContent> </CardContent>
{warehouses.length > 0 && ( {warehouses?.length > 0 && (
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>

View file

@ -3,7 +3,6 @@ import AvailabilityCard from "@saleor/components/AvailabilityCard";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Metadata from "@saleor/components/Metadata/Metadata"; import Metadata from "@saleor/components/Metadata/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
@ -13,18 +12,14 @@ import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/Prod
import { TaxTypeFragment } from "@saleor/fragments/types/TaxTypeFragment"; import { TaxTypeFragment } from "@saleor/fragments/types/TaxTypeFragment";
import { WarehouseFragment } from "@saleor/fragments/types/WarehouseFragment"; import { WarehouseFragment } from "@saleor/fragments/types/WarehouseFragment";
import useDateLocalize from "@saleor/hooks/useDateLocalize"; import useDateLocalize from "@saleor/hooks/useDateLocalize";
import useFormset from "@saleor/hooks/useFormset"; import { SubmitPromise } from "@saleor/hooks/useForm";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl"; 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, ReorderAction } from "@saleor/types"; import { FetchMoreProps, ListActions, ReorderAction } from "@saleor/types";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { convertFromRaw, RawDraftContentState } from "draft-js"; import { convertFromRaw, RawDraftContentState } from "draft-js";
import { diff } from "fast-array-diff";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -33,17 +28,7 @@ import {
ProductDetails_product_images, ProductDetails_product_images,
ProductDetails_product_variants ProductDetails_product_variants
} from "../../types/ProductDetails"; } from "../../types/ProductDetails";
import { import { getChoices, ProductUpdatePageFormData } from "../../utils/data";
getAttributeInputFromProduct,
getChoices,
getProductUpdatePageFormData,
getStockInputFromProduct,
ProductUpdatePageFormData
} from "../../utils/data";
import {
createAttributeChangeHandler,
createAttributeMultiChangeHandler
} from "../../utils/handlers";
import ProductAttributes, { ProductAttributeInput } from "../ProductAttributes"; import ProductAttributes, { ProductAttributeInput } from "../ProductAttributes";
import ProductDetailsForm from "../ProductDetailsForm"; import ProductDetailsForm from "../ProductDetailsForm";
import ProductImages from "../ProductImages"; import ProductImages from "../ProductImages";
@ -53,6 +38,7 @@ import ProductShipping from "../ProductShipping/ProductShipping";
import ProductStocks, { ProductStockInput } from "../ProductStocks"; import ProductStocks, { ProductStockInput } from "../ProductStocks";
import ProductTaxes from "../ProductTaxes"; import ProductTaxes from "../ProductTaxes";
import ProductVariants from "../ProductVariants"; import ProductVariants from "../ProductVariants";
import ProductUpdateForm from "./form";
export interface ProductUpdatePageProps extends ListActions { export interface ProductUpdatePageProps extends ListActions {
defaultWeightUnit: string; defaultWeightUnit: string;
@ -76,13 +62,13 @@ export interface ProductUpdatePageProps extends ListActions {
onVariantShow: (id: string) => () => void; onVariantShow: (id: string) => () => void;
onVariantReorder: ReorderAction; onVariantReorder: ReorderAction;
onImageDelete: (id: string) => () => void; onImageDelete: (id: string) => () => void;
onSubmit: (data: ProductUpdatePageSubmitData) => SubmitPromise;
onBack?(); onBack?();
onDelete(); onDelete();
onImageEdit?(id: string); onImageEdit?(id: string);
onImageReorder?(event: { oldIndex: number; newIndex: number }); onImageReorder?(event: { oldIndex: number; newIndex: number });
onImageUpload(file: File); onImageUpload(file: File);
onSeoClick?(); onSeoClick?();
onSubmit?(data: ProductUpdatePageSubmitData);
onVariantAdd?(); onVariantAdd?();
onSetDefaultVariant(variant: ProductDetails_product_variants); onSetDefaultVariant(variant: ProductDetails_product_variants);
onWarehouseConfigure(); onWarehouseConfigure();
@ -136,25 +122,9 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const localizeDate = useDateLocalize(); const localizeDate = useDateLocalize();
const attributeInput = React.useMemo(
() => getAttributeInputFromProduct(product),
[product]
);
const stockInput = React.useMemo(() => getStockInputFromProduct(product), [
product
]);
const { change: changeAttributeData, data: attributes } = useFormset(
attributeInput
);
const {
add: addStock,
change: changeStockData,
data: stocks,
remove: removeStock
} = useFormset(stockInput);
const [selectedCategory, setSelectedCategory] = useStateFromProps( const [selectedCategory, setSelectedCategory] = useStateFromProps(
maybe(() => product.category.name, "") product?.category?.name || ""
); );
const [selectedCollections, setSelectedCollections] = useStateFromProps( const [selectedCollections, setSelectedCollections] = useStateFromProps(
@ -165,309 +135,217 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
product?.taxType.description product?.taxType.description
); );
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const initialData = getProductUpdatePageFormData(product, variants);
const initialDescription = maybe<RawDraftContentState>(() => const initialDescription = maybe<RawDraftContentState>(() =>
JSON.parse(product.descriptionJson) JSON.parse(product.descriptionJson)
); );
const categories = getChoices(categoryChoiceList); const categories = getChoices(categoryChoiceList);
const collections = getChoices(collectionChoiceList); const collections = getChoices(collectionChoiceList);
const currency = const currency = product?.variants[0]?.price.currency;
product?.variants?.length && product.variants[0].price.currency; const hasVariants = product?.productType?.hasVariants;
const hasVariants = maybe(() => product.productType.hasVariants, false);
const taxTypeChoices = const taxTypeChoices =
taxTypes?.map(taxType => ({ taxTypes?.map(taxType => ({
label: taxType.description, label: taxType.description,
value: taxType.taxCode value: taxType.taxCode
})) || []; })) || [];
const getAvailabilityData = ({
availableForPurchase,
isAvailableForPurchase,
isPublished,
publicationDate
}: ProductUpdatePageFormData) => ({
isAvailableForPurchase: isAvailableForPurchase || !!availableForPurchase,
isPublished: isPublished || !!publicationDate
});
const getStocksData = () => {
if (product.productType.hasVariants) {
return { addStocks: [], removeStocks: [], updateStocks: [] };
}
const dataStocks = stocks.map(stock => stock.id);
const variantStocks = product.variants[0]?.stocks.map(
stock => stock.warehouse.id
);
const stockDiff = diff(variantStocks, dataStocks);
return {
addStocks: stocks.filter(stock =>
stockDiff.added.some(addedStock => addedStock === stock.id)
),
removeStocks: stockDiff.removed,
updateStocks: stocks.filter(
stock => !stockDiff.added.some(addedStock => addedStock === stock.id)
)
};
};
const getMetadata = (data: ProductUpdatePageFormData) => ({
metadata: isMetadataModified ? data.metadata : undefined,
privateMetadata: isPrivateMetadataModified
? data.privateMetadata
: undefined
});
const getParsedData = (data: ProductUpdatePageFormData) => ({
...data,
...getAvailabilityData(data),
...getStocksData(),
...getMetadata(data),
attributes
});
const handleSubmit = (data: ProductUpdatePageFormData) =>
onSubmit(getParsedData(data));
return ( return (
<Form onSubmit={handleSubmit} initial={initialData} confirmLeave> <ProductUpdateForm
{({ change, data, hasChanged, submit, triggerChange, toggleValue }) => { onSubmit={onSubmit}
const handleCollectionSelect = createMultiAutocompleteSelectHandler( product={product}
toggleValue, categories={categories}
setSelectedCollections, collections={collections}
selectedCollections, selectedCollections={selectedCollections}
collections setSelectedCategory={setSelectedCategory}
); setSelectedCollections={setSelectedCollections}
const handleCategorySelect = createSingleAutocompleteSelectHandler( setSelectedTaxType={setSelectedTaxType}
change, taxTypes={taxTypeChoices}
setSelectedCategory, warehouses={warehouses}
categories >
); {({ change, data, handlers, hasChanged, submit }) => (
const handleAttributeChange = createAttributeChangeHandler( <>
changeAttributeData, <Container>
triggerChange <AppHeader onBack={onBack}>
); {intl.formatMessage(sectionNames.products)}
const handleAttributeMultiChange = createAttributeMultiChangeHandler( </AppHeader>
changeAttributeData, <PageHeader title={header} />
attributes, <Grid>
triggerChange <div>
); <ProductDetailsForm
const changeMetadata = makeMetadataChangeHandler(change); data={data}
const handleTaxTypeSelect = createSingleAutocompleteSelectHandler( disabled={disabled}
change, errors={errors}
setSelectedTaxType, initialDescription={initialDescription}
taxTypeChoices onChange={change}
); />
<CardSpacer />
return ( <ProductImages
<> images={images}
<Container> placeholderImage={placeholderImage}
<AppHeader onBack={onBack}> onImageDelete={onImageDelete}
{intl.formatMessage(sectionNames.products)} onImageReorder={onImageReorder}
</AppHeader> onImageEdit={onImageEdit}
<PageHeader title={header} /> onImageUpload={onImageUpload}
<Grid> />
<div> <CardSpacer />
<ProductDetailsForm {data.attributes.length > 0 && (
data={data} <ProductAttributes
disabled={disabled} attributes={data.attributes}
errors={errors} errors={errors}
initialDescription={initialDescription} disabled={disabled}
onChange={change} onChange={handlers.selectAttribute}
onMultiChange={handlers.selectAttributeMultiple}
/> />
<CardSpacer /> )}
<ProductImages <CardSpacer />
images={images} {!!product?.productType && !hasVariants && (
placeholderImage={placeholderImage} <>
onImageDelete={onImageDelete} <ProductPricing
onImageReorder={onImageReorder} currency={currency}
onImageEdit={onImageEdit} data={data}
onImageUpload={onImageUpload} disabled={disabled}
/>
<CardSpacer />
{attributes.length > 0 && (
<ProductAttributes
attributes={attributes}
errors={errors} errors={errors}
disabled={disabled} onChange={change}
onChange={handleAttributeChange}
onMultiChange={handleAttributeMultiChange}
/> />
)} <CardSpacer />
<CardSpacer /> </>
{!!product?.productType && !hasVariants && ( )}
<> {hasVariants ? (
<ProductPricing <ProductVariants
currency={currency} disabled={disabled}
data={data} variants={variants}
disabled={disabled} product={product}
errors={errors} fallbackPrice={
onChange={change} product?.variants?.length
/> ? product.variants[0].price
<CardSpacer /> : undefined
</> }
)} onRowClick={onVariantShow}
{hasVariants ? ( onVariantAdd={onVariantAdd}
<ProductVariants onVariantsAdd={onVariantsAdd}
onVariantReorder={onVariantReorder}
onSetDefaultVariant={onSetDefaultVariant}
toolbar={toolbar}
isChecked={isChecked}
selected={selected}
toggle={toggle}
toggleAll={toggleAll}
/>
) : (
<>
<ProductShipping
data={data}
disabled={disabled} disabled={disabled}
variants={variants} errors={errors}
product={product} weightUnit={product?.weight?.unit || defaultWeightUnit}
fallbackPrice={ onChange={change}
product?.variants?.length />
? product.variants[0].price <CardSpacer />
: undefined <ProductStocks
data={data}
disabled={disabled}
hasVariants={false}
errors={errors}
stocks={data.stocks}
warehouses={warehouses}
onChange={handlers.changeStock}
onFormDataChange={change}
onWarehouseStockAdd={handlers.addStock}
onWarehouseStockDelete={handlers.deleteStock}
onWarehouseConfigure={onWarehouseConfigure}
/>
</>
)}
<CardSpacer />
<SeoForm
errors={errors}
title={data.seoTitle}
titlePlaceholder={data.name}
description={data.seoDescription}
descriptionPlaceholder={maybe(() =>
convertFromRaw(data.description)
.getPlainText()
.slice(0, 300)
)}
slug={data.slug}
slugPlaceholder={data.name}
loading={disabled}
onClick={onSeoClick}
onChange={change}
helperText={intl.formatMessage({
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 />
<AvailabilityCard
data={data}
errors={errors}
disabled={disabled}
messages={{
hiddenLabel: intl.formatMessage({
defaultMessage: "Not published",
description: "product label"
}),
hiddenSecondLabel: intl.formatMessage(
{
defaultMessage: "will become published on {date}",
description: "product publication date label"
},
{
date: localizeDate(data.publicationDate, "L")
} }
onRowClick={onVariantShow} ),
onVariantAdd={onVariantAdd} visibleLabel: intl.formatMessage({
onVariantsAdd={onVariantsAdd} defaultMessage: "Published",
onVariantReorder={onVariantReorder} description: "product label"
onSetDefaultVariant={onSetDefaultVariant} })
toolbar={toolbar} }}
isChecked={isChecked} onChange={change}
selected={selected} />
toggle={toggle} <CardSpacer />
toggleAll={toggleAll} <ProductTaxes
/> data={data}
) : ( disabled={disabled}
<> selectedTaxTypeDisplayName={selectedTaxType}
<ProductShipping taxTypes={taxTypes}
data={data} onChange={change}
disabled={disabled} onTaxTypeChange={handlers.selectTaxRate}
errors={errors} />
weightUnit={product?.weight?.unit || defaultWeightUnit} </div>
onChange={change} </Grid>
/> <SaveButtonBar
<CardSpacer /> onCancel={onBack}
<ProductStocks onDelete={onDelete}
data={data} onSave={submit}
disabled={disabled} state={saveButtonBarState}
hasVariants={false} disabled={disabled || !hasChanged}
errors={errors} />
stocks={stocks} </Container>
warehouses={warehouses} </>
onChange={(id, value) => { )}
triggerChange(); </ProductUpdateForm>
changeStockData(id, value);
}}
onFormDataChange={change}
onWarehouseStockAdd={id => {
triggerChange();
addStock({
data: null,
id,
label: warehouses.find(
warehouse => warehouse.id === id
).name,
value: "0"
});
}}
onWarehouseStockDelete={id => {
triggerChange();
removeStock(id);
}}
onWarehouseConfigure={onWarehouseConfigure}
/>
</>
)}
<CardSpacer />
<SeoForm
errors={errors}
title={data.seoTitle}
titlePlaceholder={data.name}
description={data.seoDescription}
descriptionPlaceholder={maybe(() =>
convertFromRaw(data.description)
.getPlainText()
.slice(0, 300)
)}
slug={data.slug}
slugPlaceholder={data.name}
loading={disabled}
onClick={onSeoClick}
onChange={change}
helperText={intl.formatMessage({
defaultMessage:
"Add search engine title and description to make this product easier to find"
})}
/>
<CardSpacer />
<Metadata data={data} onChange={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={maybe(() => product.productType)}
onCategoryChange={handleCategorySelect}
onCollectionChange={handleCollectionSelect}
/>
<CardSpacer />
<AvailabilityCard
data={data}
errors={errors}
disabled={disabled}
messages={{
hiddenLabel: intl.formatMessage({
defaultMessage: "Not published",
description: "product label"
}),
hiddenSecondLabel: intl.formatMessage(
{
defaultMessage: "will become published on {date}",
description: "product publication date label"
},
{
date: localizeDate(data.publicationDate, "L")
}
),
visibleLabel: intl.formatMessage({
defaultMessage: "Published",
description: "product label"
})
}}
onChange={change}
/>
<CardSpacer />
<ProductTaxes
data={data}
disabled={disabled}
selectedTaxTypeDisplayName={selectedTaxType}
taxTypes={taxTypes}
onChange={change}
onTaxTypeChange={handleTaxTypeSelect}
/>
</div>
</Grid>
<SaveButtonBar
onCancel={onBack}
onDelete={onDelete}
onSave={submit}
state={saveButtonBarState}
disabled={disabled || !hasChanged}
/>
</Container>
</>
);
}}
</Form>
); );
}; };
ProductUpdatePage.displayName = "ProductUpdatePage"; ProductUpdatePage.displayName = "ProductUpdatePage";

View file

@ -0,0 +1,260 @@
import { MetadataFormData } from "@saleor/components/Metadata";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm";
import useFormset, {
FormsetChange,
FormsetData
} from "@saleor/hooks/useFormset";
import { ProductDetails_product } from "@saleor/products/types/ProductDetails";
import {
getAttributeInputFromProduct,
getProductUpdatePageFormData,
getStockInputFromProduct
} from "@saleor/products/utils/data";
import {
createAttributeChangeHandler,
createAttributeMultiChangeHandler
} from "@saleor/products/utils/handlers";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { RawDraftContentState } from "draft-js";
import { diff } from "fast-array-diff";
import React from "react";
import { ProductAttributeInput } from "../ProductAttributes";
import { ProductStockInput } from "../ProductStocks";
export interface ProductUpdateFormData extends MetadataFormData {
availableForPurchase: string;
basePrice: number;
category: string | null;
changeTaxCode: boolean;
chargeTaxes: boolean;
collections: string[];
description: RawDraftContentState;
isAvailable: boolean;
isAvailableForPurchase: boolean;
isPublished: boolean;
name: string;
slug: string;
publicationDate: string;
seoDescription: string;
seoTitle: string;
sku: string;
taxCode: string;
trackInventory: boolean;
visibleInListings: boolean;
weight: string;
}
export interface ProductUpdateData extends ProductUpdateFormData {
attributes: ProductAttributeInput[];
stocks: ProductStockInput[];
}
export interface ProductUpdateSubmitData extends ProductUpdateFormData {
attributes: ProductAttributeInput[];
collections: string[];
addStocks: ProductStockInput[];
updateStocks: ProductStockInput[];
removeStocks: string[];
}
type ProductUpdateHandlers = Record<
"changeMetadata" | "selectCategory" | "selectCollection" | "selectTaxRate",
FormChange
> &
Record<
"changeStock" | "selectAttribute" | "selectAttributeMultiple",
FormsetChange<string>
> &
Record<"addStock" | "deleteStock", (id: string) => void>;
export interface UseProductUpdateFormResult {
change: FormChange;
data: ProductUpdateData;
handlers: ProductUpdateHandlers;
hasChanged: boolean;
submit: () => Promise<boolean>;
}
export interface UseProductUpdateFormOpts
extends Record<
"categories" | "collections" | "taxTypes",
SingleAutocompleteChoiceType[]
> {
setSelectedCategory: React.Dispatch<React.SetStateAction<string>>;
setSelectedCollections: React.Dispatch<
React.SetStateAction<MultiAutocompleteChoiceType[]>
>;
setSelectedTaxType: React.Dispatch<React.SetStateAction<string>>;
selectedCollections: MultiAutocompleteChoiceType[];
warehouses: SearchWarehouses_search_edges_node[];
}
export interface ProductUpdateFormProps extends UseProductUpdateFormOpts {
children: (props: UseProductUpdateFormResult) => React.ReactNode;
product: ProductDetails_product;
onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise;
}
const getAvailabilityData = ({
availableForPurchase,
isAvailableForPurchase,
isPublished,
publicationDate
}: ProductUpdateFormData) => ({
isAvailableForPurchase: isAvailableForPurchase || !!availableForPurchase,
isPublished: isPublished || !!publicationDate
});
const getStocksData = (
product: ProductDetails_product,
stocks: FormsetData<null, string>
) => {
if (product?.productType?.hasVariants) {
return { addStocks: [], removeStocks: [], updateStocks: [] };
}
const dataStocks = stocks.map(stock => stock.id);
const variantStocks =
product?.variants[0]?.stocks.map(stock => stock.warehouse.id) || [];
const stockDiff = diff(variantStocks, dataStocks);
return {
addStocks: stocks.filter(stock =>
stockDiff.added.some(addedStock => addedStock === stock.id)
),
removeStocks: stockDiff.removed,
updateStocks: stocks.filter(
stock => !stockDiff.added.some(addedStock => addedStock === stock.id)
)
};
};
const getMetadata = (
data: ProductUpdateFormData,
isMetadataModified: boolean,
isPrivateMetadataModified: boolean
) => ({
metadata: isMetadataModified ? data.metadata : undefined,
privateMetadata: isPrivateMetadataModified ? data.privateMetadata : undefined
});
function useProductUpdateForm(
product: ProductDetails_product,
onSubmit: (data: ProductUpdateSubmitData) => SubmitPromise,
opts: UseProductUpdateFormOpts
): UseProductUpdateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm(
getProductUpdatePageFormData(product, product?.variants)
);
const attributes = useFormset(getAttributeInputFromProduct(product));
const stocks = useFormset(getStockInputFromProduct(product));
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const handleCollectionSelect = createMultiAutocompleteSelectHandler(
form.toggleValue,
opts.setSelectedCollections,
opts.selectedCollections,
opts.collections
);
const handleCategorySelect = createSingleAutocompleteSelectHandler(
handleChange,
opts.setSelectedCategory,
opts.categories
);
const handleAttributeChange = createAttributeChangeHandler(
attributes.change,
triggerChange
);
const handleAttributeMultiChange = createAttributeMultiChangeHandler(
attributes.change,
attributes.data,
triggerChange
);
const handleStockChange: FormsetChange<string> = (id, value) => {
triggerChange();
stocks.change(id, value);
};
const handleStockAdd = (id: string) => {
triggerChange();
stocks.add({
data: null,
id,
label: opts.warehouses.find(warehouse => warehouse.id === id).name,
value: "0"
});
};
const handleStockDelete = (id: string) => {
triggerChange();
stocks.remove(id);
};
const handleTaxTypeSelect = createSingleAutocompleteSelectHandler(
handleChange,
opts.setSelectedTaxType,
opts.taxTypes
);
const changeMetadata = makeMetadataChangeHandler(handleChange);
const data: ProductUpdateData = {
...form.data,
attributes: attributes.data,
stocks: stocks.data
};
const submitData: ProductUpdateSubmitData = {
...data,
...getAvailabilityData(data),
...getStocksData(product, stocks.data),
...getMetadata(data, isMetadataModified, isPrivateMetadataModified),
addStocks: [],
attributes: attributes.data
};
const submit = () => handleFormSubmit(submitData, onSubmit, setChanged);
return {
change: handleChange,
data,
handlers: {
addStock: handleStockAdd,
changeMetadata,
changeStock: handleStockChange,
deleteStock: handleStockDelete,
selectAttribute: handleAttributeChange,
selectAttributeMultiple: handleAttributeMultiChange,
selectCategory: handleCategorySelect,
selectCollection: handleCollectionSelect,
selectTaxRate: handleTaxTypeSelect
},
hasChanged: changed,
submit
};
}
const ProductUpdateForm: React.FC<ProductUpdateFormProps> = ({
children,
product,
onSubmit,
...rest
}) => {
const props = useProductUpdateForm(product, onSubmit, rest);
return <form onSubmit={props.submit}>{children(props)}</form>;
};
ProductUpdateForm.displayName = "ProductUpdateForm";
export default ProductUpdateForm;

View file

@ -2,48 +2,23 @@ import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Metadata, { MetadataFormData } from "@saleor/components/Metadata"; import Metadata from "@saleor/components/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment"; import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
import useFormset, {
FormsetChange,
FormsetData
} from "@saleor/hooks/useFormset";
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 { ReorderAction } from "@saleor/types";
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";
import { maybe } from "../../../misc";
import { ProductVariantCreateData_product } from "../../types/ProductVariantCreateData"; import { ProductVariantCreateData_product } from "../../types/ProductVariantCreateData";
import ProductShipping from "../ProductShipping/ProductShipping"; import ProductShipping from "../ProductShipping/ProductShipping";
import ProductStocks, { ProductStockInput } from "../ProductStocks"; import ProductStocks from "../ProductStocks";
import ProductVariantAttributes, { import ProductVariantAttributes from "../ProductVariantAttributes";
VariantAttributeInputData
} from "../ProductVariantAttributes";
import ProductVariantNavigation from "../ProductVariantNavigation"; import ProductVariantNavigation from "../ProductVariantNavigation";
import ProductVariantPrice from "../ProductVariantPrice"; import ProductVariantPrice from "../ProductVariantPrice";
import ProductVariantCreateForm, { ProductVariantCreateData } from "./form";
interface ProductVariantCreatePageFormData extends MetadataFormData {
costPrice: string;
images: string[];
price: string;
quantity: string;
sku: string;
trackInventory: boolean;
weight: string;
}
export interface ProductVariantCreatePageSubmitData
extends ProductVariantCreatePageFormData {
attributes: FormsetData<VariantAttributeInputData>;
stocks: ProductStockInput[];
}
interface ProductVariantCreatePageProps { interface ProductVariantCreatePageProps {
currencySymbol: string; currencySymbol: string;
@ -55,7 +30,7 @@ interface ProductVariantCreatePageProps {
warehouses: SearchWarehouses_search_edges_node[]; warehouses: SearchWarehouses_search_edges_node[];
weightUnit: string; weightUnit: string;
onBack: () => void; onBack: () => void;
onSubmit: (data: ProductVariantCreatePageSubmitData) => void; onSubmit: (data: ProductVariantCreateData) => void;
onVariantClick: (variantId: string) => void; onVariantClick: (variantId: string) => void;
onVariantReorder: ReorderAction; onVariantReorder: ReorderAction;
onWarehouseConfigure: () => void; onWarehouseConfigure: () => void;
@ -77,144 +52,90 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
onWarehouseConfigure onWarehouseConfigure
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const attributeInput = React.useMemo(
() => getVariantAttributeInputFromProduct(product),
[product]
);
const { change: changeAttributeData, data: attributes } = useFormset(
attributeInput
);
const {
add: addStock,
change: changeStockData,
data: stocks,
remove: removeStock
} = useFormset<null, string>([]);
const {
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const initialForm: ProductVariantCreatePageFormData = {
costPrice: "",
images: product?.images.map(image => image.id),
metadata: [],
price: "",
privateMetadata: [],
quantity: "0",
sku: "",
trackInventory: true,
weight: ""
};
const handleSubmit = (data: ProductVariantCreatePageFormData) =>
onSubmit({
...data,
attributes,
stocks
});
return ( return (
<Form initial={initialForm} onSubmit={handleSubmit}> <ProductVariantCreateForm
{({ change, data, hasChanged, submit, triggerChange }) => { product={product}
const handleAttributeChange: FormsetChange = (id, value) => { onSubmit={onSubmit}
changeAttributeData(id, value); warehouses={warehouses}
triggerChange(); >
}; {({ change, data, handlers, hasChanged, submit }) => (
const changeMetadata = makeMetadataChangeHandler(change); <Container>
<AppHeader onBack={onBack}>{product?.name}</AppHeader>
return ( <PageHeader title={header} />
<Container> <Grid variant="inverted">
<AppHeader onBack={onBack}>{maybe(() => product.name)}</AppHeader> <div>
<PageHeader title={header} /> <ProductVariantNavigation
<Grid variant="inverted"> fallbackThumbnail={product?.thumbnail?.url}
<div> variants={product?.variants}
<ProductVariantNavigation onRowClick={(variantId: string) => {
fallbackThumbnail={maybe(() => product.thumbnail.url)} if (product && product.variants) {
variants={maybe(() => product.variants)} return onVariantClick(variantId);
onRowClick={(variantId: string) => { }
if (product && product.variants) { }}
return onVariantClick(variantId); onReorder={onVariantReorder}
} />
}} </div>
onReorder={onVariantReorder} <div>
/> <ProductVariantAttributes
</div> attributes={data.attributes}
<div> disabled={disabled}
<ProductVariantAttributes errors={errors}
attributes={attributes} onChange={handlers.selectAttribute}
disabled={disabled} />
errors={errors} <CardSpacer />
onChange={handleAttributeChange} <ProductVariantPrice
/> data={data}
<CardSpacer /> errors={errors}
<ProductVariantPrice currencySymbol={currencySymbol}
data={data} loading={disabled}
errors={errors} onChange={change}
currencySymbol={currencySymbol} />
loading={disabled} <CardSpacer />
onChange={change} <ProductShipping
/> data={data}
<CardSpacer /> disabled={disabled}
<ProductShipping errors={errors}
data={data} weightUnit={weightUnit}
disabled={disabled} onChange={change}
errors={errors} />
weightUnit={weightUnit} <CardSpacer />
onChange={change} <ProductStocks
/> data={data}
<CardSpacer /> disabled={disabled}
<ProductStocks hasVariants={true}
data={data} onFormDataChange={change}
disabled={disabled} errors={errors}
hasVariants={true} stocks={data.stocks}
onFormDataChange={change} warehouses={warehouses}
errors={errors} onChange={handlers.changeStock}
stocks={stocks} onWarehouseStockAdd={handlers.addStock}
warehouses={warehouses} onWarehouseStockDelete={handlers.deleteStock}
onChange={(id, value) => { onWarehouseConfigure={onWarehouseConfigure}
triggerChange(); />
changeStockData(id, value); <CardSpacer />
}} <Metadata data={data} onChange={handlers.changeMetadata} />
onWarehouseStockAdd={id => { </div>
triggerChange(); </Grid>
addStock({ <SaveButtonBar
data: null, disabled={disabled || !onSubmit || !hasChanged}
id, labels={{
label: warehouses.find(warehouse => warehouse.id === id) delete: intl.formatMessage({
.name, defaultMessage: "Delete Variant",
value: "0" description: "button"
}); }),
}} save: intl.formatMessage({
onWarehouseStockDelete={id => { defaultMessage: "Save variant",
triggerChange(); description: "button"
removeStock(id); })
}} }}
onWarehouseConfigure={onWarehouseConfigure} state={saveButtonBarState}
/> onCancel={onBack}
<CardSpacer /> onSave={submit}
<Metadata data={data} onChange={changeMetadata} /> />
</div> </Container>
</Grid> )}
<SaveButtonBar </ProductVariantCreateForm>
disabled={disabled || !onSubmit || !hasChanged}
labels={{
delete: intl.formatMessage({
defaultMessage: "Delete Variant",
description: "button"
}),
save: intl.formatMessage({
defaultMessage: "Save variant",
description: "button"
})
}}
state={saveButtonBarState}
onCancel={onBack}
onSave={submit}
/>
</Container>
);
}}
</Form>
); );
}; };
ProductVariantCreatePage.displayName = "ProductVariantCreatePage"; ProductVariantCreatePage.displayName = "ProductVariantCreatePage";

View file

@ -0,0 +1,140 @@
import { MetadataFormData } from "@saleor/components/Metadata";
import useForm, { FormChange } from "@saleor/hooks/useForm";
import useFormset, {
FormsetChange,
FormsetData
} from "@saleor/hooks/useFormset";
import { ProductVariantCreateData_product } from "@saleor/products/types/ProductVariantCreateData";
import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import React from "react";
import { ProductStockInput } from "../ProductStocks";
import { VariantAttributeInputData } from "../ProductVariantAttributes";
export interface ProductVariantCreateFormData extends MetadataFormData {
costPrice: string;
price: string;
sku: string;
trackInventory: boolean;
weight: string;
}
export interface ProductVariantCreateData extends ProductVariantCreateFormData {
attributes: FormsetData<VariantAttributeInputData, string>;
stocks: ProductStockInput[];
}
export interface UseProductVariantCreateFormOpts {
warehouses: SearchWarehouses_search_edges_node[];
}
export interface UseProductVariantCreateFormResult {
change: FormChange;
data: ProductVariantCreateData;
// TODO: type FormsetChange
handlers: Record<"changeStock" | "selectAttribute", FormsetChange> &
Record<"addStock" | "deleteStock", (id: string) => void> & {
changeMetadata: FormChange;
};
hasChanged: boolean;
submit: () => void;
}
export interface ProductVariantCreateFormProps
extends UseProductVariantCreateFormOpts {
children: (props: UseProductVariantCreateFormResult) => React.ReactNode;
product: ProductVariantCreateData_product;
onSubmit: (data: ProductVariantCreateData) => void;
}
const initial: ProductVariantCreateFormData = {
costPrice: "",
metadata: [],
price: "",
privateMetadata: [],
sku: "",
trackInventory: true,
weight: ""
};
function useProductVariantCreateForm(
product: ProductVariantCreateData_product,
onSubmit: (data: ProductVariantCreateData) => void,
opts: UseProductVariantCreateFormOpts
): UseProductVariantCreateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const attributeInput = getVariantAttributeInputFromProduct(product);
const form = useForm(initial);
const attributes = useFormset(attributeInput);
const stocks = useFormset<null, string>([]);
const {
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const changeMetadata = makeMetadataChangeHandler(handleChange);
const handleAttributeChange: FormsetChange = (id, value) => {
attributes.change(id, value);
triggerChange();
};
const handleStockAdd = (id: string) => {
triggerChange();
stocks.add({
data: null,
id,
label: opts.warehouses.find(warehouse => warehouse.id === id).name,
value: "0"
});
};
const handleStockChange = (id: string, value: string) => {
triggerChange();
stocks.change(id, value);
};
const handleStockDelete = (id: string) => {
triggerChange();
stocks.remove(id);
};
const data: ProductVariantCreateData = {
...form.data,
attributes: attributes.data,
stocks: stocks.data
};
const submit = () => onSubmit(data);
return {
change: handleChange,
data,
handlers: {
addStock: handleStockAdd,
changeMetadata,
changeStock: handleStockChange,
deleteStock: handleStockDelete,
selectAttribute: handleAttributeChange
},
hasChanged: changed,
submit
};
}
const ProductVariantCreateForm: React.FC<ProductVariantCreateFormProps> = ({
children,
product,
onSubmit,
...rest
}) => {
const props = useProductVariantCreateForm(product, onSubmit, rest);
return <form onSubmit={props.submit}>{children(props)}</form>;
};
ProductVariantCreateForm.displayName = "ProductVariantCreateForm";
export default ProductVariantCreateForm;

View file

@ -2,7 +2,6 @@ import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import { MetadataFormData } from "@saleor/components/Metadata"; import { MetadataFormData } from "@saleor/components/Metadata";
import Metadata from "@saleor/components/Metadata/Metadata"; import Metadata from "@saleor/components/Metadata/Metadata";
@ -11,19 +10,9 @@ import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment"; import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
import { ProductVariant } from "@saleor/fragments/types/ProductVariant"; import { ProductVariant } from "@saleor/fragments/types/ProductVariant";
import { WarehouseFragment } from "@saleor/fragments/types/WarehouseFragment"; import { WarehouseFragment } from "@saleor/fragments/types/WarehouseFragment";
import useFormset, { import { FormsetData } from "@saleor/hooks/useFormset";
FormsetChange,
FormsetData
} from "@saleor/hooks/useFormset";
import { VariantUpdate_productVariantUpdate_errors } from "@saleor/products/types/VariantUpdate"; import { VariantUpdate_productVariantUpdate_errors } from "@saleor/products/types/VariantUpdate";
import {
getAttributeInputFromVariant,
getStockInputFromVariant
} from "@saleor/products/utils/data";
import { ReorderAction } from "@saleor/types"; import { ReorderAction } from "@saleor/types";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { diff } from "fast-array-diff";
import React from "react"; import React from "react";
import { maybe } from "../../../misc"; import { maybe } from "../../../misc";
@ -37,6 +26,7 @@ import ProductVariantImageSelectDialog from "../ProductVariantImageSelectDialog"
import ProductVariantNavigation from "../ProductVariantNavigation"; import ProductVariantNavigation from "../ProductVariantNavigation";
import ProductVariantPrice from "../ProductVariantPrice"; import ProductVariantPrice from "../ProductVariantPrice";
import ProductVariantSetDefault from "../ProductVariantSetDefault"; import ProductVariantSetDefault from "../ProductVariantSetDefault";
import ProductVariantUpdateForm from "./form";
export interface ProductVariantPageFormData extends MetadataFormData { export interface ProductVariantPageFormData extends MetadataFormData {
costPrice: string; costPrice: string;
@ -97,77 +87,16 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
onSetDefaultVariant, onSetDefaultVariant,
onWarehouseConfigure onWarehouseConfigure
}) => { }) => {
const attributeInput = React.useMemo(
() => getAttributeInputFromVariant(variant),
[variant]
);
const stockInput = React.useMemo(() => getStockInputFromVariant(variant), [
variant
]);
const { change: changeAttributeData, data: attributes } = useFormset(
attributeInput
);
const {
add: addStock,
change: changeStockData,
data: stocks,
remove: removeStock
} = useFormset(stockInput);
const [isModalOpened, setModalStatus] = React.useState(false); const [isModalOpened, setModalStatus] = React.useState(false);
const toggleModal = () => setModalStatus(!isModalOpened); const toggleModal = () => setModalStatus(!isModalOpened);
const { const variantImages = variant?.images?.map(image => image.id);
isMetadataModified, const productImages = variant?.product?.images?.sort((prev, next) =>
isPrivateMetadataModified, prev.sortOrder > next.sortOrder ? 1 : -1
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const variantImages = maybe(() => variant.images.map(image => image.id), []);
const productImages = maybe(() =>
variant.product.images.sort((prev, next) =>
prev.sortOrder > next.sortOrder ? 1 : -1
)
); );
const images = maybe(() => const images = productImages
productImages ?.filter(image => variantImages.indexOf(image.id) !== -1)
.filter(image => variantImages.indexOf(image.id) !== -1) .sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1));
.sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1))
);
const initialForm: ProductVariantPageFormData = {
costPrice: variant?.costPrice?.amount.toString() || "",
metadata: variant?.metadata?.map(mapMetadataItemToInput),
price: variant?.price?.amount.toString() || "",
privateMetadata: variant?.privateMetadata?.map(mapMetadataItemToInput),
sku: variant?.sku || "",
trackInventory: !!variant?.trackInventory,
weight: variant?.weight?.value.toString() || ""
};
const handleSubmit = (data: ProductVariantPageFormData) => {
const dataStocks = stocks.map(stock => stock.id);
const variantStocks = variant.stocks.map(stock => stock.warehouse.id);
const stockDiff = diff(variantStocks, dataStocks);
const metadata = isMetadataModified ? data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? data.privateMetadata
: undefined;
onSubmit({
...data,
addStocks: stocks.filter(stock =>
stockDiff.added.some(addedStock => addedStock === stock.id)
),
attributes,
metadata,
privateMetadata,
removeStocks: stockDiff.removed,
updateStocks: stocks.filter(
stock => !stockDiff.added.some(addedStock => addedStock === stock.id)
)
});
};
return ( return (
<> <>
@ -182,116 +111,95 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
/> />
)} )}
</PageHeader> </PageHeader>
<Form initial={initialForm} onSubmit={handleSubmit} confirmLeave> <ProductVariantUpdateForm
{({ change, data, hasChanged, submit, triggerChange }) => { variant={variant}
const handleAttributeChange: FormsetChange = (id, value) => { onSubmit={onSubmit}
changeAttributeData(id, value); warehouses={warehouses}
triggerChange(); >
}; {({ change, data, handlers, hasChanged, submit }) => (
<>
const changeMetadata = makeMetadataChangeHandler(change); <Grid variant="inverted">
<div>
return ( <ProductVariantNavigation
<> current={variant ? variant.id : undefined}
<Grid variant="inverted"> defaultVariantId={defaultVariantId}
<div> fallbackThumbnail={maybe(
<ProductVariantNavigation () => variant.product.thumbnail.url
current={variant ? variant.id : undefined} )}
defaultVariantId={defaultVariantId} variants={maybe(() => variant.product.variants)}
fallbackThumbnail={maybe( onAdd={onAdd}
() => variant.product.thumbnail.url onRowClick={(variantId: string) => {
)} if (variant) {
variants={maybe(() => variant.product.variants)} return onVariantClick(variantId);
onAdd={onAdd}
onRowClick={(variantId: string) => {
if (variant) {
return onVariantClick(variantId);
}
}}
onReorder={onVariantReorder}
/>
</div>
<div>
<ProductVariantAttributes
attributes={attributes}
disabled={loading}
errors={errors}
onChange={handleAttributeChange}
/>
<CardSpacer />
<ProductVariantImages
disabled={loading}
images={images}
placeholderImage={placeholderImage}
onImageAdd={toggleModal}
/>
<CardSpacer />
<ProductVariantPrice
errors={errors}
data={data}
currencySymbol={
variant && variant.price
? variant.price.currency
: variant && variant.costPrice
? variant.costPrice.currency
: ""
} }
loading={loading} }}
onChange={change} onReorder={onVariantReorder}
/> />
<CardSpacer /> </div>
<ProductShipping <div>
data={data} <ProductVariantAttributes
disabled={loading} attributes={data.attributes}
errors={errors} disabled={loading}
weightUnit={variant?.weight?.unit || defaultWeightUnit} errors={errors}
onChange={change} onChange={handlers.selectAttribute}
/> />
<CardSpacer /> <CardSpacer />
<ProductStocks <ProductVariantImages
data={data} disabled={loading}
disabled={loading} images={images}
hasVariants={true} placeholderImage={placeholderImage}
errors={errors} onImageAdd={toggleModal}
stocks={stocks} />
warehouses={warehouses} <CardSpacer />
onChange={(id, value) => { <ProductVariantPrice
triggerChange(); data={data}
changeStockData(id, value); errors={errors}
}} currencySymbol={
onFormDataChange={change} variant && variant.price
onWarehouseStockAdd={id => { ? variant.price.currency
triggerChange(); : variant && variant.costPrice
addStock({ ? variant.costPrice.currency
data: null, : ""
id, }
label: warehouses.find( loading={loading}
warehouse => warehouse.id === id onChange={change}
).name, />
value: "0" <CardSpacer />
}); <ProductShipping
}} data={data}
onWarehouseStockDelete={id => { disabled={loading}
triggerChange(); errors={errors}
removeStock(id); weightUnit={variant?.weight?.unit || defaultWeightUnit}
}} onChange={change}
onWarehouseConfigure={onWarehouseConfigure} />
/> <CardSpacer />
<CardSpacer /> <ProductStocks
<Metadata data={data} onChange={changeMetadata} /> data={data}
</div> disabled={loading}
</Grid> hasVariants={true}
<SaveButtonBar errors={errors}
disabled={loading || !hasChanged} stocks={data.stocks}
state={saveButtonBarState} warehouses={warehouses}
onCancel={onBack} onChange={handlers.changeStock}
onDelete={onDelete} onFormDataChange={change}
onSave={submit} onWarehouseStockAdd={handlers.addStock}
/> onWarehouseStockDelete={handlers.deleteStock}
</> onWarehouseConfigure={onWarehouseConfigure}
); />
}} <CardSpacer />
</Form> <Metadata data={data} onChange={handlers.changeMetadata} />
</div>
</Grid>
<SaveButtonBar
disabled={loading || !hasChanged}
state={saveButtonBarState}
onCancel={onBack}
onDelete={onDelete}
onSave={submit}
/>
</>
)}
</ProductVariantUpdateForm>
</Container> </Container>
{variant && ( {variant && (
<ProductVariantImageSelectDialog <ProductVariantImageSelectDialog

View file

@ -0,0 +1,180 @@
import { MetadataFormData } from "@saleor/components/Metadata";
import { ProductVariant } from "@saleor/fragments/types/ProductVariant";
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm";
import useFormset, {
FormsetChange,
FormsetData
} from "@saleor/hooks/useFormset";
import {
getAttributeInputFromVariant,
getStockInputFromVariant
} from "@saleor/products/utils/data";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { diff } from "fast-array-diff";
import React from "react";
import handleFormSubmit from "../../../utils/handlers/handleFormSubmit";
import { ProductStockInput } from "../ProductStocks";
import { VariantAttributeInputData } from "../ProductVariantAttributes";
export interface ProductVariantUpdateFormData extends MetadataFormData {
costPrice: string;
price: string;
sku: string;
trackInventory: boolean;
weight: string;
}
export interface ProductVariantUpdateData extends ProductVariantUpdateFormData {
attributes: FormsetData<VariantAttributeInputData, string>;
stocks: ProductStockInput[];
}
export interface ProductVariantUpdateSubmitData
extends ProductVariantUpdateFormData {
attributes: FormsetData<VariantAttributeInputData, string>;
addStocks: ProductStockInput[];
updateStocks: ProductStockInput[];
removeStocks: string[];
}
export interface UseProductVariantUpdateFormOpts {
warehouses: SearchWarehouses_search_edges_node[];
}
export interface UseProductVariantUpdateFormResult {
change: FormChange;
data: ProductVariantUpdateData;
handlers: Record<"changeStock" | "selectAttribute", FormsetChange> &
Record<"addStock" | "deleteStock", (id: string) => void> & {
changeMetadata: FormChange;
};
hasChanged: boolean;
submit: () => void;
}
export interface ProductVariantUpdateFormProps
extends UseProductVariantUpdateFormOpts {
children: (props: UseProductVariantUpdateFormResult) => React.ReactNode;
variant: ProductVariant;
onSubmit: (data: ProductVariantUpdateSubmitData) => SubmitPromise;
}
function useProductVariantUpdateForm(
variant: ProductVariant,
onSubmit: (data: ProductVariantUpdateSubmitData) => SubmitPromise,
opts: UseProductVariantUpdateFormOpts
): UseProductVariantUpdateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const attributeInput = getAttributeInputFromVariant(variant);
const stockInput = getStockInputFromVariant(variant);
const initial: ProductVariantUpdateFormData = {
costPrice: variant?.costPrice?.amount.toString() || "",
metadata: variant?.metadata?.map(mapMetadataItemToInput),
price: variant?.price?.amount.toString() || "",
privateMetadata: variant?.privateMetadata?.map(mapMetadataItemToInput),
sku: variant?.sku || "",
trackInventory: variant?.trackInventory,
weight: variant?.weight?.value.toString() || ""
};
const form = useForm(initial);
const attributes = useFormset(attributeInput);
const stocks = useFormset(stockInput);
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const handleChange: FormChange = (event, cb) => {
form.change(event, cb);
triggerChange();
};
const changeMetadata = makeMetadataChangeHandler(handleChange);
const handleAttributeChange: FormsetChange = (id, value) => {
attributes.change(id, value);
triggerChange();
};
const handleStockAdd = (id: string) => {
triggerChange();
stocks.add({
data: null,
id,
label: opts.warehouses.find(warehouse => warehouse.id === id).name,
value: "0"
});
};
const handleStockChange = (id: string, value: string) => {
triggerChange();
stocks.change(id, value);
};
const handleStockDelete = (id: string) => {
triggerChange();
stocks.remove(id);
};
const metadata = isMetadataModified ? form.data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? form.data.privateMetadata
: undefined;
const dataStocks = stocks.data.map(stock => stock.id);
const variantStocks = variant?.stocks.map(stock => stock.warehouse.id) || [];
const stockDiff = diff(variantStocks, dataStocks);
const addStocks = stocks.data.filter(stock =>
stockDiff.added.some(addedStock => addedStock === stock.id)
);
const updateStocks = stocks.data.filter(
stock => !stockDiff.added.some(addedStock => addedStock === stock.id)
);
const data: ProductVariantUpdateData = {
...form.data,
attributes: attributes.data,
stocks: stocks.data
};
const submitData: ProductVariantUpdateSubmitData = {
...form.data,
addStocks,
attributes: attributes.data,
metadata,
privateMetadata,
removeStocks: stockDiff.removed,
updateStocks
};
const submit = () => handleFormSubmit(submitData, onSubmit, setChanged);
return {
change: handleChange,
data,
handlers: {
addStock: handleStockAdd,
changeMetadata,
changeStock: handleStockChange,
deleteStock: handleStockDelete,
selectAttribute: handleAttributeChange
},
hasChanged: changed,
submit
};
}
const ProductVariantUpdateForm: React.FC<ProductVariantUpdateFormProps> = ({
children,
variant,
onSubmit,
...rest
}) => {
const props = useProductVariantUpdateForm(variant, onSubmit, rest);
return <form onSubmit={props.submit}>{children(props)}</form>;
};
ProductVariantUpdateForm.displayName = "ProductVariantUpdateForm";
export default ProductVariantUpdateForm;

View file

@ -123,16 +123,14 @@ export function getStockInputFromVariant(
export function getVariantAttributeInputFromProduct( export function getVariantAttributeInputFromProduct(
product: ProductVariantCreateData_product product: ProductVariantCreateData_product
): VariantAttributeInput[] { ): VariantAttributeInput[] {
return maybe(() => return product?.productType?.variantAttributes?.map(attribute => ({
product.productType.variantAttributes.map(attribute => ({ data: {
data: { values: attribute.values
values: attribute.values },
}, id: attribute.id,
id: attribute.id, label: attribute.name,
label: attribute.name, value: ""
value: "" }));
}))
);
} }
export function getStockInputFromProduct( export function getStockInputFromProduct(

View file

@ -8,7 +8,7 @@ import { getAttributeInputFromProductType, ProductType } from "./data";
export function createAttributeChangeHandler( export function createAttributeChangeHandler(
changeAttributeData: FormsetChange<string[]>, changeAttributeData: FormsetChange<string[]>,
triggerChange: () => void triggerChange: () => void
): FormsetChange { ): FormsetChange<string> {
return (attributeId: string, value: string) => { return (attributeId: string, value: string) => {
triggerChange(); triggerChange();
changeAttributeData(attributeId, value === "" ? [] : [value]); changeAttributeData(attributeId, value === "" ? [] : [value]);
@ -19,7 +19,7 @@ export function createAttributeMultiChangeHandler(
changeAttributeData: FormsetChange<string[]>, changeAttributeData: FormsetChange<string[]>,
attributes: FormsetData<ProductAttributeInputData, string[]>, attributes: FormsetData<ProductAttributeInputData, string[]>,
triggerChange: () => void triggerChange: () => void
): FormsetChange { ): FormsetChange<string> {
return (attributeId: string, value: string) => { return (attributeId: string, value: string) => {
const attribute = attributes.find( const attribute = attributes.find(
attribute => attribute.id === attributeId attribute => attribute.id === attributeId
@ -37,19 +37,18 @@ export function createAttributeMultiChangeHandler(
} }
export function createProductTypeSelectHandler( export function createProductTypeSelectHandler(
change: FormChange,
setAttributes: (data: FormsetData<ProductAttributeInputData>) => void, setAttributes: (data: FormsetData<ProductAttributeInputData>) => void,
setProductType: (productType: ProductType) => void, setProductType: (productType: ProductType) => void,
productTypeChoiceList: ProductType[] productTypeChoiceList: ProductType[],
triggerChange: () => void
): FormChange { ): FormChange {
return (event: React.ChangeEvent<any>) => { return (event: React.ChangeEvent<any>) => {
const id = event.target.value; const id = event.target.value;
const selectedProductType = productTypeChoiceList.find( const selectedProductType = productTypeChoiceList.find(
productType => productType.id === id productType => productType.id === id
); );
triggerChange();
setProductType(selectedProductType); setProductType(selectedProductType);
change(event);
setAttributes(getAttributeInputFromProductType(selectedProductType)); setAttributes(getAttributeInputFromProductType(selectedProductType));
}; };
} }

View file

@ -19,9 +19,8 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { decimal, weight } from "../../misc"; import { decimal, weight } from "../../misc";
import ProductCreatePage, { import ProductCreatePage from "../components/ProductCreatePage";
ProductCreatePageSubmitData import { ProductCreateData } from "../components/ProductCreatePage/form";
} from "../components/ProductCreatePage";
import { import {
useProductCreateMutation, useProductCreateMutation,
useProductSetAvailabilityForPurchase useProductSetAvailabilityForPurchase
@ -91,7 +90,7 @@ export const ProductCreateView: React.FC = () => {
} }
}); });
const handleCreate = async (formData: ProductCreatePageSubmitData) => { const handleCreate = async (formData: ProductCreateData) => {
const result = await productCreate({ const result = await productCreate({
variables: { variables: {
input: { input: {
@ -106,7 +105,7 @@ export const ProductCreateView: React.FC = () => {
descriptionJson: JSON.stringify(formData.description), descriptionJson: JSON.stringify(formData.description),
isPublished: formData.isPublished, isPublished: formData.isPublished,
name: formData.name, name: formData.name,
productType: formData.productType, productType: formData.productType?.id,
publicationDate: publicationDate:
formData.publicationDate !== "" ? formData.publicationDate : null, formData.publicationDate !== "" ? formData.publicationDate : null,
seo: { seo: {

View file

@ -15,9 +15,8 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { decimal, weight } from "../../misc"; import { decimal, weight } from "../../misc";
import ProductVariantCreatePage, { import ProductVariantCreatePage from "../components/ProductVariantCreatePage";
ProductVariantCreatePageSubmitData import { ProductVariantCreateData } from "../components/ProductVariantCreatePage/form";
} from "../components/ProductVariantCreatePage";
import { import {
useProductVariantReorderMutation, useProductVariantReorderMutation,
useVariantCreateMutation useVariantCreateMutation
@ -84,7 +83,7 @@ export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
); );
const handleBack = () => navigate(productUrl(productId)); const handleBack = () => navigate(productUrl(productId));
const handleCreate = async (formData: ProductVariantCreatePageSubmitData) => { const handleCreate = async (formData: ProductVariantCreateData) => {
const result = await variantCreate({ const result = await variantCreate({
variables: { variables: {
input: { input: {

View file

@ -14,6 +14,7 @@ import {
ShippingZoneDetailsFragment, ShippingZoneDetailsFragment,
ShippingZoneDetailsFragment_warehouses ShippingZoneDetailsFragment_warehouses
} from "@saleor/fragments/types/ShippingZoneDetailsFragment"; } from "@saleor/fragments/types/ShippingZoneDetailsFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import React from "react"; import React from "react";
@ -46,7 +47,7 @@ export interface ShippingZoneDetailsPageProps
onPriceRateAdd: () => void; onPriceRateAdd: () => void;
onPriceRateEdit: (id: string) => void; onPriceRateEdit: (id: string) => void;
onRateRemove: (rateId: string) => void; onRateRemove: (rateId: string) => void;
onSubmit: (data: FormData) => void; onSubmit: (data: FormData) => SubmitPromise;
onWarehouseAdd: () => void; onWarehouseAdd: () => void;
onWeightRateAdd: () => void; onWeightRateAdd: () => void;
onWeightRateEdit: (id: string) => void; onWeightRateEdit: (id: string) => void;

View file

@ -148,13 +148,13 @@ const ShippingZoneDetails: React.FC<ShippingZoneDetailsProps> = ({
} }
}); });
const handleSubmit = (submitData: FormData) => { const handleSubmit = async (submitData: FormData) => {
const warehouseDiff = diff( const warehouseDiff = diff(
data.shippingZone.warehouses.map(warehouse => warehouse.id), data.shippingZone.warehouses.map(warehouse => warehouse.id),
submitData.warehouses submitData.warehouses
); );
updateShippingZone({ const result = await updateShippingZone({
variables: { variables: {
id, id,
input: { input: {
@ -164,6 +164,8 @@ const ShippingZoneDetails: React.FC<ShippingZoneDetailsProps> = ({
} }
} }
}); });
return result.data.shippingZoneUpdate.errors;
}; };
if (data?.shippingZone === null) { if (data?.shippingZone === null) {

View file

@ -11,6 +11,7 @@ import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ShopErrorFragment } from "@saleor/fragments/types/ShopErrorFragment"; import { ShopErrorFragment } from "@saleor/fragments/types/ShopErrorFragment";
import useAddressValidation from "@saleor/hooks/useAddressValidation"; import useAddressValidation from "@saleor/hooks/useAddressValidation";
import { SubmitPromise } from "@saleor/hooks/useForm";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { commonMessages, sectionNames } from "@saleor/intl"; import { commonMessages, sectionNames } from "@saleor/intl";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
@ -54,7 +55,7 @@ export interface SiteSettingsPageProps {
onBack: () => void; onBack: () => void;
onKeyAdd: () => void; onKeyAdd: () => void;
onKeyRemove: (keyType: AuthorizationKeyType) => void; onKeyRemove: (keyType: AuthorizationKeyType) => void;
onSubmit: (data: SiteSettingsPageFormData) => void; onSubmit: (data: SiteSettingsPageFormData) => SubmitPromise;
} }
export function areAddressInputFieldsModified( export function areAddressInputFieldsModified(
@ -105,7 +106,7 @@ const SiteSettingsPage: React.FC<SiteSettingsPageProps> = props => {
const { const {
errors: validationErrors, errors: validationErrors,
submit: handleSubmitWithAddress submit: handleSubmitWithAddress
} = useAddressValidation<SiteSettingsPageFormData>(onSubmit); } = useAddressValidation(onSubmit);
const initialFormAddress: SiteSettingsPageAddressFormData = { const initialFormAddress: SiteSettingsPageAddressFormData = {
city: maybe(() => shop.companyAddress.city, ""), city: maybe(() => shop.companyAddress.city, ""),
@ -134,7 +135,7 @@ const SiteSettingsPage: React.FC<SiteSettingsPageProps> = props => {
const submitFunc = areAddressInputFieldsModified(data) const submitFunc = areAddressInputFieldsModified(data)
? handleSubmitWithAddress ? handleSubmitWithAddress
: onSubmit; : onSubmit;
submitFunc(data); return submitFunc(data);
}} }}
confirmLeave confirmLeave
> >

View file

@ -106,7 +106,7 @@ export const SiteSettings: React.FC<SiteSettingsProps> = ({ params }) => {
keyType: data.type keyType: data.type
} }
}); });
const handleUpdateShopSettings = ( const handleUpdateShopSettings = async (
data: SiteSettingsPageFormData data: SiteSettingsPageFormData
) => { ) => {
const addressInput = areAddressInputFieldsModified(data) const addressInput = areAddressInputFieldsModified(data)
@ -123,7 +123,7 @@ export const SiteSettings: React.FC<SiteSettingsProps> = ({ params }) => {
: { : {
companyName: data.companyName companyName: data.companyName
}; };
updateShopSettings({ const result = await updateShopSettings({
variables: { variables: {
addressInput, addressInput,
shopDomainInput: { shopDomainInput: {
@ -139,6 +139,12 @@ export const SiteSettings: React.FC<SiteSettingsProps> = ({ params }) => {
} }
} }
}); });
return [
...result.data.shopAddressUpdate.errors,
...result.data.shopDomainUpdate.errors,
...result.data.shopSettingsUpdate.errors
];
}; };
return ( return (

View file

@ -12,6 +12,7 @@ import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocomplet
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { StaffErrorFragment } from "@saleor/fragments/types/StaffErrorFragment"; import { StaffErrorFragment } from "@saleor/fragments/types/StaffErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import useLocale from "@saleor/hooks/useLocale"; import useLocale from "@saleor/hooks/useLocale";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
@ -50,7 +51,7 @@ export interface StaffDetailsPageProps extends SearchPageProps {
onChangePassword: () => void; onChangePassword: () => void;
onDelete: () => void; onDelete: () => void;
onImageDelete: () => void; onImageDelete: () => void;
onSubmit: (data: StaffDetailsFormData) => void; onSubmit: (data: StaffDetailsFormData) => SubmitPromise;
onImageUpload(file: File); onImageUpload(file: File);
} }

View file

@ -12,7 +12,9 @@ import usePermissionGroupSearch from "@saleor/searches/usePermissionGroupSearch"
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import StaffDetailsPage from "../components/StaffDetailsPage/StaffDetailsPage"; import StaffDetailsPage, {
StaffDetailsFormData
} from "../components/StaffDetailsPage/StaffDetailsPage";
import StaffPasswordResetDialog from "../components/StaffPasswordResetDialog"; import StaffPasswordResetDialog from "../components/StaffPasswordResetDialog";
import { import {
TypedStaffAvatarDeleteMutation, TypedStaffAvatarDeleteMutation,
@ -122,168 +124,175 @@ export const StaffDetails: React.FC<OrderListProps> = ({ id, params }) => {
return ( return (
<TypedStaffMemberUpdateMutation onCompleted={handleStaffMemberUpdate}> <TypedStaffMemberUpdateMutation onCompleted={handleStaffMemberUpdate}>
{(updateStaffMember, updateResult) => ( {(updateStaffMember, updateResult) => {
<TypedStaffMemberDeleteMutation const handleSubmit = async (formData: StaffDetailsFormData) => {
variables={{ id }} const result = await updateStaffMember({
onCompleted={handleStaffMemberDelete} variables: {
> id,
{(deleteStaffMember, deleteResult) => ( input: {
<TypedStaffAvatarUpdateMutation email: formData.email,
onCompleted={handleStaffMemberAvatarUpdate} firstName: formData.firstName,
> isActive: formData.isActive,
{updateStaffAvatar => ( lastName: formData.lastName,
<TypedStaffAvatarDeleteMutation ...groupsDiff(data?.user, formData)
onCompleted={handleStaffMemberAvatarDelete} }
> }
{(deleteStaffAvatar, deleteAvatarResult) => { });
const isUserSameAsViewer =
user.user?.id === data?.user?.id;
return ( return result.data.staffUpdate.errors;
<> };
<WindowTitle
title={getStringOrPlaceholder( return (
staffMember?.email <TypedStaffMemberDeleteMutation
)} variables={{ id }}
/> onCompleted={handleStaffMemberDelete}
<StaffDetailsPage >
errors={ {(deleteStaffMember, deleteResult) => (
updateResult?.data?.staffUpdate?.errors || [] <TypedStaffAvatarUpdateMutation
} onCompleted={handleStaffMemberAvatarUpdate}
canEditAvatar={isUserSameAsViewer} >
canEditPreferences={isUserSameAsViewer} {updateStaffAvatar => (
canEditStatus={!isUserSameAsViewer} <TypedStaffAvatarDeleteMutation
canRemove={!isUserSameAsViewer} onCompleted={handleStaffMemberAvatarDelete}
disabled={loading} >
onBack={handleBack} {(deleteStaffAvatar, deleteAvatarResult) => {
initialSearch="" const isUserSameAsViewer =
onChangePassword={() => user.user?.id === data?.user?.id;
navigate(
staffMemberDetailsUrl(id, { return (
action: "change-password" <>
}) <WindowTitle
) title={getStringOrPlaceholder(
} staffMember?.email
onDelete={() => )}
navigate( />
staffMemberDetailsUrl(id, { <StaffDetailsPage
action: "remove" errors={
}) updateResult?.data?.staffUpdate?.errors ||
) []
} }
onSubmit={variables => { canEditAvatar={isUserSameAsViewer}
updateStaffMember({ canEditPreferences={isUserSameAsViewer}
variables: { canEditStatus={!isUserSameAsViewer}
id, canRemove={!isUserSameAsViewer}
input: { disabled={loading}
email: variables.email, onBack={handleBack}
firstName: variables.firstName, initialSearch=""
isActive: variables.isActive, onChangePassword={() =>
lastName: variables.lastName, navigate(
...groupsDiff(data?.user, variables) staffMemberDetailsUrl(id, {
action: "change-password"
})
)
}
onDelete={() =>
navigate(
staffMemberDetailsUrl(id, {
action: "remove"
})
)
}
onSubmit={handleSubmit}
onImageUpload={file =>
updateStaffAvatar({
variables: {
image: file
} }
}
});
}}
onImageUpload={file =>
updateStaffAvatar({
variables: {
image: file
}
})
}
onImageDelete={() =>
navigate(
staffMemberDetailsUrl(id, {
action: "remove-avatar"
}) })
) }
} onImageDelete={() =>
availablePermissionGroups={searchPermissionGroupsOpts.data?.search.edges.map( navigate(
edge => edge.node staffMemberDetailsUrl(id, {
)} action: "remove-avatar"
staffMember={staffMember} })
saveButtonBarState={updateResult.status} )
fetchMorePermissionGroups={{ }
hasMore: availablePermissionGroups={searchPermissionGroupsOpts.data?.search.edges.map(
searchPermissionGroupsOpts.data?.search edge => edge.node
.pageInfo.hasNextPage, )}
loading: searchPermissionGroupsOpts.loading, staffMember={staffMember}
onFetchMore: loadMorePermissionGroups saveButtonBarState={updateResult.status}
}} fetchMorePermissionGroups={{
onSearchChange={searchPermissionGroups} hasMore:
/> searchPermissionGroupsOpts.data?.search
<ActionDialog .pageInfo.hasNextPage,
open={params.action === "remove"} loading: searchPermissionGroupsOpts.loading,
title={intl.formatMessage({ onFetchMore: loadMorePermissionGroups
defaultMessage: "delete Staff User", }}
description: "dialog header" onSearchChange={searchPermissionGroups}
})} />
confirmButtonState={deleteResult.status} <ActionDialog
variant="delete" open={params.action === "remove"}
onClose={closeModal} title={intl.formatMessage({
onConfirm={deleteStaffMember} defaultMessage: "delete Staff User",
> description: "dialog header"
<DialogContentText> })}
<FormattedMessage confirmButtonState={deleteResult.status}
defaultMessage="Are you sure you want to delete {email} from staff members?" variant="delete"
values={{ onClose={closeModal}
email: getStringOrPlaceholder( onConfirm={deleteStaffMember}
data?.user?.email >
) <DialogContentText>
}} <FormattedMessage
/> defaultMessage="Are you sure you want to delete {email} from staff members?"
</DialogContentText> values={{
</ActionDialog> email: getStringOrPlaceholder(
<ActionDialog data?.user?.email
open={params.action === "remove-avatar"} )
title={intl.formatMessage({ }}
defaultMessage: "Delete Staff User Avatar", />
description: "dialog header" </DialogContentText>
})} </ActionDialog>
confirmButtonState={deleteAvatarResult.status} <ActionDialog
variant="delete" open={params.action === "remove-avatar"}
onClose={closeModal} title={intl.formatMessage({
onConfirm={deleteStaffAvatar} defaultMessage: "Delete Staff User Avatar",
> description: "dialog header"
<DialogContentText> })}
<FormattedMessage confirmButtonState={deleteAvatarResult.status}
defaultMessage="Are you sure you want to remove {email} avatar?" variant="delete"
values={{ onClose={closeModal}
email: ( onConfirm={deleteStaffAvatar}
<strong> >
{getStringOrPlaceholder( <DialogContentText>
data?.user?.email <FormattedMessage
)} defaultMessage="Are you sure you want to remove {email} avatar?"
</strong> values={{
) email: (
}} <strong>
/> {getStringOrPlaceholder(
</DialogContentText> data?.user?.email
</ActionDialog> )}
<StaffPasswordResetDialog </strong>
confirmButtonState={changePasswordOpts.status} )
errors={ }}
changePasswordOpts?.data?.passwordChange />
?.errors || [] </DialogContentText>
} </ActionDialog>
open={params.action === "change-password"} <StaffPasswordResetDialog
onClose={closeModal} confirmButtonState={changePasswordOpts.status}
onSubmit={data => errors={
changePassword({ changePasswordOpts?.data?.passwordChange
variables: data ?.errors || []
}) }
} open={params.action === "change-password"}
/> onClose={closeModal}
</> onSubmit={data =>
); changePassword({
}} variables: data
</TypedStaffAvatarDeleteMutation> })
)} }
</TypedStaffAvatarUpdateMutation> />
)} </>
</TypedStaffMemberDeleteMutation> );
)} }}
</TypedStaffAvatarDeleteMutation>
)}
</TypedStaffAvatarUpdateMutation>
)}
</TypedStaffMemberDeleteMutation>
);
}}
</TypedStaffMemberUpdateMutation> </TypedStaffMemberUpdateMutation>
); );
}} }}

View file

@ -2,6 +2,7 @@ import placeholderImage from "@assets/images/placeholder60x60.png";
import AssignProductDialog, { import AssignProductDialog, {
AssignProductDialogProps AssignProductDialogProps
} from "@saleor/components/AssignProductDialog"; } from "@saleor/components/AssignProductDialog";
import { fetchMoreProps } from "@saleor/fixtures";
import { products } from "@saleor/products/fixtures"; import { products } from "@saleor/products/fixtures";
import { storiesOf } from "@storybook/react"; import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
@ -9,6 +10,7 @@ import React from "react";
import Decorator from "../../Decorator"; import Decorator from "../../Decorator";
const props: AssignProductDialogProps = { const props: AssignProductDialogProps = {
...fetchMoreProps,
confirmButtonState: "default", confirmButtonState: "default",
loading: false, loading: false,
onClose: () => undefined, onClose: () => undefined,

View file

@ -3,7 +3,7 @@ import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import VoucherDetailsPage, { import VoucherDetailsPage, {
FormData, VoucherDetailsPageFormData,
VoucherDetailsPageProps, VoucherDetailsPageProps,
VoucherDetailsPageTab VoucherDetailsPageTab
} from "../../../discounts/components/VoucherDetailsPage"; } from "../../../discounts/components/VoucherDetailsPage";
@ -59,7 +59,7 @@ storiesOf("Views / Discounts / Voucher details", module)
"type", "type",
"usageLimit", "usageLimit",
"discountValue" "discountValue"
] as Array<keyof FormData>).map(field => ({ ] as Array<keyof VoucherDetailsPageFormData>).map(field => ({
__typename: "DiscountError", __typename: "DiscountError",
code: DiscountErrorCode.INVALID, code: DiscountErrorCode.INVALID,
field field

View file

@ -3,7 +3,7 @@ import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import PageDetailsPage, { import PageDetailsPage, {
FormData, PageDetailsPageFormData,
PageDetailsPageProps PageDetailsPageProps
} from "../../../pages/components/PageDetailsPage"; } from "../../../pages/components/PageDetailsPage";
import { page } from "../../../pages/fixtures"; import { page } from "../../../pages/fixtures";
@ -36,7 +36,7 @@ storiesOf("Views / Pages / Page details", module)
"isPublished", "isPublished",
"seoDescription", "seoDescription",
"seoTitle" "seoTitle"
] as Array<keyof FormData>).map(field => ({ ] as Array<keyof PageDetailsPageFormData>).map(field => ({
__typename: "PageError", __typename: "PageError",
code: PageErrorCode.INVALID, code: PageErrorCode.INVALID,
field field

View file

@ -3,7 +3,7 @@ import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import PluginsDetailsPage, { import PluginsDetailsPage, {
FormData, PluginDetailsPageFormData,
PluginsDetailsPageProps PluginsDetailsPageProps
} from "../../../plugins/components/PluginsDetailsPage"; } from "../../../plugins/components/PluginsDetailsPage";
import { plugin } from "../../../plugins/fixtures"; import { plugin } from "../../../plugins/fixtures";
@ -31,7 +31,7 @@ storiesOf("Views / Plugins / Plugin details", module)
{...props} {...props}
errors={[ errors={[
...(["active", "Username or account", "Password or license"] as Array< ...(["active", "Username or account", "Password or license"] as Array<
keyof FormData keyof PluginDetailsPageFormData
>).map(field => ({ >).map(field => ({
__typename: "PluginError" as "PluginError", __typename: "PluginError" as "PluginError",
code: PluginErrorCode.INVALID, code: PluginErrorCode.INVALID,

View file

@ -5,7 +5,7 @@ import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import ProductCreatePage, { import ProductCreatePage, {
ProductCreatePageSubmitData ProductCreateFormData
} from "../../../products/components/ProductCreatePage"; } from "../../../products/components/ProductCreatePage";
import { product as productFixture } from "../../../products/fixtures"; import { product as productFixture } from "../../../products/fixtures";
import { productTypes } from "../../../productTypes/fixtures"; import { productTypes } from "../../../productTypes/fixtures";
@ -74,17 +74,15 @@ storiesOf("Views / Products / Create product", module)
"productType", "productType",
"category", "category",
"sku" "sku"
] as Array<keyof ProductCreatePageSubmitData | "attributes">).map( ] as Array<keyof ProductCreateFormData | "attributes">).map(field => ({
field => ({ __typename: "ProductError",
__typename: "ProductError", attributes:
attributes: field === "attributes"
field === "attributes" ? [productTypes[0].productAttributes[0].id]
? [productTypes[0].productAttributes[0].id] : null,
: null, code: ProductErrorCode.INVALID,
code: ProductErrorCode.INVALID, field
field }))}
})
)}
header="Add product" header="Add product"
collections={product.collections} collections={product.collections}
fetchCategories={() => undefined} fetchCategories={() => undefined}
@ -94,7 +92,7 @@ storiesOf("Views / Products / Create product", module)
fetchMoreCollections={fetchMoreProps} fetchMoreCollections={fetchMoreProps}
fetchMoreProductTypes={fetchMoreProps} fetchMoreProductTypes={fetchMoreProps}
initial={{ initial={{
productType: productTypes[0].id productType: productTypes[0]
}} }}
productTypes={productTypes} productTypes={productTypes}
categories={[product.category]} categories={[product.category]}

View file

@ -0,0 +1,28 @@
import makeStyles from "@material-ui/core/styles/makeStyles";
const useScrollableDialogStyle = makeStyles(
theme => ({
content: {
overflowY: "hidden"
},
dialog: {
height: "calc(100% - 64px)",
maxHeight: 700
},
loadMoreLoaderContainer: {
alignItems: "center",
display: "flex",
height: theme.spacing(3),
justifyContent: "center",
marginTop: theme.spacing(3)
},
scrollArea: {
overflowY: "scroll"
}
}),
{
name: "ScrollableDialog"
}
);
export default useScrollableDialogStyle;

View file

@ -0,0 +1,18 @@
import { SubmitPromise } from "@saleor/hooks/useForm";
async function handleFormSubmit<T>(
data: T,
onSubmit: (data: T) => SubmitPromise,
setChanged: (changed: boolean) => void
): Promise<boolean> {
const errors = await onSubmit(data);
const ok = errors.length === 0;
if (ok) {
setChanged(false);
}
return ok;
}
export default handleFormSubmit;

View file

@ -1,4 +1,5 @@
import { MetadataFormData } from "@saleor/components/Metadata/types"; import { MetadataFormData } from "@saleor/components/Metadata/types";
import { MetadataErrorFragment } from "@saleor/fragments/types/MetadataErrorFragment";
import { MetadataInput } from "@saleor/types/globalTypes"; import { MetadataInput } from "@saleor/types/globalTypes";
import { diff } from "fast-array-diff"; import { diff } from "fast-array-diff";
import { MutationFetchResult } from "react-apollo"; import { MutationFetchResult } from "react-apollo";
@ -28,7 +29,9 @@ function createMetadataUpdateHandler<TData extends MetadataFormData, TError>(
variables: UpdatePrivateMetadataVariables variables: UpdatePrivateMetadataVariables
) => Promise<MutationFetchResult<UpdatePrivateMetadata>> ) => Promise<MutationFetchResult<UpdatePrivateMetadata>>
) { ) {
return async (data: TData) => { return async (
data: TData
): Promise<Array<MetadataErrorFragment | TError>> => {
const errors = await update(data); const errors = await update(data);
if (errors.length > 0) { if (errors.length > 0) {

View file

@ -58,7 +58,7 @@ const WarehouseCreatePage: React.FC<WarehouseCreatePageProps> = ({
const { const {
errors: validationErrors, errors: validationErrors,
submit: handleSubmit submit: handleSubmit
} = useAddressValidation<WarehouseCreatePageFormData>(onSubmit); } = useAddressValidation(onSubmit);
return ( return (
<Form initial={initialForm} onSubmit={handleSubmit}> <Form initial={initialForm} onSubmit={handleSubmit}>

View file

@ -11,6 +11,7 @@ import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo"
import { AddressTypeInput } from "@saleor/customers/types"; import { AddressTypeInput } from "@saleor/customers/types";
import { WarehouseErrorFragment } from "@saleor/fragments/types/WarehouseErrorFragment"; import { WarehouseErrorFragment } from "@saleor/fragments/types/WarehouseErrorFragment";
import useAddressValidation from "@saleor/hooks/useAddressValidation"; import useAddressValidation from "@saleor/hooks/useAddressValidation";
import { SubmitPromise } from "@saleor/hooks/useForm";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { findValueInEnum, maybe } from "@saleor/misc"; import { findValueInEnum, maybe } from "@saleor/misc";
@ -36,7 +37,7 @@ export interface WarehouseDetailsPageProps {
onBack: () => void; onBack: () => void;
onDelete: () => void; onDelete: () => void;
onShippingZoneClick: (id: string) => void; onShippingZoneClick: (id: string) => void;
onSubmit: (data: WarehouseDetailsPageFormData) => void; onSubmit: (data: WarehouseDetailsPageFormData) => SubmitPromise;
} }
const WarehouseDetailsPage: React.FC<WarehouseDetailsPageProps> = ({ const WarehouseDetailsPage: React.FC<WarehouseDetailsPageProps> = ({
@ -58,7 +59,7 @@ const WarehouseDetailsPage: React.FC<WarehouseDetailsPageProps> = ({
const { const {
errors: validationErrors, errors: validationErrors,
submit: handleSubmit submit: handleSubmit
} = useAddressValidation<WarehouseDetailsPageFormData>(onSubmit); } = useAddressValidation(onSubmit);
const initialForm: WarehouseDetailsPageFormData = { const initialForm: WarehouseDetailsPageFormData = {
city: maybe(() => warehouse.address.city, ""), city: maybe(() => warehouse.address.city, ""),
@ -76,7 +77,7 @@ const WarehouseDetailsPage: React.FC<WarehouseDetailsPageProps> = ({
return ( return (
<Form initial={initialForm} onSubmit={handleSubmit}> <Form initial={initialForm} onSubmit={handleSubmit}>
{({ change, data, submit }) => { {({ change, data, hasChanged, submit }) => {
const countryChoices = mapCountriesToChoices(countries); const countryChoices = mapCountriesToChoices(countries);
const handleCountryChange = createSingleAutocompleteSelectHandler( const handleCountryChange = createSingleAutocompleteSelectHandler(
change, change,
@ -121,7 +122,7 @@ const WarehouseDetailsPage: React.FC<WarehouseDetailsPageProps> = ({
</div> </div>
</Grid> </Grid>
<SaveButtonBar <SaveButtonBar
disabled={disabled} disabled={disabled || !hasChanged}
onCancel={onBack} onCancel={onBack}
onDelete={onDelete} onDelete={onDelete}
onSave={submit} onSave={submit}

View file

@ -57,6 +57,7 @@ const WarehouseCreate: React.FC = () => {
streetAddress1: data.streetAddress1, streetAddress1: data.streetAddress1,
streetAddress2: data.streetAddress2 streetAddress2: data.streetAddress2
}, },
companyName: data.companyName,
name: data.name name: data.name
} }
} }

View file

@ -13,7 +13,9 @@ import { shippingZoneUrl } from "@saleor/shipping/urls";
import { CountryCode } from "@saleor/types/globalTypes"; import { CountryCode } from "@saleor/types/globalTypes";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import WarehouseDeleteDialog from "@saleor/warehouses/components/WarehouseDeleteDialog"; import WarehouseDeleteDialog from "@saleor/warehouses/components/WarehouseDeleteDialog";
import WarehouseDetailsPage from "@saleor/warehouses/components/WarehouseDetailsPage"; import WarehouseDetailsPage, {
WarehouseDetailsPageFormData
} from "@saleor/warehouses/components/WarehouseDetailsPage";
import { import {
useWarehouseDelete, useWarehouseDelete,
useWarehouseUpdate useWarehouseUpdate
@ -76,6 +78,28 @@ const WarehouseDetails: React.FC<WarehouseDetailsProps> = ({ id, params }) => {
return <NotFoundPage onBack={() => navigate(warehouseListUrl())} />; return <NotFoundPage onBack={() => navigate(warehouseListUrl())} />;
} }
const handleSubmit = async (data: WarehouseDetailsPageFormData) => {
const result = await updateWarehouse({
variables: {
id,
input: {
address: {
city: data.city,
cityArea: data.cityArea,
country: findValueInEnum(data.country, CountryCode),
countryArea: data.countryArea,
phone: data.phone,
postalCode: data.postalCode,
streetAddress1: data.streetAddress1,
streetAddress2: data.streetAddress2
},
name: data.name
}
}
});
return result.data.updateWarehouse.errors;
};
return ( return (
<> <>
<WindowTitle title={data?.warehouse?.name} /> <WindowTitle title={data?.warehouse?.name} />
@ -88,26 +112,7 @@ const WarehouseDetails: React.FC<WarehouseDetailsProps> = ({ id, params }) => {
onBack={() => navigate(warehouseListUrl())} onBack={() => navigate(warehouseListUrl())}
onDelete={() => openModal("delete")} onDelete={() => openModal("delete")}
onShippingZoneClick={id => navigate(shippingZoneUrl(id))} onShippingZoneClick={id => navigate(shippingZoneUrl(id))}
onSubmit={data => onSubmit={handleSubmit}
updateWarehouse({
variables: {
id,
input: {
address: {
city: data.city,
cityArea: data.cityArea,
country: findValueInEnum(data.country, CountryCode),
countryArea: data.countryArea,
phone: data.phone,
postalCode: data.postalCode,
streetAddress1: data.streetAddress1,
streetAddress2: data.streetAddress2
},
name: data.name
}
}
})
}
/> />
<WarehouseDeleteDialog <WarehouseDeleteDialog
confirmButtonState={deleteWarehouseTransitionState} confirmButtonState={deleteWarehouseTransitionState}