Skip to content

Commit 65098ab

Browse files
authored
Merge pull request Expensify#87543 from Expensify/tgolen-chronos-oooUI
[Payment due @dukenv0307] Add a UI to Chronos for creating OOO events
2 parents 87a513c + 92c8bbe commit 65098ab

28 files changed

Lines changed: 622 additions & 16 deletions

src/CONST/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,12 @@ const CONST = {
290290
START: 'start',
291291
STOP: 'stop',
292292
},
293+
OOO_DURATION_UNITS: {
294+
HOUR: 'hours',
295+
DAY: 'days',
296+
WEEK: 'weeks',
297+
MONTH: 'months',
298+
},
293299
},
294300

295301
RECEIPT_CAMERA: {

src/ONYXKEYS.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,8 @@ const ONYXKEYS = {
890890
ONBOARDING_WORKSPACE_DETAILS_FORM_DRAFT: 'onboardingWorkspaceDetailsFormDraft',
891891
ROOM_NAME_FORM: 'roomNameForm',
892892
ROOM_NAME_FORM_DRAFT: 'roomNameFormDraft',
893+
CHRONOS_SCHEDULE_OOO_FORM: 'chronosScheduleOOOForm',
894+
CHRONOS_SCHEDULE_OOO_FORM_DRAFT: 'chronosScheduleOOOFormDraft',
893895
REPORT_DESCRIPTION_FORM: 'reportDescriptionForm',
894896
REPORT_DESCRIPTION_FORM_DRAFT: 'reportDescriptionFormDraft',
895897
LEGAL_NAME_FORM: 'legalNameForm',
@@ -1119,6 +1121,7 @@ type OnyxFormValuesMapping = {
11191121
[ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm;
11201122
[ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM]: FormTypes.DisplayNameForm;
11211123
[ONYXKEYS.FORMS.ROOM_NAME_FORM]: FormTypes.RoomNameForm;
1124+
[ONYXKEYS.FORMS.CHRONOS_SCHEDULE_OOO_FORM]: FormTypes.ChronosScheduleOOOForm;
11221125
[ONYXKEYS.FORMS.REPORT_DESCRIPTION_FORM]: FormTypes.ReportDescriptionForm;
11231126
[ONYXKEYS.FORMS.LEGAL_NAME_FORM]: FormTypes.LegalNameForm;
11241127
[ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: FormTypes.WorkspaceInviteMessageForm;

src/ROUTES.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,10 @@ const ROUTES = {
996996
route: 'r/:reportID/settings/columns',
997997
getRoute: (reportID: string) => `r/${reportID}/settings/columns` as const,
998998
},
999+
CHRONOS_SCHEDULE_OOO: {
1000+
route: 'r/:reportID/chronos/schedule-ooo',
1001+
getRoute: (reportID: string) => `r/${reportID}/chronos/schedule-ooo` as const,
1002+
},
9991003
SPLIT_BILL_DETAILS: {
10001004
route: 'r/:reportID/split/:reportActionID',
10011005
getRoute: (reportID: string | undefined, reportActionID: string, backTo?: string) => {

src/SCREENS.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ const SCREENS = {
339339
DOMAIN: 'Domain',
340340
EXPENSE_REPORT: 'ExpenseReport',
341341
MULTIFACTOR_AUTHENTICATION: 'MultifactorAuthentication',
342+
CHRONOS_SCHEDULE_OOO: 'Chronos_Schedule_OOO',
342343
},
343344
REPORT_CARD_ACTIVATE: 'Report_Card_Activate_Root',
344345
SAML_SIGN_IN: 'SAMLSignIn',
@@ -924,6 +925,7 @@ const SCREENS = {
924925
AUTO_SUBMIT_ROOT: 'AutoSubmit_Modal_Root',
925926
CHANGE_POLICY_EDUCATIONAL_ROOT: 'ChangePolicyEducational_Root',
926927
REPORT_DESCRIPTION_ROOT: 'Report_Description_Root',
928+
CHRONOS_SCHEDULE_OOO_ROOT: 'Chronos_Schedule_OOO_Root',
927929
REPORT_PARTICIPANTS: {
928930
ROOT: 'ReportParticipants_Root',
929931
INVITE: 'ReportParticipants_Invite',

src/components/AmountForm.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ type AmountFormProps = {
4646
/** Whether to hide the currency symbol */
4747
hideCurrencySymbol?: boolean;
4848

49+
/** When true, shows the trailing dropdown (same as currency picker in IOU amount flows) */
50+
shouldShowCurrencyButton?: boolean;
51+
52+
/** Text on the trailing dropdown button. Use with `shouldShowCurrencyButton` when the suffix is not a currency code (e.g. duration unit). */
53+
currencyButtonLabel?: string;
54+
55+
/** Accessibility label for the trailing dropdown */
56+
currencyButtonAccessibilityLabel?: string;
57+
4958
/** Whether the input should be disabled */
5059
disabled?: boolean;
5160

@@ -77,6 +86,9 @@ function AmountForm({
7786
label,
7887
decimals: decimalsProp,
7988
hideCurrencySymbol = false,
89+
shouldShowCurrencyButton = false,
90+
currencyButtonLabel,
91+
currencyButtonAccessibilityLabel,
8092
disabled = false,
8193
autoFocus,
8294
autoGrowExtraSpace,
@@ -94,7 +106,7 @@ function AmountForm({
94106
return (
95107
<NumberWithSymbolForm
96108
label={label}
97-
value={value}
109+
value={value ?? ''}
98110
decimals={decimals}
99111
currency={currency}
100112
displayAsTextInput={displayAsTextInput}
@@ -113,6 +125,9 @@ function AmountForm({
113125
symbolPosition={CONST.TEXT_INPUT_SYMBOL_POSITION.PREFIX}
114126
isSymbolPressable={isCurrencyPressable}
115127
hideSymbol={hideCurrencySymbol}
128+
shouldShowCurrencyButton={shouldShowCurrencyButton}
129+
currencyButtonLabel={currencyButtonLabel}
130+
currencyButtonAccessibilityLabel={currencyButtonAccessibilityLabel}
116131
maxLength={amountMaxLength}
117132
errorText={errorText}
118133
style={displayAsTextInput ? undefined : styles.iouAmountTextInput}

src/components/ChronosTimerHeaderButton.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,25 @@ import useOnyx from '@hooks/useOnyx';
99
import useReportIsArchived from '@hooks/useReportIsArchived';
1010
import useThemeStyles from '@hooks/useThemeStyles';
1111
import {isChronosTimerRunningFromVisibleActions} from '@libs/ChronosUtils';
12+
import Navigation from '@libs/Navigation/Navigation';
1213
import {getSortedReportActionsForDisplay} from '@libs/ReportActionsUtils';
1314
import {canUserPerformWriteAction, canWriteInReport} from '@libs/ReportUtils';
1415
import {addComment} from '@userActions/Report';
1516
import {callFunctionIfActionIsAllowed} from '@userActions/Session';
1617
import CONST from '@src/CONST';
1718
import ONYXKEYS from '@src/ONYXKEYS';
19+
import ROUTES from '@src/ROUTES';
1820
import type * as OnyxTypes from '@src/types/onyx';
1921
import type {ReportActions} from '@src/types/onyx/ReportAction';
20-
import Button from './Button';
22+
import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
23+
import type {DropdownOption} from './ButtonWithDropdownMenu/types';
2124

2225
type ChronosTimerHeaderButtonProps = {
2326
report: OnyxTypes.Report;
2427
};
2528

29+
type ChronosAction = 'timer' | 'scheduleOOO';
30+
2631
function ChronosTimerHeaderButton({report}: ChronosTimerHeaderButtonProps) {
2732
const {translate} = useLocalize();
2833
const styles = useThemeStyles();
@@ -59,16 +64,31 @@ function ChronosTimerHeaderButton({report}: ChronosTimerHeaderButtonProps) {
5964
});
6065
}
6166

67+
const options: Array<DropdownOption<ChronosAction>> = [
68+
{
69+
value: 'timer' as const,
70+
text: translate(isTimerRunning ? 'chronos.stopTimer' : 'chronos.startTimer'),
71+
},
72+
{
73+
value: 'scheduleOOO' as const,
74+
text: translate('chronos.scheduleOOO'),
75+
onSelected: () => Navigation.navigate(ROUTES.CHRONOS_SCHEDULE_OOO.getRoute(report.reportID)),
76+
shouldUpdateSelectedIndex: false,
77+
},
78+
];
79+
6280
if (!canWriteInReport(report)) {
6381
return null;
6482
}
6583

6684
return (
6785
<View style={[styles.flexRow, styles.alignItemsCenter, styles.justifyContentEnd]}>
68-
<Button
86+
<ButtonWithDropdownMenu<ChronosAction>
6987
success={!isTimerRunning}
70-
text={translate(isTimerRunning ? 'chronos.stopTimer' : 'chronos.startTimer')}
71-
onPress={callFunctionIfActionIsAllowed(sendCommentToChronos)}
88+
onPress={() => {
89+
callFunctionIfActionIsAllowed(sendCommentToChronos)();
90+
}}
91+
options={options}
7292
style={styles.flex1}
7393
sentryLabel={CONST.SENTRY_LABEL.HEADER_VIEW.CHRONOS_TIMER_BUTTON}
7494
/>

src/components/DatePicker/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ function DatePicker({
3939
const {translate} = useLocalize();
4040

4141
const [isModalVisible, setIsModalVisible] = useState(false);
42-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
43-
const [selectedDate, setSelectedDate] = useState(value || defaultValue || undefined);
42+
const [selectedDate, setSelectedDate] = useState(() => value ?? defaultValue ?? '');
4443
const [popoverPosition, setPopoverPosition] = useState({horizontal: 0, vertical: 0});
4544
const textInputRef = useRef<BaseTextInputRef>(null);
4645
const anchorRef = useRef<View>(null);
@@ -51,7 +50,10 @@ function DatePicker({
5150
if (shouldSaveDraft && formID) {
5251
setDraftValues(formID, {[inputID]: selectedDate});
5352
}
54-
if (selectedDate === value || !value) {
53+
if (selectedDate === value) {
54+
return;
55+
}
56+
if (value === undefined) {
5557
return;
5658
}
5759

src/components/NumberWithSymbolForm.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ type NumberWithSymbolFormProps = {
106106

107107
/** Callback when currency button is pressed */
108108
onCurrencyButtonPress?: () => void;
109+
110+
/**
111+
* Label on the trailing dropdown button (e.g. currency code). When set, used instead of `currency` so the same control can show a unit or other suffix.
112+
*/
113+
currencyButtonLabel?: string;
114+
115+
/** Accessibility label for the trailing dropdown button (defaults to currency-based copy when unset) */
116+
currencyButtonAccessibilityLabel?: string;
109117
} & Omit<TextInputWithSymbolProps, 'formattedAmount' | 'onAmountChange' | 'placeholder' | 'onSelectionChange' | 'onKeyPress' | 'onMouseDown' | 'onMouseUp'>;
110118

111119
type NumberWithSymbolFormRef = {
@@ -173,6 +181,8 @@ function NumberWithSymbolForm({
173181
shouldShowFlipButton = false,
174182
shouldShowCurrencyButton = false,
175183
onCurrencyButtonPress,
184+
currencyButtonLabel,
185+
currencyButtonAccessibilityLabel,
176186
...props
177187
}: NumberWithSymbolFormProps) {
178188
const icons = useMemoizedLazyExpensifyIcons(['DownArrow', 'PlusMinus']);
@@ -199,6 +209,9 @@ function NumberWithSymbolForm({
199209
// The ref is used to ignore any onSelectionChange event that happens while we are updating the selection manually in setNewNumber
200210
const willSelectionBeUpdatedManually = useRef(false);
201211

212+
const currencyOrUnitButtonText = currencyButtonLabel ?? currency;
213+
const onTrailingDropdownPress = onCurrencyButtonPress ?? onSymbolButtonPress;
214+
202215
const {setMouseDown, setMouseUp} = useMouseActions();
203216
const handleMouseDown = (e: React.MouseEvent<Element, MouseEvent>) => {
204217
e.stopPropagation();
@@ -430,21 +443,33 @@ function NumberWithSymbolForm({
430443
isDisabled={disabled}
431444
/>
432445
)}
433-
{shouldShowCurrencyButton && !!currency && (
446+
{shouldShowCurrencyButton && !!currencyOrUnitButtonText && (
434447
<Button
435448
shouldShowRightIcon
436449
small
437450
iconRight={icons.DownArrow}
438-
onPress={onCurrencyButtonPress}
451+
onPress={onTrailingDropdownPress}
439452
isContentCentered
440-
text={currency}
441-
accessibilityLabel={`${translate('common.selectCurrency')}, ${currency}`}
453+
text={currencyOrUnitButtonText}
454+
accessibilityLabel={currencyButtonAccessibilityLabel ?? `${translate('common.selectCurrency')}, ${currencyOrUnitButtonText}`}
442455
isDisabled={disabled}
443456
/>
444457
)}
445458
</View>
446459
);
447-
}, [shouldShowFlipButton, allowNegativeInput, disabled, shouldShowCurrencyButton, styles, icons, handleFlipPress, onCurrencyButtonPress, currency, translate]);
460+
}, [
461+
shouldShowFlipButton,
462+
allowNegativeInput,
463+
disabled,
464+
shouldShowCurrencyButton,
465+
styles,
466+
icons,
467+
handleFlipPress,
468+
onTrailingDropdownPress,
469+
currencyOrUnitButtonText,
470+
currencyButtonAccessibilityLabel,
471+
translate,
472+
]);
448473

449474
if (displayAsTextInput) {
450475
return (
@@ -463,7 +488,7 @@ function NumberWithSymbolForm({
463488
textInput.current = newRef;
464489
}}
465490
disabled={disabled}
466-
prefixCharacter={symbol}
491+
prefixCharacter={hideSymbol ? '' : symbol}
467492
prefixStyle={styles.colorMuted}
468493
keyboardType={props.keyboardType ?? CONST.KEYBOARD_TYPE.DECIMAL_PAD}
469494
// On android autoCapitalize="words" is necessary when keyboardType="decimal-pad" or inputMode="decimal" to prevent input lag.

src/components/PercentageForm.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {ForwardedRef} from 'react';
22
import React, {useCallback, useMemo, useRef} from 'react';
33
import useLocalize from '@hooks/useLocalize';
4+
import useThemeStyles from '@hooks/useThemeStyles';
45
import {addLeadingZero, replaceAllDigits, replaceCommasWithPeriod, stripSpacesFromAmount, validatePercentage} from '@libs/MoneyRequestUtils';
56
import CONST from '@src/CONST';
67
import TextInput from './TextInput';
@@ -32,8 +33,20 @@ type PercentageFormProps = BaseTextInputProps & {
3233
ref?: ForwardedRef<BaseTextInputRef>;
3334
};
3435

35-
function PercentageForm({value: amount, errorText, onInputChange, label, allowExceedingHundred = false, allowDecimal = false, allowNegative = false, ref, ...rest}: PercentageFormProps) {
36+
function PercentageForm({
37+
value: amount,
38+
errorText,
39+
onInputChange,
40+
label,
41+
allowExceedingHundred = false,
42+
allowDecimal = false,
43+
allowNegative = false,
44+
ref,
45+
suffixStyle,
46+
...rest
47+
}: PercentageFormProps) {
3648
const {toLocaleDigit, numberFormat} = useLocalize();
49+
const styles = useThemeStyles();
3750

3851
const textInput = useRef<BaseTextInputRef | null>(null);
3952

@@ -78,6 +91,7 @@ function PercentageForm({value: amount, errorText, onInputChange, label, allowEx
7891
textInput.current = newRef;
7992
}}
8093
suffixCharacter="%"
94+
suffixStyle={[styles.colorMuted, suffixStyle]}
8195
keyboardType={rest.keyboardType ?? CONST.KEYBOARD_TYPE.DECIMAL_PAD}
8296
// On android autoCapitalize="words" is necessary when keyboardType="decimal-pad" or inputMode="decimal" to prevent input lag.
8397
// See https://github.com/Expensify/App/issues/51868 for more information

src/languages/de.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7906,6 +7906,21 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
79067906
oooEventSummaryPartialDay: (summary: string, timePeriod: string, date: string) => `${summary} von ${timePeriod} am ${date}`,
79077907
startTimer: 'Timer starten',
79087908
stopTimer: 'Timer stoppen',
7909+
scheduleOOO: 'Abwesenheit planen',
7910+
scheduleOOOTitle: 'Abwesenheit planen',
7911+
date: 'Datum',
7912+
time: 'Zeit (24-Stunden-Format)',
7913+
durationAmount: 'Dauer',
7914+
durationUnit: 'Einheit',
7915+
reason: 'Grund',
7916+
workingPercentage: 'Arbeitsprozentsatz',
7917+
dateRequired: 'Datum ist erforderlich.',
7918+
invalidTimeFormat: 'Bitte geben Sie eine gültige Uhrzeit im 24‑Stunden-Format ein (z. B. 14:30).',
7919+
enterANumber: 'Bitte geben Sie eine Zahl ein.',
7920+
hour: 'Stunden',
7921+
day: 'Tage',
7922+
week: 'Wochen',
7923+
month: 'Monate',
79097924
},
79107925
footer: {
79117926
features: 'Funktionen',

0 commit comments

Comments
 (0)