Skip to content

Commit

Permalink
Merge pull request #46562 from software-mansion-labs/Guccio163/45951_…
Browse files Browse the repository at this point in the history
…implementApprovalWorkflowSection

#45951 & #45958: Implement <ApprovalWorkflowSection /> component and Enhance toLocaleOrdinal to support string ordinals
  • Loading branch information
tgolen authored Aug 5, 2024
2 parents 4a558f4 + 6fb0b6e commit 02d0d61
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 40 deletions.
9 changes: 9 additions & 0 deletions assets/images/user-check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
106 changes: 106 additions & 0 deletions src/components/ApprovalWorkflowSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, {useCallback} from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Navigation from '@libs/Navigation/Navigation';
import ROUTES from '@src/ROUTES';
import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import MenuItem from './MenuItem';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import Text from './Text';

type ApprovalWorkflowSectionProps = {
/** Single workflow displayed in this component */
approvalWorkflow: ApprovalWorkflow;

/** ID of the policy */
policyId?: string;
};

function ApprovalWorkflowSection({approvalWorkflow, policyId}: ApprovalWorkflowSectionProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate, toLocaleOrdinal} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
const openApprovalsEdit = useCallback(
() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(policyId ?? '', approvalWorkflow.approvers[0].email)),
[approvalWorkflow.approvers, policyId],
);
const approverTitle = useCallback(
(index: number) =>
approvalWorkflow.approvers.length > 1 ? `${toLocaleOrdinal(index + 1, true)} ${translate('workflowsPage.approver').toLowerCase()}` : `${translate('workflowsPage.approver')}`,
[approvalWorkflow.approvers.length, toLocaleOrdinal, translate],
);

return (
<PressableWithoutFeedback
accessibilityRole="button"
style={[styles.border, isSmallScreenWidth ? styles.p3 : styles.p4, styles.flexRow, styles.justifyContentBetween, styles.mt6, styles.mbn3]}
onPress={openApprovalsEdit}
accessibilityLabel={translate('workflowsPage.addApprovalsTitle')}
>
<View style={[styles.flex1]}>
{approvalWorkflow.isDefault && (
<View style={[styles.flexRow, styles.mb4, styles.alignItemsCenter, styles.pb1, styles.pt1]}>
<Icon
src={Expensicons.Lightbulb}
fill={theme.icon}
additionalStyles={styles.mr2}
small
/>
<Text
style={[styles.textLabelSupportingNormal]}
suppressHighlighting
>
{translate('workflowsPage.addApprovalTip')}
</Text>
</View>
)}
<MenuItem
title={translate('workflowsExpensesFromPage.title')}
style={styles.p0}
titleStyle={styles.textLabelSupportingNormal}
descriptionTextStyle={styles.textNormalThemeText}
description={approvalWorkflow.isDefault ? translate('workspace.common.everyone') : approvalWorkflow.members.map((m) => m.displayName).join(', ')}
icon={Expensicons.Users}
iconHeight={20}
iconWidth={20}
iconFill={theme.icon}
onPress={openApprovalsEdit}
shouldRemoveBackground
/>

{approvalWorkflow.approvers.map((approver, index) => (
// eslint-disable-next-line react/no-array-index-key
<View key={`approver-${approver.email}-${index}`}>
<View style={styles.workflowApprovalVerticalLine} />
<MenuItem
title={approverTitle(index)}
style={styles.p0}
titleStyle={styles.textLabelSupportingNormal}
descriptionTextStyle={styles.textNormalThemeText}
description={approver.displayName}
icon={Expensicons.UserCheck}
iconHeight={20}
iconWidth={20}
iconFill={theme.icon}
onPress={openApprovalsEdit}
shouldRemoveBackground
/>
</View>
))}
</View>
<Icon
src={Expensicons.ArrowRight}
fill={theme.icon}
additionalStyles={[styles.alignSelfCenter]}
/>
</PressableWithoutFeedback>
);
}

