Skip to content

Commit

Permalink
Merge pull request #56548 from callstack-internal/feat/policy-custom-…
Browse files Browse the repository at this point in the history
…rules

feat: policy custom rules
  • Loading branch information
iwiznia authored Feb 7, 2025
2 parents 2fc0566 + e233f7f commit b34e5f8
Show file tree
Hide file tree
Showing 21 changed files with 258 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ const CONST = {
NEWDOT_MANAGER_MCTEST: 'newDotManagerMcTest',
NEWDOT_INTERNATIONAL_DEPOSIT_BANK_ACCOUNT: 'newDotInternationalDepositBankAccount',
NSQS: 'nsqs',
CUSTOM_RULES: 'customRules',
},
BUTTON_STATES: {
DEFAULT: 'default',
Expand Down
3 changes: 3 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,8 @@ const ONYXKEYS = {
RULES_MAX_EXPENSE_AMOUNT_FORM_DRAFT: 'rulesMaxExpenseAmountFormDraft',
RULES_MAX_EXPENSE_AGE_FORM: 'rulesMaxExpenseAgeForm',
RULES_MAX_EXPENSE_AGE_FORM_DRAFT: 'rulesMaxExpenseAgeFormDraft',
RULES_CUSTOM_FORM: 'rulesCustomForm',
RULES_CUSTOM_FORM_DRAFT: 'rulesCustomFormDraft',
DEBUG_DETAILS_FORM: 'debugDetailsForm',
DEBUG_DETAILS_FORM_DRAFT: 'debugDetailsFormDraft',
WORKSPACE_PER_DIEM_FORM: 'workspacePerDiemForm',
Expand Down Expand Up @@ -850,6 +852,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.RULES_REQUIRED_RECEIPT_AMOUNT_FORM]: FormTypes.RulesRequiredReceiptAmountForm;
[ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AMOUNT_FORM]: FormTypes.RulesMaxExpenseAmountForm;
[ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AGE_FORM]: FormTypes.RulesMaxExpenseAgeForm;
[ONYXKEYS.FORMS.RULES_CUSTOM_FORM]: FormTypes.RulesCustomForm;
[ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm;
[ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm | FormTypes.DebugTransactionForm | FormTypes.DebugTransactionViolationForm;
[ONYXKEYS.FORMS.INTERNATIONAL_BANK_ACCOUNT_FORM]: FormTypes.InternationalBankAccountForm;
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1624,6 +1624,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/rules/billable',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/billable` as const,
},
RULES_CUSTOM: {
route: 'settings/workspaces/:policyID/rules/custom',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/custom` as const,
},
// Referral program promotion
REFERRAL_DETAILS_MODAL: {
route: 'referral/:contentType',
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ const SCREENS = {
RULES_MAX_EXPENSE_AMOUNT: 'Rules_Max_Expense_Amount',
RULES_MAX_EXPENSE_AGE: 'Rules_Max_Expense_Age',
RULES_BILLABLE_DEFAULT: 'Rules_Billable_Default',
RULES_CUSTOM: 'Rules_Custom',
PER_DIEM: 'Per_Diem',
PER_DIEM_IMPORT: 'Per_Diem_Import',
PER_DIEM_IMPORTED: 'Per_Diem_Imported',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4705,6 +4705,11 @@ const translations = {
goTo: 'Go to',
andEnableWorkflows: 'and enable workflows, then add approvals to unlock this feature.',
},
customRules: {
title: 'Custom rules',
subtitle: 'Description',
description: 'Input custom rules for expense reports',
},
},
planTypePage: {
planTypes: {
Expand Down
5 changes: 5 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4774,6 +4774,11 @@ const translations = {
goTo: 'Ve a',
andEnableWorkflows: 'y habilita los flujos de trabajo, luego añade aprobaciones para desbloquear esta función.',
},
customRules: {
title: 'Reglas personalizadas',
subtitle: 'Descripción',
description: 'Introduzca reglas personalizadas para los informes de gastos',
},
},
},
getAssistancePage: {
Expand Down
6 changes: 6 additions & 0 deletions src/libs/API/parameters/UpdateCustomRules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type UpdateCustomRules = {
policyID: string;
description: string;
};

export default UpdateCustomRules;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ export type {default as EnablePolicyDefaultReportTitleParams} from './EnablePoli
export type {default as SetPolicyExpenseMaxAmountNoReceipt} from './SetPolicyExpenseMaxAmountNoReceipt';
export type {default as SetPolicyExpenseMaxAmount} from './SetPolicyExpenseMaxAmount';
export type {default as SetPolicyExpenseMaxAge} from './SetPolicyExpenseMaxAge';
export type {default as UpdateCustomRules} from './UpdateCustomRules';
export type {default as SetPolicyBillableModeParams} from './SetPolicyBillableModeParams';
export type {default as DisablePolicyBillableModeParams} from './DisablePolicyBillableModeParams';
export type {default as SetWorkspaceEReceiptsEnabled} from './SetWorkspaceEReceiptsEnabled';
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ const WRITE_COMMANDS = {
SET_POLICY_EXPENSE_MAX_AMOUNT_NO_RECEIPT: 'SetPolicyExpenseMaxAmountNoReceipt',
SET_POLICY_EXPENSE_MAX_AMOUNT: 'SetPolicyExpenseMaxAmount',
SET_POLICY_EXPENSE_MAX_AGE: ' SetPolicyExpenseMaxAge',
UPDATE_CUSTOM_RULES: 'UpdateCustomRules',
SET_POLICY_BILLABLE_MODE: ' SetPolicyBillableMode',
DISABLE_POLICY_BILLABLE_MODE: 'DisablePolicyBillableExpenses',
SET_WORKSPACE_ERECEIPTS_ENABLED: 'SetWorkspaceEReceiptsEnabled',
Expand Down Expand Up @@ -726,6 +727,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SET_POLICY_EXPENSE_MAX_AMOUNT_NO_RECEIPT]: Parameters.SetPolicyExpenseMaxAmountNoReceipt;
[WRITE_COMMANDS.SET_POLICY_EXPENSE_MAX_AMOUNT]: Parameters.SetPolicyExpenseMaxAmount;
[WRITE_COMMANDS.SET_POLICY_EXPENSE_MAX_AGE]: Parameters.SetPolicyExpenseMaxAge;
[WRITE_COMMANDS.UPDATE_CUSTOM_RULES]: Parameters.UpdateCustomRules;
[WRITE_COMMANDS.SET_POLICY_BILLABLE_MODE]: Parameters.SetPolicyBillableModeParams;
[WRITE_COMMANDS.DISABLE_POLICY_BILLABLE_MODE]: Parameters.DisablePolicyBillableModeParams;
[WRITE_COMMANDS.SET_WORKSPACE_ERECEIPTS_ENABLED]: Parameters.SetWorkspaceEReceiptsEnabled;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.RULES_MAX_EXPENSE_AMOUNT]: () => require<ReactComponentModule>('../../../../pages/workspace/rules/RulesMaxExpenseAmountPage').default,
[SCREENS.WORKSPACE.RULES_MAX_EXPENSE_AGE]: () => require<ReactComponentModule>('../../../../pages/workspace/rules/RulesMaxExpenseAgePage').default,
[SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT]: () => require<ReactComponentModule>('../../../../pages/workspace/rules/RulesBillableDefaultPage').default,
[SCREENS.WORKSPACE.RULES_CUSTOM]: () => require<ReactComponentModule>('../../../../pages/workspace/rules/RulesCustomPage').default,
[SCREENS.WORKSPACE.PER_DIEM_IMPORT]: () => require<ReactComponentModule>('../../../../pages/workspace/perDiem/ImportPerDiemPage').default,
[SCREENS.WORKSPACE.PER_DIEM_IMPORTED]: () => require<ReactComponentModule>('../../../../pages/workspace/perDiem/ImportedPerDiemPage').default,
[SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: () => require<ReactComponentModule>('../../../../pages/workspace/perDiem/WorkspacePerDiemSettingsPage').default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial<Record<FullScreenName, string[]>> = {
SCREENS.WORKSPACE.RULES_MAX_EXPENSE_AMOUNT,
SCREENS.WORKSPACE.RULES_MAX_EXPENSE_AGE,
SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT,
SCREENS.WORKSPACE.RULES_CUSTOM,
],
[SCREENS.WORKSPACE.PER_DIEM]: [
SCREENS.WORKSPACE.PER_DIEM_IMPORT,
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,9 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT]: {
path: ROUTES.RULES_BILLABLE_DEFAULT.route,
},
[SCREENS.WORKSPACE.RULES_CUSTOM]: {
path: ROUTES.RULES_CUSTOM.route,
},
[SCREENS.WORKSPACE.PER_DIEM_IMPORT]: {
path: ROUTES.WORKSPACE_PER_DIEM_IMPORT.route,
},
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT]: {
policyID: string;
};
[SCREENS.WORKSPACE.RULES_CUSTOM]: {
policyID: string;
};
[SCREENS.WORKSPACE.PER_DIEM_IMPORT]: {
policyID: string;
};
Expand Down
5 changes: 5 additions & 0 deletions src/libs/Permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ function canUseNSQS(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.NSQS) || canUseAllBetas(betas);
}

function canUseCustomRules(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.CUSTOM_RULES) || canUseAllBetas(betas);
}

export default {
canUseDefaultRooms,
canUseLinkPreviews,
Expand All @@ -60,4 +64,5 @@ export default {
canUseManagerMcTest,
canUseInternationalBankAccount,
canUseNSQS,
canUseCustomRules,
};
54 changes: 54 additions & 0 deletions src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3869,6 +3869,59 @@ function setPolicyMaxExpenseAge(policyID: string, maxExpenseAge: string) {
API.write(WRITE_COMMANDS.SET_POLICY_EXPENSE_MAX_AGE, parameters, onyxData);
}

/**
* Call the API to set the custom rules for the given policy
* @param policyID - id of the policy to set the max expense age
* @param customRules - the custom rules description in natural language
*/
function updateCustomRules(policyID: string, customRules: string) {
const policy = getPolicy(policyID);
const originalCustomRules = policy?.customRules;

const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
customRules,
},
},
],
successData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
pendingFields: {
// TODO
// maxExpenseAge: null,
},
},
},
],
failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
customRules: originalCustomRules,
// TODO
// pendingFields: {maxExpenseAge: null},
// errorFields: {maxExpenseAge: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')},
},
},
],
};

const parameters = {
policyID,
description: customRules,
};

API.write(WRITE_COMMANDS.UPDATE_CUSTOM_RULES, parameters, onyxData);
}

/**
* Call the API to enable or disable the billable mode for the given policy
* @param policyID - id of the policy to enable or disable the bilable mode
Expand Down Expand Up @@ -4918,6 +4971,7 @@ export {
setPolicyMaxExpenseAmountNoReceipt,
setPolicyMaxExpenseAmount,
setPolicyMaxExpenseAge,
updateCustomRules,
setPolicyBillableMode,
disableWorkspaceBillableExpenses,
setWorkspaceEReceiptsEnabled,
Expand Down
47 changes: 47 additions & 0 deletions src/pages/workspace/rules/CustomRulesSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import {View} from 'react-native';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import Section from '@components/Section';
import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import ROUTES from '@src/ROUTES';

type CustomRulesSectionProps = {
policyID: string;
};

function CustomRulesSection({policyID}: CustomRulesSectionProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const policy = usePolicy(policyID);

return (
<Section
isCentralPane
title={translate('workspace.rules.customRules.title')}
subtitle={translate('workspace.rules.customRules.description')}
titleStyles={styles.accountSettingsSectionTitle}
subtitleMuted
>
<View style={[styles.mt3]}>
{/* <OfflineWithFeedback
pendingAction={item.pendingAction}
key={translate(item.descriptionTranslationKey)}
> */}
<MenuItemWithTopDescription
shouldShowRightIcon
title={policy?.customRules ?? ''}
description={translate('workspace.rules.customRules.subtitle')}
onPress={() => Navigation.navigate(ROUTES.RULES_CUSTOM.getRoute(policyID))}
wrapperStyle={[styles.sectionMenuItemTopDescription]}
numberOfLinesTitle={2}
/>
{/* </OfflineWithFeedback> */}
</View>
</Section>
);
}

export default CustomRulesSection;
4 changes: 4 additions & 0 deletions src/pages/workspace/rules/PolicyRulesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
Expand All @@ -10,13 +11,15 @@ import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSection
import * as Illustrations from '@src/components/Icon/Illustrations';
import CONST from '@src/CONST';
import type SCREENS from '@src/SCREENS';
import CustomRulesSection from './CustomRulesSection';
import ExpenseReportRulesSection from './ExpenseReportRulesSection';
import IndividualExpenseRulesSection from './IndividualExpenseRulesSection';

type PolicyRulesPageProps = PlatformStackScreenProps<FullScreenNavigatorParamList, typeof SCREENS.WORKSPACE.RULES>;

function PolicyRulesPage({route}: PolicyRulesPageProps) {
const {translate} = useLocalize();
const {canUseCustomRules} = usePermissions();
const {policyID} = route.params;
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
Expand All @@ -41,6 +44,7 @@ function PolicyRulesPage({route}: PolicyRulesPageProps) {
<View style={[styles.mt3, shouldUseNarrowLayout ? styles.workspaceSectionMobile : styles.workspaceSection]}>
<IndividualExpenseRulesSection policyID={policyID} />
<ExpenseReportRulesSection policyID={policyID} />
{canUseCustomRules ? <CustomRulesSection policyID={policyID} /> : null}
</View>
</WorkspacePageWithSections>
</AccessOrNotFoundWrapper>
Expand Down
90 changes: 90 additions & 0 deletions src/pages/workspace/rules/RulesCustomPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, {useCallback, useState} from 'react';
import {View} from 'react-native';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import Parser from '@libs/Parser';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import {updateCustomRules} from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/RulesCustomForm';

type RulesCustomPageProps = PlatformStackScreenProps<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.RULES_CUSTOM>;

function RulesCustomPage({
route: {
params: {policyID},
},
}: RulesCustomPageProps) {
const policy = usePolicy(policyID);

const {inputCallbackRef} = useAutoFocusInput();
const {translate} = useLocalize();
const styles = useThemeStyles();

const [customRulesValue, setCustomRulesValue] = useState(() => Parser.htmlToMarkdown(policy?.customRules ?? ''));

const onChangeCustomRules = useCallback((newValue: string) => {
setCustomRulesValue(newValue);
}, []);

return (
<AccessOrNotFoundWrapper
policyID={policyID}
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
featureName={CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED}
>
<ScreenWrapper
includeSafeAreaPaddingBottom
shouldEnableMaxHeight
testID={RulesCustomPage.displayName}
>
<HeaderWithBackButton
title={translate('workspace.rules.customRules.title')}
onBackButtonPress={() => Navigation.goBack()}
/>
<FormProvider
style={[styles.flexGrow1, styles.ph5]}
formID={ONYXKEYS.FORMS.RULES_CUSTOM_FORM}
onSubmit={({customRules}) => {
updateCustomRules(policyID, customRules);
Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack);
}}
submitButtonText={translate('workspace.editor.save')}
enabledWhenOffline
>
<View style={styles.mb4}>
<InputWrapper
InputComponent={TextInput}
inputID={INPUT_IDS.CUSTOM_RULES}
label={translate('workspace.rules.customRules.subtitle')}
role={CONST.ROLE.PRESENTATION}
value={customRulesValue}
onChangeText={onChangeCustomRules}
ref={inputCallbackRef}
type="markdown"
autoGrowHeight
/>
<Text style={[styles.mutedTextLabel, styles.mt2]}>{translate('workspace.rules.customRules.description')}</Text>
</View>
</FormProvider>
</ScreenWrapper>
</AccessOrNotFoundWrapper>
);
}

RulesCustomPage.displayName = 'RulesCustomPage';

export default RulesCustomPage;
Loading

0 comments on commit b34e5f8

Please sign in to comment.