Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ const CONST = {
START: 'start',
STOP: 'stop',
},
OOO_DURATION_UNITS: {
HOUR: 'hours',
DAY: 'days',
WEEK: 'weeks',
MONTH: 'months',
},
},

RECEIPT_CAMERA: {
Expand Down
3 changes: 3 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,8 @@ const ONYXKEYS = {
ONBOARDING_WORKSPACE_DETAILS_FORM_DRAFT: 'onboardingWorkspaceDetailsFormDraft',
ROOM_NAME_FORM: 'roomNameForm',
ROOM_NAME_FORM_DRAFT: 'roomNameFormDraft',
CHRONOS_SCHEDULE_OOO_FORM: 'chronosScheduleOOOForm',
CHRONOS_SCHEDULE_OOO_FORM_DRAFT: 'chronosScheduleOOOFormDraft',
REPORT_DESCRIPTION_FORM: 'reportDescriptionForm',
REPORT_DESCRIPTION_FORM_DRAFT: 'reportDescriptionFormDraft',
LEGAL_NAME_FORM: 'legalNameForm',
Expand Down Expand Up @@ -1119,6 +1121,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm;
[ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM]: FormTypes.DisplayNameForm;
[ONYXKEYS.FORMS.ROOM_NAME_FORM]: FormTypes.RoomNameForm;
[ONYXKEYS.FORMS.CHRONOS_SCHEDULE_OOO_FORM]: FormTypes.ChronosScheduleOOOForm;
[ONYXKEYS.FORMS.REPORT_DESCRIPTION_FORM]: FormTypes.ReportDescriptionForm;
[ONYXKEYS.FORMS.LEGAL_NAME_FORM]: FormTypes.LegalNameForm;
[ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: FormTypes.WorkspaceInviteMessageForm;
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,10 @@ const ROUTES = {
route: 'r/:reportID/settings/columns',
getRoute: (reportID: string) => `r/${reportID}/settings/columns` as const,
},
CHRONOS_SCHEDULE_OOO: {
route: 'r/:reportID/chronos/schedule-ooo',
getRoute: (reportID: string) => `r/${reportID}/chronos/schedule-ooo` as const,
},
SPLIT_BILL_DETAILS: {
route: 'r/:reportID/split/:reportActionID',
getRoute: (reportID: string | undefined, reportActionID: string, backTo?: string) => {
Expand Down
2 changes: 2 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ const SCREENS = {
DOMAIN: 'Domain',
EXPENSE_REPORT: 'ExpenseReport',
MULTIFACTOR_AUTHENTICATION: 'MultifactorAuthentication',
CHRONOS_SCHEDULE_OOO: 'Chronos_Schedule_OOO',
},
REPORT_CARD_ACTIVATE: 'Report_Card_Activate_Root',
SAML_SIGN_IN: 'SAMLSignIn',
Expand Down Expand Up @@ -923,6 +924,7 @@ const SCREENS = {
AUTO_SUBMIT_ROOT: 'AutoSubmit_Modal_Root',
CHANGE_POLICY_EDUCATIONAL_ROOT: 'ChangePolicyEducational_Root',
REPORT_DESCRIPTION_ROOT: 'Report_Description_Root',
CHRONOS_SCHEDULE_OOO_ROOT: 'Chronos_Schedule_OOO_Root',
REPORT_PARTICIPANTS: {
ROOT: 'ReportParticipants_Root',
INVITE: 'ReportParticipants_Invite',
Expand Down
17 changes: 16 additions & 1 deletion src/components/AmountForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ type AmountFormProps = {
/** Whether to hide the currency symbol */
hideCurrencySymbol?: boolean;

/** When true, shows the trailing dropdown (same as currency picker in IOU amount flows) */
shouldShowCurrencyButton?: boolean;

/** Text on the trailing dropdown button. Use with `shouldShowCurrencyButton` when the suffix is not a currency code (e.g. duration unit). */
currencyButtonLabel?: string;

/** Accessibility label for the trailing dropdown */
currencyButtonAccessibilityLabel?: string;

/** Whether the input should be disabled */
disabled?: boolean;

Expand Down Expand Up @@ -77,6 +86,9 @@ function AmountForm({
label,
decimals: decimalsProp,
hideCurrencySymbol = false,
shouldShowCurrencyButton = false,
currencyButtonLabel,
currencyButtonAccessibilityLabel,
disabled = false,
autoFocus,
autoGrowExtraSpace,
Expand All @@ -94,7 +106,7 @@ function AmountForm({
return (
<NumberWithSymbolForm
label={label}
value={value}
value={value ?? ''}
decimals={decimals}
currency={currency}
displayAsTextInput={displayAsTextInput}
Expand All @@ -113,6 +125,9 @@ function AmountForm({
symbolPosition={CONST.TEXT_INPUT_SYMBOL_POSITION.PREFIX}
isSymbolPressable={isCurrencyPressable}
hideSymbol={hideCurrencySymbol}
shouldShowCurrencyButton={shouldShowCurrencyButton}
currencyButtonLabel={currencyButtonLabel}
currencyButtonAccessibilityLabel={currencyButtonAccessibilityLabel}
maxLength={amountMaxLength}
errorText={errorText}
style={displayAsTextInput ? undefined : styles.iouAmountTextInput}
Expand Down
28 changes: 24 additions & 4 deletions src/components/ChronosTimerHeaderButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,25 @@ import useOnyx from '@hooks/useOnyx';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useThemeStyles from '@hooks/useThemeStyles';
import {isChronosTimerRunningFromVisibleActions} from '@libs/ChronosUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getSortedReportActionsForDisplay} from '@libs/ReportActionsUtils';
import {canUserPerformWriteAction, canWriteInReport} from '@libs/ReportUtils';
import {addComment} from '@userActions/Report';
import {callFunctionIfActionIsAllowed} from '@userActions/Session';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {ReportActions} from '@src/types/onyx/ReportAction';
import Button from './Button';
import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
import type {DropdownOption} from './ButtonWithDropdownMenu/types';

type ChronosTimerHeaderButtonProps = {
report: OnyxTypes.Report;
};

type ChronosAction = 'timer' | 'scheduleOOO';

function ChronosTimerHeaderButton({report}: ChronosTimerHeaderButtonProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
Expand Down Expand Up @@ -59,16 +64,31 @@ function ChronosTimerHeaderButton({report}: ChronosTimerHeaderButtonProps) {
});
}

const options: Array<DropdownOption<ChronosAction>> = [
{
value: 'timer' as const,
text: translate(isTimerRunning ? 'chronos.stopTimer' : 'chronos.startTimer'),
},
{
value: 'scheduleOOO' as const,
text: translate('chronos.scheduleOOO'),
onSelected: () => Navigation.navigate(ROUTES.CHRONOS_SCHEDULE_OOO.getRoute(report.reportID)),
shouldUpdateSelectedIndex: false,
},
];

if (!canWriteInReport(report)) {
return null;
}

return (
<View style={[styles.flexRow, styles.alignItemsCenter, styles.justifyContentEnd]}>
<Button
<ButtonWithDropdownMenu<ChronosAction>
success={!isTimerRunning}
text={translate(isTimerRunning ? 'chronos.stopTimer' : 'chronos.startTimer')}
onPress={callFunctionIfActionIsAllowed(sendCommentToChronos)}
onPress={() => {
callFunctionIfActionIsAllowed(sendCommentToChronos)();
}}
options={options}
style={styles.flex1}
sentryLabel={CONST.SENTRY_LABEL.HEADER_VIEW.CHRONOS_TIMER_BUTTON}
/>
Expand Down
8 changes: 5 additions & 3 deletions src/components/DatePicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ function DatePicker({
const {translate} = useLocalize();

const [isModalVisible, setIsModalVisible] = useState(false);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const [selectedDate, setSelectedDate] = useState(value || defaultValue || undefined);
const [selectedDate, setSelectedDate] = useState(() => value ?? defaultValue ?? '');
const [popoverPosition, setPopoverPosition] = useState({horizontal: 0, vertical: 0});
const textInputRef = useRef<BaseTextInputRef>(null);
const anchorRef = useRef<View>(null);
Expand All @@ -51,7 +50,10 @@ function DatePicker({
if (shouldSaveDraft && formID) {
setDraftValues(formID, {[inputID]: selectedDate});
}
if (selectedDate === value || !value) {
if (selectedDate === value) {
return;
}
if (value === undefined) {
return;
}

Expand Down
37 changes: 31 additions & 6 deletions src/components/NumberWithSymbolForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ type NumberWithSymbolFormProps = {

/** Callback when currency button is pressed */
onCurrencyButtonPress?: () => void;

/**
* 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.
*/
currencyButtonLabel?: string;

/** Accessibility label for the trailing dropdown button (defaults to currency-based copy when unset) */
currencyButtonAccessibilityLabel?: string;
} & Omit<TextInputWithSymbolProps, 'formattedAmount' | 'onAmountChange' | 'placeholder' | 'onSelectionChange' | 'onKeyPress' | 'onMouseDown' | 'onMouseUp'>;

type NumberWithSymbolFormRef = {
Expand Down Expand Up @@ -173,6 +181,8 @@ function NumberWithSymbolForm({
shouldShowFlipButton = false,
shouldShowCurrencyButton = false,
onCurrencyButtonPress,
currencyButtonLabel,
currencyButtonAccessibilityLabel,
...props
}: NumberWithSymbolFormProps) {
const icons = useMemoizedLazyExpensifyIcons(['DownArrow', 'PlusMinus']);
Expand All @@ -199,6 +209,9 @@ function NumberWithSymbolForm({
// The ref is used to ignore any onSelectionChange event that happens while we are updating the selection manually in setNewNumber
const willSelectionBeUpdatedManually = useRef(false);

const currencyOrUnitButtonText = currencyButtonLabel ?? currency;
const onTrailingDropdownPress = onCurrencyButtonPress ?? onSymbolButtonPress;

const {setMouseDown, setMouseUp} = useMouseActions();
const handleMouseDown = (e: React.MouseEvent<Element, MouseEvent>) => {
e.stopPropagation();
Expand Down Expand Up @@ -430,21 +443,33 @@ function NumberWithSymbolForm({
isDisabled={disabled}
/>
)}
{shouldShowCurrencyButton && !!currency && (
{shouldShowCurrencyButton && !!currencyOrUnitButtonText && (
<Button
shouldShowRightIcon
small
iconRight={icons.DownArrow}
onPress={onCurrencyButtonPress}
onPress={onTrailingDropdownPress}
isContentCentered
text={currency}
accessibilityLabel={`${translate('common.selectCurrency')}, ${currency}`}
text={currencyOrUnitButtonText}
accessibilityLabel={currencyButtonAccessibilityLabel ?? `${translate('common.selectCurrency')}, ${currencyOrUnitButtonText}`}
isDisabled={disabled}
/>
)}
</View>
);
}, [shouldShowFlipButton, allowNegativeInput, disabled, shouldShowCurrencyButton, styles, icons, handleFlipPress, onCurrencyButtonPress, currency, translate]);
}, [
shouldShowFlipButton,
allowNegativeInput,
disabled,
shouldShowCurrencyButton,
styles,
icons,
handleFlipPress,
onTrailingDropdownPress,
currencyOrUnitButtonText,
currencyButtonAccessibilityLabel,
translate,
]);

if (displayAsTextInput) {
return (
Expand All @@ -463,7 +488,7 @@ function NumberWithSymbolForm({
textInput.current = newRef;
}}
disabled={disabled}
prefixCharacter={symbol}
prefixCharacter={hideSymbol ? '' : symbol}
prefixStyle={styles.colorMuted}
keyboardType={props.keyboardType ?? CONST.KEYBOARD_TYPE.DECIMAL_PAD}
// On android autoCapitalize="words" is necessary when keyboardType="decimal-pad" or inputMode="decimal" to prevent input lag.
Expand Down
37 changes: 15 additions & 22 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1701,17 +1701,13 @@ const translations: TranslationDeepObject<typeof en> = {
backdropLabel: 'Modal-Hintergrund',
},
nextStep: {
// All nextStep.message functions share a common positional signature (actor, actorType, eta, etaType) for type compatibility, so unused params are expected
/* eslint-disable @typescript-eslint/no-unused-vars */
message: {
[CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_ADD_TRANSACTIONS]: (
actor: string,
actorType: ValueOf<typeof CONST.NEXT_STEP.ACTOR_TYPE>,
_eta?: string,
_etaType?: ValueOf<typeof CONST.NEXT_STEP.ETA_TYPE>,
) => {
// Disabling the default-case lint rule here is actually safer as this forces us to make the switch cases exhaustive
// eslint-disable-next-line default-case
switch (actorType) {
case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
return `Warte darauf, dass <strong>Sie</strong> Spesen hinzufügen.`;
Expand All @@ -1727,8 +1723,6 @@ const translations: TranslationDeepObject<typeof en> = {
_eta?: string,
_etaType?: ValueOf<typeof CONST.NEXT_STEP.ETA_TYPE>,
) => {
// Disabling the default-case lint rule here is actually safer as this forces us to make the switch cases exhaustive
// eslint-disable-next-line default-case
switch (actorType) {
case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
return `Wartet darauf, dass <strong>Sie</strong> Spesen einreichen.`;
Expand All @@ -1750,8 +1744,6 @@ const translations: TranslationDeepObject<typeof en> = {
_eta?: string,
_etaType?: ValueOf<typeof CONST.NEXT_STEP.ETA_TYPE>,
) => {
// Disabling the default-case lint rule here is actually safer as this forces us to make the switch cases exhaustive
// eslint-disable-next-line default-case
switch (actorType) {
case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
return `Warten darauf, dass <strong>Sie</strong> ein Bankkonto hinzufügen.`;
Expand All @@ -1771,8 +1763,6 @@ const translations: TranslationDeepObject<typeof en> = {
if (eta) {
formattedETA = etaType === CONST.NEXT_STEP.ETA_TYPE.DATE_TIME ? `am ${eta} eines jeden Monats` : ` ${eta}`;
}
// Disabling the default-case lint rule here is actually safer as this forces us to make the switch cases exhaustive
// eslint-disable-next-line default-case
switch (actorType) {
case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
return `Warten darauf, dass <strong>Ihre</strong> Ausgaben automatisch eingereicht werden${formattedETA}.`;
Expand All @@ -1788,8 +1778,6 @@ const translations: TranslationDeepObject<typeof en> = {
_eta?: string,
_etaType?: ValueOf<typeof CONST.NEXT_STEP.ETA_TYPE>,
) => {
// Disabling the default-case lint rule here is actually safer as this forces us to make the switch cases exhaustive
// eslint-disable-next-line default-case
switch (actorType) {
case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
return `Warten auf <strong>Sie</strong>, um die Probleme zu beheben.`;
Expand All @@ -1805,8 +1793,6 @@ const translations: TranslationDeepObject<typeof en> = {
_eta?: string,
_etaType?: ValueOf<typeof CONST.NEXT_STEP.ETA_TYPE>,
) => {
// Disabling the default-case lint rule here is actually safer as this forces us to make the switch cases exhaustive
// eslint-disable-next-line default-case
switch (actorType) {
case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
return `Es wird darauf gewartet, dass <strong>Sie</strong> Spesen genehmigen.`;
Expand All @@ -1822,8 +1808,6 @@ const translations: TranslationDeepObject<typeof en> = {
_eta?: string,
_etaType?: ValueOf<typeof CONST.NEXT_STEP.ETA_TYPE>,
) => {
// Disabling the default-case lint rule here is actually safer as this forces us to make the switch cases exhaustive
// eslint-disable-next-line default-case
switch (actorType) {
case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
return `Warte darauf, dass <strong>Sie</strong> diesen Bericht exportieren.`;
Expand All @@ -1839,8 +1823,6 @@ const translations: TranslationDeepObject<typeof en> = {
_eta?: string,
_etaType?: ValueOf<typeof CONST.NEXT_STEP.ETA_TYPE>,
) => {
// Disabling the default-case lint rule here is actually safer as this forces us to make the switch cases exhaustive
// eslint-disable-next-line default-case
switch (actorType) {
case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
return `Wartet darauf, dass <strong>Sie</strong> Spesen bezahlen.`;
Expand All @@ -1856,8 +1838,6 @@ const translations: TranslationDeepObject<typeof en> = {
_eta?: string,
_etaType?: ValueOf<typeof CONST.NEXT_STEP.ETA_TYPE>,
) => {
// Disabling the default-case lint rule here is actually safer as this forces us to make the switch cases exhaustive
// eslint-disable-next-line default-case
switch (actorType) {
case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
return `Warten darauf, dass <strong>Sie</strong> die Einrichtung eines Geschäftskontos abschließen.`;
Expand Down Expand Up @@ -7378,8 +7358,6 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
updatedDefaultTitle: (newDefaultTitle: string, oldDefaultTitle: string) => `benutzerdefinierte Berichtstitelformel in „${newDefaultTitle}“ geändert (zuvor „${oldDefaultTitle}“)`,
updatedOwnership: (oldOwnerEmail: string, oldOwnerName: string, policyName: string) => `hat die Inhaberschaft von ${policyName} von ${oldOwnerName} (${oldOwnerEmail}) übernommen`,
updatedAutoHarvesting: (enabled: boolean) => `${enabled ? 'aktiviert' : 'deaktiviert'} geplante Einreichung`,
// This function requires 11 params to match the budget notification data model; reducing further would hurt readability
// eslint-disable-next-line @typescript-eslint/max-params
updatedIndividualBudgetNotification: (
budgetAmount: string,
budgetFrequency: string,
Expand Down Expand Up @@ -7875,6 +7853,21 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
oooEventSummaryPartialDay: (summary: string, timePeriod: string, date: string) => `${summary} von ${timePeriod} am ${date}`,
startTimer: 'Timer starten',
stopTimer: 'Timer stoppen',
scheduleOOO: 'Abwesenheit planen',
scheduleOOOTitle: 'Abwesenheit planen',
date: 'Datum',
time: 'Zeit (24-Stunden-Format)',
durationAmount: 'Dauer',
durationUnit: 'Einheit',
reason: 'Grund',
workingPercentage: 'Arbeitsprozentsatz',
dateRequired: 'Datum ist erforderlich.',
invalidTimeFormat: 'Bitte geben Sie eine gültige Uhrzeit im 24‑Stunden-Format ein (z. B. 14:30).',
enterANumber: 'Bitte geben Sie eine Zahl ein.',
hour: 'Stunden',
day: 'Tage',
week: 'Wochen',
month: 'Monate',
},
footer: {
features: 'Funktionen',
Expand Down
Loading
Loading