diff --git a/assets/images/task.svg b/assets/images/task.svg
new file mode 100644
index 000000000000..20412f771b69
--- /dev/null
+++ b/assets/images/task.svg
@@ -0,0 +1,15 @@
+
+
+
diff --git a/src/CONST.js b/src/CONST.js
index 20e28c18e928..8677b5ff7f72 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -202,6 +202,7 @@ const CONST = {
POLICY_ROOMS: 'policyRooms',
POLICY_EXPENSE_CHAT: 'policyExpenseChat',
PASSWORDLESS: 'passwordless',
+ TASKS: 'tasks',
},
BUTTON_STATES: {
DEFAULT: 'default',
diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js
index 49d5435c8ebd..b0526ae4b61a 100755
--- a/src/ONYXKEYS.js
+++ b/src/ONYXKEYS.js
@@ -188,6 +188,7 @@ export default {
HOME_ADDRESS_FORM: 'homeAddressForm',
NEW_ROOM_FORM: 'newRoomForm',
ROOM_SETTINGS_FORM: 'roomSettingsForm',
+ NEW_TASK_FORM: 'newTaskForm',
MONEY_REQUEST_DESCRIPTION_FORM: 'moneyRequestDescriptionForm',
},
diff --git a/src/ROUTES.js b/src/ROUTES.js
index 423169d6dd3e..117cd0e6c85a 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -13,6 +13,7 @@ const IOU_DETAILS = 'iou/details';
const IOU_REQUEST_CURRENCY = `${IOU_REQUEST}/currency`;
const IOU_BILL_CURRENCY = `${IOU_BILL}/currency`;
const IOU_SEND_CURRENCY = `${IOU_SEND}/currency`;
+const NEW_TASK = 'new/task';
const SETTINGS_PERSONAL_DETAILS = 'settings/profile/personal-details';
const SETTINGS_CONTACT_METHODS = 'settings/profile/contact-methods';
@@ -55,6 +56,7 @@ export default {
SETTINGS_NEW_CONTACT_METHOD: `${SETTINGS_CONTACT_METHODS}/new`,
NEW_GROUP: 'new/group',
NEW_CHAT: 'new/chat',
+ NEW_TASK,
REPORT,
REPORT_WITH_ID: 'r/:reportID',
getReportRoute: reportID => `r/${reportID}`,
@@ -89,6 +91,9 @@ export default {
IOU_DETAILS_ENABLE_PAYMENTS: `${IOU_DETAILS}/enable-payments`,
IOU_DETAILS_WITH_IOU_REPORT_ID: `${IOU_DETAILS}/:chatReportID/:iouReportID/`,
getIouDetailsRoute: (chatReportID, iouReportID) => `iou/details/${chatReportID}/${iouReportID}`,
+ getNewTaskRoute: reportID => `${NEW_TASK}/${reportID}`,
+ NEW_TASK_WITH_REPORT_ID: `${NEW_TASK}/:reportID?`,
+ getTaskDetailsRoute: taskID => `task/details/${taskID}`,
SEARCH: 'search',
SET_PASSWORD_WITH_VALIDATE_CODE: 'setpassword/:accountID/:validateCode',
DETAILS: 'details',
diff --git a/src/components/Icon/Expensicons.js b/src/components/Icon/Expensicons.js
index 976d846dcb81..d3c7c2f4bc57 100644
--- a/src/components/Icon/Expensicons.js
+++ b/src/components/Icon/Expensicons.js
@@ -109,6 +109,7 @@ import Podcast from '../../../assets/images/social-podcast.svg';
import Linkedin from '../../../assets/images/social-linkedin.svg';
import Instagram from '../../../assets/images/social-instagram.svg';
import AddReaction from '../../../assets/images/add-reaction.svg';
+import Task from '../../../assets/images/task.svg';
export {
ActiveRoomAvatar,
@@ -204,6 +205,7 @@ export {
Send,
Shield,
Sync,
+ Task,
ThreeDots,
Transfer,
Trashcan,
diff --git a/src/languages/en.js b/src/languages/en.js
index 913d4669aac1..b3fd2113f7c3 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -1137,6 +1137,13 @@ export default {
public_announce: 'Public Announce',
},
},
+ newTaskPage: {
+ assignTask: 'Assign task',
+ title: 'Title',
+ description: 'Description',
+ shareIn: 'Share in',
+ pleaseEnterTaskName: 'Please enter a title',
+ },
statementPage: {
generatingPDF: 'We\'re generating your PDF right now. Please come back later!',
},
diff --git a/src/languages/es.js b/src/languages/es.js
index ba6e38232047..693308cec735 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -1138,6 +1138,13 @@ export default {
public_announce: 'Anuncio Público',
},
},
+ newTaskPage: {
+ assignTask: 'Asignar tarea',
+ title: 'Título',
+ description: 'Descripción',
+ shareIn: 'Compartir en',
+ pleaseEnterTaskName: 'Por favor introduce un título',
+ },
statementPage: {
generatingPDF: 'Estamos generando tu PDF ahora mismo. ¡Por favor, vuelve más tarde!',
},
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js
index 776508f27864..f033f5a8a243 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.js
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.js
@@ -288,6 +288,12 @@ class AuthScreens extends React.Component {
component={ModalStackNavigators.IOURequestModalStackNavigator}
listeners={modalScreenListeners}
/>
+
{
+ const NewTaskPage = require('../../../pages/NewTaskPage').default;
+ return NewTaskPage;
+ },
+ name: 'NewTask_Root',
+}]);
+
const SettingsModalStackNavigator = createModalStackNavigator([
{
getComponent: () => {
@@ -545,6 +553,7 @@ export {
SearchModalStackNavigator,
NewGroupModalStackNavigator,
NewChatModalStackNavigator,
+ NewTaskModalStackNavigator,
SettingsModalStackNavigator,
EnablePaymentsStackNavigator,
AddPersonalBankAccountModalStackNavigator,
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index 71caa196abd0..f9c1361538ed 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -206,6 +206,11 @@ export default {
NewChat_Root: ROUTES.NEW_CHAT,
},
},
+ NewTask: {
+ screens: {
+ NewTask_Root: ROUTES.NEW_TASK_WITH_REPORT_ID,
+ },
+ },
Search: {
screens: {
Search_Root: ROUTES.SEARCH,
diff --git a/src/libs/Permissions.js b/src/libs/Permissions.js
index 68f1a493f158..d3e407260e20 100644
--- a/src/libs/Permissions.js
+++ b/src/libs/Permissions.js
@@ -94,6 +94,14 @@ function canUsePasswordlessLogins(betas) {
return _.contains(betas, CONST.BETAS.PASSWORDLESS) || _.contains(betas, CONST.BETAS.ALL);
}
+/**
+ * @param {Array} betas
+ * @returns {Boolean}
+ */
+function canUseTasks(betas) {
+ return _.contains(betas, CONST.BETAS.TASKS) || _.contains(betas, CONST.BETAS.ALL);
+}
+
export default {
canUseChronos,
canUseIOU,
@@ -105,4 +113,5 @@ export default {
canUsePolicyRooms,
canUsePolicyExpenseChat,
canUsePasswordlessLogins,
+ canUseTasks,
};
diff --git a/src/pages/NewTaskPage.js b/src/pages/NewTaskPage.js
new file mode 100644
index 000000000000..a93025dfd5a6
--- /dev/null
+++ b/src/pages/NewTaskPage.js
@@ -0,0 +1,96 @@
+import React from 'react';
+import {View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import PropTypes from 'prop-types';
+import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
+import compose from '../libs/compose';
+import HeaderWithCloseButton from '../components/HeaderWithCloseButton';
+import Navigation from '../libs/Navigation/Navigation';
+import ScreenWrapper from '../components/ScreenWrapper';
+import styles from '../styles/styles';
+import ONYXKEYS from '../ONYXKEYS';
+import * as ErrorUtils from '../libs/ErrorUtils';
+import Form from '../components/Form';
+import TextInput from '../components/TextInput';
+import Permissions from '../libs/Permissions';
+
+const propTypes = {
+ /** List of betas available to current user */
+ betas: PropTypes.arrayOf(PropTypes.string),
+
+ ...withLocalizePropTypes,
+};
+const defaultProps = {
+ betas: [],
+};
+
+// NOTE: This page is going to be updated in https://github.com/Expensify/App/issues/16855, this is just a placeholder for now
+const NewTaskPage = (props) => {
+ /**
+ * @param {Object} values - form input values passed by the Form component
+ * @returns {Boolean}
+ */
+ function validate(values) {
+ const errors = {};
+
+ if (!values.taskTitle) {
+ // We error if the user doesn't enter a room name
+ ErrorUtils.addErrorMessage(errors, 'taskTitle', props.translate('newTaskPage.pleaseEnterTaskName'));
+ }
+
+ return errors;
+ }
+
+ function onSubmit() {
+
+ }
+
+ if (!Permissions.canUseTasks(props.betas)) {
+ Navigation.dismissModal();
+ return null;
+ }
+
+ return (
+
+ Navigation.dismissModal()}
+ />
+
+
+ );
+};
+
+NewTaskPage.displayName = 'NewTaskPage';
+NewTaskPage.propTypes = propTypes;
+NewTaskPage.defaultProps = defaultProps;
+
+export default compose(
+ withOnyx({
+ betas: {
+ key: ONYXKEYS.BETAS,
+ },
+ }),
+ withLocalize,
+)(NewTaskPage);
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index aca688c797c3..735ea16c50cb 100644
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -52,6 +52,7 @@ import withKeyboardState, {keyboardStatePropTypes} from '../../../components/wit
import ArrowKeyFocusManager from '../../../components/ArrowKeyFocusManager';
import OfflineWithFeedback from '../../../components/OfflineWithFeedback';
import KeyboardShortcut from '../../../libs/KeyboardShortcut';
+import Permissions from '../../../libs/Permissions';
const propTypes = {
/** Beta features list */
@@ -173,6 +174,7 @@ class ReportActionCompose extends React.Component {
this.setTextInputRef = this.setTextInputRef.bind(this);
this.getInputPlaceholder = this.getInputPlaceholder.bind(this);
this.getMoneyRequestOptions = this.getMoneyRequestOptions.bind(this);
+ this.getTaskOption = this.getTaskOption.bind(this);
this.addAttachment = this.addAttachment.bind(this);
this.insertSelectedEmoji = this.insertSelectedEmoji.bind(this);
this.setExceededMaxCommentLength = this.setExceededMaxCommentLength.bind(this);
@@ -391,6 +393,29 @@ class ReportActionCompose extends React.Component {
}
}
+ /**
+ * Determines if we can show the task option
+ * @param {Array} reportParticipants
+ * @returns {Boolean}
+ */
+ getTaskOption(reportParticipants) {
+ // We only prevent the task option from showing if it's a DM and the other user is an Expensify default email
+ if (!Permissions.canUseTasks(this.props.betas) || (lodashGet(this.props.report, 'participants', []).length === 1 && _.some(reportParticipants, email => _.contains(
+ CONST.EXPENSIFY_EMAILS,
+ email,
+ )))) {
+ return [];
+ }
+
+ return [
+ {
+ icon: Expensicons.Task,
+ text: this.props.translate('newTaskPage.assignTask'),
+ onSelected: () => Navigation.navigate(ROUTES.getNewTaskRoute(this.props.reportID)),
+ },
+ ];
+ }
+
/**
* Clean data related to EmojiSuggestions
*/
@@ -798,7 +823,7 @@ class ReportActionCompose extends React.Component {
onClose={() => this.setMenuVisibility(false)}
onItemSelected={() => this.setMenuVisibility(false)}
anchorPosition={styles.createMenuPositionReportActionCompose}
- menuItems={[...this.getMoneyRequestOptions(reportParticipants),
+ menuItems={[...this.getMoneyRequestOptions(reportParticipants), ...this.getTaskOption(reportParticipants),
{
icon: Expensicons.Paperclip,
text: this.props.translate('reportActionCompose.addAttachment'),
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
index 426a50e68285..ebff5f4e6a51 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
@@ -211,6 +211,13 @@ class FloatingActionButtonAndPopover extends React.Component {
onSelected: () => Navigation.navigate(ROUTES.IOU_BILL),
},
] : []),
+ ...(Permissions.canUseTasks(this.props.betas) ? [
+ {
+ icon: Expensicons.Task,
+ text: this.props.translate('newTaskPage.assignTask'),
+ onSelected: () => Navigation.navigate(ROUTES.NEW_TASK),
+ },
+ ] : []),
...(!this.props.isLoading && !Policy.hasActiveFreePolicy(this.props.allPolicies) ? [
{
icon: Expensicons.NewWorkspace,