diff --git a/assets/images/document-slash.svg b/assets/images/document-slash.svg
index 25a4c96038b4..ebb183142e40 100644
--- a/assets/images/document-slash.svg
+++ b/assets/images/document-slash.svg
@@ -1,6 +1 @@
-
+
diff --git a/src/CONST.ts b/src/CONST.ts
index d4fbd0ff6ef3..6b7ede6f0e10 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1414,6 +1414,11 @@ const CONST = {
MAKE_MEMBER: 'makeMember',
MAKE_ADMIN: 'makeAdmin',
},
+ CATEGORIES_BULK_ACTION_TYPES: {
+ DELETE: 'delete',
+ DISABLE: 'disable',
+ ENABLE: 'enable',
+ },
DISTANCE_RATES_BULK_ACTION_TYPES: {
DELETE: 'delete',
DISABLE: 'disable',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 7e442eee2236..c602b2fad14c 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1752,6 +1752,7 @@ export default {
testTransactions: 'Test transactions',
issueAndManageCards: 'Issue and manage cards',
reconcileCards: 'Reconcile cards',
+ selected: ({selectedNumber}) => `${selectedNumber} selected`,
settlementFrequency: 'Settlement frequency',
deleteConfirmation: 'Are you sure you want to delete this workspace?',
unavailable: 'Unavailable workspace',
@@ -1766,7 +1767,6 @@ export default {
moreFeatures: 'More features',
requested: 'Requested',
distanceRates: 'Distance rates',
- selected: ({selectedNumber}) => `${selectedNumber} selected`,
},
type: {
free: 'Free',
@@ -1774,6 +1774,10 @@ export default {
collect: 'Collect',
},
categories: {
+ deleteCategories: 'Delete categories',
+ disableCategories: 'Disable categories',
+ enableCategories: 'Enable categories',
+ deleteFailureMessage: 'An error occurred while deleting the category, please try again.',
categoryName: 'Category name',
requiresCategory: 'Members must categorize all spend',
enableCategory: 'Enable category',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 267581f043ee..bb54aa6e51f9 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1776,6 +1776,7 @@ export default {
testTransactions: 'Transacciones de prueba',
issueAndManageCards: 'Emitir y gestionar tarjetas',
reconcileCards: 'Reconciliar tarjetas',
+ selected: ({selectedNumber}) => `${selectedNumber} seleccionados`,
settlementFrequency: 'Frecuencia de liquidación',
deleteConfirmation: '¿Estás seguro de que quieres eliminar este espacio de trabajo?',
unavailable: 'Espacio de trabajo no disponible',
@@ -1790,7 +1791,6 @@ export default {
moreFeatures: 'Más características',
requested: 'Solicitado',
distanceRates: 'Tasas de distancia',
- selected: ({selectedNumber}) => `${selectedNumber} seleccionados`,
},
type: {
free: 'Gratis',
@@ -1798,6 +1798,10 @@ export default {
collect: 'Recolectar',
},
categories: {
+ deleteCategories: 'Eliminar categorías',
+ disableCategories: 'Desactivar categorías',
+ enableCategories: 'Activar categorías',
+ 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',
diff --git a/src/libs/API/parameters/DeleteWorkspaceCategoriesParams.ts b/src/libs/API/parameters/DeleteWorkspaceCategoriesParams.ts
new file mode 100644
index 000000000000..07a8103a9b06
--- /dev/null
+++ b/src/libs/API/parameters/DeleteWorkspaceCategoriesParams.ts
@@ -0,0 +1,10 @@
+type DeleteWorkspaceCategoriesParams = {
+ policyID: string;
+ /**
+ * A JSON-encoded string representing an array of category names to be deleted.
+ * Each element in the array is a string that specifies the name of a category.
+ */
+ categories: string;
+};
+
+export default DeleteWorkspaceCategoriesParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 687f32f5b6de..1cf59f0eafea 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -152,6 +152,7 @@ export type {default as UpdateWorkspaceMembersRoleParams} from './UpdateWorkspac
export type {default as SetWorkspaceCategoriesEnabledParams} from './SetWorkspaceCategoriesEnabledParams';
export type {default as CreateWorkspaceCategoriesParams} from './CreateWorkspaceCategoriesParams';
export type {default as SetWorkspaceRequiresCategoryParams} from './SetWorkspaceRequiresCategoryParams';
+export type {default as DeleteWorkspaceCategoriesParams} from './DeleteWorkspaceCategoriesParams';
export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAutoReportingParams';
export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams';
export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 17c38b252e0b..fa110933c18a 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -120,6 +120,7 @@ const WRITE_COMMANDS = {
CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories',
CREATE_POLICY_TAG: 'CreatePolicyTag',
SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory',
+ DELETE_WORKSPACE_CATEGORIES: 'DeleteWorkspaceCategories',
SET_POLICY_REQUIRES_TAG: 'SetPolicyRequiresTag',
RENAME_POLICY_TAG_LIST: 'RenamePolicyTaglist',
CREATE_TASK: 'CreateTask',
@@ -282,6 +283,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams;
[WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES]: Parameters.CreateWorkspaceCategoriesParams;
[WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
+ [WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams;
[WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag;
[WRITE_COMMANDS.RENAME_POLICY_TAG_LIST]: Parameters.RenamePolicyTaglist;
[WRITE_COMMANDS.CREATE_POLICY_TAG]: Parameters.CreatePolicyTagsParams;
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 4689fd03ebd0..d5575869444a 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -4,7 +4,7 @@ import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {PersonalDetailsList, Policy, PolicyMembers, PolicyTagList, PolicyTags} from '@src/types/onyx';
+import type {PersonalDetailsList, Policy, PolicyCategories, PolicyMembers, PolicyTagList, PolicyTags} from '@src/types/onyx';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import Navigation from './Navigation/Navigation';
@@ -30,6 +30,13 @@ function hasPolicyMemberError(policyMembers: OnyxEntry): boolean
return Object.values(policyMembers ?? {}).some((member) => Object.keys(member?.errors ?? {}).length > 0);
}
+/**
+ * Check if the policy has any errors within the categories.
+ */
+function hasPolicyCategoriesError(policyCategories: OnyxEntry): boolean {
+ return Object.keys(policyCategories ?? {}).some((categoryName) => Object.keys(policyCategories?.[categoryName]?.errors ?? {}).length > 0);
+}
+
/**
* Check if the policy has any error fields.
*/
@@ -295,6 +302,7 @@ export {
getPathWithoutPolicyID,
getPolicyMembersByIdWithoutCurrentUser,
goBackFromInvalidPolicy,
+ hasPolicyCategoriesError,
};
export type {MemberEmailsToAccountIDs};
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index 48b9948fc1ff..68ed15ece9a9 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -64,6 +64,7 @@ import type {
PersonalDetailsList,
Policy,
PolicyCategories,
+ PolicyCategory,
PolicyMember,
PolicyTagList,
RecentlyUsedCategories,
@@ -2693,7 +2694,7 @@ function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Recor
categories: JSON.stringify(Object.keys(categoriesToUpdate).map((key) => categoriesToUpdate[key])),
};
- API.write('SetWorkspaceCategoriesEnabled', parameters, onyxData);
+ API.write(WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED, parameters, onyxData);
}
function createPolicyCategory(policyID: string, categoryName: string) {
@@ -2861,124 +2862,193 @@ function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolea
requiresCategory,
};
- API.write('SetWorkspaceRequiresCategory', parameters, onyxData);
+ API.write(WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY, parameters, onyxData);
}
-function setPolicyRequiresTag(policyID: string, requiresTag: boolean) {
+function clearCategoryErrors(policyID: string, categoryName: string) {
+ const category = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName];
+
+ if (!category) {
+ return;
+ }
+
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {
+ [category.name]: {
+ errors: null,
+ },
+ });
+}
+
+function deleteWorkspaceCategories(policyID: string, categoryNamesToDelete: string[]) {
const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- value: {
- requiresTag,
- errors: {
- requiresTag: null,
- },
- pendingFields: {
- requiresTag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
- },
- },
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: categoryNamesToDelete.reduce>>((acc, categoryName) => {
+ acc[categoryName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE};
+ return acc;
+ }, {}),
},
],
successData: [
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- value: {
- errors: {
- requiresTag: null,
- },
- pendingFields: {
- requiresTag: null,
- },
- },
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: categoryNamesToDelete.reduce>((acc, categoryName) => {
+ acc[categoryName] = null;
+ return acc;
+ }, {}),
},
],
failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- value: {
- requiresTag: !requiresTag,
- errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'),
- pendingFields: {
- requiresTag: null,
- },
- },
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: categoryNamesToDelete.reduce>>((acc, categoryName) => {
+ acc[categoryName] = {
+ pendingAction: null,
+ errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.deleteFailureMessage'),
+ };
+ return acc;
+ }, {}),
},
],
};
const parameters = {
policyID,
- requiresTag,
+ categories: JSON.stringify(categoryNamesToDelete),
};
- API.write(WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG, parameters, onyxData);
+ API.write(WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES, parameters, onyxData);
}
-function renamePolicyTaglist(policyID: string, policyTagListName: {oldName: string; newName: string}, policyTags: OnyxEntry) {
- const newName = policyTagListName.newName;
- const oldName = policyTagListName.oldName;
-
- if (oldName === newName) {
+/**
+ * Accept user join request to a workspace
+ */
+function acceptJoinRequest(reportID: string, reportAction: OnyxEntry) {
+ const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT;
+ if (!reportAction) {
return;
}
- const oldPolicyTags = policyTags?.[oldName] ?? {};
- const onyxData: OnyxData = {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
- value: {
- [newName]: {...oldPolicyTags, name: newName, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD},
- [oldName]: null,
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice},
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
},
- ],
- successData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
- value: {
- [newName]: {pendingAction: null},
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice},
+ pendingAction: null,
},
},
- ],
- failureData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
- value: {
- [newName]: null,
- [oldName]: {...oldPolicyTags, errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage')},
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice: ''},
+ pendingAction: null,
},
},
- ],
- };
+ },
+ ];
+
const parameters = {
- policyID,
- oldName,
- newName,
+ requests: JSON.stringify({
+ [(reportAction.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage']).policyID]: {
+ requests: [{accountID: reportAction?.actorAccountID, adminsRoomMessageReportActionID: reportAction.reportActionID}],
+ },
+ }),
};
- API.write(WRITE_COMMANDS.RENAME_POLICY_TAG_LIST, parameters, onyxData);
+ API.write(WRITE_COMMANDS.ACCEPT_JOIN_REQUEST, parameters, {optimisticData, failureData, successData});
}
-function clearCategoryErrors(policyID: string, categoryName: string) {
- const category = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName];
-
- if (!category) {
+/**
+ * Decline user join request to a workspace
+ */
+function declineJoinRequest(reportID: string, reportAction: OnyxEntry) {
+ if (!reportAction) {
return;
}
+ const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.DECLINE;
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice},
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ },
+ ];
- Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {
- [category.name]: {
- errors: null,
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice},
+ pendingAction: null,
+ },
+ },
},
- });
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice: ''},
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ const parameters = {
+ requests: JSON.stringify({
+ [(reportAction.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage']).policyID]: {
+ requests: [{accountID: reportAction?.actorAccountID, adminsRoomMessageReportActionID: reportAction.reportActionID}],
+ },
+ }),
+ };
+
+ API.write(WRITE_COMMANDS.DECLINE_JOIN_REQUEST, parameters, {optimisticData, failureData, successData});
+}
+
+function openPolicyDistanceRatesPage(policyID?: string) {
+ if (!policyID) {
+ return;
+ }
+
+ const params: OpenPolicyDistanceRatesPageParams = {policyID};
+
+ API.read(READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE, params);
}
function navigateWhenEnableFeature(policyID: string, featureRoute: Route) {
@@ -3321,131 +3391,105 @@ function enablePolicyWorkflows(policyID: string, enabled: boolean) {
}
}
-/**
- * Accept user join request to a workspace
- */
-function acceptJoinRequest(reportID: string, reportAction: OnyxEntry) {
- const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT;
- if (!reportAction) {
- return;
- }
-
- const optimisticData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
- value: {
- [reportAction.reportActionID]: {
- originalMessage: {choice},
- pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+function renamePolicyTaglist(policyID: string, policyTagListName: {oldName: string; newName: string}, policyTags: OnyxEntry) {
+ const newName = policyTagListName.newName;
+ const oldName = policyTagListName.oldName;
+ const oldPolicyTags = policyTags?.[oldName] ?? {};
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ value: {
+ [newName]: {...oldPolicyTags, name: newName, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD},
+ [oldName]: null,
},
},
- },
- ];
-
- const successData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
- value: {
- [reportAction.reportActionID]: {
- originalMessage: {choice},
- pendingAction: null,
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ value: {
+ [newName]: {pendingAction: null},
+ [oldName]: null,
},
},
- },
- ];
-
- const failureData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
- value: {
- [reportAction.reportActionID]: {
- originalMessage: {choice: ''},
- pendingAction: null,
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ value: {
+ errors: {
+ [oldName]: oldName,
+ [newName]: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'),
+ },
+ [newName]: null,
+ [oldName]: oldPolicyTags,
},
},
- },
- ];
-
+ ],
+ };
const parameters = {
- requests: JSON.stringify({
- [(reportAction.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage']).policyID]: {
- requests: [{accountID: reportAction?.actorAccountID, adminsRoomMessageReportActionID: reportAction.reportActionID}],
- },
- }),
+ policyID,
+ oldName,
+ newName,
};
- API.write(WRITE_COMMANDS.ACCEPT_JOIN_REQUEST, parameters, {optimisticData, failureData, successData});
+ API.write(WRITE_COMMANDS.RENAME_POLICY_TAG_LIST, parameters, onyxData);
}
-/**
- * Decline user join request to a workspace
- */
-function declineJoinRequest(reportID: string, reportAction: OnyxEntry) {
- if (!reportAction) {
- return;
- }
- const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.DECLINE;
- const optimisticData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
- value: {
- [reportAction.reportActionID]: {
- originalMessage: {choice},
- pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+function setPolicyRequiresTag(policyID: string, requiresTag: boolean) {
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ requiresTag,
+ errors: {requiresTag: null},
+ pendingFields: {
+ requiresTag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
},
},
- },
- ];
-
- const successData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
- value: {
- [reportAction.reportActionID]: {
- originalMessage: {choice},
- pendingAction: null,
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ errors: {
+ requiresTag: null,
+ },
+ pendingFields: {
+ requiresTag: null,
+ },
},
},
- },
- ];
-
- const failureData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
- value: {
- [reportAction.reportActionID]: {
- originalMessage: {choice: ''},
- pendingAction: null,
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ requiresTag: !requiresTag,
+ errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'),
+ pendingFields: {
+ requiresTag: null,
+ },
},
},
- },
- ];
+ ],
+ };
const parameters = {
- requests: JSON.stringify({
- [(reportAction.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage']).policyID]: {
- requests: [{accountID: reportAction?.actorAccountID, adminsRoomMessageReportActionID: reportAction.reportActionID}],
- },
- }),
+ policyID,
+ requiresTag,
};
- API.write(WRITE_COMMANDS.DECLINE_JOIN_REQUEST, parameters, {optimisticData, failureData, successData});
-}
-
-function openPolicyDistanceRatesPage(policyID?: string) {
- if (!policyID) {
- return;
- }
-
- const params: OpenPolicyDistanceRatesPageParams = {policyID};
-
- API.read(READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE, params);
+ API.write(WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG, parameters, onyxData);
}
export {
@@ -3519,4 +3563,5 @@ export {
openPolicyDistanceRatesPage,
createPolicyTag,
clearWorkspaceReimbursementErrors,
+ deleteWorkspaceCategories,
};
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 38828a0406ef..c1b3a490fec0 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -48,6 +48,9 @@ type WorkspaceMenuItem = {
type WorkspaceInitialPageOnyxProps = {
/** Bank account attached to free plan */
reimbursementAccount: OnyxEntry;
+
+ /** Collection of categories attached to a policy */
+ policyCategories: OnyxEntry;
};
type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInitialPageOnyxProps & StackScreenProps;
@@ -57,7 +60,7 @@ function dismissError(policyID: string) {
Policy.removeWorkspace(policyID);
}
-function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, reimbursementAccount}: WorkspaceInitialPageProps) {
+function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, reimbursementAccount, policyCategories}: WorkspaceInitialPageProps) {
const styles = useThemeStyles();
const policy = policyDraft?.id ? policyDraft : policyProp;
const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false);
@@ -97,6 +100,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
}, [policyID, policyName]);
const hasMembersError = PolicyUtils.hasPolicyMemberError(policyMembers);
+ const hasPolicyCategoryError = PolicyUtils.hasPolicyCategoriesError(policyCategories);
const hasGeneralSettingsError = !isEmptyObject(policy?.errorFields?.generalSettings ?? {}) || !isEmptyObject(policy?.errorFields?.avatar ?? {});
const shouldShowProtectedItems = PolicyUtils.isPolicyAdmin(policy);
const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy);
@@ -170,6 +174,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
translationKey: 'workspace.common.categories',
icon: Expensicons.Folder,
action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)))),
+ brickRoadIndicator: hasPolicyCategoryError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
routeName: SCREENS.WORKSPACE.CATEGORIES,
});
}
@@ -302,5 +307,8 @@ export default withPolicyAndFullscreenLoading(
reimbursementAccount: {
key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
},
+ policyCategories: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params?.policyID ?? '0'}`,
+ },
})(WorkspaceInitialPage),
);
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
index 3f2ef8ce6aa6..d22b822359f9 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -1,9 +1,11 @@
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 HeaderWithBackButton from '@components/HeaderWithBackButton';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -11,6 +13,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import TableListItem from '@components/SelectionList/TableListItem';
+import type {ListItem} from '@components/SelectionList/types';
import Text from '@components/Text';
import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection';
import useLocalize from '@hooks/useLocalize';
@@ -18,6 +21,7 @@ import useNetwork from '@hooks/useNetwork';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import {deleteWorkspaceCategories, setWorkspaceCategoryEnabled} from '@libs/actions/Policy';
import localeCompare from '@libs/LocaleCompare';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
@@ -30,13 +34,11 @@ 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;
+type PolicyOption = ListItem & {
+ /** Category name is used as a key for the selectedCategories state */
keyForList: string;
- isSelected: boolean;
- rightElement: React.ReactNode;
};
type WorkspaceCategoriesOnyxProps = {
@@ -55,6 +57,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
const theme = useTheme();
const {translate} = useLocalize();
const [selectedCategories, setSelectedCategories] = useState>({});
+ const dropdownButtonRef = useRef(null);
function fetchCategories() {
Policy.openPolicyCategoriesPage(route.params.policyID);
@@ -67,43 +70,63 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- const categoryList = useMemo(
+ const categoryList = useMemo(
() =>
Object.values(policyCategories ?? {})
.sort((a, b) => localeCompare(a.name, b.name))
- .map((value) => ({
- value: value.name,
- text: value.name,
- keyForList: value.name,
- isSelected: !!selectedCategories[value.name],
- pendingAction: value.pendingAction,
- rightElement: (
-
-
- {value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')}
-
-
-
+ .map((value) => {
+ const isDisabled = value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.values(value.pendingFields ?? {}).length > 0;
+ return {
+ text: value.name,
+ keyForList: value.name,
+ isSelected: !!selectedCategories[value.name],
+ isDisabled,
+ pendingAction: value.pendingAction,
+ errors: value.errors ?? undefined,
+ rightElement: (
+
+
+ {value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')}
+
+
+
+
-
- ),
- })),
- [policyCategories, selectedCategories, styles.alignSelfCenter, styles.flexRow, styles.label, styles.p1, styles.pl2, styles.textSupporting, theme.icon, translate],
+ ),
+ };
+ }),
+ [
+ policyCategories,
+ selectedCategories,
+ styles.alignSelfCenter,
+ styles.buttonOpacityDisabled,
+ styles.flexRow,
+ styles.label,
+ styles.p1,
+ styles.pl2,
+ styles.textSupporting,
+ theme.icon,
+ translate,
+ ],
);
- const toggleCategory = (category: PolicyForList) => {
- setSelectedCategories((prev) => ({
- ...prev,
- [category.value]: !prev[category.value],
- }));
+ const toggleCategory = (category: PolicyOption) => {
+ setSelectedCategories((prev) => {
+ if (prev[category.keyForList]) {
+ const {[category.keyForList]: omittedCategory, ...newCategories} = prev;
+ return newCategories;
+ }
+ return {...prev, [category.keyForList]: true};
+ });
};
const toggleAllCategories = () => {
- const isAllSelected = categoryList.every((category) => !!selectedCategories[category.value]);
- setSelectedCategories(isAllSelected ? {} : Object.fromEntries(categoryList.map((item) => [item.value, true])));
+ const availableCategories = categoryList.filter((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
+ const isAllSelected = availableCategories.length === Object.keys(selectedCategories).length;
+ setSelectedCategories(isAllSelected ? {} : Object.fromEntries(availableCategories.map((item) => [item.keyForList, true])));
};
const getCustomListHeader = () => (
@@ -113,42 +136,122 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
);
- const navigateToCategoriesSettings = () => {
- Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES_SETTINGS.getRoute(route.params.policyID));
+ const navigateToCategorySettings = (category: PolicyOption) => {
+ Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, category.keyForList));
};
- const navigateToCategorySettings = (category: PolicyForList) => {
- Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, category.text));
+ const navigateToCategoriesSettings = () => {
+ Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES_SETTINGS.getRoute(route.params.policyID));
};
const navigateToCreateCategoryPage = () => {
Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_CREATE.getRoute(route.params.policyID));
};
- const isLoading = !isOffline && policyCategories === undefined;
+ const dismissError = (item: PolicyOption) => {
+ Policy.clearCategoryErrors(route.params.policyID, item.keyForList);
+ };
+
+ const selectedCategoriesArray = Object.keys(selectedCategories).filter((key) => selectedCategories[key]);
+
+ const getHeaderButtons = () => {
+ const options: Array>> = [];
+
+ if (selectedCategoriesArray.length > 0) {
+ options.push({
+ icon: Expensicons.Trashcan,
+ text: translate('workspace.categories.deleteCategories'),
+ value: CONST.POLICY.CATEGORIES_BULK_ACTION_TYPES.DELETE,
+ onSelected: () => {
+ setSelectedCategories({});
+ deleteWorkspaceCategories(route.params.policyID, selectedCategoriesArray);
+ },
+ });
+
+ const enabledCategories = selectedCategoriesArray.filter((categoryName) => policyCategories?.[categoryName].enabled);
+ if (enabledCategories.length > 0) {
+ const categoriesToDisable = selectedCategoriesArray
+ .filter((categoryName) => policyCategories?.[categoryName].enabled)
+ .reduce>((acc, categoryName) => {
+ acc[categoryName] = {
+ name: categoryName,
+ enabled: false,
+ };
+ return acc;
+ }, {});
- const headerButtons = (
-
- {!PolicyUtils.hasAccountingConnections(policy) && (
+ options.push({
+ icon: Expensicons.DocumentSlash,
+ text: translate('workspace.categories.disableCategories'),
+ value: CONST.POLICY.CATEGORIES_BULK_ACTION_TYPES.DISABLE,
+ onSelected: () => {
+ setSelectedCategories({});
+ setWorkspaceCategoryEnabled(route.params.policyID, categoriesToDisable);
+ },
+ });
+ }
+
+ const disabledCategories = selectedCategoriesArray.filter((categoryName) => !policyCategories?.[categoryName].enabled);
+ if (disabledCategories.length > 0) {
+ const categoriesToEnable = selectedCategoriesArray
+ .filter((categoryName) => !policyCategories?.[categoryName].enabled)
+ .reduce>((acc, categoryName) => {
+ acc[categoryName] = {
+ name: categoryName,
+ enabled: true,
+ };
+ return acc;
+ }, {});
+ options.push({
+ icon: Expensicons.Document,
+ text: translate('workspace.categories.enableCategories'),
+ value: CONST.POLICY.CATEGORIES_BULK_ACTION_TYPES.ENABLE,
+ onSelected: () => {
+ setSelectedCategories({});
+ setWorkspaceCategoryEnabled(route.params.policyID, categoriesToEnable);
+ },
+ });
+ }
+
+ return (
+ null}
+ shouldAlwaysShowDropdownMenu
+ pressOnEnter
+ buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
+ customText={translate('workspace.common.selected', {selectedNumber: selectedCategoriesArray.length})}
+ options={options}
+ style={[isSmallScreenWidth && styles.w50, isSmallScreenWidth && styles.mb3]}
+ />
+ );
+ }
+
+ return (
+
+ {!PolicyUtils.hasAccountingConnections(policy) && (
+
+ )}
- )}
-
-
- );
+
+ );
+ };
+
+ const isLoading = !isOffline && policyCategories === undefined;
return (
@@ -164,9 +267,9 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
title={translate('workspace.common.categories')}
shouldShowBackButton={isSmallScreenWidth}
>
- {!isSmallScreenWidth && headerButtons}
+ {!isSmallScreenWidth && getHeaderButtons()}
- {isSmallScreenWidth && {headerButtons}}
+ {isSmallScreenWidth && {getHeaderButtons()}}
{translate('workspace.categories.subtitle')}
@@ -193,6 +296,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
onSelectAll={toggleAllCategories}
showScrollIndicator
ListItem={TableListItem}
+ onDismissError={dismissError}
customListHeader={getCustomListHeader()}
listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
/>