diff --git a/assets/images/simple-illustrations/simple-illustration__tag.svg b/assets/images/simple-illustrations/simple-illustration__tag.svg
new file mode 100644
index 000000000000..0cac51679a5e
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__tag.svg
@@ -0,0 +1,33 @@
+
+
+
diff --git a/assets/images/tag.svg b/assets/images/tag.svg
new file mode 100644
index 000000000000..f5e13b8135cb
--- /dev/null
+++ b/assets/images/tag.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index fb99108c7e97..f3499c983378 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -542,7 +542,10 @@ const ROUTES = {
route: 'workspace/:policyID/categories/settings',
getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const,
},
-
+ WORKSPACE_TAGS: {
+ route: 'workspace/:policyID/tags',
+ getRoute: (policyID: string) => `workspace/${policyID}/tags` as const,
+ },
// Referral program promotion
REFERRAL_DETAILS_MODAL: {
route: 'referral/:contentType',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index cc7df01524f7..76a9eaa96009 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -214,6 +214,7 @@ const SCREENS = {
INVITE: 'Workspace_Invite',
INVITE_MESSAGE: 'Workspace_Invite_Message',
CATEGORIES: 'Workspace_Categories',
+ TAGS: 'Workspace_Tags',
CURRENCY: 'Workspace_Profile_Currency',
WORKFLOWS: 'Workspace_Workflows',
WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency',
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 6c6c1b86eee1..d9f46203a703 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -135,6 +135,7 @@ import Twitter from '@assets/images/social-twitter.svg';
import Youtube from '@assets/images/social-youtube.svg';
import Stopwatch from '@assets/images/stopwatch.svg';
import Sync from '@assets/images/sync.svg';
+import Tag from '@assets/images/tag.svg';
import Task from '@assets/images/task.svg';
import ThreeDots from '@assets/images/three-dots.svg';
import ThumbsUp from '@assets/images/thumbs-up.svg';
@@ -222,6 +223,7 @@ export {
FlagLevelThree,
Fullscreen,
Folder,
+ Tag,
Gallery,
Gear,
Globe,
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index f8c048ebc4c0..7f60ad3867c8 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -65,6 +65,7 @@ import ReceiptWrangler from '@assets/images/simple-illustrations/simple-illustra
import SanFrancisco from '@assets/images/simple-illustrations/simple-illustration__sanfrancisco.svg';
import ShieldYellow from '@assets/images/simple-illustrations/simple-illustration__shield.svg';
import SmallRocket from '@assets/images/simple-illustrations/simple-illustration__smallrocket.svg';
+import Tag from '@assets/images/simple-illustrations/simple-illustration__tag.svg';
import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg';
import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg';
import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg';
@@ -146,4 +147,5 @@ export {
Workflows,
ThreeLeggedLaptopWoman,
House,
+ Tag,
};
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 2a0139c64c07..2206b44899c2 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1727,6 +1727,7 @@ export default {
settings: 'Settings',
reimburse: 'Reimbursements',
categories: 'Categories',
+ tags: 'Tags',
bills: 'Bills',
invoices: 'Invoices',
travel: 'Travel',
@@ -1766,6 +1767,15 @@ export default {
},
genericFailureMessage: 'An error occurred while updating the category, please try again.',
},
+ tags: {
+ requiresTag: 'Members must tag all spend',
+ enableTag: 'Enable tag',
+ subtitle: 'Tags add more detailed ways to classify costs.',
+ emptyTags: {
+ title: "You haven't created any tags",
+ subtitle: 'Add a tag to track projects, locations, departments, and more.',
+ },
+ },
emptyWorkspace: {
title: 'Create a workspace',
subtitle: 'Workspaces are where you’ll chat with your team, reimburse expenses, issue cards, send invoices, pay bills, and more - all in one place.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 20f4cf8aeac8..9e63af9dc982 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1751,6 +1751,7 @@ export default {
settings: 'Configuración',
reimburse: 'Reembolsos',
categories: 'Categorías',
+ tags: 'Etiquetas',
bills: 'Pagar facturas',
invoices: 'Enviar facturas',
travel: 'Viajes',
@@ -1790,6 +1791,15 @@ export default {
},
genericFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.',
},
+ tags: {
+ requiresTag: 'Los miembros deben etiquetar todos los gastos',
+ enableTag: 'Habilitar etiqueta',
+ subtitle: 'Las etiquetas añaden formas más detalladas de clasificar los costos.',
+ emptyTags: {
+ title: 'No has creado ninguna etiqueta',
+ subtitle: 'Añade una etiqueta para realizar el seguimiento de proyectos, ubicaciones, departamentos y otros.',
+ },
+ },
emptyWorkspace: {
title: 'Crea un espacio de trabajo',
subtitle: 'En los espacios de trabajo podrás chatear con tu equipo, reembolsar gastos, emitir tarjetas, enviar y pagar facturas, y mucho más - todo en un mismo lugar.',
diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
index 1e5d3639a32f..976699e31716 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
@@ -24,6 +24,7 @@ const workspaceSettingsScreens = {
[SCREENS.WORKSPACE.TRAVEL]: () => require('../../../../../pages/workspace/travel/WorkspaceTravelPage').default as React.ComponentType,
[SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../../pages/workspace/WorkspaceMembersPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CATEGORIES]: () => require('../../../../../pages/workspace/categories/WorkspaceCategoriesPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.TAGS]: () => require('../../../../../pages/workspace/tags/WorkspaceTagsPage').default as React.ComponentType,
} satisfies Screens;
function BaseCentralPaneNavigator() {
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 7e0e6c028ff1..8328e0e19688 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -67,6 +67,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.CATEGORIES]: {
path: ROUTES.WORKSPACE_CATEGORIES.route,
},
+ [SCREENS.WORKSPACE.TAGS]: {
+ path: ROUTES.WORKSPACE_TAGS.route,
+ },
},
},
[SCREENS.NOT_FOUND]: '*',
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 6d680ac7e190..e1729f36e6f0 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -87,6 +87,9 @@ type CentralPaneNavigatorParamList = {
[SCREENS.WORKSPACE.CATEGORIES]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.TAGS]: {
+ policyID: string;
+ };
};
type WorkspaceSwitcherNavigatorParamList = {
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 571e4cafce74..a18d2667f1ea 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -169,6 +169,12 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)))),
routeName: SCREENS.WORKSPACE.CATEGORIES,
},
+ {
+ translationKey: 'workspace.common.tags',
+ icon: Expensicons.Tag,
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAGS.getRoute(policyID)))),
+ routeName: SCREENS.WORKSPACE.TAGS,
+ },
];
const menuItems: WorkspaceMenuItem[] = [
diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
new file mode 100644
index 000000000000..c82740eff361
--- /dev/null
+++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
@@ -0,0 +1,144 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useMemo, useState} 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 Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import * as Illustrations from '@components/Icon/Illustrations';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import TableListItem from '@components/SelectionList/TableListItem';
+import Text from '@components/Text';
+import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection';
+import useLocalize from '@hooks/useLocalize';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import type {CentralPaneNavigatorParamList} from '@navigation/types';
+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 PolicyForList = {
+ value: string;
+ text: string;
+ keyForList: string;
+ isSelected: boolean;
+ rightElement: React.ReactNode;
+};
+
+type WorkspaceTagsOnyxProps = {
+ /** Collection of tags attached to a policy */
+ policyTags: OnyxEntry;
+};
+
+type WorkspaceTagsPageProps = WorkspaceTagsOnyxProps & StackScreenProps;
+
+function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const {translate} = useLocalize();
+ const [selectedTags, setSelectedTags] = useState>({});
+ const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]);
+ const tagList = useMemo(
+ () =>
+ policyTagLists
+ .map((policyTagList) =>
+ Object.values(policyTagList.tags).map((value) => ({
+ value: value.name,
+ text: value.name,
+ keyForList: value.name,
+ isSelected: !!selectedTags[value.name],
+ rightElement: (
+
+
+ {value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')}
+
+
+
+
+
+ ),
+ })),
+ )
+ .flat(),
+ [policyTagLists, selectedTags, styles.alignSelfCenter, styles.disabledText, styles.flexRow, styles.p1, styles.pl2, theme.icon, translate],
+ );
+
+ const toggleTag = (tag: PolicyForList) => {
+ setSelectedTags((prev) => ({
+ ...prev,
+ [tag.value]: !prev[tag.value],
+ }));
+ };
+
+ const toggleAllTags = () => {
+ const isAllSelected = tagList.every((tag) => !!selectedTags[tag.value]);
+ setSelectedTags(isAllSelected ? {} : Object.fromEntries(tagList.map((item) => [item.value, true])));
+ };
+
+ const getCustomListHeader = () => (
+
+ {translate('common.name')}
+ {translate('statusPage.status')}
+
+ );
+
+ return (
+
+
+
+
+
+ {translate('workspace.tags.subtitle')}
+
+ {tagList.length ? (
+ {}}
+ onSelectAll={toggleAllTags}
+ showScrollIndicator
+ ListItem={TableListItem}
+ customListHeader={getCustomListHeader()}
+ listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
+ />
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+WorkspaceTagsPage.displayName = 'WorkspaceTagsPage';
+
+export default withOnyx({
+ policyTags: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${route.params.policyID}`,
+ },
+})(WorkspaceTagsPage);