Skip to content

Commit

Permalink
feat: add bulk tag delete options
Browse files Browse the repository at this point in the history
  • Loading branch information
allroundexperts committed Mar 19, 2024
1 parent 77d2868 commit 0ee8c4d
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 21 deletions.
5 changes: 5 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1477,6 +1477,11 @@ const CONST = {
DISABLE: 'disable',
ENABLE: 'enable',
},
TAGS_BULK_ACTION_TYPES: {
DELETE: 'delete',
DISABLE: 'disable',
ENABLE: 'enable',
},
DISTANCE_RATES_BULK_ACTION_TYPES: {
DELETE: 'delete',
DISABLE: 'disable',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1847,6 +1847,9 @@ export default {
requiresTag: 'Members must tag all spend',
customTagName: 'Custom tag name',
enableTag: 'Enable tag',
enableTags: 'Enable tags',
disableTag: 'Disable tag',
disableTags: 'Disable tags',
addTag: 'Add tag',
editTag: 'Edit tag',
subtitle: 'Tags add more detailed ways to classify costs.',
Expand All @@ -1855,7 +1858,9 @@ export default {
subtitle: 'Add a tag to track projects, locations, departments, and more.',
},
deleteTag: 'Delete tag',
deleteTags: 'Delete tags',
deleteTagConfirmation: 'Are you sure that you want to delete this tag?',
deleteTagsConfirmation: 'Are you sure that you want to delete these tags?',
deleteFailureMessage: 'An error occurred while deleting the tag, please try again.',
tagRequiredError: 'Tag name is required.',
existingTagError: 'A tag with this name already exists.',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1871,6 +1871,9 @@ export default {
requiresTag: 'Los miembros deben etiquetar todos los gastos',
customTagName: 'Nombre de etiqueta personalizada',
enableTag: 'Habilitar etiqueta',
enableTags: 'Habilitar etiquetas',
disableTag: 'Desactivar etiqueta',
disableTags: 'Desactivar etiquetas',
addTag: 'Añadir etiqueta',
editTag: 'Editar etiqueta',
subtitle: 'Las etiquetas añaden formas más detalladas de clasificar los costos.',
Expand All @@ -1879,7 +1882,9 @@ export default {
subtitle: 'Añade una etiqueta para realizar el seguimiento de proyectos, ubicaciones, departamentos y otros.',
},
deleteTag: 'Eliminar etiqueta',
deleteTags: 'Eliminar etiquetas',
deleteTagConfirmation: '¿Estás seguro de que quieres eliminar esta etiqueta?',
deleteTagsConfirmation: '¿Estás seguro de que quieres eliminar estas etiquetas?',
deleteFailureMessage: 'Se ha producido un error al intentar eliminar la etiqueta. Por favor, inténtalo más tarde.',
tagRequiredError: 'Lo nombre de la etiqueta es obligatorio.',
existingTagError: 'Ya existe una etiqueta con este nombre.',
Expand Down
144 changes: 123 additions & 21 deletions src/pages/workspace/tags/WorkspaceTagsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useEffect, useMemo, useState} from 'react';
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
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 @@ -31,13 +34,15 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
import type DeepValueOf from '@src/types/utils/DeepValueOf';

type PolicyForList = {
value: string;
text: string;
keyForList: string;
isSelected: boolean;
rightElement: React.ReactNode;
enabled: boolean;
};

type PolicyOption = ListItem & {
Expand All @@ -58,6 +63,8 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
const theme = useTheme();
const {translate} = useLocalize();
const [selectedTags, setSelectedTags] = useState<Record<string, boolean>>({});
const dropdownButtonRef = useRef(null);
const [deleteTagsConfirmModalVisible, setDeleteTagsConfirmModalVisible] = useState(false);

function fetchTags() {
Policy.openPolicyTagsPage(route.params.policyID);
Expand All @@ -84,6 +91,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
isSelected: !!selectedTags[value.name],
pendingAction: value.pendingAction,
errors: value.errors ?? undefined,
enabled: value.enabled,
rightElement: (
<View style={styles.flexRow}>
<Text style={[styles.textSupporting, styles.alignSelfCenter, styles.pl2, styles.label]}>
Expand All @@ -103,6 +111,11 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
[policyTagLists, selectedTags, styles.alignSelfCenter, styles.flexRow, styles.label, styles.p1, styles.pl2, styles.textSupporting, theme.icon, translate],
);

const tagListKeyedByName = tagList.reduce<Record<string, PolicyForList>>((acc, tag) => {
acc[tag.value] = tag;
return acc;
}, {});

const toggleTag = (tag: PolicyForList) => {
setSelectedTags((prev) => ({
...prev,
Expand Down Expand Up @@ -135,29 +148,108 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
Navigation.navigate(ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(route.params.policyID, tag.keyForList));
};

const selectedTagsArray = Object.keys(selectedTags).filter((key) => selectedTags[key]);

const handleDeleteTags = () => {
setSelectedTags({});
Policy.deletePolicyTags(route.params.policyID, selectedTagsArray);
setDeleteTagsConfirmModalVisible(false);
};

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

const headerButtons = (
<View style={[styles.w100, styles.flexRow, isSmallScreenWidth && styles.mb3]}>
<Button
medium
success
onPress={navigateToCreateTagPage}
icon={Expensicons.Plus}
text={translate('workspace.tags.addTag')}
style={[styles.mr3, isSmallScreenWidth && styles.w50]}
/>
{policyTags && (
const getHeaderButtons = () => {
const options: Array<DropdownOption<DeepValueOf<typeof CONST.POLICY.TAGS_BULK_ACTION_TYPES>>> = [];

if (selectedTagsArray.length > 0) {
options.push({
icon: Expensicons.Trashcan,
text: translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTag' : 'workspace.tags.deleteTags'),
value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.DELETE,
onSelected: () => setDeleteTagsConfirmModalVisible(true),
});

const enabledTags = selectedTagsArray.filter((tagName) => tagListKeyedByName?.[tagName]?.enabled);
if (enabledTags.length > 0) {
const tagsToDisable = selectedTagsArray
.filter((tagName) => tagListKeyedByName?.[tagName]?.enabled)
.reduce<Record<string, {name: string; enabled: boolean}>>((acc, tagName) => {
acc[tagName] = {
name: tagName,
enabled: false,
};
return acc;
}, {});

options.push({
icon: Expensicons.DocumentSlash,
text: translate(enabledTags.length === 1 ? 'workspace.tags.disableTag' : 'workspace.tags.disableTags'),
value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.DISABLE,
onSelected: () => {
setSelectedTags({});
Policy.setWorkspaceTagEnabled(route.params.policyID, tagsToDisable);
},
});
}

const disabledTags = selectedTagsArray.filter((tagName) => !tagListKeyedByName?.[tagName]?.enabled);
if (disabledTags.length > 0) {
const tagsToEnable = selectedTagsArray
.filter((tagName) => !tagListKeyedByName?.[tagName]?.enabled)
.reduce<Record<string, {name: string; enabled: boolean}>>((acc, tagName) => {
acc[tagName] = {
name: tagName,
enabled: true,
};
return acc;
}, {});
options.push({
icon: Expensicons.Document,
text: translate(disabledTags.length === 1 ? 'workspace.tags.enableTag' : 'workspace.tags.enableTags'),
value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.ENABLE,
onSelected: () => {
setSelectedTags({});
Policy.setWorkspaceTagEnabled(route.params.policyID, tagsToEnable);
},
});
}

return (
<ButtonWithDropdownMenu
buttonRef={dropdownButtonRef}
onPress={() => null}
shouldAlwaysShowDropdownMenu
pressOnEnter
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
customText={translate('workspace.common.selected', {selectedNumber: selectedTagsArray.length})}
options={options}
style={[isSmallScreenWidth && styles.w50, isSmallScreenWidth && styles.mb3]}
/>
);
}

return (
<View style={[styles.w100, styles.flexRow, isSmallScreenWidth && styles.mb3]}>
<Button
medium
onPress={navigateToTagsSettings}
icon={Expensicons.Gear}
text={translate('common.settings')}
style={[isSmallScreenWidth && styles.w50]}
success
onPress={navigateToCreateTagPage}
icon={Expensicons.Plus}
text={translate('workspace.tags.addTag')}
style={[styles.mr3, isSmallScreenWidth && styles.w50]}
/>
)}
</View>
);
{policyTags && (
<Button
medium
onPress={navigateToTagsSettings}
icon={Expensicons.Gear}
text={translate('common.settings')}
style={[isSmallScreenWidth && styles.w50]}
/>
)}
</View>
);
};

return (
<AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
Expand All @@ -173,9 +265,19 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
title={translate('workspace.common.tags')}
shouldShowBackButton={isSmallScreenWidth}
>
{!isSmallScreenWidth && headerButtons}
{!isSmallScreenWidth && getHeaderButtons()}
</HeaderWithBackButton>
{isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{headerButtons}</View>}
<ConfirmModal
isVisible={deleteTagsConfirmModalVisible}
onConfirm={handleDeleteTags}
onCancel={() => setDeleteTagsConfirmModalVisible(false)}
title={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTag' : 'workspace.tags.deleteTags')}
prompt={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTagConfirmation' : 'workspace.tags.deleteTagsConfirmation')}
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.tags.subtitle')}</Text>
</View>
Expand Down

0 comments on commit 0ee8c4d

Please sign in to comment.