Skip to content

Commit

Permalink
Merge pull request Expensify#37209 from ArekChr/feat/category_setting…
Browse files Browse the repository at this point in the history
…s_page

feat: category settings page
  • Loading branch information
luacmartins authored Mar 1, 2024
2 parents a2d0cf2 + 7db77b2 commit 46fbe51
Show file tree
Hide file tree
Showing 15 changed files with 249 additions and 23 deletions.
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,10 @@ const ROUTES = {
route: 'workspace/:policyID/categories',
getRoute: (policyID: string) => `workspace/${policyID}/categories` as const,
},
WORKSPACE_CATEGORY_SETTINGS: {
route: 'workspace/:policyID/categories/:categoryName',
getRoute: (policyID: string, categoryName: string) => `workspace/${policyID}/categories/${encodeURI(categoryName)}` as const,
},
WORKSPACE_CATEGORIES_SETTINGS: {
route: 'workspace/:policyID/categories/settings',
getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` 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 @@ -221,6 +221,7 @@ const SCREENS = {
DESCRIPTION: 'Workspace_Profile_Description',
SHARE: 'Workspace_Profile_Share',
NAME: 'Workspace_Profile_Name',
CATEGORY_SETTINGS: 'Category_Settings',
CATEGORIES_SETTINGS: 'Categories_Settings',
},

Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1756,13 +1756,15 @@ export default {
collect: 'Collect',
},
categories: {
categoryName: 'Category name',
requiresCategory: 'Members must categorize all spend',
enableCategory: 'Enable category',
subtitle: 'Get a better overview of where money is being spent. Use our default categories or add your own.',
emptyCategories: {
title: "You haven't created any categories",
subtitle: 'Add a category to organize your spend.',
},
genericFailureMessage: 'An error occurred while updating the category, please try again.',
},
emptyWorkspace: {
title: 'Create a workspace',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1780,13 +1780,15 @@ export default {
collect: 'Recolectar',
},
categories: {
categoryName: 'Nombre de la categoría',
requiresCategory: 'Los miembros deben categorizar todos los gastos',
enableCategory: 'Activar categoría',
subtitle: 'Obtén una visión general de dónde te gastas el dinero. Utiliza las categorías predeterminadas o añade las tuyas propias.',
emptyCategories: {
title: 'No has creado ninguna categoría',
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.',
},
emptyWorkspace: {
title: 'Crea un espacio de trabajo',
Expand Down
10 changes: 10 additions & 0 deletions src/libs/API/parameters/SetWorkspaceCategoriesEnabledParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
type SetWorkspaceCategoriesEnabledParams = {
policyID: string;
/**
* Stringified JSON object with type of following structure:
* Array<{name: string; enabled: boolean}>
*/
categories: string;
};

export default SetWorkspaceCategoriesEnabledParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export type {default as UnHoldMoneyRequestParams} from './UnHoldMoneyRequestPara
export type {default as CancelPaymentParams} from './CancelPaymentParams';
export type {default as AcceptACHContractForBankAccount} from './AcceptACHContractForBankAccount';
export type {default as UpdateWorkspaceDescriptionParams} from './UpdateWorkspaceDescriptionParams';
export type {default as SetWorkspaceCategoriesEnabledParams} from './SetWorkspaceCategoriesEnabledParams';
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 @@ -113,6 +113,7 @@ const WRITE_COMMANDS = {
UPDATE_WORKSPACE_DESCRIPTION: 'UpdateWorkspaceDescription',
CREATE_WORKSPACE: 'CreateWorkspace',
CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment',
SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled',
SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory',
CREATE_TASK: 'CreateTask',
CANCEL_TASK: 'CancelTask',
Expand Down Expand Up @@ -260,6 +261,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT_AND_RATE]: Parameters.UpdateWorkspaceCustomUnitAndRateParams;
[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.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 @@ -248,6 +248,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.DESCRIPTION]: () => require('../../../pages/workspace/WorkspaceProfileDescriptionPage').default as React.ComponentType,
[SCREENS.WORKSPACE.SHARE]: () => require('../../../pages/workspace/WorkspaceProfileSharePage').default as React.ComponentType,
[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.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType,
[SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType,
Expand Down
6 changes: 6 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,12 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.WORKSPACE.INVITE_MESSAGE]: {
path: ROUTES.WORKSPACE_INVITE_MESSAGE.route,
},
[SCREENS.WORKSPACE.CATEGORY_SETTINGS]: {
path: ROUTES.WORKSPACE_CATEGORY_SETTINGS.route,
parse: {
categoryName: (categoryName: string) => decodeURI(categoryName),
},
},
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route,
},
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 @@ -186,6 +186,10 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.INVITE_MESSAGE]: {
policyID: string;
};
[SCREENS.WORKSPACE.CATEGORY_SETTINGS]: {
policyID: string;
categoryName: string;
};
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
policyID: string;
};
Expand Down
102 changes: 100 additions & 2 deletions src/libs/actions/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import type {
InvitedEmailsToAccountIDs,
PersonalDetailsList,
Policy,
PolicyCategories,
PolicyMember,
PolicyTagList,
RecentlyUsedCategories,
Expand Down Expand Up @@ -202,6 +203,13 @@ Onyx.connect({
callback: (val) => (allRecentlyUsedTags = val),
});

let allPolicyCategories: OnyxCollection<PolicyCategories> = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY_CATEGORIES,
waitForCollectionCallback: true,
callback: (val) => (allPolicyCategories = val),
});

/**
* Stores in Onyx the policy ID of the last workspace that was accessed by the user
*/
Expand Down Expand Up @@ -2270,7 +2278,81 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string
return policyID;
}

const setWorkspaceRequiresCategory = (policyID: string, requiresCategory: boolean) => {
function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Record<string, {name: string; enabled: boolean}>) {
const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {};

const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
value: {
...Object.keys(categoriesToUpdate).reduce<PolicyCategories>((acc, key) => {
acc[key] = {
...policyCategories[key],
...categoriesToUpdate[key],
errors: null,
pendingFields: {
enabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
};

return acc;
}, {}),
},
},
],
successData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
value: {
...Object.keys(categoriesToUpdate).reduce<PolicyCategories>((acc, key) => {
acc[key] = {
...policyCategories[key],
...categoriesToUpdate[key],
errors: null,
pendingFields: {
enabled: null,
},
};

return acc;
}, {}),
},
},
],
failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
value: {
...Object.keys(categoriesToUpdate).reduce<PolicyCategories>((acc, key) => {
acc[key] = {
...policyCategories[key],
...categoriesToUpdate[key],
errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.genericFailureMessage'),
pendingFields: {
enabled: null,
},
};

return acc;
}, {}),
},
},
],
};

const parameters = {
policyID,
categories: JSON.stringify(Object.keys(categoriesToUpdate).map((key) => categoriesToUpdate[key])),
};

API.write('SetWorkspaceCategoriesEnabled', parameters, onyxData);
}

function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolean) {
const onyxData: OnyxData = {
optimisticData: [
{
Expand Down Expand Up @@ -2322,7 +2404,21 @@ const setWorkspaceRequiresCategory = (policyID: string, requiresCategory: boolea
};

API.write('SetWorkspaceRequiresCategory', parameters, onyxData);
};
}

function clearCategoryErrors(policyID: string, categoryName: string) {
const category = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName];

if (!category) {
return;
}

Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {
[category.name]: {
errors: null,
},
});
}

export {
removeMembers,
Expand Down Expand Up @@ -2369,5 +2465,7 @@ export {
setWorkspaceAutoReportingFrequency,
setWorkspaceAutoReportingMonthlyOffset,
updateWorkspaceDescription,
setWorkspaceCategoryEnabled,
setWorkspaceRequiresCategory,
clearCategoryErrors,
};
90 changes: 90 additions & 0 deletions src/pages/workspace/categories/CategorySettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import Switch from '@components/Switch';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import {setWorkspaceCategoryEnabled} from '@libs/actions/Policy';
import * as ErrorUtils from '@libs/ErrorUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';

type CategorySettingsPageOnyxProps = {
/** Collection of categories attached to a policy */
policyCategories: OnyxEntry<OnyxTypes.PolicyCategories>;
};

type CategorySettingsPageProps = CategorySettingsPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.CATEGORY_SETTINGS>;

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

const policyCategory = policyCategories?.[route.params.categoryName];

if (!policyCategory) {
return <NotFoundPage />;
}

const updateWorkspaceRequiresCategory = (value: boolean) => {
setWorkspaceCategoryEnabled(route.params.policyID, {[policyCategory.name]: {name: policyCategory.name, enabled: value}});
};

return (
<AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={CategorySettingsPage.displayName}
>
<HeaderWithBackButton title={route.params.categoryName} />
<View style={styles.flexGrow1}>
<OfflineWithFeedback
errors={ErrorUtils.getLatestErrorMessageField(policyCategory)}
pendingAction={policyCategory?.pendingFields?.enabled}
errorRowStyles={styles.mh5}
onClose={() => Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)}
>
<View style={[styles.mt2, styles.mh5]}>
<View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}>
<Text>{translate('workspace.categories.enableCategory')}</Text>
<Switch
isOn={policyCategory.enabled}
accessibilityLabel={translate('workspace.categories.enableCategory')}
onToggle={updateWorkspaceRequiresCategory}
/>
</View>
</View>
</OfflineWithFeedback>
<MenuItemWithTopDescription
title={policyCategory.name}
description={translate(`workspace.categories.categoryName`)}
/>
</View>
</ScreenWrapper>
</PaidPolicyAccessOrNotFoundWrapper>
</AdminPolicyAccessOrNotFoundWrapper>
);
}

CategorySettingsPage.displayName = 'CategorySettingsPage';

export default withOnyx<CategorySettingsPageProps, CategorySettingsPageOnyxProps>({
policyCategories: {
key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params.policyID}`,
},
})(CategorySettingsPage);
8 changes: 6 additions & 2 deletions src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,19 @@ function WorkspaceCategoriesPage({policyCategories, route}: WorkspaceCategoriesP
</View>
);

const navigateToCategorySettings = () => {
const navigateToCategoriesSettings = () => {
Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES_SETTINGS.getRoute(route.params.policyID));
};

const navigateToCategorySettings = (category: PolicyForList) => {
Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, category.text));
};

const settingsButton = (
<View style={[styles.w100, styles.flexRow, isSmallScreenWidth && styles.mb3]}>
<Button
medium
onPress={navigateToCategorySettings}
onPress={navigateToCategoriesSettings}
icon={Expensicons.Gear}
text={translate('common.settings')}
style={[isSmallScreenWidth && styles.w50]}
Expand Down
Loading

0 comments on commit 46fbe51

Please sign in to comment.