Skip to content

Commit f27bbb2

Browse files
authored
Merge pull request Expensify#85746 from nkdengineer/fix/83130
fix: Offline deleted rules reappear after reconnecting until cache is cleared
2 parents 643d371 + f83a495 commit f27bbb2

6 files changed

Lines changed: 125 additions & 66 deletions

File tree

src/libs/PolicyUtils.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -321,10 +321,20 @@ function getCustomUnitsForDuplication(
321321
return undefined;
322322
}
323323

324-
if (isDistanceRatesOptionSelected && isPerDiemOptionSelected) {
325-
const distanceCustomUnit = Object.values(customUnits).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE);
326-
const perDiemUnit = Object.values(customUnits).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_PER_DIEM_INTERNATIONAL);
324+
const getUnitWithoutPendingDeleteRates = (customUnit: CustomUnit | undefined) => {
325+
if (!customUnit) {
326+
return undefined;
327+
}
328+
return {
329+
...customUnit,
330+
rates: Object.fromEntries(Object.entries(customUnit.rates).filter(([, rate]) => rate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)),
331+
};
332+
};
333+
334+
const distanceCustomUnit = getUnitWithoutPendingDeleteRates(Object.values(customUnits).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE));
335+
const perDiemUnit = getUnitWithoutPendingDeleteRates(Object.values(customUnits).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_PER_DIEM_INTERNATIONAL));
327336

337+
if (isDistanceRatesOptionSelected && isPerDiemOptionSelected) {
328338
if (!perDiemUnit || !distanceCustomUnit || !perDiemCustomUnitID || !distanceCustomUnitID) {
329339
return undefined;
330340
}
@@ -333,14 +343,12 @@ function getCustomUnitsForDuplication(
333343
}
334344

335345
if (isDistanceRatesOptionSelected && distanceCustomUnitID) {
336-
const distanceCustomUnit = Object.values(customUnits).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE);
337346
if (!distanceCustomUnit) {
338347
return undefined;
339348
}
340349
return {[distanceCustomUnitID]: distanceCustomUnit};
341350
}
342351

343-
const perDiemUnit = Object.values(customUnits).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_PER_DIEM_INTERNATIONAL);
344352
if (!perDiemUnit || !perDiemCustomUnitID) {
345353
return undefined;
346354
}

