From fa3416c2ff91b820d1ff26b3554b521320ce4666 Mon Sep 17 00:00:00 2001 From: levenecav Date: Fri, 14 Dec 2018 17:10:00 +0300 Subject: [PATCH] Leve/trello/1066 weight size product (#809) * intermediate * intermediate * add weight and size functionality in product form * intermediate * metrics with delivery --- src/components/Modal/Modal.scss | 4 + src/components/common/Input/Input.js | 18 +- src/components/common/Input/Input.scss | 2 +- .../common/InputNumber/InputNumber.js | 110 +++++++++ .../common/InputPrice/InputPrice.js | 5 +- src/components/common/index.js | 1 + .../Products/Product/EditProduct/index.js | 84 +++++-- .../Store/Products/Product/Form/i18n.js | 2 + .../Store/Products/Product/Form/index.js | 224 ++++++++++++++---- .../Products/Product/Metrics/Metrics.scss | 76 ++++++ .../Store/Products/Product/Metrics/i18n.js | 43 ++++ .../Store/Products/Product/Metrics/index.js | 138 +++++++++++ .../Products/Product/NewProduct/index.js | 44 +++- .../Store/Products/Product/Product.scss | 48 +++- .../handlerLocalShippingDecorator/index.js | 4 + .../Manage/Store/Products/types/index.js | 12 + .../common/FormItemTitle/FormItemTitle.scss | 6 + src/pages/common/FormItemTitle/index.js | 13 + .../CreateBaseProductWithVariantsMutation.js | 4 + .../mutations/UpdateBaseProductMutation.js | 12 + 20 files changed, 762 insertions(+), 88 deletions(-) create mode 100644 src/components/common/InputNumber/InputNumber.js create mode 100644 src/pages/Manage/Store/Products/Product/Metrics/Metrics.scss create mode 100644 src/pages/Manage/Store/Products/Product/Metrics/i18n.js create mode 100644 src/pages/Manage/Store/Products/Product/Metrics/index.js create mode 100644 src/pages/common/FormItemTitle/FormItemTitle.scss create mode 100644 src/pages/common/FormItemTitle/index.js diff --git a/src/components/Modal/Modal.scss b/src/components/Modal/Modal.scss index 9596b469..ee652cb0 100644 --- a/src/components/Modal/Modal.scss +++ b/src/components/Modal/Modal.scss @@ -20,6 +20,10 @@ justify-content: center; align-items: center; margin: 15rem 5rem; + + @media (max-width: #{$sm - 1px}) { + margin: 10rem 2rem; + } } .body { diff --git a/src/components/common/Input/Input.js b/src/components/common/Input/Input.js index 9bf03673..d821358f 100644 --- a/src/components/common/Input/Input.js +++ b/src/components/common/Input/Input.js @@ -32,6 +32,7 @@ type PropsType = { search: boolean, align?: 'center' | 'left' | 'right', disabled?: boolean, + limitHidden?: boolean, }; type StateType = { @@ -77,7 +78,11 @@ class Input extends Component { input: ?HTMLInputElement; handleChange = (e: SyntheticInputEvent) => { - const { onChange } = this.props; + const { onChange, limit } = this.props; + const { value } = e.target; + if (limit != null && value.length > limit) { + return; + } onChange(e); }; @@ -101,7 +106,6 @@ class Input extends Component { renderInput() { const { - onChange, inputRef, isAutocomplete, id, @@ -115,9 +119,9 @@ class Input extends Component { { type={!isNil(type) ? type : 'text'} value={value || ''} disabled={disabled || false} - onChange={onChange} + onChange={this.handleChange} onFocus={this.handleFocus} onBlur={this.handleBlur} onKeyDown={this.props.onKeyDown} @@ -156,6 +160,7 @@ class Input extends Component { postfix, inline, search, + limitHidden, } = this.props; const { labelFloat, isFocus } = this.state; return ( @@ -204,7 +209,8 @@ class Input extends Component { )} {isFocus && !isUrl && - !isNil(limit) && ( + !isNil(limit) && + limitHidden !== true && (
void, + onFocus?: () => void, + onBlur?: () => void, + value: number, +}; + +class InputNumber extends Component { + static getDerivedStateFromProps(nextProps: PropsType, prevState: StateType) { + const value = `${nextProps.value}`; + if (Number(value) !== Number(prevState.value)) { + return { ...prevState, value }; + } + return null; + } + + constructor(props: PropsType) { + super(props); + this.state = { + value: props.value ? `${props.value}` : '0', + }; + } + + handleOnChange = (e: SyntheticInputEvent) => { + const { + target: { value }, + } = e; + const { onChange } = this.props; + const regexp = /(^[0-9]*[.,]?[0-9]*$)/; + if (regexp.test(value)) { + this.setState({ + value: + value + .replace(/^0+/, '0') + .replace(/^[.,]/, '0.') + .replace(/^0([0-9])/, '$1') + .replace(/,/, '.') || '0', + }); + onChange( + Number( + value + .replace(/[.,]$/, '') + .replace(/^0([0-9])/, '$1') + .replace(/(^0\.[0-9])0+$/, '$1'), + ), + ); + return; + } + if (value === '') { + this.setState({ value: '0' }, () => { + onChange(0); + }); + } + }; + + handleOnFocus = () => { + const { onFocus } = this.props; + if (onFocus) { + onFocus(); + } + }; + + handleOnBlur = () => { + const value = `${this.state.value}`; + if (Number(value) === 0) { + this.setState({ + value: '0', + }); + } else { + this.setState({ + value: value + .replace(/\.$/, '') + .replace(/^0([0-9])/, '$1') + .replace(/\.0+$/, '') + .replace(/(^0\.[0-9])0+$/, '$1'), + }); + } + const { onBlur } = this.props; + if (onBlur) { + onBlur(); + } + }; + + render() { + const { value } = this.state; + const props = omit(['value', 'onChange', 'onFocus', 'onBlur'], this.props); + return ( + + ); + } +} + +export default InputNumber; diff --git a/src/components/common/InputPrice/InputPrice.js b/src/components/common/InputPrice/InputPrice.js index a1ca002b..6eaa70c8 100644 --- a/src/components/common/InputPrice/InputPrice.js +++ b/src/components/common/InputPrice/InputPrice.js @@ -75,8 +75,9 @@ class InputPrice extends Component { return; } if (value === '') { - this.setState({ price: '0' }); - onChangePrice(0); + this.setState({ price: '0' }, () => { + onChangePrice(0); + }); } }; diff --git a/src/components/common/index.js b/src/components/common/index.js index 9776abbd..f6bf869d 100644 --- a/src/components/common/index.js +++ b/src/components/common/index.js @@ -3,6 +3,7 @@ export { default as Input } from './Input/Input'; export { default as Textarea } from './Textarea/Textarea'; export { default as InputPrice } from './InputPrice/InputPrice'; +export { default as InputNumber } from './InputNumber/InputNumber'; export { default as InputSlug } from './InputSlug/InputSlug'; export { default as Select } from './Select/Select'; export { default as Button } from './Button/Button'; diff --git a/src/pages/Manage/Store/Products/Product/EditProduct/index.js b/src/pages/Manage/Store/Products/Product/EditProduct/index.js index a58669cd..98539327 100644 --- a/src/pages/Manage/Store/Products/Product/EditProduct/index.js +++ b/src/pages/Manage/Store/Products/Product/EditProduct/index.js @@ -87,7 +87,7 @@ class EditProduct extends Component { formErrors: {}, isLoading: false, availablePackages: null, - isLoadingPackages: true, + isLoadingPackages: false, isLoadingShipping: false, shippingData: null, customAttributes: newCustomAttributes, @@ -95,6 +95,26 @@ class EditProduct extends Component { } componentDidMount() { + this.handleFetchPackages(); + } + + setLoadingPackages = (value: boolean) => { + this.setState({ isLoadingPackages: value }); + }; + + handleFetchPackages = (metrics?: { + lengthCm: number, + widthCm: number, + heightCm: number, + weightG: number, + }) => { + this.setState({ isLoadingPackages: true }); + const size = metrics + ? metrics.lengthCm * metrics.widthCm * metrics.heightCm + : 0; + const weight = metrics ? metrics.weightG : 0; + // $FlowIgnore + const baseProduct = pathOr(null, ['me', 'baseProduct'], this.props); // $FlowIgnore const warehouses = pathOr( null, @@ -109,8 +129,8 @@ class EditProduct extends Component { this.setLoadingPackages(true); const variables = { countryCode, - size: 0, - weight: 0, + size: metrics ? size : baseProduct.volumeCubicCm || 0, + weight: metrics ? weight : baseProduct.weightG || 0, }; fetchPackages(this.props.environment, variables) @@ -135,17 +155,16 @@ class EditProduct extends Component { } else { this.handlerOffLoadingPackages(); } - } - - setLoadingPackages = (value: boolean) => { - this.setState({ isLoadingPackages: value }); }; handlerOffLoadingPackages = () => { this.setState({ isLoadingPackages: false }); }; - handleSave = (form: FormType & { currency: string }) => { + handleSave = ( + form: FormType & { currency: string }, + withSavingShipping?: boolean, + ) => { this.setState({ formErrors: {} }); const baseProduct = path(['me', 'baseProduct'], this.props); if (!baseProduct || !baseProduct.id) { @@ -164,6 +183,7 @@ class EditProduct extends Component { shortDescription, longDescription, currency, + metrics, } = form; this.setState(() => ({ isLoading: true })); UpdateBaseProductMutation.commit({ @@ -179,6 +199,10 @@ class EditProduct extends Component { ? [{ lang: 'EN', text: seoDescription }] : null, currency, + lengthCm: metrics.lengthCm, + widthCm: metrics.widthCm, + heightCm: metrics.heightCm, + weightG: metrics.weightG, environment: this.props.environment, onCompleted: (response: ?Object, errors: ?Array) => { this.setState({ isLoading: false }); @@ -213,19 +237,22 @@ class EditProduct extends Component { } if (form && form.rawIdMainVariant) { - this.handleUpdateVariant({ - idMainVariant: form.idMainVariant, - rawIdMainVariant: form.rawIdMainVariant, - photoMain: form.photoMain, - photos: form.photos, - vendorCode: form.vendorCode, - price: form.price, - cashback: form.cashback, - discount: form.discount, - preOrderDays: form.preOrderDays, - preOrder: form.preOrder, - attributeValues: form.attributeValues, - }); + this.handleUpdateVariant( + { + idMainVariant: form.idMainVariant, + rawIdMainVariant: form.rawIdMainVariant, + photoMain: form.photoMain, + photos: form.photos, + vendorCode: form.vendorCode, + price: form.price, + cashback: form.cashback, + discount: form.discount, + preOrderDays: form.preOrderDays, + preOrder: form.preOrder, + attributeValues: form.attributeValues, + }, + withSavingShipping, + ); } }, onError: (error: Error) => { @@ -248,7 +275,10 @@ class EditProduct extends Component { }); }; - handleUpdateVariant = (variantData: VariantType) => { + handleUpdateVariant = ( + variantData: VariantType, + withSavingShipping?: boolean, + ) => { if (!variantData.idMainVariant) { this.props.showAlert({ type: 'danger', @@ -353,6 +383,10 @@ class EditProduct extends Component { text: t.productUpdated, link: { text: '' }, }); + + if (withSavingShipping) { + this.handleSaveShipping(); + } }, onError: (error: Error) => { this.setState(() => ({ isLoading: false })); @@ -549,6 +583,7 @@ class EditProduct extends Component { router={router} match={match} isLoadingShipping={isLoadingShipping} + onFetchPackages={this.handleFetchPackages} />
)} @@ -701,6 +736,11 @@ export default createFragmentContainer( lang text } + lengthCm + widthCm + heightCm + weightG + volumeCubicCm } } `, diff --git a/src/pages/Manage/Store/Products/Product/Form/i18n.js b/src/pages/Manage/Store/Products/Product/Form/i18n.js index 9bb1edd1..ee6bb9d9 100644 --- a/src/pages/Manage/Store/Products/Product/Form/i18n.js +++ b/src/pages/Manage/Store/Products/Product/Form/i18n.js @@ -13,6 +13,7 @@ type TranslationDicType = {| categoryIsRequired: string, vendorCodeIsRequired: string, priceIsRequired: string, + metricsError: string, addAtLeastOneDeliveryServiceOrPickup: string, addAtLeastOneDeliveryDelivery: string, productPhotos: string, @@ -59,6 +60,7 @@ const translations: TranslationsBundleType = { categoryIsRequired: 'Category is required', vendorCodeIsRequired: 'Vendor code is required', priceIsRequired: 'Price is required', + metricsError: 'Please, specify all metrics', addAtLeastOneDeliveryServiceOrPickup: 'Add at least one delivery service or pickup', addAtLeastOneDeliveryDelivery: 'Add at least one delivery service', diff --git a/src/pages/Manage/Store/Products/Product/Form/index.js b/src/pages/Manage/Store/Products/Product/Form/index.js index 21aa986d..5d2d9e27 100644 --- a/src/pages/Manage/Store/Products/Product/Form/index.js +++ b/src/pages/Manage/Store/Products/Product/Form/index.js @@ -21,10 +21,12 @@ import { propEq, drop, length, + values, } from 'ramda'; import { validate } from '@storiqa/shared'; import classNames from 'classnames'; import { Environment } from 'relay-runtime'; +import debounce from 'lodash.debounce'; import { withErrorBoundary } from 'components/common/ErrorBoundaries'; import { Select, SpinnerCircle, Button, InputPrice } from 'components/common'; @@ -34,6 +36,7 @@ import { Textarea } from 'components/common/Textarea'; import { Input } from 'components/common/Input'; import { withShowAlert } from 'components/Alerts/AlertContext'; import ModerationStatus from 'pages/common/ModerationStatus'; +import { Modal } from 'components/Modal'; import { getNameText, @@ -53,6 +56,7 @@ import type { ProductType, ValueForAttributeInputType, GetAttributeType, + MetricsType, } from 'pages/Manage/Store/Products/types'; import type { SelectItemType, CategoryType } from 'types'; import type { AddAlertInputType } from 'components/Alerts/AlertContext'; @@ -66,6 +70,7 @@ import Characteristics from '../Characteristics'; import VariantForm from '../VariantForm'; import AdditionalAttributes from '../AdditionalAttributes'; import PreOrder from '../PreOrder'; +import Metrics from '../Metrics'; import sendProductToModerationMutation from '../mutations/SendProductToModerationMutation'; import type { @@ -98,6 +103,7 @@ type PropsType = { onResetAttribute: () => void, onSaveShipping: (onlyShippingSave?: boolean) => void, showAlert: (input: AddAlertInputType) => void, + onFetchPackages?: (metrics: MetricsType) => void, }; type StateType = { @@ -118,6 +124,7 @@ type StateType = { }>, variantForForm: ?ProductType | 'new', isSendingToModeration: boolean, + isShippingPopup: boolean, }; class Form extends Component { @@ -143,7 +150,12 @@ class Form extends Component { constructor(props: PropsType) { super(props); - const { baseProduct, currencies, customAttributes } = props; + const { + baseProduct, + currencies, + customAttributes, + onFetchPackages, + } = props; // $FlowIgnore const currency = pathOr('STQ', ['baseProduct', 'currency'], props); let form = {}; @@ -200,6 +212,12 @@ class Form extends Component { attributeValues: !isEmpty(customAttributes) ? this.resetAttrValues(customAttributes, mainVariant) : [], + metrics: { + weightG: baseProduct.weightG || 0, + widthCm: baseProduct.widthCm || 0, + lengthCm: baseProduct.lengthCm || 0, + heightCm: baseProduct.heightCm || 0, + }, }; } else { form = { @@ -223,6 +241,12 @@ class Form extends Component { attributeValues: !isEmpty(customAttributes) ? this.resetAttrValues(customAttributes, null) : [], + metrics: { + weightG: 0, + widthCm: 0, + lengthCm: 0, + heightCm: 0, + }, }; } this.state = { @@ -236,10 +260,11 @@ class Form extends Component { 'name', 'shortDescription', 'longDescription', - 'categoryId', 'vendorCode', - 'price', + 'categoryId', 'attributes', + 'price', + 'metrics', ], activeTab: 'variants', tabs: [ @@ -254,7 +279,11 @@ class Form extends Component { ], variantForForm, isSendingToModeration: false, + isShippingPopup: false, }; + if (onFetchPackages) { + this.onFetchPackages = debounce(onFetchPackages, 1000); + } } componentDidUpdate(prevProps: PropsType) { @@ -302,9 +331,11 @@ class Form extends Component { } } - onChangeValues = (values: Array) => { + onFetchPackages = undefined; + + onChangeValues = (attributeValues: Array) => { this.setState((prevState: StateType) => - assocPath(['form', 'attributeValues'], values, prevState), + assocPath(['form', 'attributeValues'], attributeValues, prevState), ); }; @@ -375,10 +406,10 @@ class Form extends Component { metaField: attrFromVariant.metaField, }; } - const { values, translatedValues } = attr.metaField; - if (values) { + const { values: attributeValues, translatedValues } = attr.metaField; + if (attributeValues) { return { - value: head(values) || '', + value: head(attributeValues) || '', }; } else if (translatedValues && !isEmpty(translatedValues)) { return { @@ -408,6 +439,7 @@ class Form extends Component { categoryId: [[val => Boolean(val), t.categoryIsRequired]], vendorCode: [[val => Boolean(val), t.vendorCodeIsRequired]], price: [[val => Boolean(val), t.priceIsRequired]], + metrics: [[val => !contains(0, values(val)), t.metricsError]], }, this.state.form, ); @@ -455,7 +487,27 @@ class Form extends Component { return shippingErrors; }; - handleSave = (isAddVariant?: boolean) => { + handleUpdateProduct = () => { + const { baseProduct } = this.props; + if (!baseProduct) { + this.handleSave(); + return; + } + const { form } = this.state; + if ( + baseProduct.weightG !== form.metrics.weightG || + baseProduct.widthCm !== form.metrics.widthCm || + baseProduct.lengthCm !== form.metrics.lengthCm || + baseProduct.heightCm !== form.metrics.heightCm + ) { + this.setState({ isShippingPopup: true }); + return; + } + this.handleSave(); + }; + + handleSave = (isAddVariant?: boolean, withSavingShipping?: boolean) => { + const { baseProduct } = this.props; const { form, currency } = this.state; this.setState({ formErrors: {}, @@ -470,10 +522,12 @@ class Form extends Component { }); return; } - this.props.onSave( - { ...form, currency: currency ? currency.id : null }, - isAddVariant, - ); + const savingData = { ...form, currency: currency ? currency.id : null }; + if (baseProduct) { + this.props.onSave(savingData, withSavingShipping); + return; + } + this.props.onSave(savingData, isAddVariant); }; sendToModeration = () => { @@ -612,11 +666,11 @@ class Form extends Component { ); }; - handleChangeValues = (values: Array) => { + handleChangeValues = (attributeValues: Array) => { this.setState((prevState: StateType) => { const formErrors = dissoc('attributes', prevState.formErrors); return { - ...assocPath(['form', 'attributeValues'], values, prevState), + ...assocPath(['form', 'attributeValues'], attributeValues, prevState), formErrors, }; }); @@ -705,6 +759,27 @@ class Form extends Component { })); }; + handleChangeMetrics = (metrics: MetricsType) => { + this.setState( + (prevState: StateType) => ({ + form: { + ...prevState.form, + metrics, + }, + formErrors: dissoc('metrics', prevState.formErrors), + }), + () => { + if (this.onFetchPackages) { + this.onFetchPackages(metrics); + } + }, + ); + }; + + handleCloseShippingPopup = () => { + this.setState({ isShippingPopup: false }); + }; + renderInput = (props: { id: string, label: string, @@ -788,6 +863,7 @@ class Form extends Component { tabs, variantForForm, isSendingToModeration, + isShippingPopup, } = this.state; // $FlowIgnore @@ -832,6 +908,7 @@ class Form extends Component { attributeValues, preOrder, preOrderDays, + metrics, } = form; return ( @@ -926,6 +1003,36 @@ class Form extends Component {
{formErrors.categoryId}
)} + {defaultAttributes && + !isEmpty(defaultAttributes) && + (!baseProduct || + (!isEmpty(customAttributes) && baseProduct)) && ( + +
+ {t.characteristics} +
+
+ +
+
+ )} + {!isEmpty(customAttributes) && ( +
+ +
+ )}
{t.pricing}
@@ -970,36 +1077,16 @@ class Form extends Component { /> {t.percent} - {defaultAttributes && - !isEmpty(defaultAttributes) && - (!baseProduct || - (!isEmpty(customAttributes) && baseProduct)) && ( - -
- {t.characteristics} -
-
- -
-
- )} - {!isEmpty(customAttributes) && ( -
- -
- )} +
+ + {formErrors && + formErrors.metrics && ( +
{formErrors.metrics}
+ )} +
{
)} + +
+
+
+ After saving the lists of logistics companies will be updated. + Do you want to continue? +
+
+ +
+ +
+
+
+
+
); } diff --git a/src/pages/Manage/Store/Products/Product/Metrics/Metrics.scss b/src/pages/Manage/Store/Products/Product/Metrics/Metrics.scss new file mode 100644 index 00000000..224bca9d --- /dev/null +++ b/src/pages/Manage/Store/Products/Product/Metrics/Metrics.scss @@ -0,0 +1,76 @@ +@import "../../../../../../styles/variables"; + +.container { + // +} + +.body { + display: flex; + margin-top: 1rem; + + @media (max-width: #{$sm - 1px}) { + display: block; + } +} + +.weight { + margin-right: 2rem; +} + +.label { + margin-bottom: -2.9rem; + color: $color_grey_minor; + font-size: 14px; + line-height: 2rem; + transform: scale(0.9); + transform-origin: left bottom; + transition: $transition_all_ease; + + &.labelFloat { + color: $color_blue; + } + + .asteriks { + color: $color_red; + } +} + +.weightInput { + display: flex; + align-items: flex-end; + width: 18rem; + margin-right: 2rem; +} + +.dimensions { + @media (max-width: #{$sm - 1px}) { + margin-top: 2rem; + } +} + +.dimensionInputs { + display: flex; + align-items: flex-end; + + .input { + position: relative; + width: 11rem; + + & + .input { + margin-left: 2rem; + } + + & .sign { + position: absolute; + bottom: -3rem; + left: 0; + font-size: 12px; + color: $color_grey_minor; + } + } +} + +.unit { + margin-left: 1rem; + font-size: 14px; +} diff --git a/src/pages/Manage/Store/Products/Product/Metrics/i18n.js b/src/pages/Manage/Store/Products/Product/Metrics/i18n.js new file mode 100644 index 00000000..1aed15d8 --- /dev/null +++ b/src/pages/Manage/Store/Products/Product/Metrics/i18n.js @@ -0,0 +1,43 @@ +// @flow strict +// @flow-runtime + +import { t } from 'translation/utils'; +import type { Translation } from 'translation/utils'; + +type TranslationDicType = {| + metrics: string, + width: string, + length: string, + height: string, + sm: string, + g: string, + weight: string, + dimensions: string, +|}; +type TranslationsBundleType = Translation; + +const translations: TranslationsBundleType = { + en: { + metrics: 'Metrics', + width: 'Width', + length: 'Length', + height: 'Height', + sm: 'sm', + g: 'g', + weight: 'Weight', + dimensions: 'Dimensions', + }, +}; + +const validate = (json: {}, verbose: boolean = false): boolean => { + try { + (json: TranslationsBundleType); // eslint-disable-line + return true; + } catch (err) { + verbose && console.error(err); // eslint-disable-line + return false; + } +}; + +export { translations, validate }; +export default t(translations); diff --git a/src/pages/Manage/Store/Products/Product/Metrics/index.js b/src/pages/Manage/Store/Products/Product/Metrics/index.js new file mode 100644 index 00000000..c8ae806b --- /dev/null +++ b/src/pages/Manage/Store/Products/Product/Metrics/index.js @@ -0,0 +1,138 @@ +// @flow strict + +import React, { PureComponent } from 'react'; +import classNames from 'classnames'; + +import { InputNumber } from 'components/common'; +import FormItemTitle from 'pages/common/FormItemTitle'; + +import './Metrics.scss'; + +import t from './i18n'; + +type StateType = { + isWeightFocus: boolean, + isDimensionFocus: boolean, +}; + +type PropsType = { + lengthCm: number, + widthCm: number, + heightCm: number, + weightG: number, + onChangeMetrics: (metrics: { + lengthCm: number, + widthCm: number, + heightCm: number, + weightG: number, + }) => void, +}; + +class Metrics extends PureComponent { + constructor(props: PropsType) { + super(props); + + this.state = { + isWeightFocus: false, + isDimensionFocus: false, + }; + } + + handleOnChangeMetrics = (id: string, value: number) => { + const { weightG, lengthCm, widthCm, heightCm } = this.props; + const metrics = { weightG, lengthCm, widthCm, heightCm }; + this.props.onChangeMetrics({ + ...metrics, + [id]: value, + }); + }; + + handleOnFocusWeight = () => { + this.setState({ isWeightFocus: true }); + }; + + handleOnBlurWeight = () => { + this.setState({ isWeightFocus: false }); + }; + + handleOnFocusDimension = () => { + this.setState({ isDimensionFocus: true }); + }; + + handleOnBlurDimension = () => { + this.setState({ isDimensionFocus: false }); + }; + + renderDimensionInput = (id: string, value: number) => ( + { + this.handleOnChangeMetrics(id, dimensionValue); + }} + onFocus={this.handleOnFocusDimension} + onBlur={this.handleOnBlurDimension} + limit={3} + limitHidden + fullWidth + /> + ); + + render() { + const { lengthCm, widthCm, heightCm, weightG } = this.props; + + const { isWeightFocus, isDimensionFocus } = this.state; + + return ( +
+ +
+
+
+ {t.weight} * +
+
+ { + this.handleOnChangeMetrics('weightG', value); + }} + onFocus={this.handleOnFocusWeight} + onBlur={this.handleOnBlurWeight} + limit={6} + limitHidden + fullWidth + /> +
{t.g}
+
+
+
+
+ {t.dimensions} * +
+
+
+ {this.renderDimensionInput('widthCm', widthCm)} +
{t.width}
+
+
+ {this.renderDimensionInput('lengthCm', lengthCm)} +
{t.length}
+
+
+ {this.renderDimensionInput('heightCm', heightCm)} +
{t.height}
+
+
{t.sm}
+
+
+
+
+ ); + } +} + +export default Metrics; diff --git a/src/pages/Manage/Store/Products/Product/NewProduct/index.js b/src/pages/Manage/Store/Products/Product/NewProduct/index.js index b4ed3a95..83a277f6 100644 --- a/src/pages/Manage/Store/Products/Product/NewProduct/index.js +++ b/src/pages/Manage/Store/Products/Product/NewProduct/index.js @@ -54,6 +54,7 @@ type StateType = { availablePackages: ?AvailablePackagesType, shippingData: ?FullShippingType, customAttributes: Array, + isLoadingPackages: boolean, }; class NewProduct extends Component { @@ -63,9 +64,24 @@ class NewProduct extends Component { availablePackages: null, shippingData: null, customAttributes: [], + isLoadingPackages: false, }; componentDidMount() { + this.handleFetchPackages(); + } + + handleFetchPackages = (metrics?: { + lengthCm: number, + widthCm: number, + heightCm: number, + weightG: number, + }) => { + const size = metrics + ? metrics.lengthCm * metrics.widthCm * metrics.heightCm + : 0; + const weight = metrics ? metrics.weightG : 0; + this.setState({ isLoadingPackages: true }); // $FlowIgnore const warehouses = pathOr( null, @@ -74,22 +90,28 @@ class NewProduct extends Component { ); const warehouse = warehouses && !isEmpty(warehouses) ? head(warehouses) : null; - const countryCode = pathOr(null, ['addressFull', 'country'], warehouse); + const countryCode = pathOr(null, ['addressFull', 'countryCode'], warehouse); if (countryCode && process.env.BROWSER) { const variables = { countryCode: 'RUS', - size: 0, - weight: 0, + size, + weight, }; fetchPackages(this.props.environment, variables) .then(({ availablePackages }) => { - this.setState({ availablePackages: availablePackages || null }); + this.setState({ + availablePackages: availablePackages || null, + isLoadingPackages: false, + }); return true; }) .catch(() => { - this.setState({ availablePackages: null }); + this.setState({ + availablePackages: null, + isLoadingPackages: false, + }); this.props.showAlert({ type: 'danger', text: t.somethingGoingWrongWithShipping, @@ -97,7 +119,7 @@ class NewProduct extends Component { }); }); } - } + }; handleSave = ( form: FormType & { currency: string }, @@ -113,6 +135,7 @@ class NewProduct extends Component { shortDescription, longDescription, currency, + metrics, } = form; if (!categoryId) { @@ -165,6 +188,10 @@ class NewProduct extends Component { attributes: form.attributeValues, }, ], + lengthCm: metrics.lengthCm, + widthCm: metrics.widthCm, + heightCm: metrics.heightCm, + weightG: metrics.weightG, }, }, }) @@ -375,8 +402,8 @@ class NewProduct extends Component { shippingData, customAttributes, formErrors, + isLoadingPackages, } = this.state; - return ( {({ directories }) => ( @@ -395,6 +422,9 @@ class NewProduct extends Component { onCreateAttribute={this.handleCreateAttribute} onRemoveAttribute={this.handleRemoveAttribute} onResetAttribute={this.handleResetAttribute} + isLoadingPackages={isLoadingPackages} + onFetchPackages={this.handleFetchPackages} + handleFetchPackages={this.handleFetchPackages} /> )} diff --git a/src/pages/Manage/Store/Products/Product/Product.scss b/src/pages/Manage/Store/Products/Product/Product.scss index 2344174f..aa85b3d1 100644 --- a/src/pages/Manage/Store/Products/Product/Product.scss +++ b/src/pages/Manage/Store/Products/Product/Product.scss @@ -32,7 +32,7 @@ } &.titlePricing { - margin-top: 4rem; + margin-top: 6rem; } &.titleCharacteristics { @@ -147,7 +147,7 @@ } .preOrder { - margin-top: 5rem; + margin-top: 8rem; } .tabs { @@ -214,3 +214,47 @@ } } } + +.metrics { + position: relative; + margin-top: 7rem; + + .metricsError { + position: absolute; + bottom: -2.5rem; + width: 19rem; + margin-top: 0.5rem; + line-height: 2rem; + color: $color_red; + font-size: 12px; + } +} + +.shippingPopup { + padding: 10rem; + width: 82rem; + + @media (max-width: #{$md - 1px}) { + padding: 4rem; + width: 100%; + } + + .shippingPopupWrapper { + // + } + + .shippingPopupDescription { + text-align: center; + } + + .shippingPopupButtons { + display: flex; + justify-content: center; + margin-top: 4rem; + + .shippingPopupOkButton { + margin-left: 2rem; + } + } + +} \ No newline at end of file diff --git a/src/pages/Manage/Store/Products/Product/Shipping/handlerLocalShippingDecorator/index.js b/src/pages/Manage/Store/Products/Product/Shipping/handlerLocalShippingDecorator/index.js index c4f87e69..af3a147d 100644 --- a/src/pages/Manage/Store/Products/Product/Shipping/handlerLocalShippingDecorator/index.js +++ b/src/pages/Manage/Store/Products/Product/Shipping/handlerLocalShippingDecorator/index.js @@ -42,6 +42,10 @@ type StateType = { export default (OriginalComponent: ComponentType<*>): ComponentType<*> => class HandlerShippingDecorator extends Component { + // static getDerivedStateFromProps(nextProps: PropsType, prevState: StateType) { + // + // } + constructor(props: PropsType) { super(props); const { localShipping, onChangeShippingData, pickupShipping } = props; diff --git a/src/pages/Manage/Store/Products/types/index.js b/src/pages/Manage/Store/Products/types/index.js index e45aad3c..b293351b 100644 --- a/src/pages/Manage/Store/Products/types/index.js +++ b/src/pages/Manage/Store/Products/types/index.js @@ -12,6 +12,13 @@ export type AttributeValueType = { metaField?: ?string, }; +export type MetricsType = { + lengthCm: number, + widthCm: number, + heightCm: number, + weightG: number, +}; + export type FormType = { name: string, seoTitle: string, @@ -31,6 +38,7 @@ export type FormType = { preOrderDays: string, preOrder: boolean, attributeValues: Array, + metrics: MetricsType, }; export type VariantType = { @@ -141,6 +149,10 @@ export type BaseProductType = { }, }>, }, + lengthCm: ?number, + widthCm: ?number, + heightCm: ?number, + weightG: ?number, }; export type ValueForAttributeInputType = { diff --git a/src/pages/common/FormItemTitle/FormItemTitle.scss b/src/pages/common/FormItemTitle/FormItemTitle.scss new file mode 100644 index 00000000..d3ea4a63 --- /dev/null +++ b/src/pages/common/FormItemTitle/FormItemTitle.scss @@ -0,0 +1,6 @@ +.container { + font-size: 14px; + line-height: 18px; + text-transform: uppercase; + letter-spacing: 1.2px; +} \ No newline at end of file diff --git a/src/pages/common/FormItemTitle/index.js b/src/pages/common/FormItemTitle/index.js new file mode 100644 index 00000000..708d31b4 --- /dev/null +++ b/src/pages/common/FormItemTitle/index.js @@ -0,0 +1,13 @@ +// @flow strict + +import React from 'react'; + +import './FormItemTitle.scss'; + +const FormItemTitle = ({ title }: { title: string }) => ( +
+ {title} +
+); + +export default FormItemTitle; diff --git a/src/relay/mutations/CreateBaseProductWithVariantsMutation.js b/src/relay/mutations/CreateBaseProductWithVariantsMutation.js index b51fb67e..df8a666c 100644 --- a/src/relay/mutations/CreateBaseProductWithVariantsMutation.js +++ b/src/relay/mutations/CreateBaseProductWithVariantsMutation.js @@ -100,6 +100,10 @@ const mutation = graphql` id } } + lengthCm + widthCm + heightCm + weightG } } `; diff --git a/src/relay/mutations/UpdateBaseProductMutation.js b/src/relay/mutations/UpdateBaseProductMutation.js index 84dcd250..2511cfb8 100644 --- a/src/relay/mutations/UpdateBaseProductMutation.js +++ b/src/relay/mutations/UpdateBaseProductMutation.js @@ -75,6 +75,10 @@ const mutation = graphql` } } } + lengthCm + widthCm + heightCm + weightG } } `; @@ -88,6 +92,10 @@ export type UpdateBaseProductMutationVariablesType = { seoDescription: Array<{ lang: string, text: string }>, currency: string, categoryId: number, + lengthCm: number, + widthCm: number, + heightCm: number, + weightG: number, }; export type { UpdateBaseProductMutationResponseType }; @@ -116,6 +124,10 @@ const commit = (params: MutationParamsType) => seoDescription: params.seoDescription, currency: params.currency, categoryId: params.categoryId, + lengthCm: params.lengthCm, + widthCm: params.widthCm, + heightCm: params.heightCm, + weightG: params.weightG, }, }, onCompleted: params.onCompleted,