Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7153,6 +7153,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
syncError: (providerName: string) => `Verbindung zu ${providerName} nicht möglich`,
connectionDescription: (providerName: string) => `Verbinden Sie ${providerName}, um Mitarbeitergenehmigungen mit Ihrem Workspace zu synchronisieren.`,
approvalMode: 'Genehmigungsmodus',
providerApprovalMode: (providerName: string) => `${providerName}-Genehmigungsmodus`,
finalApprover: 'Endgültige:r Genehmiger:in',
notSet: 'Nicht festgelegt',
approvalModeDescription: (providerName: string) => `Mitglieder und Manager sind für die Synchronisation mit ${providerName} eingerichtet.`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6455,6 +6455,7 @@ const translations = {
syncError: (providerName: string) => `Can't connect to ${providerName}`,
connectionDescription: (providerName: string) => `Connect ${providerName} to keep employee approvals in sync with your workspace.`,
approvalMode: 'Approval mode',
providerApprovalMode: (providerName: string) => `${providerName} approval mode`,
finalApprover: 'Final approver',
notSet: 'Not set',
approvalModeDescription: (providerName: string) => `Members and managers are set up to sync with ${providerName}.`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6268,6 +6268,7 @@ ${amount} para ${merchant} - ${date}`,
syncError: (providerName: string) => `No se puede conectar con ${providerName}`,
connectionDescription: (providerName: string) => `Conecta ${providerName} para mantener sincronizadas las aprobaciones de empleados con tu espacio de trabajo.`,
approvalMode: 'Modo de aprobación',
providerApprovalMode: (providerName: string) => `Modo de aprobación de ${providerName}`,
finalApprover: 'Aprobador final',
notSet: 'No configurado',
approvalModeDescription: (providerName: string) => `Los miembros y gerentes están configurados para sincronizarse con ${providerName}.`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7176,6 +7176,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
syncError: (providerName: string) => `Impossible de se connecter à ${providerName}`,
connectionDescription: (providerName: string) => `Connectez ${providerName} pour synchroniser les approbations des employés avec votre espace de travail.`,
approvalMode: "Mode d'approbation",
providerApprovalMode: (providerName: string) => `Mode d'approbation ${providerName}`,
finalApprover: 'Approbateur final',
notSet: 'Non défini',
approvalModeDescription: (providerName: string) => `Les membres et les responsables sont configurés pour se synchroniser avec ${providerName}.`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7140,6 +7140,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
syncError: (providerName: string) => `Impossibile connettersi a ${providerName}`,
connectionDescription: (providerName: string) => `Collega ${providerName} per mantenere sincronizzate le approvazioni dei dipendenti con il tuo spazio di lavoro.`,
approvalMode: 'Modalità di approvazione',
providerApprovalMode: (providerName: string) => `Modalità di approvazione ${providerName}`,
finalApprover: 'Approvatore finale',
notSet: 'Non impostato',
approvalModeDescription: (providerName: string) => `I membri e i responsabili sono configurati per la sincronizzazione con ${providerName}.`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7059,6 +7059,7 @@ ${reportName}
syncError: (providerName: string) => `${providerName}に接続できません`,
connectionDescription: (providerName: string) => `${providerName}を接続して、従業員の承認をワークスペースと同期させましょう。`,
approvalMode: '承認モード',
providerApprovalMode: (providerName: string) => `${providerName}の承認モード`,
finalApprover: '最終承認者',
notSet: '未設定',
approvalModeDescription: (providerName: string) => `メンバーとマネージャーは ${providerName} と同期するように設定されています。`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7115,6 +7115,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
syncError: (providerName: string) => `Kan geen verbinding maken met ${providerName}`,
connectionDescription: (providerName: string) => `Verbind ${providerName} om goedkeuringen van werknemers gesynchroniseerd te houden met je werkruimte.`,
approvalMode: 'Goedkeuringsmodus',
providerApprovalMode: (providerName: string) => `${providerName}-goedkeuringsmodus`,
finalApprover: 'Eindgoedkeurder',
notSet: 'Niet ingesteld',
approvalModeDescription: (providerName: string) => `Leden en managers zijn ingesteld om te synchroniseren met ${providerName}.`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7110,6 +7110,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
syncError: (providerName: string) => `Nie można połączyć z ${providerName}`,
connectionDescription: (providerName: string) => `Połącz ${providerName}, aby synchronizować akceptacje pracowników z Twoim miejscem pracy.`,
approvalMode: 'Tryb zatwierdzania',
providerApprovalMode: (providerName: string) => `Tryb zatwierdzania ${providerName}`,
finalApprover: 'Ostateczny zatwierdzający',
notSet: 'Nie ustawiono',
approvalModeDescription: (providerName: string) => `Członkowie i menedżerowie są skonfigurowani do synchronizacji z ${providerName}.`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7115,6 +7115,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
syncError: (providerName: string) => `Não é possível conectar ao ${providerName}`,
connectionDescription: (providerName: string) => `Conecte ${providerName} para manter as aprovações de funcionários sincronizadas com seu workspace.`,
approvalMode: 'Modo de aprovação',
providerApprovalMode: (providerName: string) => `Modo de aprovação do ${providerName}`,
finalApprover: 'Aprovador final',
notSet: 'Não definido',
approvalModeDescription: (providerName: string) => `Membros e gerentes estão configurados para sincronizar com ${providerName}.`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6932,6 +6932,7 @@ ${reportName}
syncError: (providerName: string) => `无法连接到 ${providerName}`,
connectionDescription: (providerName: string) => `连接 ${providerName},以在您的工作区中同步员工审批。`,
approvalMode: '审批模式',
providerApprovalMode: (providerName: string) => `${providerName}审批模式`,
finalApprover: '最终审批人',
notSet: '未设置',
approvalModeDescription: (providerName: string) => `成员和管理员已设置为与 ${providerName} 同步。`,
Expand Down
4 changes: 2 additions & 2 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ const WRITE_COMMANDS = {
UPGRADE_TO_CORPORATE: 'UpgradeToCorporate',
UPDATE_GUSTO_APPROVAL_MODE: 'UpdateGustoApprovalMode',
UPDATE_GUSTO_FINAL_APPROVER: 'UpdateGustoFinalApprover',
UPDATE_MERGE_HR_APPROVAL_MODE: 'UpdateMergeHRApprovalMode',
UPDATE_MERGE_APPROVAL_MODE: 'UpdateMergeApprovalMode',
UPDATE_MERGE_HR_FINAL_APPROVER: 'UpdateMergeHRFinalApprover',
UPDATE_ZENEFITS_APPROVAL_MODE: 'UpdateZenefitsApprovalMode',
UPDATE_ZENEFITS_FINAL_APPROVER: 'UpdateZenefitsFinalApprover',
Expand Down Expand Up @@ -1020,7 +1020,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SET_PROMO_CODE]: Parameters.SetPromoCodeParams;
[WRITE_COMMANDS.UPDATE_GUSTO_APPROVAL_MODE]: Parameters.UpdateGustoApprovalModeParams;
[WRITE_COMMANDS.UPDATE_GUSTO_FINAL_APPROVER]: Parameters.UpdateGustoFinalApproverParams;
[WRITE_COMMANDS.UPDATE_MERGE_HR_APPROVAL_MODE]: Parameters.UpdateMergeHRApprovalModeParams;
[WRITE_COMMANDS.UPDATE_MERGE_APPROVAL_MODE]: Parameters.UpdateMergeHRApprovalModeParams;
[WRITE_COMMANDS.UPDATE_MERGE_HR_FINAL_APPROVER]: Parameters.UpdateMergeHRFinalApproverParams;
[WRITE_COMMANDS.UPDATE_ZENEFITS_APPROVAL_MODE]: Parameters.UpdateZenefitsApprovalModeParams;
[WRITE_COMMANDS.UPDATE_ZENEFITS_FINAL_APPROVER]: Parameters.UpdateZenefitsFinalApproverParams;
Expand Down
22 changes: 19 additions & 3 deletions src/libs/actions/connections/MergeHR.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type {TupleToUnion, ValueOf} from 'type-fest';
import {read, write} from '@libs/API';
import type {ConnectPolicyToMergeParams} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
Expand Down Expand Up @@ -106,7 +106,7 @@ function updateMergeHRApprovalMode(policyID: string, approvalMode: ValueOf<typeo
];

write(
WRITE_COMMANDS.UPDATE_MERGE_HR_APPROVAL_MODE,
WRITE_COMMANDS.UPDATE_MERGE_APPROVAL_MODE,
{
policyID,
approvalMode,
Expand Down Expand Up @@ -184,6 +184,22 @@ function updateMergeHRFinalApprover(policyID: string, finalApprover: string | nu
);
}

export {syncMergeHR, updateMergeHRApprovalMode, updateMergeHRFinalApprover};
type HRProviderName = TupleToUnion<typeof CONST.POLICY.CONNECTIONS.HR_CONNECTION_NAMES>;

function clearMergeHRErrorField(policyID?: string, provider?: HRProviderName) {
Copy link
Copy Markdown
Contributor

@bernhardoj bernhardoj May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead of making the params optional, we should update the type to be undefined-able.

Suggested change
function clearMergeHRErrorField(policyID?: string, provider?: HRProviderName) {
function clearMergeHRErrorField(policyID: string | undefined, provider: HRProviderName | undefined) {

This will prevent unintended bug like the on caught by AI here

if (!policyID || !provider) {
return;
}
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
connections: {
[provider]: {
config: {
errorFields: null,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This clears the whole errorFields. If we have both approvalMode and finalApprover errorFields, clearing one of them will clear both.

},
},
},
});
}
export {syncMergeHR, updateMergeHRApprovalMode, updateMergeHRFinalApprover, clearMergeHRErrorField};

export default getMergeHRSetupLink;
156 changes: 156 additions & 0 deletions src/pages/workspace/hr/HRApprovalModePageBase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React, {useState} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import Button from '@components/Button';
import FixedFooter from '@components/FixedFooter';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import {ModalActions} from '@components/Modal/Global/ModalContext';
import RenderHTML from '@components/RenderHTML';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
import type {ListItem} from '@components/SelectionList/types';
import Text from '@components/Text';
import useConfirmModal from '@hooks/useConfirmModal';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import usePolicy from '@hooks/usePolicy';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type Beta from '@src/types/onyx/Beta';
import type Policy from '@src/types/onyx/Policy';
import type {PolicyConnectionSyncProgress} from '@src/types/onyx/Policy';

type ApprovalModeValue = ValueOf<typeof CONST.GUSTO.APPROVAL_MODE> | ValueOf<typeof CONST.MERGE_HR.APPROVAL_MODE>;

type HRApprovalModeProviderConfig<T extends ApprovalModeValue = ApprovalModeValue> = {
testID: string;
beta: Beta;
isConnected: (policy: OnyxEntry<Policy>) => boolean;
approvalModes: {BASIC: T; MANAGER: T; CUSTOM: T};
getCurrentApprovalMode: (policy: OnyxEntry<Policy>) => T | null;
getProviderName: (policy: OnyxEntry<Policy>) => string;
getHeaderTitle: (providerName: string) => string;
handleSave: (params: {policyID: string; draftApprovalMode: T; currentApprovalMode: T | null; connectionSyncProgress?: OnyxEntry<PolicyConnectionSyncProgress>}) => void;
};

type ApprovalModeListItem<T extends ApprovalModeValue = ApprovalModeValue> = ListItem & {
value: T;
};

type HRApprovalModePageBaseProps<T extends ApprovalModeValue = ApprovalModeValue> = {
policyID: string;
config: HRApprovalModeProviderConfig<T>;
};

function HRApprovalModePageBase<T extends ApprovalModeValue>({policyID, config}: HRApprovalModePageBaseProps<T>) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {showConfirmModal} = useConfirmModal();
const {isBetaEnabled} = usePermissions();
const policy = usePolicy(policyID);
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`);

const providerName = config.getProviderName(policy);
const currentApprovalMode = config.getCurrentApprovalMode(policy);
const [draftApprovalMode, setDraftApprovalMode] = useState<T | undefined>();
const selectedApprovalMode = draftApprovalMode ?? currentApprovalMode;
const isSaveDisabled = !draftApprovalMode || draftApprovalMode === currentApprovalMode;

const approvalModeOptions: Array<ApprovalModeListItem<T>> = [
{
text: translate('workspace.hr.approvalModes.basic.label'),
alternateText: translate('workspace.hr.approvalModes.basic.description'),
keyForList: config.approvalModes.BASIC,
value: config.approvalModes.BASIC,
isSelected: selectedApprovalMode === config.approvalModes.BASIC,
},
{
text: translate('workspace.hr.approvalModes.manager.label'),
alternateText: translate('workspace.hr.approvalModes.manager.description', providerName),
keyForList: config.approvalModes.MANAGER,
value: config.approvalModes.MANAGER,
isSelected: selectedApprovalMode === config.approvalModes.MANAGER,
},
{
text: translate('workspace.hr.approvalModes.custom.label'),
alternateText: translate('workspace.hr.approvalModes.custom.description'),
keyForList: config.approvalModes.CUSTOM,
value: config.approvalModes.CUSTOM,
isSelected: selectedApprovalMode === config.approvalModes.CUSTOM,
},
];
const selectedApprovalModeKey = approvalModeOptions.find((option) => option.isSelected)?.keyForList;

const saveApprovalMode = () => {
if (!draftApprovalMode) {
return;
}

config.handleSave({policyID, draftApprovalMode, currentApprovalMode, connectionSyncProgress});
Navigation.goBack();
};

const confirmSaveApprovalMode = () => {
showConfirmModal({
title: translate('workspace.hr.approvalModeWarningTitle'),
prompt: <RenderHTML html={translate('workspace.hr.approvalModeWarningPrompt', providerName, CONST.CONFIGURE_APPROVAL_WORKFLOWS_HELP_URL)} />,
confirmText: translate('workspace.hr.approvalModeWarningConfirm'),
cancelText: translate('common.cancel'),
}).then((result) => {
if (result?.action !== ModalActions.CONFIRM) {
return;
}
saveApprovalMode();
});
};

return (
<AccessOrNotFoundWrapper
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED}
shouldBeBlocked={!isBetaEnabled(config.beta) || (!!policy && !config.isConnected(policy))}
>
<ScreenWrapper
enableEdgeToEdgeBottomSafeAreaPadding
shouldEnableMaxHeight
testID={config.testID}
>
<HeaderWithBackButton
title={config.getHeaderTitle(providerName)}
onBackButtonPress={() => Navigation.goBack()}
/>
<View style={styles.flex1}>
<Text style={[styles.textSupporting, styles.ph5, styles.mt3, styles.mb3]}>{translate('workspace.hr.approvalModeDescription', providerName)}</Text>
<SelectionList
data={approvalModeOptions}
ListItem={SingleSelectListItem}
onSelectRow={(option) => setDraftApprovalMode(option.value)}
shouldSingleExecuteRowSelect
initiallyFocusedItemKey={selectedApprovalModeKey}
alternateNumberOfSupportedLines={3}
showScrollIndicator={false}
/>
<FixedFooter style={styles.mtAuto}>
<Button
large
success
text={translate('common.save')}
onPress={confirmSaveApprovalMode}
isDisabled={isSaveDisabled}
/>
</FixedFooter>
</View>
</ScreenWrapper>
</AccessOrNotFoundWrapper>
);
}

export default HRApprovalModePageBase;
export type {HRApprovalModeProviderConfig};
7 changes: 6 additions & 1 deletion src/pages/workspace/hr/HRProviderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import {removePolicyConnection, syncConnection} from '@libs/actions/connections';
import {clearMergeHRErrorField} from '@libs/actions/connections/MergeHR';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import type Policy from '@src/types/onyx/Policy';
Expand Down Expand Up @@ -133,7 +134,11 @@ function HRProviderCard({card, policy, handleConnect}: HRProviderCardProps) {
fallbackIcon={fallbackIcon}
/>
{card.isConnected && !!approvalModeRoute && (
<OfflineWithFeedback pendingAction={card.config?.pendingFields?.approvalMode}>
<OfflineWithFeedback
pendingAction={card.config?.pendingFields?.approvalMode}
errors={card.config?.errorFields?.approvalMode}
onClose={() => clearMergeHRErrorField(policy?.id)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-6 (docs)

The clearMergeHRErrorField function requires both policyID and provider parameters, but only policy?.id is passed here. Since provider will be undefined, the function will early-return without clearing the error, making the onClose callback a silent no-op.

Pass the provider name as the second argument. The card object likely has a provider identifier available:

onClose={() => clearMergeHRErrorField(policy?.id, card.providerName)}

Reviewed at: d0b9302 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmusial this one is always early returning

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass provider when clearing approval mode errors

onClose calls clearMergeHRErrorField(policy?.id) without a provider, but clearMergeHRErrorField now returns early unless both policyID and provider are present. In the approval-mode error state, dismissing the OfflineWithFeedback error will therefore no-op and the error remains stuck in Onyx/UI. This affects any HR approval mode update failure where the user tries to clear the error.

Useful? React with 👍 / 👎.

Comment thread
jmusial marked this conversation as resolved.
Outdated
>
<MenuItemWithTopDescription
description={translate('workspace.hr.approvalMode')}
title={card.approvalModeLabel}
Expand Down
Loading
Loading