diff --git a/dev-server/Phone.js b/dev-server/Phone.js index 8e775a6336..c522632020 100644 --- a/dev-server/Phone.js +++ b/dev-server/Phone.js @@ -242,12 +242,32 @@ export default class Phone extends RcModule { tabManager: this.tabManager, getState: () => this.state.forwardingNumber, })); + reducers.forwardingNumber = this.forwardingNumber.reducer; this.addModule('contactMatcher', new ContactMatcher({ ...options, storage: this.storage, getState: () => this.state.contactMatcher, })); reducers.contactMatcher = this.contactMatcher.reducer; + this.addModule('regionSettings', new RegionSettings({ + ...options, + storage: this.storage, + extensionInfo: this.extensionInfo, + dialingPlan: this.dialingPlan, + alert: this.alert, + tabManager: this.tabManager, + getState: () => this.state.regionSettings, + })); + reducers.regionSettings = this.regionSettings.reducer; + this.addModule('numberValidate', new NumberValidate({ + ...options, + client: this.client, + accountExtension: this.accountExtension, + regionSettings: this.regionSettings, + accountInfo: this.accountInfo, + getState: () => this.state.numberValidate, + })); + reducers.numberValidate = this.numberValidate.reducer; this.addModule('webphone', new Webphone({ appKey: apiConfig.appKey, appName: 'RingCentral Widget', @@ -261,20 +281,10 @@ export default class Phone extends RcModule { webphoneLogLevel: 3, extensionDevice: this.extensionDevice, globalStorage: this.globalStorage, + numberValidate: this.numberValidate, getState: () => this.state.webphone, })); reducers.webphone = this.webphone.reducer; - reducers.forwardingNumber = this.forwardingNumber.reducer; - this.addModule('regionSettings', new RegionSettings({ - ...options, - storage: this.storage, - extensionInfo: this.extensionInfo, - dialingPlan: this.dialingPlan, - alert: this.alert, - tabManager: this.tabManager, - getState: () => this.state.regionSettings, - })); - reducers.regionSettings = this.regionSettings.reducer; this.addModule('callingSettings', new CallingSettings({ ...options, alert: this.alert, @@ -289,15 +299,6 @@ export default class Phone extends RcModule { getState: () => this.state.callingSettings, })); reducers.callingSettings = this.callingSettings.reducer; - this.addModule('numberValidate', new NumberValidate({ - ...options, - client: this.client, - accountExtension: this.accountExtension, - regionSettings: this.regionSettings, - accountInfo: this.accountInfo, - getState: () => this.state.numberValidate, - })); - reducers.numberValidate = this.numberValidate.reducer; this.addModule('call', new Call({ ...options, alert: this.alert, diff --git a/dev-server/containers/App/index.js b/dev-server/containers/App/index.js index 5bd0d4fcc6..ee21ad08c7 100644 --- a/dev-server/containers/App/index.js +++ b/dev-server/containers/App/index.js @@ -43,6 +43,7 @@ export default function App({ null; props.reject = () => null; props.toVoiceMail = () => null; props.currentLocale = 'en-US'; - +props.forwardingNumbers = [{ + id: '123', + label: 'Mobile', + phoneNumber: '12345678', +}]; +props.onForward = () => null; /** * A example of `IncomingCallPad` */ diff --git a/docs/src/app/pages/Components/IncomingCallPanel/Demo.js b/docs/src/app/pages/Components/IncomingCallPanel/Demo.js index 7d0841b100..1e671ecd27 100644 --- a/docs/src/app/pages/Components/IncomingCallPanel/Demo.js +++ b/docs/src/app/pages/Components/IncomingCallPanel/Demo.js @@ -17,6 +17,12 @@ props.phoneNumber = '1234567890'; props.selectedMatcherIndex = 0; props.onSelectMatcherName = () => null; props.onBackButtonClick = () => null; +props.forwardingNumbers = [{ + id: '123', + label: 'Mobile', + phoneNumber: '12345678', +}]; +props.onForward = () => null; /** * A example of `IncomingCallPanel` */ diff --git a/src/components/CircleButton/styles.scss b/src/components/CircleButton/styles.scss index 5b9f0ce5bb..df0d98804a 100644 --- a/src/components/CircleButton/styles.scss +++ b/src/components/CircleButton/styles.scss @@ -24,8 +24,8 @@ fill: transparent; stroke-width: 10; font-weight: 100; - stroke: $primary-color; - opacity: 0.3; + stroke: $primary-color-highlight; + opacity: 0.9; } .noBorder { @@ -38,7 +38,7 @@ &:hover { .circle { - opacity: 0.5; + stroke: $primary-color-highlight-solid; } .noBorder { stroke: transparent; diff --git a/src/components/ForwardForm/i18n/en-US.js b/src/components/ForwardForm/i18n/en-US.js new file mode 100644 index 0000000000..87848b1ea6 --- /dev/null +++ b/src/components/ForwardForm/i18n/en-US.js @@ -0,0 +1,5 @@ +export default { + forward: 'Forward', + cancel: 'Cancel', + customNumber: 'Custom number' +}; diff --git a/src/components/ForwardForm/i18n/index.js b/src/components/ForwardForm/i18n/index.js new file mode 100644 index 0000000000..25458f23b1 --- /dev/null +++ b/src/components/ForwardForm/i18n/index.js @@ -0,0 +1,4 @@ +import I18n from 'ringcentral-integration/lib/I18n'; +import loadLocale from './loadLocale'; + +export default new I18n(loadLocale); diff --git a/src/components/ForwardForm/i18n/loadLocale.js b/src/components/ForwardForm/i18n/loadLocale.js new file mode 100644 index 0000000000..12b11cfa2e --- /dev/null +++ b/src/components/ForwardForm/i18n/loadLocale.js @@ -0,0 +1 @@ +/* loadLocale */ diff --git a/src/components/ForwardForm/index.js b/src/components/ForwardForm/index.js new file mode 100644 index 0000000000..d65ae6703a --- /dev/null +++ b/src/components/ForwardForm/index.js @@ -0,0 +1,208 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import isBlank from 'ringcentral-integration/lib/isBlank'; + +import TextInput from '../TextInput'; +import Button from '../Button'; + +import styles from './styles.scss'; +import i18n from './i18n'; + +const cleanRegex = /[^\d+*-]/g; + +function ForwardNumbers({ + numbers, + onSelect, + selected, + formatPhone, +}) { + return ( +
+ { + numbers.map((number, index) => ( +
onSelect(index)} + > + {number.label} + : + {formatPhone(number.phoneNumber)} +
+ )) + } +
+ ); +} + +ForwardNumbers.propTypes = { + numbers: PropTypes.array.isRequired, + onSelect: PropTypes.func.isRequired, + selected: PropTypes.number.isRequired, + formatPhone: PropTypes.func.isRequired, +}; + +export default class ForwardForm extends Component { + constructor(props) { + super(props); + this.state = { + selectedIndex: 0, + customValue: '', + handling: false, + }; + + this.filter = value => value.replace(cleanRegex, ''); + this.onCustomValueChange = (e) => { + const value = e.currentTarget.value; + const cleanValue = this.filter(value); + this.setState({ + customValue: cleanValue, + }); + if (typeof this.props.onChange === 'function') { + this.props.onChange(cleanValue); + } + }; + + this.onSelect = (index) => { + this.setState({ + selectedIndex: index, + }); + if (typeof this.props.onChange === 'function') { + this.props.onChange(this.getValue()); + } + }; + + this.onForward = async () => { + this.setState({ + handling: true, + }); + const result = await this.props.onForward(this.getValue()); + if (!this._mounted) { + return; + } + this.setState({ + handling: false, + }); + if (result) { + this.props.onCancel(); + } + }; + + this.onSelectCustomNumber = () => { + this.onSelect(this.props.forwardingNumbers.length); + if (this.customInput && this.customInput.input) { + setTimeout(() => { + this.customInput.input.focus(); + }, 100); + } + }; + } + + componentDidMount() { + this._mounted = true; + this.focusInput(); + } + + componentWillUnmount() { + this._mounted = false; + } + + getValue() { + if (this.state.selectedIndex < this.props.forwardingNumbers.length) { + const forwardingNumber = this.props.forwardingNumbers[this.state.selectedIndex]; + return ( + forwardingNumber && forwardingNumber.phoneNumber + ); + } + return this.state.customValue; + } + + focusInput() { + if ( + this.state.selectedIndex === this.props.forwardingNumbers.length && + this.customInput && + this.customInput.input + ) { + this.customInput.input.focus(); + } + } + + render() { + const { + className, + onCancel, + currentLocale, + forwardingNumbers, + formatPhone, + } = this.props; + const value = this.getValue(); + const disableButton = isBlank(value) && !this.state.handling; + return ( +
+ +
+
+ {i18n.getString('customNumber', currentLocale)} +
+ { this.customInput = input; }} + filter={this.filter} + className={styles.customInput} + value={this.state.customValue} + onChange={this.onCustomValueChange} + /> +
+
+ + +
+
+ ); + } +} + +ForwardForm.propTypes = { + className: PropTypes.string, + onCancel: PropTypes.func.isRequired, + currentLocale: PropTypes.string.isRequired, + forwardingNumbers: PropTypes.array.isRequired, + formatPhone: PropTypes.func.isRequired, + onForward: PropTypes.func.isRequired, + onChange: PropTypes.func, +}; + +ForwardForm.defaultProps = { + className: null, + onChange: undefined, +}; diff --git a/src/components/ForwardForm/styles.scss b/src/components/ForwardForm/styles.scss new file mode 100644 index 0000000000..0b41d259d8 --- /dev/null +++ b/src/components/ForwardForm/styles.scss @@ -0,0 +1,120 @@ +@import '../../lib/commonStyles/fonts.scss'; +@import '../../lib/commonStyles/colors.scss'; + +.root { + box-sizing: border-box; + width: 100%; + display: block; + padding: 0 0 20px 0; + background: #ffffff; +} + +.buttonGroup { + @include primary-font; + display: block; + margin-top: 10px; + padding: 0 20px; +} + +.cancelButton { + font-size: 12px; + color: $primary-color; + display: inline-block; + width: 65px; + height: 28px; + border-radius: 100px; + border: solid 1px $primary-color-highlight; + line-height: 28px; + text-align: center; + margin-right: 20px; + opacity: 0.9; + &:hover { + color: $primary-color; + } +} + +.forwardButton { + display: inline-block; + width: 65px; + height: 28px; + border-radius: 100px; + background: $primary-color; + font-size: 12px; + color: #ffffff; + line-height: 28px; + text-align: center; + &:hover { + color: #ffffff; + } + &.disabled { + background: #ffffff; + border: solid 1px #e3e3e3; + .buttonText { + color: $darkergray; + } + } +} + +.customInput { + display: none; + padding: 0; + background: #ffffff; + border: none; + margin-top: 10px; + input { + @include secondary-font; + padding: 0; + font-size: 12px; + line-height: 14px; + padding: 7px 10px; + } +} + +.custromNumber { + padding: 5px 20px 10px 20px; + .customLabel { + @include secondary-font; + font-size: 14px; + line-height: 16px; + } + &.active { + .customLabel { + color: $primary-color; + } + .customInput { + display: block; + } + } +} + +.active { + color: $primary-color; + background-color: #f5f5f5; +} + +.numbers { + @include secondary-font; + width: 100%; + font-size: 14px; + line-height: 18px; + max-height: 80px; + overflow-y: auto; + overflow-x: hidden; + .number { + box-sizing: border-box; + height: 28px; + padding: 5px 20px; + span { + display: inline-block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + .label { + max-width: 38px; + } + .colon { + padding-right: 5px; + } + } +} diff --git a/src/components/IncomingCallPad/index.js b/src/components/IncomingCallPad/index.js index 65eabbc8da..915aa003b3 100644 --- a/src/components/IncomingCallPad/index.js +++ b/src/components/IncomingCallPad/index.js @@ -1,5 +1,9 @@ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import Tooltip from 'rc-tooltip'; +import 'rc-tooltip/assets/bootstrap_white.css'; + +import ForwardForm from '../ForwardForm'; import ActiveCallButton from '../ActiveCallButton'; import MessageIcon from '../../assets/images/MessageFill.svg'; import ForwardIcon from '../../assets/images/Forward.svg'; @@ -10,49 +14,105 @@ import styles from './styles.scss'; import i18n from './i18n'; -export default function IncomingCallPad(props) { - return ( -
-
- null} - icon={ForwardIcon} - title={i18n.getString('forward', props.currentLocale)} - className={styles.callButton} - /> - null} - icon={MessageIcon} - title={i18n.getString('reply', props.currentLocale)} - className={styles.callButton} - /> - -
-
- - { + this.setState({ + showForward: visible, + forwardNumber: '', + }); + }; + this.onForwardNumberChange = (forwardNumber) => { + this.setState({ forwardNumber }); + }; + this.closeForwardForm = () => { + this.onShowForwardChange(false); + }; + } + + render() { + const { + currentLocale, + reject, + toVoiceMail, + answer, + forwardingNumbers, + formatPhone, + } = this.props; + return ( +
+
{ + this.forwardContainner = containner; + }} /> +
+ } + getTooltipContainer={() => this.forwardContainner} + overlay={ + + } + > + null} + title={i18n.getString('forward', currentLocale)} + className={styles.callButton} + /> + + console.log('test')} + icon={MessageIcon} + title={i18n.getString('reply', currentLocale)} + className={styles.callButton} + /> + +
+
+ + +
-
- ); + ); + } } IncomingCallPad.propTypes = { @@ -60,4 +120,11 @@ IncomingCallPad.propTypes = { reject: PropTypes.func.isRequired, toVoiceMail: PropTypes.func.isRequired, currentLocale: PropTypes.string.isRequired, + forwardingNumbers: PropTypes.array.isRequired, + formatPhone: PropTypes.func, + onForward: PropTypes.func.isRequired, +}; + +IncomingCallPad.defaultProps = { + formatPhone: phone => phone, }; diff --git a/src/components/IncomingCallPad/styles.scss b/src/components/IncomingCallPad/styles.scss index 9d505218bc..a47b186b0a 100644 --- a/src/components/IncomingCallPad/styles.scss +++ b/src/components/IncomingCallPad/styles.scss @@ -1,4 +1,5 @@ .root { + position: relative; margin-left: 5%; margin-right: 5%; } @@ -10,6 +11,7 @@ .callButton { width: 33.33%; + padding: 0 5px 5px 5px; } .bigCallButton { @@ -29,3 +31,29 @@ composes: rejectButton; background: #4cd964; } + +.forwardContainner{ + display: block; + width: 60%; + height: 1px; + :global .rc-tooltip { + border: solid 1px #e3e3e3; + border-radius: 5px; + opacity: 1; + width: 79%; + min-width: 201px; + background: #fff; + } + :global .rc-tooltip-inner { + padding: 0; + border: none; + } + :global .rc-tooltip-arrow { + border-top-color: #e3e3e3; + } + :global .rc-tooltip-placement-topLeft { + .rc-tooltip-arrow { + left: 20.2%; + } + } +} diff --git a/src/components/IncomingCallPanel/index.js b/src/components/IncomingCallPanel/index.js index 97f8148b83..8301c735d8 100644 --- a/src/components/IncomingCallPanel/index.js +++ b/src/components/IncomingCallPanel/index.js @@ -86,10 +86,13 @@ export default function IncomingCallPanel(props) { avatarUrl={props.avatarUrl} /> {props.children} @@ -115,6 +118,8 @@ IncomingCallPanel.propTypes = { onSelectMatcherName: PropTypes.func.isRequired, avatarUrl: PropTypes.string, onBackButtonClick: PropTypes.func.isRequired, + forwardingNumbers: PropTypes.array.isRequired, + onForward: PropTypes.func.isRequired, }; IncomingCallPanel.defaultProps = { diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.js index 8fd2c322c5..6cb59e3a75 100644 --- a/src/components/TextInput/index.js +++ b/src/components/TextInput/index.js @@ -9,6 +9,7 @@ class TextInput extends Component { this.state = { value: props.value, }; + this.input = null; } componentWillReceiveProps(nextProps) { if (nextProps.value !== this.props.value) { @@ -18,9 +19,11 @@ class TextInput extends Component { } } onInputChange = (e) => { - this.setState({ - value: e.currentTarget.value, - }); + let value = e.currentTarget.value; + if (typeof this.props.filter === 'function') { + value = this.props.filter(value); + } + this.setState({ value }); if (typeof this.props.onChange === 'function') { this.props.onChange(e); } @@ -49,6 +52,7 @@ class TextInput extends Component { invalid && styles.invalid, )}> { this.input = input; }} onChange={this.onInputChange} placeholder={placeholder} disabled={disabled} @@ -116,6 +120,7 @@ TextInput.propTypes = { defaultValue: PropTypes.string, invalid: PropTypes.bool, onKeyDown: PropTypes.func, + filter: PropTypes.func, }; TextInput.defaultProps = { className: undefined, @@ -130,6 +135,7 @@ TextInput.defaultProps = { defaultValue: undefined, invalid: false, onKeyDown: undefined, + filter: undefined, }; export default TextInput; diff --git a/src/containers/ActiveCallPage/index.js b/src/containers/ActiveCallPage/index.js index bd5bc64ff2..1371b41328 100644 --- a/src/containers/ActiveCallPage/index.js +++ b/src/containers/ActiveCallPage/index.js @@ -5,6 +5,7 @@ import formatNumber from 'ringcentral-integration/lib/formatNumber'; import Webphone from 'ringcentral-integration/modules/Webphone'; import Locale from 'ringcentral-integration/modules/Locale'; import RegionSettings from 'ringcentral-integration/modules/RegionSettings'; +import ForwardingNumber from 'ringcentral-integration/modules/ForwardingNumber'; import callDirections from 'ringcentral-integration/enums/callDirections'; import sessionStatus from 'ringcentral-integration/modules/Webphone/sessionStatus'; @@ -73,6 +74,8 @@ class ActiveCallPage extends Component { this.props.toVoiceMail(this.props.session.id); this.replyWithMessage = message => this.props.replyWithMessage(this.props.session.id, message); + this.onForward = forwardNumber => + this.props.onForward(this.props.session.id, forwardNumber); } componentWillReceiveProps(nextProps) { @@ -146,6 +149,8 @@ class ActiveCallPage extends Component { onSelectMatcherName={this.onSelectMatcherName} avatarUrl={this.state.avatarUrl} onBackButtonClick={this.props.toggleMinimized} + forwardingNumbers={this.props.forwardingNumbers} + onForward={this.onForward} > {this.props.children} @@ -220,6 +225,8 @@ ActiveCallPage.propTypes = { areaCode: PropTypes.string.isRequired, countryCode: PropTypes.string.isRequired, getAvatarUrl: PropTypes.func.isRequired, + forwardingNumbers: PropTypes.array.isRequired, + onForward: PropTypes.func.isRequired, }; ActiveCallPage.defaultProps = { @@ -231,6 +238,7 @@ function mapToProps(_, { locale, contactMatcher, regionSettings, + forwardingNumber, }) { const currentSession = webphone.currentSession || {}; const contactMapping = contactMatcher && contactMatcher.dataMapping; @@ -242,6 +250,7 @@ function mapToProps(_, { minimized: webphone.minimized, areaCode: regionSettings.areaCode, countryCode: regionSettings.countryCode, + forwardingNumbers: forwardingNumber.forwardingNumbers, }; } @@ -272,6 +281,7 @@ function mapToFunctions(_, { }, sendDTMF: (value, sessionId) => webphone.sendDTMF(value, sessionId), toVoiceMail: sessionId => webphone.toVoiceMail(sessionId), + onForward: (sessionId, forwardNumber) => webphone.forward(sessionId, forwardNumber), replyWithMessage: (sessionId, message) => webphone.replyWithMessage(sessionId, message), toggleMinimized: () => webphone.toggleMinimized(), getAvatarUrl, @@ -287,6 +297,7 @@ ActiveCallContainer.propTypes = { webphone: PropTypes.instanceOf(Webphone).isRequired, locale: PropTypes.instanceOf(Locale).isRequired, regionSettings: PropTypes.instanceOf(RegionSettings).isRequired, + forwardingNumber: PropTypes.instanceOf(ForwardingNumber).isRequired, getAvatarUrl: PropTypes.func, };