From 28d0ba98697be7d6d7b7a651b75290e8dfab3c9e Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 4 Dec 2023 16:07:45 +0100 Subject: [PATCH 01/15] Fix error shows up when saving existing and valid room name with any changes --- src/components/Form/FormProvider.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index af2511fc9f74..5a3970cb8ce1 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -224,7 +224,12 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC inputValues[inputID] = propsToParse.defaultValue; } else if (_.isUndefined(inputValues[inputID])) { // We want to initialize the input value if it's undefined - inputValues[inputID] = _.isUndefined(propsToParse.defaultValue) ? getInitialValueByType(propsToParse.valueType) : propsToParse.defaultValue; + const initialValue = _.isUndefined(propsToParse.defaultValue) ? getInitialValueByType(propsToParse.valueType) : propsToParse.defaultValue; + + inputValues[inputID] = _.isFunction(propsToParse.valueParser) ? propsToParse.valueParser(initialValue) : initialValue; + if (_.isFunction(propsToParse.displayParser)) { + inputValues[`${inputID}ToDisplay`] = propsToParse.displayParser(initialValue); + } } const errorFields = lodashGet(formState, 'errorFields', {}); From 9fa1591ccddc5d24754b0af98137d496c042dc69 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 4 Dec 2023 19:37:23 +0100 Subject: [PATCH 02/15] Revert "Revert "[Form Provider Refactor] RoomNameInput fixes"" This reverts commit a60110bf0dd0a925c05f8026b729c4cfb0320852. --- src/components/Form/FormProvider.js | 22 +++++-- src/components/Form/InputWrapper.js | 4 ++ src/components/RoomNameInput/index.js | 53 ++++----------- src/components/RoomNameInput/index.native.js | 66 ------------------- .../RoomNameInput/roomNameInputPropTypes.js | 9 ++- src/libs/RoomNameInputUtils.ts | 4 +- src/pages/settings/Report/RoomNamePage.js | 16 ++--- src/pages/workspace/WorkspaceNewRoomPage.js | 19 ++++-- 8 files changed, 59 insertions(+), 134 deletions(-) delete mode 100644 src/components/RoomNameInput/index.native.js diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 5a3970cb8ce1..42d0d2bf9d81 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -242,6 +242,8 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC .first() .value() || ''; + const value = !_.isUndefined(inputValues[`${inputID}ToDisplay`]) ? inputValues[`${inputID}ToDisplay`] : inputValues[inputID]; + return { ...propsToParse, ref: @@ -254,7 +256,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC inputID, key: propsToParse.key || inputID, errorText: errors[inputID] || fieldErrorMessage, - value: inputValues[inputID], + value, // As the text input is controlled, we never set the defaultValue prop // as this is already happening by the value prop. defaultValue: undefined, @@ -314,13 +316,19 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC propsToParse.onBlur(event); } }, - onInputChange: (value, key) => { + onInputChange: (inputValue, key) => { const inputKey = key || inputID; setInputValues((prevState) => { - const newState = { - ...prevState, - [inputKey]: value, - }; + const newState = _.isFunction(propsToParse.valueParser) + ? { + ...prevState, + [inputKey]: propsToParse.valueParser(inputValue), + [`${inputKey}ToDisplay`]: _.isFunction(propsToParse.displayParser) ? propsToParse.displayParser(inputValue) : inputValue, + } + : { + ...prevState, + [inputKey]: inputValue, + }; if (shouldValidateOnChange) { onValidate(newState); @@ -333,7 +341,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC } if (_.isFunction(propsToParse.onValueChange)) { - propsToParse.onValueChange(value, inputKey); + propsToParse.onValueChange(inputValue, inputKey); } }, }; diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index 9a31210195c4..11e74d2759b1 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -9,11 +9,15 @@ const propTypes = { inputID: PropTypes.string.isRequired, valueType: PropTypes.string, forwardedRef: refPropTypes, + valueParser: PropTypes.func, + displayParser: PropTypes.func, }; const defaultProps = { forwardedRef: undefined, valueType: 'string', + valueParser: undefined, + displayParser: undefined, }; function InputWrapper(props) { diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js index 14529d7b594a..e32c264a57cc 100644 --- a/src/components/RoomNameInput/index.js +++ b/src/components/RoomNameInput/index.js @@ -1,49 +1,24 @@ -import React, {useState} from 'react'; -import _ from 'underscore'; +import React from 'react'; +import InputWrapper from '@components/Form/InputWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; +import getOperatingSystem from '@libs/getOperatingSystem'; import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, onBlur, shouldDelayFocus, inputID, roomName}) { const {translate} = useLocalize(); - const [selection, setSelection] = useState(); + const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; - /** - * Calls the onChangeText callback with a modified room name - * @param {Event} event - */ - const setModifiedRoomName = (event) => { - const roomName = event.nativeEvent.text; - const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); - onChangeText(modifiedRoomName); - - // if custom component has onInputChange, use it to trigger changes (Form input) - if (_.isFunction(onInputChange)) { - onInputChange(modifiedRoomName); - } - - // Prevent cursor jump behaviour: - // Check if newRoomNameWithHash is the same as modifiedRoomName - // If it is then the room name is valid (does not contain unallowed characters); no action required - // If not then the room name contains unvalid characters and we must adjust the cursor position manually - // Read more: https://github.com/Expensify/App/issues/12741 - const oldRoomNameWithHash = value || ''; - const newRoomNameWithHash = `${CONST.POLICY.ROOM_PREFIX}${roomName}`; - if (modifiedRoomName !== newRoomNameWithHash) { - const offset = modifiedRoomName.length - oldRoomNameWithHash.length; - const newSelection = { - start: selection.start + offset, - end: selection.end + offset, - }; - setSelection(newSelection); - } - }; + const valueParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName); + const displayParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName, true); return ( - setSelection(event.nativeEvent.selection)} errorText={errorText} + valueParser={valueParser} + displayParser={displayParser} autoCapitalize="none" onBlur={(event) => isFocused && onBlur(event)} shouldDelayFocus={shouldDelayFocus} autoFocus={isFocused && autoFocus} maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} + defaultValue={roomName} spellCheck={false} shouldInterceptSwipe + keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 /> ); } diff --git a/src/components/RoomNameInput/index.native.js b/src/components/RoomNameInput/index.native.js deleted file mode 100644 index a2c09996ad34..000000000000 --- a/src/components/RoomNameInput/index.native.js +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import _ from 'underscore'; -import TextInput from '@components/TextInput'; -import useLocalize from '@hooks/useLocalize'; -import getOperatingSystem from '@libs/getOperatingSystem'; -import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; -import CONST from '@src/CONST'; -import * as roomNameInputPropTypes from './roomNameInputPropTypes'; - -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) { - const {translate} = useLocalize(); - - /** - * Calls the onChangeText callback with a modified room name - * @param {Event} event - */ - const setModifiedRoomName = (event) => { - const roomName = event.nativeEvent.text; - const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); - onChangeText(modifiedRoomName); - - // if custom component has onInputChange, use it to trigger changes (Form input) - if (_.isFunction(onInputChange)) { - onInputChange(modifiedRoomName); - } - }; - - const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; - - return ( - isFocused && onBlur(event)} - autoFocus={isFocused && autoFocus} - autoCapitalize="none" - shouldDelayFocus={shouldDelayFocus} - /> - ); -} - -RoomNameInput.propTypes = roomNameInputPropTypes.propTypes; -RoomNameInput.defaultProps = roomNameInputPropTypes.defaultProps; -RoomNameInput.displayName = 'RoomNameInput'; - -const RoomNameInputWithRef = React.forwardRef((props, ref) => ( - -)); - -RoomNameInputWithRef.displayName = 'RoomNameInputWithRef'; - -export default RoomNameInputWithRef; diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 7f8292f0123e..f457e4e2a494 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,4 +1,5 @@ import PropTypes from 'prop-types'; +import refPropTypes from '@components/refPropTypes'; const propTypes = { /** Callback to execute when the text input is modified correctly */ @@ -14,10 +15,10 @@ const propTypes = { errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), /** A ref forwarded to the TextInput */ - forwardedRef: PropTypes.func, + forwardedRef: refPropTypes, /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, + inputID: PropTypes.string.isRequired, /** Callback that is called when the text input is blurred */ onBlur: PropTypes.func, @@ -30,6 +31,8 @@ const propTypes = { /** Whether navigation is focused */ isFocused: PropTypes.bool.isRequired, + + roomName: PropTypes.string, }; const defaultProps = { @@ -39,10 +42,10 @@ const defaultProps = { errorText: '', forwardedRef: () => {}, - inputID: undefined, onBlur: () => {}, autoFocus: false, shouldDelayFocus: false, + roomName: '', }; export {propTypes, defaultProps}; diff --git a/src/libs/RoomNameInputUtils.ts b/src/libs/RoomNameInputUtils.ts index cff0bbc30274..e6f7d420bf59 100644 --- a/src/libs/RoomNameInputUtils.ts +++ b/src/libs/RoomNameInputUtils.ts @@ -3,14 +3,14 @@ import CONST from '@src/CONST'; /** * Replaces spaces with dashes */ -function modifyRoomName(roomName: string): string { +function modifyRoomName(roomName: string, skipPolicyPrefix?: boolean): string { const modifiedRoomNameWithoutHash = roomName .replace(/ /g, '-') // Replaces the smart dash on iOS devices with two hyphens .replace(/—/g, '--'); - return `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; + return skipPolicyPrefix ? modifiedRoomNameWithoutHash : `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; } export { diff --git a/src/pages/settings/Report/RoomNamePage.js b/src/pages/settings/Report/RoomNamePage.js index 7916043c7e20..f8b6568733c7 100644 --- a/src/pages/settings/Report/RoomNamePage.js +++ b/src/pages/settings/Report/RoomNamePage.js @@ -4,7 +4,7 @@ import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import Form from '@components/Form'; +import FormProvider from '@components/Form/FormProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import RoomNameInput from '@components/RoomNameInput'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -42,13 +42,8 @@ const defaultProps = { policy: {}, }; -function RoomNamePage(props) { +function RoomNamePage({policy, report, reports, translate}) { const styles = useThemeStyles(); - const policy = props.policy; - const report = props.report; - const reports = props.reports; - const translate = props.translate; - const roomNameInputRef = useRef(null); const isFocused = useIsFocused(); @@ -91,7 +86,7 @@ function RoomNamePage(props) { title={translate('newRoomPage.roomName')} onBackButtonPress={() => Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID))} /> -
Report.updatePolicyRoomNameAndNavigate(report, values.roomName)} @@ -101,13 +96,14 @@ function RoomNamePage(props) { > (roomNameInputRef.current = ref)} + ref={roomNameInputRef} inputID="roomName" defaultValue={report.reportName} isFocused={isFocused} + roomName={report.reportName.slice(1)} /> -
+ ); diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 7ddd78621d2d..92f2ab3dd5a6 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -4,7 +4,8 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import Form from '@components/Form'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; import RoomNameInput from '@components/RoomNameInput'; @@ -239,7 +240,7 @@ function WorkspaceNewRoomPage(props) { // This is because when wrapping whole screen the screen was freezing when changing Tabs. keyboardVerticalOffset={variables.contentHeaderHeight + variables.tabSelectorButtonHeight + variables.tabSelectorButtonPadding + insets.top} > -
- - {isPolicyAdmin && ( - )} - {visibilityDescription} - + {isSmallScreenWidth && } )} From b9fc2f4b464f81c286f2e8ab9905625a8b8797fd Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 4 Dec 2023 20:08:58 +0100 Subject: [PATCH 03/15] Fix inconsistent error message --- src/components/Form/FormProvider.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 42d0d2bf9d81..847750f78bf5 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -125,7 +125,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC } FormActions.setErrorFields(formID, null); - const validateErrors = validate(values) || {}; + const validateErrors = validate(trimmedStringValues) || {}; // Validate the input for html tags. It should supercede any other error _.each(trimmedStringValues, (inputValue, inputID) => { @@ -154,6 +154,11 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC } } } + + if (isMatch && leadingSpaceIndex === -1) { + return; + } + // Add a validation error here because it is a string value that contains HTML characters validateErrors[inputID] = 'common.error.invalidCharacter'; }); From cc75622457c9c26a12ce60943e27c5bf224f1dc6 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 5 Dec 2023 10:22:22 +0100 Subject: [PATCH 04/15] Fix last character of input fields is removed after coming back to form --- src/components/Form/FormProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 847750f78bf5..5576c861d96d 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -342,7 +342,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC }); if (propsToParse.shouldSaveDraft) { - FormActions.setDraftValues(formID, {[inputKey]: value}); + FormActions.setDraftValues(formID, {[inputKey]: _.isFunction(propsToParse.valueParser) ? propsToParse.valueParser(inputValue) : inputValue}); } if (_.isFunction(propsToParse.onValueChange)) { From 02756ba5dcb00e05ac1553149034e5be989989ef Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Fri, 8 Dec 2023 16:03:16 +0100 Subject: [PATCH 05/15] Code review changes --- src/components/RoomNameInput.js | 97 +++++++++++++++++++ src/components/RoomNameInput/index.js | 59 ----------- .../RoomNameInput/roomNameInputPropTypes.js | 51 ---------- src/pages/settings/Report/RoomNamePage.js | 14 ++- src/pages/workspace/WorkspaceNewRoomPage.js | 19 +++- 5 files changed, 125 insertions(+), 115 deletions(-) create mode 100644 src/components/RoomNameInput.js delete mode 100644 src/components/RoomNameInput/index.js delete mode 100644 src/components/RoomNameInput/roomNameInputPropTypes.js diff --git a/src/components/RoomNameInput.js b/src/components/RoomNameInput.js new file mode 100644 index 000000000000..fbc10b306a6e --- /dev/null +++ b/src/components/RoomNameInput.js @@ -0,0 +1,97 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import useLocalize from '@hooks/useLocalize'; +import getOperatingSystem from '@libs/getOperatingSystem'; +import CONST from '@src/CONST'; +import refPropTypes from './refPropTypes'; +import TextInput from './TextInput'; + +const propTypes = { + /** Callback to execute when the text input is modified correctly */ + onChangeText: PropTypes.func, + + /** Room name to show in input field. This should include the '#' already prefixed to the name */ + value: PropTypes.string, + + /** Whether we should show the input as disabled */ + disabled: PropTypes.bool, + + /** Error text to show */ + errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + + /** A ref forwarded to the TextInput */ + forwardedRef: refPropTypes, + + /** The ID used to uniquely identify the input in a Form */ + inputID: PropTypes.string.isRequired, + + /** Callback that is called when the text input is blurred */ + onBlur: PropTypes.func, + + /** AutoFocus */ + autoFocus: PropTypes.bool, + + /** Whether we should wait before focusing the TextInput, useful when using transitions on Android */ + shouldDelayFocus: PropTypes.bool, + + /** Whether navigation is focused */ + isFocused: PropTypes.bool.isRequired, + + roomName: PropTypes.string, +}; + +const defaultProps = { + onChangeText: () => {}, + value: '', + disabled: false, + errorText: '', + forwardedRef: () => {}, + + onBlur: () => {}, + autoFocus: false, + shouldDelayFocus: false, + roomName: '', +}; + +function RoomNameInput({isFocused, autoFocus, disabled, forwardedRef, onBlur, shouldDelayFocus, ...restProps}) { + const {translate} = useLocalize(); + + const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; + + return ( + isFocused && onBlur(event)} + shouldDelayFocus={shouldDelayFocus} + autoFocus={isFocused && autoFocus} + maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} + spellCheck={false} + shouldInterceptSwipe + keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 + /> + ); +} + +RoomNameInput.propTypes = propTypes; +RoomNameInput.defaultProps = defaultProps; +RoomNameInput.displayName = 'RoomNameInput'; + +const RoomNameInputWithRef = React.forwardRef((props, ref) => ( + +)); + +RoomNameInputWithRef.displayName = 'RoomNameInputWithRef'; + +export default RoomNameInputWithRef; diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js deleted file mode 100644 index e32c264a57cc..000000000000 --- a/src/components/RoomNameInput/index.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import InputWrapper from '@components/Form/InputWrapper'; -import TextInput from '@components/TextInput'; -import useLocalize from '@hooks/useLocalize'; -import getOperatingSystem from '@libs/getOperatingSystem'; -import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; -import CONST from '@src/CONST'; -import * as roomNameInputPropTypes from './roomNameInputPropTypes'; - -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, onBlur, shouldDelayFocus, inputID, roomName}) { - const {translate} = useLocalize(); - - const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; - - const valueParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName); - const displayParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName, true); - - return ( - isFocused && onBlur(event)} - shouldDelayFocus={shouldDelayFocus} - autoFocus={isFocused && autoFocus} - maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} - defaultValue={roomName} - spellCheck={false} - shouldInterceptSwipe - keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 - /> - ); -} - -RoomNameInput.propTypes = roomNameInputPropTypes.propTypes; -RoomNameInput.defaultProps = roomNameInputPropTypes.defaultProps; -RoomNameInput.displayName = 'RoomNameInput'; - -const RoomNameInputWithRef = React.forwardRef((props, ref) => ( - -)); - -RoomNameInputWithRef.displayName = 'RoomNameInputWithRef'; - -export default RoomNameInputWithRef; diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js deleted file mode 100644 index f457e4e2a494..000000000000 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ /dev/null @@ -1,51 +0,0 @@ -import PropTypes from 'prop-types'; -import refPropTypes from '@components/refPropTypes'; - -const propTypes = { - /** Callback to execute when the text input is modified correctly */ - onChangeText: PropTypes.func, - - /** Room name to show in input field. This should include the '#' already prefixed to the name */ - value: PropTypes.string, - - /** Whether we should show the input as disabled */ - disabled: PropTypes.bool, - - /** Error text to show */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), - - /** A ref forwarded to the TextInput */ - forwardedRef: refPropTypes, - - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string.isRequired, - - /** Callback that is called when the text input is blurred */ - onBlur: PropTypes.func, - - /** AutoFocus */ - autoFocus: PropTypes.bool, - - /** Whether we should wait before focusing the TextInput, useful when using transitions on Android */ - shouldDelayFocus: PropTypes.bool, - - /** Whether navigation is focused */ - isFocused: PropTypes.bool.isRequired, - - roomName: PropTypes.string, -}; - -const defaultProps = { - onChangeText: () => {}, - value: '', - disabled: false, - errorText: '', - forwardedRef: () => {}, - - onBlur: () => {}, - autoFocus: false, - shouldDelayFocus: false, - roomName: '', -}; - -export {propTypes, defaultProps}; diff --git a/src/pages/settings/Report/RoomNamePage.js b/src/pages/settings/Report/RoomNamePage.js index f8b6568733c7..7d41d55bf415 100644 --- a/src/pages/settings/Report/RoomNamePage.js +++ b/src/pages/settings/Report/RoomNamePage.js @@ -5,6 +5,7 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import RoomNameInput from '@components/RoomNameInput'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -13,6 +14,7 @@ import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; +import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; import reportPropTypes from '@pages/reportPropTypes'; @@ -47,6 +49,9 @@ function RoomNamePage({policy, report, reports, translate}) { const roomNameInputRef = useRef(null); const isFocused = useIsFocused(); + const valueParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName); + const displayParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName, true); + const validate = useCallback( (values) => { const errors = {}; @@ -95,12 +100,15 @@ function RoomNamePage({policy, report, reports, translate}) { enabledWhenOffline > - diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 92f2ab3dd5a6..4c65ac86c775 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -25,6 +25,7 @@ import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; @@ -160,6 +161,8 @@ function WorkspaceNewRoomPage(props) { setWriteCapability(CONST.REPORT.WRITE_CAPABILITIES.ALL); }, [isPolicyAdmin]); + const valueParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName); + const displayParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName, true); /** * @param {Object} values - form input values passed by the Form component * @returns {Boolean} @@ -191,7 +194,15 @@ function WorkspaceNewRoomPage(props) { [props.reports], ); - const workspaceOptions = useMemo(() => _.map(PolicyUtils.getActivePolicies(props.policies), (policy) => ({label: policy.name, key: policy.id, value: policy.id})), [props.policies]); + const workspaceOptions = useMemo( + () => + _.map(PolicyUtils.getActivePolicies(props.policies), (policy) => ({ + label: policy.name, + key: policy.id, + value: policy.id, + })), + [props.policies], + ); const writeCapabilityOptions = useMemo( () => @@ -249,12 +260,16 @@ function WorkspaceNewRoomPage(props) { enabledWhenOffline > - From c56d3af7e7da4728f40c95d0eaaa21bf96912e37 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 12 Dec 2023 16:27:17 +0100 Subject: [PATCH 06/15] Revert RoomNameInput changes --- src/components/Form/FormProvider.js | 32 ++---- src/components/Form/InputWrapper.js | 4 - src/components/RoomNameInput.js | 97 ------------------- src/components/RoomNameInput/index.js | 86 ++++++++++++++++ src/components/RoomNameInput/index.native.js | 68 +++++++++++++ .../RoomNameInput/roomNameInputPropTypes.js | 48 +++++++++ src/pages/settings/Report/RoomNamePage.js | 7 +- src/pages/workspace/WorkspaceNewRoomPage.js | 5 - 8 files changed, 212 insertions(+), 135 deletions(-) delete mode 100644 src/components/RoomNameInput.js create mode 100644 src/components/RoomNameInput/index.js create mode 100644 src/components/RoomNameInput/index.native.js create mode 100644 src/components/RoomNameInput/roomNameInputPropTypes.js diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 5576c861d96d..ece9390ce443 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -229,12 +229,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC inputValues[inputID] = propsToParse.defaultValue; } else if (_.isUndefined(inputValues[inputID])) { // We want to initialize the input value if it's undefined - const initialValue = _.isUndefined(propsToParse.defaultValue) ? getInitialValueByType(propsToParse.valueType) : propsToParse.defaultValue; - - inputValues[inputID] = _.isFunction(propsToParse.valueParser) ? propsToParse.valueParser(initialValue) : initialValue; - if (_.isFunction(propsToParse.displayParser)) { - inputValues[`${inputID}ToDisplay`] = propsToParse.displayParser(initialValue); - } + inputValues[inputID] = _.isUndefined(propsToParse.defaultValue) ? getInitialValueByType(propsToParse.valueType) : propsToParse.defaultValue; } const errorFields = lodashGet(formState, 'errorFields', {}); @@ -247,8 +242,6 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC .first() .value() || ''; - const value = !_.isUndefined(inputValues[`${inputID}ToDisplay`]) ? inputValues[`${inputID}ToDisplay`] : inputValues[inputID]; - return { ...propsToParse, ref: @@ -261,8 +254,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC inputID, key: propsToParse.key || inputID, errorText: errors[inputID] || fieldErrorMessage, - value, - // As the text input is controlled, we never set the defaultValue prop + value: inputValues[inputID], // As the text input is controlled, we never set the defaultValue prop // as this is already happening by the value prop. defaultValue: undefined, onTouched: (event) => { @@ -321,19 +313,13 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC propsToParse.onBlur(event); } }, - onInputChange: (inputValue, key) => { + onInputChange: (value, key) => { const inputKey = key || inputID; setInputValues((prevState) => { - const newState = _.isFunction(propsToParse.valueParser) - ? { - ...prevState, - [inputKey]: propsToParse.valueParser(inputValue), - [`${inputKey}ToDisplay`]: _.isFunction(propsToParse.displayParser) ? propsToParse.displayParser(inputValue) : inputValue, - } - : { - ...prevState, - [inputKey]: inputValue, - }; + const newState = { + ...prevState, + [inputKey]: value, + }; if (shouldValidateOnChange) { onValidate(newState); @@ -342,11 +328,11 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC }); if (propsToParse.shouldSaveDraft) { - FormActions.setDraftValues(formID, {[inputKey]: _.isFunction(propsToParse.valueParser) ? propsToParse.valueParser(inputValue) : inputValue}); + FormActions.setDraftValues(formID, {[inputKey]: value}); } if (_.isFunction(propsToParse.onValueChange)) { - propsToParse.onValueChange(inputValue, inputKey); + propsToParse.onValueChange(value, inputKey); } }, }; diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index 11e74d2759b1..9a31210195c4 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -9,15 +9,11 @@ const propTypes = { inputID: PropTypes.string.isRequired, valueType: PropTypes.string, forwardedRef: refPropTypes, - valueParser: PropTypes.func, - displayParser: PropTypes.func, }; const defaultProps = { forwardedRef: undefined, valueType: 'string', - valueParser: undefined, - displayParser: undefined, }; function InputWrapper(props) { diff --git a/src/components/RoomNameInput.js b/src/components/RoomNameInput.js deleted file mode 100644 index fbc10b306a6e..000000000000 --- a/src/components/RoomNameInput.js +++ /dev/null @@ -1,97 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import useLocalize from '@hooks/useLocalize'; -import getOperatingSystem from '@libs/getOperatingSystem'; -import CONST from '@src/CONST'; -import refPropTypes from './refPropTypes'; -import TextInput from './TextInput'; - -const propTypes = { - /** Callback to execute when the text input is modified correctly */ - onChangeText: PropTypes.func, - - /** Room name to show in input field. This should include the '#' already prefixed to the name */ - value: PropTypes.string, - - /** Whether we should show the input as disabled */ - disabled: PropTypes.bool, - - /** Error text to show */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), - - /** A ref forwarded to the TextInput */ - forwardedRef: refPropTypes, - - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string.isRequired, - - /** Callback that is called when the text input is blurred */ - onBlur: PropTypes.func, - - /** AutoFocus */ - autoFocus: PropTypes.bool, - - /** Whether we should wait before focusing the TextInput, useful when using transitions on Android */ - shouldDelayFocus: PropTypes.bool, - - /** Whether navigation is focused */ - isFocused: PropTypes.bool.isRequired, - - roomName: PropTypes.string, -}; - -const defaultProps = { - onChangeText: () => {}, - value: '', - disabled: false, - errorText: '', - forwardedRef: () => {}, - - onBlur: () => {}, - autoFocus: false, - shouldDelayFocus: false, - roomName: '', -}; - -function RoomNameInput({isFocused, autoFocus, disabled, forwardedRef, onBlur, shouldDelayFocus, ...restProps}) { - const {translate} = useLocalize(); - - const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; - - return ( - isFocused && onBlur(event)} - shouldDelayFocus={shouldDelayFocus} - autoFocus={isFocused && autoFocus} - maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} - spellCheck={false} - shouldInterceptSwipe - keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 - /> - ); -} - -RoomNameInput.propTypes = propTypes; -RoomNameInput.defaultProps = defaultProps; -RoomNameInput.displayName = 'RoomNameInput'; - -const RoomNameInputWithRef = React.forwardRef((props, ref) => ( - -)); - -RoomNameInputWithRef.displayName = 'RoomNameInputWithRef'; - -export default RoomNameInputWithRef; diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js new file mode 100644 index 000000000000..f94637be1e5c --- /dev/null +++ b/src/components/RoomNameInput/index.js @@ -0,0 +1,86 @@ +import React, {useState} from 'react'; +import _ from 'underscore'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import getOperatingSystem from '@libs/getOperatingSystem'; +import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; +import CONST from '@src/CONST'; +import * as roomNameInputPropTypes from './roomNameInputPropTypes'; + +function RoomNameInput({value, isFocused, autoFocus, disabled, forwardedRef, onBlur, shouldDelayFocus, onChangeText, onInputChange, ...restProps}) { + const {translate} = useLocalize(); + + const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; + const [selection, setSelection] = useState(); + + /** + * Calls the onChangeText callback with a modified room name + * @param {Event} event + */ + const setModifiedRoomName = (event) => { + const roomName = event.nativeEvent.text; + const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); + onChangeText(modifiedRoomName); + + // if custom component has onInputChange, use it to trigger changes (Form input) + if (_.isFunction(onInputChange)) { + onInputChange(modifiedRoomName); + } + + // Prevent cursor jump behaviour: + // Check if newRoomNameWithHash is the same as modifiedRoomName + // If it is then the room name is valid (does not contain unallowed characters); no action required + // If not then the room name contains unvalid characters and we must adjust the cursor position manually + // Read more: https://github.com/Expensify/App/issues/12741 + const oldRoomNameWithHash = value || ''; + const newRoomNameWithHash = `${CONST.POLICY.ROOM_PREFIX}${roomName}`; + if (modifiedRoomName !== newRoomNameWithHash) { + const offset = modifiedRoomName.length - oldRoomNameWithHash.length; + const newSelection = { + start: selection.start + offset, + end: selection.end + offset, + }; + setSelection(newSelection); + } + }; + return ( + isFocused && onBlur(event)} + shouldDelayFocus={shouldDelayFocus} + autoFocus={isFocused && autoFocus} + maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} + onSelectionChange={(event) => setSelection(event.nativeEvent.selection)} + spellCheck={false} + shouldInterceptSwipe + keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 + /> + ); +} + +RoomNameInput.propTypes = roomNameInputPropTypes.propTypes; +RoomNameInput.defaultProps = roomNameInputPropTypes.defaultProps; +RoomNameInput.displayName = 'RoomNameInput'; + +const RoomNameInputWithRef = React.forwardRef((props, ref) => ( + +)); + +RoomNameInputWithRef.displayName = 'RoomNameInputWithRef'; + +export default RoomNameInputWithRef; diff --git a/src/components/RoomNameInput/index.native.js b/src/components/RoomNameInput/index.native.js new file mode 100644 index 000000000000..dec176db0a9c --- /dev/null +++ b/src/components/RoomNameInput/index.native.js @@ -0,0 +1,68 @@ +import React from 'react'; +import _ from 'underscore'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import getOperatingSystem from '@libs/getOperatingSystem'; +import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; +import CONST from '@src/CONST'; +import * as roomNameInputPropTypes from './roomNameInputPropTypes'; + +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus, ...restProps}) { + const {translate} = useLocalize(); + + /** + * Calls the onChangeText callback with a modified room name + * @param {Event} event + */ + const setModifiedRoomName = (event) => { + const roomName = event.nativeEvent.text; + const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); + onChangeText(modifiedRoomName); + + // if custom component has onInputChange, use it to trigger changes (Form input) + if (_.isFunction(onInputChange)) { + onInputChange(modifiedRoomName); + } + }; + + const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; + + return ( + isFocused && onBlur(event)} + autoFocus={isFocused && autoFocus} + autoCapitalize="none" + shouldDelayFocus={shouldDelayFocus} + /> + ); +} + +RoomNameInput.propTypes = roomNameInputPropTypes.propTypes; +RoomNameInput.defaultProps = roomNameInputPropTypes.defaultProps; +RoomNameInput.displayName = 'RoomNameInput'; + +const RoomNameInputWithRef = React.forwardRef((props, ref) => ( + +)); + +RoomNameInputWithRef.displayName = 'RoomNameInputWithRef'; + +export default RoomNameInputWithRef; diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js new file mode 100644 index 000000000000..7f8292f0123e --- /dev/null +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; + +const propTypes = { + /** Callback to execute when the text input is modified correctly */ + onChangeText: PropTypes.func, + + /** Room name to show in input field. This should include the '#' already prefixed to the name */ + value: PropTypes.string, + + /** Whether we should show the input as disabled */ + disabled: PropTypes.bool, + + /** Error text to show */ + errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + + /** A ref forwarded to the TextInput */ + forwardedRef: PropTypes.func, + + /** The ID used to uniquely identify the input in a Form */ + inputID: PropTypes.string, + + /** Callback that is called when the text input is blurred */ + onBlur: PropTypes.func, + + /** AutoFocus */ + autoFocus: PropTypes.bool, + + /** Whether we should wait before focusing the TextInput, useful when using transitions on Android */ + shouldDelayFocus: PropTypes.bool, + + /** Whether navigation is focused */ + isFocused: PropTypes.bool.isRequired, +}; + +const defaultProps = { + onChangeText: () => {}, + value: '', + disabled: false, + errorText: '', + forwardedRef: () => {}, + + inputID: undefined, + onBlur: () => {}, + autoFocus: false, + shouldDelayFocus: false, +}; + +export {propTypes, defaultProps}; diff --git a/src/pages/settings/Report/RoomNamePage.js b/src/pages/settings/Report/RoomNamePage.js index 7d41d55bf415..f4df6f89cfe7 100644 --- a/src/pages/settings/Report/RoomNamePage.js +++ b/src/pages/settings/Report/RoomNamePage.js @@ -49,9 +49,6 @@ function RoomNamePage({policy, report, reports, translate}) { const roomNameInputRef = useRef(null); const isFocused = useIsFocused(); - const valueParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName); - const displayParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName, true); - const validate = useCallback( (values) => { const errors = {}; @@ -104,10 +101,8 @@ function RoomNamePage({policy, report, reports, translate}) { InputComponent={RoomNameInput} ref={roomNameInputRef} inputID="roomName" - defaultValue={report.reportName.slice(1)} + defaultValue={report.reportName} isFocused={isFocused} - valueParser={valueParser} - displayParser={displayParser} prefixCharacter={CONST.POLICY.ROOM_PREFIX} /> diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 4c65ac86c775..b0d4c0b1b53f 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -25,7 +25,6 @@ import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; @@ -161,8 +160,6 @@ function WorkspaceNewRoomPage(props) { setWriteCapability(CONST.REPORT.WRITE_CAPABILITIES.ALL); }, [isPolicyAdmin]); - const valueParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName); - const displayParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName, true); /** * @param {Object} values - form input values passed by the Form component * @returns {Boolean} @@ -267,8 +264,6 @@ function WorkspaceNewRoomPage(props) { isFocused={props.isFocused} shouldDelayFocus autoFocus - valueParser={valueParser} - displayParser={displayParser} prefixCharacter={CONST.POLICY.ROOM_PREFIX} /> From f54b32647f6de50fdfccbd2ac7f93284664826a8 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 12 Dec 2023 16:32:29 +0100 Subject: [PATCH 07/15] Cleanup --- src/components/Form/FormProvider.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index ece9390ce443..520d1c65b367 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -254,7 +254,8 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC inputID, key: propsToParse.key || inputID, errorText: errors[inputID] || fieldErrorMessage, - value: inputValues[inputID], // As the text input is controlled, we never set the defaultValue prop + value: inputValues[inputID], + // As the text input is controlled, we never set the defaultValue prop // as this is already happening by the value prop. defaultValue: undefined, onTouched: (event) => { From abbca9008b1c8f104d2c0451da3365e94b630b2e Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 12 Dec 2023 16:39:18 +0100 Subject: [PATCH 08/15] Cleanup --- src/components/RoomNameInput/index.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js index f94637be1e5c..0ee74c6b6780 100644 --- a/src/components/RoomNameInput/index.js +++ b/src/components/RoomNameInput/index.js @@ -7,10 +7,9 @@ import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; -function RoomNameInput({value, isFocused, autoFocus, disabled, forwardedRef, onBlur, shouldDelayFocus, onChangeText, onInputChange, ...restProps}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus, ...restProps}) { const {translate} = useLocalize(); - const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; const [selection, setSelection] = useState(); /** @@ -53,18 +52,17 @@ function RoomNameInput({value, isFocused, autoFocus, disabled, forwardedRef, onB accessibilityLabel={translate('newRoomPage.roomName')} role={CONST.ACCESSIBILITY_ROLE.TEXT} placeholder={translate('newRoomPage.social')} - autoCapitalize="none" - value={value.substring(1)} onChange={setModifiedRoomName} + value={value.substring(1)} // Since the room name always starts with a prefix, we omit the first character to avoid displaying it twice. selection={selection} + onSelectionChange={(event) => setSelection(event.nativeEvent.selection)} + autoCapitalize="none" onBlur={(event) => isFocused && onBlur(event)} shouldDelayFocus={shouldDelayFocus} autoFocus={isFocused && autoFocus} maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} - onSelectionChange={(event) => setSelection(event.nativeEvent.selection)} spellCheck={false} shouldInterceptSwipe - keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 /> ); } From f0ac1bddc70bd87da7b6d3ac7f2b738003924067 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 12 Dec 2023 16:46:54 +0100 Subject: [PATCH 09/15] RoomNameInput cleanup --- src/components/RoomNameInput/index.js | 5 +++-- src/components/RoomNameInput/roomNameInputPropTypes.js | 5 +++++ src/pages/settings/Report/RoomNamePage.js | 2 -- src/pages/workspace/WorkspaceNewRoomPage.js | 1 - 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js index 0ee74c6b6780..1597acab7cd5 100644 --- a/src/components/RoomNameInput/index.js +++ b/src/components/RoomNameInput/index.js @@ -2,12 +2,11 @@ import React, {useState} from 'react'; import _ from 'underscore'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; -import getOperatingSystem from '@libs/getOperatingSystem'; import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus, ...restProps}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus, prefixCharacter, ...restProps}) { const {translate} = useLocalize(); const [selection, setSelection] = useState(); @@ -42,6 +41,7 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, setSelection(newSelection); } }; + return ( {}, autoFocus: false, shouldDelayFocus: false, + prefixCharacter: CONST.POLICY.ROOM_PREFIX, }; export {propTypes, defaultProps}; diff --git a/src/pages/settings/Report/RoomNamePage.js b/src/pages/settings/Report/RoomNamePage.js index f4df6f89cfe7..9ae98cc067cc 100644 --- a/src/pages/settings/Report/RoomNamePage.js +++ b/src/pages/settings/Report/RoomNamePage.js @@ -14,7 +14,6 @@ import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; -import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; import reportPropTypes from '@pages/reportPropTypes'; @@ -103,7 +102,6 @@ function RoomNamePage({policy, report, reports, translate}) { inputID="roomName" defaultValue={report.reportName} isFocused={isFocused} - prefixCharacter={CONST.POLICY.ROOM_PREFIX} />
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index b0d4c0b1b53f..49880cce8048 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -264,7 +264,6 @@ function WorkspaceNewRoomPage(props) { isFocused={props.isFocused} shouldDelayFocus autoFocus - prefixCharacter={CONST.POLICY.ROOM_PREFIX} /> From 885ba8d5ce3f48de18df5ec13103c0b04be880da Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 13 Dec 2023 11:44:41 +0100 Subject: [PATCH 10/15] Remove unnecessary change --- src/components/RoomNameInput/index.js | 4 ++-- src/components/RoomNameInput/roomNameInputPropTypes.js | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js index 1597acab7cd5..5a868714720a 100644 --- a/src/components/RoomNameInput/index.js +++ b/src/components/RoomNameInput/index.js @@ -6,7 +6,7 @@ import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus, prefixCharacter, ...restProps}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus, ...restProps}) { const {translate} = useLocalize(); const [selection, setSelection] = useState(); @@ -51,7 +51,7 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, label={translate('newRoomPage.roomName')} accessibilityLabel={translate('newRoomPage.roomName')} role={CONST.ACCESSIBILITY_ROLE.TEXT} - prefixCharacter={prefixCharacter} + prefixCharacter={CONST.POLICY.ROOM_PREFIX} placeholder={translate('newRoomPage.social')} onChange={setModifiedRoomName} value={value.substring(1)} // Since the room name always starts with a prefix, we omit the first character to avoid displaying it twice. diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index e5c326624563..732ad358ad16 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -31,9 +31,6 @@ const propTypes = { /** Whether navigation is focused */ isFocused: PropTypes.bool.isRequired, - - /** Prefix character */ - prefixCharacter: PropTypes.string, }; const defaultProps = { @@ -47,7 +44,6 @@ const defaultProps = { onBlur: () => {}, autoFocus: false, shouldDelayFocus: false, - prefixCharacter: CONST.POLICY.ROOM_PREFIX, }; export {propTypes, defaultProps}; From b750a6b43bc0c07096d599e12c2bbbfe9128dc9c Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 13 Dec 2023 11:46:05 +0100 Subject: [PATCH 11/15] Revert changes in RoomNameInputUtils --- src/libs/RoomNameInputUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/RoomNameInputUtils.ts b/src/libs/RoomNameInputUtils.ts index e6f7d420bf59..cff0bbc30274 100644 --- a/src/libs/RoomNameInputUtils.ts +++ b/src/libs/RoomNameInputUtils.ts @@ -3,14 +3,14 @@ import CONST from '@src/CONST'; /** * Replaces spaces with dashes */ -function modifyRoomName(roomName: string, skipPolicyPrefix?: boolean): string { +function modifyRoomName(roomName: string): string { const modifiedRoomNameWithoutHash = roomName .replace(/ /g, '-') // Replaces the smart dash on iOS devices with two hyphens .replace(/—/g, '--'); - return skipPolicyPrefix ? modifiedRoomNameWithoutHash : `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; + return `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; } export { From 6e66e3f96bc84b85bfcf9cb365616e00caf50984 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 13 Dec 2023 11:46:24 +0100 Subject: [PATCH 12/15] Cleanup --- src/components/RoomNameInput/roomNameInputPropTypes.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 732ad358ad16..7f8292f0123e 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import CONST from '@src/CONST'; const propTypes = { /** Callback to execute when the text input is modified correctly */ From 06c17359824a3b23f959682e4b98297d9bdb3fb4 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Thu, 14 Dec 2023 12:26:21 +0100 Subject: [PATCH 13/15] Fix prop types error --- src/components/RoomNameInput/roomNameInputPropTypes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 7f8292f0123e..0d7cbb4ab8be 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,4 +1,5 @@ import PropTypes from 'prop-types'; +import refPropTypes from "@components/refPropTypes"; const propTypes = { /** Callback to execute when the text input is modified correctly */ @@ -14,7 +15,7 @@ const propTypes = { errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), /** A ref forwarded to the TextInput */ - forwardedRef: PropTypes.func, + forwardedRef: refPropTypes, /** The ID used to uniquely identify the input in a Form */ inputID: PropTypes.string, From 8c701f251c2de45c17a3d72867fb284722374eda Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Thu, 14 Dec 2023 12:50:31 +0100 Subject: [PATCH 14/15] Revert changes in RoomNameInputUtils --- src/components/RoomNameInput/index.js | 5 ++--- src/components/RoomNameInput/index.native.js | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js index 5a868714720a..14529d7b594a 100644 --- a/src/components/RoomNameInput/index.js +++ b/src/components/RoomNameInput/index.js @@ -6,7 +6,7 @@ import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus, ...restProps}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) { const {translate} = useLocalize(); const [selection, setSelection] = useState(); @@ -44,8 +44,6 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, return ( setSelection(event.nativeEvent.selection)} + errorText={errorText} autoCapitalize="none" onBlur={(event) => isFocused && onBlur(event)} shouldDelayFocus={shouldDelayFocus} diff --git a/src/components/RoomNameInput/index.native.js b/src/components/RoomNameInput/index.native.js index dec176db0a9c..a2c09996ad34 100644 --- a/src/components/RoomNameInput/index.native.js +++ b/src/components/RoomNameInput/index.native.js @@ -7,7 +7,7 @@ import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus, ...restProps}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) { const {translate} = useLocalize(); /** @@ -29,8 +29,6 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, return ( Date: Thu, 14 Dec 2023 14:29:18 +0100 Subject: [PATCH 15/15] Lint fix --- src/components/Form/FormProvider.js | 36 +++++++++---------- .../RoomNameInput/roomNameInputPropTypes.js | 2 +- src/pages/workspace/WorkspaceNewRoomPage.js | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 3cff9fe2a3a5..50b24e368fc6 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -126,7 +126,7 @@ const FormProvider = forwardRef( } FormActions.setErrorFields(formID, null); - const validateErrors = validate(trimmedStringValues) || {}; + const validateErrors = validate(trimmedStringValues) || {}; // Validate the input for html tags. It should supercede any other error _.each(trimmedStringValues, (inputValue, inputID) => { @@ -142,27 +142,27 @@ const FormProvider = forwardRef( return; } - const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX); - let isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(inputValue)); - // Check for any matches that the original regex (foundHtmlTagIndex) matched - if (matchedHtmlTags) { - // Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed. - for (let i = 0; i < matchedHtmlTags.length; i++) { - const htmlTag = matchedHtmlTags[i]; - isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(htmlTag)); - if (!isMatch) { - break; + const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX); + let isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(inputValue)); + // Check for any matches that the original regex (foundHtmlTagIndex) matched + if (matchedHtmlTags) { + // Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed. + for (let i = 0; i < matchedHtmlTags.length; i++) { + const htmlTag = matchedHtmlTags[i]; + isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(htmlTag)); + if (!isMatch) { + break; + } } } - } - if (isMatch && leadingSpaceIndex === -1) { - return; - } + if (isMatch && leadingSpaceIndex === -1) { + return; + } - // Add a validation error here because it is a string value that contains HTML characters - validateErrors[inputID] = 'common.error.invalidCharacter'; - }); + // Add a validation error here because it is a string value that contains HTML characters + validateErrors[inputID] = 'common.error.invalidCharacter'; + }); if (!_.isObject(validateErrors)) { throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 0d7cbb4ab8be..60be8430b056 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import refPropTypes from "@components/refPropTypes"; +import refPropTypes from '@components/refPropTypes'; const propTypes = { /** Callback to execute when the text input is modified correctly */ diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index be20bdc4f89e..6f86e871e8ae 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -6,9 +6,9 @@ import _ from 'underscore'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import Button from '@components/Button'; -import * as Illustrations from '@components/Icon/Illustrations'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import * as Illustrations from '@components/Icon/Illustrations'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; import RoomNameInput from '@components/RoomNameInput';