From a853cf681ea848b4bfb3ec2a15b877d55b7bed87 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 6 Oct 2026 12:45:54 +0800 Subject: [PATCH 01/11] add ashby specific approver field --- src/CONST/MERGE_ATS_PROVIDERS.ts | 15 +++++- src/CONST/index.ts | 1 + src/languages/en.ts | 1 + src/libs/merge/RecruitingUtils.ts | 10 ++++ .../approver/MergeATSApproverFieldPage.tsx | 19 +++---- src/pages/workspace/recruiting/utils.ts | 2 + tests/unit/RecruitingUtilsTest.ts | 54 +++++++++++++++++++ 7 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/CONST/MERGE_ATS_PROVIDERS.ts b/src/CONST/MERGE_ATS_PROVIDERS.ts index 06f1eae4302d..e0b5c61c03da 100644 --- a/src/CONST/MERGE_ATS_PROVIDERS.ts +++ b/src/CONST/MERGE_ATS_PROVIDERS.ts @@ -1,3 +1,7 @@ +import type {ValueOf} from 'type-fest'; + +import CONST from '.'; + // The provider slug keys below are dictated by Merge's API. type MergeATSProviderEntry = { /** Human-readable label used in the UI */ @@ -5,13 +9,22 @@ type MergeATSProviderEntry = { /** Provider logo served from the Merge CDN */ iconUrl: string; + + /** The ATS fields the provider exposes that a candidate's default approver can be read from, in display order */ + approverFields: ReadonlyArray>; }; -// Merge supports many more ATS providers than the one below. Add them here as they are rolled out. +// Merge supports many more ATS providers than the ones below. Add them here as they are rolled out. const MERGE_ATS_PROVIDERS = { + ashby: { + displayName: 'Ashby', + iconUrl: 'https://merge-api-public.s3.amazonaws.com/media/Ashby_Square_Logo.png', + approverFields: [CONST.MERGE.ATS_APPROVER_FIELD.RECRUITER, CONST.MERGE.ATS_APPROVER_FIELD.HIRING_MANAGER, CONST.MERGE.ATS_APPROVER_FIELD.RECRUITING_COORDINATOR], + }, greenhouse: { displayName: 'Greenhouse', iconUrl: 'https://merge-api-public.s3.amazonaws.com/media/Greenhouse_Square_Logo.jpg', + approverFields: [CONST.MERGE.ATS_APPROVER_FIELD.RECRUITER, CONST.MERGE.ATS_APPROVER_FIELD.RECRUITING_COORDINATOR], }, } as const satisfies Record; diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 69d557570e35..ae2f1abb06c8 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3439,6 +3439,7 @@ const CONST = { }, ATS_APPROVER_FIELD: { RECRUITER: 'recruiter', + HIRING_MANAGER: 'hiringManager', RECRUITING_COORDINATOR: 'coordinator', }, ATS_FILTER_TYPE: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 61ab6e68a69f..241fe1e3cee0 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7951,6 +7951,7 @@ const translations = { }, approverFields: { recruiter: 'Recruiter', + hiringManager: 'Hiring manager', recruitingCoordinator: 'Recruiting coordinator', }, filters: { diff --git a/src/libs/merge/RecruitingUtils.ts b/src/libs/merge/RecruitingUtils.ts index 5733af175942..1462e53d745b 100644 --- a/src/libs/merge/RecruitingUtils.ts +++ b/src/libs/merge/RecruitingUtils.ts @@ -153,6 +153,15 @@ function getMergeATSApproverField(policy: OnyxEntry): MergeATSApproverFi return policy?.connections?.merge_ats?.config?.approverField ?? undefined; } +/** + * Returns the ATS fields the connected provider supports as the source of a candidate's default approver. + */ +function getMergeATSApproverFields(policy: OnyxEntry): ReadonlyArray { + const slug = getConnectedATSProvider(policy)?.mergeSlug; + const approverFields = slug ? MERGE_ATS_PROVIDERS[slug]?.approverFields : undefined; + return approverFields ?? []; +} + /** Checks if the recruiting connection on the policy is in an error state the admin needs to resolve. */ function shouldShowRecruitingConnectionError(policy: OnyxEntry, isAdmin: boolean): boolean { if (!isAdmin || !isMergeConnected(policy, CONST.POLICY.CONNECTIONS.NAME.MERGE_ATS)) { @@ -165,6 +174,7 @@ export { getConnectedATSProvider, getMergeATSApprovalMode, getMergeATSApproverField, + getMergeATSApproverFields, getMergeATSFilterLabel, getMergeATSFilterOptions, getMergeATSFilterValues, diff --git a/src/pages/workspace/recruiting/approver/MergeATSApproverFieldPage.tsx b/src/pages/workspace/recruiting/approver/MergeATSApproverFieldPage.tsx index dc4ce3fc0893..de4b66044824 100644 --- a/src/pages/workspace/recruiting/approver/MergeATSApproverFieldPage.tsx +++ b/src/pages/workspace/recruiting/approver/MergeATSApproverFieldPage.tsx @@ -11,7 +11,7 @@ import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import {isMergeConnected} from '@libs/merge/MergeUtils'; -import {getConnectedATSProvider} from '@libs/merge/RecruitingUtils'; +import {getConnectedATSProvider, getMergeATSApproverFields} from '@libs/merge/RecruitingUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {MergeATSApprovalNavigatorParamList} from '@libs/Navigation/types'; @@ -22,7 +22,6 @@ import {getApproverFieldName} from '@pages/workspace/recruiting/utils'; import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; import type {MergeATSApproverField} from '@src/types/onyx/Policy'; -import ObjectUtils from '@src/types/utils/ObjectUtils'; import React from 'react'; import {View} from 'react-native'; @@ -49,16 +48,12 @@ function MergeATSApproverFieldPage({ const {approverField: currentApproverField} = useMergeATSApprovalDraftState(policyID); const {setDraftApproverField} = useMergeATSApprovalDraftActions(); - const approverFieldOptions: ApproverFieldListItem[] = ObjectUtils.typedKeys(CONST.MERGE.ATS_APPROVER_FIELD).map((key) => { - const value = CONST.MERGE.ATS_APPROVER_FIELD[key]; - - return { - text: getApproverFieldName(value, translate), - keyForList: value, - value, - isSelected: currentApproverField === value, - }; - }); + const approverFieldOptions: ApproverFieldListItem[] = getMergeATSApproverFields(policy).map((value) => ({ + text: getApproverFieldName(value, translate), + keyForList: value, + value, + isSelected: currentApproverField === value, + })); const selectApproverField = (approverField: MergeATSApproverField) => { setDraftApproverField(approverField); diff --git a/src/pages/workspace/recruiting/utils.ts b/src/pages/workspace/recruiting/utils.ts index 842312fbe5b8..e57a3f8416f6 100644 --- a/src/pages/workspace/recruiting/utils.ts +++ b/src/pages/workspace/recruiting/utils.ts @@ -31,6 +31,8 @@ function getApproverFieldName(approverField: MergeATSApproverField | undefined, switch (approverField) { case CONST.MERGE.ATS_APPROVER_FIELD.RECRUITER: return translate('workspace.recruiting.approverFields.recruiter'); + case CONST.MERGE.ATS_APPROVER_FIELD.HIRING_MANAGER: + return translate('workspace.recruiting.approverFields.hiringManager'); case CONST.MERGE.ATS_APPROVER_FIELD.RECRUITING_COORDINATOR: return translate('workspace.recruiting.approverFields.recruitingCoordinator'); default: diff --git a/tests/unit/RecruitingUtilsTest.ts b/tests/unit/RecruitingUtilsTest.ts index 58efa26abab6..455e862020fb 100644 --- a/tests/unit/RecruitingUtilsTest.ts +++ b/tests/unit/RecruitingUtilsTest.ts @@ -3,6 +3,7 @@ import { getConnectedATSProvider, getMergeATSApprovalMode, getMergeATSApproverField, + getMergeATSApproverFields, getMergeATSFilterLabel, getMergeATSFilterOptions, isAnyRecruitingConnected, @@ -36,6 +37,7 @@ const {TAGS, STAGES, OFFICES} = CONST.MERGE.ATS_FILTER_TYPE; const POLICY_ID = 'ABC123'; const GREENHOUSE: MergeATSProviderSlug = 'greenhouse'; +const ASHBY: MergeATSProviderSlug = 'ashby'; const STUB_ICON: IconAsset = {uri: 'stub'}; const APPROVER_LOGIN = 'approver@test.com'; const ERROR_TIMESTAMP = 123; @@ -441,6 +443,40 @@ describe('RecruitingUtils', () => { }); }); + describe('getMergeATSApproverFields', () => { + it('offers only the recruiter and recruiting coordinator for Greenhouse', () => { + // Given a policy connected to Greenhouse, which has no hiring manager field + const policy = makeMergeATSPolicy({config: {integration: GREENHOUSE}}); + + // When the approver fields are read + // Then only the fields Greenhouse supports are offered + expect(getMergeATSApproverFields(policy)).toEqual([CONST.MERGE.ATS_APPROVER_FIELD.RECRUITER, CONST.MERGE.ATS_APPROVER_FIELD.RECRUITING_COORDINATOR]); + }); + + it('also offers the hiring manager for Ashby', () => { + // Given a policy connected to Ashby, which exposes a hiring manager field + const policy = makeMergeATSPolicy({config: {integration: ASHBY}}); + + // When the approver fields are read + // Then the hiring manager is offered between the recruiter and the recruiting coordinator + expect(getMergeATSApproverFields(policy)).toEqual([ + CONST.MERGE.ATS_APPROVER_FIELD.RECRUITER, + CONST.MERGE.ATS_APPROVER_FIELD.HIRING_MANAGER, + CONST.MERGE.ATS_APPROVER_FIELD.RECRUITING_COORDINATOR, + ]); + }); + + it('falls back to the fields every provider supports when the provider is unknown', () => { + // Given a connection whose integration slug we can't resolve, since the backend sets it + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- deliberately simulating a slug we cannot resolve + const policy = makeMergeATSPolicy({config: {integration: undefined as unknown as MergeATSProviderSlug}}); + + // When the approver fields are read + // Then only the fields every provider supports are offered, so the admin can't pick one the ATS lacks + expect(getMergeATSApproverFields(policy)).toEqual([CONST.MERGE.ATS_APPROVER_FIELD.RECRUITER, CONST.MERGE.ATS_APPROVER_FIELD.RECRUITING_COORDINATOR]); + }); + }); + describe('shouldShowRecruitingConnectionError', () => { it('returns false when the user is not an admin', () => { const policy = makeMergeATSPolicy({ @@ -738,6 +774,24 @@ describe('getRecruitingCards', () => { ).toBe(`workspace.merge.approvalModes.advanced • workspace.recruiting.approverFields.recruitingCoordinator -> ${APPROVER_LOGIN}`); }); + it('translates the hiring manager field on the Ashby card in advanced mode', () => { + // Given an Ashby connection in advanced mode reading the approver from the hiring manager field + const policy = makeMergeATSPolicy({ + config: { + integration: ASHBY, + approvalMode: CONST.MERGE.APPROVAL_MODE.ADVANCED, + approverField: CONST.MERGE.ATS_APPROVER_FIELD.HIRING_MANAGER, + finalApprover: APPROVER_LOGIN, + }, + }); + + // When the Ashby card is built + const card = getRecruitingCards(makeGetRecruitingCardsParams({policy})).find((c) => c.key === `merge_ats_${ASHBY}`); + + // Then its default approver row names the hiring manager field + expect(getRow(card, 'approvalMode')?.title).toBe(`workspace.merge.approvalModes.advanced • workspace.recruiting.approverFields.hiringManager -> ${APPROVER_LOGIN}`); + }); + it('reads "not set" for the ATS field when advanced mode has none', () => { // Given a connection in advanced mode with no approver field chosen // When the default approver row is built From bf15931f782a3d1da25f09c862d692dcdaf89be0 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 17 Oct 2026 16:54:45 +0800 Subject: [PATCH 02/11] fix test --- tests/unit/RecruitingUtilsTest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/RecruitingUtilsTest.ts b/tests/unit/RecruitingUtilsTest.ts index 455e862020fb..8634a8efe965 100644 --- a/tests/unit/RecruitingUtilsTest.ts +++ b/tests/unit/RecruitingUtilsTest.ts @@ -466,14 +466,14 @@ describe('RecruitingUtils', () => { ]); }); - it('falls back to the fields every provider supports when the provider is unknown', () => { + it('offers nothing when the provider is unknown', () => { // Given a connection whose integration slug we can't resolve, since the backend sets it // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- deliberately simulating a slug we cannot resolve const policy = makeMergeATSPolicy({config: {integration: undefined as unknown as MergeATSProviderSlug}}); // When the approver fields are read - // Then only the fields every provider supports are offered, so the admin can't pick one the ATS lacks - expect(getMergeATSApproverFields(policy)).toEqual([CONST.MERGE.ATS_APPROVER_FIELD.RECRUITER, CONST.MERGE.ATS_APPROVER_FIELD.RECRUITING_COORDINATOR]); + // Then no fields are offered + expect(getMergeATSApproverFields(policy)).toEqual([]); }); }); From 0635b37537addc0673007df76283c763e42cb67d Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 17 Oct 2026 16:54:53 +0800 Subject: [PATCH 03/11] lint --- src/libs/merge/RecruitingUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/merge/RecruitingUtils.ts b/src/libs/merge/RecruitingUtils.ts index 1462e53d745b..8992bc7d9e0e 100644 --- a/src/libs/merge/RecruitingUtils.ts +++ b/src/libs/merge/RecruitingUtils.ts @@ -156,7 +156,7 @@ function getMergeATSApproverField(policy: OnyxEntry): MergeATSApproverFi /** * Returns the ATS fields the connected provider supports as the source of a candidate's default approver. */ -function getMergeATSApproverFields(policy: OnyxEntry): ReadonlyArray { +function getMergeATSApproverFields(policy: OnyxEntry): readonly MergeATSApproverField[] { const slug = getConnectedATSProvider(policy)?.mergeSlug; const approverFields = slug ? MERGE_ATS_PROVIDERS[slug]?.approverFields : undefined; return approverFields ?? []; From 361ce199fea71c8dd27e15dcedbe55301351453e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 19 Oct 2026 12:52:36 +0800 Subject: [PATCH 04/11] add translations --- src/languages/de.ts | 2 +- src/languages/el.ts | 2 +- src/languages/es.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 2 +- src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 5dd7a081fe84..c1728a1b37ad 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -9037,7 +9037,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc dontSeeYourATS: `Sie sehen Ihr ATS hier nicht? Fragen Sie Concierge, und wir können es hinzufügen.`, importSettings: 'Import-Einstellungen', defaultApprover: 'Standardgenehmiger', - approverFields: {recruiter: 'Personalvermittler', recruitingCoordinator: 'Recruiting-Koordinator'}, + approverFields: {recruiter: 'Personalvermittler', recruitingCoordinator: 'Recruiting-Koordinator', hiringManager: 'Einstellende Führungskraft'}, filters: { description: (providerName: string) => `Wählen Sie aus, welche Mitglieder aus ${providerName} importiert werden. Sie können nach Jobphasen, Tags und Büros auswählen.`, stages: { diff --git a/src/languages/el.ts b/src/languages/el.ts index 3a0a2f2c3a30..a05ae477e089 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -7948,7 +7948,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ dontSeeYourATS: `Δεν βλέπετε εδώ το ATS σας; Ρωτήστε το Concierge και μπορούμε να το προσθέσουμε.`, importSettings: 'Ρυθμίσεις εισαγωγής', defaultApprover: 'Προεπιλεγμένος εγκρίνων', - approverFields: {recruiter: 'Στρατολογητής', recruitingCoordinator: 'Συντονιστής προσλήψεων'}, + approverFields: {recruiter: 'Στρατολογητής', recruitingCoordinator: 'Συντονιστής προσλήψεων', hiringManager: 'Υπεύθυνος προσλήψεων'}, filters: { description: (providerName: string) => `Επιλέξτε ποια μέλη θα εισαχθούν από το ${providerName}. Μπορείτε να επιλέξετε από στάδια εργασίας, ετικέτες και γραφεία.`, stages: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 1255ad655989..c5aaf863f969 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7677,7 +7677,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, dontSeeYourATS: `¿No ves tu ATS aquí? Pregunta a Concierge y podremos añadirlo.`, importSettings: 'Importar ajustes', defaultApprover: 'Aprobador predeterminado', - approverFields: {recruiter: 'Reclutador', recruitingCoordinator: 'Coordinador de selección'}, + approverFields: {recruiter: 'Reclutador', recruitingCoordinator: 'Coordinador de selección', hiringManager: 'responsable de contratación'}, filters: { description: (providerName: string) => `Selecciona qué miembros se importan desde ${providerName}. Puedes elegir entre etapas de trabajo, etiquetas y oficinas.`, stages: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index c629e2bd4b0e..e710558d16e4 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -9056,7 +9056,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e dontSeeYourATS: `Vous ne voyez pas votre ATS ici ? Demandez à Concierge et nous pourrons l’ajouter.`, importSettings: 'Paramètres d’importation', defaultApprover: 'Approbateur par défaut', - approverFields: {recruiter: 'Recruteur', recruitingCoordinator: 'Coordinateur recrutement'}, + approverFields: {recruiter: 'Recruteur', recruitingCoordinator: 'Coordinateur recrutement', hiringManager: 'Responsable du recrutement'}, filters: { description: (providerName: string) => `Sélectionnez quels membres sont importés depuis ${providerName}. Vous pouvez choisir selon les étapes de poste, les tags et les bureaux.`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 9db5fb5ac92f..403707a86249 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -8980,7 +8980,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, dontSeeYourATS: `Non vedi il tuo ATS qui? Chiedi a Concierge e possiamo aggiungerlo.`, importSettings: 'Impostazioni di importazione', defaultApprover: 'Approvatore predefinito', - approverFields: {recruiter: 'Recruiter', recruitingCoordinator: 'Coordinatore recruiting'}, + approverFields: {recruiter: 'Recruiter', recruitingCoordinator: 'Coordinatore recruiting', hiringManager: 'Responsabile assunzioni'}, filters: { description: (providerName: string) => `Seleziona quali membri importare da ${providerName}. Puoi scegliere in base alle fasi lavorative, ai tag e alle sedi.`, stages: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index d412979fa9bc..8428f9bc4a90 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -8866,7 +8866,7 @@ ${reportName}`, dontSeeYourATS: `ご利用のATSが見つかりませんか?Concierge にお問い合わせいただければ、追加できます。`, importSettings: 'インポート設定', defaultApprover: 'デフォルト承認者', - approverFields: {recruiter: '採用担当者', recruitingCoordinator: '採用コーディネーター'}, + approverFields: {recruiter: '採用担当者', recruitingCoordinator: '採用コーディネーター', hiringManager: '採用マネージャー'}, filters: { description: (providerName: string) => `${providerName} からインポートするメンバーを選択してください。採用ステージ、タグ、オフィスから選択できます。`, stages: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 60718020161a..cf247a9462cc 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -8958,7 +8958,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, dontSeeYourATS: `Zie je je ATS hier niet? Vraag het aan Concierge en we kunnen het toevoegen.`, importSettings: 'Importinstellingen', defaultApprover: 'Standaardgoedkeurder', - approverFields: {recruiter: 'Recruiter', recruitingCoordinator: 'Coördinator werving'}, + approverFields: {recruiter: 'Recruiter', recruitingCoordinator: 'Coördinator werving', hiringManager: 'Wervingsmanager'}, filters: { description: (providerName: string) => `Selecteer welke leden worden geïmporteerd uit ${providerName}. Je kunt kiezen op basis van functieniveaus, tags en kantoren.`, stages: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 9792f3bfe001..b5e37be98ee1 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -8979,7 +8979,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, dontSeeYourATS: `Nie widzisz tutaj swojego ATS? Zapytaj Concierge, a my możemy go dodać.`, importSettings: 'Ustawienia importu', defaultApprover: 'Domyślny zatwierdzający', - approverFields: {recruiter: 'Rekruter', recruitingCoordinator: 'Koordynator rekrutacji'}, + approverFields: {recruiter: 'Rekruter', recruitingCoordinator: 'Koordynator rekrutacji', hiringManager: 'Rekruter'}, filters: { description: (providerName: string) => `Wybierz, którzy członkowie zostaną zaimportowani z ${providerName}. Możesz wybierać spośród etapów rekrutacji, tagów i biur.`, stages: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index ca2cb4c94ba2..9e3ea7068b6f 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -8961,7 +8961,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, dontSeeYourATS: `Não está vendo seu ATS aqui? Pergunte ao Concierge e podemos adicioná-lo.`, importSettings: 'Importar configurações', defaultApprover: 'Aprovador padrão', - approverFields: {recruiter: 'Recrutador', recruitingCoordinator: 'Coordenador de recrutamento'}, + approverFields: {recruiter: 'Recrutador', recruitingCoordinator: 'Coordenador de recrutamento', hiringManager: 'Gerente de contratação'}, filters: { description: (providerName: string) => `Selecione quais membros serão importados de ${providerName}. Você pode escolher por etapas do trabalho, tags e escritórios.`, stages: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 44a092b96b81..9d7454b29cdf 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -8639,7 +8639,7 @@ ${reportName}`, dontSeeYourATS: `没有看到你的 ATS?询问 Concierge,我们可以帮你添加。`, importSettings: '导入设置', defaultApprover: '默认审批人', - approverFields: {recruiter: '招聘人员', recruitingCoordinator: '招聘协调员'}, + approverFields: {recruiter: '招聘人员', recruitingCoordinator: '招聘协调员', hiringManager: '招聘经理'}, filters: { description: (providerName: string) => `选择要从 ${providerName} 导入的成员。您可以按工作阶段、标签和办公室进行选择。`, stages: { From f93cef4e6c5ba48f5dbdc76d47272d7c72ba1e40 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 19 Oct 2026 17:18:05 +0800 Subject: [PATCH 05/11] update first approver subtitle --- src/languages/en.ts | 7 ++++++- .../recruiting/approver/MergeATSApproverFieldPage.tsx | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index f45c9ec6c880..da90d2f591dd 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -8223,7 +8223,12 @@ const translations = { finalApprover: `Final approver`, finalApproverOptional: 'Final approver (optional)', approvalModeDescription: (providerName: string) => `Set the approver for new members being imported from ${providerName} to Expensify.`, - approverFieldDescription: (providerName: string) => `Choose the first approver for your candidates: either their Recruiter or their Coordinator assigned in ${providerName}.`, + approverFieldDescription: (providerName: string, approverFields: string[]) => { + const fieldNames = approverFields.map((approverField) => approverField.toLowerCase()); + const lastFieldName = fieldNames.at(-1) ?? ''; + const fieldList = fieldNames.length > 2 ? `their ${fieldNames.slice(0, -1).join(', ')}, or ${lastFieldName}` : `either their ${fieldNames.join(' or their ')}`; + return `Choose the first approver for your candidates: ${fieldList} assigned in ${providerName}.`; + }, approvalModeDescriptions: { basic: 'Choose a single approver', advanced: `Candidate's recruiter or coordinator becomes their expense approver`, diff --git a/src/pages/workspace/recruiting/approver/MergeATSApproverFieldPage.tsx b/src/pages/workspace/recruiting/approver/MergeATSApproverFieldPage.tsx index de4b66044824..9d40c65fa9bd 100644 --- a/src/pages/workspace/recruiting/approver/MergeATSApproverFieldPage.tsx +++ b/src/pages/workspace/recruiting/approver/MergeATSApproverFieldPage.tsx @@ -55,6 +55,8 @@ function MergeATSApproverFieldPage({ isSelected: currentApproverField === value, })); + const approverFieldNames = approverFieldOptions.map((option) => option.text).filter((text): text is string => !!text); + const selectApproverField = (approverField: MergeATSApproverField) => { setDraftApproverField(approverField); Navigation.goBack(); @@ -73,7 +75,7 @@ function MergeATSApproverFieldPage({ > - {translate('workspace.recruiting.approverFieldDescription', providerName)} + {translate('workspace.recruiting.approverFieldDescription', providerName, approverFieldNames)} Date: Mon, 19 Oct 2026 17:18:53 +0800 Subject: [PATCH 06/11] update comment --- src/libs/merge/RecruitingUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/merge/RecruitingUtils.ts b/src/libs/merge/RecruitingUtils.ts index 8992bc7d9e0e..e2805e2350b3 100644 --- a/src/libs/merge/RecruitingUtils.ts +++ b/src/libs/merge/RecruitingUtils.ts @@ -154,7 +154,7 @@ function getMergeATSApproverField(policy: OnyxEntry): MergeATSApproverFi } /** - * Returns the ATS fields the connected provider supports as the source of a candidate's default approver. + * Returns the ATS approver fields the connected provider supports as the source of a candidate's default approver. */ function getMergeATSApproverFields(policy: OnyxEntry): readonly MergeATSApproverField[] { const slug = getConnectedATSProvider(policy)?.mergeSlug; From 46173933382725b83d64c093596e473639242533 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 20 Oct 2026 09:08:12 +0800 Subject: [PATCH 07/11] add translation --- src/languages/de.ts | 10 +++++++--- src/languages/el.ts | 8 ++++++-- src/languages/es.ts | 7 ++++++- src/languages/fr.ts | 8 ++++++-- src/languages/it.ts | 10 +++++++--- src/languages/ja.ts | 8 ++++++-- src/languages/nl.ts | 7 ++++++- src/languages/pl.ts | 7 ++++++- src/languages/pt-BR.ts | 7 ++++++- src/languages/zh-hans.ts | 7 ++++++- 10 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index c1728a1b37ad..28d4ec3f79d4 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -9037,7 +9037,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc dontSeeYourATS: `Sie sehen Ihr ATS hier nicht? Fragen Sie Concierge, und wir können es hinzufügen.`, importSettings: 'Import-Einstellungen', defaultApprover: 'Standardgenehmiger', - approverFields: {recruiter: 'Personalvermittler', recruitingCoordinator: 'Recruiting-Koordinator', hiringManager: 'Einstellende Führungskraft'}, + approverFields: {recruiter: 'Personalvermittler', recruitingCoordinator: 'Recruiting-Koordinator', hiringManager: 'Einstellungsmanager'}, filters: { description: (providerName: string) => `Wählen Sie aus, welche Mitglieder aus ${providerName} importiert werden. Sie können nach Jobphasen, Tags und Büros auswählen.`, stages: { @@ -9071,8 +9071,12 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc finalApprover: `Endgültige genehmigende Person`, finalApproverOptional: 'Endgültige Freigabe (optional)', approvalModeDescription: (providerName: string) => `Legen Sie die genehmigende Person für neue Mitglieder fest, die von ${providerName} nach Expensify importiert werden.`, - approverFieldDescription: (providerName: string) => - `Wählen Sie die erste approver:ent für Ihre Kandidat:innen: entweder ihre:n Recruiter:in oder ihre:n Koordinator:in, die bzw. der in ${providerName} zugewiesen ist.`, + approverFieldDescription: (providerName: string, approverFields: string[]) => { + const fieldNames = approverFields.map((approverField) => approverField.toLowerCase()); + const lastFieldName = fieldNames.at(-1) ?? ''; + const fieldList = fieldNames.length > 2 ? `ihre*n ${fieldNames.slice(0, -1).join(', ')} oder ${lastFieldName}` : `entweder ihre ${fieldNames.join('oder deren')}`; + return `Wählen Sie die erste genehmigende Person für Ihre Kandidat:innen: ${fieldList}, zugewiesen in ${providerName}.`; + }, approvalModeDescriptions: { basic: 'Wählen Sie eine einzelne genehmigende Person', advanced: `Die Personalvermittlerin/der Personalvermittler oder die Koordinatorin/der Koordinator der Kandidatin/des Kandidaten wird zur Genehmigerin/zum Genehmiger ihrer Spesen`, diff --git a/src/languages/el.ts b/src/languages/el.ts index a05ae477e089..78113fb38ef4 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -7982,8 +7982,12 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ finalApprover: `Τελική έγκριση`, finalApproverOptional: 'Τελικός εγκρίτης (προαιρετικό)', approvalModeDescription: (providerName: string) => `Ορίστε τον εγκριτή για τα νέα μέλη που εισάγονται από το ${providerName} στο Expensify.`, - approverFieldDescription: (providerName: string) => - `Επιλέξτε τον πρώτο εγκρίνοντα για τους υποψηφίους σας: είτε τον υπεύθυνο προσλήψεών τους είτε τον συντονιστή τους που έχει οριστεί στο ${providerName}.`, + approverFieldDescription: (providerName: string, approverFields: string[]) => { + const fieldNames = approverFields.map((approverField) => approverField.toLowerCase()); + const lastFieldName = fieldNames.at(-1) ?? ''; + const fieldList = fieldNames.length > 2 ? `το ${fieldNames.slice(0, -1).join(', ')} τους ή το ${lastFieldName} τους` : `είτε ο/η ${fieldNames.join('ή την/τον/τα')} τους`; + return `Επιλέξτε τον πρώτο εγκριτή για τους υποψηφίους σας: ${fieldList} που έχουν ανατεθεί στο ${providerName}.`; + }, approvalModeDescriptions: { basic: 'Επιλέξτε έναν μόνο εγκρίνοντα', advanced: `Ο υπεύθυνος προσλήψεων ή ο συντονιστής της υποψηφιότητας γίνεται εγκρίνων των εξόδων τους`, diff --git a/src/languages/es.ts b/src/languages/es.ts index c5aaf863f969..781379b883ad 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7711,7 +7711,12 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, finalApprover: `Aprobador final`, finalApproverOptional: 'Aprobador final (opcional)', approvalModeDescription: (providerName: string) => `Establece el aprobador para los nuevos miembros que se importen desde ${providerName} a Expensify.`, - approverFieldDescription: (providerName: string) => `Elige el primer aprobador para tus candidatos: puede ser su reclutador o su coordinador asignado en ${providerName}.`, + approverFieldDescription: (providerName: string, approverFields: string[]) => { + const fieldNames = approverFields.map((approverField) => approverField.toLowerCase()); + const lastFieldName = fieldNames.at(-1) ?? ''; + const fieldList = fieldNames.length > 2 ? `su ${fieldNames.slice(0, -1).join(', ')}, o ${lastFieldName}` : `ya sea su ${fieldNames.join('o su')}`; + return `Elige al primer aprobador para tus candidatos: ${fieldList} asignado en ${providerName}.`; + }, approvalModeDescriptions: { basic: 'Elige un único aprobador', advanced: `El reclutador o coordinador del candidato se convierte en su aprobador de gastos`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index e710558d16e4..95c5e8272e25 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -9091,8 +9091,12 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e finalApprover: `Approbateur final`, finalApproverOptional: 'Approbateur final (facultatif)', approvalModeDescription: (providerName: string) => `Définissez l’approbateur pour les nouveaux membres importés de ${providerName} vers Expensify.`, - approverFieldDescription: (providerName: string) => - `Choisissez le premier approbateur pour vos candidat·es : soit leur recruteur·se, soit leur coordinateur·rice assigné·e dans ${providerName}.`, + approverFieldDescription: (providerName: string, approverFields: string[]) => { + const fieldNames = approverFields.map((approverField) => approverField.toLowerCase()); + const lastFieldName = fieldNames.at(-1) ?? ''; + const fieldList = fieldNames.length > 2 ? `leur ${fieldNames.slice(0, -1).join(', ')}, ou leur ${lastFieldName}` : `soit leur ${fieldNames.join('ou leur')}`; + return `Choisissez le premier approbateur pour vos candidats : ${fieldList} assigné dans ${providerName}.`; + }, approvalModeDescriptions: { basic: 'Choisir un seul approbateur', advanced: `Le recruteur ou le coordinateur du candidat devient la personne qui approuve ses dépenses`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 403707a86249..44d622c2a3f4 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -8980,7 +8980,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, dontSeeYourATS: `Non vedi il tuo ATS qui? Chiedi a Concierge e possiamo aggiungerlo.`, importSettings: 'Impostazioni di importazione', defaultApprover: 'Approvatore predefinito', - approverFields: {recruiter: 'Recruiter', recruitingCoordinator: 'Coordinatore recruiting', hiringManager: 'Responsabile assunzioni'}, + approverFields: {recruiter: 'Recruiter', recruitingCoordinator: 'Coordinatore recruiting', hiringManager: 'Responsabile delle assunzioni'}, filters: { description: (providerName: string) => `Seleziona quali membri importare da ${providerName}. Puoi scegliere in base alle fasi lavorative, ai tag e alle sedi.`, stages: { @@ -9014,8 +9014,12 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, finalApprover: `Approvatore finale`, finalApproverOptional: 'Approvatore finale (facoltativo)', approvalModeDescription: (providerName: string) => `Imposta il responsabile delle approvazioni per i nuovi membri importati da ${providerName} in Expensify.`, - approverFieldDescription: (providerName: string) => - `Scegli il primo approvatore per i tuoi candidati: il loro Recruiter oppure il loro Coordinator assegnato in ${providerName}.`, + approverFieldDescription: (providerName: string, approverFields: string[]) => { + const fieldNames = approverFields.map((approverField) => approverField.toLowerCase()); + const lastFieldName = fieldNames.at(-1) ?? ''; + const fieldList = fieldNames.length > 2 ? `il loro ${fieldNames.slice(0, -1).join(', ')} o ${lastFieldName}` : `o il loro ${fieldNames.join('o il loro')}`; + return `Scegli il primo approvatore per i tuoi candidati: ${fieldList} assegnato in ${providerName}.`; + }, approvalModeDescriptions: { basic: 'Scegli un solo approvatore', advanced: `Il recruiter o il coordinatore del candidato diventa il suo approvatore di spese`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 8428f9bc4a90..3605467ad7ac 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -8895,8 +8895,12 @@ ${reportName}`, finalApprover: `最終承認者`, finalApproverOptional: '最終承認者(任意)', approvalModeDescription: (providerName: string) => `${providerName} から Expensify にインポートされる新しいメンバーの承認者を設定します。`, - approverFieldDescription: (providerName: string) => - `候補者の最初の承認者を選択します。${providerName} で割り当てられているリクルーターまたはコーディネーターのいずれかを指定してください。`, + approverFieldDescription: (providerName: string, approverFields: string[]) => { + const fieldNames = approverFields.map((approverField) => approverField.toLowerCase()); + const lastFieldName = fieldNames.at(-1) ?? ''; + const fieldList = fieldNames.length > 2 ? `${fieldNames.slice(0, -1).join(', ')} または ${lastFieldName}` : `または ${fieldNames.join('またはその')} のいずれか`; + return `候補者の最初の承認者を選択してください:${providerName} で割り当てられている ${fieldList}`; + }, approvalModeDescriptions: { basic: '承認者を 1 名選択してください', advanced: `候補者のリクルーターまたはコーディネーターが、その候補者の経費承認者になります`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index cf247a9462cc..daf9d4a8aeef 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -8992,7 +8992,12 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, finalApprover: `Laatste goedkeurder`, finalApproverOptional: 'Laatste fiatteur (optioneel)', approvalModeDescription: (providerName: string) => `Stel de fiatteur in voor nieuwe leden die vanuit ${providerName} in Expensify worden geïmporteerd.`, - approverFieldDescription: (providerName: string) => `Kies de eerste fiatteur voor je kandidaten: ofwel hun recruiter of hun coördinator die is toegewezen in ${providerName}.`, + approverFieldDescription: (providerName: string, approverFields: string[]) => { + const fieldNames = approverFields.map((approverField) => approverField.toLowerCase()); + const lastFieldName = fieldNames.at(-1) ?? ''; + const fieldList = fieldNames.length > 2 ? `hun ${fieldNames.slice(0, -1).join(', ')}, of ${lastFieldName}` : `hun ${fieldNames.join('of hun')}`; + return `Kies de eerste fiatteur voor je kandidaten: ${fieldList} toegewezen in ${providerName}.`; + }, approvalModeDescriptions: { basic: 'Kies één fiatteur', advanced: `Recruiter of coördinator van de kandidaat wordt zijn/haar onkostengoedkeurder`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index b5e37be98ee1..8a7d7552afa6 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -9008,7 +9008,12 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, finalApprover: `Ostateczny akceptujący`, finalApproverOptional: 'Ostateczny zatwierdzający (opcjonalnie)', approvalModeDescription: (providerName: string) => `Ustaw osobę zatwierdzającą dla nowych członków importowanych z ${providerName} do Expensify.`, - approverFieldDescription: (providerName: string) => `Wybierz pierwszą osobę akceptującą dla swoich kandydatów: Rekrutera lub Koordynatora przypisanego w ${providerName}.`, + approverFieldDescription: (providerName: string, approverFields: string[]) => { + const fieldNames = approverFields.map((approverField) => approverField.toLowerCase()); + const lastFieldName = fieldNames.at(-1) ?? ''; + const fieldList = fieldNames.length > 2 ? `ich ${fieldNames.slice(0, -1).join(', ')} lub ${lastFieldName}` : `albo ich ${fieldNames.join('lub ich')}`; + return `Wybierz pierwszą osobę zatwierdzającą dla swoich kandydatów: ${fieldList} przypisane w ${providerName}.`; + }, approvalModeDescriptions: { basic: 'Wybierz jedną osobę zatwierdzającą', advanced: `Rekruter lub koordynator kandydata zostaje jego akceptującym wydatki`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 9e3ea7068b6f..fabe4e5a940e 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -8995,7 +8995,12 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, finalApprover: `Aprovador final`, finalApproverOptional: 'Aprovador final (opcional)', approvalModeDescription: (providerName: string) => `Defina o aprovador para novos membros importados de ${providerName} para o Expensify.`, - approverFieldDescription: (providerName: string) => `Escolha o primeiro aprovador para seus candidatos: o recrutador ou o coordenador atribuídos a eles em ${providerName}.`, + approverFieldDescription: (providerName: string, approverFields: string[]) => { + const fieldNames = approverFields.map((approverField) => approverField.toLowerCase()); + const lastFieldName = fieldNames.at(-1) ?? ''; + const fieldList = fieldNames.length > 2 ? `seu(s) ${fieldNames.slice(0, -1).join(', ')} ou ${lastFieldName}` : `ou sua ${fieldNames.join('ou de seus')}`; + return `Escolha o primeiro aprovador para seus candidatos: ${fieldList} atribuídos em ${providerName}.`; + }, approvalModeDescriptions: { basic: 'Escolha um único aprovador', advanced: `O recrutador ou coordenador da pessoa candidata se torna o aprovador de despesas dela`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 9d7454b29cdf..55f50e852c88 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -8663,7 +8663,12 @@ ${reportName}`, finalApprover: `最终审批人`, finalApproverOptional: '最终审批人(可选)', approvalModeDescription: (providerName: string) => `为从 ${providerName} 导入到 Expensify 的新成员设置审批人。`, - approverFieldDescription: (providerName: string) => `为候选人选择第一位审批人:可以是他们在 ${providerName} 中指定的招聘人员或协调员。`, + approverFieldDescription: (providerName: string, approverFields: string[]) => { + const fieldNames = approverFields.map((approverField) => approverField.toLowerCase()); + const lastFieldName = fieldNames.at(-1) ?? ''; + const fieldList = fieldNames.length > 2 ? `其 ${fieldNames.slice(0, -1).join(', ')} 或 ${lastFieldName}` : `其 ${fieldNames.join('或其')}`; + return `为您的候选人选择第一位审批人:在 ${providerName} 中分配的 ${fieldList}。`; + }, approvalModeDescriptions: {basic: '选择单个审批人', advanced: `候选人的招聘人员或协调人将成为其报销审批人`, custom: '在 Expensify 中手动设置审批人'}, }, merge: { From cbb1ffd8f136cf218bc13448d8d4d9284912956f Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 21 Oct 2026 11:11:13 +0800 Subject: [PATCH 08/11] don't show anything when final approver is not set --- src/libs/merge/MergeUtils.ts | 4 ++-- src/pages/workspace/hr/utils.ts | 4 ++-- src/pages/workspace/recruiting/utils.ts | 2 +- tests/unit/MergeUtilsTest.ts | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/libs/merge/MergeUtils.ts b/src/libs/merge/MergeUtils.ts index 77be4c4942c1..8c64765aa02e 100644 --- a/src/libs/merge/MergeUtils.ts +++ b/src/libs/merge/MergeUtils.ts @@ -62,9 +62,9 @@ function getMergeFinalApproverDisplayName( policyEmployeePersonalDetails: PersonalDetailsByLogin, translate: LocaleContextProps['translate'], formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], -): string { +): string | undefined { if (!finalApprover) { - return translate('workspace.merge.notSet'); + return undefined; } return temporaryGetDisplayNameOrDefault({ passedPersonalDetails: policyEmployeePersonalDetails[finalApprover], diff --git a/src/pages/workspace/hr/utils.ts b/src/pages/workspace/hr/utils.ts index ec1d08137f5f..3e8196adee7f 100644 --- a/src/pages/workspace/hr/utils.ts +++ b/src/pages/workspace/hr/utils.ts @@ -219,7 +219,7 @@ function getHRCards({ { field: 'finalApprover', description: translate('workspace.merge.finalApprover'), - title: getMergeFinalApproverDisplayName(config?.finalApprover, policyEmployeePersonalDetails, translate, formatPhoneNumber), + title: getMergeFinalApproverDisplayName(config?.finalApprover, policyEmployeePersonalDetails, translate, formatPhoneNumber) ?? translate('workspace.merge.notSet'), route: provider.finalApproverRoute.getRoute(policyID), pendingAction: config?.pendingFields?.finalApprover, errors: config?.errorFields?.finalApprover, @@ -268,7 +268,7 @@ function getHRCards({ { field: 'finalApprover', description: translate('workspace.merge.finalApprover'), - title: getMergeFinalApproverDisplayName(mergeConfig?.finalApprover, policyEmployeePersonalDetails, translate, formatPhoneNumber), + title: getMergeFinalApproverDisplayName(mergeConfig?.finalApprover, policyEmployeePersonalDetails, translate, formatPhoneNumber) ?? translate('workspace.merge.notSet'), route: ROUTES.WORKSPACE_HR_MERGE_FINAL_APPROVER.getRoute(policyID), pendingAction: mergeConfig?.pendingFields?.finalApprover, errors: mergeConfig?.errorFields?.finalApprover, diff --git a/src/pages/workspace/recruiting/utils.ts b/src/pages/workspace/recruiting/utils.ts index e57a3f8416f6..48fbab536035 100644 --- a/src/pages/workspace/recruiting/utils.ts +++ b/src/pages/workspace/recruiting/utils.ts @@ -60,7 +60,7 @@ function getDefaultApproverLabel( return `${translate('workspace.merge.approvalModes.basic')} • ${approver}`; } - return `${translate('workspace.merge.approvalModes.advanced')} • ${getApproverFieldLabel(policy, translate)} -> ${approver}`; + return `${translate('workspace.merge.approvalModes.advanced')} • ${getApproverFieldLabel(policy, translate)}${approver ? `-> ${approver}` : ''}`; } if (approvalMode === CONST.MERGE.APPROVAL_MODE.CUSTOM) { diff --git a/tests/unit/MergeUtilsTest.ts b/tests/unit/MergeUtilsTest.ts index 1eef25bf677d..93dd52b92240 100644 --- a/tests/unit/MergeUtilsTest.ts +++ b/tests/unit/MergeUtilsTest.ts @@ -230,13 +230,13 @@ describe('MergeUtils', () => { [login]: {accountID: 42, login, ...overrides}, }); - it('returns "not set" when there is no final approver', () => { + it('returns undefined when there is no final approver', () => { // Given a connection with no final approver configured // When the approver's display name is resolved - // Then the missing approver is called out rather than left blank - expect(getMergeFinalApproverDisplayName(undefined, {}, translateLocal, formatPhoneNumber)).toBe('workspace.merge.notSet'); - expect(getMergeFinalApproverDisplayName(null, {}, translateLocal, formatPhoneNumber)).toBe('workspace.merge.notSet'); - expect(getMergeFinalApproverDisplayName('', {}, translateLocal, formatPhoneNumber)).toBe('workspace.merge.notSet'); + // Then the approver is undefined + expect(getMergeFinalApproverDisplayName(undefined, {}, translateLocal, formatPhoneNumber)).toBeUndefined(); + expect(getMergeFinalApproverDisplayName(null, {}, translateLocal, formatPhoneNumber)).toBeUndefined(); + expect(getMergeFinalApproverDisplayName('', {}, translateLocal, formatPhoneNumber)).toBeUndefined(); }); it('returns the display name when the approver is a known workspace member', () => { From 5b0e1b7db2598b56c6e46bf7fcdb3437adecfde8 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 21 Oct 2026 11:12:13 +0800 Subject: [PATCH 09/11] add missing space --- src/pages/workspace/recruiting/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/recruiting/utils.ts b/src/pages/workspace/recruiting/utils.ts index 48fbab536035..933bb06ca26f 100644 --- a/src/pages/workspace/recruiting/utils.ts +++ b/src/pages/workspace/recruiting/utils.ts @@ -60,7 +60,7 @@ function getDefaultApproverLabel( return `${translate('workspace.merge.approvalModes.basic')} • ${approver}`; } - return `${translate('workspace.merge.approvalModes.advanced')} • ${getApproverFieldLabel(policy, translate)}${approver ? `-> ${approver}` : ''}`; + return `${translate('workspace.merge.approvalModes.advanced')} • ${getApproverFieldLabel(policy, translate)}${approver ? ` -> ${approver}` : ''}`; } if (approvalMode === CONST.MERGE.APPROVAL_MODE.CUSTOM) { From d2d41870b6f47f5a9cdd642c057eab35e6f64274 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 21 Oct 2026 14:56:42 +0800 Subject: [PATCH 10/11] fix test --- tests/unit/RecruitingUtilsTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/RecruitingUtilsTest.ts b/tests/unit/RecruitingUtilsTest.ts index 8634a8efe965..0a2fd5171cab 100644 --- a/tests/unit/RecruitingUtilsTest.ts +++ b/tests/unit/RecruitingUtilsTest.ts @@ -731,10 +731,10 @@ describe('getRecruitingCards', () => { ); }); - it('falls back to "not set" for the approver when basic mode has none', () => { + it('undefined for the approver when basic mode has none', () => { // Given a connection in basic mode with no final approver chosen // When the default approver row is built - // Then the missing approver is called out rather than left blank + // Then the missing approver is undefiend expect(getDefaultApproverTitle({config: {approvalMode: CONST.MERGE.APPROVAL_MODE.BASIC, finalApprover: null}})).toBe( 'workspace.merge.approvalModes.basic • workspace.merge.notSet', ); From e4dc1555d1ac8e3d9e86e0ecdc37f083f3204269 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 21 Oct 2026 14:57:15 +0800 Subject: [PATCH 11/11] remove test --- tests/unit/RecruitingUtilsTest.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/unit/RecruitingUtilsTest.ts b/tests/unit/RecruitingUtilsTest.ts index 0a2fd5171cab..2c6abd4dee2f 100644 --- a/tests/unit/RecruitingUtilsTest.ts +++ b/tests/unit/RecruitingUtilsTest.ts @@ -731,15 +731,6 @@ describe('getRecruitingCards', () => { ); }); - it('undefined for the approver when basic mode has none', () => { - // Given a connection in basic mode with no final approver chosen - // When the default approver row is built - // Then the missing approver is undefiend - expect(getDefaultApproverTitle({config: {approvalMode: CONST.MERGE.APPROVAL_MODE.BASIC, finalApprover: null}})).toBe( - 'workspace.merge.approvalModes.basic • workspace.merge.notSet', - ); - }); - it('resolves the approver login to their display name', () => { // Given a final approver who is a known workspace member // When the default approver row is built