diff --git a/package-lock.json b/package-lock.json index 2a7e349d25a3..dfceedd823d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27979,22 +27979,6 @@ "jest": "bin/jest.js" } }, - "node_modules/jest-expo/node_modules/@babel/code-frame": { - "version": "7.10.4", - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, - "node_modules/jest-expo/node_modules/@expo/json-file": { - "version": "8.3.1", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "~7.10.4", - "json5": "^2.2.2", - "write-file-atomic": "^2.3.0" - } - }, "node_modules/jest-expo/node_modules/json5": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", diff --git a/src/CONST.ts b/src/CONST.ts index 30c233e6847b..a83e21d3ea32 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2162,7 +2162,15 @@ const CONST = { // Often referred to as "collect" workspaces TEAM: 'team', }, - FIELD_LIST_TITLE_FIELD_ID: 'text_title', + RULE_CONDITIONS: { + MATCHES: 'matches', + }, + FIELDS: { + TAG: 'tag', + CATEGORY: 'category', + FIELD_LIST_TITLE: 'text_title', + TAX: 'tax', + }, DEFAULT_REPORT_NAME_PATTERN: '{report:type} {report:startdate}', ROLE: { ADMIN: 'admin', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 04cc8a125fbb..7c662c31b0de 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -854,6 +854,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName', getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}` as const, }, + WORKSPACE_TAG_APPROVER: { + route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/approver', + getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${tagName}/approver` as const, + }, WORKSPACE_TAG_LIST_VIEW: { route: 'settings/workspaces/:policyID/tag-list/:orderWeight', getRoute: (policyID: string, orderWeight: number) => `settings/workspaces/${policyID}/tag-list/${orderWeight}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2369e231f519..75eee24e5065 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -434,6 +434,7 @@ const SCREENS = { TAX_CREATE: 'Workspace_Tax_Create', TAG_CREATE: 'Tag_Create', TAG_SETTINGS: 'Tag_Settings', + TAG_APPROVER: 'Tag_Approver', TAG_LIST_VIEW: 'Tag_List_View', TAG_GL_CODE: 'Tag_GL_Code', CURRENCY: 'Workspace_Profile_Currency', diff --git a/src/languages/en.ts b/src/languages/en.ts index 5aecf780eb63..d2921df4fda9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3184,6 +3184,8 @@ export default { importedFromAccountingSoftware: 'The tags below are imported from your', glCode: 'GL code', updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.', + tagRules: 'Tag rules', + approverDescription: 'Approver', importTags: 'Import tags', importedTagsMessage: (columnCounts: number) => `We found *${columnCounts} columns* in your spreadsheet. Select *Name* next to the column that contains tags names. You can also select *Enabled* next to the column that sets tags status.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 5229beb16eca..9446753977f6 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3233,6 +3233,8 @@ export default { importedFromAccountingSoftware: 'Etiquetas importadas desde', glCode: 'Código de Libro Mayor', updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código de Libro Mayor. Por favor, inténtelo nuevamente.', + tagRules: 'Reglas de etiquetas', + approverDescription: 'Aprobador', importTags: 'Importar categorías', importedTagsMessage: (columnCounts: number) => `Hemos encontrado *${columnCounts} columnas* en su hoja de cálculo. Seleccione *Nombre* junto a la columna que contiene los nombres de las etiquetas. También puede seleccionar *Habilitado* junto a la columna que establece el estado de la etiqueta.`, diff --git a/src/libs/API/parameters/SetPolicyTagApproverParams.ts b/src/libs/API/parameters/SetPolicyTagApproverParams.ts new file mode 100644 index 000000000000..48112b323fad --- /dev/null +++ b/src/libs/API/parameters/SetPolicyTagApproverParams.ts @@ -0,0 +1,7 @@ +type SetPolicyTagApproverParams = { + policyID: string; + tagName: string; + approver: string | null; +}; + +export default SetPolicyTagApproverParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 97a5ddadf7c9..3e4251628394 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -305,6 +305,7 @@ export type {default as EnablePolicyCompanyCardsParams} from './EnablePolicyComp export type {default as ToggleCardContinuousReconciliationParams} from './ToggleCardContinuousReconciliationParams'; export type {default as CardDeactivateParams} from './CardDeactivateParams'; export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams'; +export type {default as SetPolicyTagApproverParams} from './SetPolicyTagApproverParams'; export type {default as SaveSearchParams} from './SaveSearch'; export type {default as DeleteSavedSearchParams} from './DeleteSavedSearch'; export type {default as SetPolicyCategoryReceiptsRequiredParams} from './SetPolicyCategoryReceiptsRequiredParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 2fa068c8a753..580a8104755f 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -375,6 +375,7 @@ const WRITE_COMMANDS = { CREATE_ADMIN_ISSUED_VIRTUAL_CARD: 'CreateAdminIssuedVirtualCard', ADD_DELEGATE: 'AddDelegate', TOGGLE_CARD_CONTINUOUS_RECONCILIATION: 'ToggleCardContinuousReconciliation', + SET_POLICY_TAG_APPROVER: 'SetPolicyTagApprover', SAVE_SEARCH: 'SaveSearch', DELETE_SAVED_SEARCH: 'DeleteSavedSearch', UPDATE_CARD_SETTLEMENT_FREQUENCY: 'UpdateCardSettlementFrequency', @@ -634,6 +635,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; [WRITE_COMMANDS.SET_POLICY_TAXES_CURRENCY_DEFAULT]: Parameters.SetPolicyCurrencyDefaultParams; [WRITE_COMMANDS.SET_POLICY_CUSTOM_TAX_NAME]: Parameters.SetPolicyCustomTaxNameParams; + [WRITE_COMMANDS.SET_POLICY_TAG_APPROVER]: Parameters.SetPolicyTagApproverParams; [WRITE_COMMANDS.SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT]: Parameters.SetPolicyForeignCurrencyDefaultParams; [WRITE_COMMANDS.CREATE_POLICY_TAX]: Parameters.CreatePolicyTaxParams; [WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED]: Parameters.SetPolicyTaxesEnabledParams; diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts index 7f971f37d3fa..7b2f71dbd101 100644 --- a/src/libs/CategoryUtils.ts +++ b/src/libs/CategoryUtils.ts @@ -44,8 +44,11 @@ function formatRequireReceiptsOverText(translate: LocaleContextProps['translate' ); } -function getCategoryApprover(approvalRules: ApprovalRule[], categoryName: string) { - return approvalRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.approver; +function getCategoryApproverRule(approvalRules: ApprovalRule[], categoryName: string) { + const approverRule = approvalRules?.find((rule) => + rule.applyWhen.find(({condition, field, value}) => condition === CONST.POLICY.RULE_CONDITIONS.MATCHES && field === CONST.POLICY.FIELDS.CATEGORY && value === categoryName), + ); + return approverRule; } function getCategoryDefaultTaxRate(expenseRules: ExpenseRule[], categoryName: string, defaultTaxRate?: string) { @@ -59,4 +62,4 @@ function getCategoryDefaultTaxRate(expenseRules: ExpenseRule[], categoryName: st return categoryDefaultTaxRate; } -export {formatDefaultTaxRateText, formatRequireReceiptsOverText, getCategoryApprover, getCategoryDefaultTaxRate}; +export {formatDefaultTaxRateText, formatRequireReceiptsOverText, getCategoryApproverRule, getCategoryDefaultTaxRate}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index b4b676bbdec5..e2912eb409f3 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -269,6 +269,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/tags/WorkspaceEditTagsPage').default, [SCREENS.WORKSPACE.TAG_CREATE]: () => require('../../../../pages/workspace/tags/WorkspaceCreateTagPage').default, [SCREENS.WORKSPACE.TAG_EDIT]: () => require('../../../../pages/workspace/tags/EditTagPage').default, + [SCREENS.WORKSPACE.TAG_APPROVER]: () => require('../../../../pages/workspace/tags/TagApproverPage').default, [SCREENS.WORKSPACE.TAG_GL_CODE]: () => require('../../../../pages/workspace/tags/TagGLCodePage').default, [SCREENS.WORKSPACE.TAXES_SETTINGS]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesSettingsPage').default, [SCREENS.WORKSPACE.TAXES_SETTINGS_CUSTOM_TAX_NAME]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index bc86a2d77388..2daca22cc81c 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -133,6 +133,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.TAG_EDIT, SCREENS.WORKSPACE.TAG_LIST_VIEW, SCREENS.WORKSPACE.TAG_GL_CODE, + SCREENS.WORKSPACE.TAG_APPROVER, SCREENS.WORKSPACE.TAGS_IMPORT, SCREENS.WORKSPACE.TAGS_IMPORTED, ], diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index d2e6191349bd..b5d60b68f3bb 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -715,6 +715,13 @@ const config: LinkingOptions['config'] = { tagName: (tagName: string) => decodeURIComponent(tagName), }, }, + [SCREENS.WORKSPACE.TAG_APPROVER]: { + path: ROUTES.WORKSPACE_TAG_APPROVER.route, + parse: { + orderWeight: Number, + tagName: (tagName: string) => decodeURIComponent(tagName), + }, + }, [SCREENS.WORKSPACE.TAG_GL_CODE]: { path: ROUTES.WORKSPACE_TAG_GL_CODE.route, parse: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 9f76814740d3..47bd9f45c77c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -300,6 +300,11 @@ type SettingsNavigatorParamList = { orderWeight: number; tagName: string; }; + [SCREENS.WORKSPACE.TAG_APPROVER]: { + policyID: string; + orderWeight: number; + tagName: string; + }; [SCREENS.WORKSPACE.TAG_GL_CODE]: { policyID: string; orderWeight: number; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 63db52ff303f..2e52760b698e 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -988,6 +988,17 @@ function getWorkspaceAccountID(policyID: string) { return policy.workspaceAccountID ?? 0; } +function getTagApproverRule(policyID: string, tagName: string) { + const policy = getPolicy(policyID); + + const approvalRules = policy?.rules?.approvalRules ?? []; + const approverRule = approvalRules.find((rule) => + rule.applyWhen.find(({condition, field, value}) => condition === CONST.POLICY.RULE_CONDITIONS.MATCHES && field === CONST.POLICY.FIELDS.TAG && value === tagName), + ); + + return approverRule; +} + function getDomainNameForPolicy(policyID?: string): string { if (!policyID) { return ''; @@ -1107,6 +1118,7 @@ export { getWorkspaceAccountID, getAllTaxRatesNamesAndKeys as getAllTaxRates, getTagNamesFromTagsLists, + getTagApproverRule, getDomainNameForPolicy, hasUnsupportedIntegration, getWorkflowApprovalsUnavailable, diff --git a/src/libs/ReportActionsConnection.ts b/src/libs/ReportActionsConnection.ts new file mode 100644 index 000000000000..e3c8a4c3cf60 --- /dev/null +++ b/src/libs/ReportActionsConnection.ts @@ -0,0 +1,25 @@ +import type {OnyxCollection} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportActions} from '@src/types/onyx/ReportAction'; + +let allReportActions: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + if (!actions) { + return; + } + + allReportActions = actions; + }, +}); + +// This function is used to get all reports +function getAllReportActions() { + return allReportActions; +} + +// eslint-disable-next-line import/prefer-default-export +export {getAllReportActions}; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 2e11143ca00d..f561dd9293cc 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -18,6 +18,7 @@ import type { } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ApiUtils from '@libs/ApiUtils'; +import * as CategoryUtils from '@libs/CategoryUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; @@ -27,6 +28,7 @@ import Log from '@libs/Log'; import enhanceParameters from '@libs/Network/enhanceParameters'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import {navigateWhenEnableFeature, removePendingFieldsFromCustomUnit} from '@libs/PolicyUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -519,8 +521,28 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[]) } function renamePolicyCategory(policyID: string, policyCategory: {oldName: string; newName: string}) { + const policy = PolicyUtils.getPolicy(policyID); const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[policyCategory.oldName]; + const policyCategoryRule = CategoryUtils.getCategoryApproverRule(policy?.rules?.approvalRules ?? [], policyCategory.oldName); + const approvalRules = policy?.rules?.approvalRules ?? []; + const updatedApprovalRules: ApprovalRule[] = lodashCloneDeep(approvalRules); + + // Its related by name, so the corresponding rule has to be updated to handle offline scenario + if (policyCategoryRule) { + const indexToUpdate = updatedApprovalRules.findIndex((rule) => rule.id === policyCategoryRule.id); + policyCategoryRule.applyWhen = policyCategoryRule.applyWhen.map((ruleCondition) => { + const {value, field, condition} = ruleCondition; + + if (value === policyCategory.oldName && field === CONST.POLICY.FIELDS.CATEGORY && condition === CONST.POLICY.RULE_CONDITIONS.MATCHES) { + return {...ruleCondition, value: policyCategory.newName}; + } + + return ruleCondition; + }); + updatedApprovalRules[indexToUpdate] = policyCategoryRule; + } + const onyxData: OnyxData = { optimisticData: [ { @@ -540,6 +562,15 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + approvalRules: updatedApprovalRules, + }, + }, + }, ], successData: [ { @@ -574,6 +605,15 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + approvalRules, + }, + }, + }, ], }; @@ -1152,7 +1192,8 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const approvalRules = policy?.rules?.approvalRules ?? []; let updatedApprovalRules: ApprovalRule[] = lodashCloneDeep(approvalRules); - const existingCategoryApproverRule = updatedApprovalRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); + const existingCategoryApproverRule = CategoryUtils.getCategoryApproverRule(updatedApprovalRules, categoryName); + let newApprover = approver; if (!existingCategoryApproverRule) { @@ -1160,7 +1201,7 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro approver, applyWhen: [ { - condition: 'matches', + condition: CONST.POLICY.RULE_CONDITIONS.MATCHES, field: 'category', value: categoryName, }, @@ -1183,27 +1224,10 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro rules: { approvalRules: updatedApprovalRules, }, - pendingRulesUpdates: { - [categoryName]: { - approvalRule: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - }, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - pendingRulesUpdates: { - [categoryName]: { - approvalRule: null, - }, - }, }, }, ], + failureData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -1212,11 +1236,6 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro rules: { approvalRules, }, - pendingRulesUpdates: { - [categoryName]: { - approvalRule: null, - }, - }, }, }, ], @@ -1232,7 +1251,7 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro } function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: string) { - const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + const policy = PolicyUtils.getPolicy(policyID); const expenseRules = policy?.rules?.expenseRules ?? []; const updatedExpenseRules: ExpenseRule[] = lodashCloneDeep(expenseRules); const existingCategoryExpenseRule = updatedExpenseRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); @@ -1247,7 +1266,7 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str }, applyWhen: [ { - condition: 'matches', + condition: CONST.POLICY.RULE_CONDITIONS.MATCHES, field: 'category', value: categoryName, }, @@ -1267,24 +1286,6 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str rules: { expenseRules: updatedExpenseRules, }, - pendingRulesUpdates: { - [categoryName]: { - expenseRule: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - }, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - pendingRulesUpdates: { - [categoryName]: { - expenseRule: null, - }, - }, }, }, ], @@ -1296,11 +1297,6 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str rules: { expenseRules, }, - pendingRulesUpdates: { - [categoryName]: { - expenseRule: null, - }, - }, }, }, ], diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index d73e2b8c4166..50db00cc64df 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -71,6 +71,7 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; import {navigateWhenEnableFeature} from '@libs/PolicyUtils'; +import * as ReportActionsConnection from '@libs/ReportActionsConnection'; import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -92,7 +93,6 @@ import type { } from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {Attributes, CompanyAddress, CustomUnit, NetSuiteCustomList, NetSuiteCustomSegment, Rate, TaxRate, Unit} from '@src/types/onyx/Policy'; -import type {ReportActions} from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {buildOptimisticPolicyCategories} from './Category'; @@ -165,19 +165,6 @@ Onyx.connect({ }, }); -let allReportActions: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (actions) => { - if (!actions) { - return; - } - - allReportActions = actions; - }, -}); - let lastAccessedWorkspacePolicyID: OnyxEntry; Onyx.connect({ key: ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, @@ -2632,10 +2619,9 @@ function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): WorkspaceF }); // We need to move the report preview action from the DM to the workspace chat. - const reportPreview = - iouReport?.parentReportID && iouReport.parentReportActionID - ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.parentReportID}`]?.[iouReport.parentReportActionID] - : undefined; + const parentReport = ReportActionsConnection.getAllReportActions()?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.parentReportID}`]; + const parentReportActionID = iouReport.parentReportActionID; + const reportPreview = iouReport?.parentReportID && parentReportActionID ? parentReport?.[parentReportActionID] : undefined; optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -3963,8 +3949,8 @@ function enablePolicyDefaultReportTitle(policyID: string, enabled: boolean) { return; } - const previousReportTitleField = policy?.fieldList?.[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID] ?? {}; - const titleFieldValues = enabled ? {} : {fieldList: {[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: {...previousReportTitleField, defaultValue: CONST.POLICY.DEFAULT_REPORT_NAME_PATTERN}}}; + const previousReportTitleField = policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE] ?? {}; + const titleFieldValues = enabled ? {} : {fieldList: {[CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: {...previousReportTitleField, defaultValue: CONST.POLICY.DEFAULT_REPORT_NAME_PATTERN}}}; const optimisticData: OnyxUpdate[] = [ { @@ -4000,7 +3986,7 @@ function enablePolicyDefaultReportTitle(policyID: string, enabled: boolean) { value: { shouldShowCustomReportTitleOption: !!policy?.shouldShowCustomReportTitleOption, fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: previousReportTitleField, + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: previousReportTitleField, }, pendingFields: { shouldShowCustomReportTitleOption: null, @@ -4032,11 +4018,11 @@ function enablePolicyDefaultReportTitle(policyID: string, enabled: boolean) { function setPolicyDefaultReportTitle(policyID: string, customName: string) { const policy = getPolicy(policyID); - if (customName === policy?.fieldList?.[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]?.defaultValue) { + if (customName === policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE]?.defaultValue) { return; } - const previousReportTitleField = policy?.fieldList?.[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID] ?? {}; + const previousReportTitleField = policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE] ?? {}; const optimisticData: OnyxUpdate[] = [ { @@ -4044,7 +4030,7 @@ function setPolicyDefaultReportTitle(policyID: string, customName: string) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: { + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: { defaultValue: customName, pendingFields: {defaultValue: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, }, @@ -4059,7 +4045,7 @@ function setPolicyDefaultReportTitle(policyID: string, customName: string) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: {pendingFields: {defaultValue: null}}, + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: {pendingFields: {defaultValue: null}}, }, errorFields: null, }, @@ -4072,7 +4058,7 @@ function setPolicyDefaultReportTitle(policyID: string, customName: string) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: {...previousReportTitleField, pendingFields: {defaultValue: null}}, + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: {...previousReportTitleField, pendingFields: {defaultValue: null}}, }, errorFields: { fieldList: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), @@ -4101,11 +4087,11 @@ function setPolicyDefaultReportTitle(policyID: string, customName: string) { function setPolicyPreventMemberCreatedTitle(policyID: string, enforced: boolean) { const policy = getPolicy(policyID); - if (!enforced === policy?.fieldList?.[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID].deletable) { + if (!enforced === policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE].deletable) { return; } - const previousReportTitleField = policy?.fieldList?.[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID] ?? {}; + const previousReportTitleField = policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE] ?? {}; const optimisticData: OnyxUpdate[] = [ { @@ -4113,7 +4099,7 @@ function setPolicyPreventMemberCreatedTitle(policyID: string, enforced: boolean) key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: {...previousReportTitleField, deletable: !enforced, pendingFields: {deletable: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}}, + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: {...previousReportTitleField, deletable: !enforced, pendingFields: {deletable: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}}, }, }, }, @@ -4125,7 +4111,7 @@ function setPolicyPreventMemberCreatedTitle(policyID: string, enforced: boolean) key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: {pendingFields: {deletable: null}}, + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: {pendingFields: {deletable: null}}, }, errorFields: null, }, @@ -4138,7 +4124,7 @@ function setPolicyPreventMemberCreatedTitle(policyID: string, enforced: boolean) key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: {...previousReportTitleField, pendingFields: {deletable: null}}, + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: {...previousReportTitleField, pendingFields: {deletable: null}}, }, errorFields: { fieldList: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 6455e7fad947..88430b941838 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -1,3 +1,4 @@ +import lodashCloneDeep from 'lodash/cloneDeep'; import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; @@ -6,6 +7,7 @@ import type { OpenPolicyTagsPageParams, RenamePolicyTaglistParams, RenamePolicyTagsParams, + SetPolicyTagApproverParams, SetPolicyTagsEnabled, SetPolicyTagsRequired, UpdatePolicyTagGLCodeParams, @@ -27,7 +29,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyTag, PolicyTagLists, PolicyTags, RecentlyUsedTags, Report} from '@src/types/onyx'; import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; -import type {Attributes, Rate} from '@src/types/onyx/Policy'; +import type {ApprovalRule, Attributes, Rate} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; type NewCustomUnit = { @@ -453,10 +455,31 @@ function clearPolicyTagListErrors(policyID: string, tagListIndex: number) { } function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: string}, tagListIndex: number) { + const policy = PolicyUtils.getPolicy(policyID); const tagList = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[tagListIndex] ?? {}; const tag = tagList.tags?.[policyTag.oldName]; const oldTagName = policyTag.oldName; const newTagName = PolicyUtils.escapeTagName(policyTag.newName); + + const policyTagRule = PolicyUtils.getTagApproverRule(policyID, oldTagName); + const approvalRules = policy?.rules?.approvalRules ?? []; + const updatedApprovalRules: ApprovalRule[] = lodashCloneDeep(approvalRules); + + // Its related by name, so the corresponding rule has to be updated to handle offline scenario + if (policyTagRule) { + const indexToUpdate = updatedApprovalRules.findIndex((rule) => rule.id === policyTagRule.id); + policyTagRule.applyWhen = policyTagRule.applyWhen.map((ruleCondition) => { + const {value, field, condition} = ruleCondition; + + if (value === policyTag.oldName && field === CONST.POLICY.FIELDS.TAG && condition === CONST.POLICY.RULE_CONDITIONS.MATCHES) { + return {...ruleCondition, value: policyTag.newName}; + } + + return ruleCondition; + }); + updatedApprovalRules[indexToUpdate] = policyTagRule; + } + const onyxData: OnyxData = { optimisticData: [ { @@ -481,6 +504,15 @@ function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + approvalRules: updatedApprovalRules, + }, + }, + }, ], successData: [ { @@ -890,6 +922,74 @@ function setPolicyTagGLCode(policyID: string, tagName: string, tagListIndex: num API.write(WRITE_COMMANDS.UPDATE_POLICY_TAG_GL_CODE, parameters, onyxData); } +function setPolicyTagApprover(policyID: string, tag: string, approver: string) { + const policy = PolicyUtils.getPolicy(policyID); + const prevApprovalRules = policy?.rules?.approvalRules ?? []; + const approverRuleToUpdate = PolicyUtils.getTagApproverRule(policyID, tag); + const filteredApprovalRules = approverRuleToUpdate ? prevApprovalRules.filter((rule) => rule.id !== approverRuleToUpdate.id) : prevApprovalRules; + const toBeUnselected = approverRuleToUpdate?.approver === approver; + + const updatedApproverRule = approverRuleToUpdate + ? {...approverRuleToUpdate, approver} + : { + applyWhen: [ + { + condition: CONST.POLICY.RULE_CONDITIONS.MATCHES, + field: CONST.POLICY.FIELDS.TAG, + value: tag, + }, + ], + approver, + id: '-1', + }; + + const updatedApprovalRules = toBeUnselected ? filteredApprovalRules : [...filteredApprovalRules, updatedApproverRule]; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + approvalRules: updatedApprovalRules, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + approvalRules: updatedApprovalRules, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + approvalRules: prevApprovalRules, + }, + }, + }, + ], + }; + + const parameters: SetPolicyTagApproverParams = { + policyID, + tagName: tag, + approver: toBeUnselected ? null : approver, + }; + + API.write(WRITE_COMMANDS.SET_POLICY_TAG_APPROVER, parameters, onyxData); +} + function downloadTagsCSV(policyID: string) { const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_TAGS_CSV, { policyID, @@ -919,6 +1019,7 @@ export { renamePolicyTaglist, setWorkspaceTagEnabled, setPolicyTagGLCode, + setPolicyTagApprover, importPolicyTags, downloadTagsCSV, }; diff --git a/src/pages/workspace/categories/CategoryApproverPage.tsx b/src/pages/workspace/categories/CategoryApproverPage.tsx index 390a577d9cf8..649681db6155 100644 --- a/src/pages/workspace/categories/CategoryApproverPage.tsx +++ b/src/pages/workspace/categories/CategoryApproverPage.tsx @@ -26,7 +26,7 @@ function CategoryApproverPage({ const {translate} = useLocalize(); const policy = usePolicy(policyID); - const selectedApprover = CategoryUtils.getCategoryApprover(policy?.rules?.approvalRules ?? [], categoryName) ?? ''; + const selectedApprover = CategoryUtils.getCategoryApproverRule(policy?.rules?.approvalRules ?? [], categoryName)?.approver ?? ''; return ( ; -}; - -type CategorySettingsPageProps = CategorySettingsPageOnyxProps & StackScreenProps; +type CategorySettingsPageProps = StackScreenProps; function CategorySettingsPage({ route: { params: {backTo, policyID, categoryName}, }, - policyCategories, navigation, }: CategorySettingsPageProps) { + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); const styles = useThemeStyles(); const {translate} = useLocalize(); const [deleteCategoryConfirmModalVisible, setDeleteCategoryConfirmModalVisible] = useState(false); @@ -83,7 +77,7 @@ function CategorySettingsPage({ }, [policyCategory?.maxExpenseAmount, policyCategoryExpenseLimitType, policyCurrency, translate]); const approverText = useMemo(() => { - const categoryApprover = CategoryUtils.getCategoryApprover(policy?.rules?.approvalRules ?? [], categoryName); + const categoryApprover = CategoryUtils.getCategoryApproverRule(policy?.rules?.approvalRules ?? [], categoryName)?.approver; return categoryApprover ?? ''; }, [categoryName, policy?.rules?.approvalRules]); @@ -133,6 +127,8 @@ function CategorySettingsPage({ }; const isThereAnyAccountingConnection = Object.keys(policy?.connections ?? {}).length !== 0; + const workflowApprovalsUnavailable = PolicyUtils.getWorkflowApprovalsUnavailable(policy); + const approverDisabled = !policy?.areWorkflowsEnabled || workflowApprovalsUnavailable; return ( )} - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - disabled={!policy?.areWorkflowsEnabled} - /> - - {!policy?.areWorkflowsEnabled && ( + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + disabled={approverDisabled} + /> + {approverDisabled && ( {translate('workspace.rules.categoryRules.goTo')}{' '} )} {policy?.tax?.trackingEnabled && ( - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> )} @@ -333,8 +325,4 @@ function CategorySettingsPage({ CategorySettingsPage.displayName = 'CategorySettingsPage'; -export default withOnyx({ - policyCategories: { - key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params.policyID}`, - }, -})(CategorySettingsPage); +export default CategorySettingsPage; diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index a950567e903e..87bf62cbf7b3 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -63,15 +63,15 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { subMenuItems: [ Navigation.navigate(ROUTES.RULES_CUSTOM_NAME.getRoute(policyID))} @@ -79,8 +79,8 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { , PolicyActions.setPolicyPreventMemberCreatedTitle(policyID, isEnabled)} />, ], diff --git a/src/pages/workspace/rules/RulesCustomNamePage.tsx b/src/pages/workspace/rules/RulesCustomNamePage.tsx index c32615415755..4a142c01e1da 100644 --- a/src/pages/workspace/rules/RulesCustomNamePage.tsx +++ b/src/pages/workspace/rules/RulesCustomNamePage.tsx @@ -40,7 +40,7 @@ function RulesCustomNamePage({route}: RulesCustomNamePageProps) { translate('workspace.rules.expenseReportRules.customNameTotalExample'), ] as const satisfies string[]; - const customNameDefaultValue = policy?.fieldList?.[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID].defaultValue; + const customNameDefaultValue = policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE].defaultValue; const validateCustomName = ({customName}: FormOnyxValues) => { const errors: FormInputErrors = {}; diff --git a/src/pages/workspace/tags/TagApproverPage.tsx b/src/pages/workspace/tags/TagApproverPage.tsx new file mode 100644 index 000000000000..874754b2cf4b --- /dev/null +++ b/src/pages/workspace/tags/TagApproverPage.tsx @@ -0,0 +1,57 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import WorkspaceMembersSelectionList from '@components/WorkspaceMembersSelectionList'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as Tag from '@userActions/Policy/Tag'; +import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; + +type TagApproverPageProps = StackScreenProps; + +function TagApproverPage({route}: TagApproverPageProps) { + const {policyID, tagName} = route.params; + + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const tagApprover = PolicyUtils.getTagApproverRule(policyID, tagName)?.approver; + + return ( + + + Navigation.goBack()} + /> + { + Tag.setPolicyTagApprover(policyID, tagName, email); + Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack); + }} + /> + + + ); +} + +TagApproverPage.displayName = 'TagApproverPage'; + +export default TagApproverPage; diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index a0f98ef699ed..6b70b6f636fe 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -1,8 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -12,6 +11,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import Switch from '@components/Switch'; import Text from '@components/Text'; +import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -27,73 +27,72 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {PolicyTagLists} from '@src/types/onyx'; -type TagSettingsPageOnyxProps = { - /** All policy tags */ - policyTags: OnyxEntry; -}; +type TagSettingsPageProps = StackScreenProps; -type TagSettingsPageProps = TagSettingsPageOnyxProps & StackScreenProps; - -function TagSettingsPage({route, policyTags, navigation}: TagSettingsPageProps) { +function TagSettingsPage({route, navigation}: TagSettingsPageProps) { + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${route.params.policyID}`); + const {orderWeight, policyID, tagName} = route.params; const styles = useThemeStyles(); const {translate} = useLocalize(); - const policyTag = useMemo(() => PolicyUtils.getTagList(policyTags, route.params.orderWeight), [policyTags, route.params.orderWeight]); - const policy = usePolicy(route.params.policyID); + const policyTag = useMemo(() => PolicyUtils.getTagList(policyTags, orderWeight), [policyTags, orderWeight]); + const policy = usePolicy(policyID); const [isDeleteTagModalOpen, setIsDeleteTagModalOpen] = React.useState(false); - const currentPolicyTag = policyTag.tags[route.params.tagName] ?? Object.values(policyTag.tags ?? {}).find((tag) => tag.previousTagName === route.params.tagName); + const currentPolicyTag = policyTag.tags[tagName] ?? Object.values(policyTag.tags ?? {}).find((tag) => tag.previousTagName === tagName); useEffect(() => { - if (currentPolicyTag?.name === route.params.tagName || !currentPolicyTag) { + if (currentPolicyTag?.name === tagName || !currentPolicyTag) { return; } navigation.setParams({tagName: currentPolicyTag?.name}); - }, [route.params.tagName, currentPolicyTag, navigation]); + }, [tagName, currentPolicyTag, navigation]); if (!currentPolicyTag) { return ; } const deleteTagAndHideModal = () => { - Tag.deletePolicyTags(route.params.policyID, [currentPolicyTag.name]); + Tag.deletePolicyTags(policyID, [currentPolicyTag.name]); setIsDeleteTagModalOpen(false); Navigation.goBack(); }; const updateWorkspaceTagEnabled = (value: boolean) => { - setWorkspaceTagEnabled(route.params.policyID, {[currentPolicyTag.name]: {name: currentPolicyTag.name, enabled: value}}, policyTag.orderWeight); + setWorkspaceTagEnabled(policyID, {[currentPolicyTag.name]: {name: currentPolicyTag.name, enabled: value}}, policyTag.orderWeight); }; const navigateToEditTag = () => { - Navigation.navigate(ROUTES.WORKSPACE_TAG_EDIT.getRoute(route.params.policyID, route.params.orderWeight, currentPolicyTag.name)); + Navigation.navigate(ROUTES.WORKSPACE_TAG_EDIT.getRoute(policyID, orderWeight, currentPolicyTag.name)); }; const navigateToEditGlCode = () => { if (!PolicyUtils.isControlPolicy(policy)) { Navigation.navigate( - ROUTES.WORKSPACE_UPGRADE.getRoute( - route.params.policyID, - CONST.UPGRADE_FEATURE_INTRO_MAPPING.glCodes.alias, - ROUTES.WORKSPACE_TAG_GL_CODE.getRoute(policy?.id ?? '', route.params.orderWeight, route.params.tagName), - ), + ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.glCodes.alias, ROUTES.WORKSPACE_TAG_GL_CODE.getRoute(policy?.id ?? '', orderWeight, tagName)), ); return; } - Navigation.navigate(ROUTES.WORKSPACE_TAG_GL_CODE.getRoute(route.params.policyID, route.params.orderWeight, currentPolicyTag.name)); + Navigation.navigate(ROUTES.WORKSPACE_TAG_GL_CODE.getRoute(policyID, orderWeight, currentPolicyTag.name)); + }; + + const navigateToEditTagApprover = () => { + Navigation.navigate(ROUTES.WORKSPACE_TAG_APPROVER.getRoute(policyID, orderWeight, currentPolicyTag.name)); }; const isThereAnyAccountingConnection = Object.keys(policy?.connections ?? {}).length !== 0; const isMultiLevelTags = PolicyUtils.isMultiLevelTags(policyTags); + const tagApprover = PolicyUtils.getTagApproverRule(policyID, route.params.tagName)?.approver; const shouldShowDeleteMenuItem = !isThereAnyAccountingConnection && !isMultiLevelTags; + const workflowApprovalsUnavailable = PolicyUtils.getWorkflowApprovalsUnavailable(policy); + const approverDisabled = !policy?.areWorkflowsEnabled || workflowApprovalsUnavailable; return ( Tag.clearPolicyTagErrors(route.params.policyID, route.params.tagName, route.params.orderWeight)} + onClose={() => Tag.clearPolicyTagErrors(policyID, tagName, orderWeight)} > @@ -150,6 +149,34 @@ function TagSettingsPage({route, policyTags, navigation}: TagSettingsPageProps) shouldShowRightIcon /> + + {policy?.areRulesEnabled && ( + <> + + {translate('workspace.tags.tagRules')} + + + {approverDisabled && ( + + {translate('workspace.rules.categoryRules.goTo')}{' '} + Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID))} + > + {translate('workspace.common.moreFeatures')} + {' '} + {translate('workspace.rules.categoryRules.andEnableWorkflows')} + + )} + + )} + {shouldShowDeleteMenuItem && ( ({ - policyTags: { - key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${route.params.policyID}`, - }, -})(TagSettingsPage); +export default TagSettingsPage; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 95a9907526e4..1f1be7e89c20 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1373,10 +1373,10 @@ type PendingJoinRequestPolicy = { /** Data informing when a given rule should be applied */ type ApplyRulesWhen = { /** The condition for applying the rule to the workspace */ - condition: 'matches'; + condition: string; /** The target field to which the rule is applied */ - field: 'category'; + field: string; /** The value of the target field */ value: string; @@ -1412,9 +1412,6 @@ type ExpenseRule = { id?: string; }; -/** The name of the category or tag */ -type CategoryOrTagName = string; - /** Model of policy data */ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< { @@ -1686,18 +1683,6 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** Workspace account ID configured for Expensify Card */ workspaceAccountID?: number; - - /** Information about rules being updated */ - pendingRulesUpdates?: Record< - CategoryOrTagName, - { - /** Indicates whether the approval rule is updated for the given category or tag */ - approvalRule: OnyxCommon.PendingAction; - - /** Indicates whether the expense rule is updated for the given category or tag */ - expenseRule: OnyxCommon.PendingAction; - } - >; } & Partial, 'addWorkspaceRoom' | keyof ACHAccount | keyof Attributes >;