From 38f676fca4f2f36630fc4259a957bd3b03853791 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Tue, 12 Mar 2024 22:33:45 +0500 Subject: [PATCH 01/11] feat: add tag create page --- src/CONST.ts | 3 + src/ONYXKEYS.ts | 3 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/languages/en.ts | 4 + src/languages/es.ts | 4 + .../parameters/CreateWorkspaceTagsParams.ts | 10 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../AppNavigator/ModalStackNavigators.tsx | 1 + .../CENTRAL_PANE_TO_RHP_MAPPING.ts | 2 +- src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 3 + src/libs/actions/Policy.ts | 54 +++++++++ .../workspace/tags/WorkspaceCreateTagPage.tsx | 110 ++++++++++++++++++ .../workspace/tags/WorkspaceTagsPage.tsx | 30 +++-- src/types/form/WorkspaceTagCreateForm.ts | 18 +++ src/types/form/index.ts | 1 + 18 files changed, 245 insertions(+), 9 deletions(-) create mode 100644 src/libs/API/parameters/CreateWorkspaceTagsParams.ts create mode 100644 src/pages/workspace/tags/WorkspaceCreateTagPage.tsx create mode 100644 src/types/form/WorkspaceTagCreateForm.ts diff --git a/src/CONST.ts b/src/CONST.ts index a163c63404a7..2e0f5819d029 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1663,6 +1663,8 @@ const CONST = { LOGIN_CHARACTER_LIMIT: 254, CATEGORY_NAME_LIMIT: 256, + TAG_NAME_LIMIT: 256, + TITLE_CHARACTER_LIMIT: 100, DESCRIPTION_LIMIT: 500, @@ -1743,6 +1745,7 @@ const CONST = { MAX_64BIT_MIDDLE_PART: 7203685, MAX_64BIT_RIGHT_PART: 4775807, INVALID_CATEGORY_NAME: '###', + INVALID_TAG_NAME: '###', // When generating a random value to fit in 7 digits (for the `middle` or `right` parts above), this is the maximum value to multiply by Math.random(). MAX_INT_FOR_RANDOM_7_DIGIT_VALUE: 10000000, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b9e7c4d5d274..8c48cbad561f 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -332,6 +332,8 @@ const ONYXKEYS = { WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm', WORKSPACE_CATEGORY_CREATE_FORM: 'workspaceCategoryCreate', WORKSPACE_CATEGORY_CREATE_FORM_DRAFT: 'workspaceCategoryCreateDraft', + WORKSPACE_TAG_CREATE_FORM: 'workspaceTagCreate', + WORKSPACE_TAG_CREATE_FORM_DRAFT: 'workspaceTagCreateDraft', WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft', WORKSPACE_DESCRIPTION_FORM: 'workspaceDescriptionForm', WORKSPACE_DESCRIPTION_FORM_DRAFT: 'workspaceDescriptionFormDraft', @@ -416,6 +418,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: FormTypes.AddDebitCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm; [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM]: FormTypes.WorkspaceCategoryCreateForm; + [ONYXKEYS.FORMS.WORKSPACE_TAG_CREATE_FORM]: FormTypes.WorkspaceTagCreateForm; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d9f0c6658a2b..fd6822d03924 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -569,6 +569,10 @@ const ROUTES = { route: 'workspace/:policyID/tags', getRoute: (policyID: string) => `workspace/${policyID}/tags` as const, }, + WORKSPACE_TAG_CREATE: { + route: 'workspace/:policyID/tag/new', + getRoute: (policyID: string) => `workspace/${policyID}/tag/new` as const, + }, WORKSPACE_TAGS_SETTINGS: { route: 'workspace/:policyID/tags/settings', getRoute: (policyID: string) => `workspace/${policyID}/tags/settings` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index a0e06b98da2b..1b2eb10e7cb5 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -218,6 +218,7 @@ const SCREENS = { TAGS: 'Workspace_Tags', TAGS_SETTINGS: 'Tags_Settings', TAGS_EDIT: 'Tags_Edit', + TAG_CREATE: 'Tag_Create', CURRENCY: 'Workspace_Profile_Currency', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_APPROVER: 'Workspace_Workflows_Approver', diff --git a/src/languages/en.ts b/src/languages/en.ts index ff91a4f6f205..0e2ed9e949e1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1829,11 +1829,15 @@ export default { requiresTag: 'Members must tag all spend', customTagName: 'Custom tag name', enableTag: 'Enable tag', + addTag: 'Add tag', subtitle: 'Tags add more detailed ways to classify costs.', emptyTags: { title: "You haven't created any tags", subtitle: 'Add a tag to track projects, locations, departments, and more.', }, + tagRequiredError: 'Tag name is required.', + existingTagError: 'A tag with this name already exists.', + invalidTagName: 'Invalid tag name.', genericFailureMessage: 'An error occurred while updating the tag, please try again.', }, emptyWorkspace: { diff --git a/src/languages/es.ts b/src/languages/es.ts index c21f46ed8853..e7a189cd25f1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1853,11 +1853,15 @@ export default { requiresTag: 'Los miembros deben etiquetar todos los gastos', customTagName: 'Nombre de etiqueta personalizada', enableTag: 'Habilitar etiqueta', + addTag: 'Añadir etiqueta', subtitle: 'Las etiquetas añaden formas más detalladas de clasificar los costos.', emptyTags: { title: 'No has creado ninguna etiqueta', subtitle: 'Añade una etiqueta para realizar el seguimiento de proyectos, ubicaciones, departamentos y otros.', }, + tagRequiredError: 'Tag name is required.', + existingTagError: 'A tag with this name already exists.', + invalidTagName: 'Invalid tag name.', genericFailureMessage: 'Se produjo un error al actualizar la etiqueta, inténtelo nuevamente.', }, emptyWorkspace: { diff --git a/src/libs/API/parameters/CreateWorkspaceTagsParams.ts b/src/libs/API/parameters/CreateWorkspaceTagsParams.ts new file mode 100644 index 000000000000..71ae7869e074 --- /dev/null +++ b/src/libs/API/parameters/CreateWorkspaceTagsParams.ts @@ -0,0 +1,10 @@ +type CreateWorkspaceTagsParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array<{name: string;}> + */ + tags: string; +}; + +export default CreateWorkspaceTagsParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 7e0e9b6e4a96..c54891c27229 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -170,3 +170,4 @@ export type {default as AcceptJoinRequestParams} from './AcceptJoinRequest'; export type {default as DeclineJoinRequestParams} from './DeclineJoinRequest'; export type {default as JoinPolicyInviteLinkParams} from './JoinPolicyInviteLink'; export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams'; +export type {default as CreateWorkspaceTagsParams} from './CreateWorkspaceTagsParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index bf215e09e37d..1703d192bae3 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -116,6 +116,7 @@ const WRITE_COMMANDS = { CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment', SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled', CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories', + CREATE_WORKSPACE_TAG: 'CreatePolicyTag', SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory', SET_POLICY_REQUIRES_TAG: 'SetPolicyRequiresTag', RENAME_POLICY_TAG_LIST: 'RenamePolicyTaglist', @@ -281,6 +282,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams; [WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag; [WRITE_COMMANDS.RENAME_POLICY_TAG_LIST]: Parameters.RenamePolicyTaglist; + [WRITE_COMMANDS.CREATE_WORKSPACE_TAG]: Parameters.CreateWorkspaceTagsParams; [WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams; [WRITE_COMMANDS.CANCEL_TASK]: Parameters.CancelTaskParams; [WRITE_COMMANDS.EDIT_TASK_ASSIGNEE]: Parameters.EditTaskAssigneeParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index d56e38564149..e42b08ea6303 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -256,6 +256,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require('../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAGS_EDIT]: () => require('../../../pages/workspace/tags/WorkspaceEditTagsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.TAG_CREATE]: () => require('../../../pages/workspace/tags/WorkspaceCreateTagPage').default as React.ComponentType, [SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType, [SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType, [SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index 743bf2e0cff1..d11c6c9d2874 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -6,7 +6,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = [SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT, SCREENS.WORKSPACE.RATE_AND_UNIT_RATE, SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT], [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE, SCREENS.WORKSPACE.MEMBER_DETAILS, SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION], [SCREENS.WORKSPACE.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET], - [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT], + [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE], [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], }; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 97d7650a9043..68ad95eaea61 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -303,6 +303,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.TAGS_EDIT]: { path: ROUTES.WORKSPACE_EDIT_TAGS.route, }, + [SCREENS.WORKSPACE.TAG_CREATE]: { + path: ROUTES.WORKSPACE_TAG_CREATE.route, + }, [SCREENS.REIMBURSEMENT_ACCOUNT]: { path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 33e79b637cc4..edf29d87d8c8 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -214,6 +214,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: { policyID: string; }; + [SCREENS.WORKSPACE.TAG_CREATE]: { + policyID: string; + }; [SCREENS.WORKSPACE.TAGS_SETTINGS]: { policyID: string; }; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 85572ebe04a7..e81ab18a7eab 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -2599,6 +2599,59 @@ function createPolicyCategory(policyID: string, categoryName: string) { API.write(WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES, parameters, onyxData); } +function createPolicyTag(policyID: string, tagName: string) { + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + value: { + Tag: { + tags: { + [tagName]: { + name: tagName, + enabled: false, + }, + }, + errors: null, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + value: { + Tag: { + errors: null, + pendingAction: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + value: { + Tag: { + errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'), + }, + }, + }, + ], + }; + + const parameters = { + policyID, + tags: JSON.stringify([{name: tagName}]), + }; + + API.write(WRITE_COMMANDS.CREATE_WORKSPACE_TAG, parameters, onyxData); +} + function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolean) { const onyxData: OnyxData = { optimisticData: [ @@ -3302,4 +3355,5 @@ export { enablePolicyTaxes, enablePolicyWorkflows, openPolicyDistanceRatesPage, + createPolicyTag, }; diff --git a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx new file mode 100644 index 000000000000..1d6870db725b --- /dev/null +++ b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx @@ -0,0 +1,110 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback} from 'react'; +import {Keyboard} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import * as Policy from '@userActions/Policy'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/WorkspaceTagCreateForm'; +import type {PolicyTagList} from '@src/types/onyx'; + +type WorkspaceCreateTagPageOnyxProps = { + /** All policy tags */ + policyTags: OnyxEntry; +}; + +type CreateTagPageProps = WorkspaceCreateTagPageOnyxProps & StackScreenProps; + +function CreateTagPage({route, policyTags}: CreateTagPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const validate = useCallback( + (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; + const tagName = values.tagName.trim(); + + if (!ValidationUtils.isRequiredFulfilled(tagName)) { + errors.tagName = 'workspace.tags.tagRequiredError'; + } else if (policyTags?.[tagName]) { + errors.tagName = 'workspace.tags.existingTagError'; + } else if (tagName === CONST.INVALID_TAG_NAME) { + errors.tagName = 'workspace.tags.invalidTagName'; + } else if ([...tagName].length > CONST.TAG_NAME_LIMIT) { + // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 code units. + ErrorUtils.addErrorMessage(errors, 'tagName', ['common.error.characterLimitExceedCounter', {length: [...tagName].length, limit: CONST.TAG_NAME_LIMIT}]); + } + + return errors; + }, + [policyTags], + ); + + const createTag = useCallback( + (values: FormOnyxValues) => { + Policy.createPolicyTag(route.params.policyID, values.tagName.trim()); + Keyboard.dismiss(); + Navigation.goBack(); + }, + [route.params.policyID], + ); + + return ( + + + + + + + + + + + ); +} + +CreateTagPage.displayName = 'CreateTagPage'; + +export default withOnyx({ + policyTags: { + key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${route?.params?.policyID}`, + }, +})(CreateTagPage); diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index cc809892d45e..244456bf6fdb 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -115,18 +115,32 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { Navigation.navigate(ROUTES.WORKSPACE_TAGS_SETTINGS.getRoute(route.params.policyID)); }; + const navigateToCreateTagPage = () => { + Navigation.navigate(ROUTES.WORKSPACE_TAG_CREATE.getRoute(route.params.policyID)); + }; + const isLoading = !isOffline && policyTags === undefined; - const settingsButton = ( + const headerButtons = (