From 3cf5d9acb1214dab41db377b9400fb761aabe2c2 Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Wed, 18 Dec 2019 16:00:07 -0600 Subject: [PATCH 01/19] feat: new address validation rules table and form Signed-off-by: Eric Dobbertin --- .../AddressValidationSettingsForm.js | 333 ++++++++++++++---- .../ShopAddressValidationSettings.js | 315 +++++++++-------- .../ShopAddressValidationSettings.js | 136 ++----- .../hoc/withAddressValidationServices.js | 38 -- .../client/hooks/useAddressValidationRules.js | 43 +++ .../hooks/useAddressValidationServices.js | 26 ++ imports/plugins/core/address/client/index.js | 12 +- 7 files changed, 546 insertions(+), 357 deletions(-) delete mode 100644 imports/plugins/core/address/client/hoc/withAddressValidationServices.js create mode 100644 imports/plugins/core/address/client/hooks/useAddressValidationRules.js create mode 100644 imports/plugins/core/address/client/hooks/useAddressValidationServices.js diff --git a/imports/plugins/core/address/client/components/AddressValidationSettingsForm.js b/imports/plugins/core/address/client/components/AddressValidationSettingsForm.js index bd90ec83d1..58d2389e09 100644 --- a/imports/plugins/core/address/client/components/AddressValidationSettingsForm.js +++ b/imports/plugins/core/address/client/components/AddressValidationSettingsForm.js @@ -1,87 +1,276 @@ -import React, { Component } from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; -import { uniqueId } from "lodash"; -import { Form } from "reacto-form"; -import { i18next } from "/client/api"; -import ErrorsBlock from "@reactioncommerce/components/ErrorsBlock/v1"; -import Field from "@reactioncommerce/components/Field/v1"; -import MultiSelect from "@reactioncommerce/components/MultiSelect/v1"; -import Select from "@reactioncommerce/components/Select/v1"; +import i18next from "i18next"; +import { useMutation } from "@apollo/react-hooks"; +import gql from "graphql-tag"; +import { useSnackbar } from "notistack"; +import SimpleSchema from "simpl-schema"; +import Button from "@reactioncommerce/catalyst/Button"; +import Grid from "@material-ui/core/Grid"; +import MenuItem from "@material-ui/core/MenuItem"; +import TextField from "@material-ui/core/TextField"; +import { makeStyles } from "@material-ui/styles"; +import muiOptions from "reacto-form/cjs/muiOptions"; +import useReactoForm from "reacto-form/cjs/useReactoForm"; -/** - * @summary The default form validator - * @see http://composableforms.com/spec/validation/ - * @param {Object} doc The document to validate - * @returns {Object[]} ReactoForm validation errors array - */ -async function defaultValidator(doc) { - if (typeof doc.serviceName !== "string" || doc.serviceName.length === 0) { - return [{ name: "serviceName", message: "You must choose an address validation service" }]; +const createRuleMutation = gql` + mutation createRuleMutation($input: CreateAddressValidationRuleInput!) { + createAddressValidationRule(input: $input) { + addressValidationRule { + _id + } + } } +`; - return []; -} +const deleteRuleMutation = gql` + mutation deleteRuleMutation($input: DeleteAddressValidationRuleInput!) { + deleteAddressValidationRule(input: $input) { + addressValidationRule { + _id + } + } + } +`; -export default class AddressValidationSettingsForm extends Component { - static propTypes = { - countryOptions: PropTypes.arrayOf(PropTypes.shape({ - label: PropTypes.string.isRequired, - value: PropTypes.string.isRequired - })), - onChange: PropTypes.func, - onSubmit: PropTypes.func.isRequired, - serviceOptions: PropTypes.arrayOf(PropTypes.shape({ - label: PropTypes.string.isRequired, - value: PropTypes.string.isRequired - })), - validator: PropTypes.func, - value: PropTypes.shape({ - countryCodes: PropTypes.arrayOf(PropTypes.string), - serviceName: PropTypes.string.isRequired - }) - }; +const updateRuleMutation = gql` + mutation updateRuleMutation($input: UpdateAddressValidationRuleInput!) { + updateAddressValidationRule(input: $input) { + addressValidationRule { + _id + } + } + } +`; - static defaultProps = { - validator: defaultValidator - }; +const useStyles = makeStyles((theme) => ({ + deleteButton: { + marginRight: theme.spacing(1) + }, + rightAlignedGrid: { + textAlign: "right" + }, + textField: { + marginBottom: theme.spacing(4), + minWidth: 350 + } +})); - uniqueInstanceIdentifier = uniqueId("AddressValidationSettingsForm"); +const formSchema = new SimpleSchema({ + "countryCodes": { + type: Array, + optional: true + }, + "countryCodes.$": String, + "serviceName": { + type: String, + min: 1 + } +}); +const validator = formSchema.getFormValidator(); - submit() { - if (this.form) { - this.form.submit(); +/** + * @summary React component that renders the form for adding, updating, or deleting + * a custom tax rate record. + * @param {Object} props React props + * @return {React.Node} React node + */ +export default function AddressValidationSettingsForm(props) { + const classes = useStyles(); + const [isSubmitting, setIsSubmitting] = useState(false); + const { enqueueSnackbar } = useSnackbar(); + + const { + doc, + getSupportedCountryOptionsForService = () => [], + onSuccess = () => {}, + serviceOptions, + shopId + } = props; + + const [createRule] = useMutation(createRuleMutation, { + ignoreResults: true, + onCompleted() { + setIsSubmitting(false); + onSuccess(); + }, + onError() { + setIsSubmitting(false); + enqueueSnackbar(i18next.t("admin.taxSettings.shopCustomTaxRatesFailed"), { variant: "warning" }); } - } + }); - render() { - const { countryOptions, onChange, onSubmit, serviceOptions, validator, value } = this.props; + const [deleteRule] = useMutation(deleteRuleMutation, { + ignoreResults: true, + onCompleted() { + setIsSubmitting(false); + onSuccess(); + }, + onError() { + setIsSubmitting(false); + enqueueSnackbar(i18next.t("admin.taxSettings.shopCustomTaxRatesFailed"), { variant: "warning" }); + } + }); + + const [updateRule] = useMutation(updateRuleMutation, { + ignoreResults: true, + onCompleted() { + setIsSubmitting(false); + onSuccess(); + }, + onError() { + setIsSubmitting(false); + enqueueSnackbar(i18next.t("admin.taxSettings.shopCustomTaxRatesFailed"), { variant: "warning" }); + } + }); - const serviceNameInputId = `serviceName_${this.uniqueInstanceIdentifier}`; - const countryCodesInputId = `countryCodes_${this.uniqueInstanceIdentifier}`; + const { + formData: currentFormData, + getFirstErrorMessage, + getInputProps, + hasErrors, + submitForm + } = useReactoForm({ + async onSubmit(formData) { + setIsSubmitting(true); - return ( -
{ this.form = formRef; }} - validator={validator} - value={value} + if (doc) { + await updateRule({ + variables: { + input: { + // In case doc has additional fields not allowed, we'll copy just what we want + countryCodes: formData.countryCodes || null, + serviceName: formData.serviceName, + ruleId: formData._id, + shopId + } + } + }); + } else { + await createRule({ + variables: { + input: { + // In case doc has additional fields not allowed, we'll copy just what we want + countryCodes: formData.countryCodes || null, + serviceName: formData.serviceName, + shopId + } + } + }); + } + }, + validator(formData) { + return validator({ + // Don't include `_id` when we're updating + countryCodes: formData.countryCodes, + serviceName: formData.serviceName + }); + }, + value: doc + }); + + const countryCodesInputProps = getInputProps("countryCodes", muiOptions); + + return ( +
+ { + if (event.key === "Enter") submitForm(); + }} + select + {...getInputProps("serviceName", muiOptions)} + > + {serviceOptions.map((option) => ( + + {option.label} + + ))} + + { + if (event.key === "Enter") submitForm(); + }} + select + SelectProps={{ multiple: true }} + {...countryCodesInputProps} + // Avoid "value must be an array" error + value={countryCodesInputProps.value || []} > - - -
- {{/if}} - -
-
- {{> React component=ReactComponentOrBlazeTemplate name=template props=.}} -
-
- - {{/each}} {{#if shopSettingsBlockProps.shopId}} @@ -103,17 +69,6 @@ {{/if}} - -