diff --git a/docs/articles/expensify-classic/expenses/reports/Create-A-Report.md b/docs/articles/expensify-classic/expenses/reports/Create-A-Report.md index 88ec2b730d1e..e8dfdbf44bcb 100644 --- a/docs/articles/expensify-classic/expenses/reports/Create-A-Report.md +++ b/docs/articles/expensify-classic/expenses/reports/Create-A-Report.md @@ -91,7 +91,7 @@ There are three ways you can change the report layout under the Details section # How to retract your report (Undo Submit) -As long as the report is still in a Processing state, you can retract this submission to put the report back to Draft status to make corrections and re-submit. +You can edit expenses on a report in a **Processing** state so long as it hasn't been approved yet. If a report has been through a level of approval and is still in the **Processing** state, you can retract this submission to put the report back to Draft status to make corrections and re-submit. To retract a **Processing** report on the web app, click the Undo Submit button at the upper left-hand corner of the report. diff --git a/package-lock.json b/package-lock.json index a1b60ce853d4..6e7f23674852 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@expensify/react-native-live-markdown": "0.1.5", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", - "@formatjs/intl-getcanonicallocales": "^2.2.0", "@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-locale": "^3.3.0", "@formatjs/intl-numberformat": "^8.5.0", diff --git a/package.json b/package.json index 675cc6288185..83c3095585be 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "@expensify/react-native-live-markdown": "0.1.5", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", - "@formatjs/intl-getcanonicallocales": "^2.2.0", "@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-locale": "^3.3.0", "@formatjs/intl-numberformat": "^8.5.0", @@ -155,8 +154,8 @@ "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", - "react-native-release-profiler": "^0.1.6", "react-native-reanimated": "^3.7.2", + "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", diff --git a/src/CONST.ts b/src/CONST.ts index 3109b9ea90ca..955ddda76741 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2974,7 +2974,7 @@ const CONST = { CURRENCY: 'XAF', FORMAT: 'symbol', SAMPLE_INPUT: '123456.789', - EXPECTED_OUTPUT: 'FCFA 123,457', + EXPECTED_OUTPUT: 'FCFA 123,457', }, PATHS_TO_TREAT_AS_EXTERNAL: ['NewExpensify.dmg', 'docs/index.html'], diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.tsx similarity index 63% rename from src/components/AddPaymentMethodMenu.js rename to src/components/AddPaymentMethodMenu.tsx index 803b7f2cdabe..ac9657694500 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.tsx @@ -1,78 +1,75 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {RefObject} from 'react'; import React from 'react'; +import type {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import iouReportPropTypes from '@pages/iouReportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {AnchorPosition} from '@src/styles'; +import type {Report, Session} from '@src/types/onyx'; +import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import * as Expensicons from './Icon/Expensicons'; +import type {PaymentMethod} from './KYCWall/types'; import PopoverMenu from './PopoverMenu'; -import refPropTypes from './refPropTypes'; -const propTypes = { +type AddPaymentMethodMenuOnyxProps = { + /** Session info for the currently logged-in user. */ + session: OnyxEntry; +}; + +type AddPaymentMethodMenuProps = AddPaymentMethodMenuOnyxProps & { /** Should the component be visible? */ - isVisible: PropTypes.bool.isRequired, + isVisible: boolean; /** Callback to execute when the component closes. */ - onClose: PropTypes.func.isRequired, + onClose: () => void; /** Callback to execute when the payment method is selected. */ - onItemSelected: PropTypes.func.isRequired, + onItemSelected: (paymentMethod: PaymentMethod) => void; /** The IOU/Expense report we are paying */ - iouReport: iouReportPropTypes, + iouReport?: OnyxEntry | EmptyObject; /** Anchor position for the AddPaymentMenu. */ - anchorPosition: PropTypes.shape({ - horizontal: PropTypes.number, - vertical: PropTypes.number, - }), + anchorPosition: AnchorPosition; /** Where the popover should be positioned relative to the anchor points. */ - anchorAlignment: PropTypes.shape({ - horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), - vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)), - }), + anchorAlignment?: AnchorAlignment; /** Popover anchor ref */ - anchorRef: refPropTypes, - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), + anchorRef: RefObject; /** Whether the personal bank account option should be shown */ - shouldShowPersonalBankAccountOption: PropTypes.bool, + shouldShowPersonalBankAccountOption?: boolean; }; -const defaultProps = { - iouReport: {}, - anchorPosition: {}, - anchorAlignment: { +function AddPaymentMethodMenu({ + isVisible, + onClose, + anchorPosition, + anchorAlignment = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }, - anchorRef: () => {}, - session: {}, - shouldShowPersonalBankAccountOption: false, -}; - -function AddPaymentMethodMenu({isVisible, onClose, anchorPosition, anchorAlignment, anchorRef, iouReport, onItemSelected, session, shouldShowPersonalBankAccountOption}) { + anchorRef, + iouReport, + onItemSelected, + session, + shouldShowPersonalBankAccountOption = false, +}: AddPaymentMethodMenuProps) { const {translate} = useLocalize(); // Users can choose to pay with business bank account in case of Expense reports or in case of P2P IOU report // which then starts a bottom up flow and creates a Collect workspace where the payer is an admin and payee is an employee. + const isIOUReport = ReportUtils.isIOUReport(iouReport ?? {}); const canUseBusinessBankAccount = - ReportUtils.isExpenseReport(iouReport) || - (ReportUtils.isIOUReport(iouReport) && !ReportActionsUtils.hasRequestFromCurrentAccount(lodashGet(iouReport, 'reportID', 0), lodashGet(session, 'accountID', 0))); + ReportUtils.isExpenseReport(iouReport ?? {}) || (isIOUReport && !ReportActionsUtils.hasRequestFromCurrentAccount(iouReport?.reportID ?? '', session?.accountID ?? 0)); - const canUsePersonalBankAccount = shouldShowPersonalBankAccountOption || ReportUtils.isIOUReport(iouReport); + const canUsePersonalBankAccount = shouldShowPersonalBankAccountOption || isIOUReport; return ( ({ session: { key: ONYXKEYS.SESSION, }, diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index b80d6a138c9e..453e72dc761f 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -1,6 +1,6 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useImperativeHandle} from 'react'; -import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; @@ -46,6 +46,9 @@ type ContextMenuItemProps = { wrapperStyle?: StyleProp; shouldPreventDefaultFocusOnPress?: boolean; + + /** The ref of mini context menu item */ + buttonRef?: React.RefObject; }; type ContextMenuItemHandle = { @@ -66,6 +69,7 @@ function ContextMenuItem( shouldLimitWidth = true, wrapperStyle, shouldPreventDefaultFocusOnPress = true, + buttonRef = {current: null}, }: ContextMenuItemProps, ref: ForwardedRef, ) { @@ -94,6 +98,7 @@ function ContextMenuItem( return isMini ? ( { const [emojiPopoverAnchorOrigin, setEmojiPopoverAnchorOrigin] = useState(DEFAULT_ANCHOR_ORIGIN); const [activeID, setActiveID] = useState(); const emojiPopoverAnchorRef = useRef(null); + const emojiAnchorDimension = useRef({ + width: 0, + height: 0, + }); const onModalHide = useRef(() => {}); const onEmojiSelected = useRef(() => {}); const activeEmoji = useRef(); @@ -76,7 +80,14 @@ const EmojiPicker = forwardRef((props, ref) => { // eslint-disable-next-line es/no-optional-chaining onWillShow?.(); setIsEmojiPickerVisible(true); - setEmojiPopoverAnchorPosition(value); + setEmojiPopoverAnchorPosition({ + horizontal: value.horizontal, + vertical: value.vertical, + }); + emojiAnchorDimension.current = { + width: value.width, + height: value.height, + }; setEmojiPopoverAnchorOrigin(anchorOriginValue); setActiveID(id); }); @@ -155,7 +166,14 @@ const EmojiPicker = forwardRef((props, ref) => { return; } calculateAnchorPosition(emojiPopoverAnchor.current, emojiPopoverAnchorOrigin).then((value) => { - setEmojiPopoverAnchorPosition(value); + setEmojiPopoverAnchorPosition({ + horizontal: value.horizontal, + vertical: value.vertical, + }); + emojiAnchorDimension.current = { + width: value.width, + height: value.height, + }; }); }); return () => { @@ -192,7 +210,9 @@ const EmojiPicker = forwardRef((props, ref) => { anchorAlignment={emojiPopoverAnchorOrigin} outerStyle={StyleUtils.getOuterModalStyle(windowHeight, props.viewportOffsetTop)} innerContainerStyle={styles.popoverInnerContainer} + anchorDimensions={emojiAnchorDimension.current} avoidKeyboard + shoudSwitchPositionIfOverflow > ({ item, pressableStyle, wrapperStyle, + containerStyle, selectMultipleStyle, isDisabled = false, shouldPreventDefaultFocusOnSelectRow = false, @@ -62,6 +63,7 @@ function BaseListItem({ pendingAction={pendingAction} errors={errors} errorRowStyles={styles.ph5} + style={containerStyle} > = { /** Styles for the wrapper view */ wrapperStyle?: StyleProp; + /** Styles for the container view */ + containerStyle?: StyleProp; + /** Styles for the checkbox wrapper view if select multiple option is on */ selectMultipleStyle?: StyleProp; diff --git a/src/languages/en.ts b/src/languages/en.ts index 41e02692e760..358a3ce61d4f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -589,7 +589,7 @@ export default { recordDistance: 'Record Distance', requestMoney: 'Request Money', splitBill: 'Split Bill', - splitReceipt: 'Split Receipt', + splitScan: 'Split Receipt', splitDistance: 'Split Distance', sendMoney: 'Send Money', assignTask: 'Assign Task', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5e5e958ae075..16d0748eed1d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -585,7 +585,7 @@ export default { recordDistance: 'Grabar Distancia', requestMoney: 'Solicitar Dinero', splitBill: 'Dividir Cuenta', - splitReceipt: 'Dividir Recibo', + splitScan: 'Dividir Recibo', splitDistance: 'Dividir Distancia', sendMoney: 'Enviar Dinero', assignTask: 'Assignar Tarea', diff --git a/src/libs/API/parameters/SendMoneyParams.ts b/src/libs/API/parameters/SendMoneyParams.ts index ac6f42de5aa5..449a87fb5313 100644 --- a/src/libs/API/parameters/SendMoneyParams.ts +++ b/src/libs/API/parameters/SendMoneyParams.ts @@ -9,6 +9,7 @@ type SendMoneyParams = { newIOUReportDetails: string; createdReportActionID: string; reportPreviewReportActionID: string; + createdIOUReportActionID: string; transactionThreadReportID: string; createdReportActionIDForThread: string; }; diff --git a/src/libs/IntlPolyfill/index.android.ts b/src/libs/IntlPolyfill/index.android.ts new file mode 100644 index 000000000000..7a21ae26bfa4 --- /dev/null +++ b/src/libs/IntlPolyfill/index.android.ts @@ -0,0 +1,17 @@ +import polyfillListFormat from './polyfillListFormat'; +import type IntlPolyfill from './types'; + +/** + * Polyfill the Intl API, always performed for native devices. + */ +const intlPolyfill: IntlPolyfill = () => { + // Native devices require extra polyfills which are + // not yet implemented in hermes. + // see support: https://hermesengine.dev/docs/intl/ + + require('@formatjs/intl-locale/polyfill'); + + polyfillListFormat(); +}; + +export default intlPolyfill; diff --git a/src/libs/IntlPolyfill/index.native.ts b/src/libs/IntlPolyfill/index.ios.ts similarity index 56% rename from src/libs/IntlPolyfill/index.native.ts rename to src/libs/IntlPolyfill/index.ios.ts index ca1c8f4c250e..569b666eb434 100644 --- a/src/libs/IntlPolyfill/index.native.ts +++ b/src/libs/IntlPolyfill/index.ios.ts @@ -7,12 +7,21 @@ import type IntlPolyfill from './types'; * Polyfill the Intl API, always performed for native devices. */ const intlPolyfill: IntlPolyfill = () => { - // Native devices require extra polyfills - require('@formatjs/intl-getcanonicallocales/polyfill'); + // Native devices require extra polyfills which are + // not yet implemented in hermes. + // see support: https://hermesengine.dev/docs/intl/ + require('@formatjs/intl-locale/polyfill'); + + // Required to polyfill NumberFormat on iOS + // see: https://github.com/facebook/hermes/issues/1172#issuecomment-1776156538 require('@formatjs/intl-pluralrules/polyfill'); polyfillNumberFormat(); + + // Required to polyfill DateTimeFormat on iOS + // see: https://github.com/facebook/hermes/issues/1172#issuecomment-1776156538 polyfillDateTimeFormat(); + polyfillListFormat(); }; diff --git a/src/libs/IntlPolyfill/polyfillNumberFormat.ts b/src/libs/IntlPolyfill/polyfillNumberFormat.ts index 1fac01958f05..58d8adf1b761 100644 --- a/src/libs/IntlPolyfill/polyfillNumberFormat.ts +++ b/src/libs/IntlPolyfill/polyfillNumberFormat.ts @@ -1,21 +1,29 @@ import CONST from '@src/CONST'; +const numberFormat = new Intl.NumberFormat(CONST.LOCALES.DEFAULT, { + style: CONST.POLYFILL_TEST.STYLE, + currency: CONST.POLYFILL_TEST.CURRENCY, + currencyDisplay: CONST.POLYFILL_TEST.FORMAT, +}); + /** * Check if the locale data is as expected on the device. * Ensures that the currency data is consistent across devices. */ function hasOldCurrencyData(): boolean { - return ( - new Intl.NumberFormat(CONST.LOCALES.DEFAULT, { - style: CONST.POLYFILL_TEST.STYLE, - currency: CONST.POLYFILL_TEST.CURRENCY, - currencyDisplay: CONST.POLYFILL_TEST.FORMAT, - }).format(Number(CONST.POLYFILL_TEST.SAMPLE_INPUT)) !== CONST.POLYFILL_TEST.EXPECTED_OUTPUT - ); + return numberFormat.format(Number(CONST.POLYFILL_TEST.SAMPLE_INPUT)) !== CONST.POLYFILL_TEST.EXPECTED_OUTPUT; +} + +/** + * Checks if the formatToParts function is available on the + * Intl.NumberFormat object. + */ +function hasFormatToParts(): boolean { + return typeof numberFormat.formatToParts === 'function'; } export default function () { - if (Intl && 'NumberFormat' in Intl && !hasOldCurrencyData()) { + if (Intl && 'NumberFormat' in Intl && !hasOldCurrencyData() && hasFormatToParts()) { return; } diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index b22a9612a23f..fee8a0b437c7 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; import * as ActiveClientManager from '@libs/ActiveClientManager'; +import {WRITE_COMMANDS} from '@libs/API/types'; import * as Request from '@libs/Request'; import * as RequestThrottle from '@libs/RequestThrottle'; import * as PersistedRequests from '@userActions/PersistedRequests'; @@ -45,6 +46,47 @@ function flushOnyxUpdatesQueue() { QueuedOnyxUpdates.flushQueue(); } +/** + * Identifies and removes conflicting requests from the queue + */ +function reconcileRequests(persistedRequests: OnyxRequest[], commands: string[]) { + const requestsByActionID: Record = {}; + + // Group requests by reportActionID + persistedRequests.forEach((request) => { + const {data} = request; + const reportActionID = data?.reportActionID as string; + if (reportActionID) { + if (!requestsByActionID[reportActionID]) { + requestsByActionID[reportActionID] = []; + } + requestsByActionID[reportActionID].push(request); + } + }); + + // Process requests with conflicting actions + Object.values(requestsByActionID).forEach((requests) => { + const conflictingRequests: OnyxRequest[] = []; + commands.forEach((command) => { + const conflictingRequest = requests.find((request) => request.command === command); + if (conflictingRequest) { + conflictingRequests.push(conflictingRequest); + } + }); + + if (conflictingRequests.length > 1) { + // Remove all requests as they cancel each other out + conflictingRequests.forEach((request) => { + // Perform action: Remove request from persisted requests + const index = persistedRequests.findIndex((req) => req === request); + if (index !== -1) { + persistedRequests.splice(index, 1); + } + }); + } + }); +} + /** * Process any persisted requests, when online, one at a time until the queue is empty. * @@ -63,6 +105,11 @@ function process(): Promise { if (persistedRequests.length === 0 || NetworkStore.isOffline()) { return Promise.resolve(); } + + // Remove conflicting requests from the queue to avoid processing them + const commands = [WRITE_COMMANDS.ADD_COMMENT, WRITE_COMMANDS.DELETE_COMMENT]; + reconcileRequests(persistedRequests, commands); + const requestToProcess = persistedRequests[0]; // Set the current request to a promise awaiting its processing so that getCurrentRequest can be used to take some action after the current request has processed. diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 529ab10fab8b..46e217ba20b1 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -249,11 +249,9 @@ Onyx.connect({ }); const policyExpenseReports: OnyxCollection = {}; -const allReports: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: (report, key) => { - allReports[key] = report; if (!ReportUtils.isPolicyExpenseChat(report)) { return; } @@ -741,12 +739,12 @@ function createOption( * Get the option for a given report. */ function getReportOption(participant: Participant): ReportUtils.OptionData { - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`]; + const report = ReportUtils.getReport(participant.reportID); const option = createOption( report?.visibleChatMemberAccountIDs ?? [], allPersonalDetails ?? {}, - report ?? null, + !isEmptyObject(report) ? report : null, {}, { showChatPreviewLine: false, diff --git a/src/libs/PopoverWithMeasuredContentUtils.ts b/src/libs/PopoverWithMeasuredContentUtils.ts index 5d5af6f7c83d..1f42d741bdbd 100644 --- a/src/libs/PopoverWithMeasuredContentUtils.ts +++ b/src/libs/PopoverWithMeasuredContentUtils.ts @@ -32,18 +32,20 @@ function computeHorizontalShift(anchorLeftEdge: number, menuWidth: number, windo * @param anchorTopEdge - Menu's anchor Top edge. * @param menuHeight - The height of the menu itself. * @param windowHeight - The height of the Window. + * @param anchorHeight - The height of anchor component + * @param shoudSwitchPositionIfOverflow - */ -function computeVerticalShift(anchorTopEdge: number, menuHeight: number, windowHeight: number): number { +function computeVerticalShift(anchorTopEdge: number, menuHeight: number, windowHeight: number, anchorHeight: number, shoudSwitchPositionIfOverflow = false): number { const popoverBottomEdge = anchorTopEdge + menuHeight; if (anchorTopEdge < 0) { // Anchor is in top window Edge, shift bottom by a multiple of four. - return roundToNearestMultipleOfFour(0 - anchorTopEdge); + return roundToNearestMultipleOfFour(shoudSwitchPositionIfOverflow ? menuHeight + anchorHeight : 0 - anchorTopEdge); } if (popoverBottomEdge > windowHeight) { // Anchor is in Bottom window Edge, shift top by a multiple of four. - return roundToNearestMultipleOfFour(windowHeight - popoverBottomEdge); + return roundToNearestMultipleOfFour(shoudSwitchPositionIfOverflow ? -(menuHeight + anchorHeight) : windowHeight - popoverBottomEdge); } // Anchor is not in the gutter, so no need to shift it vertically diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a3ec8cebc992..0bb3b1101b7c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4123,10 +4123,11 @@ function buildTransactionThread(reportAction: OnyxEntry): boolean { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 85ab98bb7819..ae3bf0cdbd68 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1114,22 +1114,22 @@ function getMoneyRequestInformation( // 4. The transaction thread, which requires the iouAction, and CREATED action for the transaction thread // 5. REPORTPREVIEW action for the chatReport // Note: The CREATED action for the IOU report must be optimistically generated before the IOU action so there's no chance that it appears after the IOU action in the chat - const optimisticCreatedActionForChat = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); - const [optimisticCreatedActionForIOUReport, iouAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = ReportUtils.buildOptimisticMoneyRequestEntities( - iouReport, - CONST.IOU.REPORT_ACTION_TYPE.CREATE, - amount, - currency, - comment, - payeeEmail, - [participant], - optimisticTransaction.transactionID, - undefined, - false, - false, - receiptObject, - false, - ); + const [optimisticCreatedActionForChat, optimisticCreatedActionForIOUReport, iouAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = + ReportUtils.buildOptimisticMoneyRequestEntities( + iouReport, + CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount, + currency, + comment, + payeeEmail, + [participant], + optimisticTransaction.transactionID, + undefined, + false, + false, + receiptObject, + false, + ); let reportPreviewAction = shouldCreateNewMoneyRequestReport ? null : ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID); if (reportPreviewAction) { @@ -2522,17 +2522,17 @@ function createSplitsAndOnyxData( // 3. IOU action for the iouReport // 4. Transaction Thread and the CREATED action for it // 5. REPORTPREVIEW action for the chatReport - const oneOnOneCreatedActionForChat = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit); - const [oneOnOneCreatedActionForIOU, oneOnOneIOUAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = ReportUtils.buildOptimisticMoneyRequestEntities( - oneOnOneIOUReport, - CONST.IOU.REPORT_ACTION_TYPE.CREATE, - splitAmount, - currency, - comment, - currentUserEmailForIOUSplit, - [participant], - oneOnOneTransaction.transactionID, - ); + const [oneOnOneCreatedActionForChat, oneOnOneCreatedActionForIOU, oneOnOneIOUAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = + ReportUtils.buildOptimisticMoneyRequestEntities( + oneOnOneIOUReport, + CONST.IOU.REPORT_ACTION_TYPE.CREATE, + splitAmount, + currency, + comment, + currentUserEmailForIOUSplit, + [participant], + oneOnOneTransaction.transactionID, + ); // Add optimistic personal details for new participants const oneOnOnePersonalDetailListAction: OnyxTypes.PersonalDetailsList = shouldCreateOptimisticPersonalDetails @@ -3173,18 +3173,18 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA updatedTransaction.billable, ); - const oneOnOneCreatedActionForChat = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit); - const [oneOnOneCreatedActionForIOU, oneOnOneIOUAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = ReportUtils.buildOptimisticMoneyRequestEntities( - oneOnOneIOUReport, - CONST.IOU.REPORT_ACTION_TYPE.CREATE, - splitAmount, - currency ?? '', - updatedTransaction.comment.comment ?? '', - currentUserEmailForIOUSplit, - [participant], - oneOnOneTransaction.transactionID, - undefined, - ); + const [oneOnOneCreatedActionForChat, oneOnOneCreatedActionForIOU, oneOnOneIOUAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = + ReportUtils.buildOptimisticMoneyRequestEntities( + oneOnOneIOUReport, + CONST.IOU.REPORT_ACTION_TYPE.CREATE, + splitAmount, + currency ?? '', + updatedTransaction.comment.comment ?? '', + currentUserEmailForIOUSplit, + [participant], + oneOnOneTransaction.transactionID, + undefined, + ); let oneOnOneReportPreviewAction = ReportActionsUtils.getReportPreviewAction(oneOnOneChatReport?.reportID ?? '', oneOnOneIOUReport?.reportID ?? ''); if (oneOnOneReportPreviewAction) { @@ -4070,7 +4070,7 @@ function getSendMoneyParams( value: optimisticTransaction, }; - const [optimisticCreatedActionForIOUReport, optimisticIOUReportAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = + const [optimisticCreatedActionForChat, optimisticCreatedActionForIOUReport, optimisticIOUReportAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = ReportUtils.buildOptimisticMoneyRequestEntities( optimisticIOUReport, CONST.IOU.REPORT_ACTION_TYPE.PAY, @@ -4137,6 +4137,7 @@ function getSendMoneyParams( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticIOUReport.reportID}`, value: { + [optimisticCreatedActionForIOUReport.reportActionID]: optimisticCreatedActionForIOUReport, [optimisticIOUReportAction.reportActionID]: { ...(optimisticIOUReportAction as OnyxTypes.ReportAction), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, @@ -4269,7 +4270,7 @@ function getSendMoneyParams( if (optimisticChatReportActionsData.value) { // Add an optimistic created action to the optimistic chat reportActions data - optimisticChatReportActionsData.value[optimisticCreatedActionForIOUReport.reportActionID] = optimisticCreatedActionForIOUReport; + optimisticChatReportActionsData.value[optimisticCreatedActionForChat.reportActionID] = optimisticCreatedActionForChat; } } else { failureData.push({ @@ -4306,8 +4307,9 @@ function getSendMoneyParams( paymentMethodType, transactionID: optimisticTransaction.transactionID, newIOUReportDetails, - createdReportActionID: isNewChat ? optimisticCreatedActionForIOUReport.reportActionID : '0', + createdReportActionID: isNewChat ? optimisticCreatedActionForChat.reportActionID : '0', reportPreviewReportActionID: reportPreviewAction.reportActionID, + createdIOUReportActionID: optimisticCreatedActionForIOUReport.reportActionID, transactionThreadReportID: optimisticTransactionThread.reportID, createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, }, diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts index 9fe6e8f018d8..365bd1ece28b 100644 --- a/src/libs/calculateAnchorPosition.ts +++ b/src/libs/calculateAnchorPosition.ts @@ -2,7 +2,7 @@ import type {ValueOf} from 'type-fest'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {AnchorPosition} from '@src/styles'; +import type {AnchorDimensions, AnchorPosition} from '@src/styles'; type AnchorOrigin = { horizontal: ValueOf; @@ -13,18 +13,18 @@ type AnchorOrigin = { /** * Gets the x,y position of the passed in component for the purpose of anchoring another component to it. */ -export default function calculateAnchorPosition(anchorComponent: ContextMenuAnchor, anchorOrigin?: AnchorOrigin): Promise { +export default function calculateAnchorPosition(anchorComponent: ContextMenuAnchor, anchorOrigin?: AnchorOrigin): Promise { return new Promise((resolve) => { if (!anchorComponent || !('measureInWindow' in anchorComponent)) { - resolve({horizontal: 0, vertical: 0}); + resolve({horizontal: 0, vertical: 0, width: 0, height: 0}); return; } anchorComponent.measureInWindow((x, y, width, height) => { if (anchorOrigin?.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP && anchorOrigin?.horizontal === CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT) { - resolve({horizontal: x, vertical: y + height + (anchorOrigin?.shiftVertical ?? 0)}); + resolve({horizontal: x, vertical: y + height + (anchorOrigin?.shiftVertical ?? 0), width, height}); return; } - resolve({horizontal: x + width, vertical: y + (anchorOrigin?.shiftVertical ?? 0)}); + resolve({horizontal: x + width, vertical: y + (anchorOrigin?.shiftVertical ?? 0), width, height}); }); }); } diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 4a5948545345..10c7dddf2631 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -120,6 +120,7 @@ function BaseReportActionContextMenu({ const [shouldKeepOpen, setShouldKeepOpen] = useState(false); const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, isSmallScreenWidth); const {isOffline} = useNetwork(); + const threedotRef = useRef(null); const reportAction: OnyxEntry = useMemo(() => { if (isEmptyObject(reportActions) || reportActionID === '0') { @@ -193,14 +194,14 @@ function BaseReportActionContextMenu({ {isActive: shouldEnableArrowNavigation}, ); - const openOverflowMenu = (event: GestureResponderEvent | MouseEvent) => { + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: MutableRefObject) => { const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); const originalReport = ReportUtils.getReport(originalReportID); showContextMenu( CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, event, selection, - anchor?.current as ViewType | RNText | null, + anchorRef?.current as ViewType | RNText | null, reportID, reportAction?.reportActionID, originalReportID, @@ -216,6 +217,8 @@ function BaseReportActionContextMenu({ undefined, filteredContextMenuActions, true, + () => {}, + true, ); }; @@ -249,19 +252,26 @@ function BaseReportActionContextMenu({ textTranslateKey === 'reportActionContextMenu.deleteAction' || textTranslateKey === 'reportActionContextMenu.deleteConfirmation'; const text = textTranslateKey && (isKeyInActionUpdateKeys ? translate(textTranslateKey, {action: reportAction}) : translate(textTranslateKey)); + const isMenuAction = textTranslateKey === 'reportActionContextMenu.menu'; return ( { menuItemRefs.current[index] = ref; }} + buttonRef={isMenuAction ? threedotRef : {current: null}} icon={contextAction.icon} text={text ?? ''} successIcon={contextAction.successIcon} successText={contextAction.successTextTranslateKey ? translate(contextAction.successTextTranslateKey) : undefined} isMini={isMini} key={contextAction.textTranslateKey} - onPress={(event) => interceptAnonymousUser(() => contextAction.onPress?.(closePopup, {...payload, event}), contextAction.isAnonymousAction)} + onPress={(event) => + interceptAnonymousUser( + () => contextAction.onPress?.(closePopup, {...payload, event, ...(isMenuAction ? {anchorRef: threedotRef} : {})}), + contextAction.isAnonymousAction, + ) + } description={contextAction.getDescription?.(selection) ?? ''} isAnonymousAction={contextAction.isAnonymousAction} isFocused={focusedIndex === index} diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 83b36501a898..ea65ab40ac0a 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -73,9 +73,10 @@ type ContextMenuActionPayload = { interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; anchor?: MutableRefObject; checkIfContextMenuActive?: () => void; - openOverflowMenu: (event: GestureResponderEvent | MouseEvent) => void; + openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: MutableRefObject) => void; event?: GestureResponderEvent | MouseEvent | KeyboardEvent; setIsEmojiPickerActive?: (state: boolean) => void; + anchorRef?: MutableRefObject; }; type OnPress = (closePopover: boolean, payload: ContextMenuActionPayload, selection?: string, reportID?: string, draftMessage?: string) => void; @@ -490,8 +491,8 @@ const ContextMenuActions: ContextMenuAction[] = [ textTranslateKey: 'reportActionContextMenu.menu', icon: Expensicons.ThreeDots, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline, isMini) => isMini, - onPress: (closePopover, {openOverflowMenu, event, openContextMenu}) => { - openOverflowMenu(event as GestureResponderEvent | MouseEvent); + onPress: (closePopover, {openOverflowMenu, event, openContextMenu, anchorRef}) => { + openOverflowMenu(event as GestureResponderEvent | MouseEvent, anchorRef ?? {current: null}); openContextMenu(); }, getDescription: () => {}, diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 9bf32bb92b35..fcd291e55882 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -12,6 +12,7 @@ import calculateAnchorPosition from '@libs/calculateAnchorPosition'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; +import type {AnchorDimensions} from '@src/styles'; import type {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; import type {ContextMenuAction} from './ContextMenuActions'; @@ -67,6 +68,10 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef(null); const contextMenuAnchorRef = useRef(null); const contextMenuTargetNode = useRef(null); + const contextMenuDimensions = useRef({ + width: 0, + height: 0, + }); const onPopoverShow = useRef(() => {}); const onPopoverHide = useRef(() => {}); @@ -164,6 +169,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef {}, + isOverflowMenu = false, ) => { const {pageX = 0, pageY = 0} = extractPointerEvent(event); contextMenuAnchorRef.current = contextMenuAnchor; @@ -180,9 +186,10 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef((resolve) => { - if (!pageX && !pageY && contextMenuAnchorRef.current) { + if (Boolean(!pageX && !pageY && contextMenuAnchorRef.current) || isOverflowMenu) { calculateAnchorPosition(contextMenuAnchorRef.current).then((position) => { - popoverAnchorPosition.current = position; + popoverAnchorPosition.current = {horizontal: position.horizontal, vertical: position.vertical}; + contextMenuDimensions.current = {width: position.vertical, height: position.height}; resolve(); }); } else { @@ -319,7 +326,9 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef void, + isOverflowMenu?: boolean, ) => void; type ReportActionContextMenu = { @@ -117,6 +118,7 @@ function showContextMenu( disabledActions: ContextMenuAction[] = [], shouldCloseOnTarget = false, setIsEmojiPickerActive = () => {}, + isOverflowMenu = false, ) { if (!contextMenuRef.current) { return; @@ -146,6 +148,7 @@ function showContextMenu( disabledActions, shouldCloseOnTarget, setIsEmojiPickerActive, + isOverflowMenu, ); } diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 117083293b5d..24603de5679c 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -70,9 +70,9 @@ const getQuickActionTitle = (action) => { case CONST.QUICK_ACTIONS.SPLIT_MANUAL: return 'quickAction.splitBill'; case CONST.QUICK_ACTIONS.SPLIT_SCAN: - return 'quickAction.splitReceipt'; - case CONST.QUICK_ACTIONS.SPLIT_DISTANCE: return 'quickAction.splitScan'; + case CONST.QUICK_ACTIONS.SPLIT_DISTANCE: + return 'quickAction.splitDistance'; case CONST.QUICK_ACTIONS.SEND_MONEY: return 'quickAction.sendMoney'; case CONST.QUICK_ACTIONS.ASSIGN_TASK: @@ -335,7 +335,7 @@ function FloatingActionButtonAndPopover(props) { }, ] : []), - ...(props.quickAction + ...(props.quickAction && props.quickAction.action ? [ { icon: getQuickActionIcon(props.quickAction.action), diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 02fe06e29ab3..549d307b2a2f 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -182,21 +182,22 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r } return ( - - {({hovered}) => ( - + + {({hovered}) => ( - - )} - + )} + + ); }, [isLessThanMediumScreen, styles.mb3, styles.mh5, styles.ph5, styles.hoveredComponentBG, translate], diff --git a/src/styles/index.ts b/src/styles/index.ts index 31e3941cc4e5..bb772fff7534 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -44,6 +44,11 @@ import variables from './variables'; type ColorScheme = (typeof CONST.COLOR_SCHEME)[keyof typeof CONST.COLOR_SCHEME]; type StatusBarStyle = (typeof CONST.STATUS_BAR_STYLE)[keyof typeof CONST.STATUS_BAR_STYLE]; +type AnchorDimensions = { + width: number; + height: number; +}; + type AnchorPosition = { horizontal: number; vertical: number; @@ -4309,7 +4314,6 @@ const styles = (theme: ThemeColors) => paddingHorizontal: 16, paddingVertical: 16, marginHorizontal: 20, - marginBottom: 12, backgroundColor: theme.highlightBG, borderRadius: 8, }, @@ -4698,4 +4702,4 @@ const defaultStyles = styles(defaultTheme); export default styles; export {defaultStyles}; -export type {Styles, ThemeStyles, StatusBarStyle, ColorScheme, AnchorPosition}; +export type {Styles, ThemeStyles, StatusBarStyle, ColorScheme, AnchorPosition, AnchorDimensions}; diff --git a/src/types/modules/act.d.ts b/src/types/modules/act.d.ts new file mode 100644 index 000000000000..5fe00ec479cf --- /dev/null +++ b/src/types/modules/act.d.ts @@ -0,0 +1,14 @@ +import type {StepIdentifier as ActStepIdentifier} from '@kie/act-js'; + +declare module '@kie/act-js' { + // eslint-disable-next-line rulesdir/no-inline-named-export + export declare type StepIdentifier = { + id?: string; + name: string; + run?: string; + mockWith?: string; + with?: string; + envs?: string[]; + inputs?: string[]; + } & Omit; +} diff --git a/workflow_tests/assertions/authorChecklistAssertions.ts b/workflow_tests/assertions/authorChecklistAssertions.ts index 2886e6eecc10..997eac2d3ea9 100644 --- a/workflow_tests/assertions/authorChecklistAssertions.ts +++ b/workflow_tests/assertions/authorChecklistAssertions.ts @@ -16,5 +16,4 @@ function assertChecklistJobExecuted(workflowResult: Step[], didExecute = true) { }); } -// eslint-disable-next-line import/prefer-default-export -export {assertChecklistJobExecuted}; +export default {assertChecklistJobExecuted}; diff --git a/workflow_tests/assertions/cherryPickAssertions.ts b/workflow_tests/assertions/cherryPickAssertions.ts index 29a32c8b19d6..8cf5dbd56d54 100644 --- a/workflow_tests/assertions/cherryPickAssertions.ts +++ b/workflow_tests/assertions/cherryPickAssertions.ts @@ -113,4 +113,4 @@ function assertCherryPickJobExecuted(workflowResult: Step[], user = 'Dummy Autho }); } -export {assertValidateActorJobExecuted, assertCreateNewVersionJobExecuted, assertCherryPickJobExecuted}; +export default {assertValidateActorJobExecuted, assertCreateNewVersionJobExecuted, assertCherryPickJobExecuted}; diff --git a/workflow_tests/assertions/preDeployAssertions.ts b/workflow_tests/assertions/preDeployAssertions.ts index 8f6fb457b003..8a89d2377606 100644 --- a/workflow_tests/assertions/preDeployAssertions.ts +++ b/workflow_tests/assertions/preDeployAssertions.ts @@ -121,7 +121,7 @@ function assertUpdateStagingJobFailed(workflowResult: Step[], didFail = false) { }); } -export { +export default { assertTypecheckJobExecuted, assertLintJobExecuted, assertTestJobExecuted, diff --git a/workflow_tests/authorChecklist.test.js b/workflow_tests/authorChecklist.test.ts similarity index 87% rename from workflow_tests/authorChecklist.test.js rename to workflow_tests/authorChecklist.test.ts index 9d2b71b520ec..c0b1c7cf7533 100644 --- a/workflow_tests/authorChecklist.test.js +++ b/workflow_tests/authorChecklist.test.ts @@ -1,13 +1,17 @@ -const path = require('path'); -const kieMockGithub = require('@kie/mock-github'); -const utils = require('./utils/utils'); -const assertions = require('./assertions/authorChecklistAssertions'); -const mocks = require('./mocks/authorChecklistMocks').default; -const ExtendedAct = require('./utils/ExtendedAct').default; +import type {MockStep} from '@kie/act-js/build/src/step-mocker/step-mocker.types'; +import * as kieMockGithub from '@kie/mock-github'; +import type {CreateRepositoryFile, MockGithub} from '@kie/mock-github'; +import path from 'path'; +import assertions from './assertions/authorChecklistAssertions'; +import mocks from './mocks/authorChecklistMocks'; +import ExtendedAct from './utils/ExtendedAct'; +import * as utils from './utils/utils'; jest.setTimeout(90 * 1000); -let mockGithub; -const FILES_TO_COPY_INTO_TEST_REPO = [ + +let mockGithub: MockGithub; + +const FILES_TO_COPY_INTO_TEST_REPO: CreateRepositoryFile[] = [ ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), { src: path.resolve(__dirname, '..', '.github', 'workflows', 'authorChecklist.yml'), @@ -18,7 +22,7 @@ const FILES_TO_COPY_INTO_TEST_REPO = [ describe('test workflow authorChecklist', () => { const githubToken = 'dummy_github_token'; - beforeAll(async () => { + beforeAll(() => { // in case of the tests being interrupted without cleanup the mock repo directory may be left behind // which breaks the next test run, this removes any possible leftovers utils.removeMockRepoDir(); @@ -47,11 +51,11 @@ describe('test workflow authorChecklist', () => { }; describe('actor is not OSBotify', () => { it('executes workflow', async () => { - const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'); let act = new ExtendedAct(repoPath, workflowPath); act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); - const testMockSteps = { + const testMockSteps: MockStep = { checklist: mocks.AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, }; const result = await act.runEvent(event, { @@ -66,11 +70,11 @@ describe('test workflow authorChecklist', () => { }); describe('actor is OSBotify', () => { it('does not execute workflow', async () => { - const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'); let act = new ExtendedAct(repoPath, workflowPath); act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); - const testMockSteps = { + const testMockSteps: MockStep = { checklist: mocks.AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, }; const result = await act.runEvent(event, { @@ -91,11 +95,11 @@ describe('test workflow authorChecklist', () => { }; describe('actor is not OSBotify', () => { it('executes workflow', async () => { - const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'); let act = new ExtendedAct(repoPath, workflowPath); act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); - const testMockSteps = { + const testMockSteps: MockStep = { checklist: mocks.AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, }; const result = await act.runEvent(event, { @@ -110,11 +114,11 @@ describe('test workflow authorChecklist', () => { }); describe('actor is OSBotify', () => { it('does not execute workflow', async () => { - const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'); let act = new ExtendedAct(repoPath, workflowPath); act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); - const testMockSteps = { + const testMockSteps: MockStep = { checklist: mocks.AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, }; const result = await act.runEvent(event, { @@ -135,11 +139,11 @@ describe('test workflow authorChecklist', () => { }; describe('actor is not OSBotify', () => { it('executes workflow', async () => { - const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'); let act = new ExtendedAct(repoPath, workflowPath); act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); - const testMockSteps = { + const testMockSteps: MockStep = { checklist: mocks.AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, }; const result = await act.runEvent(event, { @@ -154,11 +158,11 @@ describe('test workflow authorChecklist', () => { }); describe('actor is OSBotify', () => { it('does not execute workflow', async () => { - const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testAuthorChecklistWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'authorChecklist.yml'); let act = new ExtendedAct(repoPath, workflowPath); act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); - const testMockSteps = { + const testMockSteps: MockStep = { checklist: mocks.AUTHORCHECKLIST__CHECKLIST__STEP_MOCKS, }; const result = await act.runEvent(event, { diff --git a/workflow_tests/cherryPick.test.js b/workflow_tests/cherryPick.test.ts similarity index 92% rename from workflow_tests/cherryPick.test.js rename to workflow_tests/cherryPick.test.ts index beae7f03b3ef..47a1c489df70 100644 --- a/workflow_tests/cherryPick.test.js +++ b/workflow_tests/cherryPick.test.ts @@ -1,13 +1,18 @@ -const path = require('path'); -const kieMockGithub = require('@kie/mock-github'); -const utils = require('./utils/utils'); -const assertions = require('./assertions/cherryPickAssertions'); -const mocks = require('./mocks/cherryPickMocks'); -const ExtendedAct = require('./utils/ExtendedAct').default; +import type {MockStep} from '@kie/act-js/build/src/step-mocker/step-mocker.types'; +import * as kieMockGithub from '@kie/mock-github'; +import type {CreateRepositoryFile, MockGithub} from '@kie/mock-github'; +import path from 'path'; +import assertions from './assertions/cherryPickAssertions'; +import mocks from './mocks/cherryPickMocks'; +import ExtendedAct from './utils/ExtendedAct'; +import type {MockJobs} from './utils/JobMocker'; +import * as utils from './utils/utils'; jest.setTimeout(90 * 1000); -let mockGithub; -const FILES_TO_COPY_INTO_TEST_REPO = [ + +let mockGithub: MockGithub; + +const FILES_TO_COPY_INTO_TEST_REPO: CreateRepositoryFile[] = [ ...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO), { src: path.resolve(__dirname, '..', '.github', 'workflows', 'cherryPick.yml'), @@ -16,7 +21,7 @@ const FILES_TO_COPY_INTO_TEST_REPO = [ ]; describe('test workflow cherryPick', () => { - beforeAll(async () => { + beforeAll(() => { // in case of the tests being interrupted without cleanup the mock repo directory may be left behind // which breaks the next test run, this removes any possible leftovers utils.removeMockRepoDir(); @@ -43,7 +48,7 @@ describe('test workflow cherryPick', () => { describe('actor is not deployer', () => { const actor = 'Dummy Author'; it('workflow ends after validate job', async () => { - const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); let act = new ExtendedAct(repoPath, workflowPath); act = utils.setUpActParams( @@ -61,11 +66,11 @@ describe('test workflow cherryPick', () => { PULL_REQUEST_NUMBER: '1234', }, ); - const testMockSteps = { + const testMockSteps: MockStep = { validateActor: mocks.CHERRYPICK__VALIDATEACTOR__FALSE__STEP_MOCKS, cherryPick: mocks.getCherryPickMockSteps(true, false), }; - const testMockJobs = { + const testMockJobs: MockJobs = { createNewVersion: { steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, outputs: { @@ -93,14 +98,14 @@ describe('test workflow cherryPick', () => { const mergeConflicts = false; const versionsMatch = true; it('behaviour is the same as with actor being the deployer', async () => { - const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); let act = new ExtendedAct(repoPath, workflowPath); - const testMockSteps = { + const testMockSteps: MockStep = { validateActor: mocks.CHERRYPICK__VALIDATEACTOR__FALSE__STEP_MOCKS, cherryPick: mocks.getCherryPickMockSteps(versionsMatch, mergeConflicts), }; - const testMockJobs = { + const testMockJobs: MockJobs = { createNewVersion: { steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, outputs: { @@ -145,14 +150,14 @@ describe('test workflow cherryPick', () => { describe('version match', () => { const versionsMatch = true; it('workflow executes, PR approved and merged automatically', async () => { - const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); let act = new ExtendedAct(repoPath, workflowPath); - const testMockSteps = { + const testMockSteps: MockStep = { validateActor: mocks.CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, }; testMockSteps.cherryPick = mocks.getCherryPickMockSteps(versionsMatch, mergeConflicts); - const testMockJobs = { + const testMockJobs: MockJobs = { createNewVersion: { steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, outputs: { @@ -193,14 +198,14 @@ describe('test workflow cherryPick', () => { describe('version does not match', () => { const versionsMatch = false; it('workflow executes, PR auto-assigned and commented, approved and merged automatically', async () => { - const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); let act = new ExtendedAct(repoPath, workflowPath); - const testMockSteps = { + const testMockSteps: MockStep = { validateActor: mocks.CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, }; testMockSteps.cherryPick = mocks.getCherryPickMockSteps(versionsMatch, mergeConflicts); - const testMockJobs = { + const testMockJobs: MockJobs = { createNewVersion: { steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, outputs: { @@ -244,14 +249,14 @@ describe('test workflow cherryPick', () => { describe('version match', () => { const versionsMatch = true; it('workflow executes, PR auto-assigned and commented, not merged automatically', async () => { - const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); let act = new ExtendedAct(repoPath, workflowPath); - const testMockSteps = { + const testMockSteps: MockStep = { validateActor: mocks.CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, }; testMockSteps.cherryPick = mocks.getCherryPickMockSteps(versionsMatch, mergeConflicts); - const testMockJobs = { + const testMockJobs: MockJobs = { createNewVersion: { steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, outputs: { @@ -292,14 +297,14 @@ describe('test workflow cherryPick', () => { describe('version does not match', () => { const versionsMatch = false; it('workflow executes, PR auto-assigned and commented, not merged automatically', async () => { - const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); let act = new ExtendedAct(repoPath, workflowPath); - const testMockSteps = { + const testMockSteps: MockStep = { validateActor: mocks.CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, }; testMockSteps.cherryPick = mocks.getCherryPickMockSteps(versionsMatch, mergeConflicts); - const testMockJobs = { + const testMockJobs: MockJobs = { createNewVersion: { steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, outputs: { @@ -343,7 +348,7 @@ describe('test workflow cherryPick', () => { describe('automatic trigger', () => { const event = 'pull_request'; it('workflow does not execute', async () => { - const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') || ''; + const repoPath = mockGithub.repo.getPath('testCherryPickWorkflowRepo') ?? ''; const workflowPath = path.join(repoPath, '.github', 'workflows', 'cherryPick.yml'); let act = new ExtendedAct(repoPath, workflowPath); act = utils.setUpActParams( @@ -361,11 +366,11 @@ describe('test workflow cherryPick', () => { PULL_REQUEST_NUMBER: '1234', }, ); - const testMockSteps = { + const testMockSteps: MockStep = { validateActor: mocks.CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, cherryPick: mocks.getCherryPickMockSteps(true, false), }; - const testMockJobs = { + const testMockJobs: MockJobs = { createNewVersion: { steps: mocks.CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, outputs: { diff --git a/workflow_tests/mocks/cherryPickMocks.ts b/workflow_tests/mocks/cherryPickMocks.ts index e7762731c066..c1a33be868d6 100644 --- a/workflow_tests/mocks/cherryPickMocks.ts +++ b/workflow_tests/mocks/cherryPickMocks.ts @@ -110,4 +110,4 @@ const getCherryPickMockSteps = (upToDate: boolean, hasConflicts: boolean): StepI CHERRYPICK__CHERRYPICK__ANNOUNCES_A_CP_FAILURE_IN_THE_ANNOUNCE_SLACK_ROOM__STEP_MOCK, ]; -export {CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, CHERRYPICK__VALIDATEACTOR__FALSE__STEP_MOCKS, CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, getCherryPickMockSteps}; +export default {CHERRYPICK__CREATENEWVERSION__STEP_MOCKS, CHERRYPICK__VALIDATEACTOR__FALSE__STEP_MOCKS, CHERRYPICK__VALIDATEACTOR__TRUE__STEP_MOCKS, getCherryPickMockSteps}; diff --git a/workflow_tests/preDeploy.test.js b/workflow_tests/preDeploy.test.js index 36fe74d687d2..93dde1db061d 100644 --- a/workflow_tests/preDeploy.test.js +++ b/workflow_tests/preDeploy.test.js @@ -1,7 +1,7 @@ const path = require('path'); const kieMockGithub = require('@kie/mock-github'); const utils = require('./utils/utils'); -const assertions = require('./assertions/preDeployAssertions'); +const assertions = require('./assertions/preDeployAssertions').default; const mocks = require('./mocks/preDeployMocks').default; const ExtendedAct = require('./utils/ExtendedAct').default; diff --git a/workflow_tests/utils/JobMocker.ts b/workflow_tests/utils/JobMocker.ts index b6dc99771dd2..dadb85014d01 100644 --- a/workflow_tests/utils/JobMocker.ts +++ b/workflow_tests/utils/JobMocker.ts @@ -1,3 +1,4 @@ +import type {StepIdentifier} from '@kie/act-js'; import type {PathOrFileDescriptor} from 'fs'; import fs from 'fs'; import path from 'path'; @@ -11,26 +12,16 @@ type YamlWorkflow = { }; type MockJob = { - steps: MockJobStep[]; + steps: StepIdentifier[]; uses?: string; secrets?: string[]; with?: string; - outputs?: string[]; - runsOn?: string; + outputs?: Record; + runsOn: string; }; type MockJobs = Record; -type MockJobStep = { - id?: string; - name: string; - run?: string; - mockWith?: string; - with?: string; - envs?: string[]; - inputs?: string[]; -}; - class JobMocker { workflowFile: string; @@ -59,8 +50,8 @@ class JobMocker { jobWith = job.with; delete job.with; } - job.steps = mockJob.steps.map((step) => { - const mockStep: MockJobStep = { + job.steps = mockJob.steps.map((step): StepIdentifier => { + const mockStep: StepIdentifier = { name: step.name, run: step.mockWith, }; @@ -114,4 +105,4 @@ class JobMocker { } export default JobMocker; -export type {MockJob, MockJobs, YamlWorkflow, YamlMockJob, MockJobStep}; +export type {MockJob, MockJobs, YamlWorkflow, YamlMockJob}; diff --git a/workflow_tests/utils/preGenerateTest.ts b/workflow_tests/utils/preGenerateTest.ts index cd57aec8f134..25bdb8f00ae3 100644 --- a/workflow_tests/utils/preGenerateTest.ts +++ b/workflow_tests/utils/preGenerateTest.ts @@ -1,10 +1,11 @@ /* eslint no-console: ["error", { allow: ["warn", "log"] }] */ +import type {StepIdentifier} from '@kie/act-js'; import type {PathLike} from 'fs'; import fs from 'fs'; import path from 'path'; import {exit} from 'process'; import yaml from 'yaml'; -import type {MockJobStep, YamlMockJob, YamlWorkflow} from './JobMocker'; +import type {YamlMockJob, YamlWorkflow} from './JobMocker'; const workflowsDirectory = path.resolve(__dirname, '..', '..', '.github', 'workflows'); const workflowTestsDirectory = path.resolve(__dirname, '..'); @@ -91,7 +92,7 @@ describe('test workflow ${workflowName}', () => { }); `; -const mockStepTemplate = (stepMockName: string, step: MockJobStep, jobId: string | undefined) => ` +const mockStepTemplate = (stepMockName: string, step: StepIdentifier, jobId: string | undefined) => ` const ${stepMockName} = utils.createMockStep( '${step.name ?? ''}', '${step.name ?? ''}', @@ -147,7 +148,7 @@ const assertionsExportsTemplate = (jobAssertions: string[]): string => { // There are other pre-generated files using imports from here, so to keep the interface uniform it's better to just disable it const eslintDisable = jobAssertions.length === 1 ? '// eslint-disable-next-line import/prefer-default-export\n' : ''; - return `\n${eslintDisable}export {\n${assertionsString}\n};\n`; + return `\n${eslintDisable}export default {\n${assertionsString}\n};\n`; }; const checkArguments = (args: string[]) => {