export default ApprovalWorkflowSection;
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ import Trashcan from '@assets/images/trashcan.svg';
import Unlock from '@assets/images/unlock.svg';
import UploadAlt from '@assets/images/upload-alt.svg';
import Upload from '@assets/images/upload.svg';
import UserCheck from '@assets/images/user-check.svg';
import User from '@assets/images/user.svg';
import Users from '@assets/images/users.svg';
import VolumeHigh from '@assets/images/volume-high.svg';
Expand Down Expand Up @@ -350,6 +351,7 @@ export {
Upload,
UploadAlt,
User,
UserCheck,
Users,
VolumeHigh,
VolumeLow,
Expand Down
9 changes: 7 additions & 2 deletions src/components/LocaleContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type LocaleContextProps = {
toLocaleDigit: (digit: string) => string;

/** Formats a number into its localized ordinal representation */
toLocaleOrdinal: (number: number) => string;
toLocaleOrdinal: (number: number, returnWords?: boolean) => string;

/** Gets the standard digit corresponding to a locale digit */
fromLocaleDigit: (digit: string) => string;
Expand Down Expand Up @@ -101,7 +101,12 @@ function LocaleContextProvider({preferredLocale, currentUserPersonalDetails, chi

const toLocaleDigit = useMemo<LocaleContextProps['toLocaleDigit']>(() => (digit) => LocaleDigitUtils.toLocaleDigit(locale, digit), [locale]);

const toLocaleOrdinal = useMemo<LocaleContextProps['toLocaleOrdinal']>(() => (number) => LocaleDigitUtils.toLocaleOrdinal(locale, number), [locale]);
const toLocaleOrdinal = useMemo<LocaleContextProps['toLocaleOrdinal']>(
() =>
(number, writtenOrdinals = false) =>
LocaleDigitUtils.toLocaleOrdinal(locale, number, writtenOrdinals),
[locale],
);

const fromLocaleDigit = useMemo<LocaleContextProps['fromLocaleDigit']>(() => (localeDigit) => LocaleDigitUtils.fromLocaleDigit(locale, localeDigit), [locale]);

Expand Down
9 changes: 7 additions & 2 deletions src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ type MenuItemBaseProps = {
/** Should we grey out the menu item when it is disabled? */
shouldGreyOutWhenDisabled?: boolean;

/** Should we remove the background color of the menu item */
shouldRemoveBackground?: boolean;

/** Should we use default cursor for disabled content */
shouldUseDefaultCursorWhenDisabled?: boolean;

Expand Down Expand Up @@ -377,6 +380,7 @@ function MenuItem(
shouldRenderAsHTML = false,
shouldEscapeText = undefined,
shouldGreyOutWhenDisabled = true,
shouldRemoveBackground = false,
shouldUseDefaultCursorWhenDisabled = false,
shouldShowLoadingSpinnerIcon = false,
isAnonymousAction = false,
Expand Down Expand Up @@ -537,11 +541,12 @@ function MenuItem(
containerStyle,
combinedStyle,
!interactive && styles.cursorDefault,
StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true),
!shouldRemoveBackground &&
StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true),
...(Array.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]),
!focused && (isHovered || pressed) && hoverAndPressStyle,
shouldGreyOutWhenDisabled && disabled && styles.buttonOpacityDisabled,
isHovered && interactive && !focused && !pressed && styles.hoveredComponentBG,
isHovered && interactive && !focused && !pressed && !shouldRemoveBackground && styles.hoveredComponentBG,
] as StyleProp<ViewStyle>
}
disabledStyle={shouldUseDefaultCursorWhenDisabled && [styles.cursorDefault]}
Expand Down
14 changes: 14 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,7 @@ export default {
submissionFrequencyDateOfMonth: 'Date of month',
addApprovalsTitle: 'Add approvals',
addApprovalButton: 'Add approval workflow',
addApprovalTip: 'This default workflow applies to all members, unless a more specific workflow exists',
approver: 'Approver',
connectBankAccount: 'Connect bank account',
addApprovalsDescription: 'Require additional approval before authorizing a payment.',
Expand All @@ -1276,6 +1277,18 @@ export default {
two: 'nd',
few: 'rd',
other: 'th',
/* eslint-disable @typescript-eslint/naming-convention */
'1': 'First',
'2': 'Second',
'3': 'Third',
'4': 'Fourth',
'5': 'Fifth',
'6': 'Sixth',
'7': 'Seventh',
'8': 'Eighth',
'9': 'Ninth',
'10': 'Tenth',
/* eslint-enable @typescript-eslint/naming-convention */
},
},
},
Expand Down Expand Up @@ -2045,6 +2058,7 @@ export default {
edit: 'Edit workspace',
enabled: 'Enabled',
disabled: 'Disabled',
everyone: 'Everyone',
delete: 'Delete workspace',
settings: 'Settings',
reimburse: 'Reimbursements',
Expand Down
14 changes: 14 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,7 @@ export default {
submissionFrequencyDateOfMonth: 'Fecha del mes',
addApprovalsTitle: 'Requerir aprobaciones',
addApprovalButton: 'Añadir flujo de aprobación',
addApprovalTip: 'Este flujo de trabajo por defecto se aplica a todos los miembros, a menos que exista un flujo de trabajo más específico',
approver: 'Aprobador',
connectBankAccount: 'Conectar cuenta bancaria',
addApprovalsDescription: 'Requiere una aprobación adicional antes de autorizar un pago.',
Expand All @@ -1285,6 +1286,18 @@ export default {
two: '.º',
few: '.º',
other: '.º',
/* eslint-disable @typescript-eslint/naming-convention */
'1': 'Primero',
'2': 'Segundo',
'3': 'Tercero',
'4': 'Cuarto',
'5': 'Quinto',
'6': 'Sexto',
'7': 'Séptimo',
'8': 'Octavo',
'9': 'Noveno',
'10': 'Décimo',
/* eslint-enable @typescript-eslint/naming-convention */
},
},
},
Expand Down Expand Up @@ -2080,6 +2093,7 @@ export default {
edit: 'Editar espacio de trabajo',
enabled: 'Activada',
disabled: 'Desactivada',
everyone: 'Todos',
delete: 'Eliminar espacio de trabajo',
settings: 'Configuración',
reimburse: 'Reembolsos',
Expand Down
13 changes: 10 additions & 3 deletions src/libs/LocaleDigitUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,24 @@ function fromLocaleDigit(locale: Locale, localeDigit: string): string {

/**
* Formats a number into its localized ordinal representation i.e 1st, 2nd etc
* @param locale - The locale to use for formatting
* @param number - The number to format
* @param writtenOrdinals - If true, returns the written ordinal (e.g. "first", "second") for numbers 1-10
*/
function toLocaleOrdinal(locale: Locale, number: number): string {
function toLocaleOrdinal(locale: Locale, number: number, writtenOrdinals = false): string {
// Defaults to "other" suffix or "th" in English
let suffixKey = 'workflowsPage.frequencies.ordinals.other';
let suffixKey: TranslationPaths = 'workflowsPage.frequencies.ordinals.other';

// Calculate last digit of the number to determine basic ordinality
const lastDigit = number % 10;

// Calculate last two digits to handle exceptions in the 11-13 range
const lastTwoDigits = number % 100;

if (writtenOrdinals && number >= 1 && number <= 10) {
return Localize.translate(locale, `workflowsPage.frequencies.ordinals.${number}` as TranslationPaths);
}

if (lastDigit === 1 && lastTwoDigits !== 11) {
suffixKey = 'workflowsPage.frequencies.ordinals.one';
} else if (lastDigit === 2 && lastTwoDigits !== 12) {
Expand All @@ -92,7 +99,7 @@ function toLocaleOrdinal(locale: Locale, number: number): string {
suffixKey = 'workflowsPage.frequencies.ordinals.few';
}

const suffix = Localize.translate(locale, suffixKey as TranslationPaths);
const suffix = Localize.translate(locale, suffixKey);

return `${number}${suffix}`;
}
Expand Down
Loading

0 comments on commit 02d0d61

Please sign in to comment.