From e608638391f557facb96497e942266de89461daf Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 20 Sep 2023 23:32:53 +0200 Subject: [PATCH 1/9] Migrate policy utils lib --- src/libs/{PolicyUtils.js => PolicyUtils.ts} | 116 +++++++------------- src/types/onyx/Policy.ts | 1 + 2 files changed, 41 insertions(+), 76 deletions(-) rename src/libs/{PolicyUtils.js => PolicyUtils.ts} (50%) diff --git a/src/libs/PolicyUtils.js b/src/libs/PolicyUtils.ts similarity index 50% rename from src/libs/PolicyUtils.js rename to src/libs/PolicyUtils.ts index 164f284a4ef5..1f2abfa2b7a8 100644 --- a/src/libs/PolicyUtils.js +++ b/src/libs/PolicyUtils.ts @@ -1,72 +1,56 @@ -import _ from 'underscore'; -import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; +import * as OnyxTypes from '../types/onyx'; + +type PolicyMemberList = Record; +type PolicyMembersCollection = Record; +type MemberEmailsToAccountIDs = Record; +type PersonalDetailsList = Record; /** * Filter out the active policies, which will exclude policies with pending deletion - * @param {Object} policies - * @returns {Array} */ -function getActivePolicies(policies) { - return _.filter(policies, (policy) => policy && policy.isPolicyExpenseChatEnabled && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); +function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { + return policies.filter((policy) => policy?.isPolicyExpenseChatEnabled && policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); } /** * Checks if we have any errors stored within the POLICY_MEMBERS. Determines whether we should show a red brick road error or not. * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...} - * - * @param {Object} policyMembers - * @returns {Boolean} */ -function hasPolicyMemberError(policyMembers) { - return _.some(policyMembers, (member) => !_.isEmpty(member.errors)); +function hasPolicyMemberError(policyMembers: PolicyMemberList): boolean { + return Object.values(policyMembers).some((member) => Object.keys(member?.errors ?? {}).length > 0); } /** * Check if the policy has any error fields. - * - * @param {Object} policy - * @param {Object} policy.errorFields - * @return {Boolean} */ -function hasPolicyErrorFields(policy) { - return _.some(lodashGet(policy, 'errorFields', {}), (fieldErrors) => !_.isEmpty(fieldErrors)); +function hasPolicyErrorFields(policy: OnyxTypes.Policy): boolean { + return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors).length > 0); } /** * Check if the policy has any errors, and if it doesn't, then check if it has any error fields. - * - * @param {Object} policy - * @param {Object} policy.errors - * @param {Object} policy.errorFields - * @return {Boolean} */ -function hasPolicyError(policy) { - return !_.isEmpty(lodashGet(policy, 'errors', {})) ? true : hasPolicyErrorFields(policy); +function hasPolicyError(policy: OnyxTypes.Policy): boolean { + return Object.keys(policy?.errors ?? {}).length > 0 ? true : hasPolicyErrorFields(policy); } /** * Checks if we have any errors stored within the policy custom units. - * - * @param {Object} policy - * @returns {Boolean} */ -function hasCustomUnitsError(policy) { - return !_.isEmpty(_.pick(lodashGet(policy, 'customUnits', {}), 'errors')); +function hasCustomUnitsError(policy: OnyxTypes.Policy): boolean { + return Object.keys(policy?.customUnits?.error ?? {}).length > 0; } /** * Get the brick road indicator status for a policy. The policy has an error status if there is a policy member error, a custom unit error or a field error. - * - * @param {Object} policy - * @param {String} policy.id - * @param {Object} policyMembersCollection - * @returns {String} */ -function getPolicyBrickRoadIndicatorStatus(policy, policyMembersCollection) { - const policyMembers = lodashGet(policyMembersCollection, `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy.id}`, {}); +function getPolicyBrickRoadIndicatorStatus(policy: OnyxTypes.Policy, policyMembersCollection: PolicyMembersCollection): string { + if (!(`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}` in policyMembersCollection)) return ''; + + const policyMembers = policyMembersCollection[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}`]; if (hasPolicyMemberError(policyMembers) || hasCustomUnitsError(policy) || hasPolicyErrorFields(policy)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } @@ -79,84 +63,64 @@ function getPolicyBrickRoadIndicatorStatus(policy, policyMembersCollection) { * If online, show the policy pending deletion only if there is an error. * Note: Using a local ONYXKEYS.NETWORK subscription will cause a delay in * updating the screen. Passing the offline status from the component. - * @param {Object} policy - * @param {Boolean} isOffline - * @returns {Boolean} */ -function shouldShowPolicy(policy, isOffline) { +function shouldShowPolicy(policy: OnyxTypes.Policy, isOffline: boolean): boolean { return ( policy && - policy.isPolicyExpenseChatEnabled && - policy.role === CONST.POLICY.ROLE.ADMIN && - (isOffline || policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(policy.errors)) + policy?.isPolicyExpenseChatEnabled && + policy?.role === CONST.POLICY.ROLE.ADMIN && + (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy?.errors).length > 0) ); } -/** - * @param {string} email - * @returns {boolean} - */ -function isExpensifyTeam(email) { +function isExpensifyTeam(email: string): boolean { const emailDomain = Str.extractEmailDomain(email); return emailDomain === CONST.EXPENSIFY_PARTNER_NAME || emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } -/** - * @param {string} email - * @returns {boolean} - */ -function isExpensifyGuideTeam(email) { +function isExpensifyGuideTeam(email: string): boolean { const emailDomain = Str.extractEmailDomain(email); return emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } /** * Checks if the current user is an admin of the policy. - * - * @param {Object} policy - * @returns {Boolean} */ -const isPolicyAdmin = (policy) => lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; +const isPolicyAdmin = (policy: OnyxTypes.Policy): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; /** - * @param {Object} policyMembers - * @param {Object} personalDetails - * @returns {Object} - * * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. * * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ -function getMemberAccountIDsForWorkspace(policyMembers, personalDetails) { - const memberEmailsToAccountIDs = {}; - _.each(policyMembers, (member, accountID) => { - if (!_.isEmpty(member.errors)) { +function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): MemberEmailsToAccountIDs { + const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; + Object.keys(policyMembers).forEach((accountID) => { + const member = policyMembers[accountID]; + if (Object.keys(member?.errors ?? {}).length > 0) { return; } const personalDetail = personalDetails[accountID]; - if (!personalDetail || !personalDetail.login) { + if (!personalDetail?.login) { return; } - memberEmailsToAccountIDs[personalDetail.login] = accountID; + memberEmailsToAccountIDs[personalDetail?.login] = accountID; }); return memberEmailsToAccountIDs; } /** * Get login list that we should not show in the workspace invite options - * - * @param {Object} policyMembers - * @param {Object} personalDetails - * @returns {Array} */ -function getIneligibleInvitees(policyMembers, personalDetails) { - const memberEmailsToExclude = [...CONST.EXPENSIFY_EMAILS]; - _.each(policyMembers, (policyMember, accountID) => { +function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): string[] { + const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; + Object.keys(policyMembers).forEach((accountID) => { + const policyMember = policyMembers[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). - if (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(policyMember.errors)) { + if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { return; } - const memberEmail = lodashGet(personalDetails, `[${accountID}].login`); + const memberEmail = personalDetails[accountID]?.login; if (!memberEmail) { return; } diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index cacbb5d15199..10ee569c93e6 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -32,6 +32,7 @@ type Policy = { isFromFullPolicy?: boolean; lastModified?: string; customUnits?: Record; + isPolicyExpenseChatEnabled: boolean; }; export default Policy; From 8113ebf244d214dc2e9d9d02ebb8d93fb88377a1 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Fri, 29 Sep 2023 12:24:07 +0200 Subject: [PATCH 2/9] Type fixes --- src/libs/PolicyUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 1f2abfa2b7a8..5b4ed6fcd208 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -96,11 +96,11 @@ const isPolicyAdmin = (policy: OnyxTypes.Policy): boolean => policy?.role === CO function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): MemberEmailsToAccountIDs { const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; Object.keys(policyMembers).forEach((accountID) => { - const member = policyMembers[accountID]; + const member = policyMembers?.[accountID]; if (Object.keys(member?.errors ?? {}).length > 0) { return; } - const personalDetail = personalDetails[accountID]; + const personalDetail = personalDetails?.[accountID]; if (!personalDetail?.login) { return; } @@ -115,12 +115,12 @@ function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, person function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): string[] { const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; Object.keys(policyMembers).forEach((accountID) => { - const policyMember = policyMembers[accountID]; + const policyMember = policyMembers?.[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { return; } - const memberEmail = personalDetails[accountID]?.login; + const memberEmail = personalDetails?.[accountID]?.login; if (!memberEmail) { return; } From 837bd88068113e148404d989f4331e8bfef0d7ec Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 2 Oct 2023 10:58:11 +0200 Subject: [PATCH 3/9] Migrate PolicyUtils to TS --- src/libs/PolicyUtils.ts | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index a901384ef668..9b45318e67eb 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -14,7 +14,9 @@ type UnitRate = {rate: number}; * These are policies that we can use to create reports with in NewDot. */ function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { - return policies.filter((policy) => policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + return (policies ?? []).filter( + (policy) => policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + ); } /** @@ -22,14 +24,14 @@ function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...} */ function hasPolicyMemberError(policyMembers: PolicyMemberList): boolean { - return Object.values(policyMembers).some((member) => Object.keys(member?.errors ?? {}).length > 0); + return Object.values(policyMembers ?? {}).some((member) => Object.keys(member?.errors ?? {}).length > 0); } /** * Check if the policy has any error fields. */ function hasPolicyErrorFields(policy: OnyxTypes.Policy): boolean { - return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors).length > 0); + return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors ?? {}).length > 0); } /** @@ -87,19 +89,19 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxTypes.Policy, policyMembe function shouldShowPolicy(policy: OnyxTypes.Policy, isOffline: boolean): boolean { return ( policy && - policy.isPolicyExpenseChatEnabled && - policy.role === CONST.POLICY.ROLE.ADMIN && - (isOffline || policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors).length > 0) + policy?.isPolicyExpenseChatEnabled && + policy?.role === CONST.POLICY.ROLE.ADMIN && + (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) ); } function isExpensifyTeam(email: string): boolean { - const emailDomain = Str.extractEmailDomain(email); + const emailDomain = Str.extractEmailDomain(email ?? ''); return emailDomain === CONST.EXPENSIFY_PARTNER_NAME || emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } function isExpensifyGuideTeam(email: string): boolean { - const emailDomain = Str.extractEmailDomain(email); + const emailDomain = Str.extractEmailDomain(email ?? ''); return emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } @@ -115,9 +117,9 @@ const isPolicyAdmin = (policy: OnyxTypes.Policy): boolean => policy?.role === CO */ function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): MemberEmailsToAccountIDs { const memberEmailsToAccountIDs: Record = {}; - Object.keys(policyMembers).forEach((accountID) => { + Object.keys(policyMembers ?? {}).forEach((accountID) => { const member = policyMembers?.[accountID]; - if (Object.keys(member?.errors ?? {}).length > 0) { + if (Object.keys(member?.errors ?? {})?.length > 0) { return; } const personalDetail = personalDetails[accountID]; @@ -134,7 +136,7 @@ function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, person */ function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): string[] { const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; - Object.keys(policyMembers).forEach((accountID) => { + Object.keys(policyMembers ?? {}).forEach((accountID) => { const policyMember = policyMembers?.[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). if (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { @@ -154,11 +156,11 @@ function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: * Gets the tag from policy tags, defaults to the first if no key is provided. */ function getTag(policyTags: Record, tagKey?: keyof typeof policyTags) { - if (Object.keys(policyTags).length === 0) { + if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } - const policyTagKey = tagKey ?? Object.keys(policyTags)[0]; + const policyTagKey = tagKey ?? Object.keys(policyTags ?? {})[0]; return policyTags?.[policyTagKey] ?? {}; } @@ -167,11 +169,11 @@ function getTag(policyTags: Record, tagKey?: keyof * Gets the first tag name from policy tags. */ function getTagListName(policyTags: Record) { - if (Object.keys(policyTags).length === 0) { + if (Object.keys(policyTags ?? {})?.length === 0) { return ''; } - const policyTagKeys = Object.keys(policyTags)[0] ?? []; + const policyTagKeys = Object.keys(policyTags ?? {})[0] ?? []; return policyTags?.[policyTagKeys]?.name ?? ''; } @@ -180,17 +182,17 @@ function getTagListName(policyTags: Record) { * Gets the tags of a policy for a specific key. Defaults to the first tag if no key is provided. */ function getTagList(policyTags: Record>, tagKey: string) { - if (Object.keys(policyTags).length === 0) { + if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } - const policyTagKey = tagKey ?? Object.keys(policyTags)[0]; + const policyTagKey = tagKey ?? Object.keys(policyTags ?? {})[0]; return policyTags?.[policyTagKey]?.tags ?? {}; } function isPendingDeletePolicy(policy: OnyxTypes.Policy): boolean { - return policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + return policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } export { From bbcdbd146d234e201c05f74eaf40842b5a8c3811 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 3 Oct 2023 19:17:51 +0200 Subject: [PATCH 4/9] Changes after review of PolicyUtils --- src/ONYXKEYS.ts | 2 +- src/libs/PolicyUtils.ts | 50 ++++++++++++++++++---------------- src/types/onyx/PolicyMember.ts | 3 ++ src/types/onyx/PolicyTag.ts | 3 ++ src/types/onyx/index.ts | 6 ++-- 5 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a1afc4fef2c1..b01ffbc37141 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -373,7 +373,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTag; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; - [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMember; + [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 9b45318e67eb..c0bb7e539bec 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,21 +1,24 @@ +import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; -import * as OnyxTypes from '../types/onyx'; +import {PersonalDetails, Policy, PolicyMembers, PolicyTags} from '../types/onyx'; -type PolicyMemberList = Record; -type PolicyMembersCollection = Record; type MemberEmailsToAccountIDs = Record; -type PersonalDetailsList = Record; +type PersonalDetailsList = Record; type UnitRate = {rate: number}; /** * Filter out the active policies, which will exclude policies with pending deletion * These are policies that we can use to create reports with in NewDot. */ -function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { - return (policies ?? []).filter( - (policy) => policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, +function getActivePolicies(policies: OnyxCollection): Policy[] | undefined { + if (!policies) { + return; + } + return (Object.values(policies) ?? []).filter( + (policy): policy is Policy => + policy !== null && policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); } @@ -23,28 +26,28 @@ function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { * Checks if we have any errors stored within the POLICY_MEMBERS. Determines whether we should show a red brick road error or not. * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...} */ -function hasPolicyMemberError(policyMembers: PolicyMemberList): boolean { +function hasPolicyMemberError(policyMembers: OnyxEntry): boolean { return Object.values(policyMembers ?? {}).some((member) => Object.keys(member?.errors ?? {}).length > 0); } /** * Check if the policy has any error fields. */ -function hasPolicyErrorFields(policy: OnyxTypes.Policy): boolean { +function hasPolicyErrorFields(policy: OnyxEntry): boolean { return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors ?? {}).length > 0); } /** * Check if the policy has any errors, and if it doesn't, then check if it has any error fields. */ -function hasPolicyError(policy: OnyxTypes.Policy): boolean { +function hasPolicyError(policy: OnyxEntry): boolean { return Object.keys(policy?.errors ?? {}).length > 0 ? true : hasPolicyErrorFields(policy); } /** * Checks if we have any errors stored within the policy custom units. */ -function hasCustomUnitsError(policy: OnyxTypes.Policy): boolean { +function hasCustomUnitsError(policy: OnyxEntry): boolean { return Object.keys(policy?.customUnits?.errors ?? {}).length > 0; } @@ -71,8 +74,8 @@ function getUnitRateValue(customUnitRate: UnitRate, toLocaleDigit: (arg: string) /** * Get the brick road indicator status for a policy. The policy has an error status if there is a policy member error, a custom unit error or a field error. */ -function getPolicyBrickRoadIndicatorStatus(policy: OnyxTypes.Policy, policyMembersCollection: PolicyMembersCollection): string { - const policyMembers = policyMembersCollection?.[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy.id}`] ?? {}; +function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMembersCollection: OnyxCollection): string { + const policyMembers = policyMembersCollection?.[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}`] ?? {}; if (hasPolicyMemberError(policyMembers) || hasCustomUnitsError(policy) || hasPolicyErrorFields(policy)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } @@ -86,8 +89,9 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxTypes.Policy, policyMembe * Note: Using a local ONYXKEYS.NETWORK subscription will cause a delay in * updating the screen. Passing the offline status from the component. */ -function shouldShowPolicy(policy: OnyxTypes.Policy, isOffline: boolean): boolean { +function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolean { return ( + policy !== null && policy && policy?.isPolicyExpenseChatEnabled && policy?.role === CONST.POLICY.ROLE.ADMIN && @@ -108,21 +112,21 @@ function isExpensifyGuideTeam(email: string): boolean { /** * Checks if the current user is an admin of the policy. */ -const isPolicyAdmin = (policy: OnyxTypes.Policy): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; +const isPolicyAdmin = (policy: OnyxEntry): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; /** * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. * * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ -function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): MemberEmailsToAccountIDs { +function getMemberAccountIDsForWorkspace(policyMembers: OnyxEntry, personalDetails: OnyxEntry): MemberEmailsToAccountIDs { const memberEmailsToAccountIDs: Record = {}; Object.keys(policyMembers ?? {}).forEach((accountID) => { const member = policyMembers?.[accountID]; if (Object.keys(member?.errors ?? {})?.length > 0) { return; } - const personalDetail = personalDetails[accountID]; + const personalDetail = personalDetails?.[accountID]; if (!personalDetail?.login) { return; } @@ -134,12 +138,12 @@ function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, person /** * Get login list that we should not show in the workspace invite options */ -function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): string[] { +function getIneligibleInvitees(policyMembers: OnyxEntry, personalDetails: OnyxEntry): string[] { const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; Object.keys(policyMembers ?? {}).forEach((accountID) => { const policyMember = policyMembers?.[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). - if (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { + if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { return; } const memberEmail = personalDetails?.[accountID]?.login; @@ -155,7 +159,7 @@ function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: /** * Gets the tag from policy tags, defaults to the first if no key is provided. */ -function getTag(policyTags: Record, tagKey?: keyof typeof policyTags) { +function getTag(policyTags: OnyxEntry, tagKey?: keyof typeof policyTags) { if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } @@ -168,7 +172,7 @@ function getTag(policyTags: Record, tagKey?: keyof /** * Gets the first tag name from policy tags. */ -function getTagListName(policyTags: Record) { +function getTagListName(policyTags: OnyxEntry) { if (Object.keys(policyTags ?? {})?.length === 0) { return ''; } @@ -181,7 +185,7 @@ function getTagListName(policyTags: Record) { /** * Gets the tags of a policy for a specific key. Defaults to the first tag if no key is provided. */ -function getTagList(policyTags: Record>, tagKey: string) { +function getTagList(policyTags: OnyxCollection, tagKey: string) { if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } @@ -191,7 +195,7 @@ function getTagList(policyTags: Record): boolean { return policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } diff --git a/src/types/onyx/PolicyMember.ts b/src/types/onyx/PolicyMember.ts index 055465020c36..60836b276ea0 100644 --- a/src/types/onyx/PolicyMember.ts +++ b/src/types/onyx/PolicyMember.ts @@ -14,4 +14,7 @@ type PolicyMember = { pendingAction?: OnyxCommon.PendingAction; }; +type PolicyMembers = Record; + export default PolicyMember; +export type {PolicyMembers}; diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index fe6bee3a1f31..7807dcc00433 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -10,4 +10,7 @@ type PolicyTag = { 'GL Code': string; }; +type PolicyTags = Record; + export default PolicyTag; +export type {PolicyTags}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e50925e7adf2..9067229a17d9 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -32,7 +32,7 @@ import WalletTransfer from './WalletTransfer'; import MapboxAccessToken from './MapboxAccessToken'; import {OnyxUpdatesFromServer, OnyxUpdateEvent} from './OnyxUpdatesFromServer'; import Download from './Download'; -import PolicyMember from './PolicyMember'; +import PolicyMember, {PolicyMembers} from './PolicyMember'; import Policy from './Policy'; import PolicyCategory from './PolicyCategory'; import Report from './Report'; @@ -45,7 +45,7 @@ import Form, {AddDebitCardForm} from './Form'; import RecentWaypoint from './RecentWaypoint'; import RecentlyUsedCategories from './RecentlyUsedCategories'; import RecentlyUsedTags from './RecentlyUsedTags'; -import PolicyTag from './PolicyTag'; +import PolicyTag, {PolicyTags} from './PolicyTag'; export type { Account, @@ -98,4 +98,6 @@ export type { RecentlyUsedCategories, RecentlyUsedTags, PolicyTag, + PolicyTags, + PolicyMembers, }; From 3c41ef5638447698d902e9285a8505e5254d6b92 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 11 Oct 2023 11:43:12 +0200 Subject: [PATCH 5/9] Fixes in onyxkeys --- src/ONYXKEYS.ts | 2 +- src/libs/PolicyUtils.ts | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b01ffbc37141..495b9c4595f9 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -370,7 +370,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; [ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategory; - [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTag; + [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTags; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index c0bb7e539bec..7afac2ce5b67 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -13,10 +13,7 @@ type UnitRate = {rate: number}; * These are policies that we can use to create reports with in NewDot. */ function getActivePolicies(policies: OnyxCollection): Policy[] | undefined { - if (!policies) { - return; - } - return (Object.values(policies) ?? []).filter( + return (Object.values(policies ?? {}) ?? []).filter( (policy): policy is Policy => policy !== null && policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); @@ -91,8 +88,7 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMemb */ function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolean { return ( - policy !== null && - policy && + !!policy && policy?.isPolicyExpenseChatEnabled && policy?.role === CONST.POLICY.ROLE.ADMIN && (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) @@ -120,7 +116,7 @@ const isPolicyAdmin = (policy: OnyxEntry): boolean => policy?.role === C * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ function getMemberAccountIDsForWorkspace(policyMembers: OnyxEntry, personalDetails: OnyxEntry): MemberEmailsToAccountIDs { - const memberEmailsToAccountIDs: Record = {}; + const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; Object.keys(policyMembers ?? {}).forEach((accountID) => { const member = policyMembers?.[accountID]; if (Object.keys(member?.errors ?? {})?.length > 0) { From 010b47b660a11449b6e64babfe9bfdee232e450d Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 16 Oct 2023 10:34:48 +0200 Subject: [PATCH 6/9] Quick fixes for policy utils --- src/libs/PolicyUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 7afac2ce5b67..e75fdd48be0e 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -13,7 +13,7 @@ type UnitRate = {rate: number}; * These are policies that we can use to create reports with in NewDot. */ function getActivePolicies(policies: OnyxCollection): Policy[] | undefined { - return (Object.values(policies ?? {}) ?? []).filter( + return Object.values(policies ?? {}).filter( (policy): policy is Policy => policy !== null && policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); From f921458f3c660a7b0473f7365389e751e26267df Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 23 Oct 2023 09:39:04 +0200 Subject: [PATCH 7/9] Remove PolicyUtils.ts --- src/libs/PolicyUtils.ts | 217 ---------------------------------------- 1 file changed, 217 deletions(-) delete mode 100644 src/libs/PolicyUtils.ts diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts deleted file mode 100644 index ebf89867ff50..000000000000 --- a/src/libs/PolicyUtils.ts +++ /dev/null @@ -1,217 +0,0 @@ -import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import Str from 'expensify-common/lib/str'; -import CONST from '../CONST'; -import ONYXKEYS from '../ONYXKEYS'; -import {PersonalDetails, Policy, PolicyMembers, PolicyTags} from '../types/onyx'; - -type MemberEmailsToAccountIDs = Record; -type PersonalDetailsList = Record; -type UnitRate = {rate: number}; - -/** - * Filter out the active policies, which will exclude policies with pending deletion - * These are policies that we can use to create reports with in NewDot. - */ -function getActivePolicies(policies: OnyxCollection): Policy[] | undefined { - return Object.values(policies ?? {}).filter( - (policy): policy is Policy => - policy !== null && policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - ); -} - -/** - * Checks if we have any errors stored within the POLICY_MEMBERS. Determines whether we should show a red brick road error or not. - * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...} - */ -function hasPolicyMemberError(policyMembers: OnyxEntry): boolean { - return Object.values(policyMembers ?? {}).some((member) => Object.keys(member?.errors ?? {}).length > 0); -} - -/** - * Check if the policy has any error fields. - */ -function hasPolicyErrorFields(policy: OnyxEntry): boolean { - return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors ?? {}).length > 0); -} - -/** - * Check if the policy has any errors, and if it doesn't, then check if it has any error fields. - */ -function hasPolicyError(policy: OnyxEntry): boolean { - return Object.keys(policy?.errors ?? {}).length > 0 ? true : hasPolicyErrorFields(policy); -} - -/** - * Checks if we have any errors stored within the policy custom units. - */ -function hasCustomUnitsError(policy: OnyxEntry): boolean { - return Object.keys(policy?.customUnits?.errors ?? {}).length > 0; -} - -function getNumericValue(value: number, toLocaleDigit: (arg: string) => string): number | string { - const numValue = parseFloat(value.toString().replace(toLocaleDigit('.'), '.')); - if (Number.isNaN(numValue)) { - return NaN; - } - return numValue.toFixed(CONST.CUSTOM_UNITS.RATE_DECIMALS); -} - -function getRateDisplayValue(value: number, toLocaleDigit: (arg: string) => string): string { - const numValue = getNumericValue(value, toLocaleDigit); - if (Number.isNaN(numValue)) { - return ''; - } - return numValue.toString().replace('.', toLocaleDigit('.')).substring(0, value.toString().length); -} - -function getUnitRateValue(customUnitRate: UnitRate, toLocaleDigit: (arg: string) => string) { - return getRateDisplayValue((customUnitRate?.rate ?? 0) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, toLocaleDigit); -} - -/** - * Get the brick road indicator status for a policy. The policy has an error status if there is a policy member error, a custom unit error or a field error. - */ -function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMembersCollection: OnyxCollection): string { - const policyMembers = policyMembersCollection?.[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}`] ?? {}; - if (hasPolicyMemberError(policyMembers) || hasCustomUnitsError(policy) || hasPolicyErrorFields(policy)) { - return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; - } - return ''; -} - -/** - * Check if the policy can be displayed - * If offline, always show the policy pending deletion. - * If online, show the policy pending deletion only if there is an error. - * Note: Using a local ONYXKEYS.NETWORK subscription will cause a delay in - * updating the screen. Passing the offline status from the component. - */ -function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolean { - return ( - !!policy && - policy?.isPolicyExpenseChatEnabled && - policy?.role === CONST.POLICY.ROLE.ADMIN && - (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) - ); -} - -function isExpensifyTeam(email: string): boolean { - const emailDomain = Str.extractEmailDomain(email ?? ''); - return emailDomain === CONST.EXPENSIFY_PARTNER_NAME || emailDomain === CONST.EMAIL.GUIDES_DOMAIN; -} - -function isExpensifyGuideTeam(email: string): boolean { - const emailDomain = Str.extractEmailDomain(email ?? ''); - return emailDomain === CONST.EMAIL.GUIDES_DOMAIN; -} - -/** - * Checks if the current user is an admin of the policy. - */ -const isPolicyAdmin = (policy: OnyxEntry): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; - -/** - * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. - * - * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. - */ -function getMemberAccountIDsForWorkspace(policyMembers: OnyxEntry, personalDetails: OnyxEntry): MemberEmailsToAccountIDs { - const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; - Object.keys(policyMembers ?? {}).forEach((accountID) => { - const member = policyMembers?.[accountID]; - if (Object.keys(member?.errors ?? {})?.length > 0) { - return; - } - const personalDetail = personalDetails?.[accountID]; - if (!personalDetail?.login) { - return; - } - memberEmailsToAccountIDs[personalDetail.login] = Number(accountID); - }); - return memberEmailsToAccountIDs; -} - -/** - * Get login list that we should not show in the workspace invite options - */ -function getIneligibleInvitees(policyMembers: OnyxEntry, personalDetails: OnyxEntry): string[] { - const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; - Object.keys(policyMembers ?? {}).forEach((accountID) => { - const policyMember = policyMembers?.[accountID]; - // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). - if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { - return; - } - const memberEmail = personalDetails?.[accountID]?.login; - if (!memberEmail) { - return; - } - memberEmailsToExclude.push(memberEmail); - }); - - return memberEmailsToExclude; -} - -/** - * Gets the tag from policy tags, defaults to the first if no key is provided. - */ -function getTag(policyTags: OnyxEntry, tagKey?: keyof typeof policyTags) { - if (Object.keys(policyTags ?? {})?.length === 0) { - return {}; - } - - const policyTagKey = tagKey ?? Object.keys(policyTags ?? {})[0]; - - return policyTags?.[policyTagKey] ?? {}; -} - -/** - * Gets the first tag name from policy tags. - */ -function getTagListName(policyTags: OnyxEntry) { - if (Object.keys(policyTags ?? {})?.length === 0) { - return ''; - } - - const policyTagKeys = Object.keys(policyTags ?? {})[0] ?? []; - - return policyTags?.[policyTagKeys]?.name ?? ''; -} - -/** - * Gets the tags of a policy for a specific key. Defaults to the first tag if no key is provided. - */ -function getTagList(policyTags: OnyxCollection, tagKey: string) { - if (Object.keys(policyTags ?? {})?.length === 0) { - return {}; - } - - const policyTagKey = tagKey ?? Object.keys(policyTags ?? {})[0]; - - return policyTags?.[policyTagKey]?.tags ?? {}; -} - -function isPendingDeletePolicy(policy: OnyxEntry): boolean { - return policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; -} - -export { - getActivePolicies, - hasPolicyMemberError, - hasPolicyError, - hasPolicyErrorFields, - hasCustomUnitsError, - getNumericValue, - getUnitRateValue, - getPolicyBrickRoadIndicatorStatus, - shouldShowPolicy, - isExpensifyTeam, - isExpensifyGuideTeam, - isPolicyAdmin, - getMemberAccountIDsForWorkspace, - getIneligibleInvitees, - getTag, - getTagListName, - getTagList, - isPendingDeletePolicy, -}; From db434434c6a45eacfea9b21324326b056d653a6a Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 23 Oct 2023 09:40:04 +0200 Subject: [PATCH 8/9] Rename PolicyUtils --- src/libs/{PolicyUtils.js => PolicyUtils.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/libs/{PolicyUtils.js => PolicyUtils.ts} (100%) diff --git a/src/libs/PolicyUtils.js b/src/libs/PolicyUtils.ts similarity index 100% rename from src/libs/PolicyUtils.js rename to src/libs/PolicyUtils.ts From 560b8cac16b9c5e44e00318d91c5eea7e845999c Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 23 Oct 2023 09:40:37 +0200 Subject: [PATCH 9/9] Migrate PolicyUtils to TS --- src/libs/PolicyUtils.ts | 192 +++++++++++++--------------------------- 1 file changed, 63 insertions(+), 129 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 6bbae72f1d80..ebf89867ff50 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,69 +1,54 @@ -import _ from 'underscore'; -import lodashGet from 'lodash/get'; +import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; +import {PersonalDetails, Policy, PolicyMembers, PolicyTags} from '../types/onyx'; + +type MemberEmailsToAccountIDs = Record; +type PersonalDetailsList = Record; +type UnitRate = {rate: number}; /** * Filter out the active policies, which will exclude policies with pending deletion * These are policies that we can use to create reports with in NewDot. - * @param {Object} policies - * @returns {Array} */ -function getActivePolicies(policies) { - return _.filter(policies, (policy) => policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); +function getActivePolicies(policies: OnyxCollection): Policy[] | undefined { + return Object.values(policies ?? {}).filter( + (policy): policy is Policy => + policy !== null && policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + ); } /** * Checks if we have any errors stored within the POLICY_MEMBERS. Determines whether we should show a red brick road error or not. * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...} - * - * @param {Object} policyMembers - * @returns {Boolean} */ -function hasPolicyMemberError(policyMembers) { - return _.some(policyMembers, (member) => !_.isEmpty(member.errors)); +function hasPolicyMemberError(policyMembers: OnyxEntry): boolean { + return Object.values(policyMembers ?? {}).some((member) => Object.keys(member?.errors ?? {}).length > 0); } /** * Check if the policy has any error fields. - * - * @param {Object} policy - * @param {Object} policy.errorFields - * @return {Boolean} */ -function hasPolicyErrorFields(policy) { - return _.some(lodashGet(policy, 'errorFields', {}), (fieldErrors) => !_.isEmpty(fieldErrors)); +function hasPolicyErrorFields(policy: OnyxEntry): boolean { + return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors ?? {}).length > 0); } /** * Check if the policy has any errors, and if it doesn't, then check if it has any error fields. - * - * @param {Object} policy - * @param {Object} policy.errors - * @param {Object} policy.errorFields - * @return {Boolean} */ -function hasPolicyError(policy) { - return !_.isEmpty(lodashGet(policy, 'errors', {})) ? true : hasPolicyErrorFields(policy); +function hasPolicyError(policy: OnyxEntry): boolean { + return Object.keys(policy?.errors ?? {}).length > 0 ? true : hasPolicyErrorFields(policy); } /** * Checks if we have any errors stored within the policy custom units. - * - * @param {Object} policy - * @returns {Boolean} */ -function hasCustomUnitsError(policy) { - return !_.isEmpty(_.pick(lodashGet(policy, 'customUnits', {}), 'errors')); +function hasCustomUnitsError(policy: OnyxEntry): boolean { + return Object.keys(policy?.customUnits?.errors ?? {}).length > 0; } -/** - * @param {Number} value - * @param {Function} toLocaleDigit - * @returns {Number} - */ -function getNumericValue(value, toLocaleDigit) { +function getNumericValue(value: number, toLocaleDigit: (arg: string) => string): number | string { const numValue = parseFloat(value.toString().replace(toLocaleDigit('.'), '.')); if (Number.isNaN(numValue)) { return NaN; @@ -71,39 +56,23 @@ function getNumericValue(value, toLocaleDigit) { return numValue.toFixed(CONST.CUSTOM_UNITS.RATE_DECIMALS); } -/** - * @param {Number} value - * @param {Function} toLocaleDigit - * @returns {String} - */ -function getRateDisplayValue(value, toLocaleDigit) { +function getRateDisplayValue(value: number, toLocaleDigit: (arg: string) => string): string { const numValue = getNumericValue(value, toLocaleDigit); if (Number.isNaN(numValue)) { return ''; } - return numValue.toString().replace('.', toLocaleDigit('.')).substring(0, value.length); + return numValue.toString().replace('.', toLocaleDigit('.')).substring(0, value.toString().length); } -/** - * @param {Object} customUnitRate - * @param {Number} customUnitRate.rate - * @param {Function} toLocaleDigit - * @returns {String} - */ -function getUnitRateValue(customUnitRate, toLocaleDigit) { - return getRateDisplayValue(lodashGet(customUnitRate, 'rate', 0) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, toLocaleDigit); +function getUnitRateValue(customUnitRate: UnitRate, toLocaleDigit: (arg: string) => string) { + return getRateDisplayValue((customUnitRate?.rate ?? 0) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, toLocaleDigit); } /** * Get the brick road indicator status for a policy. The policy has an error status if there is a policy member error, a custom unit error or a field error. - * - * @param {Object} policy - * @param {String} policy.id - * @param {Object} policyMembersCollection - * @returns {String} */ -function getPolicyBrickRoadIndicatorStatus(policy, policyMembersCollection) { - const policyMembers = lodashGet(policyMembersCollection, `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy.id}`, {}); +function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMembersCollection: OnyxCollection): string { + const policyMembers = policyMembersCollection?.[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}`] ?? {}; if (hasPolicyMemberError(policyMembers) || hasCustomUnitsError(policy) || hasPolicyErrorFields(policy)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } @@ -116,62 +85,45 @@ function getPolicyBrickRoadIndicatorStatus(policy, policyMembersCollection) { * If online, show the policy pending deletion only if there is an error. * Note: Using a local ONYXKEYS.NETWORK subscription will cause a delay in * updating the screen. Passing the offline status from the component. - * @param {Object} policy - * @param {Boolean} isOffline - * @returns {Boolean} */ -function shouldShowPolicy(policy, isOffline) { +function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolean { return ( - policy && - policy.isPolicyExpenseChatEnabled && - policy.role === CONST.POLICY.ROLE.ADMIN && - (isOffline || policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(policy.errors)) + !!policy && + policy?.isPolicyExpenseChatEnabled && + policy?.role === CONST.POLICY.ROLE.ADMIN && + (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) ); } -/** - * @param {string} email - * @returns {boolean} - */ -function isExpensifyTeam(email) { - const emailDomain = Str.extractEmailDomain(email); +function isExpensifyTeam(email: string): boolean { + const emailDomain = Str.extractEmailDomain(email ?? ''); return emailDomain === CONST.EXPENSIFY_PARTNER_NAME || emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } -/** - * @param {string} email - * @returns {boolean} - */ -function isExpensifyGuideTeam(email) { - const emailDomain = Str.extractEmailDomain(email); +function isExpensifyGuideTeam(email: string): boolean { + const emailDomain = Str.extractEmailDomain(email ?? ''); return emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } /** * Checks if the current user is an admin of the policy. - * - * @param {Object} policy - * @returns {Boolean} */ -const isPolicyAdmin = (policy) => lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; +const isPolicyAdmin = (policy: OnyxEntry): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; /** - * @param {Object} policyMembers - * @param {Object} personalDetails - * @returns {Object} - * * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. * * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ -function getMemberAccountIDsForWorkspace(policyMembers, personalDetails) { - const memberEmailsToAccountIDs = {}; - _.each(policyMembers, (member, accountID) => { - if (!_.isEmpty(member.errors)) { +function getMemberAccountIDsForWorkspace(policyMembers: OnyxEntry, personalDetails: OnyxEntry): MemberEmailsToAccountIDs { + const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; + Object.keys(policyMembers ?? {}).forEach((accountID) => { + const member = policyMembers?.[accountID]; + if (Object.keys(member?.errors ?? {})?.length > 0) { return; } - const personalDetail = personalDetails[accountID]; - if (!personalDetail || !personalDetail.login) { + const personalDetail = personalDetails?.[accountID]; + if (!personalDetail?.login) { return; } memberEmailsToAccountIDs[personalDetail.login] = Number(accountID); @@ -181,19 +133,16 @@ function getMemberAccountIDsForWorkspace(policyMembers, personalDetails) { /** * Get login list that we should not show in the workspace invite options - * - * @param {Object} policyMembers - * @param {Object} personalDetails - * @returns {Array} */ -function getIneligibleInvitees(policyMembers, personalDetails) { - const memberEmailsToExclude = [...CONST.EXPENSIFY_EMAILS]; - _.each(policyMembers, (policyMember, accountID) => { +function getIneligibleInvitees(policyMembers: OnyxEntry, personalDetails: OnyxEntry): string[] { + const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; + Object.keys(policyMembers ?? {}).forEach((accountID) => { + const policyMember = policyMembers?.[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). - if (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(policyMember.errors)) { + if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { return; } - const memberEmail = lodashGet(personalDetails, `[${accountID}].login`); + const memberEmail = personalDetails?.[accountID]?.login; if (!memberEmail) { return; } @@ -205,60 +154,45 @@ function getIneligibleInvitees(policyMembers, personalDetails) { /** * Gets the tag from policy tags, defaults to the first if no key is provided. - * - * @param {Object} policyTags - * @param {String} [tagKey] - * @returns {Object} */ -function getTag(policyTags, tagKey) { - if (_.isEmpty(policyTags)) { +function getTag(policyTags: OnyxEntry, tagKey?: keyof typeof policyTags) { + if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } - const policyTagKey = tagKey || _.first(_.keys(policyTags)); + const policyTagKey = tagKey ?? Object.keys(policyTags ?? {})[0]; - return lodashGet(policyTags, policyTagKey, {}); + return policyTags?.[policyTagKey] ?? {}; } /** * Gets the first tag name from policy tags. - * - * @param {Object} policyTags - * @returns {String} */ -function getTagListName(policyTags) { - if (_.isEmpty(policyTags)) { +function getTagListName(policyTags: OnyxEntry) { + if (Object.keys(policyTags ?? {})?.length === 0) { return ''; } - const policyTagKeys = _.keys(policyTags) || []; + const policyTagKeys = Object.keys(policyTags ?? {})[0] ?? []; - return lodashGet(policyTags, [_.first(policyTagKeys), 'name'], ''); + return policyTags?.[policyTagKeys]?.name ?? ''; } /** * Gets the tags of a policy for a specific key. Defaults to the first tag if no key is provided. - * - * @param {Object} policyTags - * @param {String} [tagKey] - * @returns {String} */ -function getTagList(policyTags, tagKey) { - if (_.isEmpty(policyTags)) { +function getTagList(policyTags: OnyxCollection, tagKey: string) { + if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } - const policyTagKey = tagKey || _.first(_.keys(policyTags)); + const policyTagKey = tagKey ?? Object.keys(policyTags ?? {})[0]; - return lodashGet(policyTags, [policyTagKey, 'tags'], {}); + return policyTags?.[policyTagKey]?.tags ?? {}; } -/** - * @param {Object} policy - * @returns {Boolean} - */ -function isPendingDeletePolicy(policy) { - return policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; +function isPendingDeletePolicy(policy: OnyxEntry): boolean { + return policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } export {