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}
/>
@@ -199,5 +258,8 @@ export default compose(
isLoadingRenamePolicyRoom: {
key: ONYXKEYS.IS_LOADING_RENAME_POLICY_ROOM,
},
+ reports: {
+ key: ONYXKEYS.COLLECTION.REPORT,
+ },
}),
)(ReportSettingsPage);
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js
index 829af0e6c2fe..57a52a16427a 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.js
+++ b/src/pages/workspace/WorkspaceNewRoomPage.js
@@ -21,12 +21,18 @@ import FixedFooter from '../../components/FixedFooter';
import Permissions from '../../libs/Permissions';
import Log from '../../libs/Log';
import KeyboardAvoidingView from '../../components/KeyboardAvoidingView';
+import * as ValidationUtils from '../../libs/ValidationUtils';
const propTypes = {
/** 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,
@@ -49,13 +55,13 @@ class WorkspaceNewRoomPage extends React.Component {
this.state = {
roomName: '',
- error: '',
policyID: '',
visibility: CONST.REPORT.VISIBILITY.RESTRICTED,
+ errors: {},
workspaceOptions: [],
};
- this.onWorkspaceSelect = this.onWorkspaceSelect.bind(this);
- this.onSubmit = this.onSubmit.bind(this);
+
+ this.validateAndCreatePolicyRoom = this.validateAndCreatePolicyRoom.bind(this);
}
componentDidMount() {
@@ -76,19 +82,58 @@ class WorkspaceNewRoomPage extends React.Component {
this.setState({workspaceOptions: _.map(workspaces, policy => ({label: policy.name, key: policy.id, value: policy.id}))});
}
+ validateAndCreatePolicyRoom() {
+ if (!this.validate()) {
+ return;
+ }
+ Report.createPolicyRoom(
+ this.state.policyID,
+ this.state.roomName,
+ this.state.visibility,
+ );
+ }
+
/**
- * Called when a workspace is selected.
- * @param {String} policyID
+ * @returns {Boolean}
*/
- onWorkspaceSelect(policyID) {
- this.setState({policyID});
+ validate() {
+ const errors = {};
+
+ // We error if the user doesn't enter a room name or left blank
+ if (!this.state.roomName || this.state.roomName === CONST.POLICY.ROOM_PREFIX) {
+ errors.roomName = this.props.translate('newRoomPage.pleaseEnterRoomName');
+ }
+
+ // We error if the room name already exists.
+ if (ValidationUtils.isExistingRoomName(this.state.roomName, this.props.reports, this.state.policyID)) {
+ errors.roomName = 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.roomName)) {
+ errors.roomName = this.props.translate('newRoomPage.roomNameReservedError');
+ }
+
+ if (!this.state.policyID) {
+ errors.policyID = this.props.translate('newRoomPage.pleaseSelectWorkspace');
+ }
+
+ this.setState({errors});
+ return _.isEmpty(errors);
}
/**
- * Called when the "Create Room" button is pressed.
+ * @param {String} inputKey
+ * @param {String} value
*/
- onSubmit() {
- Report.createPolicyRoom(this.state.policyID, this.state.roomName, this.state.visibility);
+ clearErrorAndSetValue(inputKey, value) {
+ this.setState(prevState => ({
+ [inputKey]: value,
+ errors: {
+ ...prevState.errors,
+ [inputKey]: '',
+ },
+ }));
}
render() {
@@ -97,7 +142,6 @@ class WorkspaceNewRoomPage extends React.Component {
Navigation.dismissModal();
return null;
}
- const shouldDisableSubmit = Boolean(!this.state.roomName || !this.state.policyID || this.state.error);
const visibilityOptions = _.map(_.values(CONST.REPORT.VISIBILITY), visibilityOption => ({
label: this.props.translate(`newRoomPage.visibilityOptions.${visibilityOption}`),
@@ -115,10 +159,10 @@ class WorkspaceNewRoomPage extends React.Component {
{this.props.translate('newRoomPage.roomName')}
this.setState({roomName})}
- onChangeError={error => this.setState({error})}
initialValue={this.state.roomName}
policyID={this.state.policyID}
+ errorText={this.state.errors.roomName}
+ onChangeText={roomName => this.clearErrorAndSetValue('roomName', roomName)}
/>
@@ -127,7 +171,9 @@ class WorkspaceNewRoomPage extends React.Component {
label={this.props.translate('workspace.common.workspace')}
placeholder={{value: '', label: this.props.translate('newRoomPage.selectAWorkspace')}}
items={this.state.workspaceOptions}
- onChange={this.onWorkspaceSelect}
+ errorText={this.state.errors.policyID}
+ hasError={Boolean(this.state.errors.policyID)}
+ onChange={policyID => this.clearErrorAndSetValue('policyID', policyID)}
/>