Skip to content

Commit

Permalink
Merge pull request #37704 from Expensify/cmartins-CreateWorkspaceCate…
Browse files Browse the repository at this point in the history
…goriesPage

Create CreateCategoryPage
  • Loading branch information
luacmartins authored Mar 6, 2024
2 parents 5a99fc9 + 629ecb4 commit bc655ed
Show file tree
Hide file tree
Showing 19 changed files with 275 additions and 23 deletions.
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,7 @@ const CONST = {
FORM_CHARACTER_LIMIT: 50,
LEGAL_NAMES_CHARACTER_LIMIT: 150,
LOGIN_CHARACTER_LIMIT: 254,
CATEGORY_NAME_LIMIT: 256,

TITLE_CHARACTER_LIMIT: 100,
DESCRIPTION_LIMIT: 500,
Expand Down Expand Up @@ -1726,6 +1727,7 @@ const CONST = {
MAX_64BIT_LEFT_PART: 92233,
MAX_64BIT_MIDDLE_PART: 7203685,
MAX_64BIT_RIGHT_PART: 4775807,
INVALID_CATEGORY_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,
Expand Down
3 changes: 3 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@ const ONYXKEYS = {
ADD_DEBIT_CARD_FORM: 'addDebitCardForm',
ADD_DEBIT_CARD_FORM_DRAFT: 'addDebitCardFormDraft',
WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm',
WORKSPACE_CATEGORY_CREATE_FORM: 'workspaceCategoryCreate',
WORKSPACE_CATEGORY_CREATE_FORM_DRAFT: 'workspaceCategoryCreateDraft',
WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft',
WORKSPACE_DESCRIPTION_FORM: 'workspaceDescriptionForm',
WORKSPACE_DESCRIPTION_FORM_DRAFT: 'workspaceDescriptionFormDraft',
Expand Down Expand Up @@ -407,6 +409,7 @@ type AllOnyxKeys = DeepValueOf<typeof ONYXKEYS>;
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_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm;
[ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm;
[ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm;
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,10 @@ const ROUTES = {
route: 'workspace/:policyID/categories/settings',
getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const,
},
WORKSPACE_CATEGORY_CREATE: {
route: 'workspace/:policyID/categories/new',
getRoute: (policyID: string) => `workspace/${policyID}/categories/new` as const,
},
WORKSPACE_TAGS: {
route: 'workspace/:policyID/tags',
getRoute: (policyID: string) => `workspace/${policyID}/tags` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ const SCREENS = {
DESCRIPTION: 'Workspace_Profile_Description',
SHARE: 'Workspace_Profile_Share',
NAME: 'Workspace_Profile_Name',
CATEGORY_CREATE: 'Category_Create',
CATEGORY_SETTINGS: 'Category_Settings',
CATEGORIES_SETTINGS: 'Categories_Settings',
},
Expand Down
4 changes: 4 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1770,6 +1770,10 @@ export default {
subtitle: 'Add a category to organize your spend.',
},
genericFailureMessage: 'An error occurred while updating the category, please try again.',
addCategory: 'Add category',
categoryRequiredError: 'Category name is required.',
existingCategoryError: 'A category with this name already exists.',
invalidCategoryName: 'Invalid category name.',
},
tags: {
requiresTag: 'Members must tag all spend',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1794,6 +1794,10 @@ export default {
subtitle: 'Añade una categoría para organizar tu gasto.',
},
genericFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.',
addCategory: 'Añadir categoría',
categoryRequiredError: 'Lo nombre de la categoría es obligatorio.',
existingCategoryError: 'Ya existe una categoría con este nombre.',
invalidCategoryName: 'Lo nombre de la categoría es invalido.',
},
tags: {
requiresTag: 'Los miembros deben etiquetar todos los gastos',
Expand Down
10 changes: 10 additions & 0 deletions src/libs/API/parameters/CreateWorkspaceCategoriesParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
type CreateWorkspaceCategoriesParams = {
policyID: string;
/**
* Stringified JSON object with type of following structure:
* Array<{name: string;}>
*/
categories: string;
};

export default CreateWorkspaceCategoriesParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export type {default as AcceptACHContractForBankAccount} from './AcceptACHContra
export type {default as UpdateWorkspaceDescriptionParams} from './UpdateWorkspaceDescriptionParams';
export type {default as UpdateWorkspaceMembersRoleParams} from './UpdateWorkspaceMembersRoleParams';
export type {default as SetWorkspaceCategoriesEnabledParams} from './SetWorkspaceCategoriesEnabledParams';
export type {default as CreateWorkspaceCategoriesParams} from './CreateWorkspaceCategoriesParams';
export type {default as SetWorkspaceRequiresCategoryParams} from './SetWorkspaceRequiresCategoryParams';
export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAutoReportingParams';
export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams';
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const WRITE_COMMANDS = {
CREATE_WORKSPACE: 'CreateWorkspace',
CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment',
SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled',
CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories',
SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory',
CREATE_TASK: 'CreateTask',
CANCEL_TASK: 'CancelTask',
Expand Down Expand Up @@ -264,6 +265,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.CREATE_WORKSPACE]: Parameters.CreateWorkspaceParams;
[WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams;
[WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams;
[WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES]: Parameters.CreateWorkspaceCategoriesParams;
[WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
[WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams;
[WRITE_COMMANDS.CANCEL_TASK]: Parameters.CancelTaskParams;
Expand Down
1 change: 1 addition & 0 deletions src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceProfileCurrencyPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../pages/workspace/categories/CategorySettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../pages/workspace/categories/CreateCategoryPage').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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial<Record<CentralPaneName, string[]>> =
[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.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET],
[SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS],
[SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS],
};

export default CENTRAL_PANE_TO_RHP_MAPPING;
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route,
},
[SCREENS.WORKSPACE.CATEGORY_CREATE]: {
path: ROUTES.WORKSPACE_CATEGORY_CREATE.route,
},
[SCREENS.REIMBURSEMENT_ACCOUNT]: {
path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route,
exact: true,
Expand Down
4 changes: 4 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type CentralPaneNavigatorParamList = {
};
[SCREENS.WORKSPACE.TAGS]: {
policyID: string;
categoryName: string;
};
};

Expand Down Expand Up @@ -197,6 +198,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.INVITE_MESSAGE]: {
policyID: string;
};
[SCREENS.WORKSPACE.CATEGORY_CREATE]: {
policyID: string;
};
[SCREENS.WORKSPACE.CATEGORY_SETTINGS]: {
policyID: string;
categoryName: string;
Expand Down
8 changes: 8 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,13 @@ function extractPolicyIDFromPath(path: string) {
return path.match(CONST.REGEX.POLICY_ID_FROM_PATH)?.[1];
}

/**
* Whether the policy has active accounting integration connections
*/
function hasAccountingConnections(policy: OnyxEntry<Policy>) {
return Boolean(policy?.connections);
}

function getPathWithoutPolicyID(path: string) {
return path.replace(CONST.REGEX.PATH_WITHOUT_POLICY_ID, '/');
}
Expand All @@ -263,6 +270,7 @@ function goBackFromInvalidPolicy() {

export {
getActivePolicies,
hasAccountingConnections,
hasPolicyMemberError,
hasPolicyError,
hasPolicyErrorFields,
Expand Down
51 changes: 51 additions & 0 deletions src/libs/actions/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2435,6 +2435,56 @@ function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Recor
API.write('SetWorkspaceCategoriesEnabled', parameters, onyxData);
}

function createPolicyCategory(policyID: string, categoryName: string) {
const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
value: {
[categoryName]: {
name: categoryName,
enabled: true,
errors: null,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
},
},
],
successData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
value: {
[categoryName]: {
errors: null,
pendingAction: null,
},
},
},
],
failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
value: {
[categoryName]: {
errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.genericFailureMessage'),
pendingAction: null,
},
},
},
],
};

const parameters = {
policyID,
categories: JSON.stringify([{name: categoryName}]),
};

API.write(WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES, parameters, onyxData);
}

function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolean) {
const onyxData: OnyxData = {
optimisticData: [
Expand Down Expand Up @@ -2552,5 +2602,6 @@ export {
updateWorkspaceDescription,
setWorkspaceCategoryEnabled,
setWorkspaceRequiresCategory,
createPolicyCategory,
clearCategoryErrors,
};
110 changes: 110 additions & 0 deletions src/pages/workspace/categories/CreateCategoryPage.tsx
Original file line number Diff line number Diff line change
@@ -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/WorkspaceCategoryCreateForm';
import type {PolicyCategories} from '@src/types/onyx';

type WorkspaceCreateCategoryPageOnyxProps = {
/** All policy categories */
policyCategories: OnyxEntry<PolicyCategories>;
};

type CreateCategoryPageProps = WorkspaceCreateCategoryPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.CATEGORY_CREATE>;

function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

const validate = useCallback(
(values: FormOnyxValues<typeof ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM>) => {
const errors: FormInputErrors<typeof ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM> = {};
const categoryName = values.categoryName.trim();

if (!ValidationUtils.isRequiredFulfilled(categoryName)) {
errors.categoryName = 'workspace.categories.categoryRequiredError';
} else if (policyCategories?.[categoryName]) {
errors.categoryName = 'workspace.categories.existingCategoryError';
} else if (categoryName === CONST.INVALID_CATEGORY_NAME) {
errors.categoryName = 'workspace.categories.invalidCategoryName';
} else if ([...categoryName].length > CONST.CATEGORY_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, 'categoryName', ['common.error.characterLimitExceedCounter', {length: [...categoryName].length, limit: CONST.CATEGORY_NAME_LIMIT}]);
}

return errors;
},
[policyCategories],
);

const createCategory = useCallback(
(values: FormOnyxValues<typeof ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM>) => {
Policy.createPolicyCategory(route.params.policyID, values.categoryName.trim());
Keyboard.dismiss();
Navigation.goBack();
},
[route.params.policyID],
);

return (
<AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={CreateCategoryPage.displayName}
>
<HeaderWithBackButton
title={translate('workspace.categories.addCategory')}
onBackButtonPress={Navigation.goBack}
/>
<FormProvider
formID={ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM}
onSubmit={createCategory}
submitButtonText={translate('common.save')}
validate={validate}
style={[styles.mh5, styles.flex1]}
enabledWhenOffline
>
<InputWrapper
InputComponent={TextInput}
maxLength={CONST.CATEGORY_NAME_LIMIT}
label={translate('common.name')}
accessibilityLabel={translate('common.name')}
inputID={INPUT_IDS.CATEGORY_NAME}
role={CONST.ROLE.PRESENTATION}
autoFocus
/>
</FormProvider>
</ScreenWrapper>
</PaidPolicyAccessOrNotFoundWrapper>
</AdminPolicyAccessOrNotFoundWrapper>
);
}

CreateCategoryPage.displayName = 'CreateCategoryPage';

export default withOnyx<CreateCategoryPageProps, WorkspaceCreateCategoryPageOnyxProps>({
policyCategories: {
key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route?.params?.policyID}`,
},
})(CreateCategoryPage);
Loading

0 comments on commit bc655ed

Please sign in to comment.