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

feat: category settings page #37209

Merged
merged 18 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

},
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 @@ -219,6 +219,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
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1739,6 +1739,7 @@ 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.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,7 @@ 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.',
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 SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams';
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 @@ -111,6 +111,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 @@ -258,6 +259,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
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 @@ -259,6 +259,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),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding to checklist, It appears that this is a new pattern. Previously, parsing and linking were not utilized in this manner, In my opinion, it would be more effective to decode the category name before passing it to the route props.

Copy link
Contributor

@ishpaul777 ishpaul777 Feb 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@luacmartins ☝️ any thoughts ?

If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers

},
},
[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 @@ -175,6 +175,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
52 changes: 52 additions & 0 deletions src/libs/actions/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
UpdateWorkspaceGeneralSettingsParams,
} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as CollectionUtils from '@libs/CollectionUtils';
import DateUtils from '@libs/DateUtils';
import * as ErrorUtils from '@libs/ErrorUtils';
import Log from '@libs/Log';
Expand All @@ -43,6 +44,8 @@ import type {
InvitedEmailsToAccountIDs,
PersonalDetailsList,
Policy,
PolicyCategories,
PolicyCategory,
PolicyMember,
PolicyTagList,
RecentlyUsedCategories,
Expand Down Expand Up @@ -200,6 +203,18 @@ Onyx.connect({
callback: (val) => (allRecentlyUsedTags = val),
});

const allCategoryPolicies: OnyxCollection<PolicyCategories> = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY_CATEGORIES,
callback: (val, key) => {
if (!key) {
return;
}
const policyID = CollectionUtils.extractCollectionItemID(key);
allCategoryPolicies[policyID] = val;
},
});

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

function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Record<string, {name: string; enabled: boolean}>) {
const policyCategories = allCategoryPolicies?.[policyID] ?? {};

const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
value: {
...Object.keys(categoriesToUpdate).reduce<Record<string, PolicyCategory>>((acc, key) => {
acc[key] = {...policyCategories[key], ...categoriesToUpdate[key]};

return acc;
}, {}),
},
},
],
failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
value: {
...policyCategories,
},
},
],
};

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

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

const setWorkspaceRequiresCategory = (policyID: string, requiresCategory: boolean) => {
const onyxData: OnyxData = {
optimisticData: [
Expand Down Expand Up @@ -2276,5 +2327,6 @@ export {
setWorkspaceAutoReporting,
setWorkspaceApprovalMode,
updateWorkspaceDescription,
setWorkspaceCategoryEnabled,
setWorkspaceRequiresCategory,
};
84 changes: 84 additions & 0 deletions src/pages/workspace/categories/CategorySettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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 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 useWindowDimensions from '@hooks/useWindowDimensions';
import {setWorkspaceCategoryEnabled} from '@libs/actions/Policy';
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 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 {isSmallScreenWidth} = useWindowDimensions();
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}
shouldShowBackButton={isSmallScreenWidth}
/>

<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>
<MenuItemWithTopDescription
title={policyCategory.name}
description={translate(`workspace.categories.categoryName`)}
/>
</ScreenWrapper>
</PaidPolicyAccessOrNotFoundWrapper>
</AdminPolicyAccessOrNotFoundWrapper>
);
}

CategorySettingsPage.displayName = 'CategorySettingsPage';

export default withOnyx<CategorySettingsPageProps, CategorySettingsPageOnyxProps>({
policyCategories: {
key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params.policyID}`,
},
})(CategorySettingsPage);
7 changes: 7 additions & 0 deletions src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,18 @@
[policyCategories, selectedCategories, styles.alignSelfCenter, styles.disabledText, styles.flexRow, styles.p1, styles.pl2, theme.icon, translate],
);

const navigateToCategorySettings = (categoryName: string) => {

Check failure on line 73 in src/pages/workspace/categories/WorkspaceCategoriesPage.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Cannot redeclare block-scoped variable 'navigateToCategorySettings'.
Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, categoryName));
};

const toggleCategory = (category: PolicyForList) => {
setSelectedCategories((prev) => ({
...prev,
[category.value]: !prev[category.value],
}));

// FIXME: This is a temporary solution to navigate to category settings page
navigateToCategorySettings(category.text);
};

const toggleAllCategories = () => {
Expand All @@ -89,7 +96,7 @@
</View>
);

const navigateToCategorySettings = () => {

Check failure on line 99 in src/pages/workspace/categories/WorkspaceCategoriesPage.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Cannot redeclare block-scoped variable 'navigateToCategorySettings'.

Check failure on line 99 in src/pages/workspace/categories/WorkspaceCategoriesPage.tsx

View workflow job for this annotation

GitHub Actions / lint

'navigateToCategorySettings' is already defined
Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES_SETTINGS.getRoute(route.params.policyID));
};

Expand All @@ -97,7 +104,7 @@
<View style={[styles.w100, styles.flexRow, isSmallScreenWidth && styles.mb3]}>
<Button
medium
onPress={navigateToCategorySettings}

Check failure on line 107 in src/pages/workspace/categories/WorkspaceCategoriesPage.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Type '(categoryName: string) => void' is not assignable to type '(event?: KeyboardEvent | GestureResponderEvent | undefined) => void'.
icon={Expensicons.Gear}
text={translate('common.settings')}
style={[isSmallScreenWidth && styles.w50]}
Expand Down
Loading