Repository navigation
Add a clear after date to vacation delegates and show them on the Profile page - #102472
Conversation
…file page Co-authored-by: Kevin Brian Bader <[email protected]>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
|
🤖 Web test steps verification results: The Vacation delegate row, member picker, date minimum, clear note, and remove flow all work. But the Clear after date is not kept after Save: Profile shows no "Until ", and the date field is empty when you reopen the form. The PR's Tests section is still a TODO, so I built these steps from the PR's "What changed" list, the same as the Android run.
|
This comment has been minimized.
This comment has been minimized.
… a Choose delegate label, and keep the empty date label inside the input Co-authored-by: Kevin Brian Bader <[email protected]>
|
I pushed 5cffbfc to bring the form and the Profile row closer to the mockups. I couldn't check it in a browser: the web test session went blank during onboarding, before it reached Profile. Please check the screens by eye. What changed:
Where it still differs from the mockups:
Checks: Typecheck, lint, spell check, and the formatter pass. The React Compiler check passes for the changed components except view run · no recording available |
🦜 Polyglot Parrot! 🦜Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues: View the translation diffdiff --git a/src/languages/de.ts b/src/languages/de.ts
index 437caf4b..666c4749 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -3861,9 +3861,9 @@ ${amount} für ${merchant} – ${date}`,
`<strong>${delegate}</strong> ist kein Mitglied der unten aufgeführten Arbeitsbereiche. Admins der Arbeitsbereiche, die Sie nicht verwalten, werden gebeten, die Person hinzuzufügen. Möchten Sie die Person jetzt zu den Arbeitsbereichen einladen, in denen Sie Admin sind?`,
youAreAMemberOf: 'Sie sind Mitglied dieser Arbeitsbereiche:',
youAreAnAdminOf: 'Sie sind Admin dieser Arbeitsbereiche:',
- chooseDelegate: 'Vertretung auswählen',
+ chooseDelegate: 'Delegierte Person auswählen',
clearAfterRecommended: 'Löschen nach (empfohlen)',
- willClearOn: (date: string) => `Die Urlaubsvertretung wird am ${date} entfernt`,
+ willClearOn: (date: string) => `Urlaubsvertretung endet am ${date}`,
until: (date: string) => `Bis ${date}`,
removeDelegate: 'Urlaubsvertretung entfernen',
},
diff --git a/src/languages/el.ts b/src/languages/el.ts
index 7dc64aee..d5c49437 100644
--- a/src/languages/el.ts
+++ b/src/languages/el.ts
@@ -3911,11 +3911,11 @@ ${amount} για ${merchant} - ${date}`,
`Ο/Η <strong>${delegate}</strong> δεν είναι μέλος των παρακάτω χώρων εργασίας. Θα ζητηθεί από τους διαχειριστές των χώρων εργασίας που δεν διαχειρίζεστε να τον/την προσθέσουν. Θέλετε να τον/την προσκαλέσετε τώρα σε αυτούς που διαχειρίζεστε ως διαχειριστής;`,
youAreAMemberOf: 'Είστε μέλος αυτών των χώρων εργασίας:',
youAreAnAdminOf: 'Είστε διαχειριστής αυτών των χώρων εργασίας:',
- chooseDelegate: 'Επιλέξτε αναπληρωτή',
+ chooseDelegate: 'Επιλέξτε εκπρόσωπο',
clearAfterRecommended: 'Εκκαθάριση μετά από (συνιστάται)',
- willClearOn: (date: string) => `Ο εκπρόσωπος διακοπών θα καταργηθεί στις ${date}`,
- until: (date: string) => `Έως ${date}`,
- removeDelegate: 'Κατάργηση εκπροσώπου διακοπών',
+ willClearOn: (date: string) => `Η ανάθεση αδειούχου θα λήξει στις ${date}`,
+ until: (date: string) => `Μέχρι ${date}`,
+ removeDelegate: 'Κατάργηση αναπληρωτή διακοπών',
},
},
stepCounter: (step, total, text) => {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 65835143..f120b102 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -3781,8 +3781,8 @@ ${amount} para ${merchant} - ${date}`,
youAreAnAdminOf: 'Usted es administrador de estos espacios de trabajo:',
chooseDelegate: 'Elegir delegado',
clearAfterRecommended: 'Borrar después de (recomendado)',
- willClearOn: (date: string) => `El delegado de vacaciones se eliminará el ${date}`,
- until: (date: string) => `Hasta el ${date}`,
+ willClearOn: (date: string) => `El delegado de vacaciones se borrará el ${date}`,
+ until: (date: string) => `Hasta ${date}`,
removeDelegate: 'Eliminar delegado de vacaciones',
},
},
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 0c701d9e..1db6af88 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -3871,7 +3871,7 @@ ${amount} pour ${merchant} - ${date}`,
youAreAnAdminOf: 'Vous êtes administrateur de ces espaces de travail :',
chooseDelegate: 'Choisir un délégué',
clearAfterRecommended: 'Effacer après (recommandé)',
- willClearOn: (date: string) => `Le délégué de vacances sera supprimé le ${date}`,
+ willClearOn: (date: string) => `La délégation de vacances sera annulée le ${date}`,
until: (date: string) => `Jusqu’au ${date}`,
removeDelegate: 'Supprimer le délégué de vacances',
},
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 481e794f..0b152956 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -3844,9 +3844,9 @@ ${amount} per ${merchant} - ${date}`,
youAreAnAdminOf: 'Sei amministratore di questi spazi di lavoro:',
chooseDelegate: 'Scegli delegato',
clearAfterRecommended: 'Cancella dopo (consigliato)',
- willClearOn: (date: string) => `La delega ferie verrà rimossa il ${date}`,
+ willClearOn: (date: string) => `Il delegato per le vacanze verrà rimosso il ${date}`,
until: (date: string) => `Fino al ${date}`,
- removeDelegate: 'Rimuovi delega ferie',
+ removeDelegate: 'Rimuovi delegato ferie',
},
},
stepCounter: (step: number, total?: number, text?: string) => {
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 23e2672f..1ae059f5 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -3812,9 +3812,9 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの'
youAreAMemberOf: 'あなたはこれらのワークスペースのメンバーです:',
youAreAnAdminOf: 'あなたはこれらのワークスペースの管理者です:',
chooseDelegate: '代理人を選択',
- clearAfterRecommended: 'クリア日(推奨)',
+ clearAfterRecommended: 'この時間後に自動クリア(推奨)',
willClearOn: (date: string) => `休暇代理人は${date}に解除されます`,
- until: (date: string) => `${date}まで`,
+ until: (date: string) => `${date} まで`,
removeDelegate: '休暇代理人を削除',
},
},
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index cc775dc8..3aefa229 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -3844,11 +3844,11 @@ ${amount} voor ${merchant} - ${date}`,
`<strong>${delegate}</strong> is geen lid van de onderstaande werkruimtes. Beheerders van de werkruimtes die je niet beheert, wordt gevraagd diegene toe te voegen. Wil je diegene nu uitnodigen voor de werkruimtes waar je beheerder van bent?`,
youAreAMemberOf: 'Je bent lid van deze werkruimtes:',
youAreAnAdminOf: 'Je bent beheerder van deze werkruimtes:',
- chooseDelegate: 'Gedelegeerde kiezen',
+ chooseDelegate: 'Kies gemachtigde',
clearAfterRecommended: 'Wissen na (aanbevolen)',
- willClearOn: (date: string) => `De vertegenwoordiger tijdens vakantie wordt gewist op ${date}`,
+ willClearOn: (date: string) => `Vakantiegedelegeerde wordt op ${date} verwijderd`,
until: (date: string) => `Tot ${date}`,
- removeDelegate: 'Vertegenwoordiger tijdens vakantie verwijderen',
+ removeDelegate: 'Vakantiegemachtigde verwijderen',
},
},
stepCounter: (step: number, total?: number, text?: string) => {
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 84575180..33f043e3 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -3873,11 +3873,11 @@ ${amount} dla ${merchant} - ${date}`,
`<strong>${delegate}</strong> nie jest członkiem poniższych przestrzeni roboczych. Administratorzy przestrzeni roboczych, którymi nie zarządzasz, zostaną poproszeni o dodanie tej osoby. Czy chcesz zaprosić tę osobę do przestrzeni, w których jesteś administratorem?`,
youAreAMemberOf: 'Jesteś członkiem tych przestrzeni roboczych:',
youAreAnAdminOf: 'Jesteś administratorem tych przestrzeni roboczych:',
- chooseDelegate: 'Wybierz zastępcę',
+ chooseDelegate: 'Wybierz delegata',
clearAfterRecommended: 'Wyczyść po (zalecane)',
- willClearOn: (date: string) => `Zastępca urlopowy zostanie usunięty ${date}`,
+ willClearOn: (date: string) => `Zastępstwo wakacyjne wygaśnie ${date}`,
until: (date: string) => `Do ${date}`,
- removeDelegate: 'Usuń zastępcę urlopowego',
+ removeDelegate: 'Usuń zastępcę na czas urlopu',
},
},
stepCounter: (step: number, total?: number, text?: string) => {
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 0e02130d..3bec9312 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -3835,7 +3835,7 @@ ${amount} para ${merchant} - ${date}`,
clearAfterRecommended: 'Limpar após (recomendado)',
willClearOn: (date: string) => `O delegado de férias será removido em ${date}`,
until: (date: string) => `Até ${date}`,
- removeDelegate: 'Remover delegado de férias',
+ removeDelegate: 'Remover representante de férias',
},
},
stepCounter: (step: number, total?: number, text?: string) => {
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index c99e62f2..af721575 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -3720,9 +3720,9 @@ ${amount},商户:${merchant} - 日期:${date}`,
youAreAMemberOf: '你是这些工作区的成员:',
youAreAnAdminOf: '你是这些工作区的管理员:',
chooseDelegate: '选择代理人',
- clearAfterRecommended: '清除日期(推荐)',
+ clearAfterRecommended: '建议在此之后清除',
willClearOn: (date: string) => `休假代理将于 ${date} 清除`,
- until: (date: string) => `直到 ${date}`,
+ until: (date: string) => `截至 ${date}`,
removeDelegate: '移除休假代理',
},
},
Note You can apply these changes to your branch by copying the patch to your clipboard, then running |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Yes. The draft is ready for What the App sends:
One thing the backend must match: The App reads the date from a Expected result against the backend drafts: After Save, Profile shows "Until {date}", and the form keeps the date when you reopen it. Both web and Android runs failed that step against the current backend. The most likely reason is that the server response doesn't include Still open, but not blocking a backend test: The |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- Title both member pickers "Choose delegate". - Push the form from a picker opened straight from Profile or member details, and pop only the form on back, so back returns to the picker. - Close the whole RHP after Invite or Skip on the missing workspaces step. Its removal guard now replays the blocked action through the navigation container; replaying it through the screen stamped the screen's key as the source, so the root stack dropped it. - Keep a solid calendar icon on the clear after input after a date is picked, and hide its clear button. - Tighten the gap under the delegate row and give the remove button the same top spacing with or without the clear-on note. Co-authored-by: Kevin Brian Bader <[email protected]>
|
Hey! I see that you made changes to our Form component. Make sure to update the docs in FORMS.md accordingly. Cheers! |
1 similar comment
|
Hey! I see that you made changes to our Form component. Make sure to update the docs in FORMS.md accordingly. Cheers! |
|
I fixed all six items in 4ecae14 and updated the PR description. Typecheck, lint, formatting, and spelling pass, and so do the vacation delegate and Profile Jest suites (146 tests). I couldn't check it in a browser because the test session showed a blank page before it started, so please check the screens by hand.
Why item 6's step never closedThe The guard now uses PR description: I removed |
This comment has been minimized.
This comment has been minimized.
|
The failing check is unrelated to this PR, so I didn't push a fix. Analysis: Fix in progress: [No QA] Move NetSuite helpers out of PolicyUtils to fix max-lines lint failure fixes this on Evidence: Every other check passes. |
|
The only real failure is still Cause: Next step: [No QA] Move NetSuite helpers out of PolicyUtils to fix max-lines lint failure fixes this on
|
|
Conflicts |
|
@MelvinBot Fix conflicts and failing CIs (unit tests and TS check). |
…teClearAfter Co-authored-by: Kevin Brian Bader <[email protected]> # Conflicts: # src/libs/actions/RequestConflictUtils.ts # tests/unit/RequestConflictUtilsTest.ts
This comment has been minimized.
This comment has been minimized.
|
I merged
|
|
@MelvinBot Branch still has conflicts. |
…teClearAfter Co-authored-by: Kevin Brian Bader <[email protected]> # Conflicts: # src/components/DatePicker/index.tsx
|
The conflicts are fixed. What conflicted: Checks:
|
This comment has been minimized.
This comment has been minimized.
|
@ikevin127 checks are failing |
This comment has been minimized.
This comment has been minimized.
…teClearAfter Co-authored-by: Kevin Brian Bader <[email protected]>
|
Merged
|
|
A preview of your ExpensifyHelp changes have been deployed to https://de46a423.helpdot.pages.dev ⚡️ Updated articles: |
|
@luacmartins All 🟢 now, if you could please remove the [NoQA] from the title before merge 🙌 (bot keeps wrongly adding it since there are some changes in a docs file) - but this needs QA once merged and deployed. |
luacmartins
left a comment
There was a problem hiding this comment.
This is a really big PR given the changes. Let's try to keep it smaller in the future please!
|
🚧 luacmartins has triggered a test Expensify/App build. You can view the workflow run here. |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/luacmartins in version: 9.5.10-0 🚀
|
|
A help site update is needed. I opened Docs updates for E/A#102472 Update domain admin vacation delegate steps for new form as a draft. Why: This PR made two steps in Manage-Domain-Members.md:114-115 wrong:
Now Vacation delegate opens a form. The admin selects Choose delegate, can set an optional Clear after (recommended) date, and then selects Save. The docs PR replaces those two steps with these three. @ikevin127, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR |
|
🚀 Deployed to staging by https://github.com/luacmartins in version: 9.5.12-0 🚀
|
|
A help site update is needed. I opened Docs updates for E/A#102472 Update domain vacation delegate steps to use Save. This PR already updated
Domain admins now use the new form. They select Choose delegate, pick a member, and select Save. The docs PR changes only those two steps. @ikevin127, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR |
|
🚀 Deployed to production by https://github.com/cristipaval in version: 9.5.12-1 🚀
Bundle Size Analysis (Sentry): |




















Explanation of Change
Vacation delegates never ended on their own, and the only place they showed up was the bottom of Profile > Status. People forgot to remove them, and approvers whose admin set one had no way to see why reports stopped reaching them. This PR makes the delegate visible on Profile and lets the person setting it pick a date and time it clears at.
What changed
Profile row. A new Vacation delegate row sits after Status. It shows the delegate and "Until Oct 1, 2026 5:30 PM" when a date is set, and carries the delegate's error dot (moved off the Status row). The row always opens the form. With no delegate, the form's Choose delegate row is unselected. The domain member details row works the same way. The section is removed from the Status page.
Member picker. The picker is only reached from the form's Choose delegate row, on both Profile and the domain member details page. Its header reads Choose delegate, and picking a member returns to the form. A saved delegate whose clear after time has passed is not preselected.
New form. Picking a member no longer saves right away. The form is titled Vacation delegate and shows:
The date input shows a calendar icon while empty and a clear (X) button once a date is picked. The X clears the date, so the delegate goes back to never clearing. Domain admins use the same form on the member details page, and the date and time are read in the member's Profile timezone. A failed save shows the error in a modal and restores the previous delegate.
Data.
SetVacationDelegatesends an optionalclearAfterfrom bothsetVacationDelegateandsetDomainVacationDelegate. Per the backend note, it is a UTC SQL datetime (yyyy-MM-dd HH:mm:ss): the picked date and time in the timezone of the person the delegate belongs to, or the end of that day if no time is picked (VacationDelegateUtils.getVacationDelegateClearAfter). It is stored on the NVP and the domain record, rolled back through a newpreviousClearAfter(the same waypreviousDelegateworks), and cleared on delete. Removing your own delegate drops any queuedSetVacationDelegatefor it (resolveVacationDelegateDeletionConflicts), so an offline change can't bring the removed delegate back.Missing workspaces step. On a 305, the picked date is kept in
pendingClearAfternext topendingDelegate, so the step sends it again instead of dropping it. After Invite or Skip, the step closes the whole RHP withNavigation.dismissModal(), so the form and picker under it don't stay in the stack. ItsusePreventRemoveguard now replays the blocked action through the navigation container instead of the screen's ownnavigation. The screen's dispatch stamped its route key on the action assource, and the root stack, which can't find that key, dropped the action and left the RHP open.Display guard. A delegate whose
clearAfterhas passed shows as "no delegate" until the backend's clear reaches the client. The App never deletes it itself; the backend stays in charge.Routes. The screens moved from
settings/profile/status/vacation-delegatetosettings/profile/vacation-delegate(form),.../select(picker), and.../missing-workspaces. The domain flow addsdomain/:domainAccountID/members/:accountID/vacation-delegate/select.DatePicker(affects every date input in the App). The clear (X) button now gets the icon container's horizontal padding (clearButtonStyle), so it sits exactly where the calendar icon was. This moves the X in every date input, not just this form. Theiconprop now also overrides the default calendar icon.Help doc.
Delegate-when-out-of-office.mdnow covers the Profile row and the clear after date and time.Notes from the C+ review
clearAftersupport is verified. Setting a date, clearing the delegate at that time, and dropping a cleared date on save all work against the backend. Confirm the backend PRs are deployed to production before merging.SetDatePageis not reused. The date lives in its own form,ONYXKEYS.FORMS.VACATION_DELEGATE_FORM, so it can't overwrite a status clear date the user is in the middle of setting.TimeModalPicker, the same one per diem uses, and only shows once a date is picked. It defaults to 11:59 PM, so a date with no time change still clears at the end of that day. A time that has already passed shows "Please choose a time at least one minute ahead". The Profile row and the form note show the time too.CalendarSolid), which matches the mockups. Other date inputs in the App use the outlineCalendaricon.2026/10/01. The input keeps the sharedDatePickerformat (2026-10-01).Fixed Issues
$ #102202
PROPOSAL: #102202 (comment)
Tests
Prerequisites
In the steps below, "tomorrow" stands for tomorrow's date. The date input shows it as
yyyy-MM-dd(e.g.2026-10-13). The note under the inputs and the "Until" label show it asMMM d, yyyyfollowed by the time (e.g.Oct 13, 2026 11:59 PM).Profile row and Status page
Set a delegate with a clear after date and time
Click Vacation delegate. Verify a form opens with:
Verify there's no Time row, no "Vacation delegate will clear on …" note, and no Remove vacation delegate button.
Click Choose delegate. Verify the member picker opens with the header Choose delegate, with no member preselected. Click its back button and verify you return to the form.
Click Choose delegate again and pick Account B. Verify you return to the form, which now shows Account B's avatar and name as a pill under Choose delegate, with Save enabled.
Click the Clear after (recommended) input. Verify the calendar opens and days before today can't be picked. Pick tomorrow. Verify:
yyyy-MM-dd(e.g.2026-10-13);11:59 PM;Click the X. Verify the input is empty again with the calendar icon in the same spot, and the Time row and the note are gone. Pick tomorrow again.
Click the Time row, set 5:30 PM and confirm. Verify the Time row shows
5:30 PMand the note reads "Vacation delegate will clear on Oct 13, 2026 5:30 PM".Click Save. Verify:
Refresh the page. Verify the Profile row still shows Account B and "Until Oct 13, 2026 5:30 PM".
Edit and remove
2026-10-13in Clear after (recommended), with the X;5:30 PMin Time;2026-10-13,6:00 PM), not the unsaved ones.No date
Error from the backend
Have Account C set its own vacation delegate. Then, as Account A, open the form, pick Account C and click Save. Verify:
Click Got it. Verify the modal closes, the spinner on Save stops, the form shows the previous delegate again, and Save can be clicked again.
Automatic clearing
Missing workspaces step
Note
This step only appears when Account A belongs to at least one workspace that the chosen delegate is not a member of. With no workspaces, or a delegate who is already in all of them, the delegate is saved directly.
Open the form, pick Account D, pick tomorrow, set Time to 5:30 PM, and click Save. Verify:
Click Invite. Verify the whole RHP closes and the Profile row shows Account D's pill with "Until Oct 13, 2026 5:30 PM", appearing once without flashing.
Remove the delegate and repeat step 22, but click Skip instead of Invite. Verify the same result.
While you're someone else's delegate
settings/profile/vacation-delegatedirectly. Verify the same message shows.Domain admin
Other date inputs (shared
DatePickerchange)Offline tests
Note
Go offline with the browser's network throttling (DevTools > Network > Offline) or by turning off the device's network. The Force offline switch under Troubleshoot still sends requests to the server, so the server applies changes that should be waiting in the queue.
Self-service (Profile)
2026-10-13and5:30 PM.DeleteVacationDelegateis sent, not the queuedSetVacationDelegate.main).Domain admin
QA Steps
Same as tests, including steps 31 and 32, which check that other date inputs in the App still work after the shared
DatePickerchange.PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari