diff --git a/.github/.eslintrc.js b/.github/.eslintrc.js index d1f75405f7a..41fc57fb982 100644 --- a/.github/.eslintrc.js +++ b/.github/.eslintrc.js @@ -7,5 +7,6 @@ module.exports = { 'no-await-in-loop': 'off', 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], 'no-continue': 'off', + 'no-restricted-imports': 'off', }, }; diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index 9b4b5dac69f..612a2457d63 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -17017,6 +17017,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index cfbe438022a..7bdbafc0b72 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12258,6 +12258,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js index e0cb48b0a9c..74cd1509fbf 100644 --- a/.github/actions/javascript/checkDeployBlockers/index.js +++ b/.github/actions/javascript/checkDeployBlockers/index.js @@ -11541,6 +11541,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js index d9fa61b0844..6e7237e7cd9 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js @@ -14353,6 +14353,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index c1cca94b6cb..82bf90ef6d2 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -11502,6 +11502,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts b/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts index c15036c9323..b9d01702e66 100644 --- a/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts +++ b/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts @@ -19,26 +19,32 @@ async function run() { // eslint-disable-next-line @typescript-eslint/naming-convention workflow_id: 'platformDeploy.yml', status: 'completed', - event: isProductionDeploy ? 'release' : 'push', }) ).data.workflow_runs // Note: we filter out cancelled runs instead of looking only for success runs // because if a build fails on even one platform, then it will have the status 'failure' .filter((workflowRun) => workflowRun.conclusion !== 'cancelled'); - // Find the most recent deploy workflow for which at least one of the build jobs finished successfully. + // Find the most recent deploy workflow targeting the correct environment, for which at least one of the build jobs finished successfully let lastSuccessfulDeploy = completedDeploys.shift(); while ( - lastSuccessfulDeploy && - !( - await GithubUtils.octokit.actions.listJobsForWorkflowRun({ + lastSuccessfulDeploy?.head_branch && + (( + await GithubUtils.octokit.repos.getReleaseByTag({ owner: github.context.repo.owner, repo: github.context.repo.repo, - // eslint-disable-next-line @typescript-eslint/naming-convention - run_id: lastSuccessfulDeploy.id, - filter: 'latest', + tag: lastSuccessfulDeploy.head_branch, }) - ).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success') + ).data.prerelease === isProductionDeploy || + !( + await GithubUtils.octokit.actions.listJobsForWorkflowRun({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + // eslint-disable-next-line @typescript-eslint/naming-convention + run_id: lastSuccessfulDeploy.id, + filter: 'latest', + }) + ).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success')) ) { console.log(`Deploy was not a success: ${lastSuccessfulDeploy.html_url}, looking at the next one`); lastSuccessfulDeploy = completedDeploys.shift(); diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index 3173dd2358e..05ae086fcc2 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -11514,21 +11514,25 @@ async function run() { // eslint-disable-next-line @typescript-eslint/naming-convention workflow_id: 'platformDeploy.yml', status: 'completed', - event: isProductionDeploy ? 'release' : 'push', })).data.workflow_runs // Note: we filter out cancelled runs instead of looking only for success runs // because if a build fails on even one platform, then it will have the status 'failure' .filter((workflowRun) => workflowRun.conclusion !== 'cancelled'); - // Find the most recent deploy workflow for which at least one of the build jobs finished successfully. + // Find the most recent deploy workflow targeting the correct environment, for which at least one of the build jobs finished successfully let lastSuccessfulDeploy = completedDeploys.shift(); - while (lastSuccessfulDeploy && - !(await GithubUtils_1.default.octokit.actions.listJobsForWorkflowRun({ + while (lastSuccessfulDeploy?.head_branch && + ((await GithubUtils_1.default.octokit.repos.getReleaseByTag({ owner: github.context.repo.owner, repo: github.context.repo.repo, - // eslint-disable-next-line @typescript-eslint/naming-convention - run_id: lastSuccessfulDeploy.id, - filter: 'latest', - })).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success')) { + tag: lastSuccessfulDeploy.head_branch, + })).data.prerelease === isProductionDeploy || + !(await GithubUtils_1.default.octokit.actions.listJobsForWorkflowRun({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + // eslint-disable-next-line @typescript-eslint/naming-convention + run_id: lastSuccessfulDeploy.id, + filter: 'latest', + })).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success'))) { console.log(`Deploy was not a success: ${lastSuccessfulDeploy.html_url}, looking at the next one`); lastSuccessfulDeploy = completedDeploys.shift(); } @@ -11636,6 +11640,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/getPreviousVersion/index.js b/.github/actions/javascript/getPreviousVersion/index.js index aff2a13da16..7b7ff20ef42 100644 --- a/.github/actions/javascript/getPreviousVersion/index.js +++ b/.github/actions/javascript/getPreviousVersion/index.js @@ -2769,6 +2769,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index f1c2054cca1..8580842b380 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -11604,6 +11604,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js index 19acda9b747..9e823e8da5a 100644 --- a/.github/actions/javascript/isStagingDeployLocked/index.js +++ b/.github/actions/javascript/isStagingDeployLocked/index.js @@ -11502,6 +11502,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 06d569d6fb5..9f97e4a72d2 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -6555,6 +6555,1174 @@ function isPlainObject(o) { exports.isPlainObject = isPlainObject; +/***/ }), + +/***/ 5902: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var hashClear = __nccwpck_require__(1789), + hashDelete = __nccwpck_require__(712), + hashGet = __nccwpck_require__(5395), + hashHas = __nccwpck_require__(5232), + hashSet = __nccwpck_require__(7320); + +/** + * Creates a hash object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function Hash(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `Hash`. +Hash.prototype.clear = hashClear; +Hash.prototype['delete'] = hashDelete; +Hash.prototype.get = hashGet; +Hash.prototype.has = hashHas; +Hash.prototype.set = hashSet; + +module.exports = Hash; + + +/***/ }), + +/***/ 6608: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var listCacheClear = __nccwpck_require__(9792), + listCacheDelete = __nccwpck_require__(7716), + listCacheGet = __nccwpck_require__(5789), + listCacheHas = __nccwpck_require__(9386), + listCacheSet = __nccwpck_require__(7399); + +/** + * Creates an list cache object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function ListCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `ListCache`. +ListCache.prototype.clear = listCacheClear; +ListCache.prototype['delete'] = listCacheDelete; +ListCache.prototype.get = listCacheGet; +ListCache.prototype.has = listCacheHas; +ListCache.prototype.set = listCacheSet; + +module.exports = ListCache; + + +/***/ }), + +/***/ 881: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getNative = __nccwpck_require__(4479), + root = __nccwpck_require__(9882); + +/* Built-in method references that are verified to be native. */ +var Map = getNative(root, 'Map'); + +module.exports = Map; + + +/***/ }), + +/***/ 938: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var mapCacheClear = __nccwpck_require__(1610), + mapCacheDelete = __nccwpck_require__(6657), + mapCacheGet = __nccwpck_require__(1372), + mapCacheHas = __nccwpck_require__(609), + mapCacheSet = __nccwpck_require__(5582); + +/** + * Creates a map cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function MapCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `MapCache`. +MapCache.prototype.clear = mapCacheClear; +MapCache.prototype['delete'] = mapCacheDelete; +MapCache.prototype.get = mapCacheGet; +MapCache.prototype.has = mapCacheHas; +MapCache.prototype.set = mapCacheSet; + +module.exports = MapCache; + + +/***/ }), + +/***/ 9213: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var root = __nccwpck_require__(9882); + +/** Built-in value references. */ +var Symbol = root.Symbol; + +module.exports = Symbol; + + +/***/ }), + +/***/ 6752: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var eq = __nccwpck_require__(1901); + +/** + * Gets the index at which the `key` is found in `array` of key-value pairs. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} key The key to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + */ +function assocIndexOf(array, key) { + var length = array.length; + while (length--) { + if (eq(array[length][0], key)) { + return length; + } + } + return -1; +} + +module.exports = assocIndexOf; + + +/***/ }), + +/***/ 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; + + +/***/ }), + +/***/ 411: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var isFunction = __nccwpck_require__(7799), + isMasked = __nccwpck_require__(9058), + isObject = __nccwpck_require__(3334), + toSource = __nccwpck_require__(6928); + +/** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ +var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; + +/** Used to detect host constructors (Safari). */ +var reIsHostCtor = /^\[object .+?Constructor\]$/; + +/** Used for built-in method references. */ +var funcProto = Function.prototype, + objectProto = Object.prototype; + +/** Used to resolve the decompiled source of functions. */ +var funcToString = funcProto.toString; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** Used to detect if a method is native. */ +var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' +); + +/** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ +function baseIsNative(value) { + if (!isObject(value) || isMasked(value)) { + return false; + } + var pattern = isFunction(value) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); +} + +module.exports = baseIsNative; + + +/***/ }), + +/***/ 8380: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var root = __nccwpck_require__(9882); + +/** Used to detect overreaching core-js shims. */ +var coreJsData = root['__core-js_shared__']; + +module.exports = coreJsData; + + +/***/ }), + +/***/ 2085: +/***/ ((module) => { + +/** Detect free variable `global` from Node.js. */ +var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; + +module.exports = freeGlobal; + + +/***/ }), + +/***/ 9980: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var isKeyable = __nccwpck_require__(3308); + +/** + * Gets the data for `map`. + * + * @private + * @param {Object} map The map to query. + * @param {string} key The reference key. + * @returns {*} Returns the map data. + */ +function getMapData(map, key) { + var data = map.__data__; + return isKeyable(key) + ? data[typeof key == 'string' ? 'string' : 'hash'] + : data.map; +} + +module.exports = getMapData; + + +/***/ }), + +/***/ 4479: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var baseIsNative = __nccwpck_require__(411), + getValue = __nccwpck_require__(3542); + +/** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ +function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; +} + +module.exports = getNative; + + +/***/ }), + +/***/ 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; + + +/***/ }), + +/***/ 3542: +/***/ ((module) => { + +/** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ +function getValue(object, key) { + return object == null ? undefined : object[key]; +} + +module.exports = getValue; + + +/***/ }), + +/***/ 1789: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var nativeCreate = __nccwpck_require__(3041); + +/** + * Removes all key-value entries from the hash. + * + * @private + * @name clear + * @memberOf Hash + */ +function hashClear() { + this.__data__ = nativeCreate ? nativeCreate(null) : {}; + this.size = 0; +} + +module.exports = hashClear; + + +/***/ }), + +/***/ 712: +/***/ ((module) => { + +/** + * Removes `key` and its value from the hash. + * + * @private + * @name delete + * @memberOf Hash + * @param {Object} hash The hash to modify. + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function hashDelete(key) { + var result = this.has(key) && delete this.__data__[key]; + this.size -= result ? 1 : 0; + return result; +} + +module.exports = hashDelete; + + +/***/ }), + +/***/ 5395: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var nativeCreate = __nccwpck_require__(3041); + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED = '__lodash_hash_undefined__'; + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Gets the hash value for `key`. + * + * @private + * @name get + * @memberOf Hash + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function hashGet(key) { + var data = this.__data__; + if (nativeCreate) { + var result = data[key]; + return result === HASH_UNDEFINED ? undefined : result; + } + return hasOwnProperty.call(data, key) ? data[key] : undefined; +} + +module.exports = hashGet; + + +/***/ }), + +/***/ 5232: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var nativeCreate = __nccwpck_require__(3041); + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Checks if a hash value for `key` exists. + * + * @private + * @name has + * @memberOf Hash + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function hashHas(key) { + var data = this.__data__; + return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key); +} + +module.exports = hashHas; + + +/***/ }), + +/***/ 7320: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var nativeCreate = __nccwpck_require__(3041); + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED = '__lodash_hash_undefined__'; + +/** + * Sets the hash `key` to `value`. + * + * @private + * @name set + * @memberOf Hash + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the hash instance. + */ +function hashSet(key, value) { + var data = this.__data__; + this.size += this.has(key) ? 0 : 1; + data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; + return this; +} + +module.exports = hashSet; + + +/***/ }), + +/***/ 3308: +/***/ ((module) => { + +/** + * Checks if `value` is suitable for use as unique object key. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is suitable, else `false`. + */ +function isKeyable(value) { + var type = typeof value; + return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') + ? (value !== '__proto__') + : (value === null); +} + +module.exports = isKeyable; + + +/***/ }), + +/***/ 9058: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var coreJsData = __nccwpck_require__(8380); + +/** Used to detect methods masquerading as native. */ +var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; +}()); + +/** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ +function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); +} + +module.exports = isMasked; + + +/***/ }), + +/***/ 9792: +/***/ ((module) => { + +/** + * Removes all key-value entries from the list cache. + * + * @private + * @name clear + * @memberOf ListCache + */ +function listCacheClear() { + this.__data__ = []; + this.size = 0; +} + +module.exports = listCacheClear; + + +/***/ }), + +/***/ 7716: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var assocIndexOf = __nccwpck_require__(6752); + +/** Used for built-in method references. */ +var arrayProto = Array.prototype; + +/** Built-in value references. */ +var splice = arrayProto.splice; + +/** + * Removes `key` and its value from the list cache. + * + * @private + * @name delete + * @memberOf ListCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function listCacheDelete(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + return false; + } + var lastIndex = data.length - 1; + if (index == lastIndex) { + data.pop(); + } else { + splice.call(data, index, 1); + } + --this.size; + return true; +} + +module.exports = listCacheDelete; + + +/***/ }), + +/***/ 5789: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var assocIndexOf = __nccwpck_require__(6752); + +/** + * Gets the list cache value for `key`. + * + * @private + * @name get + * @memberOf ListCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function listCacheGet(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + return index < 0 ? undefined : data[index][1]; +} + +module.exports = listCacheGet; + + +/***/ }), + +/***/ 9386: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var assocIndexOf = __nccwpck_require__(6752); + +/** + * Checks if a list cache value for `key` exists. + * + * @private + * @name has + * @memberOf ListCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function listCacheHas(key) { + return assocIndexOf(this.__data__, key) > -1; +} + +module.exports = listCacheHas; + + +/***/ }), + +/***/ 7399: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var assocIndexOf = __nccwpck_require__(6752); + +/** + * Sets the list cache `key` to `value`. + * + * @private + * @name set + * @memberOf ListCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the list cache instance. + */ +function listCacheSet(key, value) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + ++this.size; + data.push([key, value]); + } else { + data[index][1] = value; + } + return this; +} + +module.exports = listCacheSet; + + +/***/ }), + +/***/ 1610: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var Hash = __nccwpck_require__(5902), + ListCache = __nccwpck_require__(6608), + Map = __nccwpck_require__(881); + +/** + * Removes all key-value entries from the map. + * + * @private + * @name clear + * @memberOf MapCache + */ +function mapCacheClear() { + this.size = 0; + this.__data__ = { + 'hash': new Hash, + 'map': new (Map || ListCache), + 'string': new Hash + }; +} + +module.exports = mapCacheClear; + + +/***/ }), + +/***/ 6657: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getMapData = __nccwpck_require__(9980); + +/** + * Removes `key` and its value from the map. + * + * @private + * @name delete + * @memberOf MapCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function mapCacheDelete(key) { + var result = getMapData(this, key)['delete'](key); + this.size -= result ? 1 : 0; + return result; +} + +module.exports = mapCacheDelete; + + +/***/ }), + +/***/ 1372: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getMapData = __nccwpck_require__(9980); + +/** + * Gets the map value for `key`. + * + * @private + * @name get + * @memberOf MapCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function mapCacheGet(key) { + return getMapData(this, key).get(key); +} + +module.exports = mapCacheGet; + + +/***/ }), + +/***/ 609: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getMapData = __nccwpck_require__(9980); + +/** + * Checks if a map value for `key` exists. + * + * @private + * @name has + * @memberOf MapCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function mapCacheHas(key) { + return getMapData(this, key).has(key); +} + +module.exports = mapCacheHas; + + +/***/ }), + +/***/ 5582: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getMapData = __nccwpck_require__(9980); + +/** + * Sets the map `key` to `value`. + * + * @private + * @name set + * @memberOf MapCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the map cache instance. + */ +function mapCacheSet(key, value) { + var data = getMapData(this, key), + size = data.size; + + data.set(key, value); + this.size += data.size == size ? 0 : 1; + return this; +} + +module.exports = mapCacheSet; + + +/***/ }), + +/***/ 3041: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getNative = __nccwpck_require__(4479); + +/* Built-in method references that are verified to be native. */ +var nativeCreate = getNative(Object, 'create'); + +module.exports = nativeCreate; + + +/***/ }), + +/***/ 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; + + +/***/ }), + +/***/ 6928: +/***/ ((module) => { + +/** Used for built-in method references. */ +var funcProto = Function.prototype; + +/** Used to resolve the decompiled source of functions. */ +var funcToString = funcProto.toString; + +/** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to convert. + * @returns {string} Returns the source code. + */ +function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; +} + +module.exports = toSource; + + +/***/ }), + +/***/ 1901: +/***/ ((module) => { + +/** + * Performs a + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * comparison between two values to determine if they are equivalent. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.eq(object, object); + * // => true + * + * _.eq(object, other); + * // => false + * + * _.eq('a', 'a'); + * // => true + * + * _.eq('a', Object('a')); + * // => false + * + * _.eq(NaN, NaN); + * // => true + */ +function eq(value, other) { + return value === other || (value !== value && other !== other); +} + +module.exports = eq; + + +/***/ }), + +/***/ 7799: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var baseGetTag = __nccwpck_require__(7497), + isObject = __nccwpck_require__(3334); + +/** `Object#toString` result references. */ +var asyncTag = '[object AsyncFunction]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + proxyTag = '[object Proxy]'; + +/** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ +function isFunction(value) { + if (!isObject(value)) { + return false; + } + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 9 which returns 'object' for typed arrays and other constructors. + var tag = baseGetTag(value); + return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag; +} + +module.exports = isFunction; + + +/***/ }), + +/***/ 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; + + +/***/ }), + +/***/ 9885: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var MapCache = __nccwpck_require__(938); + +/** Error message constants. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/** + * Creates a function that memoizes the result of `func`. If `resolver` is + * provided, it determines the cache key for storing the result based on the + * arguments provided to the memoized function. By default, the first argument + * provided to the memoized function is used as the map cache key. The `func` + * is invoked with the `this` binding of the memoized function. + * + * **Note:** The cache is exposed as the `cache` property on the memoized + * function. Its creation may be customized by replacing the `_.memoize.Cache` + * constructor with one whose instances implement the + * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) + * method interface of `clear`, `delete`, `get`, `has`, and `set`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to have its output memoized. + * @param {Function} [resolver] The function to resolve the cache key. + * @returns {Function} Returns the new memoized function. + * @example + * + * var object = { 'a': 1, 'b': 2 }; + * var other = { 'c': 3, 'd': 4 }; + * + * var values = _.memoize(_.values); + * values(object); + * // => [1, 2] + * + * values(other); + * // => [3, 4] + * + * object.a = 2; + * values(object); + * // => [1, 2] + * + * // Modify the result cache. + * values.cache.set(object, ['a', 'b']); + * values(object); + * // => ['a', 'b'] + * + * // Replace `_.memoize.Cache`. + * _.memoize.Cache = WeakMap; + */ +function memoize(func, resolver) { + if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) { + throw new TypeError(FUNC_ERROR_TEXT); + } + var memoized = function() { + var args = arguments, + key = resolver ? resolver.apply(this, args) : args[0], + cache = memoized.cache; + + if (cache.has(key)) { + return cache.get(key); + } + var result = func.apply(this, args); + memoized.cache = cache.set(key, result) || cache; + return result; + }; + memoized.cache = new (memoize.Cache || MapCache); + return memoized; +} + +// Expose `MapCache`. +memoize.Cache = MapCache; + +module.exports = memoize; + + /***/ }), /***/ 467: @@ -11500,6 +12668,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); /* eslint-disable @typescript-eslint/naming-convention, import/no-import-module-exports */ const core = __importStar(__nccwpck_require__(2186)); const github_1 = __nccwpck_require__(5438); +const memoize_1 = __importDefault(__nccwpck_require__(9885)); const ActionUtils = __importStar(__nccwpck_require__(6981)); const CONST_1 = __importDefault(__nccwpck_require__(9873)); const GithubUtils_1 = __importDefault(__nccwpck_require__(9296)); @@ -11535,6 +12704,7 @@ async function commentPR(PR, message) { } } const workflowURL = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; +const getCommit = (0, memoize_1.default)(GithubUtils_1.default.octokit.git.getCommit); async function run() { const prList = ActionUtils.getJSONInput('PR_LIST', { required: true }).map((num) => Number.parseInt(num, 10)); const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', { required: true }); @@ -11573,25 +12743,11 @@ async function run() { } return; } - // First find out if this is a normal staging deploy or a CP by looking at the commit message on the tag const { data: recentTags } = await GithubUtils_1.default.octokit.repos.listTags({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, per_page: 100, }); - const currentTag = recentTags.find((tag) => tag.name === version); - if (!currentTag) { - const err = `Could not find tag matching ${version}`; - console.error(err); - core.setFailed(err); - return; - } - const { data: commit } = await GithubUtils_1.default.octokit.git.getCommit({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - commit_sha: currentTag.commit.sha, - }); - const isCP = /[\S\s]*\(cherry picked from commit .*\)/.test(commit.message); for (const prNumber of prList) { /* * Determine who the deployer for the PR is. The "deployer" for staging deploys is: @@ -11604,7 +12760,28 @@ async function run() { repo: CONST_1.default.APP_REPO, pull_number: prNumber, }); - const deployer = isCP ? commit.committer.name : pr.merged_by?.login; + // Check for the CP Staging label on the issue to see if it was cherry-picked + const isCP = pr.labels.some(({ name: labelName }) => labelName === CONST_1.default.LABELS.CP_STAGING); + // Determine the deployer. For most PRs it will be whoever merged the PR. + // For CPs it will be whoever created the tag for the PR (i.e: whoever triggered the CP) + let deployer = pr.merged_by?.login; + if (isCP) { + for (const tag of recentTags) { + const { data: commit } = await getCommit({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + commit_sha: tag.commit.sha, + }); + const prNumForCPMergeCommit = commit.message.match(/Merge pull request #(\d+)[\S\s]*\(cherry picked from commit .*\)/); + if (prNumForCPMergeCommit?.at(1) === String(prNumber)) { + const cpActor = commit.message.match(/.*\(CP triggered by (.*)\)/)?.at(1); + if (cpActor) { + deployer = cpActor; + } + break; + } + } + } const title = pr.title; const deployMessage = deployer ? getDeployMessage(deployer, isCP ? 'Cherry-picked' : 'Deployed', title) : ''; await commentPR(prNumber, deployMessage); @@ -11709,6 +12886,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts index 53018cbb035..71a5c7d5c6e 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts +++ b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts @@ -2,6 +2,7 @@ import * as core from '@actions/core'; import {context} from '@actions/github'; import type {RequestError} from '@octokit/types'; +import memoize from 'lodash/memoize'; import * as ActionUtils from '@github/libs/ActionUtils'; import CONST from '@github/libs/CONST'; import GithubUtils from '@github/libs/GithubUtils'; @@ -42,6 +43,8 @@ async function commentPR(PR: number, message: string) { const workflowURL = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; +const getCommit = memoize(GithubUtils.octokit.git.getCommit); + async function run() { const prList = (ActionUtils.getJSONInput('PR_LIST', {required: true}) as string[]).map((num) => Number.parseInt(num, 10)); const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', {required: true}) as boolean; @@ -88,25 +91,11 @@ async function run() { return; } - // First find out if this is a normal staging deploy or a CP by looking at the commit message on the tag const {data: recentTags} = await GithubUtils.octokit.repos.listTags({ owner: CONST.GITHUB_OWNER, repo: CONST.APP_REPO, per_page: 100, }); - const currentTag = recentTags.find((tag) => tag.name === version); - if (!currentTag) { - const err = `Could not find tag matching ${version}`; - console.error(err); - core.setFailed(err); - return; - } - const {data: commit} = await GithubUtils.octokit.git.getCommit({ - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - commit_sha: currentTag.commit.sha, - }); - const isCP = /[\S\s]*\(cherry picked from commit .*\)/.test(commit.message); for (const prNumber of prList) { /* @@ -120,7 +109,30 @@ async function run() { repo: CONST.APP_REPO, pull_number: prNumber, }); - const deployer = isCP ? commit.committer.name : pr.merged_by?.login; + + // Check for the CP Staging label on the issue to see if it was cherry-picked + const isCP = pr.labels.some(({name: labelName}) => labelName === CONST.LABELS.CP_STAGING); + + // Determine the deployer. For most PRs it will be whoever merged the PR. + // For CPs it will be whoever created the tag for the PR (i.e: whoever triggered the CP) + let deployer = pr.merged_by?.login; + if (isCP) { + for (const tag of recentTags) { + const {data: commit} = await getCommit({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + commit_sha: tag.commit.sha, + }); + const prNumForCPMergeCommit = commit.message.match(/Merge pull request #(\d+)[\S\s]*\(cherry picked from commit .*\)/); + if (prNumForCPMergeCommit?.at(1) === String(prNumber)) { + const cpActor = commit.message.match(/.*\(CP triggered by (.*)\)/)?.at(1); + if (cpActor) { + deployer = cpActor; + } + break; + } + } + } const title = pr.title; const deployMessage = deployer ? getDeployMessage(deployer, isCP ? 'Cherry-picked' : 'Deployed', title) : ''; diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js index 6c47718584c..4f62879a441 100644 --- a/.github/actions/javascript/postTestBuildComment/index.js +++ b/.github/actions/javascript/postTestBuildComment/index.js @@ -11601,6 +11601,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index 7f7c4ecc38a..c14b825e119 100644 --- a/.github/actions/javascript/proposalPoliceComment/index.js +++ b/.github/actions/javascript/proposalPoliceComment/index.js @@ -18089,6 +18089,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js index 75d40871926..83131f363ef 100644 --- a/.github/actions/javascript/reopenIssueWithComment/index.js +++ b/.github/actions/javascript/reopenIssueWithComment/index.js @@ -11512,6 +11512,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index 0cec1bc183f..2a0977db801 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -11604,6 +11604,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js index 0d441b9f52d..49a4341b84a 100644 --- a/.github/actions/javascript/verifySignedCommits/index.js +++ b/.github/actions/javascript/verifySignedCommits/index.js @@ -11544,6 +11544,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/libs/CONST.ts b/.github/libs/CONST.ts index a46f4afc421..499ff15e5ae 100644 --- a/.github/libs/CONST.ts +++ b/.github/libs/CONST.ts @@ -14,6 +14,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/scripts/verifyPodfile.sh b/.github/scripts/verifyPodfile.sh index 2c9a7dee672..ff67b11c865 100755 --- a/.github/scripts/verifyPodfile.sh +++ b/.github/scripts/verifyPodfile.sh @@ -63,6 +63,7 @@ if ! SPEC_DIRS=$(yq '.["EXTERNAL SOURCES"].[].":path" | select( . == "*node_modu cleanupAndExit 1 fi +# Retrieve a list of podspec paths from react-native config if ! read_lines_into_array PODSPEC_PATHS < <(npx react-native config | jq --raw-output '.dependencies[].platforms.ios.podspecPath | select ( . != null)'); then error "Error: could not parse podspec paths from react-native config command" cleanupAndExit 1 diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml index dd2c92e9556..1772d5d309c 100644 --- a/.github/workflows/cherryPick.yml +++ b/.github/workflows/cherryPick.yml @@ -82,7 +82,6 @@ jobs: id: cherryPick run: | echo "Attempting to cherry-pick ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}" - git config user.name ${{ github.actor }} if git cherry-pick -S -x --mainline 1 ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}; then echo "🎉 No conflicts! CP was a success, PR can be automerged 🎉" echo "HAS_CONFLICTS=false" >> "$GITHUB_OUTPUT" @@ -93,7 +92,7 @@ jobs: GIT_MERGE_AUTOEDIT=no git cherry-pick --continue echo "HAS_CONFLICTS=true" >> "$GITHUB_OUTPUT" fi - git config user.name OSBotify + git commit --amend -m "$(git log -1 --pretty=%B)" -m "(CP triggered by ${{ github.actor }})" - name: Push changes run: | @@ -122,6 +121,11 @@ jobs: env: GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} + - name: Label PR with CP Staging + run: gh pr edit ${{ inputs.PULL_REQUEST_NUMBER }} --add-label 'CP Staging' + env: + GITHUB_TOKEN: ${{ github.token }} + - name: "Announces a CP failure in the #announce Slack room" uses: 8398a7/action-slack@v3 if: ${{ failure() }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3f5a8881f24..a20a5367871 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,11 +23,11 @@ jobs: OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }} OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }} - - name: Tag version - run: git tag "$(npm run print-version --silent)" + - name: Get current app version + run: echo "STAGING_VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" - - name: 🚀 Push tags to trigger staging deploy 🚀 - run: git push --tags + - name: 🚀 Create prerelease to trigger staging deploy 🚀 + run: gh release create ${{ env.STAGING_VERSION }} --title ${{ env.STAGING_VERSION }} --generate-notes --prerelease - name: Warn deployers if staging deploy failed if: ${{ failure() }} @@ -68,8 +68,8 @@ jobs: - name: Get current app version run: echo "PRODUCTION_VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" - - name: 🚀 Create release to trigger production deploy 🚀 - run: gh release create ${{ env.PRODUCTION_VERSION }} --title ${{ env.PRODUCTION_VERSION }} --generate-notes + - name: 🚀 Edit the release to be no longer a prerelease to deploy production 🚀 + run: gh release edit ${{ env.PRODUCTION_VERSION }} --prerelease=false --latest env: GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} diff --git a/.github/workflows/deployBlocker.yml b/.github/workflows/deployBlocker.yml index cb5dc6d28b3..47df9b4285b 100644 --- a/.github/workflows/deployBlocker.yml +++ b/.github/workflows/deployBlocker.yml @@ -39,7 +39,7 @@ jobs: channel: '#expensify-open-source', attachments: [{ color: "#DB4545", - text: '💥 We have found a New Expensify Deploy Blocker, if you have any idea which PR could be causing this, please comment in the issue: <${{ github.event.issue.html_url }}|${{ env.GH_ISSUE_TITLE }}>' + text: '💥 New Deploy Blocker: <${{ github.event.issue.html_url }}|${{ env.GH_ISSUE_TITLE }}>. If you have any idea which PR could be causing this, please comment in the issue.' }] } env: diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 3cb6cd8e5d8..39712293065 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -1,15 +1,12 @@ name: Build and deploy android, desktop, iOS, and web clients -# This workflow is run when any tag is published +# This workflow is run when a release or prerelease is created on: - push: - tags: - - '*' release: - types: [created] + types: [prereleased, released] env: - SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }} + SHOULD_DEPLOY_PRODUCTION: ${{ github.event.action == 'released' }} concurrency: group: ${{ github.workflow }}-${{ github.event_name }} @@ -36,7 +33,7 @@ jobs: deployChecklist: name: Create or update deploy checklist uses: ./.github/workflows/createDeployChecklist.yml - if: ${{ github.event_name != 'release' }} + if: ${{ github.event.action != 'released' }} needs: validateActor secrets: inherit @@ -226,7 +223,7 @@ jobs: with: timeout_minutes: 10 max_attempts: 5 - command: cd ios && bundle exec pod install + command: scripts/pod-install.sh - name: Decrypt AppStore profile run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index 5cfe5e213d2..e5ccdfa5307 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -4,7 +4,7 @@ name: Process new code merged to main on: push: branches: [main] - paths-ignore: [docs/**, contributingGuides/**, jest/**, tests/**, workflow_tests/**] + paths-ignore: [docs/**, contributingGuides/**, jest/**, tests/**] jobs: typecheck: diff --git a/.github/workflows/reactCompiler.yml b/.github/workflows/reactCompiler.yml index 07712fa5e18..dc2e1b17d80 100644 --- a/.github/workflows/reactCompiler.yml +++ b/.github/workflows/reactCompiler.yml @@ -5,6 +5,7 @@ on: paths: - ".github/workflows/reactCompiler.yml" - "src/**" + - "package.json" jobs: check: diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index a0489a52711..d4a25a63952 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -4,7 +4,7 @@ on: pull_request: types: [opened, synchronize] branches-ignore: [staging, production] - paths-ignore: [docs/**, .github/**, contributingGuides/**, tests/**, workflow_tests/**, '**.md', '**.sh'] + paths-ignore: [docs/**, .github/**, contributingGuides/**, tests/**, '**.md', '**.sh'] jobs: perf-tests: diff --git a/.github/workflows/sendReassurePerfData.yml b/.github/workflows/sendReassurePerfData.yml index 30a30918f4f..42d946cece9 100644 --- a/.github/workflows/sendReassurePerfData.yml +++ b/.github/workflows/sendReassurePerfData.yml @@ -3,7 +3,7 @@ name: Send Reassure Performance Tests to Graphite on: push: branches: [main] - paths-ignore: [docs/**, contributingGuides/**, jest/**, workflow_tests/**] + paths-ignore: [docs/**, contributingGuides/**, jest/**] jobs: perf-tests: @@ -36,7 +36,7 @@ jobs: - name: Get and save graphite string id: saveGraphiteString uses: ./.github/actions/javascript/getGraphiteString - with: + with: PR_NUMBER: ${{ steps.getMergedPullRequest.outputs.number }} - name: Send graphite data diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 024f5b712a3..da4225f0e4b 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -184,7 +184,7 @@ jobs: with: timeout_minutes: 10 max_attempts: 5 - command: cd ios && bundle exec pod install --verbose + command: scripts/pod-install.sh - name: Decrypt AdHoc profile run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AdHoc.mobileprovision NewApp_AdHoc.mobileprovision.gpg diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml deleted file mode 100644 index f65319f14be..00000000000 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Test GitHub Actions workflows - -on: - workflow_dispatch: - workflow_call: - pull_request: - types: [opened, reopened, synchronize] - branches-ignore: [staging, production] - paths: ['.github/**'] - -jobs: - testGHWorkflows: - if: ${{ github.actor != 'OSBotify' && github.actor != 'imgbot[bot]' || github.event_name == 'workflow_call' }} - runs-on: ubuntu-latest - env: - CI: true - name: test GitHub Workflows - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup Node - uses: Expensify/App/.github/actions/composite/setupNode@main - - - name: Setup Homebrew - uses: Homebrew/actions/setup-homebrew@master - - - name: Login to GitHub Container Regstry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: OSBotify - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Install Act - run: brew install act - - - name: Set ACT_BINARY - run: echo "ACT_BINARY=$(which act)" >> "$GITHUB_ENV" - - - name: Run tests - run: npm run workflow-test diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 3bfc0ed28d1..32c9e35315b 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -34,7 +34,7 @@ jobs: # - git diff is used to see the files that were added on this branch # - gh pr view is used to list files touched by this PR. Git diff may give false positives if the branch isn't up-to-date with main # - wc counts the words in the result of the intersection - count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' 'workflow_tests/*.js' '.github/libs/*.js' '.github/scripts/*.js' ':!src/libs/SearchParser/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l) + count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' '.github/libs/*.js' '.github/scripts/*.js' ':!src/libs/SearchParser/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l) if [ "$count_new_js" -gt "0" ]; then echo "ERROR: Found new JavaScript files in the project; use TypeScript instead." exit 1 diff --git a/.gitignore b/.gitignore index aa6aad4cc42..e33ec43a01f 100644 --- a/.gitignore +++ b/.gitignore @@ -110,10 +110,6 @@ tsconfig.tsbuildinfo # Mock-github /repo/ -# Workflow test logs -/workflow_tests/logs/ -/workflow_tests/repo/ - # Yalc .yalc yalc.lock diff --git a/android/app/build.gradle b/android/app/build.gradle index 7e41fe1078e..3cea1171ccb 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -108,8 +108,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009001701 - versionName "9.0.17-1" + versionCode 1009001901 + versionName "9.0.19-1" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/cards-and-domains.svg b/assets/images/cards-and-domains.svg deleted file mode 100644 index a6a3918f642..00000000000 --- a/assets/images/cards-and-domains.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/simple-illustrations/advanced-approvals-icon-square.svg b/assets/images/simple-illustrations/advanced-approvals-icon-square.svg new file mode 100644 index 00000000000..00f3de51bd4 --- /dev/null +++ b/assets/images/simple-illustrations/advanced-approvals-icon-square.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 0ddaafda2d8..61baec9d9f1 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -57,7 +57,7 @@ The 168 hours (aka 7 days) will be measured by calculating the time between when A job could be fixing a bug or working on a new feature. There are two ways you can find a job that you can contribute to: #### Finding a job that Expensify posted -This is the most common scenario for contributors. The Expensify team posts new jobs to the Upwork job list [here](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2) (you must be signed in to Upwork to view jobs). Each job in Upwork has a corresponding GitHub issue, which will include instructions to follow. You can also view all open jobs in the Expensify/App GH repository by searching for GH issues with the [`Help Wanted` label](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22). Lastly, you can follow the [@ExpensifyOSS](https://twitter.com/ExpensifyOSS) Twitter account to see a live feed of jobs that are posted. +This is the most common scenario for contributors. The Expensify team posts new jobs to the Upwork job list [here](https://www.upwork.com/nx/search/jobs/?nbs=1&q=expensify%20react%20native&sort=recency&user_location_match=2) (you must be signed in to Upwork to view jobs). Each job in Upwork has a corresponding GitHub issue, which will include instructions to follow. You can also view all open jobs in the Expensify/App GH repository by searching for GH issues with the [`Help Wanted` label](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22). Lastly, you can follow the [@ExpensifyOSS](https://twitter.com/ExpensifyOSS) Twitter account to see a live feed of jobs that are posted. >**Note:** Our problem solving approach at Expensify is to focus on high value problems and avoid small optimizations with results that are difficult to measure. We also prefer to identify and solve problems at their root. Given that, please ensure all proposed jobs fix a specific problem in a measurable way with evidence so they are easy to evaluate. Here's an example of a good problem/solution: > diff --git a/docs/articles/expensify-classic/reports/Create-a-report-approval-workflow.md b/docs/articles/expensify-classic/reports/Create-a-report-approval-workflow.md index f39ffe1a05a..18980377c4c 100644 --- a/docs/articles/expensify-classic/reports/Create-a-report-approval-workflow.md +++ b/docs/articles/expensify-classic/reports/Create-a-report-approval-workflow.md @@ -18,10 +18,13 @@ Expensify allows Workspace Admins to create workflows and automations that deter 5. Select an approval mode. - **Submit and Close**: No approval is required. Once a report is submitted, it will be automatically approved and closed. This option may be useful if your expense approvals occur in another system or if the submitter and approver are the same person. - **Submit and Approve**: All reports go to one person that you assign as the approver. Once a report is submitted, it is sent to the approver. This is the default option. - - **Advanced Approval**: This workflow feature is for companies that require more than one person to approve a report before it can be reimbursed. - Advanced Approval is only available on the Control plan. - + - **Advanced Approval**: Allows for more complex workflows, like assigning different [approvers](https://help.expensify.com/articles/expensify-classic/reports/Assign-report-approvers-to-specific-employees) for different employees or requiring secondary approvals for expenses that exceed a [set limit](https://help.expensify.com/articles/expensify-classic/reports/Require-review-for-over-limit-expenses). To add to your approval workflow, you can also set up approval rules for specific categories and tags. +### Enforce workflow +If you want to ensure your employees cannot override the workflow you set for them, enable workflow enforcement on your workspace’s Members tab. Admins will still be able to “take control” of reports and override the set workflow. + +Visit our How Complex Approval Workflows Work guide for more details. + diff --git a/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md b/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md index 191d49b046d..787602337bd 100644 --- a/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md +++ b/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md @@ -9,7 +9,7 @@ description: Coming Soon When a report exports successfully, a message is posted in the expense’s related chat room: -QBO_help_01 +![Confirmation message posted in the expense chat room](https://help.expensify.com/assets/images/QBO_help_01.png){:width="100%"} ## What happens if I manually export a report that has already been exported? diff --git a/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md b/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md index 1dbf43c4854..b132a75e929 100644 --- a/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md +++ b/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md @@ -17,16 +17,15 @@ An error on a report will prevent it from automatically exporting. To resolve this, open the expense and make the required changes. Then an admin must manually export the report to QuickBooks Online by clicking on Details > Export: -QBO_help_02 +![Click the Export button found in the Details tab](https://help.expensify.com/assets/images/QBO_help_02.png){:width="100%"} -QBO_help_03 +![Select QuickBooks Online in the Export tab](https://help.expensify.com/assets/images/QBO_help_03.png){:width="100%"} ## Unable to manually export a report To export a report, it must be in the Approved, Closed, or Reimbursed state. If it is in the Open state, clicking “Export” will lead to an empty page, as the data is not yet available for export: -QBO_help_04 - +![If the Report is in the Open status, the Not Ready to Export message shows](https://help.expensify.com/assets/images/QBO_help_04.png){:width="100%"} ### How to resolve: diff --git a/docs/articles/new-expensify/connections/xero/Configure-Xero.md b/docs/articles/new-expensify/connections/xero/Configure-Xero.md index 0c65db1b4fd..218e81c9870 100644 --- a/docs/articles/new-expensify/connections/xero/Configure-Xero.md +++ b/docs/articles/new-expensify/connections/xero/Configure-Xero.md @@ -3,4 +3,23 @@ title: Configure Xero description: Coming soon --- -# Coming soon +# FAQ + +## How do I know if a report successfully exported to Xero? + +When a report exports successfully, a message is posted in the related Expensify Chat room. + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_01.png){:width="100%"} + +## What happens if I manually export a report that has already been exported? + +When an admin manually exports a report, Expensify will warn them if the report has already been exported. If the admin chooses to export it again, it will create a duplicate report in Xero. You will need to delete the duplicate entries from within Xero. + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_05.png){:width="100%"} + +## What happens to existing reports that have already been approved and reimbursed if I enable Auto Sync? + +- If Auto Sync was disabled when your Workspace was linked to Xero, enabling it won’t impact existing reports that haven’t been exported. +- If a report has been exported and reimbursed via ACH, it will be automatically marked as paid in Xero during the next sync. +- If a report has been exported and marked as paid in Xero, it will be automatically marked as reimbursed in Expensify during the next sync. +- If a report has not yet been exported to Xero, it won’t be automatically exported. diff --git a/docs/articles/new-expensify/connections/xero/Xero-Troubleshooting b/docs/articles/new-expensify/connections/xero/Xero-Troubleshooting new file mode 100644 index 00000000000..0c69493f393 --- /dev/null +++ b/docs/articles/new-expensify/connections/xero/Xero-Troubleshooting @@ -0,0 +1,37 @@ +--- +title: Xero Troubleshooting +description: A list of common Xero errors and how to resolve them +--- + +## Report won’t automatically export to Xero + +If an error occurs during an automatic export to Xero: + +- You’ll receive an email detailing the error. +- The error will appear in the related Workspace Chat, indicated by a red dot next to the report. +- For auto-sync errors, a message will be posted in the related #admins room. The message contains a link to the workspace’s accounting settings where an explanation for the error appears next to the connection. + +An error on a report will prevent it from automatically exporting. + +## How to resolve + +Open the expense and make the required changes. Then an admin must manually export the report to Xero by clicking the heading at the top of the expense and selecting Export. Then they’ll select Xero. + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_02.png){:width="100%"} + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_03.png){:width="100%"} + +## Unable to manually export a report + +To export a report, it must be in the Approved, Closed, or Reimbursed state. If it is in the Open state, clicking Export will lead to a notification that the data is not yet available for export. + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_04.png){:width="100%"} + +## How to resolve + +Open the report and make the required changes: + +- If the report is in the Open status, ensure that it is submitted. +- If the report is in the Processing status, an admin or approver will need to approve it. + +Once complete, an admin must manually export the report to Xero by clicking the heading at the top of the expense and selecting Export. Then they’ll select Xero. diff --git a/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md index df112259edb..f06c436449e 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md +++ b/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md @@ -10,7 +10,9 @@ To export your expense data to a CSV, 1. Click the **[Search](https://new.expensify.com/search/all?sortBy=date&sortOrder=desc)** tab in the bottom left menu. 2. Select the checkbox to the left of the expenses or reports you wish to export. - 3. Click **# selected** at the top-right and select **Download**. + 3. Click **# selected** at the top-right and select **Download**. + +![Select the expenses to download]({{site.url}}/assets/images/Export-Expenses.png){:width="100%"} The CSV download will save locally to your device with the file naming prefix _“Expensify.”_ This file provides the following data for each expense: - Date diff --git a/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png b/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png new file mode 100644 index 00000000000..3dcf92d028a Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png b/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png new file mode 100644 index 00000000000..cafb106e897 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png b/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png new file mode 100644 index 00000000000..08b55385711 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png differ diff --git a/docs/assets/images/ExpensifyHelp_InviteMembers_1.png b/docs/assets/images/ExpensifyHelp_InviteMembers_1.png new file mode 100644 index 00000000000..cba73c2ce15 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_InviteMembers_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_InviteMembers_2.png b/docs/assets/images/ExpensifyHelp_InviteMembers_2.png new file mode 100644 index 00000000000..e09b8ac5b2b Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_InviteMembers_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_InviteMembers_3.png b/docs/assets/images/ExpensifyHelp_InviteMembers_3.png new file mode 100644 index 00000000000..999e6785ae5 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_InviteMembers_3.png differ diff --git a/docs/assets/images/Xero_help_01.png b/docs/assets/images/Xero_help_01.png new file mode 100644 index 00000000000..ce05ea83c92 Binary files /dev/null and b/docs/assets/images/Xero_help_01.png differ diff --git a/docs/assets/images/Xero_help_02.png b/docs/assets/images/Xero_help_02.png new file mode 100644 index 00000000000..c2d556c7aed Binary files /dev/null and b/docs/assets/images/Xero_help_02.png differ diff --git a/docs/assets/images/Xero_help_03.png b/docs/assets/images/Xero_help_03.png new file mode 100644 index 00000000000..30616ffd3d6 Binary files /dev/null and b/docs/assets/images/Xero_help_03.png differ diff --git a/docs/assets/images/Xero_help_04.png b/docs/assets/images/Xero_help_04.png new file mode 100644 index 00000000000..d0e950d3968 Binary files /dev/null and b/docs/assets/images/Xero_help_04.png differ diff --git a/docs/assets/images/Xero_help_05.png b/docs/assets/images/Xero_help_05.png new file mode 100644 index 00000000000..be65e9c6296 Binary files /dev/null and b/docs/assets/images/Xero_help_05.png differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index c92489f4ce6..b598e018946 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.17 + 9.0.19 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.17.1 + 9.0.19.1 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index f23a6b49271..7b13ec2d2d5 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.17 + 9.0.19 CFBundleSignature ???? CFBundleVersion - 9.0.17.1 + 9.0.19.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 7d5596b7989..dd9ba640b62 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.17 + 9.0.19 CFBundleVersion - 9.0.17.1 + 9.0.19.1 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4776096973b..f5c825e6386 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1871,7 +1871,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.107): + - RNLiveMarkdown (0.1.111): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1889,9 +1889,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.107) + - RNLiveMarkdown/common (= 0.1.111) - Yoga - - RNLiveMarkdown/common (0.1.107): + - RNLiveMarkdown/common (0.1.111): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2614,7 +2614,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: f0c641a0bcf5fdea3ec1bb52a64b30ff88d25c1f + RNLiveMarkdown: cf2707e6050a3548bde4f66bd752d721f91e8ab6 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c RNPermissions: d2392b754e67bc14491f5b12588bef2864e783f3 diff --git a/package-lock.json b/package-lock.json index 960f5eed7f8..527e00a0c5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "9.0.17-1", + "version": "9.0.19-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.17-1", + "version": "9.0.19-1", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "^0.1.107", + "@expensify/react-native-live-markdown": "^0.1.111", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -55,7 +55,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.61", + "expensify-common": "2.0.64", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -95,14 +95,14 @@ "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#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", + "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#93399c6410de32966eb57085936ef6951398c2c3", "react-native-key-command": "^1.0.8", "react-native-keyboard-controller": "^1.12.2", "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.57", + "react-native-onyx": "2.0.64", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -202,7 +202,7 @@ "babel-jest": "29.4.1", "babel-loader": "^9.1.3", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625", + "babel-plugin-react-compiler": "0.0.0-experimental-334f00b-20240725", "babel-plugin-react-native-web": "^0.18.7", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", @@ -221,7 +221,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-jsdoc": "^46.2.6", - "eslint-plugin-react-compiler": "0.0.0-experimental-0998c1e-20240625", + "eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^6.2.2", @@ -241,7 +241,7 @@ "portfinder": "^1.0.28", "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", - "react-compiler-healthcheck": "^0.0.0-experimental-b130d5f-20240625", + "react-compiler-healthcheck": "^0.0.0-experimental-ab3118d-20240725", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", "react-test-renderer": "18.2.0", @@ -3951,9 +3951,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.107", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.107.tgz", - "integrity": "sha512-0Yhqo1azCu3cTmzv/KkILZX2yPiyFUZNRx+AdMdT18pMxpqTAuBtFV4HM44rlimmpT3vgwQ1F/0C0AfRAk5dZA==", + "version": "0.1.111", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.111.tgz", + "integrity": "sha512-oBRKAGA6Cv+e/D+Z5YduKL7jnD0RJC26SSyUDNMfj11Y3snG0ayi4+XKjVtfbEor9Qb/54WxM8QgEAolxcZ7Xg==", "workspaces": [ "parser", "example", @@ -20186,9 +20186,9 @@ } }, "node_modules/babel-plugin-react-compiler": { - "version": "0.0.0-experimental-696af53-20240625", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-696af53-20240625.tgz", - "integrity": "sha512-OUDKms8qmcm5bX0D+sJWC1YcKcd7AZ2aJ7eY6gkR+Xr7PDfkXLbqAld4Qs9B0ntjVbUMEtW/PjlQrxDtY4raHg==", + "version": "0.0.0-experimental-334f00b-20240725", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-334f00b-20240725.tgz", + "integrity": "sha512-ktVKfOtJdHqrLib7IriUe00hnrs585He/n8uzs2yJT9pnH2eyrmMG21aRGBJKxt/P5mdizGLxgyFk0HSMrekhA==", "dev": true, "dependencies": { "@babel/generator": "7.2.0", @@ -25266,9 +25266,9 @@ } }, "node_modules/eslint-plugin-react-compiler": { - "version": "0.0.0-experimental-0998c1e-20240625", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-0.0.0-experimental-0998c1e-20240625.tgz", - "integrity": "sha512-npq2RomExoQI3jETs4OrifaygyJYgOcX/q74Q9OC7GmffLh5zSJaQpzjs2fi61NMNkJyIvTBD0C6sKTGGcetOw==", + "version": "0.0.0-experimental-9ed098e-20240725", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-0.0.0-experimental-9ed098e-20240725.tgz", + "integrity": "sha512-Xv2iD8kU6R4Wdjdh1WhdP8UnSqSV+/XcadxwBCmMr836fQUoXGuw/uVGc01v9opZs9SwKzo+8My6ayVCgAinPA==", "dev": true, "dependencies": { "@babel/core": "^7.24.4", @@ -25942,9 +25942,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.61", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.61.tgz", - "integrity": "sha512-X900glu2M/m2ggF9xlYlrrihNiwYN6cscYi7WmWp1yGzhGe5VFT+w033doJD1I8JLygtkZoV/xVMY4Porexrxw==", + "version": "2.0.64", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.64.tgz", + "integrity": "sha512-+P9+SMPlY799b2l4A3LQ1dle+KvJXcZ01vAFxIDHni4L2Gc1QyddPKLejbwjOrkGqgl3muoR9cwuX/o+QYlYxA==", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", @@ -37266,8 +37266,8 @@ }, "node_modules/react-native-image-size": { "version": "1.1.3", - "resolved": "git+ssh://git@github.com/Expensify/react-native-image-size.git#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", - "license": "MIT" + "resolved": "git+ssh://git@github.com/Expensify/react-native-image-size.git#93399c6410de32966eb57085936ef6951398c2c3", + "integrity": "sha512-hR38DhM3ewEv5VPhyCAbrhgWWlA1Hyys69BdUFkUes2wgiZc2ARVaXoLKuvzYT3g9fNYLwijylaSEs3juDkPKg==" }, "node_modules/react-native-key-command": { "version": "1.0.8", @@ -37345,16 +37345,16 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.57", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.57.tgz", - "integrity": "sha512-+/XndOz9kjCvUAYltq6wJbTsPcof+FZz6eFx0cpu/cDEHaYpjNoPWRKhWgWewg5wTYwu7SWl9aYSShRGVUsZWg==", + "version": "2.0.64", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.64.tgz", + "integrity": "sha512-RFYiEQBFw9610iTGLXIZ1nQMWuf8VyVEMqiRMLpao75+VnbD6lzh0z7Uuj1eoKMDkjeXJhsPP3rh2MkLnqruug==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", "underscore": "^1.13.6" }, "engines": { - "node": ">=20.14.0", + "node": ">=20.15.1", "npm": ">=10.7.0" }, "peerDependencies": { diff --git a/package.json b/package.json index f91731ecf4d..3304bbd4391 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.17-1", + "version": "9.0.19-1", "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.", @@ -14,7 +14,7 @@ "clean": "npx react-native clean-project-auto", "android": "scripts/set-pusher-suffix.sh && npx react-native run-android --mode=developmentDebug --appId=com.expensify.chat.dev --active-arch-only", "ios": "scripts/set-pusher-suffix.sh && npx react-native run-ios --list-devices --mode=\"DebugDevelopment\" --scheme=\"New Expensify Dev\"", - "pod-install": "cd ios && bundle exec pod install", + "pod-install": "scripts/pod-install.sh", "ipad": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (12.9-inch) (6th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", "start": "npx react-native start", @@ -59,8 +59,6 @@ "test:e2e": "ts-node tests/e2e/testRunner.ts --config ./config.local.ts", "test:e2e:dev": "ts-node tests/e2e/testRunner.ts --config ./config.dev.ts", "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", - "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", - "workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.ts", "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1", "e2e-test-runner-build": "node --max-old-space-size=8192 node_modules/.bin/ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/", "react-compiler-healthcheck": "react-compiler-healthcheck --verbose", @@ -71,7 +69,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "^0.1.107", + "@expensify/react-native-live-markdown": "^0.1.111", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -113,7 +111,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.61", + "expensify-common": "2.0.64", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -153,14 +151,14 @@ "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#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", + "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#93399c6410de32966eb57085936ef6951398c2c3", "react-native-key-command": "^1.0.8", "react-native-keyboard-controller": "^1.12.2", "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.57", + "react-native-onyx": "2.0.64", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -260,7 +258,7 @@ "babel-jest": "29.4.1", "babel-loader": "^9.1.3", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625", + "babel-plugin-react-compiler": "0.0.0-experimental-334f00b-20240725", "babel-plugin-react-native-web": "^0.18.7", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", @@ -279,7 +277,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-jsdoc": "^46.2.6", - "eslint-plugin-react-compiler": "0.0.0-experimental-0998c1e-20240625", + "eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^6.2.2", @@ -299,7 +297,7 @@ "portfinder": "^1.0.28", "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", - "react-compiler-healthcheck": "^0.0.0-experimental-b130d5f-20240625", + "react-compiler-healthcheck": "^0.0.0-experimental-ab3118d-20240725", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", "react-test-renderer": "18.2.0", diff --git a/patches/react-native+0.73.4+023+iOS-fix-adjustFontSizeToFit-new-architecture.patch b/patches/react-native+0.73.4+022+iOS-fix-adjustFontSizeToFit-new-architecture.patch similarity index 100% rename from patches/react-native+0.73.4+023+iOS-fix-adjustFontSizeToFit-new-architecture.patch rename to patches/react-native+0.73.4+022+iOS-fix-adjustFontSizeToFit-new-architecture.patch diff --git a/patches/react-native+0.73.4+022+textInputClear.patch b/patches/react-native+0.73.4+022+textInputClear.patch deleted file mode 100644 index 1cadce6a078..00000000000 --- a/patches/react-native+0.73.4+022+textInputClear.patch +++ /dev/null @@ -1,66 +0,0 @@ -diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -index 7ce04da..123968f 100644 ---- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -@@ -452,6 +452,12 @@ - (void)blur - [_backedTextInputView resignFirstResponder]; - } - -+- (void)clear -+{ -+ [self setTextAndSelection:_mostRecentEventCount value:@"" start:0 end:0]; -+ _mostRecentEventCount++; -+} -+ - - (void)setTextAndSelection:(NSInteger)eventCount - value:(NSString *__nullable)value - start:(NSInteger)start -diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h -index fe3376a..6a9a45f 100644 ---- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h -+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h -@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN - @protocol RCTTextInputViewProtocol - - (void)focus; - - (void)blur; -+- (void)clear; - - (void)setTextAndSelection:(NSInteger)eventCount - value:(NSString *__nullable)value - start:(NSInteger)start -@@ -49,6 +50,19 @@ RCTTextInputHandleCommand(id componentView, const NSSt - return; - } - -+ if ([commandName isEqualToString:@"clear"]) { -+#if RCT_DEBUG -+ if ([args count] != 0) { -+ RCTLogError( -+ @"%@ command %@ received %d arguments, expected %d.", @"TextInput", commandName, (int)[args count], 0); -+ return; -+ } -+#endif -+ -+ [componentView clear]; -+ return; -+ } -+ - if ([commandName isEqualToString:@"setTextAndSelection"]) { - #if RCT_DEBUG - if ([args count] != 4) { -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -index 8496a7d..e6bcfc4 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -@@ -331,6 +331,12 @@ public class ReactTextInputManager extends BaseViewManager) => void) + | undefined; + ++ /** ++ * Callback that is called when the text input was cleared using the native clear command. ++ */ ++ onClear?: ++ | ((e: NativeSyntheticEvent) => void) ++ | undefined; ++ + /** + * Callback that is called when the text input's text changes. + */ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +index 481938f..346acaa 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +@@ -1329,6 +1329,11 @@ function InternalTextInput(props: Props): React.Node { + }); + }; + ++ const _onClear = (event: ChangeEvent) => { ++ setMostRecentEventCount(event.nativeEvent.eventCount); ++ props.onClear && props.onClear(event); ++ }; ++ + const _onFocus = (event: FocusEvent) => { + TextInputState.focusInput(inputRef.current); + if (props.onFocus) { +@@ -1462,6 +1467,7 @@ function InternalTextInput(props: Props): React.Node { + nativeID={id ?? props.nativeID} + onBlur={_onBlur} + onKeyPressSync={props.unstable_onKeyPressSync} ++ onClear={_onClear} + onChange={_onChange} + onChangeSync={useOnChangeSync === true ? _onChangeSync : null} + onContentSizeChange={props.onContentSizeChange} +@@ -1516,6 +1522,7 @@ function InternalTextInput(props: Props): React.Node { + nativeID={id ?? props.nativeID} + numberOfLines={props.rows ?? props.numberOfLines} + onBlur={_onBlur} ++ onClear={_onClear} + onChange={_onChange} + onFocus={_onFocus} + /* $FlowFixMe[prop-missing] the types for AndroidTextInput don't match +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +index a19b555..4785987 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +@@ -62,6 +62,7 @@ @implementation RCTBaseTextInputViewManager { + + RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onKeyPressSync, RCTDirectEventBlock) ++RCT_EXPORT_VIEW_PROPERTY(onClear, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onChangeSync, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock) +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +index 7ce04da..70754bf 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm ++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +@@ -452,6 +452,19 @@ - (void)blur + [_backedTextInputView resignFirstResponder]; + } + ++- (void)clear ++{ ++ auto metrics = [self _textInputMetrics]; ++ [self setTextAndSelection:_mostRecentEventCount value:@"" start:0 end:0]; ++ ++ _mostRecentEventCount++; ++ metrics.eventCount = _mostRecentEventCount; ++ ++ // Notify JS that the event counter has changed ++ const auto &textInputEventEmitter = static_cast(*_eventEmitter); ++ textInputEventEmitter.onClear(metrics); ++} ++ + - (void)setTextAndSelection:(NSInteger)eventCount + value:(NSString *__nullable)value + start:(NSInteger)start +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h +index fe3376a..6889eed 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h ++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h +@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN + @protocol RCTTextInputViewProtocol + - (void)focus; + - (void)blur; ++- (void)clear; + - (void)setTextAndSelection:(NSInteger)eventCount + value:(NSString *__nullable)value + start:(NSInteger)start +@@ -49,6 +50,19 @@ RCTTextInputHandleCommand(id componentView, const NSSt + return; + } + ++ if ([commandName isEqualToString:@"clear"]) { ++#if RCT_DEBUG ++ if ([args count] != 0) { ++ RCTLogError( ++ @"%@ command %@ received %d arguments, expected %d.", @"TextInput", commandName, (int)[args count], 0); ++ return; ++ } ++#endif ++ ++ [componentView clear]; ++ return; ++ } ++ + if ([commandName isEqualToString:@"setTextAndSelection"]) { + #if RCT_DEBUG + if ([args count] != 4) { +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextClearEvent.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextClearEvent.java +new file mode 100644 +index 0000000..0c142a0 +--- /dev/null ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextClearEvent.java +@@ -0,0 +1,53 @@ ++/* ++ * Copyright (c) Meta Platforms, Inc. and affiliates. ++ * ++ * This source code is licensed under the MIT license found in the ++ * LICENSE file in the root directory of this source tree. ++ */ ++ ++package com.facebook.react.views.textinput; ++ ++import androidx.annotation.Nullable; ++ ++import com.facebook.react.bridge.Arguments; ++import com.facebook.react.bridge.WritableMap; ++import com.facebook.react.uimanager.common.ViewUtil; ++import com.facebook.react.uimanager.events.Event; ++ ++/** ++ * Event emitted by EditText native view when text changes. VisibleForTesting from {@link ++ * TextInputEventsTestCase}. ++ */ ++public class ReactTextClearEvent extends Event { ++ ++ public static final String EVENT_NAME = "topClear"; ++ ++ private String mText; ++ private int mEventCount; ++ ++ @Deprecated ++ public ReactTextClearEvent(int viewId, String text, int eventCount) { ++ this(ViewUtil.NO_SURFACE_ID, viewId, text, eventCount); ++ } ++ ++ public ReactTextClearEvent(int surfaceId, int viewId, String text, int eventCount) { ++ super(surfaceId, viewId); ++ mText = text; ++ mEventCount = eventCount; ++ } ++ ++ @Override ++ public String getEventName() { ++ return EVENT_NAME; ++ } ++ ++ @Nullable ++ @Override ++ protected WritableMap getEventData() { ++ WritableMap eventData = Arguments.createMap(); ++ eventData.putString("text", mText); ++ eventData.putInt("eventCount", mEventCount); ++ eventData.putInt("target", getViewTag()); ++ return eventData; ++ } ++} +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +index 8496a7d..53e5c49 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +@@ -8,6 +8,7 @@ + package com.facebook.react.views.textinput; + + import static com.facebook.react.uimanager.UIManagerHelper.getReactContext; ++import static com.facebook.react.uimanager.UIManagerHelper.getSurfaceId; + + import android.content.Context; + import android.content.res.ColorStateList; +@@ -273,6 +274,9 @@ public class ReactTextInputManager extends BaseViewManager /dev/null && pwd)")" +cd "$ROOT_DIR" || exit 1 + +# Cleanup and exit +# param - status code +function cleanupAndExit { + cd "$START_DIR" || exit 1 + exit "$1" +} + +source scripts/shellUtils.sh + +# Check if bundle is installed +if ! bundle --version > /dev/null 2>&1; then + error 'bundle is not installed. Please install bundle and try again' + cleanupAndExit 1 +fi + +# Check if jq is installed +if ! jq --version > /dev/null 2>&1; then + error 'jq is not installed. Please install jq and try again' + cleanupAndExit 1 +fi + +# Check if yq is installed +if ! yq --version > /dev/null 2>&1; then + error 'yq is not installed. Please install yq and try again' + cleanupAndExit 1 +fi + +CACHED_PODSPEC_DIR='ios/Pods/Local Podspecs' +if [ -d "$CACHED_PODSPEC_DIR" ]; then + info "Verifying pods from Podfile.lock match local podspecs..." + + # Convert podfile.lock to json since yq is missing some features of jq (namely, if/else) + PODFILE_LOCK_AS_JSON="$(yq -o=json ios/Podfile.lock)" + + # Retrieve a list of pods and their versions from Podfile.lock + declare PODS_FROM_LOCKFILE + if ! read_lines_into_array PODS_FROM_LOCKFILE < <(jq -r '.PODS | map (if (.|type) == "object" then keys[0] else . end) | .[]' < <(echo "$PODFILE_LOCK_AS_JSON")); then + error "Error: Could not parse pod versions from Podfile.lock" + cleanupAndExit 1 + fi + + for CACHED_PODSPEC_PATH in "$CACHED_PODSPEC_DIR"/*; do + if [ -f "$CACHED_PODSPEC_PATH" ]; then + # The next two lines use bash parameter expansion to get just the pod name from the path + # i.e: `ios/Pods/Local Podspecs/hermes-engine.podspec.json` to just `hermes-engine` + # It extracts the part of the string between the last `/` and the first `.` + CACHED_POD_NAME="${CACHED_PODSPEC_PATH##*/}" + CACHED_POD_NAME="${CACHED_POD_NAME%%.*}" + + info "🫛 Verifying local pod $CACHED_POD_NAME" + CACHED_POD_VERSION="$(jq -r '.version' < <(cat "$CACHED_PODSPEC_PATH"))" + for POD_FROM_LOCKFILE in "${PODS_FROM_LOCKFILE[@]}"; do + # Extract the pod name and version that was parsed from the lockfile. POD_FROM_LOCKFILE looks like `PodName (version)` + IFS=' ' read -r POD_NAME_FROM_LOCKFILE POD_VERSION_FROM_LOCKFILE <<< "$POD_FROM_LOCKFILE" + if [[ "$CACHED_POD_NAME" == "$POD_NAME_FROM_LOCKFILE" ]]; then + if [[ "$POD_VERSION_FROM_LOCKFILE" != "($CACHED_POD_VERSION)" ]]; then + clear_last_line + info "⚠️ found mismatched pod: $CACHED_POD_NAME, removing local podspec $CACHED_PODSPEC_PATH" + rm "$CACHED_PODSPEC_PATH" + echo -e "\n" + fi + break + fi + done + clear_last_line + fi + done +fi + +cd ios || cleanupAndExit 1 +bundle exec pod install + +# Go back to where we started +cleanupAndExit 0 diff --git a/scripts/shellUtils.sh b/scripts/shellUtils.sh index fa44f2ee7d3..c1ceace09d0 100644 --- a/scripts/shellUtils.sh +++ b/scripts/shellUtils.sh @@ -41,6 +41,11 @@ function title { printf "\n%s%s%s\n" "$TITLE" "$1" "$RESET" } +# Function to clear the last printed line +clear_last_line() { + echo -ne "\033[1A\033[K" +} + function assert_equal { if [[ "$1" != "$2" ]]; then error "Assertion failed: $1 is not equal to $2" diff --git a/src/CONST.ts b/src/CONST.ts index 8adb5568e0f..57c6da815eb 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -970,6 +970,8 @@ const CONST = { HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', REPORT_INITIAL_RENDER: 'report_initial_render', SWITCH_REPORT: 'switch_report', + SWITCH_REPORT_FROM_PREVIEW: 'switch_report_from_preview', + SWITCH_REPORT_THREAD: 'switch_report_thread', SIDEBAR_LOADED: 'sidebar_loaded', LOAD_SEARCH_OPTIONS: 'load_search_options', COLD: 'cold', @@ -5409,6 +5411,14 @@ const CONST = { description: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}.description` as const, icon: 'IntacctSquare', }, + approvals: { + id: 'approvals' as const, + alias: 'approvals' as const, + name: 'Advanced Approvals' as const, + title: `workspace.upgrade.approvals.title` as const, + description: `workspace.upgrade.approvals.description` as const, + icon: 'AdvancedApprovalsSquare', + }, glCodes: { id: 'glCodes' as const, alias: 'gl-codes', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 40ab87055ca..7102d639638 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -205,6 +205,9 @@ const ONYXKEYS = { /** The end date (epoch timestamp) of the workspace owner’s grace period after the free trial ends. */ NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd', + /** The NVP containing all information related to educational tooltip in workspace chat */ + NVP_WORKSPACE_TOOLTIP: 'workspaceTooltip', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -873,6 +876,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_BILLING_FUND_ID]: number; [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; + [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflow; }; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 8e69e06a310..27f565929c5 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -56,6 +56,7 @@ const ROUTES = { SEARCH_ADVANCED_FILTERS_REPORT_ID: 'search/filters/reportID', SEARCH_ADVANCED_FILTERS_CATEGORY: 'search/filters/category', + SEARCH_ADVANCED_FILTERS_KEYWORD: 'search/filters/keyword', SEARCH_ADVANCED_FILTERS_CARD: 'search/filters/card', SEARCH_REPORT: { @@ -257,7 +258,12 @@ const ROUTES = { }, REPORT_AVATAR: { route: 'r/:reportID/avatar', - getRoute: (reportID: string) => `r/${reportID}/avatar` as const, + getRoute: (reportID: string, policyID?: string) => { + if (policyID) { + return `r/${reportID}/avatar?policyID=${policyID}` as const; + } + return `r/${reportID}/avatar` as const; + }, }, EDIT_CURRENCY_REQUEST: { route: 'r/:threadReportID/edit/currency', @@ -714,7 +720,7 @@ const ROUTES = { }, WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS: { route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation/account', - getRoute: (policyID: string, connection: ValueOf) => + getRoute: (policyID: string, connection?: ValueOf) => `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation/account` as const, }, WORKSPACE_CATEGORIES: { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 20507e4bde2..d125c9658bf 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -40,6 +40,7 @@ const SCREENS = { ADVANCED_FILTERS_MERCHANT_RHP: 'Search_Advanced_Filters_Merchant_RHP', ADVANCED_FILTERS_REPORT_ID_RHP: 'Search_Advanced_Filters_ReportID_RHP', ADVANCED_FILTERS_CATEGORY_RHP: 'Search_Advanced_Filters_Category_RHP', + ADVANCED_FILTERS_KEYWORD_RHP: 'Search_Advanced_Filters_Keyword_RHP', ADVANCED_FILTERS_CARD_RHP: 'Search_Advanced_Filters_Card_RHP', TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP', BOTTOM_TAB: 'Search_Bottom_Tab', diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 0a14d18a232..edcdabed910 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -174,8 +174,10 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s if (isHEIC && targetAssetUri) { manipulateAsync(targetAssetUri, [], {format: SaveFormat.JPEG}) .then((manipResult) => { + const uri = manipResult.uri; const convertedAsset = { - uri: manipResult.uri, + uri, + name: uri.substring(uri.lastIndexOf('/') + 1).split('?')[0], type: 'image/jpeg', width: manipResult.width, height: manipResult.height, diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx index c761faccad3..c2081fa33bd 100644 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx @@ -8,21 +8,21 @@ import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; type TransparentOverlayProps = { - resetSuggestions: () => void; + onPress: () => void; }; type OnPressHandler = PressableProps['onPress']; -function TransparentOverlay({resetSuggestions}: TransparentOverlayProps) { +function TransparentOverlay({onPress: onPressProp}: TransparentOverlayProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const onResetSuggestions = useCallback>( + const onPress = useCallback>( (event) => { event?.preventDefault(); - resetSuggestions(); + onPressProp(); }, - [resetSuggestions], + [onPressProp], ); const handlePointerDown = useCallback((e: PointerEvent) => { @@ -35,7 +35,7 @@ function TransparentOverlay({resetSuggestions}: TransparentOverlayProps) { style={styles.fullScreen} > ({left = 0, width = 0, bottom return ( - + {/* eslint-disable-next-line react/jsx-props-no-spreading */} diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx index d26dd042236..4d322fe15c4 100644 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx @@ -39,7 +39,7 @@ function AutoCompleteSuggestionsPortal({ bodyElement && ReactDOM.createPortal( <> - + {componentToRender} , bodyElement, diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 38bf3912ae4..2ccdd47c320 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -58,12 +58,16 @@ function AvatarWithDisplayName({ const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const title = ReportUtils.getReportName(report); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`); + const [invoiceReceiverPolicy] = useOnyx( + `${ONYXKEYS.COLLECTION.POLICY}${parentReport?.invoiceReceiver && 'policyID' in parentReport.invoiceReceiver ? parentReport.invoiceReceiver.policyID : -1}`, + ); + const title = ReportUtils.getReportName(report, undefined, undefined, undefined, invoiceReceiverPolicy); const subtitle = ReportUtils.getChatRoomSubtitle(report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report) || ReportUtils.isInvoiceReport(report); - const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy); + const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy, invoiceReceiverPolicy); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false); const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(report); diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 1fa40ad1b6f..1eeadde84c4 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -5,10 +5,12 @@ import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PopoverMenu from '@components/PopoverMenu'; +import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import mergeRefs from '@libs/mergeRefs'; import * as Modal from '@userActions/Modal'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; @@ -37,6 +39,7 @@ function ButtonWithDropdownMenu({ onOptionsMenuHide, enterKeyEventListenerPriority = 0, wrapperStyle, + useKeyboardShortcuts = false, }: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -46,6 +49,7 @@ function ButtonWithDropdownMenu({ const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); const {windowWidth, windowHeight} = useWindowDimensions(); const dropdownAnchor = useRef(null); + const dropdownButtonRef = isSplitButton ? buttonRef : mergeRefs(buttonRef, dropdownAnchor); const selectedItem = options[selectedItemIndex] || options[0]; const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize); const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; @@ -70,6 +74,19 @@ function ButtonWithDropdownMenu({ }); } }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]); + + useKeyboardShortcut( + CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, + (e) => { + onPress(e, selectedItem.value); + }, + { + captureOnInputs: true, + shouldBubble: false, + isActive: useKeyboardShortcuts, + }, + ); + return ( {shouldAlwaysShowDropdownMenu || options.length > 1 ? ( @@ -77,12 +94,7 @@ function ButtonWithDropdownMenu({