Skip to content

Commit 78db1ae

Browse files
authored
Merge pull request #38647 from allroundexperts/feat-38635
feat: add bulk tag delete / enable / disable options
2 parents 6c2b028 + 592ab8a commit 78db1ae

File tree

5 files changed

+139
-22
lines changed

5 files changed

+139
-22
lines changed

src/CONST.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1477,6 +1477,11 @@ const CONST = {
14771477
DISABLE: 'disable',
14781478
ENABLE: 'enable',
14791479
},
1480+
TAGS_BULK_ACTION_TYPES: {
1481+
DELETE: 'delete',
1482+
DISABLE: 'disable',
1483+
ENABLE: 'enable',
1484+
},
14801485
DISTANCE_RATES_BULK_ACTION_TYPES: {
14811486
DELETE: 'delete',
14821487
DISABLE: 'disable',

src/languages/en.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1847,6 +1847,9 @@ export default {
18471847
requiresTag: 'Members must tag all spend',
18481848
customTagName: 'Custom tag name',
18491849
enableTag: 'Enable tag',
1850+
enableTags: 'Enable tags',
1851+
disableTag: 'Disable tag',
1852+
disableTags: 'Disable tags',
18501853
addTag: 'Add tag',
18511854
editTag: 'Edit tag',
18521855
subtitle: 'Tags add more detailed ways to classify costs.',
@@ -1855,7 +1858,9 @@ export default {
18551858
subtitle: 'Add a tag to track projects, locations, departments, and more.',
18561859
},
18571860
deleteTag: 'Delete tag',
1861+
deleteTags: 'Delete tags',
18581862
deleteTagConfirmation: 'Are you sure that you want to delete this tag?',
1863+
deleteTagsConfirmation: 'Are you sure that you want to delete these tags?',
18591864
deleteFailureMessage: 'An error occurred while deleting the tag, please try again.',
18601865
tagRequiredError: 'Tag name is required.',
18611866
existingTagError: 'A tag with this name already exists.',

src/languages/es.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1871,6 +1871,9 @@ export default {
18711871
requiresTag: 'Los miembros deben etiquetar todos los gastos',
18721872
customTagName: 'Nombre de etiqueta personalizada',
18731873
enableTag: 'Habilitar etiqueta',
1874+
enableTags: 'Habilitar etiquetas',
1875+
disableTag: 'Desactivar etiqueta',
1876+
disableTags: 'Desactivar etiquetas',
18741877
addTag: 'Añadir etiqueta',
18751878
editTag: 'Editar etiqueta',
18761879
subtitle: 'Las etiquetas añaden formas más detalladas de clasificar los costos.',
@@ -1879,7 +1882,9 @@ export default {
18791882
subtitle: 'Añade una etiqueta para realizar el seguimiento de proyectos, ubicaciones, departamentos y otros.',
18801883
},
18811884
deleteTag: 'Eliminar etiqueta',
1885+
deleteTags: 'Eliminar etiquetas',
18821886
deleteTagConfirmation: '¿Estás seguro de que quieres eliminar esta etiqueta?',
1887+
deleteTagsConfirmation: '¿Estás seguro de que quieres eliminar estas etiquetas?',
18831888
deleteFailureMessage: 'Se ha producido un error al intentar eliminar la etiqueta. Por favor, inténtalo más tarde.',
18841889
tagRequiredError: 'Lo nombre de la etiqueta es obligatorio.',
18851890
existingTagError: 'Ya existe una etiqueta con este nombre.',

src/pages/workspace/categories/WorkspaceCategoriesPage.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
228228
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
229229
customText={translate('workspace.common.selected', {selectedNumber: selectedCategoriesArray.length})}
230230
options={options}
231-
style={[isSmallScreenWidth && styles.w50, isSmallScreenWidth && styles.mb3]}
231+
style={[isSmallScreenWidth && styles.flexGrow1, isSmallScreenWidth && styles.mb3]}
232232
/>
233233
);
234234
}

src/pages/workspace/tags/WorkspaceTagsPage.tsx

+123-21
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import type {StackScreenProps} from '@react-navigation/stack';
2-
import React, {useEffect, useMemo, useState} from 'react';
2+
import React, {useEffect, useMemo, useRef, useState} from 'react';
33
import {ActivityIndicator, View} from 'react-native';
44
import {withOnyx} from 'react-native-onyx';
55
import type {OnyxEntry} from 'react-native-onyx';
66
import Button from '@components/Button';
7+
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
8+
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
9+
import ConfirmModal from '@components/ConfirmModal';
710
import HeaderWithBackButton from '@components/HeaderWithBackButton';
811
import Icon from '@components/Icon';
912
import * as Expensicons from '@components/Icon/Expensicons';
@@ -31,13 +34,15 @@ import ONYXKEYS from '@src/ONYXKEYS';
3134
import ROUTES from '@src/ROUTES';
3235
import type SCREENS from '@src/SCREENS';
3336
import type * as OnyxTypes from '@src/types/onyx';
37+
import type DeepValueOf from '@src/types/utils/DeepValueOf';
3438

3539
type PolicyForList = {
3640
value: string;
3741
text: string;
3842
keyForList: string;
3943
isSelected: boolean;
4044
rightElement: React.ReactNode;
45+
enabled: boolean;
4146
};
4247

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

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

114+
const tagListKeyedByName = tagList.reduce<Record<string, PolicyForList>>((acc, tag) => {
115+
acc[tag.value] = tag;
116+
return acc;
117+
}, {});
118+
106119
const toggleTag = (tag: PolicyForList) => {
107120
setSelectedTags((prev) => ({
108121
...prev,
@@ -135,29 +148,108 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
135148
Navigation.navigate(ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(route.params.policyID, tag.keyForList));
136149
};
137150

151+
const selectedTagsArray = Object.keys(selectedTags).filter((key) => selectedTags[key]);
152+
153+
const handleDeleteTags = () => {
154+
setSelectedTags({});
155+
Policy.deletePolicyTags(route.params.policyID, selectedTagsArray);
156+
setDeleteTagsConfirmModalVisible(false);
157+
};
158+
138159
const isLoading = !isOffline && policyTags === undefined;
139160

140-
const headerButtons = (
141-
<View style={[styles.w100, styles.flexRow, isSmallScreenWidth && styles.mb3]}>
142-
<Button
143-
medium
144-
success
145-
onPress={navigateToCreateTagPage}
146-
icon={Expensicons.Plus}
147-
text={translate('workspace.tags.addTag')}
148-
style={[styles.mr3, isSmallScreenWidth && styles.w50]}
149-
/>
150-
{policyTags && (
161+
const getHeaderButtons = () => {
162+
const options: Array<DropdownOption<DeepValueOf<typeof CONST.POLICY.TAGS_BULK_ACTION_TYPES>>> = [];
163+
164+
if (selectedTagsArray.length > 0) {
165+
options.push({
166+
icon: Expensicons.Trashcan,
167+
text: translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTag' : 'workspace.tags.deleteTags'),
168+
value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.DELETE,
169+
onSelected: () => setDeleteTagsConfirmModalVisible(true),
170+
});
171+
172+
const enabledTags = selectedTagsArray.filter((tagName) => tagListKeyedByName?.[tagName]?.enabled);
173+
if (enabledTags.length > 0) {
174+
const tagsToDisable = selectedTagsArray
175+
.filter((tagName) => tagListKeyedByName?.[tagName]?.enabled)
176+
.reduce<Record<string, {name: string; enabled: boolean}>>((acc, tagName) => {
177+
acc[tagName] = {
178+
name: tagName,
179+
enabled: false,
180+
};
181+
return acc;
182+
}, {});
183+
184+
options.push({
185+
icon: Expensicons.DocumentSlash,
186+
text: translate(enabledTags.length === 1 ? 'workspace.tags.disableTag' : 'workspace.tags.disableTags'),
187+
value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.DISABLE,
188+
onSelected: () => {
189+
setSelectedTags({});
190+
Policy.setWorkspaceTagEnabled(route.params.policyID, tagsToDisable);
191+
},
192+
});
193+
}
194+
195+
const disabledTags = selectedTagsArray.filter((tagName) => !tagListKeyedByName?.[tagName]?.enabled);
196+
if (disabledTags.length > 0) {
197+
const tagsToEnable = selectedTagsArray
198+
.filter((tagName) => !tagListKeyedByName?.[tagName]?.enabled)
199+
.reduce<Record<string, {name: string; enabled: boolean}>>((acc, tagName) => {
200+
acc[tagName] = {
201+
name: tagName,
202+
enabled: true,
203+
};
204+
return acc;
205+
}, {});
206+
options.push({
207+
icon: Expensicons.Document,
208+
text: translate(disabledTags.length === 1 ? 'workspace.tags.enableTag' : 'workspace.tags.enableTags'),
209+
value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.ENABLE,
210+
onSelected: () => {
211+
setSelectedTags({});
212+
Policy.setWorkspaceTagEnabled(route.params.policyID, tagsToEnable);
213+
},
214+
});
215+
}
216+
217+
return (
218+
<ButtonWithDropdownMenu
219+
buttonRef={dropdownButtonRef}
220+
onPress={() => null}
221+
shouldAlwaysShowDropdownMenu
222+
pressOnEnter
223+
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
224+
customText={translate('workspace.common.selected', {selectedNumber: selectedTagsArray.length})}
225+
options={options}
226+
style={[isSmallScreenWidth && styles.flexGrow1, isSmallScreenWidth && styles.mb3]}
227+
/>
228+
);
229+
}
230+
231+
return (
232+
<View style={[styles.w100, styles.flexRow, isSmallScreenWidth && styles.mb3]}>
151233
<Button
152234
medium
153-
onPress={navigateToTagsSettings}
154-
icon={Expensicons.Gear}
155-
text={translate('common.settings')}
156-
style={[isSmallScreenWidth && styles.w50]}
235+
success
236+
onPress={navigateToCreateTagPage}
237+
icon={Expensicons.Plus}
238+
text={translate('workspace.tags.addTag')}
239+
style={[styles.mr3, isSmallScreenWidth && styles.w50]}
157240
/>
158-
)}
159-
</View>
160-
);
241+
{policyTags && (
242+
<Button
243+
medium
244+
onPress={navigateToTagsSettings}
245+
icon={Expensicons.Gear}
246+
text={translate('common.settings')}
247+
style={[isSmallScreenWidth && styles.w50]}
248+
/>
249+
)}
250+
</View>
251+
);
252+
};
161253

162254
return (
163255
<AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
@@ -173,9 +265,19 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
173265
title={translate('workspace.common.tags')}
174266
shouldShowBackButton={isSmallScreenWidth}
175267
>
176-
{!isSmallScreenWidth && headerButtons}
268+
{!isSmallScreenWidth && getHeaderButtons()}
177269
</HeaderWithBackButton>
178-
{isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{headerButtons}</View>}
270+
<ConfirmModal
271+
isVisible={deleteTagsConfirmModalVisible}
272+
onConfirm={handleDeleteTags}
273+
onCancel={() => setDeleteTagsConfirmModalVisible(false)}
274+
title={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTag' : 'workspace.tags.deleteTags')}
275+
prompt={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTagConfirmation' : 'workspace.tags.deleteTagsConfirmation')}
276+
confirmText={translate('common.delete')}
277+
cancelText={translate('common.cancel')}
278+
danger
279+
/>
280+
{isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>}
179281
<View style={[styles.ph5, styles.pb5, styles.pt3]}>
180282
<Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.tags.subtitle')}</Text>
181283
</View>

0 commit comments

Comments
 (0)