Repository navigation
Add a clear after date to vacation delegates and show them on the Profile page #102472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
3c95faa
Add a clear after date to vacation delegates and move them to the Pro…
MelvinBot 5cffbfc
Match the vacation delegate mockups: show the delegate as a pill, add…
MelvinBot 4ecae14
Fix vacation delegate navigation and match the date input to the mockups
MelvinBot b8e31b1
Merge main into claude-vacationDelegateClearAfter
MelvinBot f778819
Let users pick a time as well as a date for the vacation delegate to …
MelvinBot 36812b0
Merge remote-tracking branch 'origin/main' into claude-vacationDelega…
MelvinBot 593f42f
Use the composed Header and a memoized useOnyx selector on the vacati…
MelvinBot 006d892
Address review comments on the vacation delegate form
MelvinBot fcf9bdf
Validate the vacation delegate clear date in the Profile timezone
MelvinBot 693aafe
Merge remote-tracking branch 'origin/main' into claude-vacationDelega…
MelvinBot 91e069c
Address C+ review on vacation delegates: domain guard, member timezon…
MelvinBot b4c2607
Address review on vacation delegates: page-driven Save spinner, queue…
MelvinBot 4c176fc
Apply a picked vacation delegate only after the backend saves it unle…
MelvinBot bcb9f88
Always open the vacation delegate form from the row, show a clear but…
MelvinBot 74e4599
Merge remote-tracking branch 'origin/main' into claude-vacationDelega…
MelvinBot 17d4691
Add tests for expired delegates in the picker and the member details …
MelvinBot 7786b7a
Merge remote-tracking branch 'origin/main' into claude-vacationDelega…
MelvinBot 7eb737e
Merge remote-tracking branch 'origin/main' into claude-vacationDelega…
MelvinBot 581b256
Merge remote-tracking branch 'origin/main' into claude-vacationDelega…
MelvinBot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; | ||
| import useInitialSelection from '@hooks/useInitialSelection'; | ||
| import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
|
|
||
| import DateUtils from '@libs/DateUtils'; | ||
| import { | ||
| formatVacationDelegateClearDateTime, | ||
| getActiveVacationDelegate, | ||
| getVacationDelegateClearAfter, | ||
| getVacationDelegateClearDate, | ||
| getVacationDelegateClearDateTime, | ||
| getVacationDelegateLocalClearDateTime, | ||
| isVacationDelegateClearAfterTooSoon, | ||
| isVacationDelegateClearDatePassed, | ||
| } from '@libs/VacationDelegateUtils'; | ||
|
|
||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import INPUT_IDS from '@src/types/form/VacationDelegateForm'; | ||
| import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; | ||
| import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails'; | ||
| import type {BaseVacationDelegate} from '@src/types/onyx/VacationDelegate'; | ||
|
|
||
| import React from 'react'; | ||
| import {View} from 'react-native'; | ||
|
|
||
| import type {FormInputErrors, FormOnyxValues} from './Form/types'; | ||
|
|
||
| import DatePicker from './DatePicker'; | ||
| import FormProvider from './Form/FormProvider'; | ||
| import InputWrapper from './Form/InputWrapper'; | ||
| import MenuItemAction from './MenuItem/presets/MenuItemAction'; | ||
| import OfflineWithFeedback from './OfflineWithFeedback'; | ||
| import Text from './Text'; | ||
| import TimeModalPicker from './TimeModalPicker'; | ||
| import VacationDelegateMenuItem from './VacationDelegateMenuItem'; | ||
|
|
||
| type VacationDelegateFormProps = { | ||
| /** The saved vacation delegate */ | ||
| vacationDelegate?: BaseVacationDelegate; | ||
|
|
||
| /** Text shown above the form */ | ||
| description?: string; | ||
|
|
||
| /** | ||
| * Timezone the clear date and time are picked in. Defaults to the current user's Profile timezone. | ||
| * A domain admin passes the member's, so the delegate clears at the end of the vacationer's day, not the admin's. | ||
| */ | ||
| timezone?: SelectedTimezone; | ||
|
|
||
| /** Opens the member selection screen. The picked member is stored in the form draft. */ | ||
| onChangeDelegate: () => void; | ||
|
|
||
| /** | ||
| * Saves the picked delegate | ||
| * @param delegate - Login of the picked delegate | ||
| * @param clearAfter - UTC datetime (yyyy-MM-dd HH:mm:ss) when the delegate clears, or undefined when it never clears | ||
| */ | ||
| onSubmit: (delegate: string, clearAfter: string | undefined) => void; | ||
|
|
||
| /** Removes the saved delegate */ | ||
| onRemove: () => void; | ||
|
|
||
| /** Errors left by the last change to the saved delegate */ | ||
| errors?: Errors; | ||
|
|
||
| /** Pending action of the last change to the saved delegate */ | ||
| pendingAction?: PendingAction; | ||
|
|
||
| /** Dismisses `errors` */ | ||
| onCloseError?: () => void; | ||
|
|
||
| /** Whether the save request is still running, which shows the spinner on the Save button */ | ||
| isLoading?: boolean; | ||
| }; | ||
|
|
||
| function VacationDelegateForm({ | ||
| vacationDelegate, | ||
| description, | ||
| timezone: timezoneProp, | ||
| onChangeDelegate, | ||
| onSubmit, | ||
| onRemove, | ||
| errors, | ||
| pendingAction, | ||
| onCloseError, | ||
| isLoading = false, | ||
| }: VacationDelegateFormProps) { | ||
| const styles = useThemeStyles(); | ||
| const {translate, dateFnsLocale, getLocalDateFromDatetime} = useLocalize(); | ||
| const icons = useMemoizedLazyExpensifyIcons(['CalendarSolid', 'Trashcan']); | ||
| const {timezone: currentUserTimezone} = useCurrentUserPersonalDetails(); | ||
| const timezone = timezoneProp ?? currentUserTimezone?.selected; | ||
| const [draftValues] = useOnyx(ONYXKEYS.FORMS.VACATION_DELEGATE_FORM_DRAFT); | ||
|
|
||
| const savedDelegate = getActiveVacationDelegate(vacationDelegate); | ||
| // Remove reflects the delegate saved when the form opened. Otherwise the optimistic write from this form's own Save | ||
| // would make the button appear while the request runs and the RHP closes. | ||
| const savedDelegateOnOpen = useInitialSelection(savedDelegate); | ||
| const savedClearDateTime = savedDelegate ? getVacationDelegateClearDateTime(vacationDelegate?.clearAfter, timezone) : ''; | ||
| const savedClearDate = savedDelegate ? getVacationDelegateClearDate(vacationDelegate?.clearAfter, timezone) : ''; | ||
| // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- an empty draft means no member was picked yet | ||
| const delegate = draftValues?.[INPUT_IDS.DELEGATE] || savedDelegate; | ||
| const clearDate = draftValues?.[INPUT_IDS.CLEAR_AFTER_DATE] ?? savedClearDate; | ||
| const formattedClearDateTime = formatVacationDelegateClearDateTime( | ||
| getVacationDelegateLocalClearDateTime(clearDate, draftValues?.[INPUT_IDS.CLEAR_AFTER_TIME] ?? savedClearDateTime), | ||
| dateFnsLocale, | ||
| ); | ||
|
|
||
| const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.VACATION_DELEGATE_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.VACATION_DELEGATE_FORM> => { | ||
| const formErrors: FormInputErrors<typeof ONYXKEYS.FORMS.VACATION_DELEGATE_FORM> = {}; | ||
| if (!values[INPUT_IDS.CLEAR_AFTER_DATE]) { | ||
| return formErrors; | ||
| } | ||
|
|
||
| // Both checks read the picked date and time in the form's timezone, the same one clearAfter is built from, not the device's | ||
| if (isVacationDelegateClearDatePassed(values[INPUT_IDS.CLEAR_AFTER_DATE], timezone)) { | ||
| formErrors[INPUT_IDS.CLEAR_AFTER_DATE] = translate('common.error.dateInvalid'); | ||
| } else if (isVacationDelegateClearAfterTooSoon(getVacationDelegateClearAfter(values[INPUT_IDS.CLEAR_AFTER_DATE], values[INPUT_IDS.CLEAR_AFTER_TIME], timezone))) { | ||
| formErrors[INPUT_IDS.CLEAR_AFTER_TIME] = translate('common.error.invalidTimeShouldBeFuture'); | ||
| } | ||
| return formErrors; | ||
| }; | ||
|
|
||
| const submit = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.VACATION_DELEGATE_FORM>) => { | ||
| if (!delegate) { | ||
| return; | ||
| } | ||
|
|
||
| onSubmit(delegate, getVacationDelegateClearAfter(values[INPUT_IDS.CLEAR_AFTER_DATE], values[INPUT_IDS.CLEAR_AFTER_TIME], timezone)); | ||
| }; | ||
|
|
||
| return ( | ||
| <FormProvider | ||
| formID={ONYXKEYS.FORMS.VACATION_DELEGATE_FORM} | ||
| style={styles.flexGrow1} | ||
| onSubmit={submit} | ||
| validate={validate} | ||
| submitButtonText={translate('common.save')} | ||
| submitButtonStyles={styles.ph5} | ||
| isSubmitDisabled={!delegate} | ||
| isLoading={isLoading} | ||
| // The press spinner only resets when the screen regains focus, which the error modal never takes away, so it would stay on | ||
| // after a failed save. The page drives the spinner through isLoading instead. | ||
| shouldShowLoadingImmediatelyOnPress={false} | ||
| enabledWhenOffline | ||
| shouldHideFixErrorsAlert | ||
| > | ||
| {!!description && <Text style={[styles.mh5, styles.mb4]}>{description}</Text>} | ||
| <VacationDelegateMenuItem | ||
| vacationDelegate={{delegate}} | ||
| label={translate('statusPage.vacationDelegate.chooseDelegate')} | ||
| errors={errors} | ||
| pendingAction={pendingAction} | ||
| onCloseError={onCloseError} | ||
| onPress={onChangeDelegate} | ||
| /> | ||
| {/* The clear after date and time belong to the same pending save as the delegate, so they grey out with it */} | ||
| <OfflineWithFeedback | ||
| pendingAction={pendingAction} | ||
| shouldHideOnDelete={false} | ||
| > | ||
| <View style={styles.ph5}> | ||
| {/* Shows the calendar icon while empty and the clear button once a date is picked, so the delegate can go back to never clearing. | ||
| The earliest day is today in the form's timezone, so it matches the validation even when the device is in another timezone. */} | ||
| <InputWrapper | ||
| InputComponent={DatePicker} | ||
| inputID={INPUT_IDS.CLEAR_AFTER_DATE} | ||
| label={translate('statusPage.vacationDelegate.clearAfterRecommended')} | ||
| defaultValue={savedClearDate} | ||
| minDate={getLocalDateFromDatetime(undefined, timezone)} | ||
| icon={icons.CalendarSolid} | ||
| shouldSaveDraft | ||
| /> | ||
| </View> | ||
| {/* The time only means something once a day is picked. Until it is changed, the delegate clears at the end of that day. */} | ||
| {!!clearDate && ( | ||
| <View style={styles.mt2}> | ||
| <InputWrapper | ||
| InputComponent={TimeModalPicker} | ||
| inputID={INPUT_IDS.CLEAR_AFTER_TIME} | ||
| label={translate('statusPage.time')} | ||
| defaultValue={savedClearDateTime || DateUtils.getEndOfToday()} | ||
| shouldSaveDraft | ||
| /> | ||
| </View> | ||
| )} | ||
| {/* The bottom margin matches the date input's own, so the remove button below sits the same distance from whichever is last */} | ||
| {!!formattedClearDateTime && ( | ||
| <Text style={[styles.mh5, styles.textLabelSupporting, styles.mt2, styles.mb2]}>{translate('statusPage.vacationDelegate.willClearOn', formattedClearDateTime)}</Text> | ||
| )} | ||
| </OfflineWithFeedback> | ||
| {!!savedDelegateOnOpen && !!savedDelegate && ( | ||
| <View style={styles.mt4}> | ||
| <MenuItemAction | ||
| title={translate('statusPage.vacationDelegate.removeDelegate')} | ||
| icon={icons.Trashcan} | ||
| onPress={onRemove} | ||
| /> | ||
| </View> | ||
| )} | ||
| </FormProvider> | ||
| ); | ||
| } | ||
|
|
||
| export default VacationDelegateForm; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 After a failed save, the Save spinner never stops once Got it is clicked, and Save stops responding.
FormProvider's press spinner (shouldShowLoadingImmediatelyOnPress, on by default) only resets when the screen regains focus. The confirm modal never takes focus away, sosubmit()keeps returning early onisLoading.Can we let the page drive the spinner instead ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in b4c2607. The form takes an
isLoadingprop and setsshouldShowLoadingImmediatelyOnPress={false}, so the page drives the spinner. The domain form saves with a queued write and closes right away, so it doesn't passisLoading.