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()} + /> +
validate(values)} + onSubmit={() => onSubmit()} + enabledWhenOffline + > + + + + + + +
+
+ ); +}; + +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,