diff --git a/.eslintrc.js b/.eslintrc.js index 0d941f5a66b4..c2198da60c52 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -250,6 +250,14 @@ module.exports = { message: "Please don't declare enums, use union types instead.", }, ], + 'no-restricted-properties': [ + 'error', + { + object: 'Image', + property: 'getSize', + message: 'Usage of Image.getImage is restricted. Please use the `react-native-image-size`.', + }, + ], 'no-restricted-imports': [ 'error', { diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index 02c97d7a0e2b..d779e197c081 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -16918,6 +16918,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.js b/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.js index 950a8e7092cc..ee3415ce21e3 100644 --- a/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.js +++ b/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.js @@ -1,4 +1,5 @@ const _ = require('underscore'); +const lodashThrottle = require('lodash/throttle'); const CONST = require('../../../libs/CONST'); const ActionUtils = require('../../../libs/ActionUtils'); const GitHubUtils = require('../../../libs/GithubUtils'); @@ -56,7 +57,7 @@ function run() { return promiseDoWhile( () => !_.isEmpty(currentStagingDeploys), - _.throttle( + lodashThrottle( throttleFunc, // Poll every 60 seconds instead of every 10 seconds diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index 6dc6a4a213e3..5f768bd4a725 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -8,6 +8,7 @@ /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { const _ = __nccwpck_require__(5067); +const lodashThrottle = __nccwpck_require__(2891); const CONST = __nccwpck_require__(4097); const ActionUtils = __nccwpck_require__(970); const GitHubUtils = __nccwpck_require__(9296); @@ -65,7 +66,7 @@ function run() { return promiseDoWhile( () => !_.isEmpty(currentStagingDeploys), - _.throttle( + lodashThrottle( throttleFunc, // Poll every 60 seconds instead of every 10 seconds @@ -6716,6 +6717,700 @@ function isPlainObject(o) { exports.isPlainObject = isPlainObject; +/***/ }), + +/***/ 9213: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var root = __nccwpck_require__(9882); + +/** Built-in value references. */ +var Symbol = root.Symbol; + +module.exports = Symbol; + + +/***/ }), + +/***/ 7497: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var Symbol = __nccwpck_require__(9213), + getRawTag = __nccwpck_require__(923), + objectToString = __nccwpck_require__(4200); + +/** `Object#toString` result references. */ +var nullTag = '[object Null]', + undefinedTag = '[object Undefined]'; + +/** Built-in value references. */ +var symToStringTag = Symbol ? Symbol.toStringTag : undefined; + +/** + * The base implementation of `getTag` without fallbacks for buggy environments. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ +function baseGetTag(value) { + if (value == null) { + return value === undefined ? undefinedTag : nullTag; + } + return (symToStringTag && symToStringTag in Object(value)) + ? getRawTag(value) + : objectToString(value); +} + +module.exports = baseGetTag; + + +/***/ }), + +/***/ 9528: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var trimmedEndIndex = __nccwpck_require__(7010); + +/** Used to match leading whitespace. */ +var reTrimStart = /^\s+/; + +/** + * The base implementation of `_.trim`. + * + * @private + * @param {string} string The string to trim. + * @returns {string} Returns the trimmed string. + */ +function baseTrim(string) { + return string + ? string.slice(0, trimmedEndIndex(string) + 1).replace(reTrimStart, '') + : string; +} + +module.exports = baseTrim; + + +/***/ }), + +/***/ 2085: +/***/ ((module) => { + +/** Detect free variable `global` from Node.js. */ +var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; + +module.exports = freeGlobal; + + +/***/ }), + +/***/ 923: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var Symbol = __nccwpck_require__(9213); + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString = objectProto.toString; + +/** Built-in value references. */ +var symToStringTag = Symbol ? Symbol.toStringTag : undefined; + +/** + * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the raw `toStringTag`. + */ +function getRawTag(value) { + var isOwn = hasOwnProperty.call(value, symToStringTag), + tag = value[symToStringTag]; + + try { + value[symToStringTag] = undefined; + var unmasked = true; + } catch (e) {} + + var result = nativeObjectToString.call(value); + if (unmasked) { + if (isOwn) { + value[symToStringTag] = tag; + } else { + delete value[symToStringTag]; + } + } + return result; +} + +module.exports = getRawTag; + + +/***/ }), + +/***/ 4200: +/***/ ((module) => { + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString = objectProto.toString; + +/** + * Converts `value` to a string using `Object.prototype.toString`. + * + * @private + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + */ +function objectToString(value) { + return nativeObjectToString.call(value); +} + +module.exports = objectToString; + + +/***/ }), + +/***/ 9882: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var freeGlobal = __nccwpck_require__(2085); + +/** Detect free variable `self`. */ +var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + +/** Used as a reference to the global object. */ +var root = freeGlobal || freeSelf || Function('return this')(); + +module.exports = root; + + +/***/ }), + +/***/ 7010: +/***/ ((module) => { + +/** Used to match a single whitespace character. */ +var reWhitespace = /\s/; + +/** + * Used by `_.trim` and `_.trimEnd` to get the index of the last non-whitespace + * character of `string`. + * + * @private + * @param {string} string The string to inspect. + * @returns {number} Returns the index of the last non-whitespace character. + */ +function trimmedEndIndex(string) { + var index = string.length; + + while (index-- && reWhitespace.test(string.charAt(index))) {} + return index; +} + +module.exports = trimmedEndIndex; + + +/***/ }), + +/***/ 3626: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var isObject = __nccwpck_require__(3334), + now = __nccwpck_require__(8349), + toNumber = __nccwpck_require__(1235); + +/** Error message constants. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeMax = Math.max, + nativeMin = Math.min; + +/** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ +function debounce(func, wait, options) { + var lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = toNumber(wait) || 0; + if (isObject(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + var args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + timeWaiting = wait - timeSinceLastCall; + + return maxing + ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) + : timeWaiting; + } + + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || + (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + var time = now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now()); + } + + function debounced() { + var time = now(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + clearTimeout(timerId); + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; +} + +module.exports = debounce; + + +/***/ }), + +/***/ 3334: +/***/ ((module) => { + +/** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ +function isObject(value) { + var type = typeof value; + return value != null && (type == 'object' || type == 'function'); +} + +module.exports = isObject; + + +/***/ }), + +/***/ 5926: +/***/ ((module) => { + +/** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ +function isObjectLike(value) { + return value != null && typeof value == 'object'; +} + +module.exports = isObjectLike; + + +/***/ }), + +/***/ 6403: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var baseGetTag = __nccwpck_require__(7497), + isObjectLike = __nccwpck_require__(5926); + +/** `Object#toString` result references. */ +var symbolTag = '[object Symbol]'; + +/** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ +function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike(value) && baseGetTag(value) == symbolTag); +} + +module.exports = isSymbol; + + +/***/ }), + +/***/ 8349: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var root = __nccwpck_require__(9882); + +/** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ +var now = function() { + return root.Date.now(); +}; + +module.exports = now; + + +/***/ }), + +/***/ 2891: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var debounce = __nccwpck_require__(3626), + isObject = __nccwpck_require__(3334); + +/** Error message constants. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds. The throttled function comes with a `cancel` + * method to cancel delayed `func` invocations and a `flush` method to + * immediately invoke them. Provide `options` to indicate whether `func` + * should be invoked on the leading and/or trailing edge of the `wait` + * timeout. The `func` is invoked with the last arguments provided to the + * throttled function. Subsequent calls to the throttled function return the + * result of the last `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the throttled function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.throttle` and `_.debounce`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] The number of milliseconds to throttle invocations to. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=true] + * Specify invoking on the leading edge of the timeout. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // Avoid excessively updating the position while scrolling. + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); + * + * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. + * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); + * jQuery(element).on('click', throttled); + * + * // Cancel the trailing throttled invocation. + * jQuery(window).on('popstate', throttled.cancel); + */ +function throttle(func, wait, options) { + var leading = true, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + if (isObject(options)) { + leading = 'leading' in options ? !!options.leading : leading; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + return debounce(func, wait, { + 'leading': leading, + 'maxWait': wait, + 'trailing': trailing + }); +} + +module.exports = throttle; + + +/***/ }), + +/***/ 1235: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var baseTrim = __nccwpck_require__(9528), + isObject = __nccwpck_require__(3334), + isSymbol = __nccwpck_require__(6403); + +/** Used as references for various `Number` constants. */ +var NAN = 0 / 0; + +/** Used to detect bad signed hexadecimal string values. */ +var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; + +/** Used to detect binary string values. */ +var reIsBinary = /^0b[01]+$/i; + +/** Used to detect octal string values. */ +var reIsOctal = /^0o[0-7]+$/i; + +/** Built-in method references without a dependency on `root`. */ +var freeParseInt = parseInt; + +/** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ +function toNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol(value)) { + return NAN; + } + if (isObject(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject(other) ? (other + '') : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = baseTrim(value); + var isBinary = reIsBinary.test(value); + return (isBinary || reIsOctal.test(value)) + ? freeParseInt(value.slice(2), isBinary ? 2 : 8) + : (reIsBadHex.test(value) ? NAN : +value); +} + +module.exports = toNumber; + + /***/ }), /***/ 467: @@ -11680,6 +12375,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js index a83cdd2b71fc..979543ac3ba8 100644 --- a/.github/actions/javascript/checkDeployBlockers/index.js +++ b/.github/actions/javascript/checkDeployBlockers/index.js @@ -11578,6 +11578,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js index 69eef6cfc7fc..c9f8f7d560ce 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js @@ -14751,6 +14751,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index 7dc0300895b0..c99381ffe0b4 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -11560,6 +11560,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index 30cef0dbdc68..da8a91a28c0d 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -11978,6 +11978,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index 71c502f6161f..dd13a9c3f8f4 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -11545,6 +11545,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/getReleaseBody/index.js b/.github/actions/javascript/getReleaseBody/index.js index f07678cbaf24..68e211b29ab0 100644 --- a/.github/actions/javascript/getReleaseBody/index.js +++ b/.github/actions/javascript/getReleaseBody/index.js @@ -11545,6 +11545,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js index 0392015830de..635e224e87bd 100644 --- a/.github/actions/javascript/isStagingDeployLocked/index.js +++ b/.github/actions/javascript/isStagingDeployLocked/index.js @@ -11560,6 +11560,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index e43aec749df7..ebd9d8b16098 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -11741,6 +11741,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js index 37f2ab3cc1e6..09cececf3177 100644 --- a/.github/actions/javascript/postTestBuildComment/index.js +++ b/.github/actions/javascript/postTestBuildComment/index.js @@ -11659,6 +11659,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js index 28b4ab01b5df..59dfa50e2a21 100644 --- a/.github/actions/javascript/reopenIssueWithComment/index.js +++ b/.github/actions/javascript/reopenIssueWithComment/index.js @@ -11570,6 +11570,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index b63c55ae5eb8..f8487d8001a2 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -11543,6 +11543,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js index 26c0757dcde7..2e199b82f31a 100644 --- a/.github/actions/javascript/verifySignedCommits/index.js +++ b/.github/actions/javascript/verifySignedCommits/index.js @@ -11543,6 +11543,7 @@ const POLL_RATE = 10000; exports.POLL_RATE = POLL_RATE; class GithubUtils { static internalOctokit; + static POLL_RATE; /** * Initialize internal octokit * diff --git a/.github/libs/GithubUtils.ts b/.github/libs/GithubUtils.ts index 52122702fb2d..9d536a1ac18b 100644 --- a/.github/libs/GithubUtils.ts +++ b/.github/libs/GithubUtils.ts @@ -75,6 +75,8 @@ type InternalOctokit = OctokitCore & Api & {paginate: PaginateInterface}; class GithubUtils { static internalOctokit: InternalOctokit | undefined; + static POLL_RATE: number; + /** * Initialize internal octokit * diff --git a/.storybook/webpack.config.ts b/.storybook/webpack.config.ts index 2fdae76c1268..3b93756f1df5 100644 --- a/.storybook/webpack.config.ts +++ b/.storybook/webpack.config.ts @@ -31,49 +31,57 @@ switch (process.env.ENV) { } const env = dotenv.config({path: path.resolve(__dirname, `../${envFile}`)}); -const custom: CustomWebpackConfig = require('../config/webpack/webpack.common')({ +const custom: CustomWebpackConfig = require('../config/webpack/webpack.common').default({ envFile, }); const webpackConfig = ({config}: {config: Configuration}) => { - if (config.resolve && config.plugins && config.module) { - config.resolve.alias = { - 'react-native-config': 'react-web-config', - 'react-native$': 'react-native-web', - '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.ts'), - '@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'), - ...custom.resolve.alias, - }; + if (!config.resolve) { + config.resolve = {}; + } + if (!config.plugins) { + config.plugins = []; + } + if (!config.module) { + config.module = {}; + } - // Necessary to overwrite the values in the existing DefinePlugin hardcoded to the Config staging values - const definePluginIndex = config.plugins.findIndex((plugin) => plugin instanceof DefinePlugin); - if (definePluginIndex !== -1 && config.plugins[definePluginIndex] instanceof DefinePlugin) { - const definePlugin = config.plugins[definePluginIndex] as DefinePlugin; - if (definePlugin.definitions) { - definePlugin.definitions.__REACT_WEB_CONFIG__ = JSON.stringify(env); - } - } - config.resolve.extensions = custom.resolve.extensions; + config.resolve.alias = { + 'react-native-config': 'react-web-config', + 'react-native$': 'react-native-web', + '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.ts'), + '@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'), + ...custom.resolve.alias, + }; - const babelRulesIndex = custom.module.rules.findIndex((rule) => rule.loader === 'babel-loader'); - const babelRule = custom.module.rules[babelRulesIndex]; - if (babelRule) { - config.module.rules?.push(babelRule); + // Necessary to overwrite the values in the existing DefinePlugin hardcoded to the Config staging values + const definePluginIndex = config.plugins.findIndex((plugin) => plugin instanceof DefinePlugin); + if (definePluginIndex !== -1 && config.plugins[definePluginIndex] instanceof DefinePlugin) { + const definePlugin = config.plugins[definePluginIndex] as DefinePlugin; + if (definePlugin.definitions) { + definePlugin.definitions.__REACT_WEB_CONFIG__ = JSON.stringify(env); } + } + config.resolve.extensions = custom.resolve.extensions; - const fileLoaderRule = config.module.rules?.find( - (rule): rule is RuleSetRule => - typeof rule !== 'boolean' && typeof rule !== 'string' && typeof rule !== 'number' && !!rule?.test && rule.test instanceof RegExp && rule.test.test('.svg'), - ); - if (fileLoaderRule) { - fileLoaderRule.exclude = /\.svg$/; - } - config.module.rules?.push({ - test: /\.svg$/, - enforce: 'pre', - loader: require.resolve('@svgr/webpack'), - }); + const babelRulesIndex = custom.module.rules.findIndex((rule) => rule.loader === 'babel-loader'); + const babelRule = custom.module.rules[babelRulesIndex]; + if (babelRule) { + config.module.rules?.push(babelRule); + } + + const fileLoaderRule = config.module.rules?.find( + (rule): rule is RuleSetRule => + typeof rule !== 'boolean' && typeof rule !== 'string' && typeof rule !== 'number' && !!rule?.test && rule.test instanceof RegExp && rule.test.test('.svg'), + ); + if (fileLoaderRule) { + fileLoaderRule.exclude = /\.svg$/; } + config.module.rules?.push({ + test: /\.svg$/, + enforce: 'pre', + loader: require.resolve('@svgr/webpack'), + }); return config; }; diff --git a/README.md b/README.md index 7019567c7acb..026a63606db0 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ If you're using another operating system, you will need to ensure `mkcert` is in ## Running the web app 🕸 * To run the **development web app**: `npm run web` -* Changes applied to Javascript will be applied automatically via WebPack as configured in `webpack.dev.js` +* Changes applied to Javascript will be applied automatically via WebPack as configured in `webpack.dev.ts` ## Running the iOS app 📱 For an M1 Mac, read this [SO](https://stackoverflow.com/questions/64901180/how-to-run-cocoapods-on-apple-silicon-m1) for installing cocoapods. diff --git a/android/app/build.gradle b/android/app/build.gradle index 7ae1bab59117..9a00228cd5f5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001045800 - versionName "1.4.58-0" + versionCode 1001045804 + versionName "1.4.58-4" } flavorDimensions "default" diff --git a/config/proxyConfig.js b/config/proxyConfig.ts similarity index 64% rename from config/proxyConfig.js rename to config/proxyConfig.ts index fa09c436461f..0fecef28c1cf 100644 --- a/config/proxyConfig.js +++ b/config/proxyConfig.ts @@ -3,7 +3,14 @@ * We only specify for staging URLs as API requests are sent to the production * servers by default. */ -module.exports = { +type ProxyConfig = { + STAGING: string; + STAGING_SECURE: string; +}; + +const proxyConfig: ProxyConfig = { STAGING: '/staging/', STAGING_SECURE: '/staging-secure/', }; + +export default proxyConfig; diff --git a/config/webpack/CustomVersionFilePlugin.js b/config/webpack/CustomVersionFilePlugin.js deleted file mode 100644 index ed7c0f3dca95..000000000000 --- a/config/webpack/CustomVersionFilePlugin.js +++ /dev/null @@ -1,33 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const APP_VERSION = require('../../package.json').version; - -/** - * Simple webpack plugin that writes the app version (from package.json) and the webpack hash to './version.json' - */ -class CustomVersionFilePlugin { - apply(compiler) { - compiler.hooks.done.tap( - this.constructor.name, - () => - new Promise((resolve, reject) => { - const versionPath = path.join(__dirname, '/../../dist/version.json'); - fs.mkdir(path.dirname(versionPath), {recursive: true}, (dirErr) => { - if (dirErr) { - reject(dirErr); - return; - } - fs.writeFile(versionPath, JSON.stringify({version: APP_VERSION}), 'utf8', (fileErr) => { - if (fileErr) { - reject(fileErr); - return; - } - resolve(); - }); - }); - }), - ); - } -} - -module.exports = CustomVersionFilePlugin; diff --git a/config/webpack/CustomVersionFilePlugin.ts b/config/webpack/CustomVersionFilePlugin.ts new file mode 100644 index 000000000000..96ab8e61e480 --- /dev/null +++ b/config/webpack/CustomVersionFilePlugin.ts @@ -0,0 +1,28 @@ +import fs from 'fs'; +import path from 'path'; +import type {Compiler} from 'webpack'; +import {version as APP_VERSION} from '../../package.json'; + +/** + * Simple webpack plugin that writes the app version (from package.json) and the webpack hash to './version.json' + */ +class CustomVersionFilePlugin { + apply(compiler: Compiler) { + compiler.hooks.done.tap(this.constructor.name, () => { + const versionPath = path.join(__dirname, '/../../dist/version.json'); + fs.mkdir(path.dirname(versionPath), {recursive: true}, (directoryError) => { + if (directoryError) { + throw directoryError; + } + fs.writeFile(versionPath, JSON.stringify({version: APP_VERSION}), {encoding: 'utf8'}, (error) => { + if (!error) { + return; + } + throw error; + }); + }); + }); + } +} + +export default CustomVersionFilePlugin; diff --git a/config/webpack/types.ts b/config/webpack/types.ts new file mode 100644 index 000000000000..45a81feb9bff --- /dev/null +++ b/config/webpack/types.ts @@ -0,0 +1,6 @@ +type Environment = { + file?: string; + platform?: 'web' | 'desktop'; +}; + +export default Environment; diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.ts similarity index 79% rename from config/webpack/webpack.common.js rename to config/webpack/webpack.common.ts index 2fed8a477aab..b0e301ef3a6c 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.ts @@ -1,13 +1,17 @@ -const path = require('path'); -const fs = require('fs'); -const {IgnorePlugin, DefinePlugin, ProvidePlugin, EnvironmentPlugin} = require('webpack'); -const {CleanWebpackPlugin} = require('clean-webpack-plugin'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const CopyPlugin = require('copy-webpack-plugin'); -const dotenv = require('dotenv'); -const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer'); +import {CleanWebpackPlugin} from 'clean-webpack-plugin'; +import CopyPlugin from 'copy-webpack-plugin'; +import dotenv from 'dotenv'; +import fs from 'fs'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import path from 'path'; +import type {Configuration} from 'webpack'; +import {DefinePlugin, EnvironmentPlugin, IgnorePlugin, ProvidePlugin} from 'webpack'; +import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; +import CustomVersionFilePlugin from './CustomVersionFilePlugin'; +import type Environment from './types'; + +// require is necessary, there are no types for this package and the declaration file can't be seen by the build process which causes an error. const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin'); -const CustomVersionFilePlugin = require('./CustomVersionFilePlugin'); const includeModules = [ 'react-native-animatable', @@ -25,29 +29,25 @@ const includeModules = [ 'expo-av', ].join('|'); -const envToLogoSuffixMap = { +const environmentToLogoSuffixMap: Record = { production: '', staging: '-stg', dev: '-dev', adhoc: '-adhoc', }; -function mapEnvToLogoSuffix(envFile) { - let env = envFile.split('.')[2]; - if (typeof env === 'undefined') { - env = 'dev'; +function mapEnvironmentToLogoSuffix(environmentFile: string): string { + let environment = environmentFile.split('.')[2]; + if (typeof environment === 'undefined') { + environment = 'dev'; } - return envToLogoSuffixMap[env]; + return environmentToLogoSuffixMap[environment]; } /** * Get a production grade config for web or desktop - * @param {Object} env - * @param {String} env.envFile path to the env file to be used - * @param {'web'|'desktop'} env.platform - * @returns {Configuration} */ -const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ +const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): Configuration => ({ mode: 'production', devtool: 'source-map', entry: { @@ -68,10 +68,10 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ new HtmlWebpackPlugin({ template: 'web/index.html', filename: 'index.html', - splashLogo: fs.readFileSync(path.resolve(__dirname, `../../assets/images/new-expensify${mapEnvToLogoSuffix(envFile)}.svg`), 'utf-8'), + splashLogo: fs.readFileSync(path.resolve(__dirname, `../../assets/images/new-expensify${mapEnvironmentToLogoSuffix(file)}.svg`), 'utf-8'), isWeb: platform === 'web', - isProduction: envFile === '.env.production', - isStaging: envFile === '.env.staging', + isProduction: file === '.env.production', + isStaging: file === '.env.staging', }), new PreloadWebpackPlugin({ rel: 'preload', @@ -121,12 +121,14 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ ...(platform === 'web' ? [new CustomVersionFilePlugin()] : []), new DefinePlugin({ ...(platform === 'desktop' ? {} : {process: {env: {}}}), - __REACT_WEB_CONFIG__: JSON.stringify(dotenv.config({path: envFile}).parsed), + // eslint-disable-next-line @typescript-eslint/naming-convention + __REACT_WEB_CONFIG__: JSON.stringify(dotenv.config({path: file}).parsed), // React Native JavaScript environment requires the global __DEV__ variable to be accessible. // react-native-render-html uses variable to log exclusively during development. // See https://reactnative.dev/docs/javascript-environment - __DEV__: /staging|prod|adhoc/.test(envFile) === false, + // eslint-disable-next-line @typescript-eslint/naming-convention + __DEV__: /staging|prod|adhoc/.test(file) === false, }), // This allows us to interactively inspect JS bundle contents @@ -203,21 +205,34 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ }, resolve: { alias: { + // eslint-disable-next-line @typescript-eslint/naming-convention 'react-native-config': 'react-web-config', + // eslint-disable-next-line @typescript-eslint/naming-convention 'react-native$': 'react-native-web', + // eslint-disable-next-line @typescript-eslint/naming-convention 'react-native-sound': 'react-native-web-sound', // Module alias for web & desktop // https://webpack.js.org/configuration/resolve/#resolvealias + // eslint-disable-next-line @typescript-eslint/naming-convention '@assets': path.resolve(__dirname, '../../assets'), + // eslint-disable-next-line @typescript-eslint/naming-convention '@components': path.resolve(__dirname, '../../src/components/'), + // eslint-disable-next-line @typescript-eslint/naming-convention '@hooks': path.resolve(__dirname, '../../src/hooks/'), + // eslint-disable-next-line @typescript-eslint/naming-convention '@libs': path.resolve(__dirname, '../../src/libs/'), + // eslint-disable-next-line @typescript-eslint/naming-convention '@navigation': path.resolve(__dirname, '../../src/libs/Navigation/'), + // eslint-disable-next-line @typescript-eslint/naming-convention '@pages': path.resolve(__dirname, '../../src/pages/'), + // eslint-disable-next-line @typescript-eslint/naming-convention '@styles': path.resolve(__dirname, '../../src/styles/'), // This path is provide alias for files like `ONYXKEYS` and `CONST`. + // eslint-disable-next-line @typescript-eslint/naming-convention '@src': path.resolve(__dirname, '../../src/'), + // eslint-disable-next-line @typescript-eslint/naming-convention '@userActions': path.resolve(__dirname, '../../src/libs/actions/'), + // eslint-disable-next-line @typescript-eslint/naming-convention '@desktop': path.resolve(__dirname, '../../desktop'), }, @@ -242,6 +257,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ '.tsx', ], fallback: { + // eslint-disable-next-line @typescript-eslint/naming-convention 'process/browser': require.resolve('process/browser'), crypto: false, }, @@ -275,4 +291,4 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ }, }); -module.exports = webpackConfig; +export default getCommonConfiguration; diff --git a/config/webpack/webpack.desktop.js b/config/webpack/webpack.desktop.ts similarity index 56% rename from config/webpack/webpack.desktop.js rename to config/webpack/webpack.desktop.ts index 20ee4a4025df..09314a9c30db 100644 --- a/config/webpack/webpack.desktop.js +++ b/config/webpack/webpack.desktop.ts @@ -1,29 +1,28 @@ -const path = require('path'); -const webpack = require('webpack'); -const _ = require('underscore'); - -const desktopDependencies = require('../../desktop/package.json').dependencies; -const getCommonConfig = require('./webpack.common'); +import path from 'path'; +import type {Configuration} from 'webpack'; +import webpack from 'webpack'; +// eslint-disable-next-line @dword-design/import-alias/prefer-alias, import/no-relative-packages -- alias imports don't work for webpack +import {dependencies as desktopDependencies} from '../../desktop/package.json'; +import type Environment from './types'; +import getCommonConfiguration from './webpack.common'; /** * Desktop creates 2 configurations in parallel * 1. electron-main - the core that serves the app content * 2. web - the app content that would be rendered in electron * Everything is placed in desktop/dist and ready for packaging - * @param {Object} env - * @returns {webpack.Configuration[]} */ -module.exports = (env) => { - const rendererConfig = getCommonConfig({...env, platform: 'desktop'}); +const getConfiguration = (environment: Environment): Configuration[] => { + const rendererConfig = getCommonConfiguration({...environment, platform: 'desktop'}); const outputPath = path.resolve(__dirname, '../../desktop/dist'); rendererConfig.name = 'renderer'; - rendererConfig.output.path = path.join(outputPath, 'www'); + (rendererConfig.output ??= {}).path = path.join(outputPath, 'www'); // Expose react-native-config to desktop-main - const definePlugin = _.find(rendererConfig.plugins, (plugin) => plugin.constructor === webpack.DefinePlugin); + const definePlugin = rendererConfig.plugins?.find((plugin) => plugin?.constructor === webpack.DefinePlugin); - const mainProcessConfig = { + const mainProcessConfig: Configuration = { mode: 'production', name: 'desktop-main', target: 'electron-main', @@ -38,13 +37,15 @@ module.exports = (env) => { }, resolve: rendererConfig.resolve, plugins: [definePlugin], - externals: [..._.keys(desktopDependencies), 'fsevents'], + externals: [...Object.keys(desktopDependencies), 'fsevents'], node: { /** * Disables webpack processing of __dirname and __filename, so it works like in node * https://github.com/webpack/webpack/issues/2010 */ + // eslint-disable-next-line @typescript-eslint/naming-convention __dirname: false, + // eslint-disable-next-line @typescript-eslint/naming-convention __filename: false, }, module: { @@ -60,3 +61,5 @@ module.exports = (env) => { return [mainProcessConfig, rendererConfig]; }; + +export default getConfiguration; diff --git a/config/webpack/webpack.dev.js b/config/webpack/webpack.dev.ts similarity index 67% rename from config/webpack/webpack.dev.js rename to config/webpack/webpack.dev.ts index e28383eff557..8f32a2d95c99 100644 --- a/config/webpack/webpack.dev.js +++ b/config/webpack/webpack.dev.ts @@ -1,34 +1,39 @@ -const path = require('path'); -const portfinder = require('portfinder'); -const {DefinePlugin} = require('webpack'); -const {merge} = require('webpack-merge'); -const {TimeAnalyticsPlugin} = require('time-analytics-webpack-plugin'); -const getCommonConfig = require('./webpack.common'); +import path from 'path'; +import portfinder from 'portfinder'; +import {TimeAnalyticsPlugin} from 'time-analytics-webpack-plugin'; +import type {Configuration} from 'webpack'; +import {DefinePlugin} from 'webpack'; +import type {Configuration as DevServerConfiguration} from 'webpack-dev-server'; +import {merge} from 'webpack-merge'; +import type Environment from './types'; +import getCommonConfiguration from './webpack.common'; const BASE_PORT = 8082; /** * Configuration for the local dev server - * @param {Object} env - * @returns {Configuration} */ -module.exports = (env = {}) => +const getConfiguration = (environment: Environment): Promise => portfinder.getPortPromise({port: BASE_PORT}).then((port) => { // Check if the USE_WEB_PROXY variable has been provided // and rewrite any requests to the local proxy server - const proxySettings = + const proxySettings: Pick = process.env.USE_WEB_PROXY === 'false' ? {} : { proxy: { + // eslint-disable-next-line @typescript-eslint/naming-convention '/api': 'http://[::1]:9000', + // eslint-disable-next-line @typescript-eslint/naming-convention '/staging': 'http://[::1]:9000', + // eslint-disable-next-line @typescript-eslint/naming-convention '/chat-attachments': 'http://[::1]:9000', + // eslint-disable-next-line @typescript-eslint/naming-convention '/receipts': 'http://[::1]:9000', }, }; - const baseConfig = getCommonConfig(env); + const baseConfig = getCommonConfiguration(environment); const config = merge(baseConfig, { mode: 'development', @@ -55,12 +60,13 @@ module.exports = (env = {}) => }, plugins: [ new DefinePlugin({ + // eslint-disable-next-line @typescript-eslint/naming-convention 'process.env.PORT': port, }), ], cache: { type: 'filesystem', - name: env.platform || 'default', + name: environment.platform ?? 'default', buildDependencies: { // By default, webpack and loaders are build dependencies // This (also) makes all dependencies of this config file - build dependencies @@ -78,3 +84,5 @@ module.exports = (env = {}) => return TimeAnalyticsPlugin.wrap(config); }); + +export default getConfiguration; diff --git a/contributingGuides/APPLE_GOOGLE_SIGNIN.md b/contributingGuides/APPLE_GOOGLE_SIGNIN.md index 5c95dfb60950..e6b999b7cb01 100644 --- a/contributingGuides/APPLE_GOOGLE_SIGNIN.md +++ b/contributingGuides/APPLE_GOOGLE_SIGNIN.md @@ -146,7 +146,7 @@ After you've set ngrok up to be able to run on your machine (requires configurin ngrok http 8082 --host-header="dev.new.expensify.com:8082" --subdomain=mysubdomain ``` -The `--host-header` flag is there to avoid webpack errors with header validation. In addition, add `allowedHosts: 'all'` to the dev server config in `webpack.dev.js`: +The `--host-header` flag is there to avoid webpack errors with header validation. In addition, add `allowedHosts: 'all'` to the dev server config in `webpack.dev.ts`: ```js devServer: { @@ -243,9 +243,9 @@ Here's how you can re-enable the SSO buttons in development mode: => { - const devServer = `webpack-dev-server --config config/webpack/webpack.dev.js --port ${port} --env platform=desktop`; - const buildMain = 'webpack watch --config config/webpack/webpack.desktop.js --config-name desktop-main --mode=development'; + .then((port) => { + const devServer = `webpack-dev-server --config config/webpack/webpack.dev.ts --port ${port} --env platform=desktop`; + const buildMain = 'webpack watch --config config/webpack/webpack.desktop.ts --config-name desktop-main --mode=development'; const env = { PORT: port, diff --git a/docs/articles/expensify-classic/workspaces/Create-tags.md b/docs/articles/expensify-classic/workspaces/Create-tags.md new file mode 100644 index 000000000000..74967ee04c7a --- /dev/null +++ b/docs/articles/expensify-classic/workspaces/Create-tags.md @@ -0,0 +1,85 @@ +--- +title: Create tags +description: Code expenses by creating tags +--- +
+ +You can tag expenses for a specific department, project, location, cost center, customer, etc. You can also use different tags for each workspace to create customized coding for different employees. + +You can use single tags or multi-level tags: +- **Single Tags**: Employees click one dropdown to select one tag. Single tags are helpful if employees need to select only one tag from a list, for example their department. +- **Multi-level Tags**: Employees click multiple dropdowns to select more than one tag. You can also create dependent tags that only appear if another tag has already been selected. Multi-tags are helpful if you have multiple tags, for example projects, locations, cost centers, etc., for employees to select, or if you have dependent tags. For example, if an employee selects a specific department, another tag can appear where they have to select their project. + +To add your tags, you can either import them for an accounting system or spreadsheet, or add them manually. + +# Single tags + +## Import a spreadsheet + +You can add a list of single tags by importing them in a .csv, .txt, .xls, or .xlsx spreadsheet. + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Tags** tab on the left. +5. Click **Import from Spreadsheet**. +6. Review the guidelines, select the checkbox if your file has headers as the first row, and click **Upload File**. + +{% include info.html %} +Each time you upload a list of tags, it will override your previous list. To avoid losing tags, update your current spreadsheet and re-import it into Expensify. +{% include end-info.html %} + +## Manually add individual tags + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Tags** tab on the left. +5. Enter a tag name into the field and click **Add**. + +# Multi-level tags + +## Automatic import with accounting integration + +When you first connect your accounting integration (for example, QuickBooks Online, QuickBooks Desktop, Sage Intacct, Xero, or NetSuite), you’ll configure classes, customers, projects, departments locations, etc. that automatically import into Expensify as tags. + +1. To update your tags in Expensify, you must first update the tag in your accounting system. Then in Expensify, +2. Hover over Settings, then click **Workspaces**. +3. Click the **Group** tab on the left. +4. Click the desired workspace name. +5. Click the **Connections** tab on the left. +6. Click **Sync Now**. + +## Import a spreadsheet + +You can add a list of single tags by importing them in a .csv, .txt, .xls, or .xlsx spreadsheet. + +1. Determine whether you will use independent (a separate tag for department and project) or dependent tags (the project tags populate different options based on the department selected), and whether you will capture general ledge (GL) codes. Then use one of the following templates to build your tags list: + - [Dependent tags with GL codes](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fus.v-cdn.net%2F6030147%2Fuploads%2FO7G7UWJCCFXC%2Fdependant-tag-with-gl-code-template.xlsx) + - [Dependent tags without GL codes](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fus.v-cdn.net%2F6030147%2Fuploads%2FY7DCMUVLSHEO%2Fdependant-tag-without-gl-code-template.xlsx) + - [Independent tags with GL codes](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fs3-us-west-1.amazonaws.com%2Fconcierge-responses-expensify-com%2Fuploads%252F1618929581886-Independent%2Bwith%2BGL%2Bcodes%2Bformat%2B-%2BSheet1.csv) + - [Independent tags without GL codes](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fs3-us-west-1.amazonaws.com%2Fconcierge-responses-expensify-com%2Fuploads%252F1618929575401-Independent%2Bwithout%2BGL%2Bcodes%2Bformat%2B-%2BSheet1.csv) + +{% include info.html %} +If you have more than 50,000 tags, divide them into two separate files. +{% include end-info.html %} + +2. Hover over Settings, then click **Workspaces**. +3. Click the **Group** tab on the left. +4. Click the desired workspace name. +5. Click the **Tags** tab on the left. +6. Enable the “Use multiple levels of tags” option. +7. Click **Import from Spreadsheet**. +8. Select the applicable checkboxes and click **Upload Tags**. + +{% include info.html %} +Each time you upload a list of tags, it will override your previous list. To avoid losing tags, update your current spreadsheet and re-import it into Expensify. +{% include end-info.html %} + +# FAQs + +**Why can’t I see a "Do you want to use multiple level tags" option on my workspace.** + +If you are connected to an accounting integration, you will not see this feature. You will need to add those tags in your integration first, then sync the connection. + +
diff --git a/docs/articles/expensify-classic/workspaces/Tags.md b/docs/articles/expensify-classic/workspaces/Tags.md deleted file mode 100644 index d802a183c8ba..000000000000 --- a/docs/articles/expensify-classic/workspaces/Tags.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: Workspace Tags ---- -# Overview -You can use tags to assign expenses to a specific department, project, location, cost center, and more. - -Note that tags function differently depending on whether or not you connect Expensify to a direct account integration (i.e., QuickBooks Online, NetSuite, etc.). With that said, this article covers tags that work for all account setups. -# How to use Tags -Tags are a workspace-level feature. They’re generally used to code expenses to things like customers, projects, locations, or departments. at the expense level. You can have different sets of tags for different workspaces, allowing you to customize coding for cohorts of employees. - -With that said, tags come in two forms: single tags and multi-level tags. - -## Single Tags -Single tags refer to the simplest version of tags, allowing users to code expenses on a single level. With a single tag setup, users will pick from the list of tags you created and make a single selection on each expense. -## Multi-Level Tags -On the other hand, Multi-Level Tags refer to a more advanced tagging system that allows you to code expenses in a hierarchical or nested manner. Unlike single tags, which are standalone labels, multi-level tags enable you to create a structured hierarchy of tags, with sub-tags nested within parent tags. This feature is particularly useful for organizations that require a more detailed and organized approach to expense tracking. -# How to import single tags (no accounting integration connected) -## Add single tags via spreadsheet -To set up Tags, follow these steps: -- Go to **Settings > Workspace > Group / Individual > [Workspace name] > Tags**. -- You can choose to add tags one by one, or upload them in bulk via a spreadsheet. - -After downloading the CSV and creating the tags you want to import, go to the Tags section in the policy editor: Settings > Workspaces > Group > [Workspace name] > Tags - Enable multi-level tags by toggling the button. -Click "Import from Spreadsheet" to bring in your CSV. - Indicate whether the first line contains the tag header. -Choose if the tag list is independent or dependent (matching your CSV). -Decide if your tags list includes GL codes. -Upload your CSV or TSV file. -Confirm your file and update your tags list. -## Manually add single tags - -If you need to add Tags to your workspace manually, you can follow the steps below. - -On web: - -1. Navigate to Settings > Workspace > Group / Individual > [Workspace name] > Tags. -2. Add new tags under Add a Category. - -On mobile: - -1. Tap the three-bar menu icon at the top left corner of the app -2. Tap on Settings in the menu on the left side -3. Scroll to the Workspace subhead and click on tags listed underneath the default policy -4. Add new categories by tapping the + button in the upper right corner. To delete a category, on iOS swipe left, on Android press and hold. Tap a category name to edit it. - -# How to import multi-level tags (no accounting integration connected) -To use multi-level tags, go to the Tags section in your workspace settings. -Toggle on "Do you want to use multiple levels of tags?" - -This feature is available for companies with group workspaces and helps accountants track more details in expenses. - -If you need to make changes to your multi-level tags, follow these steps: -1. Start by editing them in a CSV file. -2. Import the revised tags into Expensify. -3. Remember to back up your tags! Uploading a CSV will replace your existing settings. -4. Safest Option: Download the old CSV from the Tags page using 'Export to CSV,' make edits, then import it. - -## Manage multi-level tags -Once multi-level tagging has been set up, employees will be able to choose more than one tag per expense. Based on the choice made for the first tag, the second subset of tag options will appear. After the second tag is chosen, more tag lists can appear, customizable up to 5 tag levels. - -### Best Practices -- Multi-level tagging is available for companies on group workspaces and is intended to help accountants track additional information at the expense line-item level. -- If you need to make any changes to the Tags, you need to first make them on the CSV and import the revised Tags into Expensify. -- Make sure to have a backup of your tags! Every time you upload a CSV it will override your previous settings. -- The easiest way to keep the old CSV is to download it from the Tags page by clicking Export to CSV, editing the list, and then importing it to apply the changes. - - -# How to import tags with an accounting integration connected -If you have connected Expensify to a direct integration such as QuickBooks Online, QuickBooks Desktop, Sage Intacct, Xero, or NetSuite, then Expensify automatically imports XYZ from your accounting system as tags. - -When you first connect your accounting integration you’ll configure classes, customers, projects, departments locations, etc. to import as tags in Expensify. - -If you need to update your tags in Expensify, you will first need to update them in your accounting system, then sync the connection in Expensify by navigating to Settings > Workspace > Group > [Workspace Name] > Connection > Sync Now. - -Alternatively, if you update the tag details in your accounting integration, be sure to sync the policy connection so that the updated information is available on the workspace. - -# Deep Dive -## Make tags required -You can require tags for any workspace expenses by enabling People must tag expenses on the Tags page by navigating to Settings > Workspace > Group > [Workspace Name] > Tags. -{% include faq-begin.md %} - -## What are the different tag options? -If you want your second tag to depend on the first one, use dependent tags. Include GL codes if needed, especially when using accounting integrations. -For other scenarios, like not using accounting integrations, use independent tags, which can still include GL codes if necessary. - - -## Are the multi-level tags only available with a certain subscription (pricing plan)? -Multi-level tagging is only available with the Control type policy. - -## I can’t see "Do you want to use multiple level tags" feature on my company's expense workspace. Why is that? -If you are connected to an accounting integration, you will not see this feature. You will need to add those tags in your integration first, then sync the connection. - -{% include faq-end.md %} diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 93f1a36c7b21..8860c0883a12 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.58.0 + 1.4.58.4 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3e06b69b2fbf..8fe00fa73a5d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.58.0 + 1.4.58.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 143069697214..ec61c4ff9a96 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.58 CFBundleVersion - 1.4.58.0 + 1.4.58.4 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a7b017e734e2..24ef0704be25 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1363,7 +1363,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.33): + - RNLiveMarkdown (0.1.35): - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -1904,7 +1904,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 25b969a1ffc806b9f9ad2e170d4a3b049c6af85e RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: aaf75630fb2129db43fb5a873d33125e7173f3a0 + RNLiveMarkdown: aaf5afb231515d8ddfdef5f2928581e8ff606ad4 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: fcf7f1cbdc8bd7569c267d07284e8a5c7bee06ed RNPermissions: 9b086c8f05b2e2faa587fdc31f4c5ab4509728aa diff --git a/package-lock.json b/package-lock.json index a347633816ea..f0e3a21cea5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "new.expensify", - "version": "1.4.58-0", + "version": "1.4.58-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.58-0", + "version": "1.4.58-4", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.33", + "@expensify/react-native-live-markdown": "0.1.35", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -88,7 +88,7 @@ "react-native-google-places-autocomplete": "2.5.6", "react-native-haptic-feedback": "^2.2.0", "react-native-image-picker": "^7.0.3", - "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#8393b7e58df6ff65fd41f60aee8ece8822c91e2b", + "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", "react-native-key-command": "^1.0.6", "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", @@ -182,6 +182,8 @@ "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", "@types/underscore": "^1.11.5", + "@types/webpack": "^5.28.5", + "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^6.13.2", "@vercel/ncc": "0.38.1", @@ -194,9 +196,9 @@ "babel-plugin-react-native-web": "^0.18.7", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", - "clean-webpack-plugin": "^3.0.0", + "clean-webpack-plugin": "^4.0.0", "concurrently": "^8.2.2", - "copy-webpack-plugin": "^6.4.1", + "copy-webpack-plugin": "^10.1.0", "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", @@ -3095,9 +3097,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.33", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.33.tgz", - "integrity": "sha512-K9WDwb7wdupGrOrZEFFQ57qNPYdGVNkF5qnhOfkhuvSL9UdZi3NLiyGzaohIIh1lXvElDgwaY0x0WtqkOXIsiw==", + "version": "0.1.35", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.35.tgz", + "integrity": "sha512-W0FFIiU/sT+AwIrIOUHiNAHYjODAkEdYsf75tfBbkA6v2byHPxUlbzaJrZEQc0HgbvtAfTf9iQQqGWjNqe4pog==", "engines": { "node": ">= 18.0.0" }, @@ -6792,6 +6794,61 @@ "version": "2.0.1", "license": "BSD-3-Clause" }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.10", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@mapbox/point-geometry": { "version": "0.1.0", "license": "ISC" @@ -8129,8 +8186,9 @@ } }, "node_modules/@react-native-community/cli-doctor/node_modules/ip": { - "version": "1.1.8", - "license": "MIT" + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" }, "node_modules/@react-native-community/cli-doctor/node_modules/strip-ansi": { "version": "5.2.0", @@ -8212,8 +8270,9 @@ } }, "node_modules/@react-native-community/cli-hermes/node_modules/ip": { - "version": "1.1.8", - "license": "MIT" + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" }, "node_modules/@react-native-community/cli-hermes/node_modules/supports-color": { "version": "7.2.0", @@ -11918,6 +11977,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@storybook/builder-webpack4/node_modules/@types/webpack": { + "version": "4.41.38", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/@storybook/builder-webpack4/node_modules/@webassemblyjs/helper-buffer": { "version": "1.9.0", "dev": true, @@ -14435,6 +14507,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@storybook/core-server/node_modules/@types/webpack": { + "version": "4.41.38", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/@storybook/core-server/node_modules/@webassemblyjs/helper-buffer": { "version": "1.9.0", "dev": true, @@ -15411,6 +15496,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@storybook/manager-webpack4/node_modules/@types/webpack": { + "version": "4.41.38", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/@storybook/manager-webpack4/node_modules/@webassemblyjs/helper-buffer": { "version": "1.9.0", "dev": true, @@ -18827,7 +18925,7 @@ } }, "node_modules/@types/source-list-map": { - "version": "0.1.2", + "version": "0.1.6", "dev": true, "license": "MIT" }, @@ -18845,7 +18943,7 @@ "license": "MIT" }, "node_modules/@types/uglify-js": { - "version": "3.17.0", + "version": "3.17.5", "dev": true, "license": "MIT", "dependencies": { @@ -18873,16 +18971,31 @@ "optional": true }, "node_modules/@types/webpack": { - "version": "4.41.32", + "version": "5.28.5", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", - "@types/tapable": "^1", - "@types/uglify-js": "*", - "@types/webpack-sources": "*", - "anymatch": "^3.0.0", - "source-map": "^0.6.0" + "tapable": "^2.2.0", + "webpack": "^5" + } + }, + "node_modules/@types/webpack-bundle-analyzer": { + "version": "4.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, + "node_modules/@types/webpack-bundle-analyzer/node_modules/tapable": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/@types/webpack-env": { @@ -18891,7 +19004,7 @@ "license": "MIT" }, "node_modules/@types/webpack-sources": { - "version": "3.2.0", + "version": "3.2.3", "dev": true, "license": "MIT", "dependencies": { @@ -18908,6 +19021,14 @@ "node": ">= 8" } }, + "node_modules/@types/webpack/node_modules/tapable": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@types/ws": { "version": "8.5.3", "dev": true, @@ -19884,6 +20005,11 @@ "version": "2.0.6", "license": "BSD-3-Clause" }, + "node_modules/abbrev": { + "version": "1.1.1", + "license": "ISC", + "optional": true + }, "node_modules/abort-controller": { "version": "3.0.0", "license": "MIT", @@ -20397,7 +20523,7 @@ }, "node_modules/aproba": { "version": "1.2.0", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/archiver": { @@ -20463,7 +20589,7 @@ }, "node_modules/are-we-there-yet": { "version": "2.0.0", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "delegates": "^1.0.0", @@ -20475,7 +20601,7 @@ }, "node_modules/are-we-there-yet/node_modules/readable-stream": { "version": "3.6.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -22792,6 +22918,20 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas": { + "version": "2.11.2", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/canvas-size": { "version": "1.2.6", "license": "MIT" @@ -23086,8 +23226,10 @@ }, "node_modules/classnames": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.0.tgz", - "integrity": "sha512-FQuRlyKinxrb5gwJlfVASbSrDlikDJ07426TrfPsdGLvtochowmkbnSFdQGJ2aoXrSetq5KqGV9emvWpy+91xA==" + "license": "MIT", + "workspaces": [ + "benchmarks" + ] }, "node_modules/clean-css": { "version": "5.3.2", @@ -23107,18 +23249,17 @@ } }, "node_modules/clean-webpack-plugin": { - "version": "3.0.0", + "version": "4.0.0", "dev": true, "license": "MIT", "dependencies": { - "@types/webpack": "^4.4.31", "del": "^4.1.1" }, "engines": { - "node": ">=8.9.0" + "node": ">=10.0.0" }, "peerDependencies": { - "webpack": "*" + "webpack": ">=4.0.0 <6.0.0" } }, "node_modules/cli-boxes": { @@ -23330,7 +23471,7 @@ }, "node_modules/color-support": { "version": "1.1.3", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "color-support": "bin.js" @@ -23791,7 +23932,7 @@ }, "node_modules/console-control-strings": { "version": "1.1.0", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/constants-browserify": { @@ -23892,136 +24033,115 @@ } }, "node_modules/copy-webpack-plugin": { - "version": "6.4.1", + "version": "10.2.4", "dev": true, "license": "MIT", "dependencies": { - "cacache": "^15.0.5", - "fast-glob": "^3.2.4", - "find-cache-dir": "^3.3.1", - "glob-parent": "^5.1.1", - "globby": "^11.0.1", - "loader-utils": "^2.0.0", + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^12.0.2", "normalize-path": "^3.0.0", - "p-limit": "^3.0.2", - "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1", - "webpack-sources": "^1.4.3" + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 12.20.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.1.0" } }, - "node_modules/copy-webpack-plugin/node_modules/find-cache-dir": { - "version": "3.3.2", + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", "dev": true, "license": "MIT", "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" + "fast-deep-equal": "^3.1.3" }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + "peerDependencies": { + "ajv": "^8.8.2" } }, - "node_modules/copy-webpack-plugin/node_modules/find-up": { - "version": "4.1.0", + "node_modules/copy-webpack-plugin/node_modules/array-union": { + "version": "3.0.1", "dev": true, "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/copy-webpack-plugin/node_modules/locate-path": { - "version": "5.0.0", + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "p-locate": "^4.1.0" + "is-glob": "^4.0.3" }, "engines": { - "node": ">=8" + "node": ">=10.13.0" } }, - "node_modules/copy-webpack-plugin/node_modules/make-dir": { - "version": "3.1.0", + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "12.2.0", "dev": true, "license": "MIT", "dependencies": { - "semver": "^6.0.0" + "array-union": "^3.0.1", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.7", + "ignore": "^5.1.9", + "merge2": "^1.4.1", + "slash": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/copy-webpack-plugin/node_modules/p-locate": { - "version": "4.1.0", + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">=8" + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/copy-webpack-plugin/node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", + "node_modules/copy-webpack-plugin/node_modules/serialize-javascript": { + "version": "6.0.2", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "randombytes": "^2.1.0" } }, - "node_modules/copy-webpack-plugin/node_modules/path-exists": { + "node_modules/copy-webpack-plugin/node_modules/slash": { "version": "4.0.0", "dev": true, "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/copy-webpack-plugin/node_modules/pkg-dir": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" + "node": ">=12" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/copy-webpack-plugin/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/core-js": { @@ -25151,7 +25271,7 @@ }, "node_modules/delegates": { "version": "1.0.0", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/denodeify": { @@ -25214,6 +25334,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/detect-libc": { + "version": "2.0.1", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "license": "MIT", @@ -27523,14 +27651,16 @@ }, "node_modules/expo-image-loader": { "version": "4.6.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-4.6.0.tgz", + "integrity": "sha512-RHQTDak7/KyhWUxikn2yNzXL7i2cs16cMp6gEAgkHOjVhoCJQoOJ0Ljrt4cKQ3IowxgCuOrAgSUzGkqs7omj8Q==", "peerDependencies": { "expo": "*" } }, "node_modules/expo-image-manipulator": { "version": "11.8.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-11.8.0.tgz", + "integrity": "sha512-ZWVrHnYmwJq6h7auk+ropsxcNi+LyZcPFKQc8oy+JA0SaJosfShvkCm7RADWAunHmfPCmjHrhwPGEu/rs7WG/A==", "dependencies": { "expo-image-loader": "~4.6.0" }, @@ -28751,7 +28881,7 @@ }, "node_modules/gauge": { "version": "3.0.2", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "aproba": "^1.0.3 || ^2.0.0", @@ -29225,7 +29355,7 @@ }, "node_modules/has-unicode": { "version": "2.0.1", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/has-value": { @@ -30442,9 +30572,10 @@ } }, "node_modules/ip": { - "version": "2.0.0", - "dev": true, - "license": "MIT" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "dev": true }, "node_modules/ip-regex": { "version": "2.1.0", @@ -36353,7 +36484,6 @@ }, "node_modules/nan": { "version": "2.17.0", - "dev": true, "license": "MIT", "optional": true }, @@ -36673,7 +36803,7 @@ }, "node_modules/npmlog": { "version": "5.0.1", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "are-we-there-yet": "^2.0.0", @@ -37029,8 +37159,7 @@ }, "node_modules/onfido-sdk-ui": { "version": "14.15.0", - "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-14.15.0.tgz", - "integrity": "sha512-4Z+tnH6pQjK4SyazlzJq17NXO8AnhGcwEACbA3PVbAo90LBpGu1WAZ1r6VidlxFr/oPbu6sg/hisYvfXiqOtTg==" + "license": "SEE LICENSE in LICENSE" }, "node_modules/open": { "version": "8.4.2", @@ -39277,8 +39406,7 @@ }, "node_modules/react-native-image-size": { "version": "1.1.3", - "resolved": "git+ssh://git@github.com/Expensify/react-native-image-size.git#8393b7e58df6ff65fd41f60aee8ece8822c91e2b", - "integrity": "sha512-TDqeqGrxL+iRweaiDxg0jlMOmW8/7bti9PFi8rSRgGdiyu7loiuq4tmTGPAOefQtgDxoNRdSviH5isUyd3FV8g==", + "resolved": "git+ssh://git@github.com/Expensify/react-native-image-size.git#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", "license": "MIT" }, "node_modules/react-native-key-command": { @@ -39494,8 +39622,10 @@ }, "node_modules/react-native-release-profiler": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/react-native-release-profiler/-/react-native-release-profiler-0.1.6.tgz", - "integrity": "sha512-kSAPYjO3PDzV4xbjgj2NoiHtL7EaXmBira/WOcyz6S7mz1MVBoF0Bj74z5jAZo6BoBJRKqmQWI4ep+m0xvoF+g==", + "license": "MIT", + "workspaces": [ + "example" + ], "dependencies": { "@react-native-community/cli": "^12.2.1", "commander": "^11.1.0" @@ -39513,8 +39643,7 @@ }, "node_modules/react-native-release-profiler/node_modules/commander": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", "engines": { "node": ">=16" } @@ -39582,8 +39711,7 @@ }, "node_modules/react-native-share": { "version": "10.0.2", - "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-10.0.2.tgz", - "integrity": "sha512-EZs4MtsyauAI1zP8xXT1hIFB/pXOZJNDCKcgCpEfTZFXgCUzz8MDVbI1ocP2hA59XHRSkqAQdbJ0BFTpjxOBlg==", + "license": "MIT", "engines": { "node": ">=16" } @@ -41925,6 +42053,57 @@ "version": "3.0.7", "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-get/node_modules/decompress-response": { + "version": "4.2.1", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-get/node_modules/mimic-response": { + "version": "2.1.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/simple-git": { "version": "3.19.0", "license": "MIT", @@ -46190,7 +46369,7 @@ }, "node_modules/wide-align": { "version": "1.1.5", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" diff --git a/package.json b/package.json index a6171393880c..092aa4dabcc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.58-0", + "version": "1.4.58-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -20,10 +20,10 @@ "start": "npx react-native start", "web": "scripts/set-pusher-suffix.sh && concurrently npm:web-proxy npm:web-server", "web-proxy": "ts-node web/proxy.ts", - "web-server": "webpack-dev-server --open --config config/webpack/webpack.dev.js", - "build": "webpack --config config/webpack/webpack.common.js --env envFile=.env.production", - "build-staging": "webpack --config config/webpack/webpack.common.js --env envFile=.env.staging", - "build-adhoc": "webpack --config config/webpack/webpack.common.js --env envFile=.env.adhoc", + "web-server": "webpack-dev-server --open --config config/webpack/webpack.dev.ts", + "build": "webpack --config config/webpack/webpack.common.ts --env file=.env.production", + "build-staging": "webpack --config config/webpack/webpack.common.ts --env file=.env.staging", + "build-adhoc": "webpack --config config/webpack/webpack.common.ts --env file=.env.adhoc", "desktop": "scripts/set-pusher-suffix.sh && ts-node desktop/start.ts", "desktop-build": "scripts/build-desktop.sh production", "desktop-build-staging": "scripts/build-desktop.sh staging", @@ -47,7 +47,7 @@ "storybook-build-staging": "ENV=staging build-storybook -o dist/docs", "gh-actions-build": "./.github/scripts/buildActions.sh", "gh-actions-validate": "./.github/scripts/validateActionsAndWorkflows.sh", - "analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.js --env envFile=.env.production", + "analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.ts --env file=.env.production", "symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map", "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", "symbolicate-release:ios": "scripts/release-profile.ts --platform=ios", @@ -62,7 +62,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.33", + "@expensify/react-native-live-markdown": "0.1.35", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -139,7 +139,7 @@ "react-native-google-places-autocomplete": "2.5.6", "react-native-haptic-feedback": "^2.2.0", "react-native-image-picker": "^7.0.3", - "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#8393b7e58df6ff65fd41f60aee8ece8822c91e2b", + "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", "react-native-key-command": "^1.0.6", "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", @@ -233,6 +233,8 @@ "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", "@types/underscore": "^1.11.5", + "@types/webpack": "^5.28.5", + "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^6.13.2", "@vercel/ncc": "0.38.1", @@ -245,9 +247,9 @@ "babel-plugin-react-native-web": "^0.18.7", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", - "clean-webpack-plugin": "^3.0.0", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^10.1.0", "concurrently": "^8.2.2", - "copy-webpack-plugin": "^6.4.1", "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh index efbca35a498c..791f59d73330 100755 --- a/scripts/build-desktop.sh +++ b/scripts/build-desktop.sh @@ -30,7 +30,7 @@ title "Bundling Desktop js Bundle Using Webpack" info " • ELECTRON_ENV: $ELECTRON_ENV" info " • ENV file: $ENV_FILE" info "" -npx webpack --config config/webpack/webpack.desktop.js --env envFile=$ENV_FILE +npx webpack --config config/webpack/webpack.desktop.ts --env file=$ENV_FILE title "Building Desktop App Archive Using Electron" info "" diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 28835f80365b..c134d2a65db2 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -535,7 +535,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.OLD_POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; - [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; + [ONYXKEYS.COLLECTION.SELECTED_TAB]: OnyxTypes.SelectedTabRequest; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 19244913174d..e0ad50a75645 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -1,11 +1,12 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useMemo, useRef, useState} from 'react'; -import {Alert, Image as RNImage, View} from 'react-native'; +import {Alert, View} from 'react-native'; import RNFetchBlob from 'react-native-blob-util'; import RNDocumentPicker from 'react-native-document-picker'; import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker'; import {launchImageLibrary} from 'react-native-image-picker'; import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker'; +import ImageSize from 'react-native-image-size'; import type {FileObject, ImagePickerResponse as FileResponse} from '@components/AttachmentModal'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; @@ -281,7 +282,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s }; /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */ if (fileDataName && Str.isImage(fileDataName)) { - RNImage.getSize(fileDataUri, (width, height) => { + ImageSize.getSize(fileDataUri).then(({width, height}) => { fileDataObject.width = width; fileDataObject.height = height; validateAndCompleteAttachmentSelection(fileDataObject); diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index eac41b6d627a..dcec2e14a9e7 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -1,8 +1,9 @@ import React, {useCallback, useEffect, useState} from 'react'; -import {ActivityIndicator, Image, View} from 'react-native'; +import {ActivityIndicator, View} from 'react-native'; import type {LayoutChangeEvent} from 'react-native'; import {Gesture, GestureHandlerRootView} from 'react-native-gesture-handler'; import type {GestureUpdateEvent, PanGestureChangeEventPayload, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; +import ImageSize from 'react-native-image-size'; import {interpolate, runOnUI, useSharedValue, useWorkletCallback} from 'react-native-reanimated'; import Button from '@components/Button'; import HeaderGap from '@components/HeaderGap'; @@ -118,7 +119,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose if (!imageUri) { return; } - Image.getSize(imageUri, (width, height) => { + ImageSize.getSize(imageUri).then(({width, height}) => { // We need to have image sizes in shared values to properly calculate position/size/animation originalImageHeight.value = height; originalImageWidth.value = width; diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index f6afb4dae2d6..396c10151fbf 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -141,7 +141,6 @@ function AvatarWithDisplayName({ )} diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 896330f5e77e..a4e6e2c87fec 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -8,7 +8,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; import type {ButtonWithDropdownMenuProps} from './types'; @@ -100,12 +99,12 @@ function ButtonWithDropdownMenu({ > - + diff --git a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx index 8f5487f9c68b..c88364b7e8f7 100644 --- a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx +++ b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx @@ -34,7 +34,7 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear const yearsList = searchText === '' ? years : years.filter((year) => year.text?.includes(searchText)); return { headerMessage: !yearsList.length ? translate('common.noResultsFound') : '', - sections: [{data: yearsList.sort((a, b) => b.value - a.value), indexOffset: 0}], + sections: [{data: yearsList.sort((a, b) => b.value - a.value)}], }; }, [years, searchText, translate]); diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 1c72aa37b73f..2cda2530d769 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -31,6 +31,7 @@ import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type {ReceiptSource} from '@src/types/onyx/Transaction'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; import ConfirmedRoute from './ConfirmedRoute'; @@ -67,7 +68,7 @@ type MoneyRequestConfirmationListOnyxProps = { }; type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & { /** Callback to inform parent modal of success */ - onConfirm?: (selectedParticipants: Participant[]) => void; + onConfirm?: (selectedParticipants: Array) => void; /** Callback to parent modal to send money */ onSendMoney?: (paymentMethod: IouType | PaymentMethodType | undefined) => void; @@ -109,10 +110,10 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & onToggleBillable?: (isOn: boolean) => void; /** Selected participants from MoneyRequestModal with login / accountID */ - selectedParticipants: Participant[]; + selectedParticipants: Array; /** Payee of the money request with login */ - payeePersonalDetails?: OnyxTypes.PersonalDetails; + payeePersonalDetails?: OnyxEntry; /** Can the participants be modified or not */ canModifyParticipants?: boolean; @@ -130,7 +131,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & reportID?: string; /** File path of the receipt */ - receiptPath?: string; + receiptPath?: ReceiptSource; /** File name of the receipt */ receiptFilename?: string; @@ -322,7 +323,7 @@ function MoneyRequestConfirmationList({ * Returns the participants with amount */ const getParticipantsWithAmount = useCallback( - (participantsList: Participant[]): Participant[] => { + (participantsList: Array): Array => { const calculatedIouAmount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( participantsList, @@ -358,7 +359,10 @@ function MoneyRequestConfirmationList({ ]; }, [isSplitBill, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]); - const selectedParticipants: Participant[] = useMemo(() => selectedParticipantsProp.filter((participant) => participant.selected), [selectedParticipantsProp]); + const selectedParticipants: Array = useMemo( + () => selectedParticipantsProp.filter((participant) => participant.selected), + [selectedParticipantsProp], + ); const payeePersonalDetails = useMemo(() => payeePersonalDetailsProp ?? currentUserPersonalDetails, [payeePersonalDetailsProp, currentUserPersonalDetails]); const canModifyParticipants = !isReadOnly && canModifyParticipantsProp && hasMultipleParticipants; const shouldDisablePaidBySection = canModifyParticipants; @@ -388,14 +392,12 @@ function MoneyRequestConfirmationList({ title: translate('moneyRequestConfirmationList.paidBy'), data: [formattedPayeeOption], shouldShow: true, - indexOffset: 0, isDisabled: shouldDisablePaidBySection, }, { title: translate('moneyRequestConfirmationList.splitWith'), data: formattedParticipantsList, shouldShow: true, - indexOffset: 1, }, ); } else { @@ -407,7 +409,6 @@ function MoneyRequestConfirmationList({ title: translate('common.to'), data: formattedSelectedParticipants, shouldShow: true, - indexOffset: 0, }); } return sections; @@ -424,7 +425,7 @@ function MoneyRequestConfirmationList({ canModifyParticipants, ]); - const selectedOptions: Array = useMemo(() => { + const selectedOptions: Array = useMemo(() => { if (!hasMultipleParticipants) { return []; } diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 1907bc132c6b..3fd76eea657b 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -436,14 +436,12 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ title: translate('moneyRequestConfirmationList.paidBy'), data: [formattedPayeeOption], shouldShow: true, - indexOffset: 0, isDisabled: shouldDisablePaidBySection, }, { title: translate('moneyRequestConfirmationList.splitWith'), data: formattedParticipantsList, shouldShow: true, - indexOffset: 1, }, ); } else { @@ -455,7 +453,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ title: translate('common.to'), data: formattedSelectedParticipants, shouldShow: true, - indexOffset: 0, }); } return sections; diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 3844080c6f5d..436f4c147931 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -9,11 +9,12 @@ import SectionList from '@components/SectionList'; import Text from '@components/Text'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import getSectionsWithIndexOffset from '@libs/getSectionsWithIndexOffset'; import type {OptionData} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {BaseOptionListProps, OptionsList, OptionsListData, Section} from './types'; +import type {BaseOptionListProps, OptionsList, OptionsListData, OptionsListDataWithIndexOffset, SectionWithIndexOffset} from './types'; function BaseOptionsList( { @@ -67,6 +68,7 @@ function BaseOptionsList( const listContainerStyles = useMemo(() => listContainerStylesProp ?? [styles.flex1], [listContainerStylesProp, styles.flex1]); const contentContainerStyles = useMemo(() => [safeAreaPaddingBottomStyle, contentContainerStylesProp], [contentContainerStylesProp, safeAreaPaddingBottomStyle]); + const sectionsWithIndexOffset = getSectionsWithIndexOffset(sections); /** * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. @@ -133,7 +135,8 @@ function BaseOptionsList( * * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] */ - const getItemLayout = (_data: OptionsListData[] | null, flatDataArrayIndex: number) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const getItemLayout = (_data: OptionsListDataWithIndexOffset[] | null, flatDataArrayIndex: number) => { if (!flattenedData.current[flatDataArrayIndex]) { flattenedData.current = buildFlatSectionArray(); } @@ -161,7 +164,7 @@ function BaseOptionsList( * @return {Component} */ - const renderItem: SectionListRenderItem = ({item, index, section}) => { + const renderItem: SectionListRenderItem = ({item, index, section}) => { const isItemDisabled = isDisabled || !!section.isDisabled || !!item.isDisabled; const isSelected = selectedOptions?.some((option) => { if (option.keyForList && option.keyForList === item.keyForList) { @@ -202,7 +205,7 @@ function BaseOptionsList( /** * Function which renders a section header component */ - const renderSectionHeader = ({section: {title, shouldShow}}: {section: OptionsListData}) => { + const renderSectionHeader = ({section: {title, shouldShow}}: {section: OptionsListDataWithIndexOffset}) => { if (!title && shouldShow && !hideSectionHeaders && sectionHeaderStyle) { return ; } @@ -235,7 +238,7 @@ function BaseOptionsList( {headerMessage} ) : null} - + ref={ref} style={listStyles} indicatorStyle="white" @@ -247,7 +250,7 @@ function BaseOptionsList( onScroll={onScroll} contentContainerStyle={contentContainerStyles} showsVerticalScrollIndicator={showScrollIndicator} - sections={sections} + sections={sectionsWithIndexOffset} keyExtractor={extractKey} stickySectionHeadersEnabled={false} renderItem={renderItem} diff --git a/src/components/OptionsList/types.ts b/src/components/OptionsList/types.ts index fa3ef8df56f6..b7180e6281b4 100644 --- a/src/components/OptionsList/types.ts +++ b/src/components/OptionsList/types.ts @@ -2,16 +2,14 @@ import type {RefObject} from 'react'; import type {SectionList, SectionListData, StyleProp, View, ViewStyle} from 'react-native'; import type {OptionData} from '@libs/ReportUtils'; -type OptionsList = SectionList; type OptionsListData = SectionListData; +type OptionsListDataWithIndexOffset = SectionListData; +type OptionsList = SectionList; type Section = { /** Title of the section */ title: string; - /** The initial index of this section given the total number of options in each section's data array */ - indexOffset: number; - /** Array of options */ data: OptionData[]; @@ -22,6 +20,11 @@ type Section = { isDisabled?: boolean; }; +type SectionWithIndexOffset = Section & { + /** The initial index of this section given the total number of options in each section's data array */ + indexOffset: number; +}; + type OptionsListProps = { /** option flexStyle for the options list container */ listContainerStyles?: StyleProp; @@ -134,4 +137,4 @@ type BaseOptionListProps = OptionsListProps & { listStyles?: StyleProp; }; -export type {OptionsListProps, BaseOptionListProps, Section, OptionsList, OptionsListData}; +export type {OptionsListProps, BaseOptionListProps, Section, OptionsList, OptionsListData, SectionWithIndexOffset, OptionsListDataWithIndexOffset}; diff --git a/src/components/OptionsSelector/optionsSelectorPropTypes.js b/src/components/OptionsSelector/optionsSelectorPropTypes.js index 8e58a7ffdb86..b430ce8a4933 100644 --- a/src/components/OptionsSelector/optionsSelectorPropTypes.js +++ b/src/components/OptionsSelector/optionsSelectorPropTypes.js @@ -14,9 +14,6 @@ const propTypes = { /** Title of the section */ title: PropTypes.string, - /** The initial index of this section given the total number of options in each section's data array */ - indexOffset: PropTypes.number, - /** Array of options */ data: PropTypes.arrayOf(optionPropTypes), diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index d36a2e93f5b3..3109453ca6b0 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -15,14 +15,11 @@ type ParentNavigationSubtitleProps = { /** parent Report ID */ parentReportID?: string; - /** parent Report Action ID */ - parentReportActionID?: string; - /** PressableWithoutFeedack additional styles */ pressableStyles?: StyleProp; }; -function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportActionID, parentReportID = '', pressableStyles}: ParentNavigationSubtitleProps) { +function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportID = '', pressableStyles}: ParentNavigationSubtitleProps) { const styles = useThemeStyles(); const {workspaceName, reportName} = parentNavigationSubtitleData; @@ -31,7 +28,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct return ( { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(parentReportID, parentReportActionID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(parentReportID)); }} accessibilityLabel={translate('threads.parentNavigationSummary', {reportName, workspaceName})} role={CONST.ROLE.LINK} diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 5c382ca8ee33..37d939e13868 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -70,6 +70,9 @@ type MoneyRequestViewPropsWithoutTransaction = MoneyRequestViewOnyxPropsWithoutT /** Whether we should display the horizontal rule below the component */ shouldShowHorizontalRule: boolean; + + /** Whether we should display the animated banner above the component */ + shouldShowAnimatedBackground: boolean; }; type MoneyRequestViewProps = MoneyRequestViewTransactionOnyxProps & MoneyRequestViewPropsWithoutTransaction; @@ -84,6 +87,7 @@ function MoneyRequestView({ policyTagList, policy, transactionViolations, + shouldShowAnimatedBackground, }: MoneyRequestViewProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -237,9 +241,9 @@ function MoneyRequestView({ ); return ( - - - + + {shouldShowAnimatedBackground && } + {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {(showMapAsImage || hasReceipt) && ( ( { @@ -74,7 +75,7 @@ function BaseSelectionList( ) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const listRef = useRef>>(null); + const listRef = useRef>>(null); const innerTextInputRef = useRef(null); const focusTimeoutRef = useRef(null); const shouldShowTextInput = !!textInputLabel; @@ -166,15 +167,17 @@ function BaseSelectionList( const [slicedSections, ShowMoreButtonInstance] = useMemo(() => { let remainingOptionsLimit = CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * currentPage; - const processedSections = sections.map((section) => { - const data = !isEmpty(section.data) && remainingOptionsLimit > 0 ? section.data.slice(0, remainingOptionsLimit) : []; - remainingOptionsLimit -= data.length; - - return { - ...section, - data, - }; - }); + const processedSections = getSectionsWithIndexOffset( + sections.map((section) => { + const data = !isEmpty(section.data) && remainingOptionsLimit > 0 ? section.data.slice(0, remainingOptionsLimit) : []; + remainingOptionsLimit -= data.length; + + return { + ...section, + data, + }; + }), + ); const shouldShowMoreButton = flattenedSections.allOptions.length > CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * currentPage; const showMoreButton = shouldShowMoreButton ? ( @@ -312,9 +315,8 @@ function BaseSelectionList( ); }; - const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => { - const indexOffset = section.indexOffset ? section.indexOffset : 0; - const normalizedIndex = index + indexOffset; + const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => { + const normalizedIndex = index + section.indexOffset; const isDisabled = !!section.isDisabled || item.isDisabled; const isItemFocused = !isDisabled && (focusedIndex === normalizedIndex || itemsToHighlight?.has(item.keyForList ?? '')); // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js index 45ed1a865d33..eed1966f8222 100644 --- a/src/components/SelectionList/selectionListPropTypes.js +++ b/src/components/SelectionList/selectionListPropTypes.js @@ -107,9 +107,6 @@ const propTypes = { /** Title of the section */ title: PropTypes.string, - /** The initial index of this section given the total number of options in each section's data array */ - indexOffset: PropTypes.number, - /** Array of options */ data: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape(userListItemPropTypes.item), PropTypes.shape(radioListItemPropTypes.item)])), diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 8b070e1aa5cb..88a864af2728 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -161,9 +161,6 @@ type Section = { /** Title of the section */ title?: string; - /** The initial index of this section given the total number of options in each section's data array */ - indexOffset?: number; - /** Array of options */ data?: TItem[]; @@ -174,6 +171,11 @@ type Section = { shouldShow?: boolean; }; +type SectionWithIndexOffset = Section & { + /** The initial index of this section given the total number of options in each section's data array */ + indexOffset: number; +}; + type BaseSelectionListProps = Partial & { /** Sections for the section list */ sections: Array>> | typeof CONST.EMPTY_ARRAY; @@ -324,12 +326,13 @@ type FlattenedSectionsReturn = { type ButtonOrCheckBoxRoles = 'button' | 'checkbox'; -type SectionListDataType = SectionListData>; +type SectionListDataType = SectionListData>; export type { BaseSelectionListProps, CommonListItemProps, Section, + SectionWithIndexOffset, BaseListItemProps, UserListItemProps, RadioListItemProps, diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index c09c7a25e375..11cd38056f0c 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -95,7 +95,7 @@ function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStat // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing textInputLabel={label || translate('common.state')} textInputValue={searchValue} - sections={[{data: searchResults, indexOffset: 0}]} + sections={[{data: searchResults}]} onSelectRow={onStateSelected} onChangeText={setSearchValue} initiallyFocusedOptionKey={currentState} diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx index 1b8b6bc1b22b..fc19e6a8062e 100644 --- a/src/components/TabSelector/TabSelector.tsx +++ b/src/components/TabSelector/TabSelector.tsx @@ -1,5 +1,4 @@ -import type {MaterialTopTabNavigationHelpers} from '@react-navigation/material-top-tabs/lib/typescript/src/types'; -import type {TabNavigationState} from '@react-navigation/native'; +import type {MaterialTopTabBarProps} from '@react-navigation/material-top-tabs/lib/typescript/src/types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {Animated} from 'react-native'; import {View} from 'react-native'; @@ -8,23 +7,13 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {RootStackParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; import TabSelectorItem from './TabSelectorItem'; -type TabSelectorProps = { - /* Navigation state provided by React Navigation */ - state: TabNavigationState; - - /* Navigation functions provided by React Navigation */ - navigation: MaterialTopTabNavigationHelpers; - +type TabSelectorProps = MaterialTopTabBarProps & { /* Callback fired when tab is pressed */ onTabPress?: (name: string) => void; - - /* AnimatedValue for the position of the screen while swiping */ - position: Animated.AnimatedInterpolation; }; type IconAndTitle = { @@ -143,3 +132,5 @@ function TabSelector({state, navigation, onTabPress = () => {}, position}: TabSe TabSelector.displayName = 'TabSelector'; export default TabSelector; + +export type {TabSelectorProps}; diff --git a/src/languages/en.ts b/src/languages/en.ts index 74d8deb98066..d30b62fc50b3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -596,6 +596,9 @@ export default { sendMoney: 'Send Money', assignTask: 'Assign Task', shortcut: 'Shortcut', + trackManual: 'Track Manual', + trackScan: 'Track Scan', + trackDistance: 'Track Distance', }, iou: { amount: 'Amount', diff --git a/src/languages/es.ts b/src/languages/es.ts index da4a17e76fdc..30e4717bdf57 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -592,6 +592,9 @@ export default { sendMoney: 'Enviar Dinero', assignTask: 'Assignar Tarea', shortcut: 'Acceso Directo', + trackManual: 'Seguimiento de Gastos', + trackScan: 'Seguimiento de Recibo', + trackDistance: 'Seguimiento de Distancia', }, iou: { amount: 'Importe', diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts new file mode 100644 index 000000000000..a7fbc5f3bd4e --- /dev/null +++ b/src/libs/BankAccountUtils.ts @@ -0,0 +1,9 @@ +import Str from 'expensify-common/lib/str'; +import type {OnyxEntry} from 'react-native-onyx'; +import type * as OnyxTypes from '@src/types/onyx'; + +function getDefaultCompanyWebsite(session: OnyxEntry, user: OnyxEntry): string { + return user?.isFromPublicDomain ? 'https://' : `https://www.${Str.extractEmailDomain(session?.email ?? '')}`; +} +// eslint-disable-next-line import/prefer-default-export +export {getDefaultCompanyWebsite}; diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 3b3e24867a4f..8f1cb89d695b 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -193,9 +193,10 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr const splittedTag = TransactionUtils.getTagArrayFromName(transactionTag); const splittedOldTag = TransactionUtils.getTagArrayFromName(oldTransactionTag); const localizedTagListName = Localize.translateLocal('common.tag'); + const sortedTagKeys = PolicyUtils.getSortedTagKeys(policyTags); - Object.keys(policyTags).forEach((policyTagKey, index) => { - const policyTagListName = PolicyUtils.getTagListName(policyTags, index) || localizedTagListName; + sortedTagKeys.forEach((policyTagKey, index) => { + const policyTagListName = policyTags[policyTagKey].name || localizedTagListName; const newTag = splittedTag[index] ?? ''; const oldTag = splittedOldTag[index] ?? ''; diff --git a/src/libs/MoneyRequestUtils.ts b/src/libs/MoneyRequestUtils.ts index d3969558dab9..1a573ce74628 100644 --- a/src/libs/MoneyRequestUtils.ts +++ b/src/libs/MoneyRequestUtils.ts @@ -1,5 +1,7 @@ +import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; +import type {SelectedTabRequest} from '@src/types/onyx'; /** * Strip comma from the amount @@ -78,14 +80,14 @@ function replaceAllDigits(text: string, convertFn: (char: string) => string): st /** * Check if distance request or not */ -function isDistanceRequest(iouType: ValueOf, selectedTab: ValueOf): boolean { +function isDistanceRequest(iouType: ValueOf, selectedTab: OnyxEntry): boolean { return iouType === CONST.IOU.TYPE.REQUEST && selectedTab === CONST.TAB_REQUEST.DISTANCE; } /** * Check if scan request or not */ -function isScanRequest(selectedTab: ValueOf): boolean { +function isScanRequest(selectedTab: SelectedTabRequest): boolean { return selectedTab === CONST.TAB_REQUEST.SCAN; } diff --git a/src/libs/Navigation/OnyxTabNavigator.tsx b/src/libs/Navigation/OnyxTabNavigator.tsx index 2ae3414956a8..deab975e067b 100644 --- a/src/libs/Navigation/OnyxTabNavigator.tsx +++ b/src/libs/Navigation/OnyxTabNavigator.tsx @@ -4,13 +4,15 @@ import type {EventMapCore, NavigationState, ScreenListeners} from '@react-naviga import React from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; +import type {TabSelectorProps} from '@components/TabSelector/TabSelector'; import Tab from '@userActions/Tab'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {SelectedTabRequest} from '@src/types/onyx'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {defaultScreenOptions} from './OnyxTabNavigatorConfig'; type OnyxTabNavigatorOnyxProps = { - selectedTab: OnyxEntry; + selectedTab: OnyxEntry; }; type OnyxTabNavigatorProps = OnyxTabNavigatorOnyxProps & @@ -19,11 +21,13 @@ type OnyxTabNavigatorProps = OnyxTabNavigatorOnyxProps & id: string; /** Name of the selected tab */ - selectedTab?: string; + selectedTab?: SelectedTabRequest; /** A function triggered when a tab has been selected */ onTabSelected?: (newIouType: string) => void; + tabBar: (props: TabSelectorProps) => React.ReactNode; + screenListeners?: ScreenListeners; }; @@ -32,7 +36,7 @@ export const TopTab = createMaterialTopTabNavigator(); // This takes all the same props as MaterialTopTabsNavigator: https://reactnavigation.org/docs/material-top-tab-navigator/#props, // except ID is now required, and it gets a `selectedTab` from Onyx -function OnyxTabNavigator({id, selectedTab = '', children, onTabSelected = () => {}, screenListeners, ...rest}: OnyxTabNavigatorProps) { +function OnyxTabNavigator({id, selectedTab, children, onTabSelected = () => {}, screenListeners, ...rest}: OnyxTabNavigatorProps) { return ( const state = event.data.state; const index = state.index; const routeNames = state.routeNames; - Tab.setSelectedTab(id, routeNames[index]); + Tab.setSelectedTab(id, routeNames[index] as SelectedTabRequest); onTabSelected(routeNames[index]); }, ...(screenListeners ?? {}), diff --git a/src/libs/Navigation/linkTo.ts b/src/libs/Navigation/linkTo.ts index 84e0dd0d5d14..afa8fb56069a 100644 --- a/src/libs/Navigation/linkTo.ts +++ b/src/libs/Navigation/linkTo.ts @@ -134,6 +134,8 @@ export default function linkTo(navigation: NavigationContainerRef; + reportID: string; + currency: string; + }; [SCREENS.MONEY_REQUEST.PARTICIPANTS]: { iouType: string; reportID: string; @@ -463,6 +467,7 @@ type EnablePaymentsNavigatorParamList = { type SplitDetailsNavigatorParamList = { [SCREENS.SPLIT_DETAILS.ROOT]: { + reportID: string; reportActionID: string; }; [SCREENS.SPLIT_DETAILS.EDIT_REQUEST]: { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index cf988eb8aef5..7e4082bff481 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -83,7 +83,6 @@ type PayeePersonalDetails = { type CategorySectionBase = { title: string | undefined; shouldShow: boolean; - indexOffset: number; }; type CategorySection = CategorySectionBase & { @@ -151,7 +150,6 @@ type MemberForList = { type SectionForSearchTerm = { section: CategorySection; - newIndexOffset: number; }; type GetOptions = { recentReports: ReportUtils.OptionData[]; @@ -973,14 +971,11 @@ function getCategoryListSections( const categorySections: CategoryTreeSection[] = []; const numberOfEnabledCategories = enabledCategories.length; - let indexOffset = 0; - if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { categorySections.push({ // "Selected" section title: '', shouldShow: false, - indexOffset, data: getCategoryOptionTree(selectedOptions, true), }); @@ -1004,7 +999,6 @@ function getCategoryListSections( // "Search" section title: '', shouldShow: true, - indexOffset, data: getCategoryOptionTree(searchCategories, true), }); @@ -1016,11 +1010,8 @@ function getCategoryListSections( // "Selected" section title: '', shouldShow: false, - indexOffset, data: getCategoryOptionTree(selectedOptions, true), }); - - indexOffset += selectedOptions.length; } const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); @@ -1031,7 +1022,6 @@ function getCategoryListSections( // "All" section when items amount less than the threshold title: '', shouldShow: false, - indexOffset, data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames), }); @@ -1052,18 +1042,14 @@ function getCategoryListSections( // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - indexOffset, data: getCategoryOptionTree(cutRecentlyUsedCategories, true), }); - - indexOffset += filteredRecentlyUsedCategories.length; } categorySections.push({ // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - indexOffset, data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames), }); @@ -1104,7 +1090,6 @@ function getTagListSections( const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); const enabledTags = [...selectedOptions, ...sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name))]; const numberOfTags = enabledTags.length; - let indexOffset = 0; // If all tags are disabled but there's a previously selected tag, show only the selected tag if (numberOfTags === 0 && selectedOptions.length > 0) { @@ -1117,7 +1102,6 @@ function getTagListSections( // "Selected" section title: '', shouldShow: false, - indexOffset, data: getTagsOptions(selectedTagOptions), }); @@ -1131,7 +1115,6 @@ function getTagListSections( // "Search" section title: '', shouldShow: true, - indexOffset, data: getTagsOptions(searchTags), }); @@ -1143,7 +1126,6 @@ function getTagListSections( // "All" section when items amount less than the threshold title: '', shouldShow: false, - indexOffset, data: getTagsOptions(enabledTags), }); @@ -1169,11 +1151,8 @@ function getTagListSections( // "Selected" section title: '', shouldShow: true, - indexOffset, data: getTagsOptions(selectedTagOptions), }); - - indexOffset += selectedOptions.length; } if (filteredRecentlyUsedTags.length > 0) { @@ -1183,18 +1162,14 @@ function getTagListSections( // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - indexOffset, data: getTagsOptions(cutRecentlyUsedTags), }); - - indexOffset += filteredRecentlyUsedTags.length; } tagSections.push({ // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - indexOffset, data: getTagsOptions(filteredTags), }); @@ -1257,8 +1232,6 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled); const numberOfTaxRates = enabledTaxRates.length; - let indexOffset = 0; - // If all tax are disabled but there's a previously selected tag, show only the selected tag if (numberOfTaxRates === 0 && selectedOptions.length > 0) { const selectedTaxRateOptions = selectedOptions.map((option) => ({ @@ -1270,7 +1243,6 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "Selected" sectiong title: '', shouldShow: false, - indexOffset, data: getTaxRatesOptions(selectedTaxRateOptions), }); @@ -1284,7 +1256,6 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "Search" section title: '', shouldShow: true, - indexOffset, data: getTaxRatesOptions(searchTaxRates), }); @@ -1296,7 +1267,6 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "All" section when items amount less than the threshold title: '', shouldShow: false, - indexOffset, data: getTaxRatesOptions(enabledTaxRates), }); @@ -1320,18 +1290,14 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "Selected" section title: '', shouldShow: true, - indexOffset, data: getTaxRatesOptions(selectedTaxRatesOptions), }); - - indexOffset += selectedOptions.length; } policyRatesSections.push({ // "All" section when number of items are more than the threshold title: '', shouldShow: true, - indexOffset, data: getTaxRatesOptions(filteredTaxRates), }); @@ -1814,7 +1780,7 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: Person /** * Build the IOUConfirmationOptions for showing participants */ -function getIOUConfirmationOptionsFromParticipants(participants: Participant[], amountText: string): Participant[] { +function getIOUConfirmationOptionsFromParticipants(participants: Array, amountText: string): Array { return participants.map((participant) => ({ ...participant, descriptiveText: amountText, @@ -2009,7 +1975,6 @@ function formatSectionsFromSearchTerm( filteredRecentReports: ReportUtils.OptionData[], filteredPersonalDetails: ReportUtils.OptionData[], maxOptionsSelected: boolean, - indexOffset = 0, personalDetails: OnyxEntry = {}, shouldGetOptionDetails = false, ): SectionForSearchTerm { @@ -2027,9 +1992,7 @@ function formatSectionsFromSearchTerm( }) : selectedOptions, shouldShow: selectedOptions.length > 0, - indexOffset, }, - newIndexOffset: indexOffset + selectedOptions.length, }; } @@ -2053,9 +2016,7 @@ function formatSectionsFromSearchTerm( }) : selectedParticipantsWithoutDetails, shouldShow: selectedParticipantsWithoutDetails.length > 0, - indexOffset, }, - newIndexOffset: indexOffset + selectedParticipantsWithoutDetails.length, }; } diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 43bda05c9e21..6f871bea7ab8 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -170,6 +170,14 @@ function getIneligibleInvitees(policyMembers: OnyxEntry, personal return memberEmailsToExclude; } +function getSortedTagKeys(policyTagList: OnyxEntry): Array { + if (isEmptyObject(policyTagList)) { + return []; + } + + return Object.keys(policyTagList).sort((key1, key2) => policyTagList[key1].orderWeight - policyTagList[key2].orderWeight); +} + /** * Gets a tag name of policy tags based on a tag index. */ @@ -178,7 +186,7 @@ function getTagListName(policyTagList: OnyxEntry, tagIndex: numbe return ''; } - const policyTagKeys = Object.keys(policyTagList ?? {}); + const policyTagKeys = getSortedTagKeys(policyTagList ?? {}); const policyTagKey = policyTagKeys[tagIndex] ?? ''; return policyTagList?.[policyTagKey]?.name ?? ''; @@ -316,6 +324,7 @@ export { getIneligibleInvitees, getTagLists, getTagListName, + getSortedTagKeys, canEditTaxRate, getTagList, getCleanedTagName, diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index cf8937874216..849bc50e77b0 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -4,7 +4,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; -import type {ReceiptError} from '@src/types/onyx/Transaction'; +import type {ReceiptError, ReceiptSource} from '@src/types/onyx/Transaction'; import * as FileUtils from './fileDownload/FileUtils'; import * as TransactionUtils from './TransactionUtils'; @@ -25,7 +25,7 @@ type ThumbnailAndImageURI = { * @param receiptPath * @param receiptFileName */ -function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { +function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPath: ReceiptSource | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { if (TransactionUtils.isFetchingWaypointsFromServer(transaction)) { return {isThumbnail: true, isLocalFile: true}; } diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 43145dc3c7a1..c3b377783b13 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -213,6 +213,35 @@ function isTransactionThread(parentReportAction: OnyxEntry | Empty ); } +/** + * Returns the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions with a childReportID. Returns a reportID if there is exactly one transaction thread for the report, and null otherwise. + */ +function getOneTransactionThreadReportID(reportActions: OnyxEntry | ReportAction[]): string | null { + const reportActionsArray = Object.values(reportActions ?? {}); + + if (!reportActionsArray.length) { + return null; + } + + // Get all IOU report actions for the report. + const iouRequestTypes: Array> = [CONST.IOU.REPORT_ACTION_TYPE.CREATE, CONST.IOU.REPORT_ACTION_TYPE.SPLIT, CONST.IOU.REPORT_ACTION_TYPE.PAY]; + const iouRequestActions = reportActionsArray.filter( + (action) => + action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && + (iouRequestTypes.includes(action.originalMessage.type) ?? []) && + action.childReportID && + action.originalMessage.IOUTransactionID, + ); + + // If we don't have any IOU request actions, or we have more than one IOU request actions, this isn't a oneTransaction report + if (!iouRequestActions.length || iouRequestActions.length > 1) { + return null; + } + + // Ensure we have a childReportID associated with the IOU report action + return iouRequestActions[0].childReportID ?? null; +} + /** * Sort an array of reportActions by their created timestamp first, and reportActionID second * This gives us a stable order even in the case of multiple reportActions created on the same millisecond @@ -1030,6 +1059,7 @@ function getReportActionMessageText(reportAction: OnyxEntry | Empt export { extractLinksFromMessageHtml, + getOneTransactionThreadReportID, getAllReportActions, getIOUReportIDFromReportActionPreview, getLastClosedReportAction, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 07c860ce1e47..9fa28535a7a7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1264,6 +1264,23 @@ function isMoneyRequestReport(reportOrID: OnyxEntry | EmptyObject | stri return isIOUReport(report) || isExpenseReport(report); } +/** + * Checks if a report has only one transaction associated with it + */ +function isOneTransactionReport(reportID: string): boolean { + const reportActions = reportActionsByReport?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]); + return ReportActionsUtils.getOneTransactionThreadReportID(reportActions) !== null; +} + +/** + * Checks if a report is a transaction thread associated with a report that has only one transaction + */ +function isOneTransactionThread(reportID: string, parentReportID: string): boolean { + const parentReportActions = reportActionsByReport?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? ([] as ReportAction[]); + const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(parentReportActions); + return reportID === transactionThreadReportID; +} + /** * Should return true only for personal 1:1 report * @@ -1794,6 +1811,11 @@ function getIcons( }; const isManager = currentUserAccountID === report?.managerID; + // For one transaction IOUs, display a simplified report icon + if (isOneTransactionReport(report?.reportID ?? '0')) { + return [ownerIcon]; + } + return isManager ? [managerIcon, ownerIcon] : [ownerIcon, managerIcon]; } @@ -2682,7 +2704,7 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.payerSpentAmount', {payer: getDisplayNameForParticipant(report.ownerAccountID) ?? '', amount: formattedAmount}); } - return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount}); + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); } /** @@ -4397,6 +4419,11 @@ function shouldReportBeInOptionList({ return false; } + // If this is a transaction thread associated with a report that only has one transaction, omit it + if (isOneTransactionThread(report.reportID, report.parentReportID ?? '0')) { + return false; + } + // Include the currently viewed report. If we excluded the currently viewed report, then there // would be no way to highlight it in the options list and it would be confusing to users because they lose // a sense of context. @@ -4867,6 +4894,10 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean { return true; } + if (isExpenseReport(report) && isOneTransactionReport(report?.reportID ?? '')) { + return true; + } + if (isWorkspaceTaskReport(report)) { return true; } @@ -5778,6 +5809,7 @@ export { hasSingleParticipant, getReportRecipientAccountIDs, isOneOnOneChat, + isOneTransactionThread, isPayer, goBackToDetailsPage, getTransactionReportName, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index bc94c8fee8fc..907edc208570 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -133,7 +133,7 @@ function hasEReceipt(transaction: Transaction | undefined | null): boolean { return !!transaction?.hasEReceipt; } -function hasReceipt(transaction: Transaction | undefined | null): boolean { +function hasReceipt(transaction: OnyxEntry | undefined): boolean { return !!transaction?.receipt?.state || hasEReceipt(transaction); } diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index fe2e5af537a7..9296a81e3065 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -2,6 +2,7 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; import type {OnyxUpdate} from 'react-native-onyx'; import type {Phrase, PhraseParameters} from '@libs/Localize'; +import {getSortedTagKeys} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -55,7 +56,7 @@ function getTagViolationsForMultiLevelTags( policyRequiresTags: boolean, policyTagList: PolicyTagList, ): TransactionViolation[] { - const policyTagKeys = Object.keys(policyTagList); + const policyTagKeys = getSortedTagKeys(policyTagList); const selectedTags = updatedTransaction.tag?.split(CONST.COLON) ?? []; let newTransactionViolations = [...transactionViolations]; newTransactionViolations = newTransactionViolations.filter( diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 33cd660b65f9..8582e9e3f3de 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3064,9 +3064,9 @@ function startSplitBill( * @param sessionAccountID - accountID of the current user * @param sessionEmail - email of the current user */ -function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportAction, updatedTransaction: OnyxTypes.Transaction, sessionAccountID: number, sessionEmail: string) { +function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportAction, updatedTransaction: OnyxEntry, sessionAccountID: number, sessionEmail: string) { const currentUserEmailForIOUSplit = PhoneNumber.addSMSDomainIfPhoneNumber(sessionEmail); - const {transactionID} = updatedTransaction; + const transactionID = updatedTransaction?.transactionID ?? ''; const unmodifiedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; // Save optimistic updated transaction and action @@ -3127,8 +3127,9 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA }, ]; - const splitParticipants: Split[] = updatedTransaction.comment.splits ?? []; - const {modifiedAmount: amount, modifiedCurrency: currency} = updatedTransaction; + const splitParticipants: Split[] = updatedTransaction?.comment.splits ?? []; + const amount = updatedTransaction?.modifiedAmount; + const currency = updatedTransaction?.modifiedCurrency; // Exclude the current user when calculating the split amount, `calculateAmount` takes it into account const splitAmount = IOUUtils.calculateAmount(splitParticipants.length - 1, amount ?? 0, currency ?? '', false); @@ -3185,17 +3186,17 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA isPolicyExpenseChat ? -splitAmount : splitAmount, currency ?? '', oneOnOneIOUReport?.reportID ?? '', - updatedTransaction.comment.comment, - updatedTransaction.modifiedCreated, + updatedTransaction?.comment.comment, + updatedTransaction?.modifiedCreated, CONST.IOU.TYPE.SPLIT, transactionID, - updatedTransaction.modifiedMerchant, - {...updatedTransaction.receipt, state: CONST.IOU.RECEIPT_STATE.OPEN}, - updatedTransaction.filename, + updatedTransaction?.modifiedMerchant, + {...updatedTransaction?.receipt, state: CONST.IOU.RECEIPT_STATE.OPEN}, + updatedTransaction?.filename, undefined, - updatedTransaction.category, - updatedTransaction.tag, - updatedTransaction.billable, + updatedTransaction?.category, + updatedTransaction?.tag, + updatedTransaction?.billable, ); const [oneOnOneCreatedActionForChat, oneOnOneCreatedActionForIOU, oneOnOneIOUAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = @@ -3204,7 +3205,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA CONST.IOU.REPORT_ACTION_TYPE.CREATE, splitAmount, currency ?? '', - updatedTransaction.comment.comment ?? '', + updatedTransaction?.comment.comment ?? '', currentUserEmailForIOUSplit, [participant], oneOnOneTransaction.transactionID, @@ -5050,7 +5051,7 @@ function setShownHoldUseExplanation() { } /** Navigates to the next IOU page based on where the IOU request was started */ -function navigateToNextPage(iou: OnyxEntry, iouType: string, report?: OnyxTypes.Report, path = '') { +function navigateToNextPage(iou: OnyxEntry, iouType: string, report?: OnyxEntry, path = '') { const moneyRequestID = `${iouType}${report?.reportID ?? ''}`; const shouldReset = iou?.id !== moneyRequestID && !!report?.reportID; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 44258a67618b..0c6bd7682add 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -1076,6 +1076,8 @@ function requestWorkspaceOwnerChange(policyID: string) { isLoading: false, isChangeOwnerSuccessful: true, isChangeOwnerFailed: false, + owner: sessionEmail, + ownerAccountID: sessionAccountID, }, }, ]; @@ -1145,6 +1147,8 @@ function addBillingCardAndRequestPolicyOwnerChange( isLoading: false, isChangeOwnerSuccessful: true, isChangeOwnerFailed: false, + owner: sessionEmail, + ownerAccountID: sessionAccountID, }, }, ]; @@ -2412,7 +2416,7 @@ function buildOptimisticPolicyRecentlyUsedTags(policyID?: string, transactionTag } const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; - const policyTagKeys = Object.keys(policyTags); + const policyTagKeys = PolicyUtils.getSortedTagKeys(policyTags); const policyRecentlyUsedTags = allRecentlyUsedTags?.[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`] ?? {}; const newOptimisticPolicyRecentlyUsedTags: RecentlyUsedTags = {}; @@ -2422,7 +2426,7 @@ function buildOptimisticPolicyRecentlyUsedTags(policyID?: string, transactionTag } const tagListKey = policyTagKeys[index]; - newOptimisticPolicyRecentlyUsedTags[tagListKey] = [...new Set([...tag, ...(policyRecentlyUsedTags[tagListKey] ?? [])])]; + newOptimisticPolicyRecentlyUsedTags[tagListKey] = [...new Set([tag, ...(policyRecentlyUsedTags[tagListKey] ?? [])])]; }); return newOptimisticPolicyRecentlyUsedTags; diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts index 3d529ce54cd6..4663fbb5bcc3 100644 --- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts +++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts @@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import {WRITE_COMMANDS} from '@libs/API/types'; +import {getDefaultCompanyWebsite} from '@libs/BankAccountUtils'; import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; import CONST from '@src/CONST'; @@ -12,7 +13,7 @@ import type * as OnyxTypes from '@src/types/onyx'; /** * Reset user's reimbursement account. This will delete the bank account. */ -function resetFreePlanBankAccount(bankAccountID: number, session: OnyxEntry, policyID: string) { +function resetFreePlanBankAccount(bankAccountID: number, session: OnyxEntry, policyID: string, user: OnyxEntry) { if (!bankAccountID) { throw new Error('Missing bankAccountID when attempting to reset free plan bank account'); } @@ -84,7 +85,7 @@ function resetFreePlanBankAccount(bankAccountID: number, session: OnyxEntry(sections: Array>): Array> { + return sections.map((section, index) => { + const indexOffset = [...sections].splice(0, index).reduce((acc, curr) => acc + (curr.data?.length ?? 0), 0); + return {...section, indexOffset}; + }); +} diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index 3bd227c5dcf1..e887860ae155 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -139,6 +139,8 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, textInputLabel={translate('common.search')} boldStyle sections={sections} + // Focus the first option when searching + focusedIndex={0} value={searchValue} onSelectRow={(option: Record) => onSubmit({ diff --git a/src/pages/NewChatConfirmPage.tsx b/src/pages/NewChatConfirmPage.tsx index 8570c061ebce..e6214b160a99 100644 --- a/src/pages/NewChatConfirmPage.tsx +++ b/src/pages/NewChatConfirmPage.tsx @@ -129,7 +129,7 @@ function NewChatConfirmPage({newGroupDraft, allPersonalDetails}: NewChatConfirmP description={translate('groupConfirmPage.groupName')} /> 1} diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 9c2d47f684ab..fda626c77758 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -48,7 +48,7 @@ type NewChatPageWithOnyxProps = { }; type NewChatPageProps = NewChatPageWithOnyxProps & { - isGroupChat: boolean; + isGroupChat?: boolean; }; const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); @@ -82,13 +82,10 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF const sections = useMemo((): OptionsListUtils.CategorySection[] => { const sectionsList: OptionsListUtils.CategorySection[] = []; - let indexOffset = 0; - const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecentReports, filteredPersonalDetails, maxParticipantsReached, indexOffset); + const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecentReports, filteredPersonalDetails, maxParticipantsReached); sectionsList.push(formatResults.section); - indexOffset = formatResults.newIndexOffset; - if (maxParticipantsReached) { return sectionsList; } @@ -97,24 +94,19 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF title: translate('common.recents'), data: filteredRecentReports, shouldShow: filteredRecentReports.length > 0, - indexOffset, }); - indexOffset += filteredRecentReports.length; sectionsList.push({ title: translate('common.contacts'), data: filteredPersonalDetails, shouldShow: filteredPersonalDetails.length > 0, - indexOffset, }); - indexOffset += filteredPersonalDetails.length; if (filteredUserToInvite) { sectionsList.push({ title: undefined, data: [filteredUserToInvite], shouldShow: true, - indexOffset, }); } diff --git a/src/pages/NewChatSelectorPage.js b/src/pages/NewChatSelectorPage.tsx similarity index 64% rename from src/pages/NewChatSelectorPage.js rename to src/pages/NewChatSelectorPage.tsx index 5033c74cdad3..6919dce33474 100755 --- a/src/pages/NewChatSelectorPage.js +++ b/src/pages/NewChatSelectorPage.tsx @@ -1,31 +1,16 @@ import {useNavigation} from '@react-navigation/native'; import React from 'react'; -import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import TabSelector from '@components/TabSelector/TabSelector'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import compose from '@libs/compose'; +import useLocalize from '@hooks/useLocalize'; import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import NewChatPage from './NewChatPage'; import WorkspaceNewRoomPage from './workspace/WorkspaceNewRoomPage'; -const propTypes = { - ...windowDimensionsPropTypes, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - betas: [], - personalDetails: {}, - reports: {}, -}; - -function NewChatSelectorPage(props) { +function NewChatSelectorPage() { + const {translate} = useLocalize(); const navigation = useNavigation(); return ( @@ -37,7 +22,7 @@ function NewChatSelectorPage(props) { testID={NewChatSelectorPage.displayName} > (user?.isFromPublicDomain ? 'https://' : `https://www.${Str.extractEmailDomain(session?.email ?? '')}`), - [session?.email, user?.isFromPublicDomain], - ); + const defaultWebsiteExample = useMemo(() => getDefaultCompanyWebsite(session, user), [session, user]); const defaultCompanyWebsite = reimbursementAccount?.achData?.website ?? defaultWebsiteExample; const handleSubmit = useReimbursementAccountStepFormSubmit({ diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 80563fcf7b1b..f06b40af8851 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -252,7 +252,6 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD )} diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index a2b2f094ac26..66ff4e0602ba 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -110,7 +110,6 @@ function ReportParticipantsPage({report, personalDetails}: ReportParticipantsPag title: '', data: participants, shouldShow: true, - indexOffset: 0, }, ]} onSelectRow={(option: OptionData) => { diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index d0ea55a923e7..bb3e928cb47f 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -102,7 +102,6 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa const sections = useMemo(() => { const sectionsArr: Sections = []; - let indexOffset = 0; if (!didScreenTransitionEnd) { return []; @@ -125,9 +124,7 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa sectionsArr.push({ title: undefined, data: filterSelectedOptionsFormatted, - indexOffset, }); - indexOffset += filterSelectedOptions.length; // Filtering out selected users from the search results const selectedLogins = selectedOptions.map(({login}) => login); @@ -138,15 +135,12 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa sectionsArr.push({ title: translate('common.contacts'), data: personalDetailsFormatted, - indexOffset, }); - indexOffset += personalDetailsFormatted.length; if (hasUnselectedUserToInvite) { sectionsArr.push({ title: undefined, data: [OptionsListUtils.formatMemberForList(userToInvite)], - indexOffset, }); } diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index 6cdcc6e06b95..b64404f88138 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -285,7 +285,7 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { { const newSections: SearchPageSectionList = []; - let indexOffset = 0; if (recentReports?.length > 0) { newSections.push({ data: recentReports.map((report) => ({...report, isBold: report.isUnread})), shouldShow: true, - indexOffset, }); - indexOffset += recentReports.length; } if (localPersonalDetails.length > 0) { newSections.push({ data: localPersonalDetails, shouldShow: true, - indexOffset, }); - indexOffset += recentReports.length; } if (userToInvite) { newSections.push({ data: [userToInvite], shouldShow: true, - indexOffset, }); } diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index 2eb5ecaf373f..6f077f764474 100644 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ b/src/pages/WorkspaceSwitcherPage.tsx @@ -156,7 +156,6 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { () => ({ data: filteredAndSortedUserWorkspaces, shouldShow: true, - indexOffset: 0, }), [filteredAndSortedUserWorkspaces], ); diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index a6ddbc7bfb95..8aa2b855176c 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -272,7 +272,6 @@ function HeaderView({report, personalDetails, parentReport, parentReportAction, )} diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index ddff1f15c38f..f1f8eb8047c7 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -635,6 +635,7 @@ function ReportScreen({ isLoadingNewerReportActions={reportMetadata?.isLoadingNewerReportActions} isLoadingOlderReportActions={reportMetadata?.isLoadingOlderReportActions} isReadyForCommentLinking={!shouldShowSkeleton} + transactionThreadReportID={ReportActionsUtils.getOneTransactionThreadReportID(reportActions ?? [])} /> )} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 734a94d50d38..ab3ed32f5ee8 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -296,6 +296,9 @@ function ComposerWithSuggestions( const isAutoSuggestionPickerLarge = !isSmallScreenWidth || (isSmallScreenWidth && hasEnoughSpaceForLargeSuggestion); + // The ref to check whether the comment saving is in progress + const isCommentPendingSaved = useRef(false); + /** * Update frequently used emojis list. We debounce this method in the constructor so that UpdateFrequentlyUsedEmojis * API is not called too often. @@ -331,6 +334,7 @@ function ComposerWithSuggestions( () => lodashDebounce((selectedReportID, newComment) => { Report.saveReportComment(selectedReportID, newComment || ''); + isCommentPendingSaved.current = false; }, 1000), [], ); @@ -441,6 +445,7 @@ function ComposerWithSuggestions( commentRef.current = newCommentConverted; if (shouldDebounceSaveComment) { + isCommentPendingSaved.current = true; debouncedSaveReportComment(reportID, newCommentConverted); } else { Report.saveReportComment(reportID, newCommentConverted || ''); @@ -489,6 +494,7 @@ function ComposerWithSuggestions( // We don't really care about saving the draft the user was typing // We need to make sure an empty draft gets saved instead debouncedSaveReportComment.cancel(); + isCommentPendingSaved.current = false; updateComment(''); setTextInputShouldClear(true); @@ -794,6 +800,7 @@ function ComposerWithSuggestions( value={value} updateComment={updateComment} commentRef={commentRef} + isCommentPendingSaved={isCommentPendingSaved} /> )} diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx index f3780528cabe..c95e6bde67ee 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx @@ -10,7 +10,7 @@ import type {SilentCommentUpdaterOnyxProps, SilentCommentUpdaterProps} from './t * It is connected to the actual draft comment in onyx. The comment in onyx might updates multiple times, and we want to avoid * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. */ -function SilentCommentUpdater({comment, commentRef, reportID, value, updateComment}: SilentCommentUpdaterProps) { +function SilentCommentUpdater({comment, commentRef, reportID, value, updateComment, isCommentPendingSaved}: SilentCommentUpdaterProps) { const prevCommentProp = usePrevious(comment); const prevReportId = usePrevious(reportID); const {preferredLocale} = useLocalize(); @@ -34,7 +34,7 @@ function SilentCommentUpdater({comment, commentRef, reportID, value, updateComme useEffect(() => { // Value state does not have the same value as comment props when the comment gets changed from another tab. // In this case, we should synchronize the value between tabs. - const shouldSyncComment = prevCommentProp !== comment && value !== comment; + const shouldSyncComment = prevCommentProp !== comment && value !== comment && !isCommentPendingSaved.current; // As the report IDs change, make sure to update the composer comment as we need to make sure // we do not show incorrect data in there (ie. draft of message from other report). @@ -43,7 +43,7 @@ function SilentCommentUpdater({comment, commentRef, reportID, value, updateComme } updateComment(comment ?? ''); - }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, reportID, updateComment, value, commentRef]); + }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, reportID, updateComment, value, commentRef, isCommentPendingSaved]); return null; } diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts index dbc23b0279c3..6f9e8b8a6d42 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts @@ -17,6 +17,9 @@ type SilentCommentUpdaterProps = SilentCommentUpdaterOnyxProps & { /** The ref of the comment */ commentRef: React.RefObject; + + /** The ref to check whether the comment saving is in progress */ + isCommentPendingSaved: React.RefObject; }; export type {SilentCommentUpdaterProps, SilentCommentUpdaterOnyxProps}; diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index f441f8e0ea3f..2716fedcf59a 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -49,6 +49,7 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import SelectionScraper from '@libs/SelectionScraper'; +import * as TransactionUtils from '@libs/TransactionUtils'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; import * as BankAccounts from '@userActions/BankAccounts'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; @@ -100,12 +101,22 @@ type ReportActionItemOnyxProps = { /** The policy which the user has access to and which the report is tied to */ policy: OnyxEntry; + + /** Transaction associated with this report, if any */ + transaction: OnyxEntry; }; type ReportActionItemProps = { /** Report for this action */ report: OnyxTypes.Report; + /** The transaction thread report associated with the report for this action, if any */ + transactionThreadReport: OnyxEntry; + + /** Array of report actions for the report for this action */ + // eslint-disable-next-line react/no-unused-prop-types + reportActions: OnyxTypes.ReportAction[]; + /** Report action belonging to the report's parent */ parentReportAction: OnyxEntry; @@ -142,6 +153,7 @@ const isIOUReport = (actionObj: OnyxEntry): actionObj is function ReportActionItem({ action, report, + transactionThreadReport, linkedReportActionID, displayAsGroup, emojiReactions, @@ -155,6 +167,7 @@ function ReportActionItem({ shouldHideThreadDividerLine = false, shouldShowSubscriptAvatar = false, policy, + transaction, onPress = undefined, }: ReportActionItemProps) { const {translate} = useLocalize(); @@ -180,7 +193,7 @@ function ReportActionItem({ const originalReportID = ReportUtils.getOriginalReportID(report.reportID, action); const originalReport = report.reportID === originalReportID ? report : ReportUtils.getReport(originalReportID); const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID; - + const transactionCurrency = TransactionUtils.getCurrency(transaction); const reportScrollManager = useReportScrollManager(); const highlightedBackgroundColorIfNeeded = useMemo( @@ -700,6 +713,7 @@ function ReportActionItem({ ); @@ -739,11 +753,30 @@ function ReportActionItem({ if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) { return ( - + {transactionThreadReport && !isEmptyObject(transactionThreadReport) ? ( + <> + {transactionCurrency !== report.currency && ( + + )} + + + + + ) : ( + + )} ); } @@ -899,6 +932,14 @@ export default withOnyx({ userWallet: { key: ONYXKEYS.USER_WALLET, }, + transaction: { + key: ({transactionThreadReport, reportActions}) => { + const parentReportActionID = isEmptyObject(transactionThreadReport) ? '0' : transactionThreadReport.parentReportActionID; + const action = reportActions?.find((reportAction) => reportAction.reportActionID === parentReportActionID); + const transactionID = (action as OnyxTypes.OriginalMessageIOU)?.originalMessage.IOUTransactionID ? (action as OnyxTypes.OriginalMessageIOU).originalMessage.IOUTransactionID : 0; + return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; + }, + }, })( memo(ReportActionItem, (prevProps, nextProps) => { const prevParentReportAction = prevProps.parentReportAction; @@ -930,6 +971,9 @@ export default withOnyx({ prevProps.linkedReportActionID === nextProps.linkedReportActionID && lodashIsEqual(prevProps.report.fieldList, nextProps.report.fieldList) && lodashIsEqual(prevProps.policy, nextProps.policy) && + lodashIsEqual(prevProps.transactionThreadReport, nextProps.transactionThreadReport) && + lodashIsEqual(prevProps.reportActions, nextProps.reportActions) && + lodashIsEqual(prevProps.transaction, nextProps.transaction) && lodashIsEqual(prevParentReportAction, nextParentReportAction) ); }), diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 56dd1f4aa62c..bd5195f2410f 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -113,11 +113,17 @@ function ReportActionItemMessageEdit( const insertedEmojis = useRef([]); const draftRef = useRef(draft); const emojiPickerSelectionRef = useRef(undefined); + // The ref to check whether the comment saving is in progress + const isCommentPendingSaved = useRef(false); useEffect(() => { const parser = new ExpensiMark(); const originalMessage = parser.htmlToMarkdown(action.message?.[0].html ?? ''); - if (ReportActionsUtils.isDeletedAction(action) || Boolean(action.message && draftMessage === originalMessage) || Boolean(prevDraftMessage === draftMessage)) { + if ( + ReportActionsUtils.isDeletedAction(action) || + Boolean(action.message && draftMessage === originalMessage) || + Boolean(prevDraftMessage === draftMessage || isCommentPendingSaved.current) + ) { return; } setDraft(draftMessage); @@ -218,11 +224,18 @@ function ReportActionItemMessageEdit( () => lodashDebounce((newDraft: string) => { Report.saveReportActionDraft(reportID, action, newDraft); + isCommentPendingSaved.current = false; }, 1000), [reportID, action], ); - useEffect(() => () => debouncedSaveDraft.cancel(), [debouncedSaveDraft]); + useEffect( + () => () => { + debouncedSaveDraft.cancel(); + isCommentPendingSaved.current = false; + }, + [debouncedSaveDraft], + ); /** * Update frequently used emojis list. We debounce this method in the constructor so that UpdateFrequentlyUsedEmojis @@ -269,6 +282,7 @@ function ReportActionItemMessageEdit( // We want to escape the draft message to differentiate the HTML from the report action and the HTML the user drafted. debouncedSaveDraft(newDraft); + isCommentPendingSaved.current = true; }, [debouncedSaveDraft, debouncedUpdateFrequentlyUsedEmojis, preferredSkinTone, preferredLocale, selection.end], ); diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index 7dc5ace631fa..3d98973c86c4 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -31,6 +31,12 @@ type ReportActionItemParentActionProps = { /** The current report is displayed */ report: OnyxEntry; + /** The transaction thread report associated with the current report, if any */ + transactionThreadReport: OnyxEntry; + + /** Array of report actions for this report */ + reportActions: OnyxTypes.ReportAction[]; + /** Report actions belonging to the report's parent */ parentReportAction: OnyxEntry; @@ -38,7 +44,15 @@ type ReportActionItemParentActionProps = { shouldDisplayReplyDivider: boolean; }; -function ReportActionItemParentAction({report, parentReportAction, index = 0, shouldHideThreadDividerLine = false, shouldDisplayReplyDivider}: ReportActionItemParentActionProps) { +function ReportActionItemParentAction({ + report, + transactionThreadReport, + reportActions, + parentReportAction, + index = 0, + shouldHideThreadDividerLine = false, + shouldDisplayReplyDivider, +}: ReportActionItemParentActionProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -89,9 +103,11 @@ function ReportActionItemParentAction({report, parentReportAction, index = 0, sh > Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? '', ancestor.reportAction.reportActionID))} + onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? ''))} parentReportAction={parentReportAction} report={ancestor.report} + reportActions={reportActions} + transactionThreadReport={transactionThreadReport} action={ancestor.reportAction} displayAsGroup={false} isMostRecentIOUReportAction={false} diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 3b001d859ed8..d1b9c420b0af 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -41,6 +41,12 @@ type ReportActionsListProps = WithCurrentUserPersonalDetailsProps & { /** The report currently being looked at */ report: OnyxTypes.Report; + /** The transaction thread report associated with the current report, if any */ + transactionThreadReport: OnyxEntry; + + /** Array of report actions for the current report */ + reportActions: OnyxTypes.ReportAction[]; + /** The report's parentReportAction */ parentReportAction: OnyxEntry; @@ -125,6 +131,8 @@ const onScrollToIndexFailed = () => {}; function ReportActionsList({ report, + transactionThreadReport, + reportActions = [], parentReportAction, isLoadingInitialReportActions = false, isLoadingOlderReportActions = false, @@ -514,9 +522,11 @@ function ReportActionsList({ ({item: reportAction, index}: ListRenderItemInfo) => ( ; @@ -20,6 +23,9 @@ type ReportActionsListItemRendererProps = { /** Report for this action */ report: Report; + /** The transaction thread report associated with the report for this action, if any */ + transactionThreadReport: OnyxEntry; + /** Should the comment have the appearance of being grouped with the previous comment? */ displayAsGroup: boolean; @@ -41,9 +47,11 @@ type ReportActionsListItemRendererProps = { function ReportActionsListItemRenderer({ reportAction, + reportActions = [], parentReportAction, index, report, + transactionThreadReport, displayAsGroup, mostRecentIOUReportActionID = '', shouldHideThreadDividerLine, @@ -127,6 +135,8 @@ function ReportActionsListItemRenderer({ parentReportAction={parentReportAction} reportID={report.reportID} report={report} + reportActions={reportActions} + transactionThreadReport={transactionThreadReport} index={index} /> ) : ( @@ -134,7 +144,9 @@ function ReportActionsListItemRenderer({ shouldHideThreadDividerLine={shouldHideThreadDividerLine} parentReportAction={parentReportAction} report={report} + transactionThreadReport={transactionThreadReport} action={action} + reportActions={reportActions} linkedReportActionID={linkedReportActionID} displayAsGroup={displayAsGroup} shouldDisplayNewMarker={shouldDisplayNewMarker} diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index e81bfa0fcb6d..1b6a9614a466 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -28,6 +28,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getInitialPaginationSize from './getInitialPaginationSize'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import ReportActionsList from './ReportActionsList'; @@ -35,6 +36,12 @@ import ReportActionsList from './ReportActionsList'; type ReportActionsViewOnyxProps = { /** Session info for the currently logged in user. */ session: OnyxEntry; + + /** Array of report actions for the transaction thread report associated with the current report */ + transactionThreadReportActions: OnyxTypes.ReportAction[]; + + /** The transaction thread report associated with the current report, if any */ + transactionThreadReport: OnyxEntry; }; type ReportActionsViewProps = ReportActionsViewOnyxProps & { @@ -58,6 +65,10 @@ type ReportActionsViewProps = ReportActionsViewOnyxProps & { /** Whether the report is ready for comment linking */ isReadyForCommentLinking?: boolean; + + /** The reportID of the transaction thread report associated with this current report, if any */ + // eslint-disable-next-line react/no-unused-prop-types + transactionThreadReportID?: string | null; }; const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 120; @@ -67,9 +78,11 @@ let listOldID = Math.round(Math.random() * 100); function ReportActionsView({ report, + transactionThreadReport, session, parentReportAction, reportActions: allReportActions = [], + transactionThreadReportActions = [], isLoadingInitialReportActions = false, isLoadingOlderReportActions = false, isLoadingNewerReportActions = false, @@ -97,24 +110,7 @@ function ReportActionsView({ const prevIsSmallScreenWidthRef = useRef(isSmallScreenWidth); const reportID = report.reportID; const isLoading = (!!reportActionID && isLoadingInitialReportActions) || !isReadyForCommentLinking; - - /** - * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently - * displaying. - */ - const fetchNewerAction = useCallback( - (newestReportAction: OnyxTypes.ReportAction) => { - if (isLoadingNewerReportActions || isLoadingInitialReportActions) { - return; - } - - Report.getNewerActions(reportID, newestReportAction.reportActionID); - }, - [isLoadingNewerReportActions, isLoadingInitialReportActions, reportID], - ); - const isReportFullyVisible = useMemo((): boolean => getIsReportFullyVisible(isFocused), [isFocused]); - const openReportIfNecessary = () => { if (!shouldFetchReport(report)) { return; @@ -148,32 +144,85 @@ function ReportActionsView({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [route, isLoadingInitialReportActions]); + // Get a sorted array of reportActions for both the current report and the transaction thread report associated with this report (if there is one) + // so that we display transaction-level and report-level report actions in order in the one-transaction view + const combinedReportActions = useMemo(() => { + if (isEmptyObject(transactionThreadReportActions)) { + return allReportActions; + } + + // Filter out the created action from the transaction thread report actions, since we already have the parent report's created action in `reportActions` + const filteredTransactionThreadReportActions = transactionThreadReportActions?.filter((action) => action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED); + + // Filter out "created" IOU report actions because we don't want to show any preview actions for one transaction reports + const filteredReportActions = [...allReportActions, ...filteredTransactionThreadReportActions].filter( + (action) => ((action as OnyxTypes.OriginalMessageIOU).originalMessage?.type ?? '') !== CONST.IOU.REPORT_ACTION_TYPE.CREATE, + ); + return ReportActionsUtils.getSortedReportActions(filteredReportActions, true); + }, [allReportActions, transactionThreadReportActions]); + const indexOfLinkedAction = useMemo(() => { if (!reportActionID || isLoading) { return -1; } - return allReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? reportActionID : currentReportActionID)); - }, [allReportActions, currentReportActionID, reportActionID, isLoading]); + return combinedReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? reportActionID : currentReportActionID)); + }, [combinedReportActions, currentReportActionID, reportActionID, isLoading]); const reportActions = useMemo(() => { if (!reportActionID) { - return allReportActions; + return combinedReportActions; } + if (isLoading || indexOfLinkedAction === -1) { return []; } if (isFirstLinkedActionRender.current) { - return allReportActions.slice(indexOfLinkedAction); + return combinedReportActions.slice(indexOfLinkedAction); } const paginationSize = getInitialPaginationSize; - return allReportActions.slice(Math.max(indexOfLinkedAction - paginationSize, 0)); + return combinedReportActions.slice(Math.max(indexOfLinkedAction - paginationSize, 0)); + // currentReportActionID is needed to trigger batching once the report action has been positioned // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reportActionID, allReportActions, indexOfLinkedAction, isLoading, currentReportActionID]); + }, [reportActionID, combinedReportActions, indexOfLinkedAction, isLoading, currentReportActionID]); - const hasMoreCached = reportActions.length < allReportActions.length; + const reportActionIDMap = useMemo(() => { + const reportActionIDs = allReportActions.map((action) => action.reportActionID); + return reportActions.map((action) => ({ + reportActionID: action.reportActionID, + reportID: reportActionIDs.includes(action.reportActionID) ? reportID : transactionThreadReport?.reportID, + })); + }, [allReportActions, reportID, transactionThreadReport, reportActions]); + + /** + * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently + * displaying. + */ + const fetchNewerAction = useCallback( + (newestReportAction: OnyxTypes.ReportAction) => { + if (isLoadingNewerReportActions || isLoadingInitialReportActions) { + return; + } + + // If this is a one transaction report, ensure we load newer actions for both this report and the report associated with the transaction + if (!isEmptyObject(transactionThreadReport)) { + // Get newer actions based on the newest reportAction for the current report + const newestActionCurrentReport = reportActionIDMap.find((item) => item.reportID === reportID); + Report.getNewerActions(newestActionCurrentReport?.reportID ?? '0', newestActionCurrentReport?.reportActionID ?? '0'); + + // Get newer actions based on the newest reportAction for the transaction thread report + const newestActionTransactionThreadReport = reportActionIDMap.find((item) => item.reportID === transactionThreadReport.reportID); + Report.getNewerActions(newestActionTransactionThreadReport?.reportID ?? '0', newestActionTransactionThreadReport?.reportActionID ?? '0'); + } else { + Report.getNewerActions(reportID, newestReportAction.reportActionID); + } + }, + [isLoadingNewerReportActions, isLoadingInitialReportActions, reportID, transactionThreadReport, reportActionIDMap], + ); + + const hasMoreCached = reportActions.length < combinedReportActions.length; const newestReportAction = useMemo(() => reportActions?.[0], [reportActions]); const handleReportActionPagination = useCallback( ({firstReportActionID}: {firstReportActionID: string}) => { @@ -192,8 +241,7 @@ function ReportActionsView({ const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); const hasCachedActionOnFirstRender = useInitialValue(() => reportActions.length > 0); - const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; - + const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated || reportActions[0]?.created === transactionThreadReport?.lastVisibleActionCreated; const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); const hasCreatedAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; @@ -312,9 +360,20 @@ function ReportActionsView({ if (!oldestReportAction || hasCreatedAction) { return; } - // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments - Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }, [network.isOffline, isLoadingOlderReportActions, isLoadingInitialReportActions, oldestReportAction, hasCreatedAction, reportID]); + + if (!isEmptyObject(transactionThreadReport)) { + // Get newer actions based on the newest reportAction for the current report + const oldestActionCurrentReport = reportActionIDMap.findLast((item) => item.reportID === reportID); + Report.getNewerActions(oldestActionCurrentReport?.reportID ?? '0', oldestActionCurrentReport?.reportActionID ?? '0'); + + // Get newer actions based on the newest reportAction for the transaction thread report + const oldestActionTransactionThreadReport = reportActionIDMap.findLast((item) => item.reportID === transactionThreadReport.reportID); + Report.getNewerActions(oldestActionTransactionThreadReport?.reportID ?? '0', oldestActionTransactionThreadReport?.reportActionID ?? '0'); + } else { + // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments + Report.getOlderActions(reportID, oldestReportAction.reportActionID); + } + }, [network.isOffline, isLoadingOlderReportActions, isLoadingInitialReportActions, oldestReportAction, hasCreatedAction, reportID, reportActionIDMap, transactionThreadReport]); const loadNewerChats = useCallback(() => { if (isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { @@ -482,6 +541,8 @@ function ReportActionsView({ <> rendering'} session: { key: ONYXKEYS.SESSION, }, + transactionThreadReportActions: { + key: ({transactionThreadReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, + canEvict: false, + selector: (reportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + }, + transactionThreadReport: { + key: ({transactionThreadReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, + }, })(MemoizedReportActionsView), ); diff --git a/src/pages/home/report/ThreadDivider.tsx b/src/pages/home/report/ThreadDivider.tsx index f2f929091f93..083129e15e6d 100644 --- a/src/pages/home/report/ThreadDivider.tsx +++ b/src/pages/home/report/ThreadDivider.tsx @@ -26,7 +26,7 @@ function ThreadDivider({ancestor}: ThreadDividerProps) { return ( Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor?.report?.parentReportID ?? '', ancestor.reportAction.reportActionID))} + onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor?.report?.parentReportID ?? ''))} accessibilityLabel={translate('threads.thread')} role={CONST.ROLE.BUTTON} style={[styles.flexRow, styles.alignItemsCenter, styles.gap1]} diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index 73bd3532bfc8..e5e203fb5030 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -9,7 +9,7 @@ import withWindowDimensions from '@components/withWindowDimensions'; import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; import compose from '@libs/compose'; import getComponentDisplayName from '@libs/getComponentDisplayName'; -import type {FlagCommentNavigatorParamList} from '@libs/Navigation/types'; +import type {FlagCommentNavigatorParamList, SplitDetailsNavigatorParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Report from '@userActions/Report'; @@ -44,7 +44,9 @@ type OnyxProps = { isLoadingReportData: OnyxEntry; }; -type WithReportAndReportActionOrNotFoundProps = OnyxProps & WindowDimensionsProps & StackScreenProps; +type WithReportAndReportActionOrNotFoundProps = OnyxProps & + WindowDimensionsProps & + StackScreenProps; export default function ( WrappedComponent: ComponentType>, diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 9439a12c4078..be8a43b1a483 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -78,6 +78,12 @@ const getQuickActionTitle = (action) => { return 'quickAction.sendMoney'; case CONST.QUICK_ACTIONS.ASSIGN_TASK: return 'quickAction.assignTask'; + case CONST.QUICK_ACTIONS.TRACK_MANUAL: + return 'quickAction.trackManual'; + case CONST.QUICK_ACTIONS.TRACK_SCAN: + return 'quickAction.trackScan'; + case CONST.QUICK_ACTIONS.TRACK_DISTANCE: + return 'quickAction.trackDistance'; default: return ''; } @@ -155,6 +161,11 @@ function FloatingActionButtonAndPopover(props) { return []; }, [props.personalDetails, props.session.accountID, quickActionReport]); + const quickActionTitle = useMemo(() => { + const titleKey = getQuickActionTitle(props.quickAction && props.quickAction.action); + return titleKey ? translate(titleKey) : ''; + }, [props.quickAction, translate]); + const navigateToQuickAction = () => { switch (props.quickAction.action) { case CONST.QUICK_ACTIONS.REQUEST_MANUAL: @@ -340,7 +351,7 @@ function FloatingActionButtonAndPopover(props) { ? [ { icon: getQuickActionIcon(props.quickAction.action), - text: translate(getQuickActionTitle(props.quickAction.action)), + text: quickActionTitle, label: translate('quickAction.shortcut'), isLabelHoverable: false, floatRightAvatars: quickActionAvatars, diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index c1071a333aac..b939ac9b2af9 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -142,7 +142,6 @@ function IOUCurrencySelection(props) { : [ { data: filteredCurrencies, - indexOffset: 0, }, ], headerMessage: isEmpty ? translate('common.noResultsFound') : '', diff --git a/src/pages/iou/SplitBillDetailsPage.js b/src/pages/iou/SplitBillDetailsPage.js deleted file mode 100644 index be3afb822723..000000000000 --- a/src/pages/iou/SplitBillDetailsPage.js +++ /dev/null @@ -1,196 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList'; -import MoneyRequestHeaderStatusBar from '@components/MoneyRequestHeaderStatusBar'; -import ScreenWrapper from '@components/ScreenWrapper'; -import transactionPropTypes from '@components/transactionPropTypes'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import withReportAndReportActionOrNotFound from '@pages/home/report/withReportAndReportActionOrNotFound'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -const propTypes = { - /* Onyx Props */ - - /** The personal details of the person who is logged in */ - personalDetails: personalDetailsPropType, - - /** The active report */ - report: reportPropTypes.isRequired, - - /** Array of report actions for this report */ - reportActions: PropTypes.shape(reportActionPropTypes), - - /** The current transaction */ - transaction: transactionPropTypes.isRequired, - - /** The draft transaction that holds data to be persisited on the current transaction */ - draftTransaction: transactionPropTypes, - - /** Route params */ - route: PropTypes.shape({ - params: PropTypes.shape({ - /** Report ID passed via route r/:reportID/split/details */ - reportID: PropTypes.string, - - /** ReportActionID passed via route r/split/:reportActionID */ - reportActionID: PropTypes.string, - }), - }).isRequired, - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - - /** Currently logged in user email */ - email: PropTypes.string, - }).isRequired, -}; - -const defaultProps = { - personalDetails: {}, - reportActions: {}, - draftTransaction: undefined, -}; - -function SplitBillDetailsPage(props) { - const styles = useThemeStyles(); - const {reportID} = props.report; - const {translate} = useLocalize(); - const reportAction = props.reportActions[`${props.route.params.reportActionID.toString()}`]; - const participantAccountIDs = reportAction.originalMessage.participantAccountIDs; - - // In case this is workspace split bill, we manually add the workspace as the second participant of the split bill - // because we don't save any accountID in the report action's originalMessage other than the payee's accountID - let participants; - if (ReportUtils.isPolicyExpenseChat(props.report)) { - participants = [ - OptionsListUtils.getParticipantsOption({accountID: participantAccountIDs[0], selected: true}, props.personalDetails), - OptionsListUtils.getPolicyExpenseReportOption({...props.report, selected: true}), - ]; - } else { - participants = _.map(participantAccountIDs, (accountID) => OptionsListUtils.getParticipantsOption({accountID, selected: true}, props.personalDetails)); - } - const payeePersonalDetails = props.personalDetails[reportAction.actorAccountID]; - const participantsExcludingPayee = _.filter(participants, (participant) => participant.accountID !== reportAction.actorAccountID); - - const isScanning = TransactionUtils.hasReceipt(props.transaction) && TransactionUtils.isReceiptBeingScanned(props.transaction); - const hasSmartScanFailed = TransactionUtils.hasReceipt(props.transaction) && props.transaction.receipt.state === CONST.IOU.RECEIPT_STATE.SCANFAILED; - const isEditingSplitBill = props.session.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(props.transaction); - - const { - amount: splitAmount, - currency: splitCurrency, - comment: splitComment, - merchant: splitMerchant, - created: splitCreated, - category: splitCategory, - tag: splitTag, - billable: splitBillable, - } = isEditingSplitBill && props.draftTransaction ? ReportUtils.getTransactionDetails(props.draftTransaction) : ReportUtils.getTransactionDetails(props.transaction); - - const onConfirm = useCallback( - () => IOU.completeSplitBill(reportID, reportAction, props.draftTransaction, props.session.accountID, props.session.email), - [reportID, reportAction, props.draftTransaction, props.session.accountID, props.session.email], - ); - - return ( - - - - - {isScanning && ( - - )} - {Boolean(participants.length) && ( - - )} - - - - ); -} - -SplitBillDetailsPage.propTypes = propTypes; -SplitBillDetailsPage.defaultProps = defaultProps; -SplitBillDetailsPage.displayName = 'SplitBillDetailsPage'; - -export default compose( - withReportAndReportActionOrNotFound, - withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, - }, - reportActions: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${route.params.reportID}`, - canEvict: false, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - session: { - key: ONYXKEYS.SESSION, - }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - transaction: { - key: ({route, reportActions}) => { - const reportAction = reportActions[`${route.params.reportActionID.toString()}`]; - return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(reportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - draftTransaction: { - key: ({route, reportActions}) => { - const reportAction = reportActions[`${route.params.reportActionID.toString()}`]; - return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${lodashGet(reportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - }), -)(SplitBillDetailsPage); diff --git a/src/pages/iou/SplitBillDetailsPage.tsx b/src/pages/iou/SplitBillDetailsPage.tsx new file mode 100644 index 000000000000..91aa37dd01c2 --- /dev/null +++ b/src/pages/iou/SplitBillDetailsPage.tsx @@ -0,0 +1,178 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList'; +import MoneyRequestHeaderStatusBar from '@components/MoneyRequestHeaderStatusBar'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {SplitDetailsNavigatorParamList} from '@libs/Navigation/types'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; +import withReportAndReportActionOrNotFound from '@pages/home/report/withReportAndReportActionOrNotFound'; +import type {WithReportAndReportActionOrNotFoundProps} from '@pages/home/report/withReportAndReportActionOrNotFound'; +import * as IOU from '@userActions/IOU'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {PersonalDetailsList, Report, ReportAction, Session, Transaction} from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; +import type {ReportActions} from '@src/types/onyx/ReportAction'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type SplitBillDetailsPageTransactionOnyxProps = { + /** The current transaction */ + transaction: OnyxEntry; + + /** The draft transaction that holds data to be persisited on the current transaction */ + draftTransaction: OnyxEntry; +}; + +type SplitBillDetailsPageOnyxPropsWithoutTransaction = { + /** The personal details of the person who is logged in */ + personalDetails: OnyxEntry; + + /** The active report */ + report: OnyxEntry; + + /** Array of report actions for this report */ + reportActions: OnyxEntry; + + /** Session info for the currently logged in user. */ + session: OnyxEntry; +}; + +type SplitBillDetailsPageOnyxProps = SplitBillDetailsPageTransactionOnyxProps & SplitBillDetailsPageOnyxPropsWithoutTransaction; + +type SplitBillDetailsPageProps = WithReportAndReportActionOrNotFoundProps & + SplitBillDetailsPageOnyxProps & + StackScreenProps; + +function SplitBillDetailsPage({personalDetails, report, route, reportActions, transaction, draftTransaction, session}: SplitBillDetailsPageProps) { + const styles = useThemeStyles(); + const reportID = report?.reportID ?? ''; + const {translate} = useLocalize(); + const reportAction = useMemo(() => reportActions?.[route.params.reportActionID] ?? ({} as ReportAction), [reportActions, route.params.reportActionID]); + const participantAccountIDs = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? reportAction?.originalMessage.participantAccountIDs ?? [] : []; + + // In case this is workspace split bill, we manually add the workspace as the second participant of the split bill + // because we don't save any accountID in the report action's originalMessage other than the payee's accountID + let participants: Array; + if (ReportUtils.isPolicyExpenseChat(report)) { + participants = [ + OptionsListUtils.getParticipantsOption({accountID: participantAccountIDs[0], selected: true, reportID: ''}, personalDetails), + OptionsListUtils.getPolicyExpenseReportOption({...report, selected: true, reportID}), + ]; + } else { + participants = participantAccountIDs.map((accountID) => OptionsListUtils.getParticipantsOption({accountID, selected: true, reportID: ''}, personalDetails)); + } + const payeePersonalDetails = personalDetails?.[reportAction?.actorAccountID ?? -1]; + const participantsExcludingPayee = participants.filter((participant) => participant.accountID !== reportAction?.actorAccountID); + + const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); + const hasSmartScanFailed = TransactionUtils.hasReceipt(transaction) && transaction?.receipt?.state === CONST.IOU.RECEIPT_STATE.SCANFAILED; + const isEditingSplitBill = session?.accountID === reportAction?.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); + + const { + amount: splitAmount, + currency: splitCurrency, + comment: splitComment, + merchant: splitMerchant, + created: splitCreated, + category: splitCategory, + tag: splitTag, + billable: splitBillable, + } = ReportUtils.getTransactionDetails(isEditingSplitBill && draftTransaction ? draftTransaction : transaction) ?? {}; + + const onConfirm = useCallback( + () => IOU.completeSplitBill(reportID, reportAction, draftTransaction, session?.accountID ?? 0, session?.email ?? ''), + [reportID, reportAction, draftTransaction, session?.accountID, session?.email], + ); + + return ( + + + + + {isScanning && ( + + )} + {!!participants.length && ( + + )} + + + + ); +} + +SplitBillDetailsPage.displayName = 'SplitBillDetailsPage'; + +const WrappedComponent = withOnyx({ + transaction: { + key: ({route, reportActions}) => { + const reportAction = reportActions?.[route.params.reportActionID]; + const IOUTransactionID = + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction?.originalMessage?.IOUTransactionID ? reportAction.originalMessage.IOUTransactionID : 0; + return `${ONYXKEYS.COLLECTION.TRANSACTION}${IOUTransactionID}`; + }, + }, + draftTransaction: { + key: ({route, reportActions}) => { + const reportAction = reportActions?.[route.params.reportActionID]; + const IOUTransactionID = + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction?.originalMessage?.IOUTransactionID ? reportAction.originalMessage.IOUTransactionID : 0; + return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${IOUTransactionID}`; + }, + }, +})(withReportAndReportActionOrNotFound(SplitBillDetailsPage)); + +export default withOnyx, SplitBillDetailsPageOnyxPropsWithoutTransaction>({ + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, + }, + reportActions: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${route.params.reportID}`, + canEvict: false, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + session: { + key: ONYXKEYS.SESSION, + }, +})(WrappedComponent); diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 2adffcf390e4..e0a570e334d5 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -112,7 +112,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ if (!didScreenTransitionEnd) { return [newSections, {}]; } - let indexOffset = 0; const chatOptions = OptionsListUtils.getFilteredOptions( reports, personalDetails, @@ -142,13 +141,11 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ chatOptions.recentReports, chatOptions.personalDetails, maxParticipantsReached, - indexOffset, personalDetails, true, ); newSections.push(formatResults.section); - indexOffset = formatResults.newIndexOffset; if (maxParticipantsReached) { return [newSections, {}]; @@ -158,17 +155,13 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ title: translate('common.recents'), data: chatOptions.recentReports, shouldShow: !_.isEmpty(chatOptions.recentReports), - indexOffset, }); - indexOffset += chatOptions.recentReports.length; newSections.push({ title: translate('common.contacts'), data: chatOptions.personalDetails, shouldShow: !_.isEmpty(chatOptions.personalDetails), - indexOffset, }); - indexOffset += chatOptions.personalDetails.length; if (chatOptions.userToInvite && !OptionsListUtils.isCurrentUser(chatOptions.userToInvite)) { newSections.push({ @@ -178,7 +171,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); }), shouldShow: true, - indexOffset, }); } diff --git a/src/pages/iou/request/step/IOURequestStepCurrency.js b/src/pages/iou/request/step/IOURequestStepCurrency.js index 43e4e9bf0eaa..ba1354b4a2e6 100644 --- a/src/pages/iou/request/step/IOURequestStepCurrency.js +++ b/src/pages/iou/request/step/IOURequestStepCurrency.js @@ -109,7 +109,6 @@ function IOURequestStepCurrency({ : [ { data: filteredCurrencies, - indexOffset: 0, }, ], headerMessage: isEmpty ? translate('common.noResultsFound') : '', diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index dad610cbc636..94b59970b9f3 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -84,7 +84,7 @@ function IOURequestStepDistance({ const duplicateWaypointsError = useMemo(() => nonEmptyWaypointsCount >= 2 && _.size(validatedWaypoints) !== nonEmptyWaypointsCount, [nonEmptyWaypointsCount, validatedWaypoints]); const atLeastTwoDifferentWaypointsError = useMemo(() => _.size(validatedWaypoints) < 2, [validatedWaypoints]); const isEditing = action === CONST.IOU.ACTION.EDIT; - const isCreatingNewRequest = Navigation.getActiveRoute().includes('start'); + const isCreatingNewRequest = !(backTo || isEditing); useEffect(() => { MapboxToken.init(); diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.tsx b/src/pages/iou/steps/MoneyRequestAmountForm.tsx index aec2f4765fd4..a010e13ff496 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/steps/MoneyRequestAmountForm.tsx @@ -2,7 +2,6 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; -import type {ValueOf} from 'type-fest'; import BigNumberPad from '@components/BigNumberPad'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -20,6 +19,7 @@ import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/types'; import CONST from '@src/CONST'; +import type {SelectedTabRequest} from '@src/types/onyx'; type MoneyRequestAmountFormProps = { /** IOU amount saved in Onyx */ @@ -41,7 +41,7 @@ type MoneyRequestAmountFormProps = { onSubmitButtonPress: ({amount, currency}: {amount: string; currency: string}) => void; /** The current tab we have navigated to in the request modal. String that corresponds to the request type. */ - selectedTab?: ValueOf; + selectedTab?: SelectedTabRequest; }; type Selection = { diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index f64270726f2d..16608ba13de8 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -141,7 +141,6 @@ function MoneyRequestParticipantsSelector({ */ const sections = useMemo(() => { const newSections = []; - let indexOffset = 0; const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( searchTerm, @@ -149,12 +148,10 @@ function MoneyRequestParticipantsSelector({ newChatOptions.recentReports, newChatOptions.personalDetails, maxParticipantsReached, - indexOffset, personalDetails, true, ); newSections.push(formatResults.section); - indexOffset = formatResults.newIndexOffset; if (maxParticipantsReached) { return newSections; @@ -164,17 +161,13 @@ function MoneyRequestParticipantsSelector({ title: translate('common.recents'), data: newChatOptions.recentReports, shouldShow: !_.isEmpty(newChatOptions.recentReports), - indexOffset, }); - indexOffset += newChatOptions.recentReports.length; newSections.push({ title: translate('common.contacts'), data: newChatOptions.personalDetails, shouldShow: !_.isEmpty(newChatOptions.personalDetails), - indexOffset, }); - indexOffset += newChatOptions.personalDetails.length; if (newChatOptions.userToInvite && !OptionsListUtils.isCurrentUser(newChatOptions.userToInvite)) { newSections.push({ @@ -184,7 +177,6 @@ function MoneyRequestParticipantsSelector({ return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); }), shouldShow: true, - indexOffset, }); } diff --git a/src/pages/iou/steps/NewRequestAmountPage.js b/src/pages/iou/steps/NewRequestAmountPage.tsx similarity index 69% rename from src/pages/iou/steps/NewRequestAmountPage.js rename to src/pages/iou/steps/NewRequestAmountPage.tsx index 1df74569e4c3..be7668e5a6fb 100644 --- a/src/pages/iou/steps/NewRequestAmountPage.js +++ b/src/pages/iou/steps/NewRequestAmountPage.tsx @@ -1,79 +1,63 @@ import {useFocusEffect} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as IOUUtils from '@libs/IOUUtils'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {IOU as IOUType, Report, SelectedTabRequest} from '@src/types/onyx'; import MoneyRequestAmountForm from './MoneyRequestAmountForm'; -const propTypes = { - /** React Navigation route */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string, +type NavigateToNextPageOptions = {amount: string}; - /** The report ID of the IOU */ - reportID: PropTypes.string, - - /** Selected currency from IOUCurrencySelection */ - currency: PropTypes.string, - }), - }).isRequired, +type NewRequestAmountPageOnyxProps = { + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + iou: OnyxEntry; /** The report on which the request is initiated on */ - report: reportPropTypes, - - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, + report: OnyxEntry; /** The current tab we have navigated to in the request modal. String that corresponds to the request type. */ - selectedTab: PropTypes.oneOf(_.values(CONST.TAB_REQUEST)), + selectedTab: OnyxEntry; }; -const defaultProps = { - report: {}, - iou: iouDefaultProps, - selectedTab: CONST.TAB_REQUEST.MANUAL, -}; +type NewRequestAmountPageProps = NewRequestAmountPageOnyxProps & StackScreenProps; -function NewRequestAmountPage({route, iou, report, selectedTab}) { +function NewRequestAmountPage({route, iou, report, selectedTab}: NewRequestAmountPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const prevMoneyRequestID = useRef(iou.id); - const textInput = useRef(null); + const prevMoneyRequestID = useRef(iou?.id); + const textInput = useRef(null); - const iouType = lodashGet(route, 'params.iouType', ''); - const reportID = lodashGet(route, 'params.reportID', ''); + const iouType = route.params.iouType ?? ''; + const reportID = route.params.reportID ?? ''; const isEditing = Navigation.getActiveRoute().includes('amount'); - const currentCurrency = lodashGet(route, 'params.currency', ''); + const currentCurrency = route.params.currency ?? ''; const isDistanceRequestTab = MoneyRequestUtils.isDistanceRequest(iouType, selectedTab); - const currency = CurrencyUtils.isValidCurrencyCode(currentCurrency) ? currentCurrency : iou.currency; + const currency = CurrencyUtils.isValidCurrencyCode(currentCurrency) ? currentCurrency : iou?.currency ?? ''; - const focusTimeoutRef = useRef(null); + const focusTimeoutRef = useRef(null); useFocusEffect( useCallback(() => { - focusTimeoutRef.current = setTimeout(() => textInput.current && textInput.current.focus(), CONST.ANIMATED_TRANSITION); + focusTimeoutRef.current = setTimeout(() => textInput.current?.focus(), CONST.ANIMATED_TRANSITION); return () => { if (!focusTimeoutRef.current) { return; @@ -88,29 +72,29 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { useEffect(() => { if (isEditing) { // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request - if (prevMoneyRequestID.current !== iou.id) { + if (prevMoneyRequestID.current !== iou?.id) { // The ID is cleared on completing a request. In that case, we will do nothing. - if (!iou.id) { + if (!iou?.id) { return; } - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); + Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, report?.reportID), true); return; } const moneyRequestID = `${iouType}${reportID}`; - const shouldReset = iou.id !== moneyRequestID; + const shouldReset = iou?.id !== moneyRequestID; if (shouldReset) { IOU.resetMoneyRequestInfo(moneyRequestID); } - if (!isDistanceRequestTab && (_.isEmpty(iou.participants) || iou.amount === 0 || shouldReset)) { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); + if (!isDistanceRequestTab && (!iou?.participants?.length || iou?.amount === 0 || shouldReset)) { + Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, report?.reportID), true); } } return () => { - prevMoneyRequestID.current = iou.id; + prevMoneyRequestID.current = iou?.id; }; - }, [iou.participants, iou.amount, iou.id, isEditing, iouType, reportID, isDistanceRequestTab]); + }, [isEditing, iouType, reportID, isDistanceRequestTab, report?.reportID, iou?.id, iou?.participants?.length, iou?.amount]); const navigateBack = () => { Navigation.goBack(isEditing ? ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID) : ROUTES.HOME); @@ -128,7 +112,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { Navigation.navigate(ROUTES.MONEY_REQUEST_CURRENCY.getRoute(iouType, reportID, currency, activeRoute)); }; - const navigateToNextPage = ({amount}) => { + const navigateToNextPage = ({amount}: NavigateToNextPageOptions) => { const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount)); IOU.setMoneyRequestAmount(amountInSmallestCurrencyUnits); IOU.setMoneyRequestCurrency(currency); @@ -145,11 +129,11 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { (textInput.current = e)} + amount={iou?.amount} + ref={textInput} onCurrencyButtonPress={navigateToCurrencySelectionPage} onSubmitButtonPress={navigateToNextPage} - selectedTab={selectedTab} + selectedTab={selectedTab ?? CONST.TAB_REQUEST.MANUAL} /> ); @@ -180,14 +164,12 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { ); } -NewRequestAmountPage.propTypes = propTypes; -NewRequestAmountPage.defaultProps = defaultProps; NewRequestAmountPage.displayName = 'NewRequestAmountPage'; -export default withOnyx({ +export default withOnyx({ iou: {key: ONYXKEYS.IOU}, report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID', '')}`, + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, }, selectedTab: { key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index 3a056ee7c0a3..7422bad8061f 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -73,4 +73,7 @@ export default PropTypes.shape({ /** Custom fields attached to the report */ reportFields: PropTypes.objectOf(PropTypes.string), + + /** ID of the transaction thread associated with the report, if any */ + transactionThreadReportID: PropTypes.string, }); diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 1a7b23477349..70c2d301b9ac 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -64,29 +64,23 @@ function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogLis const sections = useMemo(() => { const sectionsList = []; - let indexOffset = 0; sectionsList.push({ title: translate('common.recents'), data: searchOptions.recentReports, shouldShow: searchOptions.recentReports?.length > 0, - indexOffset, }); - indexOffset += searchOptions.recentReports.length; sectionsList.push({ title: translate('common.contacts'), data: searchOptions.personalDetails, shouldShow: searchOptions.personalDetails?.length > 0, - indexOffset, }); - indexOffset += searchOptions.personalDetails.length; if (searchOptions.userToInvite) { sectionsList.push({ data: [searchOptions.userToInvite], shouldShow: true, - indexOffset, }); } diff --git a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx index eca0237eea02..672dbbb91069 100644 --- a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx +++ b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx @@ -27,7 +27,7 @@ import EXIT_SURVEY_REASON_INPUT_IDS from '@src/types/form/ExitSurveyReasonForm'; import ExitSurveyOffline from './ExitSurveyOffline'; type ExitSurveyConfirmPageOnyxProps = { - exitReason?: ExitReason; + exitReason?: ExitReason | null; isLoading: OnyxEntry; }; @@ -106,7 +106,7 @@ ExitSurveyConfirmPage.displayName = 'ExitSurveyConfirmPage'; export default withOnyx({ exitReason: { key: ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM, - selector: (value: OnyxEntry) => value?.[EXIT_SURVEY_REASON_INPUT_IDS.REASON], + selector: (value: OnyxEntry) => value?.[EXIT_SURVEY_REASON_INPUT_IDS.REASON] ?? null, }, isLoading: { key: ONYXKEYS.IS_SWITCHING_TO_OLD_DOT, diff --git a/src/pages/settings/Profile/CustomStatus/SetDatePage.js b/src/pages/settings/Profile/CustomStatus/SetDatePage.tsx similarity index 70% rename from src/pages/settings/Profile/CustomStatus/SetDatePage.js rename to src/pages/settings/Profile/CustomStatus/SetDatePage.tsx index 5b95ec757617..ae0b910dc01f 100644 --- a/src/pages/settings/Profile/CustomStatus/SetDatePage.js +++ b/src/pages/settings/Profile/CustomStatus/SetDatePage.tsx @@ -1,38 +1,45 @@ -import lodashGet from 'lodash/get'; import React, {useCallback} from 'react'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import DatePicker from '@components/DatePicker'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as User from '@libs/actions/User'; -import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/SettingsStatusClearDateForm'; +import type * as OnyxTypes from '@src/types/onyx'; -const propTypes = { - ...withLocalizePropTypes, +type DateTime = { + dateTime: string; }; -function SetDatePage({translate, customStatus}) { +type SetDatePageOnyxProps = { + customStatus: OnyxEntry; +}; + +type SetDatePageProps = SetDatePageOnyxProps; + +function SetDatePage({customStatus}: SetDatePageProps) { const styles = useThemeStyles(); - const customClearAfter = lodashGet(customStatus, 'clearAfter', ''); + const {translate} = useLocalize(); + const customClearAfter = customStatus?.clearAfter ?? ''; - const onSubmit = (v) => { - User.updateDraftCustomStatus({clearAfter: DateUtils.combineDateAndTime(customClearAfter, v.dateTime)}); + const onSubmit = (value: DateTime) => { + User.updateDraftCustomStatus({clearAfter: DateUtils.combineDateAndTime(customClearAfter, value.dateTime)}); Navigation.goBack(ROUTES.SETTINGS_STATUS_CLEAR_AFTER); }; - const validate = useCallback((values) => { - const requiredFields = ['dateTime']; - const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields); + const validate = useCallback((values: FormOnyxValues) => { + const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.DATE_TIME]); const dateError = ValidationUtils.getDatePassedError(values.dateTime); if (values.dateTime && dateError) { @@ -58,7 +65,6 @@ function SetDatePage({translate, customStatus}) { submitButtonText={translate('common.save')} validate={validate} enabledWhenOffline - shouldUseDefaultValue > ); } -SetDatePage.propTypes = propTypes; SetDatePage.displayName = 'SetDatePage'; -export default compose( - withLocalize, - withOnyx({ - customStatus: { - key: ONYXKEYS.CUSTOM_STATUS_DRAFT, - }, - clearDateForm: { - key: `${ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM}Draft`, - }, - }), -)(SetDatePage); +export default withOnyx({ + customStatus: { + key: ONYXKEYS.CUSTOM_STATUS_DRAFT, + }, +})(SetDatePage); diff --git a/src/pages/settings/Profile/CustomStatus/SetTimePage.js b/src/pages/settings/Profile/CustomStatus/SetTimePage.tsx similarity index 68% rename from src/pages/settings/Profile/CustomStatus/SetTimePage.js rename to src/pages/settings/Profile/CustomStatus/SetTimePage.tsx index 1165e708f500..101f7269616d 100644 --- a/src/pages/settings/Profile/CustomStatus/SetTimePage.js +++ b/src/pages/settings/Profile/CustomStatus/SetTimePage.tsx @@ -1,28 +1,31 @@ -import lodashGet from 'lodash/get'; import React from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import TimePicker from '@components/TimePicker/TimePicker'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as User from '@libs/actions/User'; -import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; -const propTypes = { - ...withLocalizePropTypes, +type SetTimePageOnyxProps = { + customStatus: OnyxEntry; }; -function SetTimePage({translate, customStatus}) { +type SetTimePageProps = SetTimePageOnyxProps; + +function SetTimePage({customStatus}: SetTimePageProps) { const styles = useThemeStyles(); - const clearAfter = lodashGet(customStatus, 'clearAfter', ''); + const {translate} = useLocalize(); + const clearAfter = customStatus?.clearAfter ?? ''; - const onSubmit = (time) => { + const onSubmit = (time: string) => { const timeToUse = DateUtils.combineDateAndTime(time, clearAfter); User.updateDraftCustomStatus({clearAfter: timeToUse}); @@ -40,9 +43,7 @@ function SetTimePage({translate, customStatus}) { /> @@ -50,14 +51,10 @@ function SetTimePage({translate, customStatus}) { ); } -SetTimePage.propTypes = propTypes; SetTimePage.displayName = 'SetTimePage'; -export default compose( - withLocalize, - withOnyx({ - customStatus: { - key: ONYXKEYS.CUSTOM_STATUS_DRAFT, - }, - }), -)(SetTimePage); +export default withOnyx({ + customStatus: { + key: ONYXKEYS.CUSTOM_STATUS_DRAFT, + }, +})(SetTimePage); diff --git a/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js b/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.tsx similarity index 75% rename from src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js rename to src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.tsx index 290d6431492d..40fbf097cdd9 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js +++ b/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.tsx @@ -1,59 +1,58 @@ -import _ from 'lodash'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import FormProvider from '@components/Form/FormProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps} from '@components/withCurrentUserPersonalDetails'; -import withLocalize from '@components/withLocalize'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as User from '@libs/actions/User'; -import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, +type CustomStatusTypes = ValueOf; + +type StatusType = { + value: CustomStatusTypes; + text: string; + keyForList: string; + isSelected: boolean; }; -const propTypes = { - currentUserPersonalDetails: personalDetailsPropType, - customStatus: PropTypes.shape({ - clearAfter: PropTypes.string, - }), +type StatusClearAfterPageOnyxProps = { + /** User's custom status */ + customStatus: OnyxEntry; }; +type StatusClearAfterPageProps = StatusClearAfterPageOnyxProps; + /** - * @param {string} data - either a value from CONST.CUSTOM_STATUS_TYPES or a dateTime string in the format YYYY-MM-DD HH:mm - * @returns {string} + * @param data - either a value from CONST.CUSTOM_STATUS_TYPES or a dateTime string in the format YYYY-MM-DD HH:mm */ -function getSelectedStatusType(data) { +function getSelectedStatusType(data: string): CustomStatusTypes { switch (data) { case DateUtils.getEndOfToday(): return CONST.CUSTOM_STATUS_TYPES.AFTER_TODAY; case CONST.CUSTOM_STATUS_TYPES.NEVER: case '': return CONST.CUSTOM_STATUS_TYPES.NEVER; - case false: - return CONST.CUSTOM_STATUS_TYPES.AFTER_TODAY; default: return CONST.CUSTOM_STATUS_TYPES.CUSTOM; } } -const useValidateCustomDate = (data) => { +const useValidateCustomDate = (data: string) => { const [customDateError, setCustomDateError] = useState(''); const [customTimeError, setCustomTimeError] = useState(''); const validate = () => { @@ -63,8 +62,8 @@ const useValidateCustomDate = (data) => { setCustomTimeError(timeValidationErrorKey); return { - dateValidationErrorKey, - timeValidationErrorKey, + dateError: dateValidationErrorKey, + timeError: timeValidationErrorKey, }; }; @@ -81,15 +80,17 @@ const useValidateCustomDate = (data) => { return {customDateError, customTimeError, validateCustomDate}; }; -function StatusClearAfterPage({currentUserPersonalDetails, customStatus}) { +function StatusClearAfterPage({customStatus}: StatusClearAfterPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const clearAfter = lodashGet(currentUserPersonalDetails, 'status.clearAfter', ''); - const draftClearAfter = lodashGet(customStatus, 'clearAfter', ''); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const clearAfter = currentUserPersonalDetails.status?.clearAfter ?? ''; + + const draftClearAfter = customStatus?.clearAfter ?? ''; const [draftPeriod, setDraftPeriod] = useState(getSelectedStatusType(draftClearAfter || clearAfter)); - const statusType = useMemo( + const statusType = useMemo( () => - _.map(CONST.CUSTOM_STATUS_TYPES, (value, key) => ({ + Object.entries(CONST.CUSTOM_STATUS_TYPES).map(([key, value]) => ({ value, text: translate(`statusPage.timePeriods.${value}`), keyForList: key, @@ -102,8 +103,8 @@ function StatusClearAfterPage({currentUserPersonalDetails, customStatus}) { const {redBrickDateIndicator, redBrickTimeIndicator} = useMemo( () => ({ - redBrickDateIndicator: customDateError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : null, - redBrickTimeIndicator: customTimeError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : null, + redBrickDateIndicator: customDateError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + redBrickTimeIndicator: customTimeError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }), [customTimeError, customDateError], ); @@ -113,19 +114,19 @@ function StatusClearAfterPage({currentUserPersonalDetails, customStatus}) { if (dateError || timeError) { return; } - let calculatedDraftDate = ''; + let calculatedDraftDate: string; if (draftPeriod === CONST.CUSTOM_STATUS_TYPES.CUSTOM) { calculatedDraftDate = draftClearAfter; } else { - const selectedRange = _.find(statusType, (item) => item.isSelected); - calculatedDraftDate = DateUtils.getDateFromStatusType(selectedRange.value); + const selectedRange = statusType.find((item) => item.isSelected); + calculatedDraftDate = DateUtils.getDateFromStatusType(selectedRange?.value ?? CONST.CUSTOM_STATUS_TYPES.NEVER); } User.updateDraftCustomStatus({clearAfter: calculatedDraftDate}); Navigation.goBack(ROUTES.SETTINGS_STATUS); }; const updateMode = useCallback( - (mode) => { + (mode: StatusType) => { if (mode.value === draftPeriod) { return; } @@ -134,8 +135,8 @@ function StatusClearAfterPage({currentUserPersonalDetails, customStatus}) { if (mode.value === CONST.CUSTOM_STATUS_TYPES.CUSTOM) { User.updateDraftCustomStatus({clearAfter: DateUtils.getOneHourFromNow()}); } else { - const selectedRange = _.find(statusType, (item) => item.value === mode.value); - const calculatedDraftDate = DateUtils.getDateFromStatusType(selectedRange.value); + const selectedRange = statusType.find((item) => item.value === mode.value); + const calculatedDraftDate = DateUtils.getDateFromStatusType(selectedRange?.value ?? CONST.CUSTOM_STATUS_TYPES.NEVER); User.updateDraftCustomStatus({clearAfter: calculatedDraftDate}); Navigation.goBack(ROUTES.SETTINGS_STATUS); } @@ -156,7 +157,7 @@ function StatusClearAfterPage({currentUserPersonalDetails, customStatus}) { const timePeriodOptions = useCallback( () => - _.map(statusType, (item) => ( + statusType.map((item) => ( updateMode(item)} @@ -220,24 +221,9 @@ function StatusClearAfterPage({currentUserPersonalDetails, customStatus}) { } StatusClearAfterPage.displayName = 'StatusClearAfterPage'; -StatusClearAfterPage.propTypes = propTypes; -StatusClearAfterPage.defaultProps = defaultProps; - -export default compose( - withCurrentUserPersonalDetails, - withLocalize, - withOnyx({ - timePeriodType: { - key: `${ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM}Draft`, - }, - clearDateForm: { - key: `${ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM}Draft`, - }, - customStatus: { - key: ONYXKEYS.CUSTOM_STATUS_DRAFT, - }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - }), -)(StatusClearAfterPage); + +export default withOnyx({ + customStatus: { + key: ONYXKEYS.CUSTOM_STATUS_DRAFT, + }, +})(StatusClearAfterPage); diff --git a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx index f07d560ab454..8bed21f322ec 100644 --- a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx @@ -77,7 +77,7 @@ function CountrySelectionPage({route, navigation}: CountrySelectionPageProps) { headerMessage={headerMessage} textInputLabel={translate('common.country')} textInputValue={searchValue} - sections={[{data: searchResults, indexOffset: 0}]} + sections={[{data: searchResults}]} ListItem={RadioListItem} onSelectRow={selectCountry} onChangeText={setSearchValue} diff --git a/src/pages/settings/Profile/PronounsPage.tsx b/src/pages/settings/Profile/PronounsPage.tsx index 5bd2737a98a4..b8022f6a4079 100644 --- a/src/pages/settings/Profile/PronounsPage.tsx +++ b/src/pages/settings/Profile/PronounsPage.tsx @@ -91,7 +91,7 @@ function PronounsPage({currentUserPersonalDetails, isLoadingApp = true}: Pronoun textInputLabel={translate('pronounsPage.pronouns')} textInputPlaceholder={translate('pronounsPage.placeholderText')} textInputValue={searchValue} - sections={[{data: filteredPronounsList, indexOffset: 0}]} + sections={[{data: filteredPronounsList}]} ListItem={RadioListItem} onSelectRow={updatePronouns} onChangeText={setSearchValue} diff --git a/src/pages/settings/Profile/TimezoneSelectPage.tsx b/src/pages/settings/Profile/TimezoneSelectPage.tsx index 3aff5f820cf8..97cec508f867 100644 --- a/src/pages/settings/Profile/TimezoneSelectPage.tsx +++ b/src/pages/settings/Profile/TimezoneSelectPage.tsx @@ -72,7 +72,7 @@ function TimezoneSelectPage({currentUserPersonalDetails}: TimezoneSelectPageProp textInputValue={timezoneInputText} onChangeText={filterShownTimezones} onSelectRow={saveSelectedTimezone} - sections={[{data: timezoneOptions, indexOffset: 0, isDisabled: timezone.automatic}]} + sections={[{data: timezoneOptions, isDisabled: timezone.automatic}]} initiallyFocusedOptionKey={timezoneOptions.find((tz) => tz.text === timezone.selected)?.keyForList} showScrollIndicator shouldShowTooltips={false} diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 6685a1bf18da..0a922321766e 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -116,40 +116,32 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro const sections = useMemo(() => { const sectionsList = []; - let indexOffset = 0; if (currentUserOption) { sectionsList.push({ title: translate('newTaskPage.assignMe'), data: [currentUserOption], shouldShow: true, - indexOffset, }); - indexOffset += 1; } sectionsList.push({ title: translate('common.recents'), data: recentReports, shouldShow: recentReports?.length > 0, - indexOffset, }); - indexOffset += recentReports?.length || 0; sectionsList.push({ title: translate('common.contacts'), data: personalDetails, shouldShow: personalDetails?.length > 0, - indexOffset, }); - indexOffset += personalDetails?.length || 0; if (userToInvite) { sectionsList.push({ title: '', data: [userToInvite], shouldShow: true, - indexOffset, }); } diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 00d7eaa33f6b..13e828605833 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -163,7 +163,6 @@ function WorkspaceInvitePage({ const sections: MembersSection[] = useMemo(() => { const sectionsArr: MembersSection[] = []; - let indexOffset = 0; if (!didScreenTransitionEnd) { return []; @@ -187,9 +186,7 @@ function WorkspaceInvitePage({ title: undefined, data: filterSelectedOptions, shouldShow: true, - indexOffset, }); - indexOffset += filterSelectedOptions.length; // Filtering out selected users from the search results const selectedLogins = selectedOptions.map(({login}) => login); @@ -200,9 +197,7 @@ function WorkspaceInvitePage({ title: translate('common.contacts'), data: personalDetailsFormatted, shouldShow: !isEmptyObject(personalDetailsFormatted), - indexOffset, }); - indexOffset += personalDetailsFormatted.length; Object.values(usersToInvite).forEach((userToInvite) => { const hasUnselectedUserToInvite = !selectedLogins.some((selectedLogin) => selectedLogin === userToInvite.login); @@ -212,7 +207,6 @@ function WorkspaceInvitePage({ title: undefined, data: [OptionsListUtils.formatMemberForList(userToInvite)], shouldShow: true, - indexOffset: indexOffset++, }); } }); diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index a12d4b078b00..3a876cb84a0d 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -589,7 +589,7 @@ function WorkspaceMembersPage({ (!!formState?.isLoading); const visibilityDescription = useMemo(() => translate(`newRoomPage.${visibility}Description`), [translate, visibility]); const {isLoading = false, errorFields = {}} = formState ?? {}; + const {activeWorkspaceID} = useActiveWorkspace(); + + const activeWorkspaceOrDefaultID = activeWorkspaceID ?? activePolicyID; const workspaceOptions = useMemo( () => @@ -82,8 +86,8 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli [policies], ); const [policyID, setPolicyID] = useState(() => { - if (!!activePolicyID && workspaceOptions.some((option) => option.value === activePolicyID)) { - return activePolicyID; + if (!!activeWorkspaceOrDefaultID && workspaceOptions.some((option) => option.value === activeWorkspaceOrDefaultID)) { + return activeWorkspaceOrDefaultID; } return ''; }); @@ -132,12 +136,12 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli } return; } - if (!!activePolicyID && workspaceOptions.some((opt) => opt.value === activePolicyID)) { - setPolicyID(activePolicyID); + if (!!activeWorkspaceOrDefaultID && workspaceOptions.some((opt) => opt.value === activeWorkspaceOrDefaultID)) { + setPolicyID(activeWorkspaceOrDefaultID); } else { setPolicyID(''); } - }, [activePolicyID, policyID, workspaceOptions]); + }, [activeWorkspaceOrDefaultID, policyID, workspaceOptions]); useEffect(() => { if (!(((wasLoading && !isLoading) || (isOffline && isLoading)) && isEmptyObject(errorFields))) { diff --git a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx index cc73f4a64a80..c8640d3f71b0 100644 --- a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx +++ b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx @@ -61,7 +61,7 @@ function WorkspaceProfileCurrencyPage({currencyList = {}, policy, isLoadingRepor }; }); - const sections = [{data: currencyItems, indexOffset: 0}]; + const sections = [{data: currencyItems}]; const headerMessage = searchText.trim() && !currencyItems.length ? translate('common.noResultsFound') : ''; diff --git a/src/pages/workspace/WorkspaceResetBankAccountModal.js b/src/pages/workspace/WorkspaceResetBankAccountModal.js index f98077a546ca..4c4b022039ba 100644 --- a/src/pages/workspace/WorkspaceResetBankAccountModal.js +++ b/src/pages/workspace/WorkspaceResetBankAccountModal.js @@ -20,9 +20,15 @@ const propTypes = { /** Currently logged in user email */ email: PropTypes.string, }).isRequired, + + /** Information about the logged in user's account */ + user: PropTypes.shape({ + /** Whether or not the user is on a public domain email account or not */ + isFromPublicDomain: PropTypes.bool, + }).isRequired, }; -function WorkspaceResetBankAccountModal({reimbursementAccount, session}) { +function WorkspaceResetBankAccountModal({reimbursementAccount, session, user}) { const styles = useThemeStyles(); const {translate} = useLocalize(); const achData = lodashGet(reimbursementAccount, 'achData') || {}; @@ -48,7 +54,7 @@ function WorkspaceResetBankAccountModal({reimbursementAccount, session}) { } danger onCancel={BankAccounts.cancelResetFreePlanBankAccount} - onConfirm={() => BankAccounts.resetFreePlanBankAccount(bankAccountID, session, achData.policyID)} + onConfirm={() => BankAccounts.resetFreePlanBankAccount(bankAccountID, session, achData.policyID, user)} shouldShowCancelButton isVisible /> @@ -62,4 +68,7 @@ export default withOnyx({ session: { key: ONYXKEYS.SESSION, }, + user: { + key: ONYXKEYS.USER, + }, })(WorkspaceResetBankAccountModal); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 496ae0e6e0b6..5b246caa6e07 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -315,7 +315,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat {!shouldShowEmptyState && !isLoading && ( 0 && ( item.isSelected)?.keyForList} diff --git a/src/pages/workspace/members/WorkspaceOwnerPaymentCardCurrencyModal.tsx b/src/pages/workspace/members/WorkspaceOwnerPaymentCardCurrencyModal.tsx index 95ecbf0cbe0e..fcbbbbd4af3f 100644 --- a/src/pages/workspace/members/WorkspaceOwnerPaymentCardCurrencyModal.tsx +++ b/src/pages/workspace/members/WorkspaceOwnerPaymentCardCurrencyModal.tsx @@ -38,7 +38,6 @@ function WorkspaceOwnerPaymentCardCurrencyModal({isVisible, currencies, currentC keyForList: currency, isSelected: currency === currentCurrency, })), - indexOffset: 0, }, ], }), diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index c62b05dedea9..56cf00582782 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -310,7 +310,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { {tagList.length > 0 && !isLoading && ( 0, - indexOffset: 0, }); sectionsArray.push({ title: translate('common.all'), data: formattedPolicyMembers, shouldShow: true, - indexOffset: formattedApprover.length, }); return sectionsArray; diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx index 6da120f95766..e96b19ce4442 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx @@ -149,14 +149,12 @@ function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDeta sectionsArray.push({ data: formattedAuthorizedPayer, shouldShow: true, - indexOffset: 0, }); sectionsArray.push({ title: translate('workflowsPayerPage.admins'), data: formattedPolicyAdmins, shouldShow: true, - indexOffset: formattedAuthorizedPayer.length, }); return sectionsArray; }, [formattedPolicyAdmins, formattedAuthorizedPayer, translate, searchTerm]); diff --git a/src/stories/SelectionList.stories.tsx b/src/stories/SelectionList.stories.tsx index 92936d6d73a3..11be5e2e3bad 100644 --- a/src/stories/SelectionList.stories.tsx +++ b/src/stories/SelectionList.stories.tsx @@ -40,7 +40,6 @@ const SECTIONS = [ isSelected: false, }, ], - indexOffset: 0, isDisabled: false, }, { @@ -61,7 +60,6 @@ const SECTIONS = [ isSelected: false, }, ], - indexOffset: 3, isDisabled: false, }, ]; @@ -71,7 +69,7 @@ function Default(props: BaseSelectionListProps) { const sections = props.sections.map((section) => { const data = section.data.map((item, index) => { - const isSelected = selectedIndex === index + (section?.indexOffset ?? 0); + const isSelected = selectedIndex === index; return {...item, isSelected}; }); @@ -83,7 +81,7 @@ function Default(props: BaseSelectionListProps) { const newSelectedIndex = section.data.findIndex((option) => option.keyForList === item.keyForList); if (newSelectedIndex >= 0) { - setSelectedIndex(newSelectedIndex + (section?.indexOffset ?? 0)); + setSelectedIndex(newSelectedIndex); } }); }; @@ -115,7 +113,7 @@ function WithTextInput(props: BaseSelectionListProps) { return memo; } - const isSelected = selectedIndex === index + (section?.indexOffset ?? 0); + const isSelected = selectedIndex === index; memo.push({...item, isSelected}); return memo; }, []); @@ -128,7 +126,7 @@ function WithTextInput(props: BaseSelectionListProps) { const newSelectedIndex = section.data.findIndex((option) => option.keyForList === item.keyForList); if (newSelectedIndex >= 0) { - setSelectedIndex(newSelectedIndex + (section?.indexOffset ?? 0)); + setSelectedIndex(newSelectedIndex); } }); }; @@ -177,7 +175,7 @@ function WithAlternateText(props: BaseSelectionListProps) { const sections = props.sections.map((section) => { const data = section.data.map((item, index) => { - const isSelected = selectedIndex === index + (section?.indexOffset ?? 0); + const isSelected = selectedIndex === index; return { ...item, @@ -194,7 +192,7 @@ function WithAlternateText(props: BaseSelectionListProps) { const newSelectedIndex = section.data.findIndex((option) => option.keyForList === item.keyForList); if (newSelectedIndex >= 0) { - setSelectedIndex(newSelectedIndex + (section?.indexOffset ?? 0)); + setSelectedIndex(newSelectedIndex); } }); }; @@ -225,7 +223,7 @@ function MultipleSelection(props: BaseSelectionListProps) { allIds.push(item.keyForList); } const isSelected = item.keyForList ? selectedIds.includes(item.keyForList) : false; - const isAdmin = index + (section?.indexOffset ?? 0) === 0; + const isAdmin = index === 0; return { ...item, @@ -295,7 +293,7 @@ function WithSectionHeader(props: BaseSelectionListProps) { allIds.push(item.keyForList); } const isSelected = item.keyForList ? selectedIds.includes(item.keyForList) : false; - const isAdmin = itemIndex + (section?.indexOffset ?? 0) === 0; + const isAdmin = itemIndex === 0; return { ...item, @@ -363,7 +361,7 @@ function WithConfirmButton(props: BaseSelectionListProps) { allIds.push(item.keyForList); } const isSelected = item.keyForList ? selectedIds.includes(item.keyForList) : false; - const isAdmin = itemIndex + (section.indexOffset ?? 0) === 0; + const isAdmin = itemIndex === 0; return { ...item, diff --git a/src/styles/index.ts b/src/styles/index.ts index fbc2680710b1..d6dbb539e44d 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4029,9 +4029,14 @@ const styles = (theme: ThemeColors) => paddingLeft: 0, }, - dropDownButtonArrowContain: { + dropDownMediumButtonArrowContain: { marginLeft: 12, - marginRight: 14, + marginRight: 16, + }, + + dropDownLargeButtonArrowContain: { + marginLeft: 16, + marginRight: 20, }, dropDownButtonCartIconView: { diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index b547f28137b3..a3357b8982a1 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -818,21 +818,18 @@ function getLineHeightStyle(lineHeight: number): TextStyle { /** * Gets the correct size for the empty state container based on screen dimensions */ -function getReportWelcomeContainerStyle(isSmallScreenWidth: boolean, isMoneyOrTaskReport = false): ViewStyle { +function getReportWelcomeContainerStyle(isSmallScreenWidth: boolean, isMoneyOrTaskReport = false, shouldShowAnimatedBackground = true): ViewStyle { const emptyStateBackground = isMoneyOrTaskReport ? CONST.EMPTY_STATE_BACKGROUND.MONEY_OR_TASK_REPORT : CONST.EMPTY_STATE_BACKGROUND; - if (isSmallScreenWidth) { - return { - minHeight: emptyStateBackground.SMALL_SCREEN.CONTAINER_MINHEIGHT, - display: 'flex', - justifyContent: 'space-between', - }; - } - - return { - minHeight: emptyStateBackground.WIDE_SCREEN.CONTAINER_MINHEIGHT, + const baseStyles: ViewStyle = { display: 'flex', justifyContent: 'space-between', }; + + if (shouldShowAnimatedBackground) { + baseStyles.minHeight = isSmallScreenWidth ? emptyStateBackground.SMALL_SCREEN.CONTAINER_MINHEIGHT : emptyStateBackground.WIDE_SCREEN.CONTAINER_MINHEIGHT; + } + + return baseStyles; } type GetBaseAutoCompleteSuggestionContainerStyleParams = { diff --git a/src/types/modules/preload-webpack-plugin.d.ts b/src/types/modules/preload-webpack-plugin.d.ts new file mode 100644 index 000000000000..8f9d33a51080 --- /dev/null +++ b/src/types/modules/preload-webpack-plugin.d.ts @@ -0,0 +1,16 @@ +declare module '@vue/preload-webpack-plugin' { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Options { + rel: string; + as: string; + fileWhitelist: RegExp[]; + include: string; + } + + declare class PreloadWebpackPlugin { + constructor(options?: Options); + apply(compiler: Compiler): void; + } + + export default PreloadWebpackPlugin; +} diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 398a0fe0dd28..ddb0c33c2f0c 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1,5 +1,6 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; +import type * as OnyxTypes from '.'; import type * as OnyxCommon from './OnyxCommon'; type Unit = 'mi' | 'km'; @@ -328,7 +329,7 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< submitsTo?: number; /** The employee list of the policy */ - employeeList?: []; + employeeList?: OnyxTypes.PolicyMembers | []; /** The reimbursement choice for policy */ reimbursementChoice?: ValueOf; diff --git a/src/types/onyx/PolicyMember.ts b/src/types/onyx/PolicyMember.ts index f68fe194df0e..366a7ef7d530 100644 --- a/src/types/onyx/PolicyMember.ts +++ b/src/types/onyx/PolicyMember.ts @@ -4,6 +4,15 @@ type PolicyMember = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Role of the user in the policy */ role?: string; + /** Email of the user */ + email?: string; + + /** Email of the user this user forwards all approved reports to */ + forwardsTo?: string; + + /** Email of the user this user submits all reports to */ + submitsTo?: string; + /** * Errors from api calls on the specific user * {: 'error message', : 'error message 2'} diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 0893fde82c73..ce20462df372 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -183,7 +183,9 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** Pending members of the report */ pendingChatMembers?: PendingChatMember[]; - /** If the report contains reportFields, save the field id and its value */ + /** The ID of the single transaction thread report associated with this report, if one exists */ + transactionThreadReportID?: string; + fieldList?: Record; }, PolicyReportField['fieldID'] diff --git a/src/types/onyx/SelectedTabRequest.ts b/src/types/onyx/SelectedTabRequest.ts new file mode 100644 index 000000000000..8a87db6eee82 --- /dev/null +++ b/src/types/onyx/SelectedTabRequest.ts @@ -0,0 +1,6 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type SelectedTabRequest = ValueOf; + +export default SelectedTabRequest; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index c829b9064f2e..915fba9308d2 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -64,6 +64,7 @@ import type Request from './Request'; import type Response from './Response'; import type ScreenShareRequest from './ScreenShareRequest'; import type SecurityGroup from './SecurityGroup'; +import type SelectedTabRequest from './SelectedTabRequest'; import type Session from './Session'; import type Task from './Task'; import type Transaction from './Transaction'; @@ -142,6 +143,7 @@ export type { Response, ScreenShareRequest, SecurityGroup, + SelectedTabRequest, Session, Task, TaxRate, diff --git a/tests/actions/EnforceActionExportRestrictions.ts b/tests/actions/EnforceActionExportRestrictions.ts index 92d96869d02d..f9f77b314476 100644 --- a/tests/actions/EnforceActionExportRestrictions.ts +++ b/tests/actions/EnforceActionExportRestrictions.ts @@ -10,6 +10,10 @@ describe('ReportUtils', () => { // @ts-expect-error the test is asserting that it's undefined, so the TS error is normal expect(ReportUtils.getParentReport).toBeUndefined(); }); + it('does not export isOneTransactionReport', () => { + // @ts-expect-error the test is asserting that it's undefined, so the TS error is normal + expect(ReportUtils.isOneTransactionReport).toBeUndefined(); + }); }); describe('Task', () => { diff --git a/tests/e2e/config.ts b/tests/e2e/config.ts index 52d284b9dd86..d6d097b6c346 100644 --- a/tests/e2e/config.ts +++ b/tests/e2e/config.ts @@ -88,16 +88,17 @@ export default { // #announce Chat with many messages reportID: '5421294415618529', }, - [TEST_NAMES.Linking]: { - name: TEST_NAMES.Linking, - reportScreen: { - autoFocus: true, - }, - // Crowded Policy (Do Not Delete) Report, has a input bar available: - reportID: '8268282951170052', - linkedReportID: '5421294415618529', - linkedReportActionID: '2845024374735019929', - }, + // TODO: fix and enable again + // [TEST_NAMES.Linking]: { + // name: TEST_NAMES.Linking, + // reportScreen: { + // autoFocus: true, + // }, + // // Crowded Policy (Do Not Delete) Report, has a input bar available: + // reportID: '8268282951170052', + // linkedReportID: '5421294415618529', + // linkedReportActionID: '2845024374735019929', + // }, }, }; diff --git a/tests/perf-test/BaseOptionsList.perf-test.tsx b/tests/perf-test/BaseOptionsList.perf-test.tsx index 5e8b5e9f9289..dc5768610861 100644 --- a/tests/perf-test/BaseOptionsList.perf-test.tsx +++ b/tests/perf-test/BaseOptionsList.perf-test.tsx @@ -23,7 +23,6 @@ describe('[BaseOptionsList]', () => { isSelected: selectedIds.includes(`item-${index}`), reportID: `report-${index}`, })), - indexOffset: 0, isDisabled: false, shouldShow: true, title: 'Section 1', @@ -35,7 +34,6 @@ describe('[BaseOptionsList]', () => { isSelected: selectedIds.includes(`item-${index}`), reportID: `report-${index}`, })), - indexOffset: 0, isDisabled: false, shouldShow: true, title: 'Section 2', diff --git a/tests/perf-test/OptionsSelector.perf-test.tsx b/tests/perf-test/OptionsSelector.perf-test.tsx index 835e2a15673c..44dc4ac6c317 100644 --- a/tests/perf-test/OptionsSelector.perf-test.tsx +++ b/tests/perf-test/OptionsSelector.perf-test.tsx @@ -38,24 +38,20 @@ jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType return WithNavigationFocus; }); -type GenerateSectionsProps = Array<{numberOfItems: number; indexOffset: number; shouldShow?: boolean}>; +type GenerateSectionsProps = Array<{numberOfItems: number; shouldShow?: boolean}>; const generateSections = (sections: GenerateSectionsProps) => - sections.map(({numberOfItems, indexOffset, shouldShow = true}) => ({ + sections.map(({numberOfItems, shouldShow = true}) => ({ data: Array.from({length: numberOfItems}, (v, i) => ({ - text: `Item ${i + indexOffset}`, - keyForList: `item-${i + indexOffset}`, + text: `Item ${i}`, + keyForList: `item-${i}`, })), - indexOffset, shouldShow, })); -const singleSectionsConfig = [{numberOfItems: 1000, indexOffset: 0}]; +const singleSectionsConfig = [{numberOfItems: 1000}]; -const mutlipleSectionsConfig = [ - {numberOfItems: 1000, indexOffset: 0}, - {numberOfItems: 100, indexOffset: 70}, -]; +const mutlipleSectionsConfig = [{numberOfItems: 1000}, {numberOfItems: 100}]; // @ts-expect-error TODO: Remove this once OptionsSelector is migrated to TypeScript. function OptionsSelectorWrapper(args) { const sections = generateSections(singleSectionsConfig); diff --git a/tests/perf-test/ReportActionsList.perf-test.tsx b/tests/perf-test/ReportActionsList.perf-test.tsx index 012d8dc8b90f..a952d149a598 100644 --- a/tests/perf-test/ReportActionsList.perf-test.tsx +++ b/tests/perf-test/ReportActionsList.perf-test.tsx @@ -103,6 +103,8 @@ function ReportActionsListWrapper() { listID={1} loadOlderChats={mockLoadChats} loadNewerChats={mockLoadChats} + transactionThreadReport={LHNTestUtilsModule.getFakeReport()} + reportActions={ReportTestUtils.getMockedSortedReportActions(500)} /> diff --git a/tests/perf-test/SelectionList.perf-test.tsx b/tests/perf-test/SelectionList.perf-test.tsx index ceb54abb5117..a5e783bd4b4d 100644 --- a/tests/perf-test/SelectionList.perf-test.tsx +++ b/tests/perf-test/SelectionList.perf-test.tsx @@ -73,7 +73,6 @@ function SelectionListWrapper({canSelectMultiple}: SelectionListWrapperProps) { keyForList: `item-${index}`, isSelected: selectedIds.includes(`item-${index}`), })), - indexOffset: 0, isDisabled: false, }, ]; diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 2f3e65c0c384..d89c81f58262 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -713,7 +713,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, data: [ { text: 'Food', @@ -746,7 +745,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Food', @@ -771,7 +769,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; @@ -837,7 +834,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, data: [ { text: 'Medical', @@ -852,7 +848,6 @@ describe('OptionsListUtils', () => { { title: 'Recent', shouldShow: true, - indexOffset: 1, data: [ { text: 'Restaurant', @@ -867,7 +862,6 @@ describe('OptionsListUtils', () => { { title: 'All', shouldShow: true, - indexOffset: 2, data: [ { text: 'Cars', @@ -964,7 +958,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Food', @@ -997,7 +990,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; @@ -1006,7 +998,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, data: [ { text: 'Medical', @@ -1111,7 +1102,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, // data sorted alphabetically by name data: [ { @@ -1142,7 +1132,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Accounting', @@ -1158,7 +1147,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; @@ -1212,7 +1200,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Medical', @@ -1226,7 +1213,6 @@ describe('OptionsListUtils', () => { { title: 'Recent', shouldShow: true, - indexOffset: 1, data: [ { text: 'HR', @@ -1240,7 +1226,6 @@ describe('OptionsListUtils', () => { { title: 'All', shouldShow: true, - indexOffset: 2, // data sorted alphabetically by name data: [ { @@ -1299,7 +1284,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Accounting', @@ -1322,7 +1306,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; @@ -2312,7 +2295,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, // data sorted alphabetically by name data: [ { @@ -2365,7 +2347,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, // data sorted alphabetically by name data: [ { @@ -2389,7 +2370,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index 354a90802077..30e6dc738d8b 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -211,7 +211,7 @@ describe('getViolationsOnyxData', () => { }, }, required: true, - orderWeight: 1, + orderWeight: 2, }, Region: { name: 'Region', @@ -222,7 +222,7 @@ describe('getViolationsOnyxData', () => { }, }, required: true, - orderWeight: 2, + orderWeight: 1, }, Project: { name: 'Project', @@ -251,25 +251,25 @@ describe('getViolationsOnyxData', () => { expect(result.value).toEqual([someTagLevelsRequiredViolation]); // Test case where transaction has 1 tag - transaction.tag = 'Accounting'; + transaction.tag = 'Africa'; someTagLevelsRequiredViolation.data = {errorIndexes: [1, 2]}; result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); expect(result.value).toEqual([someTagLevelsRequiredViolation]); // Test case where transaction has 2 tags - transaction.tag = 'Accounting::Project1'; + transaction.tag = 'Africa::Project1'; someTagLevelsRequiredViolation.data = {errorIndexes: [1]}; result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); expect(result.value).toEqual([someTagLevelsRequiredViolation]); // Test case where transaction has all tags - transaction.tag = 'Accounting:Africa:Project1'; + transaction.tag = 'Africa:Accounting:Project1'; result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); expect(result.value).toEqual([]); }); it('should return tagOutOfPolicy when a tag is not enabled in the policy but is set in the transaction', () => { policyTags.Department.tags.Accounting.enabled = false; - transaction.tag = 'Accounting:Africa:Project1'; + transaction.tag = 'Africa:Accounting:Project1'; const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); const violation = {...tagOutOfPolicyViolation, data: {tagName: 'Department'}}; expect(result.value).toEqual([violation]); diff --git a/tests/unit/awaitStagingDeploysTest.js b/tests/unit/awaitStagingDeploysTest.ts similarity index 80% rename from tests/unit/awaitStagingDeploysTest.js rename to tests/unit/awaitStagingDeploysTest.ts index 8b8327e99047..1bda7b0fac23 100644 --- a/tests/unit/awaitStagingDeploysTest.js +++ b/tests/unit/awaitStagingDeploysTest.ts @@ -1,29 +1,48 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + /** * @jest-environment node */ import * as core from '@actions/core'; -import _ from 'underscore'; +import asMutable from '@src/types/utils/asMutable'; import run from '../../.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys'; +import type {InternalOctokit} from '../../.github/libs/GithubUtils'; import GithubUtils from '../../.github/libs/GithubUtils'; +type Workflow = { + workflow_id: string; + branch: string; + owner: string; +}; + +type WorkflowStatus = {status: string}; + // Lower poll rate to speed up tests const TEST_POLL_RATE = 1; -const COMPLETED_WORKFLOW = {status: 'completed'}; -const INCOMPLETE_WORKFLOW = {status: 'in_progress'}; +const COMPLETED_WORKFLOW: WorkflowStatus = {status: 'completed'}; +const INCOMPLETE_WORKFLOW: WorkflowStatus = {status: 'in_progress'}; + +type MockListResponse = { + data: { + workflow_runs: WorkflowStatus[]; + }; +}; + +type MockedFunctionListResponse = jest.MockedFunction<() => Promise>; const consoleSpy = jest.spyOn(console, 'log'); const mockGetInput = jest.fn(); -const mockListPlatformDeploysForTag = jest.fn(); -const mockListPlatformDeploys = jest.fn(); -const mockListPreDeploys = jest.fn(); -const mockListWorkflowRuns = jest.fn().mockImplementation((args) => { +const mockListPlatformDeploysForTag: MockedFunctionListResponse = jest.fn(); +const mockListPlatformDeploys: MockedFunctionListResponse = jest.fn(); +const mockListPreDeploys: MockedFunctionListResponse = jest.fn(); +const mockListWorkflowRuns = jest.fn().mockImplementation((args: Workflow) => { const defaultReturn = Promise.resolve({data: {workflow_runs: []}}); - if (!_.has(args, 'workflow_id')) { + if (!args.workflow_id) { return defaultReturn; } - if (!_.isUndefined(args.branch)) { + if (args.branch !== undefined) { return mockListPlatformDeploysForTag(); } @@ -40,16 +59,18 @@ const mockListWorkflowRuns = jest.fn().mockImplementation((args) => { beforeAll(() => { // Mock core module - core.getInput = mockGetInput; + asMutable(core).getInput = mockGetInput; // Mock octokit module - const moctokit = { + const moctokit: InternalOctokit = { rest: { + // @ts-expect-error This error was removed because getting the rest of the data from internalOctokit makes the test to break actions: { - listWorkflowRuns: mockListWorkflowRuns, + listWorkflowRuns: mockListWorkflowRuns as unknown as typeof GithubUtils.octokit.actions.listWorkflowRuns, }, }, }; + GithubUtils.internalOctokit = moctokit; GithubUtils.POLL_RATE = TEST_POLL_RATE; }); diff --git a/workflow_tests/README.md b/workflow_tests/README.md index 0c491fe6d9e0..b55e2691d13e 100644 --- a/workflow_tests/README.md +++ b/workflow_tests/README.md @@ -246,7 +246,7 @@ const FILES_TO_COPY_INTO_TEST_REPO = [ ]; beforeEach(async () => { - mockGithub = new kieMockGithub.MockGithub({ + mockGithub = new MockGithub({ repo: { testWorkflowsRepo: { files: FILES_TO_COPY_INTO_TEST_REPO, @@ -339,7 +339,7 @@ const FILES_TO_COPY_INTO_TEST_REPO = [ `beforeEach` gets executed before each test. Here we create the local test repository with the files defined in the `FILES_TO_COPY_INTO_TEST_REPO` variable. `testWorkflowRepo` is the name of the test repo and can be changed to whichever name you choose, just remember to use it later when accessing this repo. _Note that we can't use `beforeAll()` method, because while mocking steps `Act-js` modifies the workflow file copied into the test repo and thus mocking could persist between tests_ ```javascript beforeEach(async () => { - mockGithub = new kieMockGithub.MockGithub({ + mockGithub = new MockGithub({ repo: { testWorkflowsRepo: { files: FILES_TO_COPY_INTO_TEST_REPO, diff --git a/workflow_tests/authorChecklist.test.ts b/workflow_tests/authorChecklist.test.ts index c0b1c7cf7533..84509818ff61 100644 --- a/workflow_tests/authorChecklist.test.ts +++ b/workflow_tests/authorChecklist.test.ts @@ -1,6 +1,6 @@ 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 {MockGithub} from '@kie/mock-github'; +import type {CreateRepositoryFile} from '@kie/mock-github'; import path from 'path'; import assertions from './assertions/authorChecklistAssertions'; import mocks from './mocks/authorChecklistMocks'; @@ -30,7 +30,7 @@ describe('test workflow authorChecklist', () => { beforeEach(async () => { // create a local repository and copy required files - mockGithub = new kieMockGithub.MockGithub({ + mockGithub = new MockGithub({ repo: { testAuthorChecklistWorkflowRepo: { files: FILES_TO_COPY_INTO_TEST_REPO, diff --git a/workflow_tests/cherryPick.test.ts b/workflow_tests/cherryPick.test.ts index 47a1c489df70..56ce851755b6 100644 --- a/workflow_tests/cherryPick.test.ts +++ b/workflow_tests/cherryPick.test.ts @@ -1,6 +1,6 @@ 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 {MockGithub} from '@kie/mock-github'; +import type {CreateRepositoryFile} from '@kie/mock-github'; import path from 'path'; import assertions from './assertions/cherryPickAssertions'; import mocks from './mocks/cherryPickMocks'; @@ -29,7 +29,7 @@ describe('test workflow cherryPick', () => { beforeEach(async () => { // create a local repository and copy required files - mockGithub = new kieMockGithub.MockGithub({ + mockGithub = new MockGithub({ repo: { testCherryPickWorkflowRepo: { files: FILES_TO_COPY_INTO_TEST_REPO, diff --git a/workflow_tests/cla.test.ts b/workflow_tests/cla.test.ts index 0203eba865be..31a721305b20 100644 --- a/workflow_tests/cla.test.ts +++ b/workflow_tests/cla.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type {Step} from '@kie/act-js'; -import type {CreateRepositoryFile, MockGithub} from '@kie/mock-github'; -import kieMockGithub from '@kie/mock-github'; +import {MockGithub} from '@kie/mock-github'; +import type {CreateRepositoryFile} from '@kie/mock-github'; import path from 'path'; import {assertCLAJobExecuted} from './assertions/claAssertions'; import {CLA__CLA__CHECK_MATCH__STEP_MOCKS, CLA__CLA__NO_MATCHES__STEP_MOCKS, CLA__CLA__RECHECK_MATCH__STEP_MOCKS} from './mocks/claMocks'; @@ -36,7 +36,7 @@ describe('test workflow cla', () => { beforeEach(async () => { // create a local repository and copy required files - mockGithub = new kieMockGithub.MockGithub({ + mockGithub = new MockGithub({ repo: { testClaWorkflowRepo: { files: FILES_TO_COPY_INTO_TEST_REPO, diff --git a/workflow_tests/failureNotifier.test.ts b/workflow_tests/failureNotifier.test.ts index 8dfc092c7e61..07ebb45517b3 100644 --- a/workflow_tests/failureNotifier.test.ts +++ b/workflow_tests/failureNotifier.test.ts @@ -1,6 +1,6 @@ import type {MockStep} from '@kie/act-js/build/src/step-mocker/step-mocker.types'; -import type {CreateRepositoryFile} from '@kie/mock-github'; import {MockGithub} from '@kie/mock-github'; +import type {CreateRepositoryFile} from '@kie/mock-github'; import path from 'path'; import assertions from './assertions/failureNotifierAssertions'; import mocks from './mocks/failureNotifierMocks'; diff --git a/workflow_tests/lockDeploys.test.ts b/workflow_tests/lockDeploys.test.ts index be739b963cbb..c9b083b4d804 100644 --- a/workflow_tests/lockDeploys.test.ts +++ b/workflow_tests/lockDeploys.test.ts @@ -1,6 +1,6 @@ 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 {MockGithub} from '@kie/mock-github'; +import type {CreateRepositoryFile} from '@kie/mock-github'; import path from 'path'; import assertions from './assertions/lockDeploysAssertions'; import mocks from './mocks/lockDeploysMocks'; @@ -28,7 +28,7 @@ describe('test workflow lockDeploys', () => { beforeEach(async () => { // create a local repository and copy required files - mockGithub = new kieMockGithub.MockGithub({ + mockGithub = new MockGithub({ repo: { testLockDeploysWorkflowRepo: { files: FILES_TO_COPY_INTO_TEST_REPO, diff --git a/workflow_tests/platformDeploy.test.ts b/workflow_tests/platformDeploy.test.ts index 82903b3bbe14..0ac68eb6d55b 100644 --- a/workflow_tests/platformDeploy.test.ts +++ b/workflow_tests/platformDeploy.test.ts @@ -1,6 +1,6 @@ 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 {MockGithub} from '@kie/mock-github'; +import type {CreateRepositoryFile} from '@kie/mock-github'; import path from 'path'; import assertions from './assertions/platformDeployAssertions'; import mocks from './mocks/platformDeployMocks'; @@ -29,7 +29,7 @@ describe('test workflow platformDeploy', () => { beforeEach(async () => { // create a local repository and copy required files - mockGithub = new kieMockGithub.MockGithub({ + mockGithub = new MockGithub({ repo: { testPlatformDeployWorkflowRepo: { files: FILES_TO_COPY_INTO_TEST_REPO, diff --git a/workflow_tests/preDeploy.test.ts b/workflow_tests/preDeploy.test.ts index b75603c5e846..1739fec13815 100644 --- a/workflow_tests/preDeploy.test.ts +++ b/workflow_tests/preDeploy.test.ts @@ -1,6 +1,6 @@ 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 {MockGithub} from '@kie/mock-github'; +import type {CreateRepositoryFile} from '@kie/mock-github'; import path from 'path'; import assertions from './assertions/preDeployAssertions'; import mocks from './mocks/preDeployMocks'; @@ -29,7 +29,7 @@ describe('test workflow preDeploy', () => { beforeEach(async () => { // create a local repository and copy required files - mockGithub = new kieMockGithub.MockGithub({ + mockGithub = new MockGithub({ repo: { testPreDeployWorkflowRepo: { files: FILES_TO_COPY_INTO_TEST_REPO, diff --git a/workflow_tests/reviewerChecklist.test.ts b/workflow_tests/reviewerChecklist.test.ts index 07b325f9d699..d70afd31f115 100644 --- a/workflow_tests/reviewerChecklist.test.ts +++ b/workflow_tests/reviewerChecklist.test.ts @@ -1,6 +1,6 @@ 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 {MockGithub} from '@kie/mock-github'; +import type {CreateRepositoryFile} from '@kie/mock-github'; import path from 'path'; import assertions from './assertions/reviewerChecklistAssertions'; import mocks from './mocks/reviewerChecklistMocks'; @@ -31,7 +31,7 @@ describe('test workflow reviewerChecklist', () => { beforeEach(async () => { // create a local repository and copy required files - mockGithub = new kieMockGithub.MockGithub({ + mockGithub = new MockGithub({ repo: { testReviewerChecklistWorkflowRepo: { files: FILES_TO_COPY_INTO_TEST_REPO, diff --git a/workflow_tests/test.test.ts b/workflow_tests/test.test.ts index 331a657a486c..085a2b3902d6 100644 --- a/workflow_tests/test.test.ts +++ b/workflow_tests/test.test.ts @@ -1,6 +1,6 @@ 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 {MockGithub} from '@kie/mock-github'; +import type {CreateRepositoryFile} from '@kie/mock-github'; import path from 'path'; import assertions from './assertions/testAssertions'; import mocks from './mocks/testMocks'; @@ -32,7 +32,7 @@ describe('test workflow test', () => { beforeEach(async () => { // create a local repository and copy required files - mockGithub = new kieMockGithub.MockGithub({ + mockGithub = new MockGithub({ repo: { testTestWorkflowRepo: { files: FILES_TO_COPY_INTO_TEST_REPO,