diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index c5c59a8e2..73f61bb54 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -4047,7 +4047,11 @@ }, "src_dot_orders_dot_components_dot_OrderCustomerAddressesEditDialog_dot_billingSameAsShipping": { "context": "checkbox label", - "string": "Billing address same as shipping address" + "string": "Set the same for billing address" + }, + "src_dot_orders_dot_components_dot_OrderCustomerAddressesEditDialog_dot_billingTitle": { + "context": "search modal billing title", + "string": "Billing address" }, "src_dot_orders_dot_components_dot_OrderCustomerAddressesEditDialog_dot_customerAddress": { "context": "address type", @@ -4065,13 +4069,24 @@ "context": "address type", "string": "Add new address" }, + "src_dot_orders_dot_components_dot_OrderCustomerAddressesEditDialog_dot_noResultsFound": { + "string": "No results found" + }, + "src_dot_orders_dot_components_dot_OrderCustomerAddressesEditDialog_dot_searchInfo": { + "context": "modal information under title", + "string": "Select an address you want to use from the list below" + }, "src_dot_orders_dot_components_dot_OrderCustomerAddressesEditDialog_dot_shippingAddressDescription": { "context": "dialog content", "string": "This customer doesn’t have any shipping addresses. Provide address for order:" }, + "src_dot_orders_dot_components_dot_OrderCustomerAddressesEditDialog_dot_shippingTitle": { + "context": "search modal shipping title", + "string": "Shipping address" + }, "src_dot_orders_dot_components_dot_OrderCustomerAddressesEditDialog_dot_title": { "context": "dialog header", - "string": "Shipping address for order" + "string": "Change address for order" }, "src_dot_orders_dot_components_dot_OrderCustomerChangeDialog_dot_changeAddress": { "context": "option label", @@ -6748,6 +6763,9 @@ "context": "select all options, button", "string": "Select All" }, + "src_dot_selected": { + "string": "Selected" + }, "src_dot_send": { "context": "button", "string": "Send" diff --git a/package-lock.json b/package-lock.json index cb5ccb001..c78ff6f31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5166,9 +5166,9 @@ } }, "@saleor/macaw-ui": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.2.6.tgz", - "integrity": "sha512-ySEhWN9kyxX+5ATXVOg4siS5RwMRIMcLAybJHax0SpzS4E6FtLM5VDQhWliHaM2hicFPMVrLBa7MkTvAV1JcIA==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.2.7.tgz", + "integrity": "sha512-sRt193W5u1Vu+5893zEsRbkVF3H1uXuqNC+BEXwTg85i+J9YHmykJZjDneExgGH24nYe948S5dffsy3NMOGSTQ==", "requires": { "clsx": "^1.1.1", "lodash": "^4.17.21", diff --git a/package.json b/package.json index a48bee827..ab8619ca1 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.58", "@material-ui/styles": "^4.11.4", - "@saleor/macaw-ui": "^0.2.6", + "@saleor/macaw-ui": "^0.2.7", "@sentry/react": "^6.0.0", "@types/faker": "^5.1.6", "@uiw/react-color-hue": "0.0.34", diff --git a/src/customers/components/CustomerAddressChoiceCard/CustomerAddressChoiceCard.tsx b/src/customers/components/CustomerAddressChoiceCard/CustomerAddressChoiceCard.tsx index b4bc4090e..260886aac 100644 --- a/src/customers/components/CustomerAddressChoiceCard/CustomerAddressChoiceCard.tsx +++ b/src/customers/components/CustomerAddressChoiceCard/CustomerAddressChoiceCard.tsx @@ -1,30 +1,48 @@ -import { Card, CardContent } from "@material-ui/core"; +import { Card, CardContent, Typography } from "@material-ui/core"; import AddressFormatter from "@saleor/components/AddressFormatter"; +import { commonMessages } from "@saleor/intl"; +import { EditIcon } from "@saleor/macaw-ui"; import classNames from "classnames"; import React from "react"; +import { useIntl } from "react-intl"; import { CustomerAddresses_user_addresses } from "../../types/CustomerAddresses"; import { useStyles } from "./styles"; export interface CustomerAddressChoiceCardProps { address: CustomerAddresses_user_addresses; - selected: boolean; - onSelect: () => void; + selected?: boolean; + editable?: boolean; + onSelect?: () => void; + onEditClick?: () => void; } const CustomerAddressChoiceCard: React.FC = props => { - const { address, selected, onSelect } = props; + const { address, selected, editable, onSelect, onEditClick } = props; const classes = useStyles(props); + const intl = useIntl(); + return ( - + + {editable && ( +
+ +
+ )} + {selected && ( + + {intl.formatMessage(commonMessages.selected)} + + )}
); diff --git a/src/customers/components/CustomerAddressChoiceCard/styles.ts b/src/customers/components/CustomerAddressChoiceCard/styles.ts index de30654a1..74ab1ede7 100644 --- a/src/customers/components/CustomerAddressChoiceCard/styles.ts +++ b/src/customers/components/CustomerAddressChoiceCard/styles.ts @@ -3,13 +3,37 @@ import { makeStyles } from "@saleor/macaw-ui"; export const useStyles = makeStyles( theme => ({ card: { - cursor: "pointer", padding: "1px" }, cardSelected: { borderColor: theme.palette.primary.main, borderWidth: "2px", padding: "0" + }, + cardContent: { + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-start" + }, + selectableCard: { + "&:hover": { + cursor: "pointer", + borderColor: theme.palette.primary.main + } + }, + selectedLabel: { + fontSize: "1.4rem", + lineHeight: "1.75", + fontWeight: 600, + textTransform: "uppercase" + }, + editIcon: { + color: theme.palette.grey[600], + "&:hover": { + color: theme.palette.primary.main, + cursor: "pointer" + } } }), { name: "CustomerAddressChoiceCard" } diff --git a/src/intl.ts b/src/intl.ts index f610600dc..c3a8ad12f 100644 --- a/src/intl.ts +++ b/src/intl.ts @@ -84,6 +84,9 @@ export const commonMessages = defineMessages({ savedChanges: { defaultMessage: "Saved changes" }, + selected: { + defaultMessage: "Selected" + }, sessionExpired: { defaultMessage: "Your session has expired. Please log in again to continue." }, diff --git a/src/misc.ts b/src/misc.ts index 81ddee293..775431f0b 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -434,3 +434,18 @@ export function getFullName( return `${data.firstName} ${data.lastName}`; } +export const flatten = (obj: unknown) => { + // Be cautious that repeated keys are overwritten + + const result = {}; + + Object.keys(obj).forEach(key => { + if (typeof obj[key] === "object" && obj[key] !== null) { + Object.assign(result, flatten(obj[key])); + } else { + result[key] = obj[key]; + } + }); + + return result; +}; diff --git a/src/orders/components/OrderCustomerAddressesEditDialog/OrderCustomerAddressEdit.tsx b/src/orders/components/OrderCustomerAddressesEditDialog/OrderCustomerAddressEdit.tsx index 637945ac2..dd4fd98d6 100644 --- a/src/orders/components/OrderCustomerAddressesEditDialog/OrderCustomerAddressEdit.tsx +++ b/src/orders/components/OrderCustomerAddressesEditDialog/OrderCustomerAddressEdit.tsx @@ -13,6 +13,7 @@ import { FormChange } from "@saleor/hooks/useForm"; import React from "react"; import { useIntl } from "react-intl"; +import { getById } from "../OrderReturnPage/utils"; import { AddressInputOptionEnum } from "./form"; import { addressEditMessages } from "./messages"; import { useStyles } from "./styles"; @@ -24,15 +25,13 @@ export interface OrderCustomerAddressEditProps { addressInputOption: AddressInputOptionEnum; addressInputName: string; onChangeAddressInputOption: FormChange; - customerAddressId: string; + selectedCustomerAddressId: string; formAddress: AddressTypeInput; formAddressCountryDisplayName: string; formErrors: Array; - onChangeCustomerAddress: ( - customerAddress: CustomerAddresses_user_addresses - ) => void; onChangeFormAddress: (event: React.ChangeEvent) => void; onChangeFormAddressCountry: (event: React.ChangeEvent) => void; + onEdit?: () => void; } const OrderCustomerAddressEdit: React.FC = props => { @@ -43,13 +42,13 @@ const OrderCustomerAddressEdit: React.FC = props addressInputOption, addressInputName, onChangeAddressInputOption, - customerAddressId, + selectedCustomerAddressId, formAddress, formAddressCountryDisplayName, formErrors, - onChangeCustomerAddress, onChangeFormAddress, - onChangeFormAddressCountry + onChangeFormAddressCountry, + onEdit } = props; const classes = useStyles(props); @@ -92,19 +91,15 @@ const OrderCustomerAddressEdit: React.FC = props className={classes.optionLabel} /> {addressInputOption === AddressInputOptionEnum.CUSTOMER_ADDRESS && ( -
- {customerAddresses.map(customerAddress => ( - - - onChangeCustomerAddress(customerAddress)} - /> - - ))} + <> + + -
+ )} = props => { const { open, @@ -85,9 +96,11 @@ const OrderCustomerAddressesEditDialog: React.FC + const getCustomerAddress = ( + selectedCustomerAddressID: string + ): AddressInput => transformAddressToAddressInput( - customerAddresses.find(getById(customerAddressId)) + customerAddresses.find(getById(selectedCustomerAddressID)) ); const handleAddressesSubmit = (data: OrderCustomerAddressesEditFormData) => { @@ -127,88 +140,73 @@ const OrderCustomerAddressesEditDialog: React.FC(defaultSearchState); + + const validatedDefaultShippingAddress = validateDefaultAddress( + defaultShippingAddress, + customerAddresses + ); + const validatedDefaultBillingAddress = validateDefaultAddress( + defaultBillingAddress, + customerAddresses + ); + return ( - + { + setAddressSearchState(defaultSearchState); + onClose(); + }} + open={open} + fullWidth + > {({ change, data, handlers }) => ( <> - - - - - - {customerAddresses.length > 0 ? ( - - ) : ( - - )} - - - error.addressType === AddressTypeEnum.SHIPPING - )} - onChangeCustomerAddress={customerAddress => + selectedCustomerAddressId={ + addressSearchState.type === AddressTypeEnum.SHIPPING + ? data.customerShippingAddress?.id + : data.customerBillingAddress?.id + } + onChangeCustomerShippingAddress={customerAddress => handlers.changeCustomerAddress( customerAddress, "customerShippingAddress" ) } - onChangeFormAddress={event => - handlers.changeFormAddress(event, "shippingAddress") + onChangeCustomerBillingAddress={customerAddress => + handlers.changeCustomerAddress( + customerAddress, + "customerBillingAddress" + ) } - onChangeFormAddressCountry={handlers.selectShippingCountry} + exitSearch={() => setAddressSearchState(defaultSearchState)} /> - - - - - change({ - target: { - name: "billingSameAsShipping", - value: !data.billingSameAsShipping - } - }) - } - data-test="billingSameAsShipping" - /> - } - label={intl.formatMessage(dialogMessages.billingSameAsShipping)} - /> - {!data.billingSameAsShipping && ( - <> - + ) : ( + <> + + + + {customerAddresses.length > 0 ? ( ) : ( )} @@ -216,43 +214,113 @@ const OrderCustomerAddressesEditDialog: React.FC error.addressType === AddressTypeEnum.BILLING + error => error.addressType === AddressTypeEnum.SHIPPING )} - onChangeCustomerAddress={customerAddress => - handlers.changeCustomerAddress( - customerAddress, - "customerBillingAddress" - ) - } onChangeFormAddress={event => - handlers.changeFormAddress(event, "billingAddress") + handlers.changeFormAddress(event, "shippingAddress") + } + onChangeFormAddressCountry={handlers.selectShippingCountry} + onEdit={() => + setAddressSearchState({ + open: true, + type: AddressTypeEnum.SHIPPING + }) } - onChangeFormAddressCountry={handlers.selectBillingCountry} /> - - )} - - - - - - + + + + + change({ + target: { + name: "billingSameAsShipping", + value: !data.billingSameAsShipping + } + }) + } + data-test="billingSameAsShipping" + /> + } + label={intl.formatMessage( + dialogMessages.billingSameAsShipping + )} + /> + {!data.billingSameAsShipping && ( + <> + + + {customerAddresses.length > 0 ? ( + + ) : ( + + )} + + + error.addressType === AddressTypeEnum.BILLING + )} + onChangeFormAddress={event => + handlers.changeFormAddress(event, "billingAddress") + } + onChangeFormAddressCountry={ + handlers.selectBillingCountry + } + onEdit={() => + setAddressSearchState({ + open: true, + type: AddressTypeEnum.BILLING + }) + } + /> + + )} + + + + + + + + )} )} diff --git a/src/orders/components/OrderCustomerAddressesEditDialog/OrderCustomerAddressesSearch.tsx b/src/orders/components/OrderCustomerAddressesEditDialog/OrderCustomerAddressesSearch.tsx new file mode 100644 index 000000000..9949f4cd4 --- /dev/null +++ b/src/orders/components/OrderCustomerAddressesEditDialog/OrderCustomerAddressesSearch.tsx @@ -0,0 +1,140 @@ +import { + Button, + DialogActions, + DialogContent, + DialogTitle, + InputAdornment, + TextField +} from "@material-ui/core"; +import CardSpacer from "@saleor/components/CardSpacer"; +import { ConfirmButton } from "@saleor/components/ConfirmButton"; +import CustomerAddressChoiceCard from "@saleor/customers/components/CustomerAddressChoiceCard"; +import { CustomerAddresses_user_addresses } from "@saleor/customers/types/CustomerAddresses"; +import { buttonMessages } from "@saleor/intl"; +import { SearchIcon } from "@saleor/macaw-ui"; +import { AddressTypeEnum } from "@saleor/types/globalTypes"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { getById } from "../OrderReturnPage/utils"; +import { addressSearchMessages as messages } from "./messages"; +import { useStyles } from "./styles"; +import { parseQuery, stringifyAddress } from "./utils"; + +export interface OrderCustomerAddressesSearchProps { + type: AddressTypeEnum; + selectedCustomerAddressId?: string; + customerAddresses: CustomerAddresses_user_addresses[]; + onChangeCustomerShippingAddress: ( + customerAddress: CustomerAddresses_user_addresses + ) => void; + onChangeCustomerBillingAddress: ( + customerAddress: CustomerAddresses_user_addresses + ) => void; + exitSearch(); +} + +const OrderCustomerAddressesSearch: React.FC = props => { + const { + type, + selectedCustomerAddressId, + customerAddresses, + onChangeCustomerShippingAddress, + onChangeCustomerBillingAddress, + exitSearch + } = props; + + const intl = useIntl(); + const classes = useStyles(props); + + const initialAddress = customerAddresses.find( + getById(selectedCustomerAddressId) + ); + + const [query, setQuery] = React.useState(""); + const [ + temporarySelectedAddress, + setTemporarySelectedAddress + ] = React.useState(initialAddress); + + const handleSelect = () => { + if (type === AddressTypeEnum.SHIPPING) { + onChangeCustomerShippingAddress(temporarySelectedAddress); + } else { + onChangeCustomerBillingAddress(temporarySelectedAddress); + } + exitSearch(); + }; + + const handleChange = (e: React.ChangeEvent) => { + setQuery(e.target.value); + }; + + const filteredCustomerAddresses = customerAddresses.filter(address => { + const parsedAddress = stringifyAddress(address); + + return parsedAddress.search(new RegExp(parseQuery(query), "i")) >= 0; + }); + + return ( + <> + + {type === AddressTypeEnum.SHIPPING ? ( + + ) : ( + + )} + + + {intl.formatMessage(messages.searchInfo)} + + + + + ) + }} + inputProps={{ className: classes.searchInput }} + /> + +
+ {filteredCustomerAddresses.length === 0 + ? intl.formatMessage(messages.noResultsFound) + : filteredCustomerAddresses?.map(address => ( + + setTemporarySelectedAddress(address)} + address={address} + /> + + + ))} +
+
+ + + + + + + + ); +}; + +OrderCustomerAddressesSearch.displayName = "OrderCustomerAddressesSearch"; +export default OrderCustomerAddressesSearch; diff --git a/src/orders/components/OrderCustomerAddressesEditDialog/messages.ts b/src/orders/components/OrderCustomerAddressesEditDialog/messages.ts index eefc351dd..c1f974b50 100644 --- a/src/orders/components/OrderCustomerAddressesEditDialog/messages.ts +++ b/src/orders/components/OrderCustomerAddressesEditDialog/messages.ts @@ -2,11 +2,11 @@ import { defineMessages } from "react-intl"; export const dialogMessages = defineMessages({ title: { - defaultMessage: "Shipping address for order", + defaultMessage: "Change address for order", description: "dialog header" }, billingSameAsShipping: { - defaultMessage: "Billing address same as shipping address", + defaultMessage: "Set the same for billing address", description: "checkbox label" }, shippingAddressDescription: { @@ -39,3 +39,21 @@ export const addressEditMessages = defineMessages({ description: "address type" } }); + +export const addressSearchMessages = defineMessages({ + shippingTitle: { + defaultMessage: "Shipping address", + description: "search modal shipping title" + }, + billingTitle: { + defaultMessage: "Billing address", + description: "search modal billing title" + }, + searchInfo: { + defaultMessage: "Select an address you want to use from the list below", + description: "modal information under title" + }, + noResultsFound: { + defaultMessage: "No results found" + } +}); diff --git a/src/orders/components/OrderCustomerAddressesEditDialog/styles.ts b/src/orders/components/OrderCustomerAddressesEditDialog/styles.ts index bcf0775a1..ed42b3149 100644 --- a/src/orders/components/OrderCustomerAddressesEditDialog/styles.ts +++ b/src/orders/components/OrderCustomerAddressesEditDialog/styles.ts @@ -1,14 +1,15 @@ import { makeStyles } from "@saleor/macaw-ui"; export const useStyles = makeStyles( - { + theme => ({ scrollableContent: { maxHeight: `calc(100vh - 250px)`, - overflow: "scroll" + overflowY: "scroll", + overflowX: "hidden" }, scrollableWrapper: { maxHeight: 400, - overflow: "scroll" + overflowY: "scroll" }, container: { display: "block" @@ -18,7 +19,11 @@ export const useStyles = makeStyles( }, overflow: { overflowY: "visible" + }, + searchInput: { + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2) } - }, + }), { name: "OrderCustomerAddressesEditDialog" } ); diff --git a/src/orders/components/OrderCustomerAddressesEditDialog/utils.ts b/src/orders/components/OrderCustomerAddressesEditDialog/utils.ts new file mode 100644 index 000000000..1b8dd0de3 --- /dev/null +++ b/src/orders/components/OrderCustomerAddressesEditDialog/utils.ts @@ -0,0 +1,35 @@ +import { + CustomerAddresses_user_addresses, + CustomerAddresses_user_defaultShippingAddress +} from "@saleor/customers/types/CustomerAddresses"; +import { flatten } from "@saleor/misc"; + +import { getById } from "../OrderReturnPage/utils"; + +export const stringifyAddress = ( + address: Partial +): string => { + const { id, ...addressWithoutId } = address; + return Object.values(flatten(addressWithoutId)).join(" "); +}; + +export const parseQuery = (query: string) => + query.replace(/([.?*+\-=:^$\\[\]<>(){}|])/g, "\\$&"); + +export function validateDefaultAddress< + T extends CustomerAddresses_user_defaultShippingAddress +>( + defaultAddress: CustomerAddresses_user_defaultShippingAddress, + customerAddresses: T[] +): CustomerAddresses_user_defaultShippingAddress { + const fallbackAddress = { + id: customerAddresses[0]?.id + } as CustomerAddresses_user_defaultShippingAddress; + if (!defaultAddress) { + return fallbackAddress; + } + if (!customerAddresses.some(getById(defaultAddress.id))) { + return fallbackAddress; + } + return defaultAddress; +}