Skip to content

Commit

Permalink
Merge pull request Expensify#38379 from ArekChr/feat/delete_category
Browse files Browse the repository at this point in the history
feat: Delete single category
  • Loading branch information
luacmartins authored Mar 18, 2024
2 parents c9f8861 + 7edf37f commit 896f40c
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 23 deletions.
6 changes: 5 additions & 1 deletion src/components/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ type SwitchProps = {

/** Accessibility label for the switch */
accessibilityLabel: string;

/** Whether the switch is disabled */
disabled?: boolean;
};

const OFFSET_X = {
OFF: 0,
ON: 20,
};

function Switch({isOn, onToggle, accessibilityLabel}: SwitchProps) {
function Switch({isOn, onToggle, accessibilityLabel, disabled}: SwitchProps) {
const styles = useThemeStyles();
const offsetX = useRef(new Animated.Value(isOn ? OFFSET_X.ON : OFFSET_X.OFF));

Expand All @@ -35,6 +38,7 @@ function Switch({isOn, onToggle, accessibilityLabel}: SwitchProps) {

return (
<PressableWithFeedback
disabled={disabled}
style={[styles.switchTrack, !isOn && styles.switchInactive]}
onPress={() => onToggle(!isOn)}
onLongPress={() => onToggle(!isOn)}
Expand Down
9 changes: 7 additions & 2 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1776,18 +1776,23 @@ export default {
},
categories: {
deleteCategories: 'Delete categories',
deleteCategoriesPrompt: 'Are you sure you want to delete these categories?',
deleteCategory: 'Delete category',
deleteCategoryPrompt: 'Are you sure you want to delete this category?',
disableCategories: 'Disable categories',
disableCategory: 'Disable category',
enableCategories: 'Enable categories',
enableCategory: 'Enable category',
deleteFailureMessage: 'An error occurred while deleting the category, please try again.',
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.',
updateFailureMessage: 'An error occurred while updating the category, please try again.',
createFailureMessage: 'An error occurred while creating the category, please try again.',
addCategory: 'Add category',
editCategory: 'Edit category',
categoryRequiredError: 'Category name is required.',
Expand Down
9 changes: 7 additions & 2 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1800,18 +1800,23 @@ export default {
},
categories: {
deleteCategories: 'Eliminar categorías',
deleteCategoriesPrompt: '¿Estás seguro de que quieres eliminar estas categorías?',
deleteCategory: 'Eliminar categoría',
deleteCategoryPrompt: '¿Estás seguro de que quieres eliminar esta categoría?',
disableCategories: 'Desactivar categorías',
disableCategory: 'Desactivar categoría',
enableCategories: 'Activar categorías',
enableCategory: 'Activar categoría',
deleteFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.',
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.',
updateFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.',
createFailureMessage: 'Se ha producido un error al intentar crear la categoría. Por favor, inténtalo más tarde.',
addCategory: 'Añadir categoría',
editCategory: 'Editar categoría',
categoryRequiredError: 'Lo nombre de la categoría es obligatorio.',
Expand Down
8 changes: 4 additions & 4 deletions src/libs/actions/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2707,7 +2707,7 @@ function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Recor
acc[key] = {
...policyCategories[key],
...categoriesToUpdate[key],
errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.genericFailureMessage'),
errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.updateFailureMessage'),
pendingFields: {
enabled: null,
},
Expand Down Expand Up @@ -2762,7 +2762,7 @@ function createPolicyCategory(policyID: string, categoryName: string) {
key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
value: {
[categoryName]: {
errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.genericFailureMessage'),
errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.createFailureMessage'),
pendingAction: null,
},
},
Expand Down Expand Up @@ -2823,7 +2823,7 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string
...policyCategoryToUpdate,
name: policyCategory.oldName,
unencodedName: decodeURIComponent(policyCategory.oldName),
errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.genericFailureMessage'),
errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.updateFailureMessage'),
pendingAction: null,
},
},
Expand Down Expand Up @@ -3055,7 +3055,7 @@ function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolea
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
requiresCategory: !requiresCategory,
errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.genericFailureMessage'),
errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.updateFailureMessage'),
pendingFields: {
requiresCategory: null,
},
Expand Down
1 change: 0 additions & 1 deletion src/pages/workspace/categories/CategoryForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ function CategoryForm({onSubmit, policyCategories, categoryName}: CategoryFormPr
accessibilityLabel={translate('common.name')}
inputID={INPUT_IDS.CATEGORY_NAME}
role={CONST.ROLE.PRESENTATION}
autoFocus
/>
</FormProvider>
);
Expand Down
38 changes: 36 additions & 2 deletions src/pages/workspace/categories/CategorySettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import React, {useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import ConfirmModal from '@components/ConfirmModal';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
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 useWindowDimensions from '@hooks/useWindowDimensions';
import {setWorkspaceCategoryEnabled} from '@libs/actions/Policy';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
Expand All @@ -34,6 +37,8 @@ type CategorySettingsPageProps = CategorySettingsPageOnyxProps & StackScreenProp
function CategorySettingsPage({route, policyCategories}: CategorySettingsPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {windowWidth} = useWindowDimensions();
const [deleteCategoryConfirmModalVisible, setDeleteCategoryConfirmModalVisible] = useState(false);

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

Expand All @@ -49,6 +54,20 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro
Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_EDIT.getRoute(route.params.policyID, policyCategory.name));
};

const deleteCategory = () => {
Policy.deleteWorkspaceCategories(route.params.policyID, [route.params.categoryName]);
setDeleteCategoryConfirmModalVisible(false);
Navigation.dismissModal();
};

const threeDotsMenuItems = [
{
icon: Expensicons.Trashcan,
text: translate('workspace.categories.deleteCategory'),
onSelected: () => setDeleteCategoryConfirmModalVisible(true),
},
];

return (
<AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
Expand All @@ -57,7 +76,22 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro
style={[styles.defaultModalContainer]}
testID={CategorySettingsPage.displayName}
>
<HeaderWithBackButton title={route.params.categoryName} />
<HeaderWithBackButton
shouldShowThreeDotsButton
title={route.params.categoryName}
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)}
threeDotsMenuItems={threeDotsMenuItems}
/>
<ConfirmModal
isVisible={deleteCategoryConfirmModalVisible}
onConfirm={deleteCategory}
onCancel={() => setDeleteCategoryConfirmModalVisible(false)}
title={translate('workspace.categories.deleteCategory')}
prompt={translate('workspace.categories.deleteCategoryPrompt')}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
danger
/>
<View style={styles.flexGrow1}>
<OfflineWithFeedback
errors={ErrorUtils.getLatestErrorMessageField(policyCategory)}
Expand Down
3 changes: 2 additions & 1 deletion src/pages/workspace/categories/EditCategoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAcce
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {PolicyCategories} from '@src/types/onyx';
import CategoryForm from './CategoryForm';
Expand Down Expand Up @@ -47,7 +48,7 @@ function EditCategoryPage({route, policyCategories}: EditCategoryPageProps) {
>
<HeaderWithBackButton
title={translate('workspace.categories.editCategory')}
onBackButtonPress={Navigation.goBack}
onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName))}
/>
<CategoryForm
onSubmit={editCategory}
Expand Down
37 changes: 27 additions & 10 deletions src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import ConfirmModal from '@components/ConfirmModal';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
Expand Down Expand Up @@ -58,6 +59,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
const {translate} = useLocalize();
const [selectedCategories, setSelectedCategories] = useState<Record<string, boolean>>({});
const dropdownButtonRef = useRef(null);
const [deleteCategoriesConfirmModalVisible, setDeleteCategoriesConfirmModalVisible] = useState(false);

function fetchCategories() {
Policy.openPolicyCategoriesPage(route.params.policyID);
Expand Down Expand Up @@ -155,21 +157,24 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat

const selectedCategoriesArray = Object.keys(selectedCategories).filter((key) => selectedCategories[key]);

const handleDeleteCategories = () => {
setSelectedCategories({});
deleteWorkspaceCategories(route.params.policyID, selectedCategoriesArray);
setDeleteCategoriesConfirmModalVisible(false);
};

const getHeaderButtons = () => {
const options: Array<DropdownOption<DeepValueOf<typeof CONST.POLICY.CATEGORIES_BULK_ACTION_TYPES>>> = [];

if (selectedCategoriesArray.length > 0) {
options.push({
icon: Expensicons.Trashcan,
text: translate('workspace.categories.deleteCategories'),
text: translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategory' : 'workspace.categories.deleteCategories'),
value: CONST.POLICY.CATEGORIES_BULK_ACTION_TYPES.DELETE,
onSelected: () => {
setSelectedCategories({});
deleteWorkspaceCategories(route.params.policyID, selectedCategoriesArray);
},
onSelected: () => setDeleteCategoriesConfirmModalVisible(true),
});

const enabledCategories = selectedCategoriesArray.filter((categoryName) => policyCategories?.[categoryName].enabled);
const enabledCategories = selectedCategoriesArray.filter((categoryName) => policyCategories?.[categoryName]?.enabled);
if (enabledCategories.length > 0) {
const categoriesToDisable = selectedCategoriesArray
.filter((categoryName) => policyCategories?.[categoryName].enabled)
Expand All @@ -183,7 +188,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat

options.push({
icon: Expensicons.DocumentSlash,
text: translate('workspace.categories.disableCategories'),
text: translate(enabledCategories.length === 1 ? 'workspace.categories.disableCategory' : 'workspace.categories.disableCategories'),
value: CONST.POLICY.CATEGORIES_BULK_ACTION_TYPES.DISABLE,
onSelected: () => {
setSelectedCategories({});
Expand All @@ -205,7 +210,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
}, {});
options.push({
icon: Expensicons.Document,
text: translate('workspace.categories.enableCategories'),
text: translate(disabledCategories.length === 1 ? 'workspace.categories.enableCategory' : 'workspace.categories.enableCategories'),
value: CONST.POLICY.CATEGORIES_BULK_ACTION_TYPES.ENABLE,
onSelected: () => {
setSelectedCategories({});
Expand Down Expand Up @@ -253,6 +258,8 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat

const isLoading = !isOffline && policyCategories === undefined;

const shouldShowEmptyState = !categoryList.some((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) && !isLoading;

return (
<AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
Expand All @@ -269,6 +276,16 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
>
{!isSmallScreenWidth && getHeaderButtons()}
</HeaderWithBackButton>
<ConfirmModal
isVisible={deleteCategoriesConfirmModalVisible}
onConfirm={handleDeleteCategories}
onCancel={() => setDeleteCategoriesConfirmModalVisible(false)}
title={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategory' : 'workspace.categories.deleteCategories')}
prompt={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategoryPrompt' : 'workspace.categories.deleteCategoriesPrompt')}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
danger
/>
{isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>}
<View style={[styles.ph5, styles.pb5, styles.pt3]}>
<Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.categories.subtitle')}</Text>
Expand All @@ -280,14 +297,14 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
color={theme.spinner}
/>
)}
{categoryList.length === 0 && !isLoading && (
{shouldShowEmptyState && (
<WorkspaceEmptyStateSection
title={translate('workspace.categories.emptyCategories.title')}
icon={Illustrations.EmptyStateExpenses}
subtitle={translate('workspace.categories.emptyCategories.subtitle')}
/>
)}
{categoryList.length > 0 && (
{!shouldShowEmptyState && (
<SelectionList
canSelectMultiple
sections={[{data: categoryList, indexOffset: 0, isDisabled: false}]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function WorkspaceCategoriesSettingsPage({route}: WorkspaceCategoriesSettingsPag
isOn={policy?.requiresCategory ?? false}
accessibilityLabel={translate('workspace.categories.requiresCategory')}
onToggle={updateWorkspaceRequiresCategory}
disabled={!policy?.areCategoriesEnabled}
/>
</View>
</View>
Expand Down

0 comments on commit 896f40c

Please sign in to comment.