diff --git a/src/CONST/index.js b/src/CONST/index.js index 8d401264cb8d..d6ea4f6c03fa 100755 --- a/src/CONST/index.js +++ b/src/CONST/index.js @@ -526,6 +526,7 @@ const CONST = { ROLE: { ADMIN: 'admin', }, + ROOM_PREFIX: '#', }, TERMS: { diff --git a/src/components/RoomNameInput.js b/src/components/RoomNameInput.js index c9ba86fbae4d..430572844a20 100644 --- a/src/components/RoomNameInput.js +++ b/src/components/RoomNameInput.js @@ -1,6 +1,5 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; -import _ from 'underscore'; import {withOnyx} from 'react-native-onyx'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; @@ -14,17 +13,14 @@ const propTypes = { /** Callback to execute when the text input is modified correctly */ onChangeText: PropTypes.func, - /** Callback to execute when an error gets found/cleared/modified */ - onChangeError: PropTypes.func, - /** Initial room name to show in input field. This should include the '#' already prefixed to the name */ initialValue: PropTypes.string, /** Whether we should show the input as disabled */ disabled: PropTypes.bool, - /** ID of policy whose room names we should be checking for duplicates */ - policyID: PropTypes.string, + /** Error text to show */ + errorText: PropTypes.string, ...withLocalizePropTypes, ...fullPolicyPropTypes, @@ -52,10 +48,9 @@ const propTypes = { const defaultProps = { onChangeText: () => {}, - onChangeError: () => {}, initialValue: '', disabled: false, - policyID: '', + errorText: '', ...fullPolicyDefaultProps, }; @@ -64,94 +59,48 @@ class RoomNameInput extends Component { super(props); this.state = { roomName: props.initialValue, - error: '', }; - this.originalRoomName = props.initialValue; - - this.checkAndModifyRoomName = this.checkAndModifyRoomName.bind(this); - this.checkExistingRoomName = this.checkExistingRoomName.bind(this); - } - - componentDidUpdate(prevProps, prevState) { - // As we are modifying the text input, we'll bubble up any changes/errors so the parent component can see it - if (prevState.roomName !== this.state.roomName) { - this.props.onChangeText(this.state.roomName); - } - if (prevState.error !== this.state.error) { - this.props.onChangeError(this.state.error); - } - - // If the selected policyID has changed we need to check if the room name already exists on this new policy. - if (prevProps.policyID !== this.props.policyID) { - this.checkExistingRoomName(this.state.roomName); - } + this.setModifiedRoomName = this.setModifiedRoomName.bind(this); } /** - * Modifies the room name to follow our conventions: - * - Max length 80 characters - * - Cannot not include space or special characters, and we automatically apply an underscore for spaces - * - Must be lowercase - * Also checks to see if this room name already exists, and displays an error message if so. + * Sets the modified room name in the state and calls the onChangeText callback * @param {Event} event - * */ - checkAndModifyRoomName(event) { + setModifiedRoomName(event) { const nativeEvent = event.nativeEvent; const roomName = nativeEvent.text; const target = nativeEvent.target; const selection = target.selectionStart; - - const modifiedRoomNameWithoutHash = roomName - .replace(/ /g, '_') - .replace(/[^a-zA-Z\d_]/g, '') - .substring(0, CONST.REPORT.MAX_ROOM_NAME_LENGTH) - .toLowerCase(); - const finalRoomName = `#${modifiedRoomNameWithoutHash}`; - - this.checkExistingRoomName(finalRoomName); - - this.setState({ - roomName: finalRoomName, - }, () => { + const modifiedRoomName = this.modifyRoomName(roomName); + this.setState({roomName: modifiedRoomName}, () => { if (!selection) { return; } target.selectionEnd = selection; }); + this.props.onChangeText(modifiedRoomName); } /** - * Checks to see if this room name already exists, and displays an error message if so. + * Modifies the room name to follow our conventions: + * - Max length 80 characters + * - Cannot not include space or special characters, and we automatically apply an underscore for spaces + * - Must be lowercase * @param {String} roomName - * + * @returns {String} */ - checkExistingRoomName(roomName) { - const isExistingRoomName = _.some( - _.values(this.props.reports), - report => report && report.policyID === this.props.policyID && report.reportName === roomName, - ); - - let error = ''; - - // We error if the room name already exists. We don't care if it matches the original name provided in this - // component because then we are not changing the room's name. - if (isExistingRoomName && roomName !== this.originalRoomName) { - error = this.props.translate('newRoomPage.roomAlreadyExistsError'); - } - - // Certain names are reserved for default rooms and should not be used for policy rooms. - if (_.contains(CONST.REPORT.RESERVED_ROOM_NAMES, roomName)) { - error = this.props.translate('newRoomPage.roomNameReservedError'); - } + modifyRoomName(roomName) { + const modifiedRoomNameWithoutHash = roomName + .replace(/ /g, '_') + .replace(/[^a-zA-Z\d_]/g, '') + .substr(0, CONST.REPORT.MAX_ROOM_NAME_LENGTH) + .toLowerCase(); - this.setState({ - error, - }); + return `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; } - render() { return ( ); diff --git a/src/languages/en.js b/src/languages/en.js index cc56939abce1..f527efc503b1 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -896,6 +896,8 @@ export default { policyRoomRenamed: 'Policy room renamed!', roomAlreadyExistsError: 'A room with this name already exists', roomNameReservedError: 'This name is reserved and cannot be used', + pleaseEnterRoomName: 'Please enter a room name', + pleaseSelectWorkspace: 'Please select a workspace', renamedRoomAction: ({oldName, newName}) => ` renamed this room from ${oldName} to ${newName}`, social: 'social', selectAWorkspace: 'Select a workspace', diff --git a/src/languages/es.js b/src/languages/es.js index c203dd4df45c..cfa9b92d2ab6 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -898,6 +898,8 @@ export default { policyRoomRenamed: '¡Espacio de trabajo renombrado!', roomAlreadyExistsError: 'Ya existe una sala con este nombre', roomNameReservedError: 'Este nombre está reservado y no puede usarse', + pleaseEnterRoomName: 'Por favor escribe el nombre de una sala', + pleaseSelectWorkspace: 'Por favor, selecciona un espacio de trabajo', renamedRoomAction: ({oldName, newName}) => ` cambió el nombre de la sala de ${oldName} a ${newName}`, social: 'social', selectAWorkspace: 'Seleccionar un espacio de trabajo', diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js index c052fcd0bf43..a8dcd55919c9 100644 --- a/src/libs/ValidationUtils.js +++ b/src/libs/ValidationUtils.js @@ -321,6 +321,33 @@ function doesFailCharacterLimit(maxLength, valuesToBeValidated) { return _.map(valuesToBeValidated, value => value.length > maxLength); } +/** + * Checks if is one of the certain names which are reserved for default rooms + * and should not be used for policy rooms. + * + * @param {String} roomName + * @returns {Boolean} + */ +function isReservedRoomName(roomName) { + return _.contains(CONST.REPORT.RESERVED_ROOM_NAMES, roomName); +} + +/** + * Checks if the room name already exists. + * + * @param {String} roomName + * @param {Object} reports + * @param {String} policyID + * @returns {Boolean} + */ +function isExistingRoomName(roomName, reports, policyID) { + return _.some( + reports, + report => report && report.policyID === policyID + && report.reportName === roomName, + ); +} + export { meetsAgeRequirements, isValidAddress, @@ -344,4 +371,6 @@ export { isValidRoutingNumber, isValidSSNLastFour, doesFailCharacterLimit, + isReservedRoomName, + isExistingRoomName, }; diff --git a/src/pages/ReportSettingsPage.js b/src/pages/ReportSettingsPage.js index 19d43a40807d..ab9b6e7130fa 100644 --- a/src/pages/ReportSettingsPage.js +++ b/src/pages/ReportSettingsPage.js @@ -18,6 +18,8 @@ import Button from '../components/Button'; import RoomNameInput from '../components/RoomNameInput'; import Picker from '../components/Picker'; import withFullPolicy, {fullPolicyDefaultProps, fullPolicyPropTypes} from './workspace/withFullPolicy'; +import * as ValidationUtils from '../libs/ValidationUtils'; +import Growl from '../libs/Growl'; const propTypes = { @@ -49,6 +51,18 @@ const propTypes = { notificationPreference: PropTypes.string, }).isRequired, + /** All reports shared with the user */ + reports: PropTypes.shape({ + /** The report name */ + reportName: PropTypes.string, + + /** The report type */ + type: PropTypes.string, + + /** ID of the policy */ + policyID: PropTypes.string, + }).isRequired, + /** The policies which the user has access to and which the report could be tied to */ policies: PropTypes.shape({ /** The policy name */ @@ -84,12 +98,61 @@ class ReportSettingsPage extends Component { this.state = { newRoomName: this.props.report.reportName, - error: '', + errors: {}, }; + + this.validateAndRenameReport = this.validateAndRenameReport.bind(this); + } + + validateAndRenameReport() { + if (!this.validate()) { + return; + } + if (this.props.report.reportName === this.state.newRoomName) { + Growl.success(this.props.translate('newRoomPage.policyRoomRenamed')); + return; + } + Report.renameReport(this.props.report.reportID, this.state.newRoomName); + } + + validate() { + const errors = {}; + + // We error if the user doesn't enter a room name or left blank + if (!this.state.newRoomName || this.state.newRoomName === CONST.POLICY.ROOM_PREFIX) { + errors.newRoomName = this.props.translate('newRoomPage.pleaseEnterRoomName'); + } + + // We error if the room name already exists. We don't error if the room name matches same as previous. + if (ValidationUtils.isExistingRoomName(this.state.newRoomName, this.props.reports, this.props.report.policyID) && this.state.newRoomName !== this.props.report.reportName) { + errors.newRoomName = this.props.translate('newRoomPage.roomAlreadyExistsError'); + } + + // Certain names are reserved for default rooms and should not be used for policy rooms. + if (ValidationUtils.isReservedRoomName(this.state.newRoomName)) { + errors.newRoomName = this.props.translate('newRoomPage.roomNameReservedError'); + } + + this.setState({errors}); + return _.isEmpty(errors); + } + + /** + * @param {String} inputKey + * @param {String} value + */ + clearErrorAndSetValue(inputKey, value) { + this.setState(prevState => ({ + [inputKey]: value, + errors: { + ...prevState.errors, + [inputKey]: '', + }, + })); } render() { - const shouldDisableRename = ReportUtils.isDefaultRoom(this.props.report) || ReportUtils.isArchivedRoom(this.props.report) || this.props.isLoadingRenamePolicyRoom; + const shouldDisableRename = ReportUtils.isDefaultRoom(this.props.report) || ReportUtils.isArchivedRoom(this.props.report); const linkedWorkspace = _.find(this.props.policies, policy => policy.id === this.props.report.policyID); return ( @@ -128,26 +191,22 @@ class ReportSettingsPage extends Component { this.setState({newRoomName})} - onChangeError={error => this.setState({error})} initialValue={this.state.newRoomName} - disabled={shouldDisableRename} policyID={linkedWorkspace && linkedWorkspace.id} + errorText={this.state.errors.newRoomName} + onChangeText={newRoomName => this.clearErrorAndSetValue('newRoomName', newRoomName)} + disabled={shouldDisableRename} />