src/libs/ReportUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4566,7 +4566,7 @@ function getReportFieldKey(reportFieldId: string | undefined) {
45664566
/**
45674567
* Get the report fields attached to the policy given policyID
45684568
*/
4569-
function getReportFieldsByPolicyID(policyID: string | undefined): Record<string, PolicyReportField> {
4569+
function getReportFieldsByPolicyID(policyID: string | undefined): Policy['fieldList'] {
45704570
if (!policyID) {
45714571
return {};
45724572
}

src/libs/actions/Policy/Category.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ function appendSetupCategoriesOnboardingData(
104104
function buildOptimisticPolicyWithExistingCategories(policyID: string, categories: PolicyCategories) {
105105
const categoriesValues = Object.values(categories);
106106
const optimisticCategoryMap = categoriesValues.reduce<Record<string, Partial<PolicyCategory>>>((acc, category) => {
107+
if (category.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
108+
return acc;
109+
}
107110
acc[category.name] = {
108111
...category,
109112
errors: null,

src/libs/actions/Policy/Policy.ts

Lines changed: 93 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3097,6 +3097,98 @@ function createDraftWorkspace(
30973097
return params;
30983098
}
30993099

3100+
function buildOptimisticDuplicatePolicy(sourcePolicy: Policy, policyOptions: DuplicatePolicyDataOptions) {
3101+
const {
3102+
policyName: duplicatedPolicyName = '',
3103+
targetPolicyID: duplicatedPolicyID,
3104+
file: duplicatedPolicyFile,
3105+
parts: duplicatedParts,
3106+
localCurrency: duplicatedLocalCurrency,
3107+
} = policyOptions;
3108+
3109+
const isMemberFeatureSelected = duplicatedParts?.people;
3110+
const isReportsFeatureSelected = duplicatedParts?.reports;
3111+
const isConnectionsFeatureSelected = duplicatedParts?.connections;
3112+
const isTaxesFeatureSelected = duplicatedParts?.taxes;
3113+
const isTagsFeatureSelected = duplicatedParts?.tags;
3114+
const isInvoicesFeatureSelected = duplicatedParts?.invoices;
3115+
const isDistanceRatesFeatureSelected = duplicatedParts?.distance;
3116+
const isRulesFeatureSelected = duplicatedParts?.expenses;
3117+
const isWorkflowsFeatureSelected = duplicatedParts?.exportLayouts;
3118+
const isPerDiemFeatureSelected = duplicatedParts?.perDiem;
3119+
const isOverviewFeatureSelected = duplicatedParts?.overview;
3120+
const isTravelFeatureSelected = duplicatedParts?.travel;
3121+
const isCodingRulesFeatureSelected = duplicatedParts?.codingRules;
3122+
const duplicatedOutputCurrency = isOverviewFeatureSelected ? sourcePolicy?.outputCurrency : duplicatedLocalCurrency;
3123+
const {customUnitID: duplicatedDistanceCustomUnitID} = buildOptimisticDistanceRateCustomUnits(duplicatedOutputCurrency);
3124+
const duplicatedPerDiemCustomUnitID = generateCustomUnitID();
3125+
3126+
const filterPendingDeleteData = <T>(data?: Record<string, T>): Record<string, T> | undefined =>
3127+
data
3128+
? (Object.fromEntries(
3129+
Object.entries(data).filter(([, value]) => {
3130+
if (!value || typeof value !== 'object' || !('pendingAction' in value)) {
3131+
return true;
3132+
}
3133+
return value.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
3134+
}),
3135+
) as Record<string, T>)
3136+
: undefined;
3137+
3138+
const codingRulesWithoutPendingDelete = filterPendingDeleteData(sourcePolicy?.rules?.codingRules);
3139+
const employeeListWithoutPendingDelete = filterPendingDeleteData(sourcePolicy?.employeeList);
3140+
const fieldListWithoutPendingDelete = filterPendingDeleteData(sourcePolicy?.fieldList);
3141+
const connectionsWithoutPendingDelete = filterPendingDeleteData(sourcePolicy?.connections);
3142+
const taxRatesWithoutPendingDelete = {
3143+
...sourcePolicy?.taxRates,
3144+
taxes: filterPendingDeleteData(sourcePolicy?.taxRates?.taxes),
3145+
};
3146+
3147+
return {
3148+
...sourcePolicy,
3149+
areCategoriesEnabled: true,
3150+
areTagsEnabled: isTagsFeatureSelected,
3151+
areDistanceRatesEnabled: isDistanceRatesFeatureSelected,
3152+
areInvoicesEnabled: isInvoicesFeatureSelected,
3153+
areRulesEnabled: isRulesFeatureSelected,
3154+
areWorkflowsEnabled: isWorkflowsFeatureSelected,
3155+
areReportFieldsEnabled: isReportsFeatureSelected,
3156+
areConnectionsEnabled: isConnectionsFeatureSelected,
3157+
arePerDiemRatesEnabled: isPerDiemFeatureSelected,
3158+
isTravelEnabled: isTravelFeatureSelected ? sourcePolicy?.isTravelEnabled : undefined,
3159+
travelSettings: undefined,
3160+
workspaceAccountID: undefined,
3161+
tax: isTaxesFeatureSelected ? sourcePolicy?.tax : undefined,
3162+
employeeList: isMemberFeatureSelected ? employeeListWithoutPendingDelete : {[sourcePolicy.owner]: sourcePolicy?.employeeList?.[sourcePolicy.owner]},
3163+
id: duplicatedPolicyID,
3164+
name: duplicatedPolicyName,
3165+
fieldList: isReportsFeatureSelected ? fieldListWithoutPendingDelete : undefined,
3166+
connections: isConnectionsFeatureSelected ? connectionsWithoutPendingDelete : undefined,
3167+
customUnits: getCustomUnitsForDuplication(sourcePolicy, isDistanceRatesFeatureSelected, isPerDiemFeatureSelected, {
3168+
distanceCustomUnitID: duplicatedDistanceCustomUnitID,
3169+
perDiemCustomUnitID: duplicatedPerDiemCustomUnitID,
3170+
}),
3171+
taxRates: isTaxesFeatureSelected ? taxRatesWithoutPendingDelete : undefined,
3172+
rules: isCodingRulesFeatureSelected ? {codingRules: codingRulesWithoutPendingDelete} : undefined,
3173+
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3174+
pendingFields: {
3175+
autoReporting: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3176+
approvalMode: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3177+
reimbursementChoice: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3178+
name: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3179+
outputCurrency: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3180+
address: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3181+
description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3182+
type: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3183+
areReportFieldsEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3184+
},
3185+
avatarURL: duplicatedPolicyFile?.uri,
3186+
originalFileName: duplicatedPolicyFile?.name,
3187+
outputCurrency: duplicatedOutputCurrency,
3188+
address: isOverviewFeatureSelected ? sourcePolicy?.address : undefined,
3189+
};
3190+
}
3191+
31003192
function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOptions) {
31013193
const {policyName = '', policyID = generatePolicyID(), file, welcomeNote, parts, targetPolicyID = generatePolicyID(), policyCategories, localCurrency} = options;
31023194

@@ -3112,19 +3204,8 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp
31123204
pendingChatMembers,
31133205
} = ReportUtils.buildOptimisticWorkspaceChats(targetPolicyID, policyName);
31143206
const isMemberOptionSelected = parts?.people;
3115-
const isReportsOptionSelected = parts?.reports;
3116-
const isConnectionsOptionSelected = parts?.connections;
31173207
const isCategoriesOptionSelected = parts?.categories;
3118-
const isTaxesOptionSelected = parts?.taxes;
3119-
const isTagsOptionSelected = parts?.tags;
3120-
const isInvoicesOptionSelected = parts?.invoices;
3121-
const isDistanceRatesOptionSelected = parts?.distance;
3122-
const isRulesOptionSelected = parts?.expenses;
3123-
const isWorkflowsOptionSelected = parts?.exportLayouts;
3124-
const isPerDiemOptionSelected = parts?.perDiem;
31253208
const isOverviewOptionSelected = parts?.overview;
3126-
const isTravelOptionSelected = parts?.travel;
3127-
const isCodingRulesOptionSelected = parts?.codingRules;
31283209

31293210
const outputCurrency = isOverviewOptionSelected ? policy?.outputCurrency : localCurrency;
31303211

@@ -3157,46 +3238,7 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp
31573238
{
31583239
onyxMethod: Onyx.METHOD.SET,
31593240
key: `${ONYXKEYS.COLLECTION.POLICY}${targetPolicyID}`,
3160-
value: {
3161-
...policy,
3162-
areCategoriesEnabled: true,
3163-
areTagsEnabled: isTagsOptionSelected,
3164-
areDistanceRatesEnabled: isDistanceRatesOptionSelected,
3165-
areInvoicesEnabled: isInvoicesOptionSelected,
3166-
areRulesEnabled: isRulesOptionSelected,
3167-
areWorkflowsEnabled: isWorkflowsOptionSelected,
3168-
areReportFieldsEnabled: isReportsOptionSelected,
3169-
areConnectionsEnabled: isConnectionsOptionSelected,
3170-
arePerDiemRatesEnabled: isPerDiemOptionSelected,
3171-
isTravelEnabled: isTravelOptionSelected ? policy?.isTravelEnabled : undefined,
3172-
travelSettings: undefined,
3173-
workspaceAccountID: undefined,
3174-
tax: isTaxesOptionSelected ? policy?.tax : undefined,
3175-
employeeList: isMemberOptionSelected ? policy.employeeList : {[policy.owner]: policy?.employeeList?.[policy.owner]},
3176-
id: targetPolicyID,
3177-
name: policyName,
3178-
fieldList: isReportsOptionSelected ? policy?.fieldList : undefined,
3179-
connections: isConnectionsOptionSelected ? policy?.connections : undefined,
3180-
customUnits: getCustomUnitsForDuplication(policy, isDistanceRatesOptionSelected, isPerDiemOptionSelected, {distanceCustomUnitID, perDiemCustomUnitID}),
3181-
taxRates: isTaxesOptionSelected ? policy?.taxRates : undefined,
3182-
rules: isCodingRulesOptionSelected ? {codingRules: policy?.rules?.codingRules} : undefined,
3183-
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3184-
pendingFields: {
3185-
autoReporting: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3186-
approvalMode: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3187-
reimbursementChoice: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3188-
name: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3189-
outputCurrency: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3190-
address: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3191-
description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3192-
type: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3193-
areReportFieldsEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
3194-
},
3195-
avatarURL: file?.uri,
3196-
originalFileName: file?.name,
3197-
outputCurrency,
3198-
address: isOverviewOptionSelected ? policy?.address : undefined,
3199-
},
3241+
value: buildOptimisticDuplicatePolicy(policy, {...options, targetPolicyID}),
32003242
},
32013243
{
32023244
onyxMethod: Onyx.METHOD.MERGE,

src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,26 +38,26 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm
3838
const [duplicateWorkspace] = useOnyx(ONYXKEYS.DUPLICATE_WORKSPACE);
3939
const [duplicatedWorkspaceAvatar, setDuplicatedWorkspaceAvatar] = useState<File | undefined>();
4040
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
41-
const allIds = getMemberAccountIDsForWorkspace(policy?.employeeList);
41+
const allIds = getMemberAccountIDsForWorkspace(policy?.employeeList, false, false);
4242
const totalMembers = Object.keys(allIds).length;
4343
const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`);
44-
const taxesLength = Object.keys(policy?.taxRates?.taxes ?? {}).length ?? 0;
44+
const taxesLength = Object.values(policy?.taxRates?.taxes ?? {}).filter((tax) => tax.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length ?? 0;
4545
const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`);
46-
const categoriesCount = Object.keys(policyCategories ?? {}).length;
47-
const codingRulesCount = Object.keys(policy?.rules?.codingRules ?? {}).length;
46+
const categoriesCount = Object.values(policyCategories ?? {}).filter((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length;
47+
const codingRulesCount = Object.values(policy?.rules?.codingRules ?? {}).filter((rule) => rule.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length;
4848
const [selectedItems, setSelectedItems] = useState<string[]>([]);
49-
const reportFields = Object.keys(getReportFieldsByPolicyID(policyID)).length ?? 0;
49+
const reportFields = Object.values(getReportFieldsByPolicyID(policyID) ?? {}).filter((field) => field.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length ?? 0;
5050
const customUnits = getPerDiemCustomUnit(policy);
5151
const customUnitRates: Record<string, Rate> = customUnits?.rates ?? {};
52-
const allRates = Object.values(customUnitRates)?.length;
52+
const allRates = Object.values(customUnitRates)?.filter((rate) => rate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length ?? 0;
5353
const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
5454
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
5555

5656
const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME);
5757
const connectedIntegration = getAllValidConnectedIntegration(policy, accountingIntegrations);
5858

5959
const customUnit = getDistanceRateCustomUnit(policy);
60-
const ratesCount = Object.keys(customUnit?.rates ?? {}).length;
60+
const ratesCount = Object.values(customUnit?.rates ?? {}).filter((rate) => rate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length;
6161
const invoiceCompany =
6262
policy?.invoice?.companyName && policy?.invoice?.companyWebsite
6363
? `${policy?.invoice?.companyName}, ${policy?.invoice?.companyWebsite}`
@@ -67,7 +67,10 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm
6767
if (!policyTags) {
6868
return 0;
6969
}
70-
return Object.values(policyTags).reduce((sum, tagGroup) => sum + Number(Object.values(tagGroup.tags)?.length ?? 0), 0);
70+
return Object.values(policyTags).reduce(
71+
(sum, tagGroup) => sum + Number(Object.values(tagGroup.tags)?.filter((tag) => tag.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length ?? 0),
72+
0,
73+
);
7174
}, [policyTags]);
7275

7376
const formattedAddress = !isEmptyObject(policy) && !isEmptyObject(policy.address) ? formatAddressToString(policy.address) : '';

tests/actions/PolicyTest.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,10 @@ describe('actions/Policy', () => {
270270
it('duplicate workspace', async () => {
271271
(fetch as MockFetch)?.pause?.();
272272
await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID});
273-
const fakePolicy = createRandomPolicy(10, CONST.POLICY.TYPE.PERSONAL);
273+
const fakePolicy = {
274+
...createRandomPolicy(10, CONST.POLICY.TYPE.PERSONAL),
275+
employeeList: {},
276+
};
274277
await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy);
275278
await Onyx.set(`${ONYXKEYS.NVP_ACTIVE_POLICY_ID}`, fakePolicy.id);
276279
await Onyx.set(`${ONYXKEYS.NVP_INTRO_SELECTED}`, {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM});

0 commit comments

Comments
 (0)