diff --git a/.github/actions/javascript/bumpVersion/index.js b/.github/actions/javascript/bumpVersion/index.js index c8360931845a..43bd09558c26 100644 --- a/.github/actions/javascript/bumpVersion/index.js +++ b/.github/actions/javascript/bumpVersion/index.js @@ -1928,7 +1928,7 @@ class SemVer { do { const a = this.build[i] const b = other.build[i] - debug('build compare', i, a, b) + debug('prerelease compare', i, a, b) if (a === undefined && b === undefined) { return 0 } else if (b === undefined) { @@ -2154,10 +2154,6 @@ const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || // Max safe segment length for coercion. const MAX_SAFE_COMPONENT_LENGTH = 16 -// Max safe length for a build identifier. The max length minus 6 characters for -// the shortest version with a build 0.0.0+BUILD. -const MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6 - const RELEASE_TYPES = [ 'major', 'premajor', @@ -2171,7 +2167,6 @@ const RELEASE_TYPES = [ module.exports = { MAX_LENGTH, MAX_SAFE_COMPONENT_LENGTH, - MAX_SAFE_BUILD_LENGTH, MAX_SAFE_INTEGER, RELEASE_TYPES, SEMVER_SPEC_VERSION, @@ -2253,11 +2248,7 @@ module.exports = parseOptions /***/ 9523: /***/ ((module, exports, __nccwpck_require__) => { -const { - MAX_SAFE_COMPONENT_LENGTH, - MAX_SAFE_BUILD_LENGTH, - MAX_LENGTH, -} = __nccwpck_require__(2293) +const { MAX_SAFE_COMPONENT_LENGTH } = __nccwpck_require__(2293) const debug = __nccwpck_require__(427) exports = module.exports = {} @@ -2268,31 +2259,16 @@ const src = exports.src = [] const t = exports.t = {} let R = 0 -const LETTERDASHNUMBER = '[a-zA-Z0-9-]' - -// Replace some greedy regex tokens to prevent regex dos issues. These regex are -// used internally via the safeRe object since all inputs in this library get -// normalized first to trim and collapse all extra whitespace. The original -// regexes are exported for userland consumption and lower level usage. A -// future breaking change could export the safer regex only with a note that -// all input should have extra whitespace removed. -const safeRegexReplacements = [ - ['\\s', 1], - ['\\d', MAX_LENGTH], - [LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH], -] - -const makeSafeRegex = (value) => { - for (const [token, max] of safeRegexReplacements) { - value = value - .split(`${token}*`).join(`${token}{0,${max}}`) - .split(`${token}+`).join(`${token}{1,${max}}`) - } - return value -} - const createToken = (name, value, isGlobal) => { - const safe = makeSafeRegex(value) + // Replace all greedy whitespace to prevent regex dos issues. These regex are + // used internally via the safeRe object since all inputs in this library get + // normalized first to trim and collapse all extra whitespace. The original + // regexes are exported for userland consumption and lower level usage. A + // future breaking change could export the safer regex only with a note that + // all input should have extra whitespace removed. + const safe = value + .split('\\s*').join('\\s{0,1}') + .split('\\s+').join('\\s') const index = R++ debug(name, index, value) t[name] = index @@ -2308,13 +2284,13 @@ const createToken = (name, value, isGlobal) => { // A single `0`, or a non-zero digit followed by zero or more digits. createToken('NUMERICIDENTIFIER', '0|[1-9]\\d*') -createToken('NUMERICIDENTIFIERLOOSE', '\\d+') +createToken('NUMERICIDENTIFIERLOOSE', '[0-9]+') // ## Non-numeric Identifier // Zero or more digits, followed by a letter or hyphen, and then zero or // more letters, digits, or hyphens. -createToken('NONNUMERICIDENTIFIER', `\\d*[a-zA-Z-]${LETTERDASHNUMBER}*`) +createToken('NONNUMERICIDENTIFIER', '\\d*[a-zA-Z-][a-zA-Z0-9-]*') // ## Main Version // Three dot-separated numeric identifiers. @@ -2349,7 +2325,7 @@ createToken('PRERELEASELOOSE', `(?:-?(${src[t.PRERELEASEIDENTIFIERLOOSE] // ## Build Metadata Identifier // Any combination of digits, letters, or hyphens. -createToken('BUILDIDENTIFIER', `${LETTERDASHNUMBER}+`) +createToken('BUILDIDENTIFIER', '[0-9A-Za-z-]+') // ## Build Metadata // Plus sign, followed by one or more period-separated build metadata @@ -2409,17 +2385,12 @@ createToken('XRANGELOOSE', `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAINLOOSE]}$`) // Coercion. // Extract anything that could conceivably be a part of a valid semver -createToken('COERCEPLAIN', `${'(^|[^\\d])' + +createToken('COERCE', `${'(^|[^\\d])' + '(\\d{1,'}${MAX_SAFE_COMPONENT_LENGTH}})` + `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` + - `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?`) -createToken('COERCE', `${src[t.COERCEPLAIN]}(?:$|[^\\d])`) -createToken('COERCEFULL', src[t.COERCEPLAIN] + - `(?:${src[t.PRERELEASE]})?` + - `(?:${src[t.BUILD]})?` + + `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` + `(?:$|[^\\d])`) createToken('COERCERTL', src[t.COERCE], true) -createToken('COERCERTLFULL', src[t.COERCEFULL], true) // Tilde ranges. // Meaning is "reasonably at or greater than" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6ef9fe299510..2ab19d13183a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -455,34 +455,40 @@ jobs: checkDeploymentSuccess: runs-on: ubuntu-latest outputs: - IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }} - IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED }} + IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }} + IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastAllPlatform.outputs.IS_ALL_PLATFORMS_DEPLOYED }} needs: [android, desktop, iOS, web] if: ${{ always() }} steps: - name: Check deployment success on at least one platform - id: checkDeploymentSuccess + id: checkDeploymentSuccessOnAtLeastOnePlatform run: | isAtLeastOnePlatformDeployed="false" - isAllPlatformsDeployed="false" if [ "${{ needs.android.result }}" == "success" ] || \ [ "${{ needs.iOS.result }}" == "success" ] || \ [ "${{ needs.desktop.result }}" == "success" ] || \ [ "${{ needs.web.result }}" == "success" ]; then isAtLeastOnePlatformDeployed="true" fi + echo "IS_AT_LEAST_ONE_PLATFORM_DEPLOYED=$isAtLeastOnePlatformDeployed" >> "$GITHUB_OUTPUT" + echo "IS_AT_LEAST_ONE_PLATFORM_DEPLOYED is $isAtLeastOnePlatformDeployed" + + - name: Check deployment success on all platforms + id: checkDeploymentSuccessOnAtLeastAllPlatform + run: | + isAllPlatformsDeployed="false" if [ "${{ needs.android.result }}" == "success" ] && \ [ "${{ needs.iOS.result }}" == "success" ] && \ [ "${{ needs.desktop.result }}" == "success" ] && \ [ "${{ needs.web.result }}" == "success" ]; then isAllPlatformsDeployed="true" fi - echo "IS_AT_LEAST_ONE_PLATFORM_DEPLOYED=\"$isAtLeastOnePlatformDeployed\"" >> "$GITHUB_OUTPUT" - echo "IS_ALL_PLATFORMS_DEPLOYED=\"$isAllPlatformsDeployed\"" >> "$GITHUB_OUTPUT" + echo "IS_ALL_PLATFORMS_DEPLOYED=$isAllPlatformsDeployed" >> "$GITHUB_OUTPUT" + echo "IS_ALL_PLATFORMS_DEPLOYED is $isAllPlatformsDeployed" createPrerelease: runs-on: ubuntu-latest - if: ${{ github.ref == 'refs/heads/staging' && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} + if: ${{ always() && github.ref == 'refs/heads/staging' && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} needs: [prep, checkDeploymentSuccess] steps: - name: Download all workflow run artifacts @@ -540,7 +546,7 @@ jobs: finalizeRelease: runs-on: ubuntu-latest - if: ${{ github.ref == 'refs/heads/production' && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} + if: ${{ always() && github.ref == 'refs/heads/production' && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} needs: [prep, checkDeploymentSuccess] steps: - name: Download all workflow run artifacts diff --git a/android/app/build.gradle b/android/app/build.gradle index 75775aca06de..5b5f9d40d4df 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009003800 - versionName "9.0.38-0" + versionCode 1009003900 + versionName "9.0.39-0" // 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/babel.config.js b/babel.config.js index 9d7224ec4436..3721edaa7afb 100644 --- a/babel.config.js +++ b/babel.config.js @@ -8,7 +8,12 @@ const ReactCompilerConfig = { enableTreatRefLikeIdentifiersAsRefs: true, }, }; -const defaultPresets = ['@babel/preset-react', '@babel/preset-env', '@babel/preset-flow', '@babel/preset-typescript']; +/** + * Setting targets to node 20 to reduce JS bundle size + * It is also recommended by babel: + * https://babeljs.io/docs/options#no-targets + */ +const defaultPresets = ['@babel/preset-react', ['@babel/preset-env', {targets: {node: 20}}], '@babel/preset-flow', '@babel/preset-typescript']; const defaultPlugins = [ ['babel-plugin-react-compiler', ReactCompilerConfig], // must run first! // Adding the commonjs: true option to react-native-web plugin can cause styling conflicts diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index 0640d007b960..1bab57905d0e 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -128,6 +128,13 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/, }), + ...(file === '.env.production' || file === '.env.staging' + ? [ + new IgnorePlugin({ + resourceRegExp: /@welldone-software\/why-did-you-render/, + }), + ] + : []), ...(platform === 'web' ? [new CustomVersionFilePlugin()] : []), new DefinePlugin({ ...(platform === 'desktop' ? {} : {process: {env: {}}}), @@ -215,6 +222,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): }, resolve: { alias: { + lodash: 'lodash-es', // eslint-disable-next-line @typescript-eslint/naming-convention 'react-native-config': 'react-web-config', // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md index d3dcda91ffcc..917c3c007b28 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md @@ -42,6 +42,10 @@ The following steps help you determine the advanced settings for your connection 1. Click **Advanced** under the QuickBooks Desktop connection. 2. **Enable or disable Auto-Sync**: If enabled, QuickBooks Desktop automatically communicates changes with Expensify to ensure that the data shared between the two systems is up to date. New report approvals/reimbursements will be synced during the next auto-sync period. +{% include info.html %} +Please note that Auto-Sync will queue information to be added to your QuickBooks Company File the next time both your QuickBooks Company File and QuickBooks Web Connector are open. +{% include end-info.html %} + # FAQ ## **How do I manually sync my QuickBooks Desktop if I have Auto-Sync disabled?** diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md index 09afd2e4e7f2..06f894ce7ef6 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md @@ -89,3 +89,15 @@ If the problem persists, download the QuickBooks Desktop log file via the Web Co {% include info.html %} If you’re using a remote server (e.g. RightNetworks), you may need to contact that support team to request your logs. {% include end-info.html %} + +# Reports not exporting to QuickBooks Desktop + +Generally, this is the result of not having both the QuickBooks Web Connector and the Company File open, since the Report was exported. + +## How to resolve + +1. Make sure that the Web Connector and QuickBooks Desktop Company File are both open. +2. In the Web Connector, check that the Last Status is “Ok”. +3. Check the Report Comments in Expensify to confirm that the report has been successfully exported to QuickBooks Desktop. + +If these general troubleshooting steps don’t work, reach out to Concierge with your Expensify Report ID and a screenshot of your QuickBooks Web Connector. diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 1d7be504fd51..bfea43969e01 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.38 + 9.0.39 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.38.0 + 9.0.39.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index e4243d03b774..2f8941238a2d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.38 + 9.0.39 CFBundleSignature ???? CFBundleVersion - 9.0.38.0 + 9.0.39.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 1133bfbb4a0e..2b639ac81226 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.38 + 9.0.39 CFBundleVersion - 9.0.38.0 + 9.0.39.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 42b4720432ac..a5ae221a1e14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.38-0", + "version": "9.0.39-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.38-0", + "version": "9.0.39-0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -42,7 +42,6 @@ "@rnmapbox/maps": "10.1.30", "@shopify/flash-list": "1.7.1", "@ua/react-native-airship": "19.2.1", - "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", "babel-polyfill": "^6.26.0", "canvas-size": "^1.2.6", @@ -60,9 +59,7 @@ "focus-trap-react": "^10.2.3", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", - "jest-expo": "51.0.4", - "jest-when": "^3.5.2", - "lodash": "4.17.21", + "lodash-es": "4.17.21", "lottie-react-native": "6.5.1", "mapbox-gl": "^2.15.0", "onfido-sdk-ui": "14.15.0", @@ -71,7 +68,6 @@ "react": "18.3.1", "react-beautiful-dnd": "^13.1.1", "react-collapse": "^5.1.0", - "react-compiler-runtime": "file:./lib/react-compiler-runtime", "react-content-loader": "^7.0.0", "react-dom": "18.3.1", "react-error-boundary": "^4.0.11", @@ -125,8 +121,7 @@ "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", - "semver": "^7.5.2", - "xlsx": "file:vendor/xlsx-0.20.3.tgz" + "react-window": "^1.8.9" }, "devDependencies": { "@actions/core": "1.10.0", @@ -149,7 +144,10 @@ "@dword-design/eslint-plugin-import-alias": "^5.0.0", "@electron/notarize": "^2.1.0", "@fullstory/babel-plugin-annotate-react": "^2.3.0", + "@fullstory/babel-plugin-react-native": "^1.2.1", "@jest/globals": "^29.5.0", + "@kie/act-js": "^2.6.2", + "@kie/mock-github": "2.0.1", "@ngneat/falso": "^7.1.1", "@octokit/core": "4.0.4", "@octokit/plugin-paginate-rest": "3.1.0", @@ -179,7 +177,7 @@ "@types/jest": "^29.5.2", "@types/jest-when": "^3.5.2", "@types/js-yaml": "^4.0.5", - "@types/lodash": "^4.14.195", + "@types/lodash-es": "4.17.12", "@types/mapbox-gl": "^2.7.13", "@types/mime-db": "^1.43.5", "@types/node": "^20.11.5", @@ -198,6 +196,7 @@ "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", "@vercel/ncc": "0.38.1", + "@vue/preload-webpack-plugin": "^2.0.0", "@welldone-software/why-did-you-render": "7.0.1", "ajv-cli": "^5.0.0", "babel-jest": "29.4.1", @@ -233,7 +232,9 @@ "jest-circus": "29.4.1", "jest-cli": "29.4.1", "jest-environment-jsdom": "^29.4.1", + "jest-expo": "51.0.4", "jest-transformer-svg": "^2.0.1", + "jest-when": "^3.5.2", "link": "^2.1.1", "memfs": "^4.6.0", "onchange": "^7.1.0", @@ -244,10 +245,12 @@ "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", "react-compiler-healthcheck": "^0.0.0-experimental-ab3118d-20240725", + "react-compiler-runtime": "file:./lib/react-compiler-runtime", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", "react-test-renderer": "18.3.1", "reassure": "^1.0.0-rc.4", + "semver": "7.5.2", "setimmediate": "^1.0.5", "shellcheck": "^1.1.0", "source-map": "^0.7.4", @@ -264,7 +267,8 @@ "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^5.0.4", "webpack-dev-server": "^5.0.4", - "webpack-merge": "^5.8.0" + "webpack-merge": "^5.8.0", + "xlsx": "file:vendor/xlsx-0.20.3.tgz" }, "engines": { "node": "20.15.1", @@ -273,6 +277,7 @@ }, "lib/react-compiler-runtime": { "version": "0.0.1", + "dev": true, "license": "MIT", "dependencies": { "react": "18.3.1" @@ -1440,6 +1445,7 @@ }, "node_modules/@babel/plugin-syntax-bigint": { "version": "7.8.3", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -2837,6 +2843,7 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", + "dev": true, "license": "MIT" }, "node_modules/@blakeembrey/deque": { @@ -3031,7 +3038,7 @@ }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -3042,7 +3049,7 @@ }, "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -4100,6 +4107,17 @@ "node": ">=4" } }, + "node_modules/@expo/cli/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/cli/node_modules/strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -4247,6 +4265,17 @@ "node": ">=8" } }, + "node_modules/@expo/config-plugins/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/config-plugins/node_modules/supports-color": { "version": "7.2.0", "license": "MIT", @@ -4345,6 +4374,17 @@ "node": ">=8" } }, + "node_modules/@expo/config/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/config/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4634,6 +4674,17 @@ "node": ">=8" } }, + "node_modules/@expo/image-utils/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/image-utils/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5261,6 +5312,17 @@ "node": ">=8" } }, + "node_modules/@expo/prebuild-config/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/prebuild-config/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5772,6 +5834,7 @@ }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", + "dev": true, "license": "ISC", "dependencies": { "camelcase": "^5.3.1", @@ -5786,6 +5849,7 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { "version": "5.3.1", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5793,6 +5857,7 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { "version": "4.1.0", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -5804,6 +5869,7 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { "version": "5.0.0", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -5814,6 +5880,7 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { "version": "2.3.0", + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -5827,6 +5894,7 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { "version": "4.1.0", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -5837,6 +5905,7 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/path-exists": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5844,6 +5913,7 @@ }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5851,6 +5921,7 @@ }, "node_modules/@jest/console": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -5866,6 +5937,7 @@ }, "node_modules/@jest/console/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5879,6 +5951,7 @@ }, "node_modules/@jest/console/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -5893,6 +5966,7 @@ }, "node_modules/@jest/console/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5903,10 +5977,12 @@ }, "node_modules/@jest/console/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/@jest/console/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5914,6 +5990,7 @@ }, "node_modules/@jest/console/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -5924,6 +6001,7 @@ }, "node_modules/@jest/core": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -5969,6 +6047,7 @@ }, "node_modules/@jest/core/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5982,6 +6061,7 @@ }, "node_modules/@jest/core/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -5996,6 +6076,7 @@ }, "node_modules/@jest/core/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -6006,10 +6087,12 @@ }, "node_modules/@jest/core/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/@jest/core/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6017,6 +6100,7 @@ }, "node_modules/@jest/core/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -6050,6 +6134,7 @@ }, "node_modules/@jest/expect": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "expect": "^29.7.0", @@ -6061,6 +6146,7 @@ }, "node_modules/@jest/expect-utils": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3" @@ -6086,6 +6172,7 @@ }, "node_modules/@jest/globals": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -6099,6 +6186,7 @@ }, "node_modules/@jest/reporters": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -6140,6 +6228,7 @@ }, "node_modules/@jest/reporters/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -6153,6 +6242,7 @@ }, "node_modules/@jest/reporters/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -6167,6 +6257,7 @@ }, "node_modules/@jest/reporters/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -6177,10 +6268,12 @@ }, "node_modules/@jest/reporters/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/@jest/reporters/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6188,6 +6281,7 @@ }, "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { "version": "6.0.2", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", @@ -6202,6 +6296,7 @@ }, "node_modules/@jest/reporters/node_modules/jest-worker": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -6215,6 +6310,7 @@ }, "node_modules/@jest/reporters/node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -6226,8 +6322,21 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/@jest/reporters/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@jest/reporters/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -6248,6 +6357,7 @@ }, "node_modules/@jest/source-map": { "version": "29.6.3", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", @@ -6260,6 +6370,7 @@ }, "node_modules/@jest/test-result": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -6273,6 +6384,7 @@ }, "node_modules/@jest/test-sequencer": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -6286,6 +6398,7 @@ }, "node_modules/@jest/transform": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -6310,6 +6423,7 @@ }, "node_modules/@jest/transform/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -6323,6 +6437,7 @@ }, "node_modules/@jest/transform/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -6337,6 +6452,7 @@ }, "node_modules/@jest/transform/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -6347,10 +6463,12 @@ }, "node_modules/@jest/transform/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/@jest/transform/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6358,6 +6476,7 @@ }, "node_modules/@jest/transform/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -6368,6 +6487,7 @@ }, "node_modules/@jest/transform/node_modules/write-file-atomic": { "version": "4.0.2", + "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -6520,6 +6640,56 @@ "react-native": "*" } }, + "node_modules/@kie/act-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@kie/act-js/-/act-js-2.6.2.tgz", + "integrity": "sha512-i366cfWluUi55rPZ6e9/aWH4tnw3Q6W1CKh9Gz6QjTvbAtS4KnUUy33I9aMXS6uwa0haw6MSahMM37vmuFCVpQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@kie/mock-github": "^2.0.0", + "adm-zip": "^0.5.10", + "ajv": "^8.12.0", + "bin-links": "^4.0.1", + "express": "^4.18.1", + "follow-redirects": "^1.15.2", + "tar": "^6.1.13", + "yaml": "^2.1.3" + }, + "bin": { + "act-js": "bin/act" + } + }, + "node_modules/@kie/mock-github": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@kie/mock-github/-/mock-github-2.0.1.tgz", + "integrity": "sha512-G1FD/jg1KyW7a6NvKI4uEVJCK3eJnzXkh4Ikxn2is5tiNC980lavi8ak6bn1QEFEgpYcfM4DpZM3yHDfOmyLuQ==", + "dev": true, + "dependencies": { + "@octokit/openapi-types-ghec": "^18.0.0", + "ajv": "^8.11.0", + "express": "^4.18.1", + "fast-glob": "^3.2.12", + "fs-extra": "^10.1.0", + "nock": "^13.2.7", + "simple-git": "^3.8.0", + "totalist": "^3.0.0" + } + }, + "node_modules/@kie/mock-github/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "dev": true, @@ -6939,6 +7109,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@octokit/openapi-types-ghec": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types-ghec/-/openapi-types-ghec-18.1.1.tgz", + "integrity": "sha512-5Ri7FLYX4gJSdG+G0Q8QDca/gOLfkPN4YR2hkbVg6hEL+0N62MIsJPTyNaT9pGEXCLd1KbYV6Lh3T2ggsmyBJw==", + "dev": true + }, "node_modules/@octokit/plugin-paginate-rest": { "version": "3.1.0", "dev": true, @@ -13218,6 +13394,18 @@ "node": ">= 4" } }, + "node_modules/@storybook/core/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@storybook/core/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -15207,22 +15395,22 @@ }, "node_modules/@tsconfig/node10": { "version": "1.0.9", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@turf/along": { @@ -15366,6 +15554,7 @@ }, "node_modules/@types/babel__core": { "version": "7.20.5", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -15377,6 +15566,7 @@ }, "node_modules/@types/babel__generator": { "version": "7.6.4", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -15384,6 +15574,7 @@ }, "node_modules/@types/babel__template": { "version": "7.4.1", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -15392,6 +15583,7 @@ }, "node_modules/@types/babel__traverse": { "version": "7.18.0", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.3.0" @@ -15543,6 +15735,7 @@ }, "node_modules/@types/graceful-fs": { "version": "4.1.9", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -15570,6 +15763,7 @@ }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", + "dev": true, "license": "MIT" }, "node_modules/@types/http-cache-semantics": { @@ -15632,6 +15826,7 @@ }, "node_modules/@types/jsdom": { "version": "20.0.1", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -15641,6 +15836,7 @@ }, "node_modules/@types/jsdom/node_modules/entities": { "version": "4.5.0", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -15651,6 +15847,7 @@ }, "node_modules/@types/jsdom/node_modules/parse5": { "version": "7.1.2", + "dev": true, "license": "MIT", "dependencies": { "entities": "^4.4.0" @@ -15681,6 +15878,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mapbox-gl": { "version": "2.7.13", "license": "MIT", @@ -15949,6 +16155,7 @@ }, "node_modules/@types/tough-cookie": { "version": "4.0.2", + "dev": true, "license": "MIT" }, "node_modules/@types/unist": { @@ -16279,6 +16486,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@typescript-eslint/rule-tester/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "7.13.1", "dev": true, @@ -16403,6 +16622,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/utils": { "version": "5.62.0", "dev": true, @@ -16587,6 +16818,7 @@ }, "node_modules/@vue/preload-webpack-plugin": { "version": "2.0.0", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=6.0.0" @@ -16842,6 +17074,7 @@ }, "node_modules/abab": { "version": "2.0.6", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/abbrev": { @@ -16883,6 +17116,7 @@ }, "node_modules/acorn-globals": { "version": "7.0.1", + "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.1.0", @@ -16891,6 +17125,7 @@ }, "node_modules/acorn-globals/node_modules/acorn": { "version": "8.11.3", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -16901,6 +17136,7 @@ }, "node_modules/acorn-globals/node_modules/acorn-walk": { "version": "8.3.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -16922,6 +17158,15 @@ "node": ">=0.4.0" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "dev": true, + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "license": "MIT", @@ -17387,7 +17632,7 @@ }, "node_modules/arg": { "version": "4.1.3", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -17739,6 +17984,7 @@ }, "node_modules/babel-jest": { "version": "29.4.1", + "dev": true, "license": "MIT", "dependencies": { "@jest/transform": "^29.4.1", @@ -17758,6 +18004,7 @@ }, "node_modules/babel-jest/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -17771,6 +18018,7 @@ }, "node_modules/babel-jest/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -17785,6 +18033,7 @@ }, "node_modules/babel-jest/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -17795,10 +18044,12 @@ }, "node_modules/babel-jest/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/babel-jest/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17806,6 +18057,7 @@ }, "node_modules/babel-jest/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -17971,6 +18223,7 @@ }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -17985,6 +18238,7 @@ }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", @@ -18283,6 +18537,7 @@ }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", + "dev": true, "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", @@ -18595,6 +18850,7 @@ }, "node_modules/babel-preset-jest": { "version": "29.6.3", + "dev": true, "license": "MIT", "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", @@ -18702,6 +18958,46 @@ "node": "*" } }, + "node_modules/bin-links": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-4.0.4.tgz", + "integrity": "sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA==", + "dev": true, + "dependencies": { + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/bin-links/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/bin-links/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "dev": true, @@ -19333,6 +19629,7 @@ }, "node_modules/camel-case": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", @@ -19421,6 +19718,7 @@ }, "node_modules/char-regex": { "version": "1.0.2", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -19557,6 +19855,7 @@ }, "node_modules/clean-css": { "version": "5.3.2", + "dev": true, "license": "MIT", "dependencies": { "source-map": "~0.6.0" @@ -19567,6 +19866,7 @@ }, "node_modules/clean-css/node_modules/source-map": { "version": "0.6.1", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -19718,8 +20018,18 @@ "node": ">=6" } }, + "node_modules/cmd-shim": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-6.0.3.tgz", + "integrity": "sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/co": { "version": "4.6.0", + "dev": true, "license": "MIT", "engines": { "iojs": ">= 1.0.0", @@ -19728,6 +20038,7 @@ }, "node_modules/collect-v8-coverage": { "version": "1.0.1", + "dev": true, "license": "MIT" }, "node_modules/color": { @@ -20475,7 +20786,7 @@ }, "node_modules/create-require": { "version": "1.1.1", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/create-storybook": { @@ -20900,6 +21211,7 @@ }, "node_modules/cssom": { "version": "0.5.0", + "dev": true, "license": "MIT" }, "node_modules/csstype": { @@ -21012,6 +21324,7 @@ }, "node_modules/decimal.js": { "version": "10.4.3", + "dev": true, "license": "MIT" }, "node_modules/decode-uri-component": { @@ -21342,6 +21655,7 @@ }, "node_modules/detect-newline": { "version": "3.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -21373,9 +21687,21 @@ "diagnostic-channel": "*" } }, + "node_modules/diagnostic-channel/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/diff": { "version": "4.0.2", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -21383,6 +21709,7 @@ }, "node_modules/diff-sequences": { "version": "29.6.3", + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -21535,6 +21862,7 @@ }, "node_modules/dom-converter": { "version": "0.2.0", + "dev": true, "license": "MIT", "dependencies": { "utila": "~0.4" @@ -21560,6 +21888,7 @@ }, "node_modules/domexception": { "version": "4.0.0", + "dev": true, "license": "MIT", "dependencies": { "webidl-conversions": "^7.0.0" @@ -21607,6 +21936,7 @@ }, "node_modules/dot-case": { "version": "3.0.4", + "dev": true, "license": "MIT", "dependencies": { "no-case": "^3.0.4", @@ -21919,6 +22249,7 @@ }, "node_modules/emittery": { "version": "0.13.1", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -22314,6 +22645,7 @@ }, "node_modules/escodegen": { "version": "2.1.0", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", @@ -22333,6 +22665,7 @@ }, "node_modules/escodegen/node_modules/source-map": { "version": "0.6.1", + "dev": true, "license": "BSD-3-Clause", "optional": true, "engines": { @@ -22601,6 +22934,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-expensify/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-config-prettier": { "version": "9.1.0", "dev": true, @@ -22802,6 +23147,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/eslint-plugin-deprecation/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-plugin-es": { "version": "4.1.0", "dev": true, @@ -23641,12 +23998,14 @@ }, "node_modules/exit": { "version": "0.1.2", + "dev": true, "engines": { "node": ">= 0.8.0" } }, "node_modules/expect": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", @@ -23679,6 +24038,17 @@ "ua-parser-js": "^1.0.38" } }, + "node_modules/expensify-common/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/expensify-common/node_modules/ua-parser-js": { "version": "1.0.38", "funding": [ @@ -23978,6 +24348,17 @@ "node": ">=8" } }, + "node_modules/expo/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/expo/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -25091,6 +25472,7 @@ }, "node_modules/get-package-type": { "version": "0.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8.0.0" @@ -25505,6 +25887,7 @@ }, "node_modules/he": { "version": "1.2.0", + "dev": true, "license": "MIT", "bin": { "he": "bin/he" @@ -25573,6 +25956,7 @@ }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", + "dev": true, "license": "MIT", "dependencies": { "whatwg-encoding": "^2.0.0" @@ -25597,10 +25981,12 @@ }, "node_modules/html-escaper": { "version": "2.0.2", + "dev": true, "license": "MIT" }, "node_modules/html-minifier-terser": { "version": "6.1.0", + "dev": true, "license": "MIT", "dependencies": { "camel-case": "^4.1.2", @@ -25620,6 +26006,7 @@ }, "node_modules/html-minifier-terser/node_modules/commander": { "version": "8.3.0", + "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -25638,6 +26025,7 @@ }, "node_modules/html-webpack-plugin": { "version": "5.5.3", + "dev": true, "license": "MIT", "dependencies": { "@types/html-minifier-terser": "^6.0.0", @@ -25957,6 +26345,7 @@ }, "node_modules/iconv-lite": { "version": "0.6.3", + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -26069,6 +26458,7 @@ }, "node_modules/import-local": { "version": "3.1.0", + "dev": true, "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", @@ -26086,6 +26476,7 @@ }, "node_modules/import-local/node_modules/find-up": { "version": "4.1.0", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -26097,6 +26488,7 @@ }, "node_modules/import-local/node_modules/locate-path": { "version": "5.0.0", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -26107,6 +26499,7 @@ }, "node_modules/import-local/node_modules/p-limit": { "version": "2.3.0", + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -26120,6 +26513,7 @@ }, "node_modules/import-local/node_modules/p-locate": { "version": "4.1.0", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -26130,6 +26524,7 @@ }, "node_modules/import-local/node_modules/path-exists": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -26137,6 +26532,7 @@ }, "node_modules/import-local/node_modules/pkg-dir": { "version": "4.2.0", + "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -26614,6 +27010,7 @@ }, "node_modules/is-generator-fn": { "version": "2.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -26820,6 +27217,7 @@ }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", + "dev": true, "license": "MIT" }, "node_modules/is-regex": { @@ -26995,6 +27393,7 @@ }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=8" @@ -27002,6 +27401,7 @@ }, "node_modules/istanbul-lib-instrument": { "version": "5.2.0", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -27016,6 +27416,7 @@ }, "node_modules/istanbul-lib-instrument/node_modules/semver": { "version": "6.3.1", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -27023,6 +27424,7 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", @@ -27035,6 +27437,7 @@ }, "node_modules/istanbul-lib-report/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -27042,6 +27445,7 @@ }, "node_modules/istanbul-lib-report/node_modules/make-dir": { "version": "4.0.0", + "dev": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" @@ -27053,8 +27457,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/istanbul-lib-report/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -27065,6 +27482,7 @@ }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", @@ -27077,6 +27495,7 @@ }, "node_modules/istanbul-lib-source-maps/node_modules/source-map": { "version": "0.6.1", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -27084,6 +27503,7 @@ }, "node_modules/istanbul-reports": { "version": "3.1.7", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", @@ -27210,6 +27630,7 @@ }, "node_modules/jest": { "version": "29.4.1", + "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.4.1", @@ -27234,6 +27655,7 @@ }, "node_modules/jest-changed-files": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "execa": "^5.0.0", @@ -27339,6 +27761,7 @@ }, "node_modules/jest-cli": { "version": "29.4.1", + "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.4.1", @@ -27371,6 +27794,7 @@ }, "node_modules/jest-cli/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -27384,6 +27808,7 @@ }, "node_modules/jest-cli/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -27398,6 +27823,7 @@ }, "node_modules/jest-cli/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -27408,10 +27834,12 @@ }, "node_modules/jest-cli/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jest-cli/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -27419,6 +27847,7 @@ }, "node_modules/jest-cli/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -27429,6 +27858,7 @@ }, "node_modules/jest-config": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -27472,6 +27902,7 @@ }, "node_modules/jest-config/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -27485,6 +27916,7 @@ }, "node_modules/jest-config/node_modules/babel-jest": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/transform": "^29.7.0", @@ -27504,6 +27936,7 @@ }, "node_modules/jest-config/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -27518,6 +27951,7 @@ }, "node_modules/jest-config/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -27528,10 +27962,12 @@ }, "node_modules/jest-config/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jest-config/node_modules/dedent": { "version": "1.5.3", + "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -27544,6 +27980,7 @@ }, "node_modules/jest-config/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -27551,6 +27988,7 @@ }, "node_modules/jest-config/node_modules/jest-circus": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -27580,6 +28018,7 @@ }, "node_modules/jest-config/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -27590,6 +28029,7 @@ }, "node_modules/jest-diff": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -27603,6 +28043,7 @@ }, "node_modules/jest-diff/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -27616,6 +28057,7 @@ }, "node_modules/jest-diff/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -27630,6 +28072,7 @@ }, "node_modules/jest-diff/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -27640,10 +28083,12 @@ }, "node_modules/jest-diff/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jest-diff/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -27651,6 +28096,7 @@ }, "node_modules/jest-diff/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -27661,6 +28107,7 @@ }, "node_modules/jest-docblock": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" @@ -27671,6 +28118,7 @@ }, "node_modules/jest-each": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -27685,6 +28133,7 @@ }, "node_modules/jest-each/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -27698,6 +28147,7 @@ }, "node_modules/jest-each/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -27712,6 +28162,7 @@ }, "node_modules/jest-each/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -27722,10 +28173,12 @@ }, "node_modules/jest-each/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jest-each/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -27733,6 +28186,7 @@ }, "node_modules/jest-each/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -27743,6 +28197,7 @@ }, "node_modules/jest-environment-jsdom": { "version": "29.4.1", + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.4.1", @@ -27768,6 +28223,7 @@ }, "node_modules/jest-environment-jsdom/node_modules/acorn": { "version": "8.11.3", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -27778,6 +28234,7 @@ }, "node_modules/jest-environment-jsdom/node_modules/cssstyle": { "version": "2.3.0", + "dev": true, "license": "MIT", "dependencies": { "cssom": "~0.3.6" @@ -27788,10 +28245,12 @@ }, "node_modules/jest-environment-jsdom/node_modules/cssstyle/node_modules/cssom": { "version": "0.3.8", + "dev": true, "license": "MIT" }, "node_modules/jest-environment-jsdom/node_modules/data-urls": { "version": "3.0.2", + "dev": true, "license": "MIT", "dependencies": { "abab": "^2.0.6", @@ -27804,6 +28263,7 @@ }, "node_modules/jest-environment-jsdom/node_modules/entities": { "version": "4.5.0", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -27814,6 +28274,7 @@ }, "node_modules/jest-environment-jsdom/node_modules/form-data": { "version": "4.0.0", + "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -27826,6 +28287,7 @@ }, "node_modules/jest-environment-jsdom/node_modules/jsdom": { "version": "20.0.3", + "dev": true, "license": "MIT", "dependencies": { "abab": "^2.0.6", @@ -27869,6 +28331,7 @@ }, "node_modules/jest-environment-jsdom/node_modules/parse5": { "version": "7.1.2", + "dev": true, "license": "MIT", "dependencies": { "entities": "^4.4.0" @@ -27879,6 +28342,7 @@ }, "node_modules/jest-environment-jsdom/node_modules/tr46": { "version": "3.0.0", + "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.1.1" @@ -27889,6 +28353,7 @@ }, "node_modules/jest-environment-jsdom/node_modules/w3c-xmlserializer": { "version": "4.0.0", + "dev": true, "license": "MIT", "dependencies": { "xml-name-validator": "^4.0.0" @@ -27899,6 +28364,7 @@ }, "node_modules/jest-environment-jsdom/node_modules/whatwg-mimetype": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -27906,6 +28372,7 @@ }, "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { "version": "11.0.0", + "dev": true, "license": "MIT", "dependencies": { "tr46": "^3.0.0", @@ -27919,6 +28386,7 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, "engines": { "node": ">=10.0.0" }, @@ -27937,6 +28405,7 @@ }, "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { "version": "4.0.0", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12" @@ -27961,6 +28430,7 @@ "version": "51.0.4", "resolved": "https://registry.npmjs.org/jest-expo/-/jest-expo-51.0.4.tgz", "integrity": "sha512-WmlR4rUur1TNF/F14brKCmPdX3TWf7Bno/6A1PuxnflN79LEIXpXuPKMlMWwCCChTohGB5FRniknRibblWu1ug==", + "dev": true, "dependencies": { "@expo/config": "~9.0.0-beta.0", "@expo/json-file": "^8.3.0", @@ -27979,26 +28449,11 @@ "jest": "bin/jest.js" } }, - "node_modules/jest-expo/node_modules/@babel/code-frame": { - "version": "7.10.4", - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, - "node_modules/jest-expo/node_modules/@expo/json-file": { - "version": "8.3.1", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "~7.10.4", - "json5": "^2.2.2", - "write-file-atomic": "^2.3.0" - } - }, "node_modules/jest-expo/node_modules/json5": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", + "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -28008,6 +28463,7 @@ }, "node_modules/jest-expo/node_modules/react-test-renderer": { "version": "18.2.0", + "dev": true, "license": "MIT", "dependencies": { "react-is": "^18.2.0", @@ -28020,6 +28476,7 @@ }, "node_modules/jest-expo/node_modules/scheduler": { "version": "0.23.2", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -28034,6 +28491,7 @@ }, "node_modules/jest-haste-map": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -28057,6 +28515,7 @@ }, "node_modules/jest-haste-map/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -28064,6 +28523,7 @@ }, "node_modules/jest-haste-map/node_modules/jest-worker": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -28077,6 +28537,7 @@ }, "node_modules/jest-haste-map/node_modules/supports-color": { "version": "8.1.1", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -28090,6 +28551,7 @@ }, "node_modules/jest-leak-detector": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3", @@ -28101,6 +28563,7 @@ }, "node_modules/jest-matcher-utils": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -28114,6 +28577,7 @@ }, "node_modules/jest-matcher-utils/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -28127,6 +28591,7 @@ }, "node_modules/jest-matcher-utils/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -28141,6 +28606,7 @@ }, "node_modules/jest-matcher-utils/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -28151,10 +28617,12 @@ }, "node_modules/jest-matcher-utils/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jest-matcher-utils/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -28162,6 +28630,7 @@ }, "node_modules/jest-matcher-utils/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -28260,6 +28729,7 @@ }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -28275,6 +28745,7 @@ }, "node_modules/jest-regex-util": { "version": "29.6.3", + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -28282,6 +28753,7 @@ }, "node_modules/jest-resolve": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -28300,6 +28772,7 @@ }, "node_modules/jest-resolve-dependencies": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "^29.6.3", @@ -28311,6 +28784,7 @@ }, "node_modules/jest-resolve/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -28324,6 +28798,7 @@ }, "node_modules/jest-resolve/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -28338,6 +28813,7 @@ }, "node_modules/jest-resolve/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -28348,10 +28824,12 @@ }, "node_modules/jest-resolve/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jest-resolve/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -28359,6 +28837,7 @@ }, "node_modules/jest-resolve/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -28369,6 +28848,7 @@ }, "node_modules/jest-runner": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -28399,6 +28879,7 @@ }, "node_modules/jest-runner/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -28412,6 +28893,7 @@ }, "node_modules/jest-runner/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -28426,6 +28908,7 @@ }, "node_modules/jest-runner/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -28436,10 +28919,12 @@ }, "node_modules/jest-runner/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jest-runner/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -28447,6 +28932,7 @@ }, "node_modules/jest-runner/node_modules/jest-worker": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -28460,6 +28946,7 @@ }, "node_modules/jest-runner/node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -28473,6 +28960,7 @@ }, "node_modules/jest-runner/node_modules/source-map": { "version": "0.6.1", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -28480,6 +28968,7 @@ }, "node_modules/jest-runner/node_modules/source-map-support": { "version": "0.5.13", + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -28488,6 +28977,7 @@ }, "node_modules/jest-runner/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -28498,6 +28988,7 @@ }, "node_modules/jest-runtime": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -28529,6 +29020,7 @@ }, "node_modules/jest-runtime/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -28542,6 +29034,7 @@ }, "node_modules/jest-runtime/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -28556,6 +29049,7 @@ }, "node_modules/jest-runtime/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -28566,10 +29060,12 @@ }, "node_modules/jest-runtime/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jest-runtime/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -28577,6 +29073,7 @@ }, "node_modules/jest-runtime/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -28587,6 +29084,7 @@ }, "node_modules/jest-snapshot": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -28616,6 +29114,7 @@ }, "node_modules/jest-snapshot/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -28629,6 +29128,7 @@ }, "node_modules/jest-snapshot/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -28643,6 +29143,7 @@ }, "node_modules/jest-snapshot/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -28653,17 +29154,32 @@ }, "node_modules/jest-snapshot/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jest-snapshot/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-snapshot/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -28829,6 +29345,7 @@ }, "node_modules/jest-watch-select-projects": { "version": "2.0.0", + "dev": true, "license": "MIT", "dependencies": { "ansi-escapes": "^4.3.0", @@ -28838,6 +29355,7 @@ }, "node_modules/jest-watch-select-projects/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -28851,6 +29369,7 @@ }, "node_modules/jest-watch-select-projects/node_modules/chalk": { "version": "3.0.0", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -28862,6 +29381,7 @@ }, "node_modules/jest-watch-select-projects/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -28872,10 +29392,12 @@ }, "node_modules/jest-watch-select-projects/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jest-watch-select-projects/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -28883,6 +29405,7 @@ }, "node_modules/jest-watch-select-projects/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -28893,6 +29416,7 @@ }, "node_modules/jest-watch-typeahead": { "version": "2.2.1", + "dev": true, "license": "MIT", "dependencies": { "ansi-escapes": "^6.0.0", @@ -28912,6 +29436,7 @@ }, "node_modules/jest-watch-typeahead/node_modules/ansi-escapes": { "version": "6.2.0", + "dev": true, "license": "MIT", "dependencies": { "type-fest": "^3.0.0" @@ -28925,6 +29450,7 @@ }, "node_modules/jest-watch-typeahead/node_modules/ansi-regex": { "version": "6.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -28935,6 +29461,7 @@ }, "node_modules/jest-watch-typeahead/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -28948,6 +29475,7 @@ }, "node_modules/jest-watch-typeahead/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -28962,6 +29490,7 @@ }, "node_modules/jest-watch-typeahead/node_modules/char-regex": { "version": "2.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=12.20" @@ -28969,6 +29498,7 @@ }, "node_modules/jest-watch-typeahead/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -28979,10 +29509,12 @@ }, "node_modules/jest-watch-typeahead/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jest-watch-typeahead/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -28990,6 +29522,7 @@ }, "node_modules/jest-watch-typeahead/node_modules/slash": { "version": "5.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=14.16" @@ -29000,6 +29533,7 @@ }, "node_modules/jest-watch-typeahead/node_modules/string-length": { "version": "5.0.1", + "dev": true, "license": "MIT", "dependencies": { "char-regex": "^2.0.0", @@ -29014,6 +29548,7 @@ }, "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { "version": "7.1.0", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -29027,6 +29562,7 @@ }, "node_modules/jest-watch-typeahead/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -29037,6 +29573,7 @@ }, "node_modules/jest-watch-typeahead/node_modules/type-fest": { "version": "3.13.1", + "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=14.16" @@ -29047,6 +29584,7 @@ }, "node_modules/jest-watcher": { "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -29064,6 +29602,7 @@ }, "node_modules/jest-watcher/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -29077,6 +29616,7 @@ }, "node_modules/jest-watcher/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -29091,6 +29631,7 @@ }, "node_modules/jest-watcher/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -29101,10 +29642,12 @@ }, "node_modules/jest-watcher/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jest-watcher/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -29112,6 +29655,7 @@ }, "node_modules/jest-watcher/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -29122,6 +29666,7 @@ }, "node_modules/jest-when": { "version": "3.5.2", + "dev": true, "license": "MIT", "peerDependencies": { "jest": ">= 25" @@ -29847,6 +30392,11 @@ "version": "4.17.21", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "dev": true, @@ -30023,6 +30573,7 @@ }, "node_modules/lower-case": { "version": "2.0.2", + "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.3" @@ -30089,7 +30640,7 @@ }, "node_modules/make-error": { "version": "1.3.6", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/make-event-props": { @@ -31328,6 +31879,7 @@ }, "node_modules/natural-compare": { "version": "1.4.0", + "dev": true, "license": "MIT" }, "node_modules/natural-compare-lite": { @@ -31357,6 +31909,7 @@ }, "node_modules/no-case": { "version": "3.0.4", + "dev": true, "license": "MIT", "dependencies": { "lower-case": "^2.0.2", @@ -31370,6 +31923,20 @@ "node": ">=12.0.0" } }, + "node_modules/nock": { + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.5.tgz", + "integrity": "sha512-XKYnqUrCwXC8DGG1xX4YH5yNIrlh9c065uaMZZHUoeUUINTOyt+x/G+ezYk0Ft6ExSREVIs+qBJDK503viTfFA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-abi": { "version": "3.65.0", "dev": true, @@ -31607,6 +32174,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/npm-package-arg": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-7.0.0.tgz", @@ -31674,6 +32250,7 @@ }, "node_modules/nwsapi": { "version": "2.2.7", + "dev": true, "license": "MIT" }, "node_modules/nypm": { @@ -32319,6 +32896,7 @@ }, "node_modules/param-case": { "version": "3.0.4", + "dev": true, "license": "MIT", "dependencies": { "dot-case": "^3.0.4", @@ -32379,6 +32957,7 @@ }, "node_modules/pascal-case": { "version": "3.1.2", + "dev": true, "license": "MIT", "dependencies": { "no-case": "^3.0.4", @@ -32502,6 +33081,18 @@ "rimraf": "bin.js" } }, + "node_modules/patch-package/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/patch-package/node_modules/slash": { "version": "2.0.0", "dev": true, @@ -33000,6 +33591,7 @@ }, "node_modules/pretty-error": { "version": "4.0.0", + "dev": true, "license": "MIT", "dependencies": { "lodash": "^4.17.20", @@ -33110,6 +33702,15 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/protocol-buffers-schema": { "version": "3.6.0", "license": "MIT" @@ -33128,6 +33729,7 @@ }, "node_modules/psl": { "version": "1.9.0", + "dev": true, "license": "MIT" }, "node_modules/pump": { @@ -33147,6 +33749,7 @@ }, "node_modules/pure-rand": { "version": "6.0.4", + "dev": true, "funding": [ { "type": "individual", @@ -33239,6 +33842,7 @@ }, "node_modules/querystringify": { "version": "2.2.0", + "dev": true, "license": "MIT" }, "node_modules/queue": { @@ -36259,6 +36863,15 @@ "read-binary-file-arch": "cli.js" } }, + "node_modules/read-cmd-shim": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz", + "integrity": "sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/read-config-file": { "version": "6.4.0", "dev": true, @@ -36591,6 +37204,7 @@ }, "node_modules/relateurl": { "version": "0.2.7", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -36603,6 +37217,7 @@ }, "node_modules/renderkid": { "version": "3.0.0", + "dev": true, "license": "MIT", "dependencies": { "css-select": "^4.1.3", @@ -36614,6 +37229,7 @@ }, "node_modules/renderkid/node_modules/css-select": { "version": "4.3.0", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -36628,6 +37244,7 @@ }, "node_modules/renderkid/node_modules/htmlparser2": { "version": "6.1.0", + "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -36709,6 +37326,7 @@ }, "node_modules/requires-port": { "version": "1.0.0", + "dev": true, "license": "MIT" }, "node_modules/resedit": { @@ -36750,6 +37368,7 @@ }, "node_modules/resolve-cwd": { "version": "3.0.0", + "dev": true, "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" @@ -36932,6 +37551,7 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", + "devOptional": true, "license": "MIT" }, "node_modules/sanitize-filename": { @@ -36948,6 +37568,7 @@ }, "node_modules/saxes": { "version": "6.0.0", + "dev": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -37035,9 +37656,12 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" }, @@ -37511,6 +38135,18 @@ "node": ">=10" } }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/simply-deferred": { "version": "3.0.0", "resolved": "git+ssh://git@github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", @@ -37981,6 +38617,7 @@ }, "node_modules/string-length": { "version": "4.0.2", + "dev": true, "license": "MIT", "dependencies": { "char-regex": "^1.0.2", @@ -38131,6 +38768,7 @@ }, "node_modules/strip-bom": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -38163,6 +38801,7 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -38432,6 +39071,7 @@ }, "node_modules/symbol-tree": { "version": "3.2.4", + "dev": true, "license": "MIT" }, "node_modules/tabbable": { @@ -38691,6 +39331,7 @@ }, "node_modules/test-exclude": { "version": "6.0.0", + "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", @@ -38925,6 +39566,7 @@ }, "node_modules/tough-cookie": { "version": "4.1.3", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.33", @@ -38938,6 +39580,7 @@ }, "node_modules/tough-cookie/node_modules/universalify": { "version": "0.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -39059,6 +39702,18 @@ "node": ">=6" } }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ts-jest/node_modules/yargs-parser": { "version": "21.1.1", "dev": true, @@ -39069,7 +39724,7 @@ }, "node_modules/ts-node": { "version": "10.9.2", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -39111,7 +39766,7 @@ }, "node_modules/ts-node/node_modules/acorn": { "version": "8.11.3", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -39122,7 +39777,7 @@ }, "node_modules/ts-node/node_modules/acorn-walk": { "version": "8.3.2", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -39688,6 +40343,7 @@ }, "node_modules/url-parse": { "version": "1.5.10", + "dev": true, "license": "MIT", "dependencies": { "querystringify": "^2.1.1", @@ -39889,6 +40545,7 @@ }, "node_modules/utila": { "version": "0.4.0", + "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -39914,11 +40571,12 @@ }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { "version": "9.2.0", + "dev": true, "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", @@ -40059,6 +40717,7 @@ }, "node_modules/webidl-conversions": { "version": "7.0.0", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -40665,6 +41324,7 @@ }, "node_modules/whatwg-encoding": { "version": "2.0.0", + "dev": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -40930,6 +41590,7 @@ "version": "0.20.3", "resolved": "file:vendor/xlsx-0.20.3.tgz", "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", + "dev": true, "bin": { "xlsx": "bin/xlsx.njs" }, @@ -41006,6 +41667,7 @@ }, "node_modules/xmlchars": { "version": "2.2.0", + "dev": true, "license": "MIT" }, "node_modules/xpath": { @@ -41075,7 +41737,7 @@ }, "node_modules/yn": { "version": "3.1.1", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" diff --git a/package.json b/package.json index 1ab3e30d7dd6..3d808af65ba2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.38-0", + "version": "9.0.39-0", "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.", @@ -99,7 +99,6 @@ "@rnmapbox/maps": "10.1.30", "@shopify/flash-list": "1.7.1", "@ua/react-native-airship": "19.2.1", - "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", "babel-polyfill": "^6.26.0", "canvas-size": "^1.2.6", @@ -117,9 +116,7 @@ "focus-trap-react": "^10.2.3", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", - "jest-expo": "51.0.4", - "jest-when": "^3.5.2", - "lodash": "4.17.21", + "lodash-es": "4.17.21", "lottie-react-native": "6.5.1", "mapbox-gl": "^2.15.0", "onfido-sdk-ui": "14.15.0", @@ -128,7 +125,6 @@ "react": "18.3.1", "react-beautiful-dnd": "^13.1.1", "react-collapse": "^5.1.0", - "react-compiler-runtime": "file:./lib/react-compiler-runtime", "react-content-loader": "^7.0.0", "react-dom": "18.3.1", "react-error-boundary": "^4.0.11", @@ -182,10 +178,18 @@ "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", - "semver": "^7.5.2", - "xlsx": "file:vendor/xlsx-0.20.3.tgz" + "react-window": "^1.8.9" }, "devDependencies": { + "@fullstory/babel-plugin-react-native": "^1.2.1", + "@kie/act-js": "^2.6.2", + "@kie/mock-github": "2.0.1", + "@vue/preload-webpack-plugin": "^2.0.0", + "jest-expo": "51.0.4", + "jest-when": "^3.5.2", + "react-compiler-runtime": "file:./lib/react-compiler-runtime", + "semver": "7.5.2", + "xlsx": "file:vendor/xlsx-0.20.3.tgz", "@actions/core": "1.10.0", "@actions/github": "5.1.1", "@babel/core": "^7.20.0", @@ -235,7 +239,7 @@ "@types/jest": "^29.5.2", "@types/jest-when": "^3.5.2", "@types/js-yaml": "^4.0.5", - "@types/lodash": "^4.14.195", + "@types/lodash-es": "4.17.12", "@types/mapbox-gl": "^2.7.13", "@types/mime-db": "^1.43.5", "@types/node": "^20.11.5", diff --git a/src/CONST.ts b/src/CONST.ts index b67f00a6cb50..bd3ea4177e3e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1153,6 +1153,7 @@ const CONST = { EXP_ERROR: 666, UNABLE_TO_RETRY: 'unableToRetry', UPDATE_REQUIRED: 426, + INCORRECT_MAGIC_CODE: 451, }, HTTP_STATUS: { // When Cloudflare throttles @@ -2163,7 +2164,15 @@ const CONST = { // Often referred to as "collect" workspaces TEAM: 'team', }, - FIELD_LIST_TITLE_FIELD_ID: 'text_title', + RULE_CONDITIONS: { + MATCHES: 'matches', + }, + FIELDS: { + TAG: 'tag', + CATEGORY: 'category', + FIELD_LIST_TITLE: 'text_title', + TAX: 'tax', + }, DEFAULT_REPORT_NAME_PATTERN: '{report:type} {report:startdate}', ROLE: { ADMIN: 'admin', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b36d41eef960..146d35611a72 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -862,6 +862,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName', getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}` as const, }, + WORKSPACE_TAG_APPROVER: { + route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/approver', + getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${tagName}/approver` as const, + }, WORKSPACE_TAG_LIST_VIEW: { route: 'settings/workspaces/:policyID/tag-list/:orderWeight', getRoute: (policyID: string, orderWeight: number) => `settings/workspaces/${policyID}/tag-list/${orderWeight}` as const, @@ -1475,6 +1479,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/nonreimbursable', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/nonreimbursable` as const, }, + POLICY_ACCOUNTING_SAGE_INTACCT_REIMBURSABLE_DESTINATION: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/reimbursable/destination', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/reimbursable/destination` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_DESTINATION: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/nonreimbursable/destination', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/nonreimbursable/destination` as const, + }, POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR: { route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/:reimbursable/default-vendor', getRoute: (policyID: string, reimbursable: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/${reimbursable}/default-vendor` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 8ebde068d012..496978677870 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -363,6 +363,8 @@ const SCREENS = { SAGE_INTACCT_EXPORT_DATE: 'Policy_Accounting_Sage_Intacct_Export_Date', SAGE_INTACCT_REIMBURSABLE_EXPENSES: 'Policy_Accounting_Sage_Intacct_Reimbursable_Expenses', SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Expenses', + SAGE_INTACCT_REIMBURSABLE_DESTINATION: 'Policy_Accounting_Sage_Intacct_Reimbursable_Destination', + SAGE_INTACCT_NON_REIMBURSABLE_DESTINATION: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Destination', SAGE_INTACCT_DEFAULT_VENDOR: 'Policy_Accounting_Sage_Intacct_Default_Vendor', SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Credit_Card_Account', SAGE_INTACCT_ADVANCED: 'Policy_Accounting_Sage_Intacct_Advanced', @@ -437,6 +439,7 @@ const SCREENS = { TAX_CREATE: 'Workspace_Tax_Create', TAG_CREATE: 'Tag_Create', TAG_SETTINGS: 'Tag_Settings', + TAG_APPROVER: 'Tag_Approver', TAG_LIST_VIEW: 'Tag_List_View', TAG_GL_CODE: 'Tag_GL_Code', CURRENCY: 'Workspace_Profile_Currency', diff --git a/src/components/ConnectionLayout.tsx b/src/components/ConnectionLayout.tsx index 3809b4f4f110..8646f3ce464a 100644 --- a/src/components/ConnectionLayout.tsx +++ b/src/components/ConnectionLayout.tsx @@ -1,4 +1,4 @@ -import {isEmpty} from 'lodash'; +import isEmpty from 'lodash/isEmpty'; import React, {useMemo} from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; diff --git a/src/components/ErrorMessageRow.tsx b/src/components/ErrorMessageRow.tsx index 2e6e41449274..7137def9a345 100644 --- a/src/components/ErrorMessageRow.tsx +++ b/src/components/ErrorMessageRow.tsx @@ -1,4 +1,4 @@ -import {mapValues} from 'lodash'; +import mapValues from 'lodash/mapValues'; import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts index 686c318a99dc..3d1fcf6fe54c 100644 --- a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts +++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts @@ -38,6 +38,7 @@ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [ SCREENS.SEARCH.CENTRAL_PANE, SCREENS.SETTINGS.TROUBLESHOOT, SCREENS.SETTINGS.SAVE_THE_WORLD, + SCREENS.WORKSPACE.RULES, ]; export default WIDE_LAYOUT_INACTIVE_SCREENS; diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index c966dd4456e9..f54009852b22 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -76,10 +76,16 @@ function InputWrapper(p const {registerInput} = useContext(FormContext); const {shouldSetTouchedOnBlurOnly, blurOnSubmit, shouldSubmitForm} = computeComponentSpecificRegistrationParams(props as InputComponentBaseProps); + const {key, ...registerInputProps} = registerInput(inputID, shouldSubmitForm, {ref, valueType, ...rest, shouldSetTouchedOnBlurOnly, blurOnSubmit}); - // TODO: Sometimes we return too many props with register input, so we need to consider if it's better to make the returned type more general and disregard the issue, or we would like to omit the unused props somehow. - // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ( + + ); } InputWrapper.displayName = 'InputWrapper'; diff --git a/src/components/MapView/PendingMapView.tsx b/src/components/MapView/PendingMapView.tsx index cc9829517154..dc84af0b395e 100644 --- a/src/components/MapView/PendingMapView.tsx +++ b/src/components/MapView/PendingMapView.tsx @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import isEmpty from 'lodash/isEmpty'; import React from 'react'; import {View} from 'react-native'; import BlockingView from '@components/BlockingViews/BlockingView'; @@ -10,7 +10,7 @@ import variables from '@styles/variables'; import type {PendingMapViewProps} from './MapViewTypes'; function PendingMapView({title = '', subtitle = '', style, isSmallerIcon = false}: PendingMapViewProps) { - const hasTextContent = !_.isEmpty(title) || !_.isEmpty(subtitle); + const hasTextContent = !isEmpty(title) || !isEmpty(subtitle); const styles = useThemeStyles(); const theme = useTheme(); const iconSize = isSmallerIcon ? variables.iconSizeSuperLarge : variables.iconSizeUltraLarge; diff --git a/src/components/MenuItemList.tsx b/src/components/MenuItemList.tsx index 747e0578a0c4..d33a17f90a5e 100644 --- a/src/components/MenuItemList.tsx +++ b/src/components/MenuItemList.tsx @@ -82,16 +82,16 @@ function MenuItemList({ return ( <> - {menuItems.map((menuItemProps) => ( + {menuItems.map(({key, ...menuItemProps}) => ( secondaryInteraction(menuItemProps.link, e) : undefined} ref={popoverAnchor} diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index 4a59b58a8273..7804ffa047f4 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -81,10 +81,13 @@ function ReceiptCell({transactionItem}: TransactionCellProps) { const backgroundStyles = transactionItem.isSelected ? StyleUtils.getBackgroundColorStyle(theme.buttonHoveredBG) : StyleUtils.getBackgroundColorStyle(theme.border); - const filename = getFileName(transactionItem?.receipt?.source ?? ''); - const receiptURIs = getThumbnailAndImageURIs(transactionItem, null, filename); - const isReceiptPDF = Str.isPDF(filename); - const source = tryResolveUrlFromApiRoot(isReceiptPDF && !receiptURIs.isLocalFile ? receiptURIs.thumbnail ?? '' : receiptURIs.image ?? ''); + let source = transactionItem?.receipt?.source ?? ''; + if (source) { + const filename = getFileName(source); + const receiptURIs = getThumbnailAndImageURIs(transactionItem, null, filename); + const isReceiptPDF = Str.isPDF(filename); + source = tryResolveUrlFromApiRoot(isReceiptPDF && !receiptURIs.isLocalFile ? receiptURIs.thumbnail ?? '' : receiptURIs.image ?? ''); + } return ( { [hasExceededMaxCommentLength], ); - const validateCommentMaxLength = useMemo(() => _.debounce(handleValueChange, 1500, {leading: true}), [handleValueChange]); + const validateCommentMaxLength = useMemo(() => debounce(handleValueChange, 1500, {leading: true}), [handleValueChange]); return {hasExceededMaxCommentLength, validateCommentMaxLength}; }; diff --git a/src/languages/en.ts b/src/languages/en.ts index 7c992adea461..eae87b70ecca 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1,5 +1,5 @@ import {CONST as COMMON_CONST, Str} from 'expensify-common'; -import {startCase} from 'lodash'; +import startCase from 'lodash/startCase'; import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; import type {ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName} from '@src/types/onyx/Policy'; @@ -3186,6 +3186,8 @@ export default { importedFromAccountingSoftware: 'The tags below are imported from your', glCode: 'GL code', updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.', + tagRules: 'Tag rules', + approverDescription: 'Approver', importTags: 'Import tags', importedTagsMessage: (columnCounts: number) => `We found *${columnCounts} columns* in your spreadsheet. Select *Name* next to the column that contains tags names. You can also select *Enabled* next to the column that sets tags status.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index c91d31a7ae61..d94db88f2720 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3235,6 +3235,8 @@ export default { importedFromAccountingSoftware: 'Etiquetas importadas desde', glCode: 'Código de Libro Mayor', updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código de Libro Mayor. Por favor, inténtelo nuevamente.', + tagRules: 'Reglas de etiquetas', + approverDescription: 'Aprobador', importTags: 'Importar categorías', importedTagsMessage: (columnCounts: number) => `Hemos encontrado *${columnCounts} columnas* en su hoja de cálculo. Seleccione *Nombre* junto a la columna que contiene los nombres de las etiquetas. También puede seleccionar *Habilitado* junto a la columna que establece el estado de la etiqueta.`, diff --git a/src/libs/API/parameters/SetPolicyTagApproverParams.ts b/src/libs/API/parameters/SetPolicyTagApproverParams.ts new file mode 100644 index 000000000000..48112b323fad --- /dev/null +++ b/src/libs/API/parameters/SetPolicyTagApproverParams.ts @@ -0,0 +1,7 @@ +type SetPolicyTagApproverParams = { + policyID: string; + tagName: string; + approver: string | null; +}; + +export default SetPolicyTagApproverParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 898e89aa5991..871a2a1da2ba 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -305,6 +305,7 @@ export type {default as EnablePolicyCompanyCardsParams} from './EnablePolicyComp export type {default as ToggleCardContinuousReconciliationParams} from './ToggleCardContinuousReconciliationParams'; export type {default as CardDeactivateParams} from './CardDeactivateParams'; export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams'; +export type {default as SetPolicyTagApproverParams} from './SetPolicyTagApproverParams'; export type {default as SaveSearchParams} from './SaveSearch'; export type {default as DeleteSavedSearchParams} from './DeleteSavedSearch'; export type {default as SetPolicyCategoryReceiptsRequiredParams} from './SetPolicyCategoryReceiptsRequiredParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index bd77e887c03b..37bdf6b81d6e 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -376,6 +376,7 @@ const WRITE_COMMANDS = { CREATE_ADMIN_ISSUED_VIRTUAL_CARD: 'CreateAdminIssuedVirtualCard', ADD_DELEGATE: 'AddDelegate', TOGGLE_CARD_CONTINUOUS_RECONCILIATION: 'ToggleCardContinuousReconciliation', + SET_POLICY_TAG_APPROVER: 'SetPolicyTagApprover', SAVE_SEARCH: 'SaveSearch', DELETE_SAVED_SEARCH: 'DeleteSavedSearch', UPDATE_CARD_SETTLEMENT_FREQUENCY: 'UpdateCardSettlementFrequency', @@ -636,6 +637,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; [WRITE_COMMANDS.SET_POLICY_TAXES_CURRENCY_DEFAULT]: Parameters.SetPolicyCurrencyDefaultParams; [WRITE_COMMANDS.SET_POLICY_CUSTOM_TAX_NAME]: Parameters.SetPolicyCustomTaxNameParams; + [WRITE_COMMANDS.SET_POLICY_TAG_APPROVER]: Parameters.SetPolicyTagApproverParams; [WRITE_COMMANDS.SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT]: Parameters.SetPolicyForeignCurrencyDefaultParams; [WRITE_COMMANDS.CREATE_POLICY_TAX]: Parameters.CreatePolicyTaxParams; [WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED]: Parameters.SetPolicyTaxesEnabledParams; diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index a317fccda550..07ed431a0bc0 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1,4 +1,4 @@ -import lodash from 'lodash'; +import groupBy from 'lodash/groupBy'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -107,7 +107,7 @@ function getDomainCards(cardList: OnyxEntry): Record { // Check for domainName to filter out personal credit cards. const activeCards = Object.values(cardList ?? {}).filter((card) => !!card?.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.some((element) => element === card.state)); - return lodash.groupBy(activeCards, (card) => card.domainName); + return groupBy(activeCards, (card) => card.domainName); } /** diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts index 7f971f37d3fa..7b2f71dbd101 100644 --- a/src/libs/CategoryUtils.ts +++ b/src/libs/CategoryUtils.ts @@ -44,8 +44,11 @@ function formatRequireReceiptsOverText(translate: LocaleContextProps['translate' ); } -function getCategoryApprover(approvalRules: ApprovalRule[], categoryName: string) { - return approvalRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.approver; +function getCategoryApproverRule(approvalRules: ApprovalRule[], categoryName: string) { + const approverRule = approvalRules?.find((rule) => + rule.applyWhen.find(({condition, field, value}) => condition === CONST.POLICY.RULE_CONDITIONS.MATCHES && field === CONST.POLICY.FIELDS.CATEGORY && value === categoryName), + ); + return approverRule; } function getCategoryDefaultTaxRate(expenseRules: ExpenseRule[], categoryName: string, defaultTaxRate?: string) { @@ -59,4 +62,4 @@ function getCategoryDefaultTaxRate(expenseRules: ExpenseRule[], categoryName: st return categoryDefaultTaxRate; } -export {formatDefaultTaxRateText, formatRequireReceiptsOverText, getCategoryApprover, getCategoryDefaultTaxRate}; +export {formatDefaultTaxRateText, formatRequireReceiptsOverText, getCategoryApproverRule, getCategoryDefaultTaxRate}; diff --git a/src/libs/Middleware/HandleUnusedOptimisticID.ts b/src/libs/Middleware/HandleUnusedOptimisticID.ts index db726855d075..ad010880bdf7 100644 --- a/src/libs/Middleware/HandleUnusedOptimisticID.ts +++ b/src/libs/Middleware/HandleUnusedOptimisticID.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import clone from 'lodash/clone'; import deepReplaceKeysAndValues from '@libs/deepReplaceKeysAndValues'; import type {Middleware} from '@libs/Request'; import * as PersistedRequests from '@userActions/PersistedRequests'; @@ -38,7 +38,7 @@ const handleUnusedOptimisticID: Middleware = (requestResponse, request, isFromSe PersistedRequests.getAll() .slice(offset) .forEach((persistedRequest, index) => { - const persistedRequestClone = _.clone(persistedRequest); + const persistedRequestClone = clone(persistedRequest); persistedRequestClone.data = deepReplaceKeysAndValues(persistedRequest.data, oldReportID as string, preexistingReportID); PersistedRequests.update(index + offset, persistedRequestClone); }); diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index f3b7bbad8152..8632234d0f7a 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -272,6 +272,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/tags/WorkspaceEditTagsPage').default, [SCREENS.WORKSPACE.TAG_CREATE]: () => require('../../../../pages/workspace/tags/WorkspaceCreateTagPage').default, [SCREENS.WORKSPACE.TAG_EDIT]: () => require('../../../../pages/workspace/tags/EditTagPage').default, + [SCREENS.WORKSPACE.TAG_APPROVER]: () => require('../../../../pages/workspace/tags/TagApproverPage').default, [SCREENS.WORKSPACE.TAG_GL_CODE]: () => require('../../../../pages/workspace/tags/TagGLCodePage').default, [SCREENS.WORKSPACE.TAXES_SETTINGS]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesSettingsPage').default, [SCREENS.WORKSPACE.TAXES_SETTINGS_CUSTOM_TAX_NAME]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName').default, @@ -414,6 +415,10 @@ const SettingsModalStackNavigator = createModalStackNavigator('../../../../pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage').default, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES]: () => require('../../../../pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_REIMBURSABLE_DESTINATION]: () => + require('../../../../pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesDestinationPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_DESTINATION]: () => + require('../../../../pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesDestinationPage').default, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_DEFAULT_VENDOR]: () => require('../../../../pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage').default, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT]: () => diff --git a/src/libs/Navigation/dismissModalWithReport.ts b/src/libs/Navigation/dismissModalWithReport.ts index 1579a0565726..854b2e586caf 100644 --- a/src/libs/Navigation/dismissModalWithReport.ts +++ b/src/libs/Navigation/dismissModalWithReport.ts @@ -1,7 +1,7 @@ import {getActionFromState} from '@react-navigation/core'; import type {NavigationContainerRef} from '@react-navigation/native'; import {StackActions} from '@react-navigation/native'; -import {findLastIndex} from 'lodash'; +import findLastIndex from 'lodash/findLastIndex'; import type {OnyxEntry} from 'react-native-onyx'; import Log from '@libs/Log'; import {isCentralPaneName} from '@libs/NavigationUtils'; diff --git a/src/libs/Navigation/linkTo/index.ts b/src/libs/Navigation/linkTo/index.ts index 1fc99c771ca5..3ca41846d2b4 100644 --- a/src/libs/Navigation/linkTo/index.ts +++ b/src/libs/Navigation/linkTo/index.ts @@ -1,7 +1,7 @@ import {getActionFromState} from '@react-navigation/core'; import type {NavigationContainerRef, NavigationState, PartialState} from '@react-navigation/native'; import {findFocusedRoute} from '@react-navigation/native'; -import {omitBy} from 'lodash'; +import omitBy from 'lodash/omitBy'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import isReportOpenInRHP from '@libs/Navigation/isReportOpenInRHP'; import {isCentralPaneName} from '@libs/NavigationUtils'; diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 7712356fb5d3..3d97b7be2db7 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -108,6 +108,8 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EXPORT_DATE, SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_REIMBURSABLE_EXPENSES, SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES, + SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_REIMBURSABLE_DESTINATION, + SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_DESTINATION, SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_DEFAULT_VENDOR, SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT, SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED, @@ -135,6 +137,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.TAG_EDIT, SCREENS.WORKSPACE.TAG_LIST_VIEW, SCREENS.WORKSPACE.TAG_GL_CODE, + SCREENS.WORKSPACE.TAG_APPROVER, SCREENS.WORKSPACE.TAGS_IMPORT, SCREENS.WORKSPACE.TAGS_IMPORTED, ], diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index e0b627a54e00..09ed50a57395 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -478,6 +478,8 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_EXPORT_DATE]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT_DATE.route}, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_REIMBURSABLE_EXPENSES]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_REIMBURSABLE_EXPENSES.route}, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.route}, + [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_REIMBURSABLE_DESTINATION]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_REIMBURSABLE_DESTINATION.route}, + [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_DESTINATION]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_DESTINATION.route}, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_DEFAULT_VENDOR]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.route}, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT]: { path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT.route, @@ -721,6 +723,13 @@ const config: LinkingOptions['config'] = { tagName: (tagName: string) => decodeURIComponent(tagName), }, }, + [SCREENS.WORKSPACE.TAG_APPROVER]: { + path: ROUTES.WORKSPACE_TAG_APPROVER.route, + parse: { + orderWeight: Number, + tagName: (tagName: string) => decodeURIComponent(tagName), + }, + }, [SCREENS.WORKSPACE.TAG_GL_CODE]: { path: ROUTES.WORKSPACE_TAG_GL_CODE.route, parse: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fadfd434b95a..a1fa710428df 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -306,6 +306,11 @@ type SettingsNavigatorParamList = { orderWeight: number; tagName: string; }; + [SCREENS.WORKSPACE.TAG_APPROVER]: { + policyID: string; + orderWeight: number; + tagName: string; + }; [SCREENS.WORKSPACE.TAG_GL_CODE]: { policyID: string; orderWeight: number; @@ -637,6 +642,12 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES]: { policyID: string; }; + [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_REIMBURSABLE_DESTINATION]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_DESTINATION]: { + policyID: string; + }; [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_DEFAULT_VENDOR]: { policyID: string; reimbursable: string; diff --git a/src/libs/NetworkConnection.ts b/src/libs/NetworkConnection.ts index c77ed94ca1bb..cb9faae31ddd 100644 --- a/src/libs/NetworkConnection.ts +++ b/src/libs/NetworkConnection.ts @@ -1,5 +1,5 @@ import NetInfo from '@react-native-community/netinfo'; -import {isBoolean} from 'lodash'; +import isBoolean from 'lodash/isBoolean'; import throttle from 'lodash/throttle'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 6dc12d6d1211..7f13e1297b6f 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -38,14 +38,19 @@ Onyx.connect({ function parseMessage(messages: Message[] | undefined) { let nextStepHTML = ''; - messages?.forEach((part) => { + messages?.forEach((part, index) => { const isEmail = Str.isValidEmail(part.text); let tagType = part.type ?? 'span'; let content = Str.safeEscape(part.text); + const previousPart = messages[index - 1]; + const nextPart = messages[index + 1]; + if (currentUserEmail === part.text || part.clickToCopyText === currentUserEmail) { tagType = 'strong'; - content = 'You'; + content = nextPart?.text === `'s` ? 'Your' : 'You'; + } else if (part.text === `'s` && (previousPart?.text === currentUserEmail || previousPart?.clickToCopyText === currentUserEmail)) { + content = ''; } else if (isEmail) { tagType = 'next-step-email'; content = EmailUtils.prefixMailSeparatorsWithBreakOpportunities(content); @@ -116,6 +121,7 @@ function buildNextStep(report: OnyxEntry, predictedNextStatus: ValueOf, predictedNextStatus: ValueOf + rule.applyWhen.find(({condition, field, value}) => condition === CONST.POLICY.RULE_CONDITIONS.MATCHES && field === CONST.POLICY.FIELDS.TAG && value === tagName), + ); + + return approverRule; +} + function getDomainNameForPolicy(policyID?: string): string { if (!policyID) { return ''; @@ -1112,6 +1123,7 @@ export { getWorkspaceAccountID, getAllTaxRatesNamesAndKeys as getAllTaxRates, getTagNamesFromTagsLists, + getTagApproverRule, getDomainNameForPolicy, hasUnsupportedIntegration, getWorkflowApprovalsUnavailable, diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 49c275278c46..ed7def2ed1ec 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -1,5 +1,5 @@ import {Str} from 'expensify-common'; -import _ from 'lodash'; +import findLast from 'lodash/findLast'; import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -30,7 +30,7 @@ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPa return {isThumbnail: true, isLocalFile: true}; } // If there're errors, we need to display them in preview. We can store many files in errors, but we just need to get the last one - const errors = _.findLast(transaction?.errors) as ReceiptError | undefined; + const errors = findLast(transaction?.errors) as ReceiptError | undefined; // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg const path = errors?.source ?? transaction?.receipt?.source ?? receiptPath ?? ''; // filename of uploaded image or last part of remote URI diff --git a/src/libs/ReportActionsConnection.ts b/src/libs/ReportActionsConnection.ts new file mode 100644 index 000000000000..e3c8a4c3cf60 --- /dev/null +++ b/src/libs/ReportActionsConnection.ts @@ -0,0 +1,25 @@ +import type {OnyxCollection} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportActions} from '@src/types/onyx/ReportAction'; + +let allReportActions: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + if (!actions) { + return; + } + + allReportActions = actions; + }, +}); + +// This function is used to get all reports +function getAllReportActions() { + return allReportActions; +} + +// eslint-disable-next-line import/prefer-default-export +export {getAllReportActions}; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1a6d33aa9f7f..f584f694edd0 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1,6 +1,7 @@ import {fastMerge, Str} from 'expensify-common'; -import _ from 'lodash'; +import clone from 'lodash/clone'; import lodashFindLast from 'lodash/findLast'; +import isEmpty from 'lodash/isEmpty'; import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -422,7 +423,7 @@ function getCombinedReportActions( const isSentMoneyReport = reportActions.some((action) => isSentMoneyReportAction(action)); // We don't want to combine report actions of transaction thread in iou report of send money request because we display the transaction report of send money request as a normal thread - if (_.isEmpty(transactionThreadReportID) || isSentMoneyReport) { + if (isEmpty(transactionThreadReportID) || isSentMoneyReport) { return reportActions; } @@ -722,7 +723,7 @@ function replaceBaseURLInPolicyChangeLogAction(reportAction: ReportAction): Repo return reportAction; } - const updatedReportAction = _.clone(reportAction); + const updatedReportAction = clone(reportAction); if (!updatedReportAction.message) { return updatedReportAction; @@ -737,7 +738,7 @@ function replaceBaseURLInPolicyChangeLogAction(reportAction: ReportAction): Repo function getLastVisibleAction(reportID: string, actionsToMerge: Record | null> = {}): OnyxEntry { let reportActions: Array = []; - if (!_.isEmpty(actionsToMerge)) { + if (!isEmpty(actionsToMerge)) { reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge ?? {}, true)) as Array< ReportAction | null | undefined >; @@ -1404,7 +1405,7 @@ function getActionableMentionWhisperMessage(reportAction: OnyxEntry { const personalDetail = personalDetails.find((personal) => personal.accountID === accountID); const displayName = PersonalDetailsUtils.getEffectiveDisplayName(personalDetail); - const handleText = _.isEmpty(displayName) ? Localize.translateLocal('common.hidden') : displayName; + const handleText = isEmpty(displayName) ? Localize.translateLocal('common.hidden') : displayName; return `@${handleText}`; }); const preMentionsText = 'Heads up, '; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 79948d51bcdc..c18b6c41b420 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1,10 +1,11 @@ import {format} from 'date-fns'; import {Str} from 'expensify-common'; -import {isEmpty, isNumber} from 'lodash'; import lodashEscape from 'lodash/escape'; import lodashFindLastIndex from 'lodash/findLastIndex'; import lodashIntersection from 'lodash/intersection'; +import isEmpty from 'lodash/isEmpty'; import lodashIsEqual from 'lodash/isEqual'; +import isNumber from 'lodash/isNumber'; import lodashMaxBy from 'lodash/maxBy'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -491,6 +492,11 @@ type ParsingDetails = { policyID?: string; }; +type Thread = { + parentReportID: string; + parentReportActionID: string; +} & Report; + let currentUserEmail: string | undefined; let currentUserPrivateDomain: string | undefined; let currentUserAccountID: number | undefined; @@ -1111,14 +1117,14 @@ function isWorkspaceTaskReport(report: OnyxEntry): boolean { /** * Returns true if report has a parent */ -function isThread(report: OnyxInputOrEntry): boolean { +function isThread(report: OnyxInputOrEntry): report is Thread { return !!(report?.parentReportID && report?.parentReportActionID); } /** * Returns true if report is of type chat and has a parent and is therefore a Thread. */ -function isChatThread(report: OnyxInputOrEntry): boolean { +function isChatThread(report: OnyxInputOrEntry): report is Thread { return isThread(report) && report?.type === CONST.REPORT.TYPE.CHAT; } @@ -1534,9 +1540,9 @@ function isChildReport(report: OnyxEntry): boolean { * An Expense Request is a thread where the parent report is an Expense Report and * the parentReportAction is a transaction. */ -function isExpenseRequest(report: OnyxInputOrEntry): boolean { +function isExpenseRequest(report: OnyxInputOrEntry): report is Thread { if (isThread(report)) { - const parentReportAction = ReportActionsUtils.getParentReportAction(report); + const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID]; const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; return isExpenseReport(parentReport) && !isEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); } @@ -1549,7 +1555,7 @@ function isExpenseRequest(report: OnyxInputOrEntry): boolean { */ function isIOURequest(report: OnyxInputOrEntry): boolean { if (isThread(report)) { - const parentReportAction = ReportActionsUtils.getParentReportAction(report); + const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID]; const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; return isIOUReport(parentReport) && !isEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); } @@ -1562,7 +1568,7 @@ function isIOURequest(report: OnyxInputOrEntry): boolean { */ function isTrackExpenseReport(report: OnyxInputOrEntry): boolean { if (isThread(report)) { - const parentReportAction = ReportActionsUtils.getParentReportAction(report); + const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID]; return !isEmptyObject(parentReportAction) && ReportActionsUtils.isTrackExpenseAction(parentReportAction); } return false; @@ -2211,7 +2217,7 @@ function getIcons( return [fallbackIcon]; } if (isExpenseRequest(report)) { - const parentReportAction = ReportActionsUtils.getParentReportAction(report); + const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID]; const workspaceIcon = getWorkspaceIcon(report, policy); const memberIcon = { source: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.avatar ?? FallbackAvatar, @@ -2224,7 +2230,7 @@ function getIcons( return [memberIcon, workspaceIcon]; } if (isChatThread(report)) { - const parentReportAction = ReportActionsUtils.getParentReportAction(report); + const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID]; const actorAccountID = getReportActionActorAccountID(parentReportAction, report); const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[actorAccountID ?? -1], '', false); @@ -3105,7 +3111,9 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry) const transactionID = moneyRequestReport ? ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID : 0; const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? ({} as Transaction); - const parentReportAction = ReportActionsUtils.getParentReportAction(moneyRequestReport); + const parentReportAction = isThread(moneyRequestReport) + ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport.parentReportID}`]?.[moneyRequestReport.parentReportActionID] + : undefined; const isRequestIOU = isIOUReport(moneyRequestReport); const isHoldActionCreator = isHoldCreator(transaction, reportAction.childReportID ?? '-1'); @@ -3712,7 +3720,12 @@ function getReportName( } let formattedName: string | undefined; - const parentReportAction = parentReportActionParam ?? ReportActionsUtils.getParentReportAction(report); + let parentReportAction: OnyxEntry; + if (parentReportActionParam) { + parentReportAction = parentReportActionParam; + } else { + parentReportAction = isThread(report) ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID] : undefined; + } const parentReportActionMessage = ReportActionsUtils.getReportActionMessage(parentReportAction); if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { @@ -4248,6 +4261,7 @@ function buildOptimisticTaskCommentReportAction( // These parameters are not saved on the reportAction, but are used to display the task in the UI // Added when we fetch the reportActions on a report + // eslint-disable-next-line reportAction.reportAction.originalMessage = { html: ReportActionsUtils.getReportActionHtml(reportAction.reportAction), taskReportID: ReportActionsUtils.getReportActionMessage(reportAction.reportAction)?.taskReportID, @@ -5994,7 +6008,7 @@ function shouldReportBeInOptionList({ // This can also happen for anyone accessing a public room or archived room for which they don't have access to the underlying policy. // Optionally exclude reports that do not belong to currently active workspace - const parentReportAction = ReportActionsUtils.getParentReportAction(report); + const parentReportAction = isThread(report) ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID] : undefined; if ( !report?.reportID || diff --git a/src/libs/StringUtils.ts b/src/libs/StringUtils.ts index 5cc80a9a9005..10730150489c 100644 --- a/src/libs/StringUtils.ts +++ b/src/libs/StringUtils.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import deburr from 'lodash/deburr'; import CONST from '@src/CONST'; /** @@ -7,7 +7,7 @@ import CONST from '@src/CONST'; * @returns The sanitized string */ function sanitizeString(str: string): string { - return _.deburr(str).toLowerCase().replaceAll(CONST.REGEX.NON_ALPHABETIC_AND_NON_LATIN_CHARS, ''); + return deburr(str).toLowerCase().replaceAll(CONST.REGEX.NON_ALPHABETIC_AND_NON_LATIN_CHARS, ''); } /** diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 15d1eadb662d..e423af5bb7e7 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -202,7 +202,7 @@ function revealVirtualCardDetails(cardID: number, validateCode: string): Promise API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_CARD_DETAILS, parameters) .then((response) => { if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { - if (response?.message === 'Incorrect or invalid magic code. Please try again.') { + if (response?.jsonCode === CONST.JSON_CODE.INCORRECT_MAGIC_CODE) { // eslint-disable-next-line prefer-promise-reject-errors reject('validateCodeForm.error.incorrectMagicCode'); return; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 65cff5baf54a..edee4a7fca6f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3170,13 +3170,13 @@ function updateMoneyRequestDistanceRate( policy: OnyxEntry, policyTagList: OnyxEntry, policyCategories: OnyxEntry, - taxAmount?: number, - taxCode?: string, + updatedTaxAmount?: number, + updatedTaxCode?: string, ) { const transactionChanges: TransactionChanges = { customUnitRateID: rateID, - ...(taxAmount ? {taxAmount} : {}), - ...(taxCode ? {taxCode} : {}), + ...(typeof updatedTaxAmount === 'number' ? {taxAmount: updatedTaxAmount} : {}), + ...(updatedTaxCode ? {taxCode: updatedTaxCode} : {}), }; const allReports = ReportConnection.getAllReports(); const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; @@ -3188,7 +3188,9 @@ function updateMoneyRequestDistanceRate( data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); } const {params, onyxData} = data; - API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DISTANCE_RATE, params, onyxData); + // `taxAmount` & `taxCode` only needs to be updated in the optimistic data, so we need to remove them from the params + const {taxAmount, taxCode, ...paramsWithoutTaxUpdated} = params; + API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DISTANCE_RATE, paramsWithoutTaxUpdated, onyxData); } /** Edits an existing distance expense */ @@ -6448,7 +6450,7 @@ function getReportFromHoldRequestsOnyxData( chatReport.reportID, chatReport.policyID ?? iouReport.policyID ?? '', recipient.accountID ?? 1, - (firstHoldTransaction?.amount ?? 0) * -1, + holdTransactions.reduce((acc, transaction) => acc + transaction.amount, 0) * -1, getCurrency(firstHoldTransaction), false, newParentReportActionID, diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 2e11143ca00d..c342fe6eedb6 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -18,6 +18,7 @@ import type { } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ApiUtils from '@libs/ApiUtils'; +import * as CategoryUtils from '@libs/CategoryUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; @@ -27,6 +28,7 @@ import Log from '@libs/Log'; import enhanceParameters from '@libs/Network/enhanceParameters'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import {navigateWhenEnableFeature, removePendingFieldsFromCustomUnit} from '@libs/PolicyUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -519,8 +521,28 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[]) } function renamePolicyCategory(policyID: string, policyCategory: {oldName: string; newName: string}) { + const policy = PolicyUtils.getPolicy(policyID); const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[policyCategory.oldName]; + const policyCategoryRule = CategoryUtils.getCategoryApproverRule(policy?.rules?.approvalRules ?? [], policyCategory.oldName); + const approvalRules = policy?.rules?.approvalRules ?? []; + const updatedApprovalRules: ApprovalRule[] = lodashCloneDeep(approvalRules); + + // Its related by name, so the corresponding rule has to be updated to handle offline scenario + if (policyCategoryRule) { + const indexToUpdate = updatedApprovalRules.findIndex((rule) => rule.id === policyCategoryRule.id); + policyCategoryRule.applyWhen = policyCategoryRule.applyWhen.map((ruleCondition) => { + const {value, field, condition} = ruleCondition; + + if (value === policyCategory.oldName && field === CONST.POLICY.FIELDS.CATEGORY && condition === CONST.POLICY.RULE_CONDITIONS.MATCHES) { + return {...ruleCondition, value: policyCategory.newName}; + } + + return ruleCondition; + }); + updatedApprovalRules[indexToUpdate] = policyCategoryRule; + } + const onyxData: OnyxData = { optimisticData: [ { @@ -540,6 +562,15 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + approvalRules: updatedApprovalRules, + }, + }, + }, ], successData: [ { @@ -574,6 +605,15 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + approvalRules, + }, + }, + }, ], }; @@ -1001,7 +1041,7 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn API.write(WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY, params, {optimisticData, successData, failureData}); } -function downloadCategoriesCSV(policyID: string) { +function downloadCategoriesCSV(policyID: string, onDownloadFailed: () => void) { const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_CATEGORIES_CSV, { policyID, }); @@ -1013,7 +1053,7 @@ function downloadCategoriesCSV(policyID: string) { formData.append(key, String(value)); }); - fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_CATEGORIES_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST); + fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_CATEGORIES_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed); } function setWorkspaceCategoryDescriptionHint(policyID: string, categoryName: string, commentHint: string) { @@ -1152,7 +1192,8 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const approvalRules = policy?.rules?.approvalRules ?? []; let updatedApprovalRules: ApprovalRule[] = lodashCloneDeep(approvalRules); - const existingCategoryApproverRule = updatedApprovalRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); + const existingCategoryApproverRule = CategoryUtils.getCategoryApproverRule(updatedApprovalRules, categoryName); + let newApprover = approver; if (!existingCategoryApproverRule) { @@ -1160,7 +1201,7 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro approver, applyWhen: [ { - condition: 'matches', + condition: CONST.POLICY.RULE_CONDITIONS.MATCHES, field: 'category', value: categoryName, }, @@ -1183,27 +1224,10 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro rules: { approvalRules: updatedApprovalRules, }, - pendingRulesUpdates: { - [categoryName]: { - approvalRule: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - }, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - pendingRulesUpdates: { - [categoryName]: { - approvalRule: null, - }, - }, }, }, ], + failureData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -1212,11 +1236,6 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro rules: { approvalRules, }, - pendingRulesUpdates: { - [categoryName]: { - approvalRule: null, - }, - }, }, }, ], @@ -1232,7 +1251,7 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro } function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: string) { - const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + const policy = PolicyUtils.getPolicy(policyID); const expenseRules = policy?.rules?.expenseRules ?? []; const updatedExpenseRules: ExpenseRule[] = lodashCloneDeep(expenseRules); const existingCategoryExpenseRule = updatedExpenseRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); @@ -1247,7 +1266,7 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str }, applyWhen: [ { - condition: 'matches', + condition: CONST.POLICY.RULE_CONDITIONS.MATCHES, field: 'category', value: categoryName, }, @@ -1267,24 +1286,6 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str rules: { expenseRules: updatedExpenseRules, }, - pendingRulesUpdates: { - [categoryName]: { - expenseRule: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - }, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - pendingRulesUpdates: { - [categoryName]: { - expenseRule: null, - }, - }, }, }, ], @@ -1296,11 +1297,6 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str rules: { expenseRules, }, - pendingRulesUpdates: { - [categoryName]: { - expenseRule: null, - }, - }, }, }, ], diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index d73e2b8c4166..b8c9014ecdcc 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1,6 +1,6 @@ import {PUBLIC_DOMAINS, Str} from 'expensify-common'; -import {escapeRegExp} from 'lodash'; import lodashClone from 'lodash/clone'; +import escapeRegExp from 'lodash/escapeRegExp'; import lodashUnion from 'lodash/union'; import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -71,6 +71,7 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; import {navigateWhenEnableFeature} from '@libs/PolicyUtils'; +import * as ReportActionsConnection from '@libs/ReportActionsConnection'; import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -92,7 +93,6 @@ import type { } from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {Attributes, CompanyAddress, CustomUnit, NetSuiteCustomList, NetSuiteCustomSegment, Rate, TaxRate, Unit} from '@src/types/onyx/Policy'; -import type {ReportActions} from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {buildOptimisticPolicyCategories} from './Category'; @@ -165,19 +165,6 @@ Onyx.connect({ }, }); -let allReportActions: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (actions) => { - if (!actions) { - return; - } - - allReportActions = actions; - }, -}); - let lastAccessedWorkspacePolicyID: OnyxEntry; Onyx.connect({ key: ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, @@ -2632,10 +2619,9 @@ function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): WorkspaceF }); // We need to move the report preview action from the DM to the workspace chat. - const reportPreview = - iouReport?.parentReportID && iouReport.parentReportActionID - ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.parentReportID}`]?.[iouReport.parentReportActionID] - : undefined; + const parentReport = ReportActionsConnection.getAllReportActions()?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.parentReportID}`]; + const parentReportActionID = iouReport.parentReportActionID; + const reportPreview = iouReport?.parentReportID && parentReportActionID ? parentReport?.[parentReportActionID] : undefined; optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -3963,8 +3949,8 @@ function enablePolicyDefaultReportTitle(policyID: string, enabled: boolean) { return; } - const previousReportTitleField = policy?.fieldList?.[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID] ?? {}; - const titleFieldValues = enabled ? {} : {fieldList: {[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: {...previousReportTitleField, defaultValue: CONST.POLICY.DEFAULT_REPORT_NAME_PATTERN}}}; + const previousReportTitleField = policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE] ?? {}; + const titleFieldValues = enabled ? {} : {fieldList: {[CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: {...previousReportTitleField, defaultValue: CONST.POLICY.DEFAULT_REPORT_NAME_PATTERN}}}; const optimisticData: OnyxUpdate[] = [ { @@ -4000,7 +3986,7 @@ function enablePolicyDefaultReportTitle(policyID: string, enabled: boolean) { value: { shouldShowCustomReportTitleOption: !!policy?.shouldShowCustomReportTitleOption, fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: previousReportTitleField, + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: previousReportTitleField, }, pendingFields: { shouldShowCustomReportTitleOption: null, @@ -4032,11 +4018,11 @@ function enablePolicyDefaultReportTitle(policyID: string, enabled: boolean) { function setPolicyDefaultReportTitle(policyID: string, customName: string) { const policy = getPolicy(policyID); - if (customName === policy?.fieldList?.[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]?.defaultValue) { + if (customName === policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE]?.defaultValue) { return; } - const previousReportTitleField = policy?.fieldList?.[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID] ?? {}; + const previousReportTitleField = policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE] ?? {}; const optimisticData: OnyxUpdate[] = [ { @@ -4044,7 +4030,7 @@ function setPolicyDefaultReportTitle(policyID: string, customName: string) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: { + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: { defaultValue: customName, pendingFields: {defaultValue: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, }, @@ -4059,7 +4045,7 @@ function setPolicyDefaultReportTitle(policyID: string, customName: string) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: {pendingFields: {defaultValue: null}}, + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: {pendingFields: {defaultValue: null}}, }, errorFields: null, }, @@ -4072,7 +4058,7 @@ function setPolicyDefaultReportTitle(policyID: string, customName: string) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: {...previousReportTitleField, pendingFields: {defaultValue: null}}, + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: {...previousReportTitleField, pendingFields: {defaultValue: null}}, }, errorFields: { fieldList: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), @@ -4101,11 +4087,11 @@ function setPolicyDefaultReportTitle(policyID: string, customName: string) { function setPolicyPreventMemberCreatedTitle(policyID: string, enforced: boolean) { const policy = getPolicy(policyID); - if (!enforced === policy?.fieldList?.[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID].deletable) { + if (!enforced === policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE].deletable) { return; } - const previousReportTitleField = policy?.fieldList?.[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID] ?? {}; + const previousReportTitleField = policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE] ?? {}; const optimisticData: OnyxUpdate[] = [ { @@ -4113,7 +4099,7 @@ function setPolicyPreventMemberCreatedTitle(policyID: string, enforced: boolean) key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: {...previousReportTitleField, deletable: !enforced, pendingFields: {deletable: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}}, + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: {...previousReportTitleField, deletable: !enforced, pendingFields: {deletable: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}}, }, }, }, @@ -4125,7 +4111,7 @@ function setPolicyPreventMemberCreatedTitle(policyID: string, enforced: boolean) key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: {pendingFields: {deletable: null}}, + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: {pendingFields: {deletable: null}}, }, errorFields: null, }, @@ -4138,7 +4124,7 @@ function setPolicyPreventMemberCreatedTitle(policyID: string, enforced: boolean) key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { fieldList: { - [CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID]: {...previousReportTitleField, pendingFields: {deletable: null}}, + [CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: {...previousReportTitleField, pendingFields: {deletable: null}}, }, errorFields: { fieldList: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index b51918b37563..d6f67e496b92 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -1,3 +1,4 @@ +import lodashCloneDeep from 'lodash/cloneDeep'; import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; @@ -6,6 +7,7 @@ import type { OpenPolicyTagsPageParams, RenamePolicyTaglistParams, RenamePolicyTagsParams, + SetPolicyTagApproverParams, SetPolicyTagsEnabled, SetPolicyTagsRequired, UpdatePolicyTagGLCodeParams, @@ -27,7 +29,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyTag, PolicyTagLists, PolicyTags, RecentlyUsedTags, Report} from '@src/types/onyx'; import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; -import type {Attributes, Rate} from '@src/types/onyx/Policy'; +import type {ApprovalRule, Attributes, Rate} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; type NewCustomUnit = { @@ -453,10 +455,31 @@ function clearPolicyTagListErrors(policyID: string, tagListIndex: number) { } function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: string}, tagListIndex: number) { + const policy = PolicyUtils.getPolicy(policyID); const tagList = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[tagListIndex] ?? {}; const tag = tagList.tags?.[policyTag.oldName]; const oldTagName = policyTag.oldName; const newTagName = PolicyUtils.escapeTagName(policyTag.newName); + + const policyTagRule = PolicyUtils.getTagApproverRule(policyID, oldTagName); + const approvalRules = policy?.rules?.approvalRules ?? []; + const updatedApprovalRules: ApprovalRule[] = lodashCloneDeep(approvalRules); + + // Its related by name, so the corresponding rule has to be updated to handle offline scenario + if (policyTagRule) { + const indexToUpdate = updatedApprovalRules.findIndex((rule) => rule.id === policyTagRule.id); + policyTagRule.applyWhen = policyTagRule.applyWhen.map((ruleCondition) => { + const {value, field, condition} = ruleCondition; + + if (value === policyTag.oldName && field === CONST.POLICY.FIELDS.TAG && condition === CONST.POLICY.RULE_CONDITIONS.MATCHES) { + return {...ruleCondition, value: policyTag.newName}; + } + + return ruleCondition; + }); + updatedApprovalRules[indexToUpdate] = policyTagRule; + } + const onyxData: OnyxData = { optimisticData: [ { @@ -481,6 +504,15 @@ function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + approvalRules: updatedApprovalRules, + }, + }, + }, ], successData: [ { @@ -890,6 +922,74 @@ function setPolicyTagGLCode(policyID: string, tagName: string, tagListIndex: num API.write(WRITE_COMMANDS.UPDATE_POLICY_TAG_GL_CODE, parameters, onyxData); } +function setPolicyTagApprover(policyID: string, tag: string, approver: string) { + const policy = PolicyUtils.getPolicy(policyID); + const prevApprovalRules = policy?.rules?.approvalRules ?? []; + const approverRuleToUpdate = PolicyUtils.getTagApproverRule(policyID, tag); + const filteredApprovalRules = approverRuleToUpdate ? prevApprovalRules.filter((rule) => rule.id !== approverRuleToUpdate.id) : prevApprovalRules; + const toBeUnselected = approverRuleToUpdate?.approver === approver; + + const updatedApproverRule = approverRuleToUpdate + ? {...approverRuleToUpdate, approver} + : { + applyWhen: [ + { + condition: CONST.POLICY.RULE_CONDITIONS.MATCHES, + field: CONST.POLICY.FIELDS.TAG, + value: tag, + }, + ], + approver, + id: '-1', + }; + + const updatedApprovalRules = toBeUnselected ? filteredApprovalRules : [...filteredApprovalRules, updatedApproverRule]; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + approvalRules: updatedApprovalRules, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + approvalRules: updatedApprovalRules, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + approvalRules: prevApprovalRules, + }, + }, + }, + ], + }; + + const parameters: SetPolicyTagApproverParams = { + policyID, + tagName: tag, + approver: toBeUnselected ? null : approver, + }; + + API.write(WRITE_COMMANDS.SET_POLICY_TAG_APPROVER, parameters, onyxData); +} + function downloadTagsCSV(policyID: string) { const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_TAGS_CSV, { policyID, @@ -919,6 +1019,7 @@ export { renamePolicyTaglist, setWorkspaceTagEnabled, setPolicyTagGLCode, + setPolicyTagApprover, importPolicyTags, downloadTagsCSV, }; diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index d7223524b32b..e19251b62ce8 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -1,7 +1,7 @@ import {getUnixTime} from 'date-fns'; -import {isEqual} from 'lodash'; import lodashClone from 'lodash/clone'; import lodashHas from 'lodash/has'; +import isEqual from 'lodash/isEqual'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; diff --git a/src/libs/fileDownload/DownloadUtils.ts b/src/libs/fileDownload/DownloadUtils.ts index a09b0aa38c75..4f0ce6ab0c92 100644 --- a/src/libs/fileDownload/DownloadUtils.ts +++ b/src/libs/fileDownload/DownloadUtils.ts @@ -53,7 +53,13 @@ const fetchFileDownload: FileDownload = (url, fileName, successMessage = '', sho }; return fetch(url, fetchOptions) - .then((response) => response.blob()) + .then((response) => { + const contentType = response.headers.get('content-type'); + if (contentType === 'application/json' && fileName?.includes('.csv')) { + throw new Error(); + } + return response.blob(); + }) .then((blob) => { // Create blob link to download const href = URL.createObjectURL(new Blob([blob])); diff --git a/src/libs/fileDownload/index.android.ts b/src/libs/fileDownload/index.android.ts index a1e81e47994d..8426e20a33f7 100644 --- a/src/libs/fileDownload/index.android.ts +++ b/src/libs/fileDownload/index.android.ts @@ -107,6 +107,10 @@ const postDownloadFile = (url: string, fileName?: string, formData?: FormData, o if (!response.ok) { throw new Error('Failed to download file'); } + const contentType = response.headers.get('content-type'); + if (contentType === 'application/json' && fileName?.includes('.csv')) { + throw new Error(); + } return response.text(); }) .then((fileData) => { diff --git a/src/libs/fileDownload/index.desktop.ts b/src/libs/fileDownload/index.desktop.ts index de000f61b41b..6a601a4af249 100644 --- a/src/libs/fileDownload/index.desktop.ts +++ b/src/libs/fileDownload/index.desktop.ts @@ -7,10 +7,10 @@ import type {FileDownload} from './types'; /** * The function downloads an attachment on desktop platforms. */ -const fileDownload: FileDownload = (url, fileName, successMessage, shouldOpenExternalLink, formData, requestType) => { +const fileDownload: FileDownload = (url, fileName, successMessage, shouldOpenExternalLink, formData, requestType, onDownloadFailed?: () => void) => { if (requestType === CONST.NETWORK.METHOD.POST) { window.electron.send(ELECTRON_EVENTS.DOWNLOAD); - return fetchFileDownload(url, fileName, successMessage, shouldOpenExternalLink, formData, requestType); + return fetchFileDownload(url, fileName, successMessage, shouldOpenExternalLink, formData, requestType, onDownloadFailed); } const options: Options = { diff --git a/src/libs/fileDownload/index.ios.ts b/src/libs/fileDownload/index.ios.ts index 1fff9fb998e6..fb2e1c2c146a 100644 --- a/src/libs/fileDownload/index.ios.ts +++ b/src/libs/fileDownload/index.ios.ts @@ -38,6 +38,10 @@ const postDownloadFile = (url: string, fileName?: string, formData?: FormData, o if (!response.ok) { throw new Error('Failed to download file'); } + const contentType = response.headers.get('content-type'); + if (contentType === 'application/json' && fileName?.includes('.csv')) { + throw new Error(); + } return response.text(); }) .then((fileData) => { diff --git a/src/libs/migrations/RemoveEmptyReportActionsDrafts.ts b/src/libs/migrations/RemoveEmptyReportActionsDrafts.ts index ce2f8de77634..5dc1ada1947d 100644 --- a/src/libs/migrations/RemoveEmptyReportActionsDrafts.ts +++ b/src/libs/migrations/RemoveEmptyReportActionsDrafts.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import isEmpty from 'lodash/isEmpty'; import type {OnyxInputValue} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; @@ -38,7 +38,7 @@ export default function (): Promise { hasUnmigratedDraft = true; Log.info(`[Migrate Onyx] Migrating draft for report action ${reportActionID}`); - if (_.isEmpty(reportActionDraft)) { + if (isEmpty(reportActionDraft)) { Log.info(`[Migrate Onyx] Removing draft for report action ${reportActionID}`); return; } diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 484cadd5c1a2..b8b551a345ca 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -82,7 +82,7 @@ function getReportID(route: ReportScreenNavigationProps['route']): string { * * @param report */ -function isEmpty(report: OnyxTypes.Report): boolean { +function isEmpty(report: OnyxEntry): boolean { if (isEmptyObject(report)) { return true; } @@ -167,101 +167,61 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro * put this into onyx selector as it will be the same. */ const report = useMemo( - (): OnyxTypes.Report => ({ - lastReadTime: reportOnyx?.lastReadTime, - reportID: reportOnyx?.reportID ?? '', - policyID: reportOnyx?.policyID, - lastVisibleActionCreated: reportOnyx?.lastVisibleActionCreated, - statusNum: reportOnyx?.statusNum, - stateNum: reportOnyx?.stateNum, - writeCapability: reportOnyx?.writeCapability, - type: reportOnyx?.type, - errorFields: reportOnyx?.errorFields, - isPolicyExpenseChat: reportOnyx?.isPolicyExpenseChat, - parentReportID: reportOnyx?.parentReportID, - parentReportActionID: reportOnyx?.parentReportActionID, - chatType: reportOnyx?.chatType, - pendingFields: reportOnyx?.pendingFields, - isDeletedParentAction: reportOnyx?.isDeletedParentAction, - reportName: reportOnyx?.reportName, - description: reportOnyx?.description, - managerID: reportOnyx?.managerID, - total: reportOnyx?.total, - nonReimbursableTotal: reportOnyx?.nonReimbursableTotal, - fieldList: reportOnyx?.fieldList, - ownerAccountID: reportOnyx?.ownerAccountID, - currency: reportOnyx?.currency, - unheldTotal: reportOnyx?.unheldTotal, - participants: reportOnyx?.participants, - isWaitingOnBankAccount: reportOnyx?.isWaitingOnBankAccount, - iouReportID: reportOnyx?.iouReportID, - isOwnPolicyExpenseChat: reportOnyx?.isOwnPolicyExpenseChat, - isPinned: reportOnyx?.isPinned, - chatReportID: reportOnyx?.chatReportID, - visibility: reportOnyx?.visibility, - oldPolicyName: reportOnyx?.oldPolicyName, - policyName: reportOnyx?.policyName, - // eslint-disable-next-line @typescript-eslint/naming-convention - private_isArchived: reportOnyx?.private_isArchived, - isOptimisticReport: reportOnyx?.isOptimisticReport, - lastMentionedTime: reportOnyx?.lastMentionedTime, - avatarUrl: reportOnyx?.avatarUrl, - avatarFileName: reportOnyx?.avatarFileName, - permissions, - invoiceReceiver: reportOnyx?.invoiceReceiver, - policyAvatar: reportOnyx?.policyAvatar, - }), - [ - reportOnyx?.lastReadTime, - reportOnyx?.reportID, - reportOnyx?.policyID, - reportOnyx?.lastVisibleActionCreated, - reportOnyx?.statusNum, - reportOnyx?.stateNum, - reportOnyx?.writeCapability, - reportOnyx?.type, - reportOnyx?.errorFields, - reportOnyx?.isPolicyExpenseChat, - reportOnyx?.parentReportID, - reportOnyx?.parentReportActionID, - reportOnyx?.chatType, - reportOnyx?.pendingFields, - reportOnyx?.isDeletedParentAction, - reportOnyx?.reportName, - reportOnyx?.description, - reportOnyx?.managerID, - reportOnyx?.total, - reportOnyx?.nonReimbursableTotal, - reportOnyx?.fieldList, - reportOnyx?.ownerAccountID, - reportOnyx?.currency, - reportOnyx?.unheldTotal, - reportOnyx?.participants, - reportOnyx?.isWaitingOnBankAccount, - reportOnyx?.iouReportID, - reportOnyx?.isOwnPolicyExpenseChat, - reportOnyx?.isPinned, - reportOnyx?.chatReportID, - reportOnyx?.visibility, - reportOnyx?.oldPolicyName, - reportOnyx?.policyName, - reportOnyx?.private_isArchived, - reportOnyx?.isOptimisticReport, - reportOnyx?.lastMentionedTime, - reportOnyx?.avatarUrl, - reportOnyx?.avatarFileName, - permissions, - reportOnyx?.invoiceReceiver, - reportOnyx?.policyAvatar, - ], + (): OnyxEntry => + reportOnyx && { + lastReadTime: reportOnyx.lastReadTime, + reportID: reportOnyx.reportID ?? '', + policyID: reportOnyx.policyID, + lastVisibleActionCreated: reportOnyx.lastVisibleActionCreated, + statusNum: reportOnyx.statusNum, + stateNum: reportOnyx.stateNum, + writeCapability: reportOnyx.writeCapability, + type: reportOnyx.type, + errorFields: reportOnyx.errorFields, + isPolicyExpenseChat: reportOnyx.isPolicyExpenseChat, + parentReportID: reportOnyx.parentReportID, + parentReportActionID: reportOnyx.parentReportActionID, + chatType: reportOnyx.chatType, + pendingFields: reportOnyx.pendingFields, + isDeletedParentAction: reportOnyx.isDeletedParentAction, + reportName: reportOnyx.reportName, + description: reportOnyx.description, + managerID: reportOnyx.managerID, + total: reportOnyx.total, + nonReimbursableTotal: reportOnyx.nonReimbursableTotal, + fieldList: reportOnyx.fieldList, + ownerAccountID: reportOnyx.ownerAccountID, + currency: reportOnyx.currency, + unheldTotal: reportOnyx.unheldTotal, + participants: reportOnyx.participants, + isWaitingOnBankAccount: reportOnyx.isWaitingOnBankAccount, + iouReportID: reportOnyx.iouReportID, + isOwnPolicyExpenseChat: reportOnyx.isOwnPolicyExpenseChat, + isPinned: reportOnyx.isPinned, + chatReportID: reportOnyx.chatReportID, + visibility: reportOnyx.visibility, + oldPolicyName: reportOnyx.oldPolicyName, + policyName: reportOnyx.policyName, + // eslint-disable-next-line @typescript-eslint/naming-convention + private_isArchived: reportOnyx.private_isArchived, + isOptimisticReport: reportOnyx.isOptimisticReport, + lastMentionedTime: reportOnyx.lastMentionedTime, + avatarUrl: reportOnyx.avatarUrl, + avatarFileName: reportOnyx.avatarFileName, + permissions, + invoiceReceiver: reportOnyx.invoiceReceiver, + policyAvatar: reportOnyx.policyAvatar, + }, + [reportOnyx, permissions], ); + const reportID = report?.reportID; const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setIsLinkingToMessage] = useState(!!reportActionIDFromRoute); const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: (value) => value?.accountID}); - const {reportActions, linkedAction, sortedAllReportActions} = usePaginatedReportActions(report.reportID, reportActionIDFromRoute); + const {reportActions, linkedAction, sortedAllReportActions} = usePaginatedReportActions(reportID, reportActionIDFromRoute); const [isBannerVisible, setIsBannerVisible] = useState(true); const [scrollPosition, setScrollPosition] = useState({}); @@ -278,7 +238,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; const isEmptyChat = useMemo(() => ReportUtils.isEmptyReport(report), [report]); - const isOptimisticDelete = report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; + const isOptimisticDelete = report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; const indexOfLinkedMessage = useMemo( (): number => reportActions.findIndex((obj) => String(obj.reportActionID) === String(reportActionIDFromRoute)), [reportActions, reportActionIDFromRoute], @@ -297,17 +257,17 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro const hasHelpfulErrors = Object.keys(report?.errorFields ?? {}).some((key) => key !== 'notFound'); const shouldHideReport = !hasHelpfulErrors && !ReportUtils.canAccessReport(report, policies, betas); - const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? [], isOffline); + const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportID ?? '', reportActions ?? [], isOffline); const [transactionThreadReportActions = {}] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`); const combinedReportActions = ReportActionsUtils.getCombinedReportActions(reportActions, transactionThreadReportID ?? null, Object.values(transactionThreadReportActions)); const lastReportAction = [...combinedReportActions, parentReportAction].find((action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action)); const isSingleTransactionView = ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report); - const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '-1'}`]; const isTopMostReportId = currentReportID === reportIDFromRoute; const didSubscribeToReportLeavingEvents = useRef(false); useEffect(() => { - if (!report.reportID || shouldHideReport) { + if (!report?.reportID || shouldHideReport) { wasReportAccessibleRef.current = false; return; } @@ -322,7 +282,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro Navigation.goBack(undefined, false, true); }, [isInNarrowPaneModal]); - let headerView = ( + let headerView = report && ( ); - if (isSingleTransactionView) { + if (isSingleTransactionView && report) { headerView = ( { - if (!transactionThreadReportID || !route?.params?.reportActionID || !ReportUtils.isOneTransactionThread(linkedAction?.childReportID ?? '-1', report.reportID, linkedAction)) { + if (!transactionThreadReportID || !route?.params?.reportActionID || !ReportUtils.isOneTransactionThread(linkedAction?.childReportID ?? '-1', reportID ?? '', linkedAction)) { return; } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(route?.params?.reportID)); - }, [transactionThreadReportID, route?.params?.reportActionID, route?.params?.reportID, linkedAction, report.reportID]); + }, [transactionThreadReportID, route?.params?.reportActionID, route?.params?.reportID, linkedAction, reportID]); - if (ReportUtils.isMoneyRequestReport(report) || ReportUtils.isInvoiceReport(report)) { + if (report && (ReportUtils.isMoneyRequestReport(report) || ReportUtils.isInvoiceReport(report))) { headerView = ( { // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely - const isTransitioning = report && report.reportID !== reportIDFromRoute; - return reportIDFromRoute !== '' && !!report.reportID && !isTransitioning; + const isTransitioning = report && report?.reportID !== reportIDFromRoute; + return reportIDFromRoute !== '' && !!report?.reportID && !isTransitioning; }, [report, reportIDFromRoute]); const isInitialPageReady = isOffline @@ -430,7 +390,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro return false; } - if (!wasReportAccessibleRef.current && !firstRenderRef.current && !report.reportID && !isOptimisticDelete && !reportMetadata?.isLoadingInitialReportActions && !userLeavingStatus) { + if (!wasReportAccessibleRef.current && !firstRenderRef.current && !reportID && !isOptimisticDelete && !reportMetadata?.isLoadingInitialReportActions && !userLeavingStatus) { return true; } @@ -442,7 +402,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro shouldShowNotFoundLinkedAction, isLoadingApp, finishedLoadingApp, - report.reportID, + reportID, isOptimisticDelete, reportMetadata?.isLoadingInitialReportActions, userLeavingStatus, @@ -455,11 +415,11 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro }, [reportIDFromRoute, reportActionIDFromRoute]); useEffect(() => { - if (!report.reportID || !isFocused) { + if (!reportID || !isFocused) { return; } - Report.updateLastVisitTime(report.reportID); - }, [report.reportID, isFocused]); + Report.updateLastVisitTime(reportID); + }, [reportID, isFocused]); const fetchReportIfNeeded = useCallback(() => { // Report ID will be empty when the reports collection is empty. @@ -480,7 +440,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro return; } - if (!shouldFetchReport(report) && (isInitialPageReady || isLinkedMessagePageReady)) { + if (report && !shouldFetchReport(report) && (isInitialPageReady || isLinkedMessagePageReady)) { return; } @@ -502,8 +462,8 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro return; } - clearReportNotifications(report.reportID); - }, [report.reportID, isTopMostReportId]); + clearReportNotifications(reportID ?? ''); + }, [reportID, isTopMostReportId]); useEffect(clearNotifications, [clearNotifications]); useAppFocusEvent(clearNotifications); @@ -521,7 +481,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro return; } - Report.unsubscribeFromLeavingRoomReportChannel(report.reportID); + Report.unsubscribeFromLeavingRoomReportChannel(reportID ?? ''); }; // I'm disabling the warning, as it expects to use exhaustive deps, even though we want this useEffect to run only on the first render. @@ -561,12 +521,12 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro ) { return; } - Report.openReport(report.reportID); + Report.openReport(reportID ?? ''); // We don't want to run this useEffect every time `report` is changed // Excluding shouldUseNarrowLayout from the dependency list to prevent re-triggering on screen resize events. // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [prevIsFocused, report.participants, isFocused, isSingleTransactionView]); + }, [prevIsFocused, report?.participants, isFocused, isSingleTransactionView, reportID]); useEffect(() => { // We don't want this effect to run on the first render. @@ -575,14 +535,14 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro return; } - const onyxReportID = report.reportID; - const prevOnyxReportID = prevReport.reportID; + const onyxReportID = report?.reportID; + const prevOnyxReportID = prevReport?.reportID; const wasReportRemoved = !!prevOnyxReportID && prevOnyxReportID === reportIDFromRoute && !onyxReportID; const isRemovalExpectedForReportType = isEmpty(report) && (ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport) || ReportUtils.isPolicyExpenseChat(prevReport) || ReportUtils.isGroupChat(prevReport)); - const didReportClose = wasReportRemoved && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; - const isTopLevelPolicyRoomWithNoStatus = !report.statusNum && !prevReport.parentReportID && prevReport.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM; + const didReportClose = wasReportRemoved && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; + const isTopLevelPolicyRoomWithNoStatus = !report?.statusNum && !prevReport?.parentReportID && prevReport?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM; const isClosedTopLevelPolicyRoom = wasReportRemoved && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && isTopLevelPolicyRoomWithNoStatus; // Navigate to the Concierge chat if the room was removed from another device (e.g. user leaving a room or removed from a room) if ( @@ -603,7 +563,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro Navigation.setShouldPopAllStateOnUP(true); Navigation.goBack(undefined, false, true); } - if (prevReport.parentReportID) { + if (prevReport?.parentReportID) { // Prevent navigation to the IOU/Expense Report if it is pending deletion. if (ReportUtils.isMoneyRequestReportPendingDeletion(prevReport.parentReportID)) { return; @@ -620,7 +580,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro // the ReportScreen never actually unmounts and the reportID in the route also doesn't change. // Therefore, we need to compare if the existing reportID is the same as the one in the route // before deciding that we shouldn't call OpenReport. - if (onyxReportID === prevReport.reportID && (!onyxReportID || onyxReportID === reportIDFromRoute)) { + if (onyxReportID === prevReport?.reportID && (!onyxReportID || onyxReportID === reportIDFromRoute)) { return; } @@ -632,12 +592,12 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro report, // errors, fetchReportIfNeeded, - prevReport.reportID, + prevReport?.reportID, prevUserLeavingStatus, userLeavingStatus, - prevReport.statusNum, - prevReport.parentReportID, - prevReport.chatType, + prevReport?.statusNum, + prevReport?.parentReportID, + prevReport?.chatType, prevReport, reportIDFromRoute, isFocused, @@ -650,14 +610,14 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro return; } // Ensures the optimistic report is created successfully - if (reportIDFromRoute !== report.reportID) { + if (reportIDFromRoute !== report?.reportID) { return; } // Ensures subscription event succeeds when the report/workspace room is created optimistically. // Check if the optimistic `OpenReport` or `AddWorkspaceRoom` has succeeded by confirming // any `pendingFields.createChat` or `pendingFields.addWorkspaceRoom` fields are set to null. // Existing reports created will have empty fields for `pendingFields`. - const didCreateReportSuccessfully = !report.pendingFields || (!report.pendingFields.addWorkspaceRoom && !report.pendingFields.createChat); + const didCreateReportSuccessfully = !report?.pendingFields || (!report?.pendingFields.addWorkspaceRoom && !report?.pendingFields.createChat); let interactionTask: ReturnType | null = null; if (!didSubscribeToReportLeavingEvents.current && didCreateReportSuccessfully) { interactionTask = InteractionManager.runAfterInteractions(() => { @@ -721,11 +681,11 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro }, [isLinkedActionInaccessibleWhisper]); useEffect(() => { - if (!!report.lastReadTime || !ReportUtils.isTaskReport(report)) { + if (!!report?.lastReadTime || !ReportUtils.isTaskReport(report)) { return; } // After creating the task report then navigating to task detail we don't have any report actions and the last read time is empty so We need to update the initial last read time when opening the task report detail. - Report.readNewestAction(report.reportID); + Report.readNewestAction(report?.reportID ?? ''); }, [report]); const mostRecentReportAction = reportActions[0]; const shouldShowMostRecentReportAction = @@ -750,7 +710,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro navigation={navigation} style={screenWrapperStyle} shouldEnableKeyboardAvoidingView={isTopMostReportId || isInNarrowPaneModal} - testID={`report-screen-${report.reportID}`} + testID={`report-screen-${reportID ?? ''}`} > {headerView} - {ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( + {report && ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( @@ -793,7 +753,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]} testID="report-actions-view-wrapper" > - {!shouldShowSkeleton && ( + {!shouldShowSkeleton && report && ( - {shouldShowMostRecentReportAction && ( + {shouldShowMostRecentReportAction && report && ( { const pronounsKey = currentUserPersonalDetails?.pronouns?.replace(CONST.PRONOUNS.PREFIX, '') ?? ''; diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index a7bd270a7a7f..316c0b66b95d 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -1,4 +1,5 @@ -import _ from 'lodash'; +import debounce from 'lodash/debounce'; +import isEmpty from 'lodash/isEmpty'; import type {ForwardedRef, RefObject} from 'react'; import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; import type {GestureResponderEvent} from 'react-native'; @@ -94,10 +95,10 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { }); const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState(false); - const hasBankAccount = !_.isEmpty(bankAccountList) || !_.isEmpty(fundList); - const hasWallet = !_.isEmpty(userWallet); + const hasBankAccount = !isEmpty(bankAccountList) || !isEmpty(fundList); + const hasWallet = !isEmpty(userWallet); const hasActivatedWallet = ([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM] as string[]).includes(userWallet?.tierName ?? ''); - const hasAssignedCard = !_.isEmpty(cardList); + const hasAssignedCard = !isEmpty(cardList); const shouldShowEmptyState = !hasBankAccount && !hasWallet && !hasAssignedCard; const isPendingOnfidoResult = userWallet?.isPendingOnfidoResult ?? false; @@ -111,7 +112,7 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { } }, [isLoadingPaymentMethods, network.isOffline, shouldShowLoadingSpinner]); - const debounceSetShouldShowLoadingSpinner = _.debounce(updateShouldShowLoadingSpinner, CONST.TIMING.SHOW_LOADING_SPINNER_DEBOUNCE_TIME); + const debounceSetShouldShowLoadingSpinner = debounce(updateShouldShowLoadingSpinner, CONST.TIMING.SHOW_LOADING_SPINNER_DEBOUNCE_TIME); /** * Set position of the payment menu @@ -308,7 +309,7 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { return; } if (shouldShowAddPaymentMenu) { - _.debounce(setMenuPosition, CONST.TIMING.RESIZE_DEBOUNCE_TIME)(); + debounce(setMenuPosition, CONST.TIMING.RESIZE_DEBOUNCE_TIME)(); return; } setMenuPosition(); @@ -329,9 +330,9 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { // We should reset selected payment method state values and close corresponding modals if the selected payment method is deleted let shouldResetPaymentMethodData = false; - if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && _.isEmpty(bankAccountList?.[paymentMethod.methodID])) { + if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && isEmpty(bankAccountList?.[paymentMethod.methodID])) { shouldResetPaymentMethodData = true; - } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD && _.isEmpty(fundList?.[paymentMethod.methodID])) { + } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD && isEmpty(fundList?.[paymentMethod.methodID])) { shouldResetPaymentMethodData = true; } if (shouldResetPaymentMethodData) { diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesDestinationPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesDestinationPage.tsx new file mode 100644 index 000000000000..51a80c57599b --- /dev/null +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesDestinationPage.tsx @@ -0,0 +1,69 @@ +import React, {useCallback} from 'react'; +import type {ValueOf} from 'type-fest'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import {settingsPendingAction} from '@libs/PolicyUtils'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import {updateSageIntacctNonreimbursableExpensesExportDestination} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +function SageIntacctNonReimbursableExpensesDestinationPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? '-1'; + const {config} = policy?.connections?.intacct ?? {}; + + const data: MenuListItem[] = Object.values(CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE).map((expenseType) => ({ + value: expenseType, + text: translate(`workspace.sageIntacct.nonReimbursableExpenses.values.${expenseType}`), + keyForList: expenseType, + isSelected: config?.export.nonReimbursable === expenseType, + })); + + const selectDestination = useCallback( + (row: MenuListItem) => { + if (row.value !== config?.export.nonReimbursable) { + updateSageIntacctNonreimbursableExpensesExportDestination(policyID, row.value, config?.export.nonReimbursable); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID)); + }, + [config?.export.nonReimbursable, policyID], + ); + + return ( + selectDestination(selection as MenuListItem)} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID))} + connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + pendingAction={settingsPendingAction([CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE], config?.pendingFields)} + errors={ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE)} + errorRowStyles={[styles.ph5, styles.pv3]} + onClose={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE)} + /> + ); +} + +SageIntacctNonReimbursableExpensesDestinationPage.displayName = 'SageIntacctNonReimbursableExpensesDestinationPage'; + +export default withPolicyConnections(SageIntacctNonReimbursableExpensesDestinationPage); diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx index 7297f5775565..354111de1489 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx @@ -1,169 +1,136 @@ -import React, {useCallback, useMemo} from 'react'; -import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; +import React from 'react'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; -import type {ListItem} from '@components/SelectionList/types'; -import type {SelectorType} from '@components/SelectionScreen'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, getSageIntacctNonReimbursableActiveDefaultVendor, settingsPendingAction} from '@libs/PolicyUtils'; -import Navigation from '@navigation/Navigation'; -import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import type {MenuItem, ToggleItem} from '@pages/workspace/accounting/intacct/types'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; -import {updateSageIntacctDefaultVendor, updateSageIntacctNonreimbursableExpensesExportDestination} from '@userActions/connections/SageIntacct'; +import {updateSageIntacctDefaultVendor} from '@userActions/connections/SageIntacct'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import {getDefaultVendorName} from './utils'; -type MenuListItem = ListItem & { - value: ValueOf; -}; +type MenuItemWithSubscribedSettings = Pick & {subscribedSettings?: string[]}; -function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyProps) { +type ToggleItemWithKey = ToggleItem & {key: string}; + +function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? '-1'; const styles = useThemeStyles(); const {data: intacctData, config} = policy?.connections?.intacct ?? {}; - const data: MenuListItem[] = Object.values(CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE).map((expenseType) => ({ - value: expenseType, - text: translate(`workspace.sageIntacct.nonReimbursableExpenses.values.${expenseType}`), - keyForList: expenseType, - isSelected: config?.export.nonReimbursable === expenseType, - })); + const activeDefaultVendor = getSageIntacctNonReimbursableActiveDefaultVendor(policy); + const defaultVendorName = getDefaultVendorName(activeDefaultVendor, intacctData?.vendors); - const selectNonReimbursableExpense = useCallback( - (row: MenuListItem) => { - if (row.value === config?.export.nonReimbursable) { - return; - } - updateSageIntacctNonreimbursableExpensesExportDestination(policyID, row.value, config?.export.nonReimbursable); + const menuItems: Array = [ + { + type: 'menuitem', + title: config?.export.nonReimbursable + ? translate(`workspace.sageIntacct.nonReimbursableExpenses.values.${config?.export.nonReimbursable}`) + : translate('workspace.sageIntacct.notConfigured'), + description: translate('workspace.accounting.exportAs'), + onPress: () => { + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_DESTINATION.getRoute(policyID)); + }, + subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE], }, - [config?.export.nonReimbursable, policyID], - ); - - const defaultVendor = useMemo(() => { - const activeDefaultVendor = getSageIntacctNonReimbursableActiveDefaultVendor(policy); - const defaultVendorName = getDefaultVendorName(activeDefaultVendor, intacctData?.vendors); - - const defaultVendorSection = { - description: translate('workspace.sageIntacct.defaultVendor'), - action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE.toLowerCase())), + { + type: 'menuitem', + title: config?.export.nonReimbursableAccount ? config.export.nonReimbursableAccount : translate('workspace.sageIntacct.notConfigured'), + description: translate('workspace.sageIntacct.creditCardAccount'), + onPress: () => { + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT.getRoute(policyID)); + }, + subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_ACCOUNT], + shouldHide: config?.export.nonReimbursable !== CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE, + }, + { + type: 'toggle', + title: translate('workspace.sageIntacct.defaultVendor'), + key: 'Default vendor toggle', + subtitle: translate('workspace.sageIntacct.defaultVendorDescription', false), + isActive: !!config?.export.nonReimbursableCreditCardChargeDefaultVendor, + switchAccessibilityLabel: translate('workspace.sageIntacct.defaultVendor'), + onToggle: (enabled) => { + const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : ''; + updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR, vendor, config?.export.nonReimbursableCreditCardChargeDefaultVendor); + }, + onCloseError: () => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR), + pendingAction: settingsPendingAction([CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR], config?.pendingFields), + errors: ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR), + shouldHide: config?.export.nonReimbursable !== CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE, + }, + { + type: 'menuitem', title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'), + description: translate('workspace.sageIntacct.defaultVendor'), + onPress: () => { + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE.toLowerCase())); + }, subscribedSettings: [ config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL ? CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_VENDOR : CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR, ], - }; - - return ( - - - - ); - }, [config?.errorFields, config?.export.nonReimbursable, config?.pendingFields, intacctData?.vendors, policy, policyID, translate]); - - const creditCardAccount = useMemo(() => { - const creditCardAccountSection = { - description: translate('workspace.sageIntacct.creditCardAccount'), - action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT.getRoute(policyID)), - title: config?.export.nonReimbursableAccount ? config.export.nonReimbursableAccount : translate('workspace.sageIntacct.notConfigured'), - subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_ACCOUNT], - }; - - return ( - - - - ); - }, [config?.errorFields, config?.export.nonReimbursableAccount, config?.pendingFields, policyID, translate]); + shouldHide: + !config?.export.nonReimbursable || + (config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE && !config?.export.nonReimbursableCreditCardChargeDefaultVendor), + }, + ]; return ( Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID))} accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} - displayName={SageIntacctNonReimbursableExpensesPage.displayName} policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + contentContainerStyle={styles.pb2} + titleStyle={styles.ph5} connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} - shouldUseScrollView={false} - shouldIncludeSafeAreaPaddingBottom > - Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE)} - style={[styles.flexGrow1, styles.flexShrink1]} - contentContainerStyle={[styles.flexGrow1, styles.flexShrink1]} - > - selectNonReimbursableExpense(selection as MenuListItem)} - sections={[{data}]} - ListItem={RadioListItem} - showScrollIndicator - shouldShowTooltips={false} - containerStyle={[styles.flexReset, styles.flexGrow1, styles.flexShrink1, styles.pb0]} - /> - - - {config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL && defaultVendor} - {config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE && ( - - {creditCardAccount} - { - const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : ''; - updateSageIntacctDefaultVendor( - policyID, - CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR, - vendor, - config?.export.nonReimbursableCreditCardChargeDefaultVendor, - ); - }} - wrapperStyle={[styles.ph5, styles.pv3]} - pendingAction={settingsPendingAction([CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR], config?.pendingFields)} - errors={ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR)} - onCloseError={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR)} - /> - {!!config?.export.nonReimbursableCreditCardChargeDefaultVendor && defaultVendor} - - )} - + {menuItems + .filter((item) => !item.shouldHide) + .map((item) => { + switch (item.type) { + case 'toggle': + // eslint-disable-next-line no-case-declarations + const {type, shouldHide, key, ...rest} = item; + return ( + + ); + default: + return ( + + + + ); + } + })} ); } diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx index 0617b8b690fe..25b42eca315b 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx @@ -1,4 +1,4 @@ -import {isEmpty} from 'lodash'; +import isEmpty from 'lodash/isEmpty'; import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import RadioListItem from '@components/SelectionList/RadioListItem'; diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesDestinationPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesDestinationPage.tsx new file mode 100644 index 000000000000..2517d1d6843e --- /dev/null +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesDestinationPage.tsx @@ -0,0 +1,73 @@ +import React, {useCallback} from 'react'; +import type {ValueOf} from 'type-fest'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import {settingsPendingAction} from '@libs/PolicyUtils'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import {changeMappingsValueFromDefaultToTag, updateSageIntacctReimbursableExpensesExportDestination} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +function SageIntacctReimbursableExpensesDestinationPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? '-1'; + const {config} = policy?.connections?.intacct ?? {}; + + const data: MenuListItem[] = Object.values(CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE).map((expenseType) => ({ + value: expenseType, + text: translate(`workspace.sageIntacct.reimbursableExpenses.values.${expenseType}`), + keyForList: expenseType, + isSelected: config?.export.reimbursable === expenseType, + })); + + const selectDestination = useCallback( + (row: MenuListItem) => { + if (row.value !== config?.export.reimbursable) { + updateSageIntacctReimbursableExpensesExportDestination(policyID, row.value, config?.export.reimbursable); + if (row.value === CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL) { + // Employee default mapping value is not allowed when expense type is VENDOR_BILL, so we have to change mapping value to Tag + changeMappingsValueFromDefaultToTag(policyID, config?.mappings); + } + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID)); + }, + [config?.export.reimbursable, config?.mappings, policyID], + ); + + return ( + selectDestination(selection as MenuListItem)} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID))} + connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + pendingAction={settingsPendingAction([CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE], config?.pendingFields)} + errors={ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE)} + errorRowStyles={[styles.ph5, styles.pv3]} + onClose={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE)} + /> + ); +} + +SageIntacctReimbursableExpensesDestinationPage.displayName = 'SageIntacctReimbursableExpensesDestinationPage'; + +export default withPolicyConnections(SageIntacctReimbursableExpensesDestinationPage); diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx index 360da1ac7a2d..e6a4bf669603 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx @@ -1,139 +1,118 @@ -import React, {useCallback, useMemo} from 'react'; -import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; +import React from 'react'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; -import type {ListItem} from '@components/SelectionList/types'; -import type {SelectorType} from '@components/SelectionScreen'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; -import Navigation from '@navigation/Navigation'; -import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import type {MenuItem, ToggleItem} from '@pages/workspace/accounting/intacct/types'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; -import {changeMappingsValueFromDefaultToTag, updateSageIntacctDefaultVendor, updateSageIntacctReimbursableExpensesExportDestination} from '@userActions/connections/SageIntacct'; +import {updateSageIntacctDefaultVendor} from '@userActions/connections/SageIntacct'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {SageIntacctDataElementWithValue} from '@src/types/onyx/Policy'; +import {getDefaultVendorName} from './utils'; -type MenuListItem = ListItem & { - value: ValueOf; -}; +type MenuItemWithSubscribedSettings = Pick & {subscribedSettings?: string[]}; -function getDefaultVendorName(defaultVendor?: string, vendors?: SageIntacctDataElementWithValue[]): string | undefined { - return (vendors ?? []).find((vendor) => vendor.id === defaultVendor)?.value ?? defaultVendor; -} +type ToggleItemWithKey = ToggleItem & {key: string}; -function SageIntacctReimbursableExpensesPage({policy}: WithPolicyProps) { +function SageIntacctReimbursableExpensesPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? '-1'; const styles = useThemeStyles(); const {data: intacctData, config} = policy?.connections?.intacct ?? {}; const {reimbursable, reimbursableExpenseReportDefaultVendor} = policy?.connections?.intacct?.config?.export ?? {}; - const data: MenuListItem[] = Object.values(CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE).map((expenseType) => ({ - value: expenseType, - text: translate(`workspace.sageIntacct.reimbursableExpenses.values.${expenseType}`), - keyForList: expenseType, - isSelected: reimbursable === expenseType, - })); + const defaultVendorName = getDefaultVendorName(reimbursableExpenseReportDefaultVendor, intacctData?.vendors); - const selectReimbursableDestination = useCallback( - (row: MenuListItem) => { - if (row.value !== reimbursable) { - updateSageIntacctReimbursableExpensesExportDestination(policyID, row.value, reimbursable); - } - if (row.value === CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL) { - // Employee default mapping value is not allowed when expense type is VENDOR_BILL, so we have to change mapping value to Tag - changeMappingsValueFromDefaultToTag(policyID, config?.mappings); - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID)); - } + const menuItems: Array = [ + { + type: 'menuitem', + title: reimbursable ? translate(`workspace.sageIntacct.reimbursableExpenses.values.${reimbursable}`) : translate('workspace.sageIntacct.notConfigured'), + description: translate('workspace.accounting.exportAs'), + onPress: () => { + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_REIMBURSABLE_DESTINATION.getRoute(policyID)); + }, + subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE], }, - [reimbursable, policyID, config?.mappings], - ); - - const defaultVendor = useMemo(() => { - const defaultVendorName = getDefaultVendorName(reimbursableExpenseReportDefaultVendor, intacctData?.vendors); - const defaultVendorSection = { - description: translate('workspace.sageIntacct.defaultVendor'), - action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE)), + { + type: 'toggle', + title: translate('workspace.sageIntacct.defaultVendor'), + key: 'Default vendor toggle', + subtitle: translate('workspace.sageIntacct.defaultVendorDescription', true), + isActive: !!config?.export.reimbursableExpenseReportDefaultVendor, + switchAccessibilityLabel: translate('workspace.sageIntacct.defaultVendor'), + onToggle: (enabled) => { + const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : ''; + updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR, vendor, config?.export.reimbursableExpenseReportDefaultVendor); + }, + onCloseError: () => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR), + pendingAction: settingsPendingAction([CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR], config?.pendingFields), + errors: ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR), + shouldHide: reimbursable !== CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT, + }, + { + type: 'menuitem', title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'), + description: translate('workspace.sageIntacct.defaultVendor'), + onPress: () => { + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE)); + }, subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR], - }; - - return ( - - - - ); - }, [config?.errorFields, config?.pendingFields, intacctData?.vendors, policyID, reimbursableExpenseReportDefaultVendor, translate]); + shouldHide: reimbursable !== CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT || !reimbursableExpenseReportDefaultVendor, + }, + ]; return ( Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID))} accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} - displayName={SageIntacctReimbursableExpensesPage.displayName} policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + contentContainerStyle={styles.pb2} + titleStyle={styles.ph5} connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} - shouldUseScrollView={false} - shouldIncludeSafeAreaPaddingBottom > - Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE)} - style={[styles.flexGrow1, styles.flexShrink1]} - contentContainerStyle={[styles.flexGrow1, styles.flexShrink1]} - > - selectReimbursableDestination(selection as MenuListItem)} - sections={[{data}]} - ListItem={RadioListItem} - showScrollIndicator - shouldShowTooltips={false} - containerStyle={[styles.flexReset, styles.flexGrow1, styles.flexShrink1, styles.pb0]} - /> - - {reimbursable === CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT && ( - - { - const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : ''; - updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR, vendor, reimbursableExpenseReportDefaultVendor); - }} - pendingAction={settingsPendingAction([CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR], config?.pendingFields)} - errors={ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR)} - wrapperStyle={[styles.ph5, styles.pv3]} - onCloseError={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR)} - /> - {!!reimbursableExpenseReportDefaultVendor && defaultVendor} - - )} + {menuItems + .filter((item) => !item.shouldHide) + .map((item) => { + switch (item.type) { + case 'toggle': + // eslint-disable-next-line no-case-declarations + const {type, shouldHide, key, ...rest} = item; + return ( + + ); + default: + return ( + + + + ); + } + })} ); } diff --git a/src/pages/workspace/accounting/intacct/types.ts b/src/pages/workspace/accounting/intacct/types.ts new file mode 100644 index 000000000000..79883a0c9104 --- /dev/null +++ b/src/pages/workspace/accounting/intacct/types.ts @@ -0,0 +1,30 @@ +import type {MenuItemProps} from '@components/MenuItem'; +import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; +import type {ToggleSettingOptionRowProps} from '@pages/workspace/workflows/ToggleSettingsOptionRow'; + +type MenuItem = MenuItemProps & { + /** Type of the item */ + type: 'menuitem'; + + /** The type of action that's pending */ + pendingAction: OfflineWithFeedbackProps['pendingAction']; + + /** Whether the item should be hidden */ + shouldHide?: boolean; + + /** Any error message to show */ + errors: OfflineWithFeedbackProps['errors']; + + /** Callback to close the error messages */ + onCloseError: OfflineWithFeedbackProps['onClose']; +}; + +type ToggleItem = ToggleSettingOptionRowProps & { + /** Type of the item */ + type: 'toggle'; + + /** Whether the item should be hidden */ + shouldHide?: boolean; +}; + +export type {MenuItem, ToggleItem}; diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx index b07a717a7abb..8e926995ec8e 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx @@ -1,4 +1,4 @@ -import {isEmpty} from 'lodash'; +import isEmpty from 'lodash/isEmpty'; import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import RadioListItem from '@components/SelectionList/RadioListItem'; diff --git a/src/pages/workspace/accounting/xero/export/XeroPreferredExporterSelectPage.tsx b/src/pages/workspace/accounting/xero/export/XeroPreferredExporterSelectPage.tsx index 23d178c725fe..ac3e37d60048 100644 --- a/src/pages/workspace/accounting/xero/export/XeroPreferredExporterSelectPage.tsx +++ b/src/pages/workspace/accounting/xero/export/XeroPreferredExporterSelectPage.tsx @@ -1,4 +1,4 @@ -import {isEmpty} from 'lodash'; +import isEmpty from 'lodash/isEmpty'; import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import RadioListItem from '@components/SelectionList/RadioListItem'; diff --git a/src/pages/workspace/accounting/xero/export/XeroPurchaseBillStatusSelectorPage.tsx b/src/pages/workspace/accounting/xero/export/XeroPurchaseBillStatusSelectorPage.tsx index bf656da26fda..4d0af9eb26fb 100644 --- a/src/pages/workspace/accounting/xero/export/XeroPurchaseBillStatusSelectorPage.tsx +++ b/src/pages/workspace/accounting/xero/export/XeroPurchaseBillStatusSelectorPage.tsx @@ -1,4 +1,4 @@ -import {isEmpty} from 'lodash'; +import isEmpty from 'lodash/isEmpty'; import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; diff --git a/src/pages/workspace/categories/CategoryApproverPage.tsx b/src/pages/workspace/categories/CategoryApproverPage.tsx index 390a577d9cf8..649681db6155 100644 --- a/src/pages/workspace/categories/CategoryApproverPage.tsx +++ b/src/pages/workspace/categories/CategoryApproverPage.tsx @@ -26,7 +26,7 @@ function CategoryApproverPage({ const {translate} = useLocalize(); const policy = usePolicy(policyID); - const selectedApprover = CategoryUtils.getCategoryApprover(policy?.rules?.approvalRules ?? [], categoryName) ?? ''; + const selectedApprover = CategoryUtils.getCategoryApproverRule(policy?.rules?.approvalRules ?? [], categoryName)?.approver ?? ''; return ( ; -}; - -type CategorySettingsPageProps = CategorySettingsPageOnyxProps & StackScreenProps; +type CategorySettingsPageProps = StackScreenProps; function CategorySettingsPage({ route: { params: {backTo, policyID, categoryName}, }, - policyCategories, navigation, }: CategorySettingsPageProps) { + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); const styles = useThemeStyles(); const {translate} = useLocalize(); const [deleteCategoryConfirmModalVisible, setDeleteCategoryConfirmModalVisible] = useState(false); @@ -83,7 +77,7 @@ function CategorySettingsPage({ }, [policyCategory?.maxExpenseAmount, policyCategoryExpenseLimitType, policyCurrency, translate]); const approverText = useMemo(() => { - const categoryApprover = CategoryUtils.getCategoryApprover(policy?.rules?.approvalRules ?? [], categoryName); + const categoryApprover = CategoryUtils.getCategoryApproverRule(policy?.rules?.approvalRules ?? [], categoryName)?.approver; return categoryApprover ?? ''; }, [categoryName, policy?.rules?.approvalRules]); @@ -133,6 +127,8 @@ function CategorySettingsPage({ }; const isThereAnyAccountingConnection = Object.keys(policy?.connections ?? {}).length !== 0; + const workflowApprovalsUnavailable = PolicyUtils.getWorkflowApprovalsUnavailable(policy); + const approverDisabled = !policy?.areWorkflowsEnabled || workflowApprovalsUnavailable; return ( )} - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - disabled={!policy?.areWorkflowsEnabled} - /> - - {!policy?.areWorkflowsEnabled && ( + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + disabled={approverDisabled} + /> + {approverDisabled && ( {translate('workspace.rules.categoryRules.goTo')}{' '} )} {policy?.tax?.trackingEnabled && ( - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> )} @@ -333,8 +325,4 @@ function CategorySettingsPage({ CategorySettingsPage.displayName = 'CategorySettingsPage'; -export default withOnyx({ - policyCategories: { - key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params.policyID}`, - }, -})(CategorySettingsPage); +export default CategorySettingsPage; diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 35ff78adba00..340bd991c609 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -8,6 +8,7 @@ import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import ConfirmModal from '@components/ConfirmModal'; +import DecisionModal from '@components/DecisionModal'; import EmptyStateComponent from '@components/EmptyStateComponent'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -55,13 +56,14 @@ type PolicyOption = ListItem & { type WorkspaceCategoriesPageProps = StackScreenProps; function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const {windowWidth} = useWindowDimensions(); const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [selectedCategories, setSelectedCategories] = useState>({}); + const [isDownloadFailureModalVisible, setIsDownloadFailureModalVisible] = useState(false); const [deleteCategoriesConfirmModalVisible, setDeleteCategoriesConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); const {environmentURL} = useEnvironment(); @@ -311,7 +313,9 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES_IMPORT.getRoute(policyId)); }, }, - { + ]; + if (hasVisibleCategories) { + menuItems.push({ icon: Expensicons.Download, text: translate('spreadsheet.downloadCSV'), onSelected: () => { @@ -319,13 +323,17 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { Modal.close(() => setIsOfflineModalVisible(true)); return; } - Category.downloadCategoriesCSV(policyId); + Modal.close(() => { + Category.downloadCategoriesCSV(policyId, () => { + setIsDownloadFailureModalVisible(true); + }); + }); }, - }, - ]; + }); + } return menuItems; - }, [policyId, translate, isOffline]); + }, [policyId, translate, isOffline, hasVisibleCategories]); const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout; @@ -418,6 +426,15 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { confirmText={translate('common.buttonConfirm')} shouldShowCancelButton={false} /> + setIsDownloadFailureModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={isDownloadFailureModalVisible} + onClose={() => setIsDownloadFailureModalVisible(false)} + /> ); diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index a950567e903e..87bf62cbf7b3 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -63,15 +63,15 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { subMenuItems: [ Navigation.navigate(ROUTES.RULES_CUSTOM_NAME.getRoute(policyID))} @@ -79,8 +79,8 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { , PolicyActions.setPolicyPreventMemberCreatedTitle(policyID, isEnabled)} />, ], diff --git a/src/pages/workspace/rules/RulesCustomNamePage.tsx b/src/pages/workspace/rules/RulesCustomNamePage.tsx index c32615415755..4a142c01e1da 100644 --- a/src/pages/workspace/rules/RulesCustomNamePage.tsx +++ b/src/pages/workspace/rules/RulesCustomNamePage.tsx @@ -40,7 +40,7 @@ function RulesCustomNamePage({route}: RulesCustomNamePageProps) { translate('workspace.rules.expenseReportRules.customNameTotalExample'), ] as const satisfies string[]; - const customNameDefaultValue = policy?.fieldList?.[CONST.POLICY.FIELD_LIST_TITLE_FIELD_ID].defaultValue; + const customNameDefaultValue = policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE].defaultValue; const validateCustomName = ({customName}: FormOnyxValues) => { const errors: FormInputErrors = {}; diff --git a/src/pages/workspace/tags/TagApproverPage.tsx b/src/pages/workspace/tags/TagApproverPage.tsx new file mode 100644 index 000000000000..874754b2cf4b --- /dev/null +++ b/src/pages/workspace/tags/TagApproverPage.tsx @@ -0,0 +1,57 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import WorkspaceMembersSelectionList from '@components/WorkspaceMembersSelectionList'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as Tag from '@userActions/Policy/Tag'; +import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; + +type TagApproverPageProps = StackScreenProps; + +function TagApproverPage({route}: TagApproverPageProps) { + const {policyID, tagName} = route.params; + + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const tagApprover = PolicyUtils.getTagApproverRule(policyID, tagName)?.approver; + + return ( + + + Navigation.goBack()} + /> + { + Tag.setPolicyTagApprover(policyID, tagName, email); + Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack); + }} + /> + + + ); +} + +TagApproverPage.displayName = 'TagApproverPage'; + +export default TagApproverPage; diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index a0f98ef699ed..6b70b6f636fe 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -1,8 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -12,6 +11,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import Switch from '@components/Switch'; import Text from '@components/Text'; +import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -27,73 +27,72 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {PolicyTagLists} from '@src/types/onyx'; -type TagSettingsPageOnyxProps = { - /** All policy tags */ - policyTags: OnyxEntry; -}; +type TagSettingsPageProps = StackScreenProps; -type TagSettingsPageProps = TagSettingsPageOnyxProps & StackScreenProps; - -function TagSettingsPage({route, policyTags, navigation}: TagSettingsPageProps) { +function TagSettingsPage({route, navigation}: TagSettingsPageProps) { + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${route.params.policyID}`); + const {orderWeight, policyID, tagName} = route.params; const styles = useThemeStyles(); const {translate} = useLocalize(); - const policyTag = useMemo(() => PolicyUtils.getTagList(policyTags, route.params.orderWeight), [policyTags, route.params.orderWeight]); - const policy = usePolicy(route.params.policyID); + const policyTag = useMemo(() => PolicyUtils.getTagList(policyTags, orderWeight), [policyTags, orderWeight]); + const policy = usePolicy(policyID); const [isDeleteTagModalOpen, setIsDeleteTagModalOpen] = React.useState(false); - const currentPolicyTag = policyTag.tags[route.params.tagName] ?? Object.values(policyTag.tags ?? {}).find((tag) => tag.previousTagName === route.params.tagName); + const currentPolicyTag = policyTag.tags[tagName] ?? Object.values(policyTag.tags ?? {}).find((tag) => tag.previousTagName === tagName); useEffect(() => { - if (currentPolicyTag?.name === route.params.tagName || !currentPolicyTag) { + if (currentPolicyTag?.name === tagName || !currentPolicyTag) { return; } navigation.setParams({tagName: currentPolicyTag?.name}); - }, [route.params.tagName, currentPolicyTag, navigation]); + }, [tagName, currentPolicyTag, navigation]); if (!currentPolicyTag) { return ; } const deleteTagAndHideModal = () => { - Tag.deletePolicyTags(route.params.policyID, [currentPolicyTag.name]); + Tag.deletePolicyTags(policyID, [currentPolicyTag.name]); setIsDeleteTagModalOpen(false); Navigation.goBack(); }; const updateWorkspaceTagEnabled = (value: boolean) => { - setWorkspaceTagEnabled(route.params.policyID, {[currentPolicyTag.name]: {name: currentPolicyTag.name, enabled: value}}, policyTag.orderWeight); + setWorkspaceTagEnabled(policyID, {[currentPolicyTag.name]: {name: currentPolicyTag.name, enabled: value}}, policyTag.orderWeight); }; const navigateToEditTag = () => { - Navigation.navigate(ROUTES.WORKSPACE_TAG_EDIT.getRoute(route.params.policyID, route.params.orderWeight, currentPolicyTag.name)); + Navigation.navigate(ROUTES.WORKSPACE_TAG_EDIT.getRoute(policyID, orderWeight, currentPolicyTag.name)); }; const navigateToEditGlCode = () => { if (!PolicyUtils.isControlPolicy(policy)) { Navigation.navigate( - ROUTES.WORKSPACE_UPGRADE.getRoute( - route.params.policyID, - CONST.UPGRADE_FEATURE_INTRO_MAPPING.glCodes.alias, - ROUTES.WORKSPACE_TAG_GL_CODE.getRoute(policy?.id ?? '', route.params.orderWeight, route.params.tagName), - ), + ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.glCodes.alias, ROUTES.WORKSPACE_TAG_GL_CODE.getRoute(policy?.id ?? '', orderWeight, tagName)), ); return; } - Navigation.navigate(ROUTES.WORKSPACE_TAG_GL_CODE.getRoute(route.params.policyID, route.params.orderWeight, currentPolicyTag.name)); + Navigation.navigate(ROUTES.WORKSPACE_TAG_GL_CODE.getRoute(policyID, orderWeight, currentPolicyTag.name)); + }; + + const navigateToEditTagApprover = () => { + Navigation.navigate(ROUTES.WORKSPACE_TAG_APPROVER.getRoute(policyID, orderWeight, currentPolicyTag.name)); }; const isThereAnyAccountingConnection = Object.keys(policy?.connections ?? {}).length !== 0; const isMultiLevelTags = PolicyUtils.isMultiLevelTags(policyTags); + const tagApprover = PolicyUtils.getTagApproverRule(policyID, route.params.tagName)?.approver; const shouldShowDeleteMenuItem = !isThereAnyAccountingConnection && !isMultiLevelTags; + const workflowApprovalsUnavailable = PolicyUtils.getWorkflowApprovalsUnavailable(policy); + const approverDisabled = !policy?.areWorkflowsEnabled || workflowApprovalsUnavailable; return ( Tag.clearPolicyTagErrors(route.params.policyID, route.params.tagName, route.params.orderWeight)} + onClose={() => Tag.clearPolicyTagErrors(policyID, tagName, orderWeight)} > @@ -150,6 +149,34 @@ function TagSettingsPage({route, policyTags, navigation}: TagSettingsPageProps) shouldShowRightIcon /> + + {policy?.areRulesEnabled && ( + <> + + {translate('workspace.tags.tagRules')} + + + {approverDisabled && ( + + {translate('workspace.rules.categoryRules.goTo')}{' '} + Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID))} + > + {translate('workspace.common.moreFeatures')} + {' '} + {translate('workspace.rules.categoryRules.andEnableWorkflows')} + + )} + + )} + {shouldShowDeleteMenuItem && ( ({ - policyTags: { - key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${route.params.policyID}`, - }, -})(TagSettingsPage); +export default TagSettingsPage; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 95a9907526e4..1f1be7e89c20 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1373,10 +1373,10 @@ type PendingJoinRequestPolicy = { /** Data informing when a given rule should be applied */ type ApplyRulesWhen = { /** The condition for applying the rule to the workspace */ - condition: 'matches'; + condition: string; /** The target field to which the rule is applied */ - field: 'category'; + field: string; /** The value of the target field */ value: string; @@ -1412,9 +1412,6 @@ type ExpenseRule = { id?: string; }; -/** The name of the category or tag */ -type CategoryOrTagName = string; - /** Model of policy data */ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< { @@ -1686,18 +1683,6 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** Workspace account ID configured for Expensify Card */ workspaceAccountID?: number; - - /** Information about rules being updated */ - pendingRulesUpdates?: Record< - CategoryOrTagName, - { - /** Indicates whether the approval rule is updated for the given category or tag */ - approvalRule: OnyxCommon.PendingAction; - - /** Indicates whether the expense rule is updated for the given category or tag */ - expenseRule: OnyxCommon.PendingAction; - } - >; } & Partial, 'addWorkspaceRoom' | keyof ACHAccount | keyof Attributes >; diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 2b915fcfc4f0..7777f04c92d7 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -113,7 +113,12 @@ describe('libs/NextStepUtils', () => { text: 'Waiting for ', }, { - text: `${currentUserEmail}'s`, + text: `${currentUserEmail}`, + clickToCopyText: `${currentUserEmail}`, + type: 'strong', + }, + { + text: "'s", type: 'strong', }, { @@ -143,7 +148,12 @@ describe('libs/NextStepUtils', () => { text: 'Waiting for ', }, { - text: `${currentUserEmail}'s`, + text: `${currentUserEmail}`, + clickToCopyText: `${currentUserEmail}`, + type: 'strong', + }, + { + text: "'s", type: 'strong', }, { @@ -173,7 +183,12 @@ describe('libs/NextStepUtils', () => { text: 'Waiting for ', }, { - text: `${currentUserEmail}'s`, + text: `${currentUserEmail}`, + clickToCopyText: `${currentUserEmail}`, + type: 'strong', + }, + { + text: "'s", type: 'strong', }, { @@ -203,7 +218,12 @@ describe('libs/NextStepUtils', () => { text: 'Waiting for ', }, { - text: `${currentUserEmail}'s`, + text: `${currentUserEmail}`, + clickToCopyText: `${currentUserEmail}`, + type: 'strong', + }, + { + text: "'s", type: 'strong', }, { @@ -234,7 +254,12 @@ describe('libs/NextStepUtils', () => { text: 'Waiting for ', }, { - text: `${currentUserEmail}'s`, + text: `${currentUserEmail}`, + clickToCopyText: `${currentUserEmail}`, + type: 'strong', + }, + { + text: "'s", type: 'strong', }, { @@ -267,7 +292,12 @@ describe('libs/NextStepUtils', () => { text: 'Waiting for ', }, { - text: `${currentUserEmail}'s`, + text: `${currentUserEmail}`, + clickToCopyText: `${currentUserEmail}`, + type: 'strong', + }, + { + text: "'s", type: 'strong', }, { @@ -298,7 +328,12 @@ describe('libs/NextStepUtils', () => { text: 'Waiting for ', }, { - text: `${currentUserEmail}'s`, + text: `${currentUserEmail}`, + clickToCopyText: `${currentUserEmail}`, + type: 'strong', + }, + { + text: "'s", type: 'strong', }, {