Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create CreateCategoryPage #37704

Merged
merged 27 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
luacmartins marked this conversation as resolved.
Show resolved Hide resolved

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: '###',
luacmartins marked this conversation as resolved.
Show resolved Hide resolved

// 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',
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
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
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 @@ -193,6 +194,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 @@ -2433,6 +2433,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 @@ -2550,5 +2600,6 @@ export {
updateWorkspaceDescription,
setWorkspaceCategoryEnabled,
setWorkspaceRequiresCategory,
createPolicyCategory,
clearCategoryErrors,
};
108 changes: 108 additions & 0 deletions src/pages/workspace/categories/CreateCategoryPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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.
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
ErrorUtils.addErrorMessage(errors, 'categoryName', ['common.error.characterLimitExceedCounter', {length: [...categoryName].length, limit: CONST.CATEGORY_NAME_LIMIT}]);
}

return errors;
},
[policyCategories],
);

const createCategory = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM>) => {
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
Policy.createPolicyCategory(route.params.policyID, values.categoryName.trim());
Keyboard.dismiss();
Navigation.goBack();
};

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
Loading