From ab1c62d9b7b18bd6df959dcbb8c41799a62b57f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20L=C3=A9ger?= Date: Wed, 20 Nov 2024 08:19:55 -0500 Subject: [PATCH 01/19] fix(mediaFiles): fix wrong path when syncing media files to OpenRosa server storage TASK-1238 (#5244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### đŸ“Ŗ Summary Corrected file path to ensure media files synchronization to OpenRosa server storage ### 📖 Description This fix addresses an issue where media files were not properly copied to the OpenRosa server storage, instead using the same file location as KPI. With this update, media files are now correctly synchronized to the OpenRosa server, ensuring that each system maintains its own file storage as intended. --- kpi/deployment_backends/openrosa_backend.py | 7 ++- kpi/tests/api/v2/test_api_submissions.py | 3 +- kpi/tests/test_deployment_backends.py | 50 ++++++++++++++++++++- kpi/tests/test_extended_file_field.py | 2 +- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/kpi/deployment_backends/openrosa_backend.py b/kpi/deployment_backends/openrosa_backend.py index c55a62ca3f..21ff83c1d2 100644 --- a/kpi/deployment_backends/openrosa_backend.py +++ b/kpi/deployment_backends/openrosa_backend.py @@ -1139,7 +1139,6 @@ def sync_media_files(self, file_type: str = AssetFile.FORM_MEDIA): 'md5': metadata['file_hash'], 'from_kpi': metadata['from_kpi'], } - metadata_filenames = metadata_files.keys() queryset = self._get_metadata_queryset(file_type=file_type) @@ -1339,7 +1338,11 @@ def _save_openrosa_metadata(self, file_: SyncBackendMediaInterface): } if not file_.is_remote_url: - metadata['data_file'] = file_.content + # Ensure file has not been read before + file_.content.seek(0) + file_content = file_.content.read() + file_.content.seek(0) + metadata['data_file'] = ContentFile(file_content, file_.filename) MetaData.objects.create(**metadata) diff --git a/kpi/tests/api/v2/test_api_submissions.py b/kpi/tests/api/v2/test_api_submissions.py index b659b4d28f..0b4a98a93b 100644 --- a/kpi/tests/api/v2/test_api_submissions.py +++ b/kpi/tests/api/v2/test_api_submissions.py @@ -5,12 +5,12 @@ import string import uuid from datetime import datetime +from unittest import mock try: from zoneinfo import ZoneInfo except ImportError: from backports.zoneinfo import ZoneInfo -from unittest import mock import lxml import pytest @@ -1972,7 +1972,6 @@ def test_duplicate_submission_as_owner_allowed(self): someuser is the owner of the project. someuser is allowed to duplicate their own data """ - print('URL :', self.submission_url, flush=True) response = self.client.post(self.submission_url, {'format': 'json'}) assert response.status_code == status.HTTP_201_CREATED self._check_duplicate(response) diff --git a/kpi/tests/test_deployment_backends.py b/kpi/tests/test_deployment_backends.py index 2a8f63126d..a3064d57c9 100644 --- a/kpi/tests/test_deployment_backends.py +++ b/kpi/tests/test_deployment_backends.py @@ -1,10 +1,15 @@ -# coding: utf-8 import pytest +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage from django.test import TestCase from kobo.apps.kobo_auth.shortcuts import User +from kobo.apps.openrosa.apps.main.models import MetaData +from kpi.deployment_backends.kc_access.storage import ( + default_kobocat_storage, +) from kpi.exceptions import DeploymentDataException -from kpi.models.asset import Asset +from kpi.models.asset import Asset, AssetFile from kpi.models.asset_version import AssetVersion @@ -151,3 +156,44 @@ def test_save_data_with_deferred_fields(self): # altered directly with self.assertRaises(DeploymentDataException) as e: asset.save() + + def test_sync_media_files(self): + + asset_file = AssetFile( + asset=self.asset, + user=self.asset.owner, + file_type=AssetFile.FORM_MEDIA, + ) + asset_file.content = ContentFile(b'foo', name='foo.txt') + asset_file.save() + assert ( + MetaData.objects.filter(xform=self.asset.deployment.xform).count() + == 0 + ) + meta_data = None + try: + self.asset.deployment.sync_media_files() + assert ( + MetaData.objects.filter( + xform=self.asset.deployment.xform + ).count() + == 1 + ) + meta_data = MetaData.objects.filter( + xform=self.asset.deployment.xform + ).first() + + assert default_kobocat_storage.exists(str(meta_data.data_file)) + assert not default_storage.exists(str(meta_data.data_file)) + + with default_kobocat_storage.open( + str(meta_data.data_file), 'r' + ) as f: + assert f.read() == 'foo' + finally: + # Clean-up + if meta_data: + data_file_path = str(meta_data.data_file) + meta_data.delete() + if default_kobocat_storage.exists(data_file_path): + default_kobocat_storage.delete(data_file_path) diff --git a/kpi/tests/test_extended_file_field.py b/kpi/tests/test_extended_file_field.py index 1ffa3982e2..6f561d5788 100644 --- a/kpi/tests/test_extended_file_field.py +++ b/kpi/tests/test_extended_file_field.py @@ -1,6 +1,6 @@ from django.core.files.base import ContentFile from django.core.files.storage import default_storage -from django.test import override_settings, TestCase +from django.test import TestCase from kpi.models.asset import Asset from kpi.models.asset_file import AssetFile From a89df99694a8599e3bf681f88e5a092ea1ba9233 Mon Sep 17 00:00:00 2001 From: Philip Edwards Date: Wed, 20 Nov 2024 13:18:28 -0500 Subject: [PATCH 02/19] test(jest): replace mocha-chrome with jest TASK-1234 (#5264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 👀 Preview steps - Check the CI log output — [✅ build (20.17.0)](https://github.com/kobotoolbox/kpi/actions/runs/11822549230/job/32939829801?pr=5264) - To run unit tests locally — use `npm run test` or `npm run test-watch` ### 💭 Notes Port unit tests to a Jest runner instead of running them in mocha-chrome. - Internal discussion: [flaky "npm-test" CI failure](https://chat.kobotoolbox.org/#narrow/stream/4-Kobo-Dev/topic/flaky.20.22npm-test.22.20CI.20failure) - **🃏 Jest Configs** — Added jest/**mocha.config.ts**, jest/**setupMochaTest.ts**, and jest/**coffeeTransformer.js** - ☕ The custom transformer accommodates our rare combination of current Jest with CoffeeScript 1.12.7. Easier than it seemed given good examples, and I was glad it didn't need more code to interop with es modules. - 🎓 I used `@ts-expect-error` when setting up some globals for the test environment. Lemme know if there's a more "correct" alternative, but I think the effect is limited - **🧰 Tests** - Changed test/**helper/fauxChai.coffee** and 4 other **.tests.coffee** files in test/**xlform/** - Added explicit `return` statements to fauxChai helpers and 4 other test files - Prevents CoffeeScript tests from implicitly returning objects, which is interpreted as a test failure in the new Jest environment. - See why Jest cares about this: https://github.com/jestjs/jest/issues/6516#issuecomment-399539647 - Added explicit import for `_` in utils.tests.coffee, matching other tests (instead of including it in the shared test environment) - **🌐 Frontend** - Changed js/**constants.ts** - 🗨ī¸ I added a check to prevent a `console.error` log from appearing if test globals are present - Otherwise the log appears frequently in the test logs, and may be interpreted as a failed test - **đŸ“Ļ Build** - Changed **package.json**, **package-lock.json**, and scripts/**hints.js** - **"scripts"** - Make `test` and `test-watch` use Jest - Keep `test-mocha-autobuild` for now - `test-mocha-autobuild` is possibly useful, since it allows logging objects to the browser console from a test for interactive inspection. - Update scripts/**hints.js**, which mainly exists because package.json doesn't allow comments - **"devDependencies"** - Add `@jest/create-cache-key-function`, `@types/coffeescript` for jest/**coffeeTransformer.js** maintainability - Add `whatwg-fetch` as a test environment global (for Node <= 20) - "overrides" - Synchronize versions of "coffeescript" used by webpack, jest transformer, and coffeelint. - I did this to reduce confusion. Coffeelint was causing an older version of "coffee-script" to appear in node_modules. - **â—ŧī¸ GitHub Actions** — Changed .github/workflows/**npm-test.yml** - Switch to the new jest command for unit tests, added `--ci`. - Removed chrome setup step, as mocha-chrome was the only step that needed it. - Also makes it easier to verify CI workflows locally with act --- .github/workflows/npm-test.yml | 49 +- jsapp/jest/coffeeTransformer.js | 44 + jsapp/jest/setupUnitTest.ts | 17 + jsapp/jest/unit.config.ts | 58 ++ jsapp/js/constants.ts | 6 +- package-lock.json | 1290 +------------------------- package.json | 14 +- patches/mocha-chrome+2.2.0.patch | 13 - scripts/hints.js | 8 +- test/helper/fauxChai.coffee | 12 + test/xlform/group.tests.coffee | 1 + test/xlform/inputParser.tests.coffee | 2 + test/xlform/model.tests.coffee | 2 + test/xlform/utils.tests.coffee | 7 +- 14 files changed, 229 insertions(+), 1294 deletions(-) create mode 100644 jsapp/jest/coffeeTransformer.js create mode 100644 jsapp/jest/setupUnitTest.ts create mode 100644 jsapp/jest/unit.config.ts delete mode 100644 patches/mocha-chrome+2.2.0.patch diff --git a/.github/workflows/npm-test.yml b/.github/workflows/npm-test.yml index a3aec92a36..302bc2d7bc 100644 --- a/.github/workflows/npm-test.yml +++ b/.github/workflows/npm-test.yml @@ -33,11 +33,8 @@ jobs: - name: Add "Node ${{ steps.resolved-node-version.outputs.NODE_VERSION }}" to summary run: echo "${{ matrix.node-version }} → **${{ steps.resolved-node-version.outputs.NODE_VERSION }}**" >> "$GITHUB_STEP_SUMMARY" - # Set up Chrome, for the unit tests - - uses: browser-actions/setup-chrome@latest - - run: chrome --version - - # Cache node_modules, keyed on os, node version, package-lock, and patches + # Cache: Use cache for node_modules + # Keyed on os, node version, package-lock, and patches - uses: actions/cache@v4 name: Check for cached node_modules id: cache-nodemodules @@ -47,37 +44,37 @@ jobs: path: node_modules key: ${{ runner.os }}-build-${{ env.cache-name }}-node-v${{ steps.resolved-node-version.outputs.NODE_VERSION }}-${{ hashFiles('**/package-lock.json', 'patches/**/*.patch') }} - # Cache hit: node_modules is copied from a previous run. Run copy-fonts - - if: steps.cache-nodemodules.outputs.cache-hit == 'true' - name: Run copy-fonts (if using cached node_modules) + # Cache hit: If the cache key matches, + # /node_modules/ will have been copied from a previous run. + # (Run the post-install step, `npm run copy-fonts`) + - name: Run copy-fonts (if using cached node_modules) + if: steps.cache-nodemodules.outputs.cache-hit == 'true' run: npm run copy-fonts - # Cache miss: Run npm install, which does copy-fonts as post-install step - - if: steps.cache-nodemodules.outputs.cache-hit != 'true' - name: Install JavaScript dependencies (npm install) + # Cache miss: If node_modules has not been cached, + # `npm install` + # (This includes `npm run copy-fonts` as post-install step) + - name: Install JavaScript dependencies (npm install) + if: steps.cache-nodemodules.outputs.cache-hit != 'true' run: npm install - # Build the app! + # Check that the full build succeeds - name: Build Prod run: SKIP_TS_CHECK=true npm run build - # Run TypeScript Checks and ESLint - - name: Check TypeScript # Separated for visibility + # Check for TypeScript errors + - name: Check TypeScript run: npm run check-types + + # Check for ESLint messages (errors only) - name: Check ESLint, errors only run: npm run lint -- --quiet - # Unit Tests - - name: Build Tests - run: npx webpack --config webpack/test.config.js + # Run the Unit test suite (formbuilder and helpers) + - name: Run unit tests and xlform tests + run: npx jest --config ./jsapp/jest/unit.config.ts --ci - - name: Run Tests, with mocha-chrome - run: npx mocha-chrome test/tests.html --chrome-launcher.connectionPollInterval=5000 - # This step takes less than 1 minute if it succeeds, but will hang for - # 6 hours if it fails with 'No inspectable targets' - # Timeout early to make it easier to manually re-run jobs. - # Tracking issue: https://github.com/kobotoolbox/kpi/issues/4337 - timeout-minutes: 1 + # Run the Jest test suite (React components) + - name: Run component tests with Jest + run: npx jest --config ./jsapp/jest/jest.config.ts --ci - - name: Run components tests with Jest - run: npm run jest diff --git a/jsapp/jest/coffeeTransformer.js b/jsapp/jest/coffeeTransformer.js new file mode 100644 index 0000000000..8e2b6b4b96 --- /dev/null +++ b/jsapp/jest/coffeeTransformer.js @@ -0,0 +1,44 @@ +const coffeescript = require('coffeescript'); +const createCacheKeyFunction = require('@jest/create-cache-key-function').default; +/** + * @typedef {import('@jest/transform').SyncTransformer} SyncTransformer + * @typedef {import('@jest/transform').TransformedSource} TransformedSource + */ + +/** + * Transform CoffeeScript files for Jest + * See: https://jestjs.io/docs/code-transformation + * + * @implements { SyncTransformer } + */ +module.exports = { + /** + * Process coffee files + * + * @param {string} sourceText + * @param {string} filename + * @returns {TransformedSource} + */ + process(sourceText, filename) { + const {js, sourceMap, v3SourceMap } = coffeescript.compile( + sourceText, + // ☕ CoffeeScript 1.12.7 compiler options + { + // 📜 For source maps + filename, + sourceMap: true, + + // đŸ“Ļ Same default as coffee-loader + bare: true, + } + ); + return { + code: js, + map: JSON.parse(v3SourceMap), + }; + }, + + getCacheKey: createCacheKeyFunction( + [__filename, require.resolve('coffeescript')], + ), +}; diff --git a/jsapp/jest/setupUnitTest.ts b/jsapp/jest/setupUnitTest.ts new file mode 100644 index 0000000000..ccb49175ce --- /dev/null +++ b/jsapp/jest/setupUnitTest.ts @@ -0,0 +1,17 @@ +import chai from 'chai'; +import $ from 'jquery'; + +// Polyfill global fetch (for Node 20 and older) +import 'whatwg-fetch'; + +// Add global t() mock (see /static/js/global_t.js) +global.t = (str: string) => str; + +// @ts-expect-error: ℹī¸ Add chai global for BDD-style tests +global.chai = chai; + +// @ts-expect-error: ℹī¸ Use chai's version of `expect` +global.expect = chai.expect; + +// @ts-expect-error: ℹī¸ Add jQuery globals for xlform code +global.jQuery = global.$ = $; diff --git a/jsapp/jest/unit.config.ts b/jsapp/jest/unit.config.ts new file mode 100644 index 0000000000..57b0ff0cb6 --- /dev/null +++ b/jsapp/jest/unit.config.ts @@ -0,0 +1,58 @@ +import type {Config} from 'jest'; +import {defaults} from 'jest-config'; + +// Config to run ☕ unit tests using the Jest runner +// +// To run the unit tests: 🏃 +// +// npx jest --config ./jsapp/jest/unit.config.ts +// + +const config: Config = { + // Naming convention (*.tests.*) + testMatch: ['**/?(*.)+(tests).(js|jsx|ts|tsx|es6|coffee)'], + + // Where to find tests. = 'kpi/jsapp/jest' + roots: [ + '/../js/', // unit tests 🛠ī¸ 'jsapp/js/**/*.tests.{ts,es6}' + '/../../test/', // xlform/coffee ☕ 'test/**/*.tests.coffee' + ], + + // Where to resolve module imports + moduleNameMapper: { + // ℹī¸ same aliases as in webpack.common.js (module.resolve.alias) + '^jsapp/(.+)$': '/../$1', // 📁 'jsapp/*' + '^js/(.*)$': '/../js/$1', // 📁 'js/*' + '^test/(.*)$': '/../../test/$1', // 📁 'test/*' + '^utils$': '/../js/utils', // 📄 'utils' + // 🎨 mock all CSS modules imported (styles.root = 'root') + '\\.(css|scss)$': 'identity-obj-proxy', + }, + + // Extensions to try in order (for import statements with no extension) + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'es6', 'coffee'], + + // Transformers (SWC for JS/TS, CoffeeScript for .coffee) + transform: { + '^.+\\.(js|jsx|ts|tsx|es6)$': '@swc/jest', + '^.+\\.coffee$': '/coffeeTransformer.js', + }, + + // Exclude these files, even if they contain tests + testPathIgnorePatterns: [ + 'test/xlform/integration.tests.coffee$', // 📄 skipped in test/index.js + ...defaults.testPathIgnorePatterns, // đŸ“Ļ exclude '/node_modules/' + ], + + // Set up test environment + testEnvironment: 'jsdom', + + // Make Chai and jQuery globals available in the test environment + setupFilesAfterEnv: ['/setupUnitTest.ts'], + + // Appearance options (for console output) + verbose: true, + displayName: {name: 'UNIT', color: 'black'}, +}; + +export default config; diff --git a/jsapp/js/constants.ts b/jsapp/js/constants.ts index 7a817b0e42..64d48bdfe7 100644 --- a/jsapp/js/constants.ts +++ b/jsapp/js/constants.ts @@ -28,8 +28,10 @@ export const ROOT_URL = (() => { ); let rootPath = ''; if (rootPathEl === null) { - console.error('no kpi-root-path meta tag set. defaulting to ""'); - rootPath = ''; + // @ts-expect-error: ℹī¸ global 'expect' indicates we're in a unit test + if (!globalThis.expect) { + console.error('no kpi-root-path meta tag set. defaulting to ""'); + } } else { // Strip trailing slashes rootPath = rootPathEl.content.replace(/\/*$/, ''); diff --git a/package-lock.json b/package-lock.json index 63ac11913d..d0288226ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,6 +80,7 @@ }, "devDependencies": { "@babel/preset-env": "^7.22.5", + "@jest/create-cache-key-function": "^29.7.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "@storybook/addon-a11y": "^7.0.24", "@storybook/addon-actions": "^7.0.24", @@ -96,6 +97,7 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@types/chai": "^4.3.1", + "@types/coffeescript": "^2.5.7", "@types/gtag.js": "^0.0.12", "@types/jest": "^29.5.13", "@types/jquery": "^3.5.10", @@ -150,7 +152,6 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mocha": "^7.2.0", - "mocha-chrome": "^2.2.0", "moment": "^2.30.1", "patch-package": "^6.5.1", "postcss": "^8.4.39", @@ -179,7 +180,8 @@ "webpack-cli": "^5.1.4", "webpack-dev-middleware": "^6.1.3", "webpack-dev-server": "^4.15.2", - "webpack-extract-translation-keys-plugin": "^6.1.0" + "webpack-extract-translation-keys-plugin": "^6.1.0", + "whatwg-fetch": "^3.6.20" }, "engines": { "node": "^20.17.0 || ^22.4.1" @@ -8261,6 +8263,16 @@ "integrity": "sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==", "dev": true }, + "node_modules/@types/coffeescript": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@types/coffeescript/-/coffeescript-2.5.7.tgz", + "integrity": "sha512-UXsSC38arfQNqQIKck80YBjL+FdQSPiWJ2lGb3E/bAZAmjuC0WLIOSYMZydiDI9ni6GW/4W3GSdvL8JJpf3jAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.1.15" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -9953,15 +9965,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -11223,29 +11226,6 @@ "node": ">=6" } }, - "node_modules/camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha512-Ej37YKYbFUI8QiYlvj9YHb6/Z60dZyPJW0Cs8sFilMbd2lP0bw3ylAq9yJkK4lcTA2dID5fG8LjmJYbO7kWb7Q==", - "dev": true, - "dependencies": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/camelcase-keys/node_modules/camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001662", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz", @@ -11404,113 +11384,6 @@ "node": ">=10" } }, - "node_modules/chrome-launcher": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.11.2.tgz", - "integrity": "sha512-jx0kJDCXdB2ARcDMwNCtrf04oY1Up4rOmVu+fqJ5MTPOOIG8EhRcEU9NZfXZc6dMw9FU8o1r21PNp8V2M0zQ+g==", - "dev": true, - "dependencies": { - "@types/node": "*", - "is-wsl": "^2.1.0", - "lighthouse-logger": "^1.0.0", - "mkdirp": "0.5.1", - "rimraf": "^2.6.1" - } - }, - "node_modules/chrome-launcher/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/chrome-launcher/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/chrome-launcher/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/chrome-launcher/node_modules/minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", - "dev": true - }, - "node_modules/chrome-launcher/node_modules/mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", - "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", - "dev": true, - "dependencies": { - "minimist": "0.0.8" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/chrome-launcher/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/chrome-remote-interface": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.28.2.tgz", - "integrity": "sha512-F7mjof7rWvRNsJqhVXuiFU/HWySCxTA9tzpLxUJxVfdLkljwFJ1aMp08AnwXRmmP7r12/doTDOMwaNhFCJsacw==", - "dev": true, - "dependencies": { - "commander": "2.11.x", - "ws": "^7.2.0" - }, - "bin": { - "chrome-remote-interface": "bin/client.js" - } - }, - "node_modules/chrome-remote-interface/node_modules/commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", - "dev": true - }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -11520,16 +11393,6 @@ "node": ">=6.0" } }, - "node_modules/chrome-unmirror": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chrome-unmirror/-/chrome-unmirror-0.1.0.tgz", - "integrity": "sha512-HmQgCN2UTpcrP85oOGnKpkGJFyOUwjsjnPBZlE8MkG0i+NoynGIkuPDZFKh+K4NLQlPiKKde16FAQ98JC1j8ew==", - "dev": true, - "engines": { - "node": ">=0.10.0", - "npm": ">=2.0.0" - } - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -11817,11 +11680,12 @@ } }, "node_modules/coffee-script": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.11.1.tgz", - "integrity": "sha512-NIWm59Fh1zkXq6TS6PQvSO3AR9DbGq1IBNZHa1E3fUCNmJhIwLf1YKcWgaHqaU7zWGC/OE2V7K3GVAXFzcmu+A==", + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", + "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", "deprecated": "CoffeeScript on NPM has moved to \"coffeescript\" (no hyphen)", "dev": true, + "license": "MIT", "bin": { "cake": "bin/cake", "coffee": "bin/coffee" @@ -12532,18 +12396,6 @@ "integrity": "sha512-wT5Y7mO8abrV16gnssKdmIhIbA9wSkeMzhh27jAguKrV82i24wER0vL5TGhUJ9dbJNDcigoRZ0IAHFEEEI4THQ==", "dev": true }, - "node_modules/currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", - "dev": true, - "dependencies": { - "array-find-index": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/d3": { "version": "3.5.17", "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", @@ -12742,19 +12594,6 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, - "node_modules/deep-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-3.0.0.tgz", - "integrity": "sha512-YX2i9XjJ7h5q/aQ/IM9PEwEnDqETAIYbggmdDB3HLTlSgo1CxPsj6pvhPG68rq6SVE0+p+6Ywsm5fTYNrYtBWw==", - "deprecated": "Check out `lodash.merge` or `merge-options` instead.", - "dev": true, - "dependencies": { - "is-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -16515,95 +16354,6 @@ "node": ">=8" } }, - "node_modules/import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "dependencies": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/import-local/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/import-local/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/import-local/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/import-local/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/import-local/node_modules/pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -17084,15 +16834,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -20185,12 +19926,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -20495,31 +20230,6 @@ "immediate": "~3.0.5" } }, - "node_modules/lighthouse-logger": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", - "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", - "dev": true, - "dependencies": { - "debug": "^2.6.9", - "marky": "^1.2.2" - } - }, - "node_modules/lighthouse-logger/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/lighthouse-logger/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -20534,34 +20244,6 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "node_modules/load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -20845,25 +20527,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==", - "dev": true, - "dependencies": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loud-rejection/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -21018,15 +20681,6 @@ "tmpl": "1.0.5" } }, - "node_modules/map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/map-or-similar": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", @@ -21045,12 +20699,6 @@ "react": ">= 0.14.0" } }, - "node_modules/marky": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", - "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", - "dev": true - }, "node_modules/mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -21125,44 +20773,6 @@ "map-or-similar": "^1.5.0" } }, - "node_modules/meow": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", - "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", - "dev": true, - "dependencies": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0", - "yargs-parser": "^10.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/meow/node_modules/camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/meow/node_modules/yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", - "dev": true, - "dependencies": { - "camelcase": "^4.1.0" - } - }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -21292,19 +20902,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minimist-options": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", - "dev": true, - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - }, - "engines": { - "node": ">= 4" - } - }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -21615,30 +21212,6 @@ "url": "https://opencollective.com/mochajs" } }, - "node_modules/mocha-chrome": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/mocha-chrome/-/mocha-chrome-2.2.0.tgz", - "integrity": "sha512-RXP6Q2mlM2X+eO2Z8gribmiH4J9x5zu/JcTZ3deQSwiC5260BzizOc0eD1NWP3JuypGCKRwReicv4KCNIFtTZQ==", - "dev": true, - "dependencies": { - "chalk": "^2.0.1", - "chrome-launcher": "^0.11.2", - "chrome-remote-interface": "^0.28.0", - "chrome-unmirror": "^0.1.0", - "debug": "^4.1.1", - "deep-assign": "^3.0.0", - "import-local": "^2.0.0", - "loglevel": "^1.4.1", - "meow": "^5.0.0", - "nanobus": "^4.2.0" - }, - "bin": { - "mocha-chrome": "cli.js" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/mocha/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -21931,23 +21504,6 @@ "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", "dev": true }, - "node_modules/nanoassert": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", - "integrity": "sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==", - "dev": true - }, - "node_modules/nanobus": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/nanobus/-/nanobus-4.5.0.tgz", - "integrity": "sha512-7sBZo9wthqNJ7QXnfVXZL7fkKJLN55GLOdX+RyZT34UOvxxnFtJe/c7K0ZRLAKOvaY1xJThFFn0Usw2H9R6Frg==", - "dev": true, - "dependencies": { - "nanoassert": "^1.1.0", - "nanotiming": "^7.2.0", - "remove-array-items": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -21966,25 +21522,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/nanoscheduler": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/nanoscheduler/-/nanoscheduler-1.0.3.tgz", - "integrity": "sha512-jBbrF3qdU9321r8n9X7yu18DjP31Do2ItJm3mWrt90wJTrnDO+HXpoV7ftaUglAtjgj9s+OaCxGufbvx6pvbEQ==", - "dev": true, - "dependencies": { - "nanoassert": "^1.1.0" - } - }, - "node_modules/nanotiming": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/nanotiming/-/nanotiming-7.3.1.tgz", - "integrity": "sha512-l3lC7v/PfOuRWQa8vV29Jo6TG10wHtnthLElFXs4Te4Aas57Fo4n1Q8LH9n+NDh9riOzTVvb2QNBhTS4JUKNjw==", - "dev": true, - "dependencies": { - "nanoassert": "^1.1.0", - "nanoscheduler": "^1.0.2" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -23262,15 +22799,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -24034,15 +23562,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", - "integrity": "sha512-tRS7sTgyxMXtLum8L65daJnHUhfDUgboRdcWW2bR9vBfrj2+O5HSMbQOJfJJjIVSPFqbBCF37FpwWXGitDc5tA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/quote-stream": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz", @@ -24581,112 +24100,6 @@ "react-dom": ">=16.6.0" } }, - "node_modules/read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", - "dev": true, - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", - "dev": true, - "dependencies": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "dev": true, - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", - "dev": true, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", - "dev": true, - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/readable-stream": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", @@ -24754,37 +24167,6 @@ "node": ">= 10.13.0" } }, - "node_modules/redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha512-XNwrTx77JQCEMXTeb8movBKuK75MgH0RZkujNuDKCezemx/voapl9i2gCSi8WWm8+ox5ycJi1gxF22fR7c0Ciw==", - "dev": true, - "dependencies": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/redent/node_modules/indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/redent/node_modules/strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -24963,12 +24345,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/remove-array-items": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/remove-array-items/-/remove-array-items-1.1.1.tgz", - "integrity": "sha512-MXW/jtHyl5F1PZI7NbpS8SOtympdLuF20aoWJT5lELR1p/HJDd5nqW8Eu9uLh/hCRY3FgvrIT5AwDCgBODklcA==", - "dev": true - }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -25248,27 +24624,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==", - "dev": true, - "dependencies": { - "resolve-from": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -28471,15 +27826,6 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true }, - "node_modules/trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha512-MTBWv3jhVjTU7XR3IQHllbiJs8sc75a80OEhB6or/q7pLTWgQ0bMGQXXYQSrSuXe6WiKWDZ5txXY5P59a/coVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -30097,6 +29443,13 @@ "node": ">=0.10.0" } }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "dev": true, + "license": "MIT" + }, "node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", @@ -30432,27 +29785,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "dev": true, - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -36057,6 +35389,15 @@ "integrity": "sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==", "dev": true }, + "@types/coffeescript": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@types/coffeescript/-/coffeescript-2.5.7.tgz", + "integrity": "sha512-UXsSC38arfQNqQIKck80YBjL+FdQSPiWJ2lGb3E/bAZAmjuC0WLIOSYMZydiDI9ni6GW/4W3GSdvL8JJpf3jAA==", + "dev": true, + "requires": { + "@types/babel__core": "^7.1.15" + } + }, "@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -37496,12 +36837,6 @@ "is-array-buffer": "^3.0.4" } }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", - "dev": true - }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -38427,25 +37762,6 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha512-Ej37YKYbFUI8QiYlvj9YHb6/Z60dZyPJW0Cs8sFilMbd2lP0bw3ylAq9yJkK4lcTA2dID5fG8LjmJYbO7kWb7Q==", - "dev": true, - "requires": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", - "dev": true - } - } - }, "caniuse-lite": { "version": "1.0.30001662", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz", @@ -38553,108 +37869,12 @@ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true }, - "chrome-launcher": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.11.2.tgz", - "integrity": "sha512-jx0kJDCXdB2ARcDMwNCtrf04oY1Up4rOmVu+fqJ5MTPOOIG8EhRcEU9NZfXZc6dMw9FU8o1r21PNp8V2M0zQ+g==", - "dev": true, - "requires": { - "@types/node": "*", - "is-wsl": "^2.1.0", - "lighthouse-logger": "^1.0.0", - "mkdirp": "0.5.1", - "rimraf": "^2.6.1" - }, - "dependencies": { - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "chrome-remote-interface": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.28.2.tgz", - "integrity": "sha512-F7mjof7rWvRNsJqhVXuiFU/HWySCxTA9tzpLxUJxVfdLkljwFJ1aMp08AnwXRmmP7r12/doTDOMwaNhFCJsacw==", - "dev": true, - "requires": { - "commander": "2.11.x", - "ws": "^7.2.0" - }, - "dependencies": { - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", - "dev": true - } - } - }, "chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true }, - "chrome-unmirror": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chrome-unmirror/-/chrome-unmirror-0.1.0.tgz", - "integrity": "sha512-HmQgCN2UTpcrP85oOGnKpkGJFyOUwjsjnPBZlE8MkG0i+NoynGIkuPDZFKh+K4NLQlPiKKde16FAQ98JC1j8ew==", - "dev": true - }, "ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -38874,9 +38094,9 @@ } }, "coffee-script": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.11.1.tgz", - "integrity": "sha512-NIWm59Fh1zkXq6TS6PQvSO3AR9DbGq1IBNZHa1E3fUCNmJhIwLf1YKcWgaHqaU7zWGC/OE2V7K3GVAXFzcmu+A==", + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", + "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", "dev": true }, "coffeelint": { @@ -38885,7 +38105,7 @@ "integrity": "sha512-6mzgOo4zb17WfdrSui/cSUEgQ0AQkW3gXDht+6lHkfkqGUtSYKwGdGcXsDfAyuScVzTlTtKdfwkAlJWfqul7zg==", "dev": true, "requires": { - "coffee-script": "~1.11.0", + "coffee-script": "^1.12.0", "glob": "^7.0.6", "ignore": "^3.0.9", "optimist": "^0.6.1", @@ -39404,15 +38624,6 @@ "integrity": "sha512-wT5Y7mO8abrV16gnssKdmIhIbA9wSkeMzhh27jAguKrV82i24wER0vL5TGhUJ9dbJNDcigoRZ0IAHFEEEI4THQ==", "dev": true }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, "d3": { "version": "3.5.17", "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", @@ -39554,15 +38765,6 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, - "deep-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-3.0.0.tgz", - "integrity": "sha512-YX2i9XjJ7h5q/aQ/IM9PEwEnDqETAIYbggmdDB3HLTlSgo1CxPsj6pvhPG68rq6SVE0+p+6Ywsm5fTYNrYtBWw==", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, "deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -42416,70 +41618,6 @@ "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", "dev": true }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - } - } - }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -42804,12 +41942,6 @@ "has-tostringtag": "^1.0.0" } }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", - "dev": true - }, "is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -44938,12 +44070,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -45200,33 +44326,6 @@ "immediate": "~3.0.5" } }, - "lighthouse-logger": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", - "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", - "dev": true, - "requires": { - "debug": "^2.6.9", - "marky": "^1.2.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - } - } - }, "lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -45238,30 +44337,6 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - } - } - }, "loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -45509,24 +44584,6 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - }, - "dependencies": { - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - } - } - }, "loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -45658,12 +44715,6 @@ "tmpl": "1.0.5" } }, - "map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==", - "dev": true - }, "map-or-similar": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", @@ -45677,12 +44728,6 @@ "dev": true, "requires": {} }, - "marky": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", - "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", - "dev": true - }, "mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -45739,40 +44784,6 @@ "map-or-similar": "^1.5.0" } }, - "meow": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", - "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0", - "yargs-parser": "^10.0.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", - "dev": true - }, - "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", - "dev": true, - "requires": { - "camelcase": "^4.1.0" - } - } - } - }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -45866,16 +44877,6 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, - "minimist-options": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - } - }, "minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -46294,24 +45295,6 @@ } } }, - "mocha-chrome": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/mocha-chrome/-/mocha-chrome-2.2.0.tgz", - "integrity": "sha512-RXP6Q2mlM2X+eO2Z8gribmiH4J9x5zu/JcTZ3deQSwiC5260BzizOc0eD1NWP3JuypGCKRwReicv4KCNIFtTZQ==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "chrome-launcher": "^0.11.2", - "chrome-remote-interface": "^0.28.0", - "chrome-unmirror": "^0.1.0", - "debug": "^4.1.1", - "deep-assign": "^3.0.0", - "import-local": "^2.0.0", - "loglevel": "^1.4.1", - "meow": "^5.0.0", - "nanobus": "^4.2.0" - } - }, "moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -46345,48 +45328,12 @@ "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", "dev": true }, - "nanoassert": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", - "integrity": "sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==", - "dev": true - }, - "nanobus": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/nanobus/-/nanobus-4.5.0.tgz", - "integrity": "sha512-7sBZo9wthqNJ7QXnfVXZL7fkKJLN55GLOdX+RyZT34UOvxxnFtJe/c7K0ZRLAKOvaY1xJThFFn0Usw2H9R6Frg==", - "dev": true, - "requires": { - "nanoassert": "^1.1.0", - "nanotiming": "^7.2.0", - "remove-array-items": "^1.0.0" - } - }, "nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true }, - "nanoscheduler": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/nanoscheduler/-/nanoscheduler-1.0.3.tgz", - "integrity": "sha512-jBbrF3qdU9321r8n9X7yu18DjP31Do2ItJm3mWrt90wJTrnDO+HXpoV7ftaUglAtjgj9s+OaCxGufbvx6pvbEQ==", - "dev": true, - "requires": { - "nanoassert": "^1.1.0" - } - }, - "nanotiming": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/nanotiming/-/nanotiming-7.3.1.tgz", - "integrity": "sha512-l3lC7v/PfOuRWQa8vV29Jo6TG10wHtnthLElFXs4Te4Aas57Fo4n1Q8LH9n+NDh9riOzTVvb2QNBhTS4JUKNjw==", - "dev": true, - "requires": { - "nanoassert": "^1.1.0", - "nanoscheduler": "^1.0.2" - } - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -47345,12 +46292,6 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true - }, "pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -47860,12 +46801,6 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, - "quick-lru": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", - "integrity": "sha512-tRS7sTgyxMXtLum8L65daJnHUhfDUgboRdcWW2bR9vBfrj2+O5HSMbQOJfJJjIVSPFqbBCF37FpwWXGitDc5tA==", - "dev": true - }, "quote-stream": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz", @@ -48247,89 +47182,6 @@ "prop-types": "^15.6.2" } }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "dependencies": { - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - } - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true - } - } - }, "readable-stream": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", @@ -48389,30 +47241,6 @@ "resolve": "^1.20.0" } }, - "redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha512-XNwrTx77JQCEMXTeb8movBKuK75MgH0RZkujNuDKCezemx/voapl9i2gCSi8WWm8+ox5ycJi1gxF22fR7c0Ciw==", - "dev": true, - "requires": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - }, - "dependencies": { - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==", - "dev": true - } - } - }, "redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -48555,12 +47383,6 @@ "unist-util-visit": "^2.0.0" } }, - "remove-array-items": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/remove-array-items/-/remove-array-items-1.1.1.tgz", - "integrity": "sha512-MXW/jtHyl5F1PZI7NbpS8SOtympdLuF20aoWJT5lELR1p/HJDd5nqW8Eu9uLh/hCRY3FgvrIT5AwDCgBODklcA==", - "dev": true - }, "renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -48772,23 +47594,6 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "dev": true - } - } - }, "resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -51232,12 +50037,6 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true }, - "trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha512-MTBWv3jhVjTU7XR3IQHllbiJs8sc75a80OEhB6or/q7pLTWgQ0bMGQXXYQSrSuXe6WiKWDZ5txXY5P59a/coVA==", - "dev": true - }, "ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -52367,6 +51166,12 @@ } } }, + "whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "dev": true + }, "whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", @@ -52618,13 +51423,6 @@ } } }, - "ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "dev": true, - "requires": {} - }, "xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", diff --git a/package.json b/package.json index f499422045..41f791d920 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "devDependencies": { "@babel/preset-env": "^7.22.5", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", + "@jest/create-cache-key-function": "^29.7.0", "@storybook/addon-a11y": "^7.0.24", "@storybook/addon-actions": "^7.0.24", "@storybook/addon-essentials": "^7.0.24", @@ -92,6 +93,7 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@types/chai": "^4.3.1", + "@types/coffeescript": "^2.5.7", "@types/gtag.js": "^0.0.12", "@types/jest": "^29.5.13", "@types/jquery": "^3.5.10", @@ -146,7 +148,6 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mocha": "^7.2.0", - "mocha-chrome": "^2.2.0", "moment": "^2.30.1", "patch-package": "^6.5.1", "postcss": "^8.4.39", @@ -175,7 +176,8 @@ "webpack-cli": "^5.1.4", "webpack-dev-middleware": "^6.1.3", "webpack-dev-server": "^4.15.2", - "webpack-extract-translation-keys-plugin": "^6.1.0" + "webpack-extract-translation-keys-plugin": "^6.1.0", + "whatwg-fetch": "^3.6.20" }, "overrides": { "reflux": { @@ -192,15 +194,17 @@ "@typescript-eslint/utils": "^6", "@typescript-eslint/rule-tester": "^6", "eslint": "$eslint" - } + }, + "coffee-script": "$coffeescript" }, "scripts": { "preinstall": "npm run hint", "postinstall": "patch-package && npm run copy-fonts", "build": "npm run hint build && webpack --config webpack/prod.config.js", "watch": "npm run hint watch && webpack-dev-server --config webpack/dev.server.js", - "test": "npm run hint test && webpack --config webpack/test.config.js && mocha-chrome test/tests.html", - "test-autobuild": "npm run hint test-autobuild && webpack --config webpack/test.autobuild.js", + "test": "jest --config ./jsapp/jest/unit.config.ts", + "test-watch": "jest --config ./jsapp/jest/unit.config.ts --watch", + "test-mocha-autobuild": "npm run hint test-mocha-autobuild && webpack --config webpack/test.autobuild.js", "lint": "eslint 'jsapp/js/**/*.{es6,js,jsx,ts,tsx}' --ext .es6,.js,.jsx,.ts,.tsx", "lint-coffee": "coffeelint -f ./coffeelint.json jsapp/xlform/src test", "lint-styles": "stylelint 'jsapp/**/*.{css,scss}' --ip jsapp/compiled --ip jsapp/fonts", diff --git a/patches/mocha-chrome+2.2.0.patch b/patches/mocha-chrome+2.2.0.patch deleted file mode 100644 index 147d8086a1..0000000000 --- a/patches/mocha-chrome+2.2.0.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/mocha-chrome/lib/client.js b/node_modules/mocha-chrome/lib/client.js -index 7629126..cd3d163 100644 ---- a/node_modules/mocha-chrome/lib/client.js -+++ b/node_modules/mocha-chrome/lib/client.js -@@ -9,7 +9,7 @@ module.exports = async function connectClient(instance, log, options) { - return fs.readFileSync(filePath, 'utf-8'); - } - -- const client = await CDP({ port: instance.port }); -+ const client = await CDP({ port: instance.port, host: '127.0.0.1' }); - const { DOM, DOMStorage, Console, Network, Page, Runtime } = client; - const mochaOptions = `window.mochaOptions = ${JSON.stringify(options.mocha)}`; - diff --git a/scripts/hints.js b/scripts/hints.js index da3a8e8b4c..175d52f041 100644 --- a/scripts/hints.js +++ b/scripts/hints.js @@ -28,13 +28,19 @@ const hints = { ${s.darkblue}Enjoy a quicker-launching dev server! `, - 'test-autobuild': ` + 'test-mocha-autobuild': ` This will rebuild the js tests on change. Open ${s.underline}file://${process.cwd()}/test/tests.html${s.nounderline} to see the test results in your browser. Reload the page to re-run the tests. + + If you don't need to inspect objects interactively + using the browser console, + + you can use 'npm run test-watch' to watch + or filter tests in the terminal console. `, SKIP_TS_CHECK: `${s.red} diff --git a/test/helper/fauxChai.coffee b/test/helper/fauxChai.coffee index d75a34853a..157e52e127 100644 --- a/test/helper/fauxChai.coffee +++ b/test/helper/fauxChai.coffee @@ -6,26 +6,38 @@ module.exports = expect: (x)-> toBe: (y)-> chaiExpect(x).to.equal(y) + return toThrow: (e)-> chaiExpect(x).to.throw(e) + return toBeDefined: ()-> chaiExpect(x).not.to.be.a('undefined') + return toContain: (y)-> chaiExpect(x).to.contain(y) + return toEqual: (y)-> chaiExpect(x).eql(y) + return toBeTruthy: -> chaiExpect(x).to.be.ok + return toBeUndefined: -> chaiExpect(x).to.be.a('undefined') + return 'not': toEqual: (y)-> chaiExpect(x).to.not.eql(y) + return toBe: (y)-> chaiExpect(x).to.not.equal(y) + return toThrow: (e)-> chaiExpect(x).to.not.throw(e) + return toBeTruthy: -> chaiExpect(x).to.not.be.ok + return toBeDefined: -> chaiExpect(x).to.be.a('undefined') + return diff --git a/test/xlform/group.tests.coffee b/test/xlform/group.tests.coffee index 6b295cf231..05e22c97b0 100644 --- a/test/xlform/group.tests.coffee +++ b/test/xlform/group.tests.coffee @@ -244,6 +244,7 @@ do -> ,end group,, """ ### + return describe 'group creation', -> beforeEach -> diff --git a/test/xlform/inputParser.tests.coffee b/test/xlform/inputParser.tests.coffee index b1eaf860be..bb4b700db6 100644 --- a/test/xlform/inputParser.tests.coffee +++ b/test/xlform/inputParser.tests.coffee @@ -20,6 +20,7 @@ do -> describe '. loadChoiceLists()"', -> list = new $choices.ChoiceList() $inputParser.loadChoiceLists($surveys.pizza_survey.main().choices, list) + return describe '. parse()"', -> describe ' translated surveys', -> @@ -62,6 +63,7 @@ do -> ] for i in [0, 1] expect(results[i]).toEqual(expected[i]) + return it 'parses group hierarchy', -> results = $inputParser.parseArr('survey', [ diff --git a/test/xlform/model.tests.coffee b/test/xlform/model.tests.coffee index 1819c72c60..e3fc57a7a2 100644 --- a/test/xlform/model.tests.coffee +++ b/test/xlform/model.tests.coffee @@ -42,6 +42,7 @@ xlform_survey_model = ($model)-> it "ensures every node has access to the parent survey", -> @pizzaSurvey.getSurvey + return it "can append a survey to another", -> dead_simple = @createSurvey(['text,q1,Question1,q1hint', 'text,q2,Question2,q2hint']) @@ -325,6 +326,7 @@ xlform_survey_model = ($model)-> survey_kuids = _as_json.survey.map((r)=>r['$kuid']) for kuid in survey_kuids expect(kuid).toBeDefined() + return describe "automatic naming", -> it "can import questions without names", -> diff --git a/test/xlform/utils.tests.coffee b/test/xlform/utils.tests.coffee index e23f49f70c..268e63e498 100644 --- a/test/xlform/utils.tests.coffee +++ b/test/xlform/utils.tests.coffee @@ -1,5 +1,6 @@ {expect} = require('../helper/fauxChai') $utils = require("../../jsapp/xlform/src/model.utils") +_ = require('underscore') pasted = [ ["list_name", "name", "label", "state", "county"], @@ -126,7 +127,8 @@ do -> splitted = $utils.split_paste(pasted) expect(splitted.length).toEqual(expectation.length) for i in [0..splitted.length] - _eqKeyVals(splitted[i], expectation[i]) + _eqKeyVals(splitted[i], expectation[i]) + return describe 'sluggify', -> it 'lowerCases: true', -> @@ -142,6 +144,7 @@ do -> ] for str in valid_xml expect($utils.isValidXmlTag(str)).toBeTruthy() + return it 'isValidXmlTag fails with invalid strings', -> invalid_xml = [ '1xyz', @@ -150,6 +153,7 @@ do -> ] for str in invalid_xml expect($utils.isValidXmlTag(str)).not.toBeTruthy() + return it 'handles a number of strings consistenly', -> inp_exps = [ @@ -164,4 +168,5 @@ do -> [str, additionals] = inps _out = $utils.sluggifyLabel(str, additionals) expect(_out).toBe(exps) + return From 9232e4a6424fe475c420fa1db41d75044ae4183f Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Wed, 20 Nov 2024 20:28:38 +0100 Subject: [PATCH 03/19] feat(organizations): create OrganizationSettingsRoute stub file TASK-981 (#5284) --- .../js/account/organizations/OrganizationSettingsRoute.tsx | 7 +++++++ jsapp/js/account/routes.constants.ts | 3 +++ jsapp/js/account/routes.tsx | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 jsapp/js/account/organizations/OrganizationSettingsRoute.tsx diff --git a/jsapp/js/account/organizations/OrganizationSettingsRoute.tsx b/jsapp/js/account/organizations/OrganizationSettingsRoute.tsx new file mode 100644 index 0000000000..f56accea7c --- /dev/null +++ b/jsapp/js/account/organizations/OrganizationSettingsRoute.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export default function OrganizationSettingsRoute() { + return ( +
Organization settings view to be implemented
+ ); +} diff --git a/jsapp/js/account/routes.constants.ts b/jsapp/js/account/routes.constants.ts index a8ff1262bd..0e5db9f414 100644 --- a/jsapp/js/account/routes.constants.ts +++ b/jsapp/js/account/routes.constants.ts @@ -19,6 +19,9 @@ export const AccountSettings = React.lazy( export const DataStorage = React.lazy( () => import(/* webpackPrefetch: true */ './usage/usageTopTabs') ); +export const OrganizationSettingsRoute = React.lazy( + () => import(/* webpackPrefetch: true */ './organizations/OrganizationSettingsRoute') +); export const ACCOUNT_ROUTES: {readonly [key: string]: string} = { ACCOUNT_SETTINGS: ROUTES.ACCOUNT_ROOT + '/settings', USAGE: ROUTES.ACCOUNT_ROOT + '/usage', diff --git a/jsapp/js/account/routes.tsx b/jsapp/js/account/routes.tsx index ba5f3a313e..927c1ef2e4 100644 --- a/jsapp/js/account/routes.tsx +++ b/jsapp/js/account/routes.tsx @@ -11,6 +11,7 @@ import { DataStorage, PlansRoute, SecurityRoute, + OrganizationSettingsRoute, } from 'js/account/routes.constants'; import {useFeatureFlag, FeatureFlag} from 'js/featureFlags'; @@ -137,7 +138,7 @@ export default function routes() { mmoOnly redirectRoute={ACCOUNT_ROUTES.ACCOUNT_SETTINGS} > -
Organization settings view to be implemented
+ } From dd3f7ad781c081fe16b155086bb3606321d6ea2b Mon Sep 17 00:00:00 2001 From: Philip Edwards Date: Wed, 20 Nov 2024 14:29:52 -0500 Subject: [PATCH 04/19] chore: remove in-browser mocha test environment (#5269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 👀 Preview steps 1. rebuild/run this branch as usual 1. đŸŸĸ note that http://kf.kobo.local/browser_tests/ now redirects to `/#/projects/home` ### 💭 Notes Cleanup after: - #5264 Removes the rest of `mocha` in favor of jest runner: remove `"test-mocha-autobuild"` script, the defunct `/browser_tests/` page and its dependencies. #### Removals - đŸ§Ē **Remove `test-mocha-autobuild` and test/index.html** > [#2890](https://github.com/kobotoolbox/kpi/issues/2890): In the past would load the mocha test suite and would provide an easy way to debug broken tests and write new ones. I've used the browser-based runner and was hesitant to remove it in [#5264](https://github.com/kobotoolbox/kpi/pull/5264). But, as others pointed out, we don't need to keep mocha around for this, as Jest supports both use cases. 1. For regular test-writing or debugging, like all the fixes in [#5264](https://github.com/kobotoolbox/kpi/pull/5264), or re-running tests on change, **use Jest's [watch mode](https://jestjs.io/docs/cli#--watch)** (`npm run test-watch`). It's pretty good. 2. For fixes like [`8d5a6ff0`](https://github.com/kobotoolbox/kpi/commit/8d5a6ff0527f6e2cb8e1cdff282365b924fc706e#diff-3cbd67091af2341ba4923ffe662f9f99a203de275d20a7f56e4524cc3235c7ae), where the console output might be misleading, or anywhere a step debugger or REPL could save time, see [Jest's troubleshooting page](https://jestjs.io/docs/troubleshooting#tests-are-failing-and-you-dont-know-why) with steps to **connect Node/Chrome Devtools**. - 🌐 **Remove [`/browser_tests/`](https://kf.kobotoolbox.org/browser_tests)** > [#2913](https://github.com/kobotoolbox/kpi/pull/2913) The tests could also be used to help identify if a user experiences a bug specific to their browser Deploying a test runner to a production server in order to help with bug triage is a nice idea. On the other hand, no one seems to have used this route since at least [#4311](https://github.com/kobotoolbox/kpi/pull/4311) when we broke it without realizing. --- jsapp/jest/unit.config.ts | 2 +- kobo/settings/base.py | 2 - kpi/templates/browser_tests.html | 23 - kpi/urls/__init__.py | 2 - kpi/views/__init__.py | 4 - package-lock.json | 1322 ------------------------------ package.json | 3 - scripts/hints.js | 15 - test/helper/phantomjs-shims.js | 34 - test/index.js | 31 - test/tests.html | 39 - webpack/dev.server.js | 1 - webpack/prod.config.js | 1 - webpack/test.autobuild.js | 16 - webpack/test.config.js | 28 - 15 files changed, 1 insertion(+), 1522 deletions(-) delete mode 100644 kpi/templates/browser_tests.html delete mode 100644 test/helper/phantomjs-shims.js delete mode 100644 test/index.js delete mode 100644 test/tests.html delete mode 100644 webpack/test.autobuild.js delete mode 100644 webpack/test.config.js diff --git a/jsapp/jest/unit.config.ts b/jsapp/jest/unit.config.ts index 57b0ff0cb6..1718ea094a 100644 --- a/jsapp/jest/unit.config.ts +++ b/jsapp/jest/unit.config.ts @@ -40,7 +40,7 @@ const config: Config = { // Exclude these files, even if they contain tests testPathIgnorePatterns: [ - 'test/xlform/integration.tests.coffee$', // 📄 skipped in test/index.js + 'test/xlform/integration.tests.coffee$', // 📄 skipped in `ee98aebe631b` ...defaults.testPathIgnorePatterns, // đŸ“Ļ exclude '/node_modules/' ], diff --git a/kobo/settings/base.py b/kobo/settings/base.py index 5d03a5b027..9dc4aa7a57 100644 --- a/kobo/settings/base.py +++ b/kobo/settings/base.py @@ -914,8 +914,6 @@ def __init__(self, *args, **kwargs): STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'jsapp'), os.path.join(BASE_DIR, 'static'), - ('mocha', os.path.join(BASE_DIR, 'node_modules', 'mocha'),), - ('chai', os.path.join(BASE_DIR, 'node_modules', 'chai'),), ) if os.path.exists(os.path.join(BASE_DIR, 'dkobo', 'jsapp')): diff --git a/kpi/templates/browser_tests.html b/kpi/templates/browser_tests.html deleted file mode 100644 index 4ab23f1588..0000000000 --- a/kpi/templates/browser_tests.html +++ /dev/null @@ -1,23 +0,0 @@ - - -{% load render_bundle from webpack_loader %} -{% load static %} - - - KPI tests - - - - - - - -
- {% render_bundle 'browsertests' 'js' %} - - - diff --git a/kpi/urls/__init__.py b/kpi/urls/__init__.py index cb3cbd7579..6b805a5858 100644 --- a/kpi/urls/__init__.py +++ b/kpi/urls/__init__.py @@ -7,7 +7,6 @@ from hub.models import ConfigurationFile from kpi.views import ( authorized_application_authenticate_user, - browser_tests, home, modern_browsers, ) @@ -42,7 +41,6 @@ authorized_application_authenticate_user, name='authenticate_user', ), - path('browser_tests/', browser_tests), path('modern_browsers/', modern_browsers), re_path(r'^i18n/', include('django.conf.urls.i18n')), # Translation catalog for client code. diff --git a/kpi/views/__init__.py b/kpi/views/__init__.py index af26887763..e375b4d370 100644 --- a/kpi/views/__init__.py +++ b/kpi/views/__init__.py @@ -17,10 +17,6 @@ def home(request): return TemplateResponse(request, 'index.html') -def browser_tests(request): - return TemplateResponse(request, 'browser_tests.html') - - def modern_browsers(request): return TemplateResponse(request, 'modern_browsers.html') diff --git a/package-lock.json b/package-lock.json index d0288226ea..8b2cad543d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,7 +117,6 @@ "@types/lodash.union": "^4.6.7", "@types/lodash.values": "^4.3.7", "@types/lodash.zip": "^4.2.7", - "@types/mocha": "^9.1.1", "@types/react": "^18.3.3", "@types/react-document-title": "^2.0.9", "@types/react-dom": "^18.3.0", @@ -151,7 +150,6 @@ "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "mocha": "^7.2.0", "moment": "^2.30.1", "patch-package": "^6.5.1", "postcss": "^8.4.39", @@ -8725,13 +8723,6 @@ "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true }, - "node_modules/@types/mocha": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", - "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "22.5.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", @@ -9755,15 +9746,6 @@ "node": ">=0.4.2" } }, - "node_modules/ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -10076,27 +10058,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.reduce": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.7.tgz", - "integrity": "sha512-mzmiUCVwtiD4lgxYP8g7IYy8El8p2CSMePvIbTS7gchKir/L1fgJrk0yDKmAX6mnRQFKNADYIk8nNlTris5H1Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-array-method-boxes-properly": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array.prototype.toreversed": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", @@ -10939,12 +10900,6 @@ "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", "dev": true }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, "node_modules/browserify-zlib": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", @@ -11520,81 +11475,6 @@ "node": ">=8" } }, - "node_modules/cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "dependencies": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -12866,15 +12746,6 @@ "node": ">= 4.0.0" } }, - "node_modules/diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -13360,12 +13231,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", - "dev": true - }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -15727,15 +15592,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true, - "engines": { - "node": ">=4.x" - } - }, "node_modules/gunzip-maybe": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", @@ -16561,29 +16417,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -20426,18 +20259,6 @@ "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==" }, - "node_modules/log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", - "dev": true, - "dependencies": { - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/loglevel": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", @@ -21169,299 +20990,6 @@ } } }, - "node_modules/mocha": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", - "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==", - "dev": true, - "dependencies": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "chokidar": "3.3.0", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "3.0.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.5", - "ms": "2.1.1", - "node-environment-flags": "1.0.6", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "1.6.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/mocha/node_modules/chokidar": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", - "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.2.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.1.1" - } - }, - "node_modules/mocha/node_modules/debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/mocha/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/mocha/node_modules/fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "deprecated": "\"Please update to latest v2.3 or v2.2\"", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/mocha/node_modules/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mocha/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/mocha/node_modules/js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/mocha/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mocha/node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "node_modules/mocha/node_modules/object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mocha/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/mocha/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/mocha/node_modules/readdirp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", - "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", - "dev": true, - "dependencies": { - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/mocha/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/mocha/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -21638,25 +21166,6 @@ "node": "*" } }, - "node_modules/node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "dev": true, - "dependencies": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, - "node_modules/node-environment-flags/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -21986,27 +21495,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz", - "integrity": "sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A==", - "dev": true, - "dependencies": { - "array.prototype.reduce": "^1.0.6", - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "gopd": "^1.0.1", - "safe-array-concat": "^1.1.2" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.groupby": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", @@ -24575,12 +24063,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, "node_modules/require-relative": { "version": "0.8.7", "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", @@ -29545,12 +29027,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "dev": true - }, "node_modules/which-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", @@ -29570,58 +29046,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "dependencies": { - "string-width": "^1.0.2 || 2" - } - }, - "node_modules/wide-align/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/wide-align/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/wide-align/node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/wide-align/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "dev": true, - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -29819,12 +29243,6 @@ "node": ">=0.4" } }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -29839,171 +29257,6 @@ "node": ">= 6" } }, - "node_modules/yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "dependencies": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "node_modules/yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "node_modules/yargs-unparser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", - "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", - "dev": true, - "dependencies": { - "flat": "^4.1.0", - "lodash": "^4.17.15", - "yargs": "^13.3.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/flat": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", - "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", - "dev": true, - "dependencies": { - "is-buffer": "~2.0.3" - }, - "bin": { - "flat": "cli.js" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "node_modules/yargs/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", @@ -35837,12 +35090,6 @@ "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true }, - "@types/mocha": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", - "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", - "dev": true - }, "@types/node": { "version": "22.5.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", @@ -36676,12 +35923,6 @@ "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", "optional": true }, - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true - }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -36915,21 +36156,6 @@ "es-shim-unscopables": "^1.0.0" } }, - "array.prototype.reduce": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.7.tgz", - "integrity": "sha512-mzmiUCVwtiD4lgxYP8g7IYy8El8p2CSMePvIbTS7gchKir/L1fgJrk0yDKmAX6mnRQFKNADYIk8nNlTris5H1Q==", - "dev": true, - "requires": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-array-method-boxes-properly": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "is-string": "^1.0.7" - } - }, "array.prototype.toreversed": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", @@ -37550,12 +36776,6 @@ "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", "dev": true }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, "browserify-zlib": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", @@ -37966,68 +37186,6 @@ } } }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - } - } - }, "clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -38962,12 +38120,6 @@ "debug": "4" } }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, "diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -39363,12 +38515,6 @@ "which-typed-array": "^1.1.15" } }, - "es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", - "dev": true - }, "es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -41184,12 +40330,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, "gunzip-maybe": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", @@ -41775,12 +40915,6 @@ "has-tostringtag": "^1.0.0" } }, - "is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true - }, "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -44509,15 +43643,6 @@ "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==" }, - "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", - "dev": true, - "requires": { - "chalk": "^2.4.2" - } - }, "loglevel": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", @@ -45074,227 +44199,6 @@ "integrity": "sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==", "requires": {} }, - "mocha": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", - "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==", - "dev": true, - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "chokidar": "3.3.0", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "3.0.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.5", - "ms": "2.1.1", - "node-environment-flags": "1.0.6", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "1.6.0" - }, - "dependencies": { - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chokidar": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", - "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.2.0" - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true - }, - "readdirp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", - "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", - "dev": true, - "requires": { - "picomatch": "^2.0.4" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, "moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -45436,24 +44340,6 @@ } } }, - "node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - }, - "dependencies": { - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true - } - } - }, "node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -45691,21 +44577,6 @@ "es-object-atoms": "^1.0.0" } }, - "object.getownpropertydescriptors": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz", - "integrity": "sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A==", - "dev": true, - "requires": { - "array.prototype.reduce": "^1.0.6", - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "gopd": "^1.0.1", - "safe-array-concat": "^1.1.2" - } - }, "object.groupby": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", @@ -47554,12 +46425,6 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, "require-relative": { "version": "0.8.7", "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", @@ -51242,12 +50107,6 @@ "is-weakset": "^2.0.3" } }, - "which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "dev": true - }, "which-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", @@ -51261,48 +50120,6 @@ "has-tostringtag": "^1.0.2" } }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -51446,12 +50263,6 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -51463,139 +50274,6 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yargs-unparser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", - "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", - "dev": true, - "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.15", - "yargs": "^13.3.0" - }, - "dependencies": { - "flat": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", - "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - } - } - } - }, "yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", diff --git a/package.json b/package.json index 41f791d920..d93907f11d 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,6 @@ "@types/lodash.union": "^4.6.7", "@types/lodash.values": "^4.3.7", "@types/lodash.zip": "^4.2.7", - "@types/mocha": "^9.1.1", "@types/react": "^18.3.3", "@types/react-document-title": "^2.0.9", "@types/react-dom": "^18.3.0", @@ -147,7 +146,6 @@ "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "mocha": "^7.2.0", "moment": "^2.30.1", "patch-package": "^6.5.1", "postcss": "^8.4.39", @@ -204,7 +202,6 @@ "watch": "npm run hint watch && webpack-dev-server --config webpack/dev.server.js", "test": "jest --config ./jsapp/jest/unit.config.ts", "test-watch": "jest --config ./jsapp/jest/unit.config.ts --watch", - "test-mocha-autobuild": "npm run hint test-mocha-autobuild && webpack --config webpack/test.autobuild.js", "lint": "eslint 'jsapp/js/**/*.{es6,js,jsx,ts,tsx}' --ext .es6,.js,.jsx,.ts,.tsx", "lint-coffee": "coffeelint -f ./coffeelint.json jsapp/xlform/src test", "lint-styles": "stylelint 'jsapp/**/*.{css,scss}' --ip jsapp/compiled --ip jsapp/fonts", diff --git a/scripts/hints.js b/scripts/hints.js index 175d52f041..20dc989397 100644 --- a/scripts/hints.js +++ b/scripts/hints.js @@ -28,21 +28,6 @@ const hints = { ${s.darkblue}Enjoy a quicker-launching dev server! `, - 'test-mocha-autobuild': ` - This will rebuild the js tests on change. - - Open ${s.underline}file://${process.cwd()}/test/tests.html${s.nounderline} - to see the test results in your browser. - - Reload the page to re-run the tests. - - If you don't need to inspect objects interactively - using the browser console, - - you can use 'npm run test-watch' to watch - or filter tests in the terminal console. - `, - SKIP_TS_CHECK: `${s.red} Skipping TypeScript check (${s.magenta}SKIP_TS_CHECK${s.red}) ${s.normal}`, diff --git a/test/helper/phantomjs-shims.js b/test/helper/phantomjs-shims.js deleted file mode 100644 index a715cc529e..0000000000 --- a/test/helper/phantomjs-shims.js +++ /dev/null @@ -1,34 +0,0 @@ -(function () { - - var Ap = Array.prototype; - var slice = Ap.slice; - var Fp = Function.prototype; - - if (!Fp.bind) { - // PhantomJS doesn't support Function.prototype.bind natively, so - // polyfill it whenever this module is required. - Fp.bind = function (context) { - var func = this; - var args = slice.call(arguments, 1); - - function bound() { - var invokedAsConstructor = func.prototype && (this instanceof func); - return func.apply( - // Ignore the context parameter when invoking the bound function - // as a constructor. Note that this includes not only constructor - // invocations using the new keyword but also calls to base class - // constructors such as BaseClass.call(this, ...) or super(...). - !invokedAsConstructor && context || this, - args.concat(slice.call(arguments)) - ); - } - - // The bound function must share the .prototype of the unbound - // function so that any object created by one constructor will count - // as an instance of both constructors. - bound.prototype = func.prototype; - - return bound; - }; - } -})(); diff --git a/test/index.js b/test/index.js deleted file mode 100644 index cbf8919300..0000000000 --- a/test/index.js +++ /dev/null @@ -1,31 +0,0 @@ -var chai = require('chai'); -var expect = chai.expect; - -window.jQuery = window.$ = require('jquery'); -require('jquery-ui/ui/widgets/sortable'); - -require('./xlform/aliases.tests'); -require('./xlform/choices.tests'); -require('./xlform/csv.tests'); -require('./xlform/deserializer.tests'); -require('./xlform/group.tests'); -require('./xlform/inputParser.tests'); -require('./xlform/translations.tests'); -// require('./xlform/integration.tests'); -require('./xlform/model.tests'); -require('./xlform/survey.tests'); -require('./xlform/utils.tests'); - -require('../jsapp/js/utils.tests'); -require('../jsapp/js/components/permissions/permParser.tests'); -require('../jsapp/js/components/permissions/utils.tests'); -require('../jsapp/js/components/permissions/userAssetPermsEditor.tests'); -require('../jsapp/js/components/formBuilder/formBuilderUtils.tests'); -require('../jsapp/js/assetUtils.tests'); -require('../jsapp/js/components/locking/lockingUtils.tests'); -require('../jsapp/js/components/submissions/submissionUtils.tests'); -require('../jsapp/js/projects/customViewStore.tests') -require('../jsapp/js/projects/projectViews/utils.tests'); -require('../jsapp/js/components/processing/processingUtils.tests'); -require('../jsapp/js/components/processing/routes.utils.tests'); -require('../jsapp/js/components/submissions/tableUtils.tests'); diff --git a/test/tests.html b/test/tests.html deleted file mode 100644 index d4556841a5..0000000000 --- a/test/tests.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - KPI Tests - - - - - - - -
- - - - - diff --git a/webpack/dev.server.js b/webpack/dev.server.js index 27f2c074ce..884324cb16 100644 --- a/webpack/dev.server.js +++ b/webpack/dev.server.js @@ -27,7 +27,6 @@ let devConfig = WebpackCommon({ }, entry: { app: ['./jsapp/js/main.es6'], - browsertests: path.resolve(__dirname, '../test/index.js'), }, output: { library: 'KPI', diff --git a/webpack/prod.config.js b/webpack/prod.config.js index 5a4002e656..cffd976ec3 100644 --- a/webpack/prod.config.js +++ b/webpack/prod.config.js @@ -30,7 +30,6 @@ const prodConfig = WebpackCommon({ }, entry: { app: './jsapp/js/main.es6', - browsertests: path.resolve(__dirname, '../test/index.js'), }, output: { path: path.resolve(__dirname, '../jsapp/compiled/'), diff --git a/webpack/test.autobuild.js b/webpack/test.autobuild.js deleted file mode 100644 index 2e17e977cf..0000000000 --- a/webpack/test.autobuild.js +++ /dev/null @@ -1,16 +0,0 @@ -const testConfig = require('./test.config'); - -// Auto-builds tests when the tests or js files change. -// Doesn't re-run tests or auto-reload any pages. - -// It's useful for visiting file://{/path/to/kpi}/test/tests.html -// to troubleshoot failing tests (refresh to re-run). - -// You can use interactive browser console to inspect logged objects. -// example: console.log(expected, actual) in a test file. - -module.exports = { - ...testConfig, - watch: true, - stats: {}, // un-hide output from test.config.js -}; diff --git a/webpack/test.config.js b/webpack/test.config.js deleted file mode 100644 index 984b6049e6..0000000000 --- a/webpack/test.config.js +++ /dev/null @@ -1,28 +0,0 @@ -const path = require('path'); -const WebpackCommon = require('./webpack.common'); - -const testConfig = WebpackCommon({ - mode: 'development', - entry: path.resolve(__dirname, '../test/index.js'), - output: { - library: 'tests', - path: path.resolve(__dirname, '../test/compiled/'), - filename: 'webpack-built-tests.js', - }, - // mainly for hiding stylelint output - stats: { - all: false, - modulesSpace: 0, - errors: true, - errorDetails: true, - }, -}); - -// Print speed measurements if env variable MEASURE_WEBPACK_PLUGIN_SPEED is set -if (process.env.MEASURE_WEBPACK_PLUGIN_SPEED) { - const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); - const smp = new SpeedMeasurePlugin(); - module.exports = smp.wrap(testConfig); -} else { - module.exports = testConfig; -} From 62d4113c920f91b7828baf30334656a53538c04f Mon Sep 17 00:00:00 2001 From: Akuukis Date: Wed, 20 Nov 2024 22:07:26 +0200 Subject: [PATCH 05/19] feat(organizations): auto-refresh on orgId change TASK-1247 (#5285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### đŸ“Ŗ Summary On organization change auto-refresh the new organization for logged-in user. ### 👀 Preview steps 1. ℹī¸ have an account and MMO enabled organization (belonging to another user) 2. log in, keep tab open 3. switch to admin panel, add account to the organization 4. switch back to account tab, notice "No Organization matches the given query." error toast. 5. 🔴 [on main] notice that toast keep going forever once a second or so. 6. đŸŸĸ [on PR] notice that toast stops after 3-4 instances. --------- Co-authored-by: James Kiger --- jsapp/js/account/stripe.api.ts | 23 ++++++++++++++++++----- jsapp/js/api.endpoints.ts | 3 +-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/jsapp/js/account/stripe.api.ts b/jsapp/js/account/stripe.api.ts index b1e2c267a1..216d441537 100644 --- a/jsapp/js/account/stripe.api.ts +++ b/jsapp/js/account/stripe.api.ts @@ -2,7 +2,7 @@ import {when} from 'mobx'; import subscriptionStore from 'js/account/subscriptionStore'; import {endpoints} from 'js/api.endpoints'; import {ACTIVE_STRIPE_STATUSES} from 'js/constants'; -import type {PaginatedResponse} from 'js/dataInterface'; +import type {FailResponse, PaginatedResponse} from 'js/dataInterface'; import envStore from 'js/envStore'; import {fetchGet, fetchGetUrl, fetchPost} from 'jsapp/js/api'; import type { @@ -20,6 +20,7 @@ import {useQuery} from '@tanstack/react-query'; import {QueryKeys} from 'js/query/queryKeys'; import {FeatureFlag, useFeatureFlag} from '../featureFlags'; import sessionStore from 'js/stores/session'; +import {useEffect} from 'react'; const DEFAULT_LIMITS: AccountLimit = Object.freeze({ submission_limit: Limits.unlimited, @@ -91,11 +92,23 @@ export const useOrganizationQuery = () => { sessionStore.isInitialLoadComplete && !!organizationUrl; - return useQuery({ + const query = useQuery({ queryFn: fetchOrganization, queryKey: [QueryKeys.organization], enabled: isQueryEnabled, }); + + // `organizationUrl` must exist, unless it's changed (e.g. user added/removed from organization). + // In such case, refetch organizationUrl to fetch the new `organizationUrl`. + // DEBT: don't throw toast within fetchGetUrl. + // DEBT: don't retry the failing url 3-4 times before switching to the new url. + useEffect(() => { + if (query.error?.status === 404) { + sessionStore.refreshAccount(); + } + }, [query.error?.status]); + + return query; }; /** @@ -121,7 +134,7 @@ export async function postCheckout( */ export async function postCustomerPortal( organizationId: string, - priceId: string = '', + priceId = '', quantity = 1 ) { return fetchPost( @@ -247,7 +260,7 @@ const addRemainingOneTimeAddOnLimits = ( oneTimeAddOns: OneTimeAddOn[] ) => { // This yields a separate object, so we need to make a copy - limits = {...limits} + limits = {...limits}; oneTimeAddOns .filter((addon) => addon.is_available) .forEach((addon) => { @@ -345,7 +358,7 @@ export async function getAccountLimits( } // create separate object with one-time addon limits added to the limits calculated so far - let remainingLimits = addRemainingOneTimeAddOnLimits( + const remainingLimits = addRemainingOneTimeAddOnLimits( recurringLimits, oneTimeAddOns ); diff --git a/jsapp/js/api.endpoints.ts b/jsapp/js/api.endpoints.ts index a60fcbd328..3a4c1a1485 100644 --- a/jsapp/js/api.endpoints.ts +++ b/jsapp/js/api.endpoints.ts @@ -5,7 +5,6 @@ export const endpoints = { PRODUCTS_URL: '/api/v2/stripe/products/', SUBSCRIPTION_URL: '/api/v2/stripe/subscriptions/', ADD_ONS_URL: '/api/v2/stripe/addons/', - ORGANIZATION_URL: '/api/v2/organizations/', /** Expected parameters: price_id and organization_id **/ CHECKOUT_URL: '/api/v2/stripe/checkout-link', /** Expected parameter: organization_id **/ @@ -14,4 +13,4 @@ export const endpoints = { CHANGE_PLAN_URL: '/api/v2/stripe/change-plan', ACCESS_LOGS_URL: '/api/v2/access-logs/me', LOGOUT_ALL: '/logout-all/', -}; +} as const; From ab59c000010e674edf5f2fef2791612d85fd1fda Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Wed, 20 Nov 2024 21:15:38 +0100 Subject: [PATCH 06/19] refactor(organizations): rename organizations directory (#5286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 🗒ī¸ Checklist 1. [x] run linter locally 2. [ ] update all related docs (API, README, inline, etc.), if any 3. [x] draft PR with a title `(): TASK-1234` 4. [x] tag PR: at least `frontend` or `backend` unless it's global 5. [x] fill in the template below and delete template comments 6. [x] review thyself: read the diff and repro the preview as written 7. [x] open PR & confirm that CI passes 8. [ ] request reviewers, if needed 9. [ ] delete this section before merging --- jsapp/js/account/accountSidebar.tsx | 2 +- .../OrganizationSettingsRoute.tsx | 0 .../organization.utils.tsx} | 0 jsapp/js/account/routes.constants.ts | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename jsapp/js/account/{organizations => organization}/OrganizationSettingsRoute.tsx (100%) rename jsapp/js/account/{organizations/organizations.utils.tsx => organization/organization.utils.tsx} (100%) diff --git a/jsapp/js/account/accountSidebar.tsx b/jsapp/js/account/accountSidebar.tsx index 1966d65fee..28ab205d1f 100644 --- a/jsapp/js/account/accountSidebar.tsx +++ b/jsapp/js/account/accountSidebar.tsx @@ -13,7 +13,7 @@ import {ACCOUNT_ROUTES} from 'js/account/routes.constants'; import {useOrganizationQuery} from './stripe.api'; import {OrganizationUserRole} from './stripe.types'; import {useFeatureFlag, FeatureFlag} from 'js/featureFlags'; -import {getSimpleMMOLabel} from './organizations/organizations.utils'; +import {getSimpleMMOLabel} from './organization/organization.utils'; import LoadingSpinner from 'js/components/common/loadingSpinner'; interface AccountNavLinkProps { diff --git a/jsapp/js/account/organizations/OrganizationSettingsRoute.tsx b/jsapp/js/account/organization/OrganizationSettingsRoute.tsx similarity index 100% rename from jsapp/js/account/organizations/OrganizationSettingsRoute.tsx rename to jsapp/js/account/organization/OrganizationSettingsRoute.tsx diff --git a/jsapp/js/account/organizations/organizations.utils.tsx b/jsapp/js/account/organization/organization.utils.tsx similarity index 100% rename from jsapp/js/account/organizations/organizations.utils.tsx rename to jsapp/js/account/organization/organization.utils.tsx diff --git a/jsapp/js/account/routes.constants.ts b/jsapp/js/account/routes.constants.ts index 0e5db9f414..11ee1897e3 100644 --- a/jsapp/js/account/routes.constants.ts +++ b/jsapp/js/account/routes.constants.ts @@ -20,7 +20,7 @@ export const DataStorage = React.lazy( () => import(/* webpackPrefetch: true */ './usage/usageTopTabs') ); export const OrganizationSettingsRoute = React.lazy( - () => import(/* webpackPrefetch: true */ './organizations/OrganizationSettingsRoute') + () => import(/* webpackPrefetch: true */ './organization/OrganizationSettingsRoute') ); export const ACCOUNT_ROUTES: {readonly [key: string]: string} = { ACCOUNT_SETTINGS: ROUTES.ACCOUNT_ROOT + '/settings', From 7abd4f17af835eb2e92de2e1345dadb4d439629c Mon Sep 17 00:00:00 2001 From: Akuukis <Akuukis@users.noreply.github.com> Date: Wed, 20 Nov 2024 23:19:40 +0200 Subject: [PATCH 07/19] feat(organizations): handle query error once at root TASK-1271 (#5287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### đŸ“Ŗ Summary Display full-screen spinner if organizations endpoint returns an error. ### 👀 Preview steps 1. ℹī¸ have an account and MMO enabled organization (belonging to another user) 2. log in, keep tab open 3. switch to admin panel, add account to the organization 4. switch back to account tab, notice "No Organization matches the given query." error toast. 5. 🔴 [on main] notice that toast keep going forever once a second or so. 6. đŸŸĸ [on PR] notice the brief moment of spinner view after error toasts and before fetching the new value. ### 💭 Notes 1. Nice error screen is out of scope. 2. I think we should do the same with few other global queries. --- jsapp/js/account/routes.tsx | 26 +++---- jsapp/js/account/stripe.api.ts | 11 ++- jsapp/js/app.jsx | 69 ++++++++++--------- jsapp/js/projects/routes.tsx | 6 +- jsapp/js/router/RequireOrg.tsx | 20 ++++++ ...sx => RequireOrgPermissions.component.tsx} | 5 +- 6 files changed, 84 insertions(+), 53 deletions(-) create mode 100644 jsapp/js/router/RequireOrg.tsx rename jsapp/js/router/{validateOrgPermissions.component.tsx => RequireOrgPermissions.component.tsx} (91%) diff --git a/jsapp/js/account/routes.tsx b/jsapp/js/account/routes.tsx index 927c1ef2e4..e609131cf7 100644 --- a/jsapp/js/account/routes.tsx +++ b/jsapp/js/account/routes.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Navigate, Route} from 'react-router-dom'; import RequireAuth from 'js/router/requireAuth'; -import {ValidateOrgPermissions} from 'js/router/validateOrgPermissions.component'; +import {RequireOrgPermissions} from 'js/router/RequireOrgPermissions.component'; import {OrganizationUserRole} from './stripe.types'; import { ACCOUNT_ROUTES, @@ -37,12 +37,12 @@ export default function routes() { index element={ <RequireAuth> - <ValidateOrgPermissions + <RequireOrgPermissions validRoles={[OrganizationUserRole.owner]} redirectRoute={ACCOUNT_ROUTES.ACCOUNT_SETTINGS} > <PlansRoute /> - </ValidateOrgPermissions> + </RequireOrgPermissions> </RequireAuth> } /> @@ -51,12 +51,12 @@ export default function routes() { index element={ <RequireAuth> - <ValidateOrgPermissions + <RequireOrgPermissions validRoles={[OrganizationUserRole.owner]} redirectRoute={ACCOUNT_ROUTES.ACCOUNT_SETTINGS} > <AddOnsRoute /> - </ValidateOrgPermissions> + </RequireOrgPermissions> </RequireAuth> } /> @@ -65,7 +65,7 @@ export default function routes() { index element={ <RequireAuth> - <ValidateOrgPermissions + <RequireOrgPermissions validRoles={[ OrganizationUserRole.owner, OrganizationUserRole.admin, @@ -73,7 +73,7 @@ export default function routes() { redirectRoute={ACCOUNT_ROUTES.ACCOUNT_SETTINGS} > <DataStorage activeRoute={ACCOUNT_ROUTES.USAGE} /> - </ValidateOrgPermissions> + </RequireOrgPermissions> </RequireAuth> } /> @@ -81,7 +81,7 @@ export default function routes() { path={ACCOUNT_ROUTES.USAGE_PROJECT_BREAKDOWN} element={ <RequireAuth> - <ValidateOrgPermissions + <RequireOrgPermissions validRoles={[ OrganizationUserRole.owner, OrganizationUserRole.admin, @@ -91,7 +91,7 @@ export default function routes() { <DataStorage activeRoute={ACCOUNT_ROUTES.USAGE_PROJECT_BREAKDOWN} /> - </ValidateOrgPermissions> + </RequireOrgPermissions> </RequireAuth> } /> @@ -117,12 +117,12 @@ export default function routes() { path={ACCOUNT_ROUTES.ORGANIZATION_MEMBERS} element={ <RequireAuth> - <ValidateOrgPermissions + <RequireOrgPermissions mmoOnly redirectRoute={ACCOUNT_ROUTES.ACCOUNT_SETTINGS} > <div>Organization members view to be implemented</div> - </ValidateOrgPermissions> + </RequireOrgPermissions> </RequireAuth> } /> @@ -130,7 +130,7 @@ export default function routes() { path={ACCOUNT_ROUTES.ORGANIZATION_SETTINGS} element={ <RequireAuth> - <ValidateOrgPermissions + <RequireOrgPermissions validRoles={[ OrganizationUserRole.owner, OrganizationUserRole.admin, @@ -139,7 +139,7 @@ export default function routes() { redirectRoute={ACCOUNT_ROUTES.ACCOUNT_SETTINGS} > <OrganizationSettingsRoute /> - </ValidateOrgPermissions> + </RequireOrgPermissions> </RequireAuth> } /> diff --git a/jsapp/js/account/stripe.api.ts b/jsapp/js/account/stripe.api.ts index 216d441537..348827dc97 100644 --- a/jsapp/js/account/stripe.api.ts +++ b/jsapp/js/account/stripe.api.ts @@ -16,6 +16,7 @@ import type { } from 'js/account/stripe.types'; import {Limits} from 'js/account/stripe.types'; import {getAdjustedQuantityForPrice} from 'js/account/stripe.utils'; +import type {UndefinedInitialDataOptions} from '@tanstack/react-query'; import {useQuery} from '@tanstack/react-query'; import {QueryKeys} from 'js/query/queryKeys'; import {FeatureFlag, useFeatureFlag} from '../featureFlags'; @@ -58,7 +59,12 @@ export async function changeSubscription( }); } -export const useOrganizationQuery = () => { +/** + * Organization object is used globally. + * For convenience, errors are handled once at the top, see `RequireOrg`. + * No need to handle errors at every usage. + */ +export const useOrganizationQuery = (options?: Omit<UndefinedInitialDataOptions<Organization, FailResponse, Organization, QueryKeys[]>, 'queryFn' | 'queryKey'>) => { const isMmosEnabled = useFeatureFlag(FeatureFlag.mmosEnabled); const currentAccount = sessionStore.currentAccount; @@ -93,9 +99,10 @@ export const useOrganizationQuery = () => { !!organizationUrl; const query = useQuery<Organization, FailResponse, Organization, QueryKeys[]>({ + ...options, queryFn: fetchOrganization, queryKey: [QueryKeys.organization], - enabled: isQueryEnabled, + enabled: isQueryEnabled && options?.enabled !== false, }); // `organizationUrl` must exist, unless it's changed (e.g. user added/removed from organization). diff --git a/jsapp/js/app.jsx b/jsapp/js/app.jsx index a061b5a61a..8c93f5016e 100644 --- a/jsapp/js/app.jsx +++ b/jsapp/js/app.jsx @@ -30,9 +30,10 @@ import {isAnyProcessingRouteActive} from 'js/components/processing/routes.utils' import pageState from 'js/pageState.store'; // Query-related -import { QueryClientProvider } from '@tanstack/react-query' -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' -import { queryClient } from './query/queryClient.ts' +import { QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { queryClient } from './query/queryClient.ts'; +import { RequireOrg } from './router/RequireOrg'; class App extends React.Component { constructor(props) { @@ -107,42 +108,44 @@ class App extends React.Component { <DocumentTitle title='KoboToolbox'> <QueryClientProvider client={queryClient}> <RootContextProvider> - <Tracking /> - <ToasterConfig /> - - {this.shouldDisplayMainLayoutElements() && - <div className='header-stretch-bg' /> - } - - <bem.PageWrapper - m={pageWrapperModifiers} - className='mdl-layout mdl-layout--fixed-header' - > - {this.state.pageState.modal && ( - <BigModal params={this.state.pageState.modal} /> - )} - - {this.shouldDisplayMainLayoutElements() && ( - <> - <MainHeader assetUid={assetid} /> - <Drawer /> - </> - )} - - <bem.PageWrapper__content - className='mdl-layout__content' - m={pageWrapperContentModifiers} + <RequireOrg> + <Tracking /> + <ToasterConfig /> + + {this.shouldDisplayMainLayoutElements() && + <div className='header-stretch-bg' /> + } + + <bem.PageWrapper + m={pageWrapperModifiers} + className='mdl-layout mdl-layout--fixed-header' > + {this.state.pageState.modal && ( + <BigModal params={this.state.pageState.modal} /> + )} + {this.shouldDisplayMainLayoutElements() && ( <> - {this.isFormSingle() && <ProjectTopTabs />} - <FormViewSideTabs show={this.isFormSingle()} /> + <MainHeader assetUid={assetid} /> + <Drawer /> </> )} - <Outlet /> - </bem.PageWrapper__content> - </bem.PageWrapper> + <bem.PageWrapper__content + className='mdl-layout__content' + m={pageWrapperContentModifiers} + > + {this.shouldDisplayMainLayoutElements() && ( + <> + {this.isFormSingle() && <ProjectTopTabs />} + <FormViewSideTabs show={this.isFormSingle()} /> + </> + )} + + <Outlet /> + </bem.PageWrapper__content> + </bem.PageWrapper> + </RequireOrg> </RootContextProvider> diff --git a/jsapp/js/projects/routes.tsx b/jsapp/js/projects/routes.tsx index 876dbc3351..3076166d1c 100644 --- a/jsapp/js/projects/routes.tsx +++ b/jsapp/js/projects/routes.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {Navigate, Route} from 'react-router-dom'; import RequireAuth from 'js/router/requireAuth'; import {PROJECTS_ROUTES} from 'js/router/routerConstants'; -import {ValidateOrgPermissions} from 'js/router/validateOrgPermissions.component'; +import {RequireOrgPermissions} from 'js/router/RequireOrgPermissions.component'; const MyProjectsRoute = React.lazy( () => import(/* webpackPrefetch: true */ './myProjectsRoute') @@ -33,12 +33,12 @@ export default function routes() { path={PROJECTS_ROUTES.MY_ORG_PROJECTS} element={ <RequireAuth> - <ValidateOrgPermissions + <RequireOrgPermissions mmoOnly redirectRoute={PROJECTS_ROUTES.MY_PROJECTS} > <MyOrgProjectsRoute /> - </ValidateOrgPermissions> + </RequireOrgPermissions> </RequireAuth> } /> diff --git a/jsapp/js/router/RequireOrg.tsx b/jsapp/js/router/RequireOrg.tsx new file mode 100644 index 0000000000..7f440e2dc4 --- /dev/null +++ b/jsapp/js/router/RequireOrg.tsx @@ -0,0 +1,20 @@ +import type React from 'react'; +import LoadingSpinner from 'js/components/common/loadingSpinner'; +import {useOrganizationQuery} from 'js/account/stripe.api'; + +interface Props { + children: React.ReactNode; +} + +/** + * Organization object is used globally. + * Handle error once at the top for convenience to avoid error handling every time. + */ +export const RequireOrg = ({children}: Props) => { + const orgQuery = useOrganizationQuery(); + + if (orgQuery.isPending) {return <LoadingSpinner />;} + if (orgQuery.error) {return <LoadingSpinner />;} // TODO: Nicier error page. + + return children; +}; diff --git a/jsapp/js/router/validateOrgPermissions.component.tsx b/jsapp/js/router/RequireOrgPermissions.component.tsx similarity index 91% rename from jsapp/js/router/validateOrgPermissions.component.tsx rename to jsapp/js/router/RequireOrgPermissions.component.tsx index a35ba98f1b..840f879f84 100644 --- a/jsapp/js/router/validateOrgPermissions.component.tsx +++ b/jsapp/js/router/RequireOrgPermissions.component.tsx @@ -1,4 +1,5 @@ -import React, {Suspense, useEffect} from 'react'; +import type React from 'react'; +import {Suspense, useEffect} from 'react'; import {useNavigate} from 'react-router-dom'; import LoadingSpinner from 'js/components/common/loadingSpinner'; import {useOrganizationQuery} from 'js/account/stripe.api'; @@ -16,7 +17,7 @@ interface Props { * or members of MMOs. Defaults to allowing access for all users, so you must supply * any restrictions. */ -export const ValidateOrgPermissions = ({ +export const RequireOrgPermissions = ({ children, redirectRoute, validRoles = undefined, From 3991fdcd5613ca50ceb69c5f9560d81e631bfbf7 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak <leszek@magicznyleszek.xyz> Date: Wed, 20 Nov 2024 23:01:31 +0100 Subject: [PATCH 08/19] refactor(organizations): move useOrganizationQuery to separate file TASK-1219 (#5288) Also move related Organization interface and OrganizationUserRole enum. --- jsapp/js/account/accountSidebar.tsx | 6 +- .../account/add-ons/addOnList.component.tsx | 2 +- .../add-ons/oneTimeAddOnRow.component.tsx | 2 +- .../billingContextProvider.component.tsx | 4 +- .../account/organization/organizationQuery.ts | 85 +++++++++++++++++++ jsapp/js/account/plans/plan.component.tsx | 4 +- .../js/account/plans/planButton.component.tsx | 4 +- jsapp/js/account/routes.tsx | 2 +- jsapp/js/account/stripe.api.ts | 70 +-------------- jsapp/js/account/stripe.types.ts | 18 ---- .../account/usage/usageProjectBreakdown.tsx | 2 +- jsapp/js/account/usage/useUsage.hook.ts | 4 +- .../header/organizationBadge.component.tsx | 3 +- jsapp/js/projects/myOrgProjectsRoute.tsx | 2 +- .../js/projects/projectViews/viewSwitcher.tsx | 2 +- jsapp/js/router/RequireOrg.tsx | 2 +- .../RequireOrgPermissions.component.tsx | 6 +- 17 files changed, 112 insertions(+), 106 deletions(-) create mode 100644 jsapp/js/account/organization/organizationQuery.ts diff --git a/jsapp/js/account/accountSidebar.tsx b/jsapp/js/account/accountSidebar.tsx index 28ab205d1f..21f11b3003 100644 --- a/jsapp/js/account/accountSidebar.tsx +++ b/jsapp/js/account/accountSidebar.tsx @@ -10,8 +10,10 @@ import subscriptionStore from 'js/account/subscriptionStore'; import envStore from 'js/envStore'; import useWhenStripeIsEnabled from 'js/hooks/useWhenStripeIsEnabled.hook'; import {ACCOUNT_ROUTES} from 'js/account/routes.constants'; -import {useOrganizationQuery} from './stripe.api'; -import {OrganizationUserRole} from './stripe.types'; +import { + useOrganizationQuery, + OrganizationUserRole, +} from 'js/account/organization/organizationQuery'; import {useFeatureFlag, FeatureFlag} from 'js/featureFlags'; import {getSimpleMMOLabel} from './organization/organization.utils'; import LoadingSpinner from 'js/components/common/loadingSpinner'; diff --git a/jsapp/js/account/add-ons/addOnList.component.tsx b/jsapp/js/account/add-ons/addOnList.component.tsx index 3d119060b2..7ff128c6a8 100644 --- a/jsapp/js/account/add-ons/addOnList.component.tsx +++ b/jsapp/js/account/add-ons/addOnList.component.tsx @@ -3,7 +3,6 @@ import useWhen from 'js/hooks/useWhen.hook'; import subscriptionStore from 'js/account/subscriptionStore'; import type { Price, - Organization, Product, SubscriptionInfo, OneTimeAddOn, @@ -16,6 +15,7 @@ import Badge from 'jsapp/js/components/common/badge'; import {formatDate} from 'js/utils'; import {OneTimeAddOnsContext} from 'jsapp/js/account/useOneTimeAddonList.hook'; import {FeatureFlag, useFeatureFlag} from 'jsapp/js/featureFlags'; +import type {Organization} from 'js/account/organization/organizationQuery'; /** * A table of add-on products along with dropdowns to purchase them. diff --git a/jsapp/js/account/add-ons/oneTimeAddOnRow.component.tsx b/jsapp/js/account/add-ons/oneTimeAddOnRow.component.tsx index 5afb147004..fc25ebe296 100644 --- a/jsapp/js/account/add-ons/oneTimeAddOnRow.component.tsx +++ b/jsapp/js/account/add-ons/oneTimeAddOnRow.component.tsx @@ -1,7 +1,6 @@ import styles from 'js/account/add-ons/addOnList.module.scss'; import React, {useMemo, useState} from 'react'; import type { - Organization, Product, SubscriptionInfo, } from 'js/account/stripe.types'; @@ -10,6 +9,7 @@ import BillingButton from 'js/account/plans/billingButton.component'; import {postCheckout, postCustomerPortal} from 'js/account/stripe.api'; import {useDisplayPrice} from 'js/account/plans/useDisplayPrice.hook'; import {isChangeScheduled} from 'js/account/stripe.utils'; +import type {Organization} from 'js/account/organization/organizationQuery'; interface OneTimeAddOnRowProps { products: Product[]; diff --git a/jsapp/js/account/billingContextProvider.component.tsx b/jsapp/js/account/billingContextProvider.component.tsx index 6b47c6b07c..ff2c79926c 100644 --- a/jsapp/js/account/billingContextProvider.component.tsx +++ b/jsapp/js/account/billingContextProvider.component.tsx @@ -3,7 +3,7 @@ import { OneTimeAddOnsContext, useOneTimeAddOns } from './useOneTimeAddonList.ho import {UsageContext, useUsage} from 'js/account/usage/useUsage.hook'; import {ProductsContext, useProducts} from 'js/account/useProducts.hook'; import sessionStore from 'js/stores/session'; -import {useOrganizationQuery} from 'js/account/stripe.api'; +import {useOrganizationQuery} from 'js/account/organization/organizationQuery'; export const BillingContextProvider = (props: {children: ReactNode}) => { const orgQuery = useOrganizationQuery(); @@ -11,7 +11,7 @@ export const BillingContextProvider = (props: {children: ReactNode}) => { if (!sessionStore.isLoggedIn) { return <>{props.children}</>; } - + const usage = useUsage(orgQuery.data?.id || null); const products = useProducts(); const oneTimeAddOns = useOneTimeAddOns(); diff --git a/jsapp/js/account/organization/organizationQuery.ts b/jsapp/js/account/organization/organizationQuery.ts new file mode 100644 index 0000000000..70f499b09a --- /dev/null +++ b/jsapp/js/account/organization/organizationQuery.ts @@ -0,0 +1,85 @@ +import type {FailResponse} from 'js/dataInterface'; +import {fetchGetUrl} from 'jsapp/js/api'; +import type {UndefinedInitialDataOptions} from '@tanstack/react-query'; +import {useQuery} from '@tanstack/react-query'; +import {QueryKeys} from 'js/query/queryKeys'; +import {FeatureFlag, useFeatureFlag} from 'js/featureFlags'; +import sessionStore from 'js/stores/session'; +import {useEffect} from 'react'; + +export interface Organization { + id: string; + name: string; + is_active: boolean; + created: string; + modified: string; + slug: string; + is_owner: boolean; + is_mmo: boolean; + request_user_role: OrganizationUserRole; +} + +export enum OrganizationUserRole { + member = 'member', + admin = 'admin', + owner = 'owner', +} + +/** + * Organization object is used globally. + * For convenience, errors are handled once at the top, see `RequireOrg`. + * No need to handle errors at every usage. + */ +export const useOrganizationQuery = (options?: Omit<UndefinedInitialDataOptions<Organization, FailResponse, Organization, QueryKeys[]>, 'queryFn' | 'queryKey'>) => { + const isMmosEnabled = useFeatureFlag(FeatureFlag.mmosEnabled); + + const currentAccount = sessionStore.currentAccount; + + const organizationUrl = + 'organization' in currentAccount ? currentAccount.organization?.url : null; + + // Using a separated function to fetch the organization data to prevent + // feature flag dependencies from being added to the hook + const fetchOrganization = async (): Promise<Organization> => { + // organizationUrl is a full url with protocol and domain name, so we're using fetchGetUrl + // We're asserting the organizationUrl is not null here because the query is disabled if it is + const organization = await fetchGetUrl<Organization>(organizationUrl!); + + if (isMmosEnabled) { + return organization; + } + + // While the project is in development we will force a false return for the is_mmo + // to make sure we don't have any implementations appearing for users + return { + ...organization, + is_mmo: false, + }; + }; + + // Setting the 'enabled' property so the query won't run until we have the session data + // loaded. Account data is needed to fetch the organization data. + const isQueryEnabled = + !sessionStore.isPending && + sessionStore.isInitialLoadComplete && + !!organizationUrl; + + const query = useQuery<Organization, FailResponse, Organization, QueryKeys[]>({ + ...options, + queryFn: fetchOrganization, + queryKey: [QueryKeys.organization], + enabled: isQueryEnabled && options?.enabled !== false, + }); + + // `organizationUrl` must exist, unless it's changed (e.g. user added/removed from organization). + // In such case, refetch organizationUrl to fetch the new `organizationUrl`. + // DEBT: don't throw toast within fetchGetUrl. + // DEBT: don't retry the failing url 3-4 times before switching to the new url. + useEffect(() => { + if (query.error?.status === 404) { + sessionStore.refreshAccount(); + } + }, [query.error?.status]); + + return query; +}; diff --git a/jsapp/js/account/plans/plan.component.tsx b/jsapp/js/account/plans/plan.component.tsx index bfab02795e..3627fd2437 100644 --- a/jsapp/js/account/plans/plan.component.tsx +++ b/jsapp/js/account/plans/plan.component.tsx @@ -9,7 +9,7 @@ import React, { } from 'react'; import {useNavigate, useSearchParams} from 'react-router-dom'; import styles from './plan.module.scss'; -import {postCheckout, postCustomerPortal, useOrganizationQuery} from '../stripe.api'; +import {postCheckout, postCustomerPortal} from '../stripe.api'; import Button from 'js/components/common/button'; import classnames from 'classnames'; import LoadingSpinner from 'js/components/common/loadingSpinner'; @@ -28,7 +28,6 @@ import { } from 'js/account/stripe.utils'; import type { Price, - Organization, Product, SubscriptionInfo, SinglePricedProduct, @@ -39,6 +38,7 @@ import {PlanContainer} from 'js/account/plans/planContainer.component'; import {ProductsContext} from '../useProducts.hook'; import {ACCOUNT_ROUTES} from 'js/account/routes.constants'; import {useRefreshApiFetcher} from 'js/hooks/useRefreshApiFetcher.hook'; +import {useOrganizationQuery, type Organization} from 'js/account/organization/organizationQuery'; export interface PlanState { subscribedProduct: null | SubscriptionInfo[]; diff --git a/jsapp/js/account/plans/planButton.component.tsx b/jsapp/js/account/plans/planButton.component.tsx index ba708953c5..9eefbeae66 100644 --- a/jsapp/js/account/plans/planButton.component.tsx +++ b/jsapp/js/account/plans/planButton.component.tsx @@ -1,8 +1,8 @@ import BillingButton from 'js/account/plans/billingButton.component'; -import React, {useContext} from 'react'; import type {Price, SinglePricedProduct} from 'js/account/stripe.types'; -import {postCustomerPortal, useOrganizationQuery} from 'js/account/stripe.api'; +import {postCustomerPortal} from 'js/account/stripe.api'; import {processCheckoutResponse} from 'js/account/stripe.utils'; +import {useOrganizationQuery} from 'js/account/organization/organizationQuery'; interface PlanButtonProps { buySubscription: (price: Price, quantity?: number) => void; diff --git a/jsapp/js/account/routes.tsx b/jsapp/js/account/routes.tsx index e609131cf7..57044c76ec 100644 --- a/jsapp/js/account/routes.tsx +++ b/jsapp/js/account/routes.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {Navigate, Route} from 'react-router-dom'; import RequireAuth from 'js/router/requireAuth'; import {RequireOrgPermissions} from 'js/router/RequireOrgPermissions.component'; -import {OrganizationUserRole} from './stripe.types'; +import {OrganizationUserRole} from 'js/account/organization/organizationQuery'; import { ACCOUNT_ROUTES, AccountSettings, diff --git a/jsapp/js/account/stripe.api.ts b/jsapp/js/account/stripe.api.ts index 348827dc97..158574649d 100644 --- a/jsapp/js/account/stripe.api.ts +++ b/jsapp/js/account/stripe.api.ts @@ -2,26 +2,19 @@ import {when} from 'mobx'; import subscriptionStore from 'js/account/subscriptionStore'; import {endpoints} from 'js/api.endpoints'; import {ACTIVE_STRIPE_STATUSES} from 'js/constants'; -import type {FailResponse, PaginatedResponse} from 'js/dataInterface'; +import type {PaginatedResponse} from 'js/dataInterface'; import envStore from 'js/envStore'; -import {fetchGet, fetchGetUrl, fetchPost} from 'jsapp/js/api'; +import {fetchGet, fetchPost} from 'jsapp/js/api'; import type { AccountLimit, ChangePlan, Checkout, OneTimeAddOn, - Organization, PriceMetadata, Product, } from 'js/account/stripe.types'; import {Limits} from 'js/account/stripe.types'; import {getAdjustedQuantityForPrice} from 'js/account/stripe.utils'; -import type {UndefinedInitialDataOptions} from '@tanstack/react-query'; -import {useQuery} from '@tanstack/react-query'; -import {QueryKeys} from 'js/query/queryKeys'; -import {FeatureFlag, useFeatureFlag} from '../featureFlags'; -import sessionStore from 'js/stores/session'; -import {useEffect} from 'react'; const DEFAULT_LIMITS: AccountLimit = Object.freeze({ submission_limit: Limits.unlimited, @@ -59,65 +52,6 @@ export async function changeSubscription( }); } -/** - * Organization object is used globally. - * For convenience, errors are handled once at the top, see `RequireOrg`. - * No need to handle errors at every usage. - */ -export const useOrganizationQuery = (options?: Omit<UndefinedInitialDataOptions<Organization, FailResponse, Organization, QueryKeys[]>, 'queryFn' | 'queryKey'>) => { - const isMmosEnabled = useFeatureFlag(FeatureFlag.mmosEnabled); - - const currentAccount = sessionStore.currentAccount; - - const organizationUrl = - 'organization' in currentAccount ? currentAccount.organization?.url : null; - - // Using a separated function to fetch the organization data to prevent - // feature flag dependencies from being added to the hook - const fetchOrganization = async (): Promise<Organization> => { - // organizationUrl is a full url with protocol and domain name, so we're using fetchGetUrl - // We're asserting the organizationUrl is not null here because the query is disabled if it is - const organization = await fetchGetUrl<Organization>(organizationUrl!); - - if (isMmosEnabled) { - return organization; - } - - // While the project is in development we will force a false return for the is_mmo - // to make sure we don't have any implementations appearing for users - return { - ...organization, - is_mmo: false, - }; - }; - - // Setting the 'enabled' property so the query won't run until we have the session data - // loaded. Account data is needed to fetch the organization data. - const isQueryEnabled = - !sessionStore.isPending && - sessionStore.isInitialLoadComplete && - !!organizationUrl; - - const query = useQuery<Organization, FailResponse, Organization, QueryKeys[]>({ - ...options, - queryFn: fetchOrganization, - queryKey: [QueryKeys.organization], - enabled: isQueryEnabled && options?.enabled !== false, - }); - - // `organizationUrl` must exist, unless it's changed (e.g. user added/removed from organization). - // In such case, refetch organizationUrl to fetch the new `organizationUrl`. - // DEBT: don't throw toast within fetchGetUrl. - // DEBT: don't retry the failing url 3-4 times before switching to the new url. - useEffect(() => { - if (query.error?.status === 404) { - sessionStore.refreshAccount(); - } - }, [query.error?.status]); - - return query; -}; - /** * Start a checkout session for the given price and organization. Response contains the checkout URL. */ diff --git a/jsapp/js/account/stripe.types.ts b/jsapp/js/account/stripe.types.ts index ff867deb5f..fb3381cdcd 100644 --- a/jsapp/js/account/stripe.types.ts +++ b/jsapp/js/account/stripe.types.ts @@ -139,24 +139,6 @@ export interface TransformQuantity { round: 'up' | 'down'; } -export interface Organization { - id: string; - name: string; - is_active: boolean; - created: string; - modified: string; - slug: string; - is_owner: boolean; - is_mmo: boolean; - request_user_role: OrganizationUserRole; -} - -export enum OrganizationUserRole { - member = 'member', - admin = 'admin', - owner = 'owner', -} - export enum PlanNames { 'FREE' = 'Community', // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values diff --git a/jsapp/js/account/usage/usageProjectBreakdown.tsx b/jsapp/js/account/usage/usageProjectBreakdown.tsx index a9a993eddc..6fe5387041 100644 --- a/jsapp/js/account/usage/usageProjectBreakdown.tsx +++ b/jsapp/js/account/usage/usageProjectBreakdown.tsx @@ -16,7 +16,7 @@ import {convertSecondsToMinutes} from 'jsapp/js/utils'; import {UsageContext} from './useUsage.hook'; import Button from 'js/components/common/button'; import Icon from 'js/components/common/icon'; -import {useOrganizationQuery} from 'js/account/stripe.api'; +import {useOrganizationQuery} from 'js/account/organization/organizationQuery'; type ButtonType = 'back' | 'forward'; diff --git a/jsapp/js/account/usage/useUsage.hook.ts b/jsapp/js/account/usage/useUsage.hook.ts index 17a016c4a6..1166605524 100644 --- a/jsapp/js/account/usage/useUsage.hook.ts +++ b/jsapp/js/account/usage/useUsage.hook.ts @@ -1,5 +1,5 @@ -import {createContext, useCallback} from 'react'; -import type {Organization, RecurringInterval} from 'js/account/stripe.types'; +import {createContext} from 'react'; +import type {RecurringInterval} from 'js/account/stripe.types'; import {getSubscriptionInterval} from 'js/account/stripe.api'; import {convertSecondsToMinutes, formatRelativeTime} from 'js/utils'; import {getUsage} from 'js/account/usage/usage.api'; diff --git a/jsapp/js/components/header/organizationBadge.component.tsx b/jsapp/js/components/header/organizationBadge.component.tsx index b7fd7e6512..534de9c5c3 100644 --- a/jsapp/js/components/header/organizationBadge.component.tsx +++ b/jsapp/js/components/header/organizationBadge.component.tsx @@ -1,4 +1,5 @@ -import {useOrganizationQuery} from 'jsapp/js/account/stripe.api'; +import {useOrganizationQuery} from 'js/account/organization/organizationQuery'; + import styles from './organizationBadge.module.scss'; export default function OrganizationBadge() { diff --git a/jsapp/js/projects/myOrgProjectsRoute.tsx b/jsapp/js/projects/myOrgProjectsRoute.tsx index 8b8bc80119..6ae77d46a2 100644 --- a/jsapp/js/projects/myOrgProjectsRoute.tsx +++ b/jsapp/js/projects/myOrgProjectsRoute.tsx @@ -6,7 +6,7 @@ import UniversalProjectsRoute from './universalProjectsRoute'; import LoadingSpinner from 'js/components/common/loadingSpinner'; // Stores, hooks and utilities -import {useOrganizationQuery} from 'js/account/stripe.api'; +import {useOrganizationQuery} from 'js/account/organization/organizationQuery'; // Constants and types import { diff --git a/jsapp/js/projects/projectViews/viewSwitcher.tsx b/jsapp/js/projects/projectViews/viewSwitcher.tsx index 786bbfaa73..143e1c6313 100644 --- a/jsapp/js/projects/projectViews/viewSwitcher.tsx +++ b/jsapp/js/projects/projectViews/viewSwitcher.tsx @@ -10,7 +10,7 @@ import KoboDropdown from 'js/components/common/koboDropdown'; // Stores and hooks import projectViewsStore from './projectViewsStore'; -import {useOrganizationQuery} from 'js/account/stripe.api'; +import {useOrganizationQuery} from 'js/account/organization/organizationQuery'; // Constants import {PROJECTS_ROUTES} from 'js/router/routerConstants'; diff --git a/jsapp/js/router/RequireOrg.tsx b/jsapp/js/router/RequireOrg.tsx index 7f440e2dc4..f2e823649f 100644 --- a/jsapp/js/router/RequireOrg.tsx +++ b/jsapp/js/router/RequireOrg.tsx @@ -1,6 +1,6 @@ import type React from 'react'; import LoadingSpinner from 'js/components/common/loadingSpinner'; -import {useOrganizationQuery} from 'js/account/stripe.api'; +import {useOrganizationQuery} from 'js/account/organization/organizationQuery'; interface Props { children: React.ReactNode; diff --git a/jsapp/js/router/RequireOrgPermissions.component.tsx b/jsapp/js/router/RequireOrgPermissions.component.tsx index 840f879f84..e0e559819e 100644 --- a/jsapp/js/router/RequireOrgPermissions.component.tsx +++ b/jsapp/js/router/RequireOrgPermissions.component.tsx @@ -2,8 +2,10 @@ import type React from 'react'; import {Suspense, useEffect} from 'react'; import {useNavigate} from 'react-router-dom'; import LoadingSpinner from 'js/components/common/loadingSpinner'; -import {useOrganizationQuery} from 'js/account/stripe.api'; -import {OrganizationUserRole} from '../account/stripe.types'; +import { + useOrganizationQuery, + OrganizationUserRole, +} from 'js/account/organization/organizationQuery'; interface Props { children: React.ReactNode; From ff2a8e9a6eb8fcc586c26efcdf20ceb409d917bb Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak <leszek@magicznyleszek.xyz> Date: Thu, 21 Nov 2024 20:20:58 +0100 Subject: [PATCH 09/19] refactor(account): rename kebab-case dirs to camelCase (#5291) --- jsapp/js/account/{add-ons => addOns}/addOnList.component.tsx | 2 +- jsapp/js/account/{add-ons => addOns}/addOnList.module.scss | 0 jsapp/js/account/{add-ons => addOns}/addOns.component.tsx | 0 jsapp/js/account/{add-ons => addOns}/addOns.module.scss | 0 .../account/{add-ons => addOns}/oneTimeAddOnRow.component.tsx | 2 +- .../updateBadge.component.tsx} | 0 jsapp/js/account/plans/plan.component.tsx | 2 +- jsapp/js/account/routes.constants.ts | 2 +- .../oneTimeAddOnList}/oneTimeAddOnList.component.tsx | 0 .../oneTimeAddOnList}/oneTimeAddOnList.module.scss | 0 .../oneTimeAddOnUsageModal.component.tsx | 2 +- .../oneTimeAddOnUsageModal.module.scss | 0 jsapp/js/account/usage/usageContainer.tsx | 2 +- 13 files changed, 6 insertions(+), 6 deletions(-) rename jsapp/js/account/{add-ons => addOns}/addOnList.component.tsx (99%) rename jsapp/js/account/{add-ons => addOns}/addOnList.module.scss (100%) rename jsapp/js/account/{add-ons => addOns}/addOns.component.tsx (100%) rename jsapp/js/account/{add-ons => addOns}/addOns.module.scss (100%) rename jsapp/js/account/{add-ons => addOns}/oneTimeAddOnRow.component.tsx (98%) rename jsapp/js/account/{add-ons/updateBadge.component..tsx => addOns/updateBadge.component.tsx} (100%) rename jsapp/js/account/usage/{one-time-add-on-usage-modal/one-time-add-on-list => oneTimeAddOnUsageModal/oneTimeAddOnList}/oneTimeAddOnList.component.tsx (100%) rename jsapp/js/account/usage/{one-time-add-on-usage-modal/one-time-add-on-list => oneTimeAddOnUsageModal/oneTimeAddOnList}/oneTimeAddOnList.module.scss (100%) rename jsapp/js/account/usage/{one-time-add-on-usage-modal => oneTimeAddOnUsageModal}/oneTimeAddOnUsageModal.component.tsx (97%) rename jsapp/js/account/usage/{one-time-add-on-usage-modal => oneTimeAddOnUsageModal}/oneTimeAddOnUsageModal.module.scss (100%) diff --git a/jsapp/js/account/add-ons/addOnList.component.tsx b/jsapp/js/account/addOns/addOnList.component.tsx similarity index 99% rename from jsapp/js/account/add-ons/addOnList.component.tsx rename to jsapp/js/account/addOns/addOnList.component.tsx index 7ff128c6a8..270fbfc6c1 100644 --- a/jsapp/js/account/add-ons/addOnList.component.tsx +++ b/jsapp/js/account/addOns/addOnList.component.tsx @@ -9,7 +9,7 @@ import type { } from 'js/account/stripe.types'; import {isAddonProduct} from 'js/account/stripe.utils'; import styles from './addOnList.module.scss'; -import {OneTimeAddOnRow} from 'js/account/add-ons/oneTimeAddOnRow.component'; +import {OneTimeAddOnRow} from 'jsapp/js/account/addOns/oneTimeAddOnRow.component'; import type {BadgeColor} from 'jsapp/js/components/common/badge'; import Badge from 'jsapp/js/components/common/badge'; import {formatDate} from 'js/utils'; diff --git a/jsapp/js/account/add-ons/addOnList.module.scss b/jsapp/js/account/addOns/addOnList.module.scss similarity index 100% rename from jsapp/js/account/add-ons/addOnList.module.scss rename to jsapp/js/account/addOns/addOnList.module.scss diff --git a/jsapp/js/account/add-ons/addOns.component.tsx b/jsapp/js/account/addOns/addOns.component.tsx similarity index 100% rename from jsapp/js/account/add-ons/addOns.component.tsx rename to jsapp/js/account/addOns/addOns.component.tsx diff --git a/jsapp/js/account/add-ons/addOns.module.scss b/jsapp/js/account/addOns/addOns.module.scss similarity index 100% rename from jsapp/js/account/add-ons/addOns.module.scss rename to jsapp/js/account/addOns/addOns.module.scss diff --git a/jsapp/js/account/add-ons/oneTimeAddOnRow.component.tsx b/jsapp/js/account/addOns/oneTimeAddOnRow.component.tsx similarity index 98% rename from jsapp/js/account/add-ons/oneTimeAddOnRow.component.tsx rename to jsapp/js/account/addOns/oneTimeAddOnRow.component.tsx index fc25ebe296..05c19d9735 100644 --- a/jsapp/js/account/add-ons/oneTimeAddOnRow.component.tsx +++ b/jsapp/js/account/addOns/oneTimeAddOnRow.component.tsx @@ -1,4 +1,4 @@ -import styles from 'js/account/add-ons/addOnList.module.scss'; +import styles from 'js/account/addOns/addOnList.module.scss'; import React, {useMemo, useState} from 'react'; import type { Product, diff --git a/jsapp/js/account/add-ons/updateBadge.component..tsx b/jsapp/js/account/addOns/updateBadge.component.tsx similarity index 100% rename from jsapp/js/account/add-ons/updateBadge.component..tsx rename to jsapp/js/account/addOns/updateBadge.component.tsx diff --git a/jsapp/js/account/plans/plan.component.tsx b/jsapp/js/account/plans/plan.component.tsx index 3627fd2437..7031d599de 100644 --- a/jsapp/js/account/plans/plan.component.tsx +++ b/jsapp/js/account/plans/plan.component.tsx @@ -18,7 +18,7 @@ import {ACTIVE_STRIPE_STATUSES} from 'js/constants'; import type {FreeTierThresholds} from 'js/envStore'; import envStore from 'js/envStore'; import useWhen from 'js/hooks/useWhen.hook'; -import AddOnList from 'js/account/add-ons/addOnList.component'; +import AddOnList from 'jsapp/js/account/addOns/addOnList.component'; import subscriptionStore from 'js/account/subscriptionStore'; import {when} from 'mobx'; import { diff --git a/jsapp/js/account/routes.constants.ts b/jsapp/js/account/routes.constants.ts index 11ee1897e3..9d29ccebed 100644 --- a/jsapp/js/account/routes.constants.ts +++ b/jsapp/js/account/routes.constants.ts @@ -11,7 +11,7 @@ export const PlansRoute = React.lazy( () => import(/* webpackPrefetch: true */ './plans/plan.component') ); export const AddOnsRoute = React.lazy( - () => import(/* webpackPrefetch: true */ './add-ons/addOns.component') + () => import(/* webpackPrefetch: true */ './addOns/addOns.component') ); export const AccountSettings = React.lazy( () => import(/* webpackPrefetch: true */ './accountSettingsRoute') diff --git a/jsapp/js/account/usage/one-time-add-on-usage-modal/one-time-add-on-list/oneTimeAddOnList.component.tsx b/jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnList/oneTimeAddOnList.component.tsx similarity index 100% rename from jsapp/js/account/usage/one-time-add-on-usage-modal/one-time-add-on-list/oneTimeAddOnList.component.tsx rename to jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnList/oneTimeAddOnList.component.tsx diff --git a/jsapp/js/account/usage/one-time-add-on-usage-modal/one-time-add-on-list/oneTimeAddOnList.module.scss b/jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnList/oneTimeAddOnList.module.scss similarity index 100% rename from jsapp/js/account/usage/one-time-add-on-usage-modal/one-time-add-on-list/oneTimeAddOnList.module.scss rename to jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnList/oneTimeAddOnList.module.scss diff --git a/jsapp/js/account/usage/one-time-add-on-usage-modal/oneTimeAddOnUsageModal.component.tsx b/jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.component.tsx similarity index 97% rename from jsapp/js/account/usage/one-time-add-on-usage-modal/oneTimeAddOnUsageModal.component.tsx rename to jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.component.tsx index a347149937..c1e32895e6 100644 --- a/jsapp/js/account/usage/one-time-add-on-usage-modal/oneTimeAddOnUsageModal.component.tsx +++ b/jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.component.tsx @@ -4,7 +4,7 @@ import KoboModalHeader from 'js/components/modals/koboModalHeader'; import styles from './oneTimeAddOnUsageModal.module.scss'; import {OneTimeAddOn, RecurringInterval, USAGE_TYPE} from '../../stripe.types'; import {useLimitDisplay} from '../../stripe.utils'; -import OneTimeAddOnList from './one-time-add-on-list/oneTimeAddOnList.component'; +import OneTimeAddOnList from './oneTimeAddOnList/oneTimeAddOnList.component'; interface OneTimeAddOnUsageModalProps { type: USAGE_TYPE; diff --git a/jsapp/js/account/usage/one-time-add-on-usage-modal/oneTimeAddOnUsageModal.module.scss b/jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.module.scss similarity index 100% rename from jsapp/js/account/usage/one-time-add-on-usage-modal/oneTimeAddOnUsageModal.module.scss rename to jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.module.scss diff --git a/jsapp/js/account/usage/usageContainer.tsx b/jsapp/js/account/usage/usageContainer.tsx index 6768dfaf30..40bca8edc5 100644 --- a/jsapp/js/account/usage/usageContainer.tsx +++ b/jsapp/js/account/usage/usageContainer.tsx @@ -13,7 +13,7 @@ import cx from 'classnames'; import subscriptionStore from 'js/account/subscriptionStore'; import Badge from 'js/components/common/badge'; import useWhenStripeIsEnabled from 'js/hooks/useWhenStripeIsEnabled.hook'; -import OneTimeAddOnUsageModal from './one-time-add-on-usage-modal/oneTimeAddOnUsageModal.component'; +import OneTimeAddOnUsageModal from './oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.component'; interface UsageContainerProps { usage: number; From e626391dc5ae1ff53e5497e63535f737add859f8 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak <leszek@magicznyleszek.xyz> Date: Thu, 21 Nov 2024 20:30:18 +0100 Subject: [PATCH 10/19] build(tsconfig): set lib to allow ES2021 functionalities (#5292) Added for `String.prototype.replaceAll()` to be available --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index b9f6f06ade..01e7baeb20 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ }, "target": "es2017", "module": "es2020", + "lib": ["ES2021"], "esModuleInterop": true, "moduleResolution": "node", "strict": true, From f5b205b3e2c17028f59f701ac7b2c46daf0abbb1 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak <leszek@magicznyleszek.xyz> Date: Thu, 21 Nov 2024 20:38:16 +0100 Subject: [PATCH 11/19] style(subscriptionStore): linter cleanup (#5293) --- jsapp/js/account/subscriptionStore.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/jsapp/js/account/subscriptionStore.ts b/jsapp/js/account/subscriptionStore.ts index be7469003f..ac9de4411c 100644 --- a/jsapp/js/account/subscriptionStore.ts +++ b/jsapp/js/account/subscriptionStore.ts @@ -1,9 +1,8 @@ import {makeAutoObservable} from 'mobx'; -import {handleApiFail} from 'js/api'; +import {handleApiFail, fetchGet} from 'js/api'; import {ACTIVE_STRIPE_STATUSES, ROOT_URL} from 'js/constants'; -import {fetchGet} from 'jsapp/js/api'; import type {PaginatedResponse} from 'js/dataInterface'; -import {Product, SubscriptionInfo} from 'js/account/stripe.types'; +import type {Product, SubscriptionInfo} from 'js/account/stripe.types'; const PRODUCTS_URL = '/api/v2/stripe/products/'; @@ -53,16 +52,16 @@ class SubscriptionStore { ); this.canceledPlans = response.results.filter( (sub) => - sub.items[0]?.price.product.metadata?.product_type == 'plan' && + sub.items[0]?.price.product.metadata?.product_type === 'plan' && sub.status === 'canceled' ); // get any active plan subscriptions for the user this.planResponse = this.activeSubscriptions.filter( - (sub) => sub.items[0]?.price.product.metadata?.product_type == 'plan' + (sub) => sub.items[0]?.price.product.metadata?.product_type === 'plan' ); // get any active recurring add-on subscriptions for the user this.addOnsResponse = this.activeSubscriptions.filter( - (sub) => sub.items[0]?.price.product.metadata?.product_type == 'addon' + (sub) => sub.items[0]?.price.product.metadata?.product_type === 'addon' ); this.isPending = false; From 9bea2b26e851f683f60169e87d26df107b71c43a Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak <leszek@magicznyleszek.xyz> Date: Thu, 21 Nov 2024 20:40:44 +0100 Subject: [PATCH 12/19] refactor(organizations): simplify shouldUseTeamLabel (#5294) --- jsapp/js/account/organization/organization.utils.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/jsapp/js/account/organization/organization.utils.tsx b/jsapp/js/account/organization/organization.utils.tsx index 4db566acda..3dbeeb4d62 100644 --- a/jsapp/js/account/organization/organization.utils.tsx +++ b/jsapp/js/account/organization/organization.utils.tsx @@ -1,7 +1,7 @@ import type {SubscriptionInfo} from 'jsapp/js/account/stripe.types'; import type {EnvStoreData} from 'jsapp/js/envStore'; -/** Only use this directly for complex cases/strings (for example, possessive case). +/** Only use this directly for complex cases/strings (for example, possessive case). * Otherwise, use getSimpleMMOLabel. * @param {EnvStoreData} envStoreData * @param {SubscriptionInfo} subscription @@ -11,13 +11,9 @@ export function shouldUseTeamLabel( envStoreData: EnvStoreData, subscription: SubscriptionInfo | null ) { - if (subscription) { - return ( - subscription.items[0].price.product.metadata?.use_team_label === 'true' - ); - } - - return envStoreData.use_team_label; + return subscription + ? subscription.items[0].price.product.metadata?.use_team_label === 'true' + : envStoreData.use_team_label; } /** From 6729e75d24ed71fcf6b05d50b25e6a0ebb5dc8ea Mon Sep 17 00:00:00 2001 From: Anji Tong <ollejna@gmail.com> Date: Thu, 21 Nov 2024 20:23:33 +0000 Subject: [PATCH 13/19] feat(organizations): disable email updates for regular organization members TASK-997 (#5233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 🗒ī¸ Checklist 1. [x] run linter locally 2. [x] update all related docs (API, README, inline, etc.), if any 3. [x] draft PR with a title `<type>(<scope>)<!>: <title> TASK-1234` 4. [x] tag PR: at least `frontend` or `backend` unless it's global 5. [x] fill in the template below and delete template comments 6. [x] review thyself: read the diff and repro the preview as written 7. [ ] open PR & confirm that CI passes 8. [x] request reviewers, if needed 9. [ ] delete this section before merging ### đŸ“Ŗ Summary <!-- Delete this section if changes are internal only. --> <!-- One sentence summary for the public changelog, worded for non-technical seasoned Kobo users. --> UI for changing emails is disabled for `Members` of a multi-member organization. ### 👀 Preview steps 1. Make sure your user's email is correct in both the user section of the django admin as well as the email address section. If there is a difference it would only show the former here. See https://www.notion.so/kobotoolbox/task-1236 2. Make an MMO that has an owner, admin, and a member, as well as a regular user 3. Navigate to the account settings page 4. Visit the security section 5. If the user is a regular user, or an MMO owner or admin, they should be able to change their email address as normal 6. If the user is a member of an MMO the text box and button should be gone ### 📖 Description <!-- Delete this section if summary already said everything. --> <!-- Full description for the public changelog, worded for non-technical seasoned Kobo users. --> Remove the text box and button entirely if the user is a `member`. Replaced with just text of the email returned from `/me`. ### 💭 Notes <!-- Delete this section if empty. --> <!-- Anything else useful that's not said above,worded for reviewers, testers, and future git archaeologist collegues. Examples: - screenshots, copy-pasted logs, etc. - what was tried but didn't work, - conscious short-term vs long-term tradeoffs, - proactively answer likely questions, --> Make sure the user either has the proper email reflected in the user section of the django admin or was created through an email confirmation link. See https://www.notion.so/kobotoolbox/task-1236 --------- Co-authored-by: James Kiger <james.kiger@gmail.com> --- .../security/email/emailSection.component.tsx | 159 ++++++++++-------- .../security/email/emailSection.module.scss | 10 ++ 2 files changed, 95 insertions(+), 74 deletions(-) diff --git a/jsapp/js/account/security/email/emailSection.component.tsx b/jsapp/js/account/security/email/emailSection.component.tsx index e350affae5..4877dcc8a8 100644 --- a/jsapp/js/account/security/email/emailSection.component.tsx +++ b/jsapp/js/account/security/email/emailSection.component.tsx @@ -10,6 +10,7 @@ import { deleteUnverifiedUserEmails, } from './emailSection.api'; import type {EmailResponse} from './emailSection.api'; +import {useOrganizationQuery} from '../../organization/organizationQuery'; // Partial components import Button from 'jsapp/js/components/common/button'; @@ -33,6 +34,8 @@ interface EmailState { export default function EmailSection() { const [session] = useState(() => sessionStore); + const orgQuery = useOrganizationQuery(); + let initialEmail = ''; if ('email' in session.currentAccount) { initialEmail = session.currentAccount.email; @@ -116,6 +119,10 @@ export default function EmailSection() { const unverifiedEmail = email.emails.find( (userEmail) => !userEmail.verified && !userEmail.primary ); + const isReady = session.isInitialLoadComplete && 'email' in currentAccount; + const userCanChangeEmail = orgQuery.data?.is_mmo + ? orgQuery.data.request_user_role !== 'member' + : true; return ( <section className={securityStyles.securitySection}> @@ -123,83 +130,87 @@ export default function EmailSection() { <h2 className={securityStyles.securitySectionTitleText}>{t('Email address')}</h2> </div> - <div className={cx(securityStyles.securitySectionBody, styles.body)}> - {!session.isPending && - session.isInitialLoadComplete && - 'email' in currentAccount && ( - <TextBox - value={email.newEmail} - placeholder={t('Type new email address')} - onChange={onTextFieldChange.bind(onTextFieldChange)} - type='email' - /> - )} - - {unverifiedEmail?.email && - !session.isPending && - session.isInitialLoadComplete && - 'email' in currentAccount && ( - <> - <div className={styles.unverifiedEmail}> - <Icon name='alert' /> - <p className={styles.blurb}> - <strong> - {t('Check your email ##UNVERIFIED_EMAIL##. ').replace( - '##UNVERIFIED_EMAIL##', - unverifiedEmail.email - )} - </strong> - - {t( - 'A verification link has been sent to confirm your ownership. Once confirmed, this address will replace ##UNVERIFIED_EMAIL##' - ).replace('##UNVERIFIED_EMAIL##', currentAccount.email)} - </p> - </div> - - <div className={styles.unverifiedEmailButtons}> - <Button - label='Resend' - size='m' - type='secondary' - onClick={resendNewUserEmail.bind( - resendNewUserEmail, + <div + className={cx([ + securityStyles.securitySectionBody, + userCanChangeEmail ? styles.body : styles.emailUpdateDisabled, + ])} + > + {isReady && userCanChangeEmail ? ( + <TextBox + value={email.newEmail} + placeholder={t('Type new email address')} + onChange={onTextFieldChange.bind(onTextFieldChange)} + type='email' + /> + ) : ( + <div className={styles.emailText}>{email.newEmail}</div> + )} + + {unverifiedEmail?.email && isReady && ( + <> + <div className={styles.unverifiedEmail}> + <Icon name='alert' /> + <p className={styles.blurb}> + <strong> + {t('Check your email ##UNVERIFIED_EMAIL##. ').replace( + '##UNVERIFIED_EMAIL##', unverifiedEmail.email )} - /> - <Button - label='Remove' - size='m' - type='secondary-danger' - onClick={deleteNewUserEmail} - /> - </div> - - {email.refreshedEmail && ( - <label> - {t('Email was sent again: ##TIMESTAMP##').replace( - '##TIMESTAMP##', - email.refreshedEmailDate - )} - </label> - )} - </> - )} + </strong> + + {t( + 'A verification link has been sent to confirm your ownership. Once confirmed, this address will replace ##UNVERIFIED_EMAIL##' + ).replace('##UNVERIFIED_EMAIL##', currentAccount.email)} + </p> + </div> + + <div className={styles.unverifiedEmailButtons}> + <Button + label='Resend' + size='m' + type='secondary' + onClick={resendNewUserEmail.bind( + resendNewUserEmail, + unverifiedEmail.email + )} + /> + <Button + label='Remove' + size='m' + type='secondary-danger' + onClick={deleteNewUserEmail} + /> + </div> + + {email.refreshedEmail && ( + <label> + {t('Email was sent again: ##TIMESTAMP##').replace( + '##TIMESTAMP##', + email.refreshedEmailDate + )} + </label> + )} + </> + )} </div> - - <form - className={styles.options} - onSubmit={(e) => { - e.preventDefault(); - handleSubmit(); - }} - > - <Button - label='Change' - size='m' - type='primary' - onClick={handleSubmit} - /> - </form> + {userCanChangeEmail && ( + <div className={styles.options}> + <form + onSubmit={(e) => { + e.preventDefault(); + handleSubmit(); + }} + > + <Button + label='Change' + size='m' + type='primary' + onClick={handleSubmit} + /> + </form> + </div> + )} </section> ); } diff --git a/jsapp/js/account/security/email/emailSection.module.scss b/jsapp/js/account/security/email/emailSection.module.scss index 7675c0999f..3350c95c9d 100644 --- a/jsapp/js/account/security/email/emailSection.module.scss +++ b/jsapp/js/account/security/email/emailSection.module.scss @@ -34,3 +34,13 @@ display: flex; gap: 10px; } + +.emailText { + font-weight: 600; +} + +.emailUpdateDisabled { + flex: 5; + // To compensate for the `options` class not displaying when there is no email + margin-right: calc(30% + 8px); +} From 79e33e8651f5ab85804053276cb3e0f7d30800a8 Mon Sep 17 00:00:00 2001 From: Raj Patel <51355159+rajpatel24@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:20:15 +0530 Subject: [PATCH 14/19] feat(organizations): add endpoints to handle organization members TASK-963 (#5235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### đŸ“Ŗ Summary - Enhance the organization member management API to support role updates and member removal via PATCH and DELETE requests, excluding member creation. ### 📖 Description - This update introduces endpoints for managing organization members: - GET `/api/v2/organizations/<org-id>/members/` to list all members in an organization. - PATCH `/api/v2/organizations/<org-id>/members/<username>/` to update a member's role (e.g., promote to admin). - DELETE `/api/v2/organizations/<org-id>/members/<username>/` to remove a member from the organization. Note: Creating members is not supported via this endpoint. Roles are restricted to 'admin' or 'member'. Including the details of invited members (those who have not yet joined the organization) is not covered in this update. ### 👷 Description for instance maintainers - This update provides new functionality to manage organization members: - Role updates for members (promotion/demotion between 'admin' and 'member'). - Member removal from an organization. - The endpoints are optimized to use database joins to fetch member roles efficiently without excessive database queries, ensuring minimal load. ### 👀 Preview steps 1. ℹī¸ Have an account and an organization with multiple members. 2. Use the GET method to list the members of the organization. 3. Use the PATCH method to update a member's role (e.g., promote a user to admin). 4. Use the DELETE method to remove a member from the organization. 5. đŸŸĸ Notice the endpoints behave as expected, with valid role updates and member removal. ### 💭 Notes - The code uses database joins to optimize role determination for members, avoiding inefficient role lookups. - Role validation restricts role assignments to 'admin' or 'member' only. - Including invited members' details is not covered in this update. This will be implemented in a future update. --- kobo/apps/organizations/permissions.py | 35 +-- kobo/apps/organizations/serializers.py | 57 ++++- .../tests/test_organization_members_api.py | 131 +++++++++++ .../tests/test_organizations_api.py | 9 +- kobo/apps/organizations/views.py | 209 +++++++++++++++++- kpi/urls/router_api_v2.py | 8 +- 6 files changed, 421 insertions(+), 28 deletions(-) create mode 100644 kobo/apps/organizations/tests/test_organization_members_api.py diff --git a/kobo/apps/organizations/permissions.py b/kobo/apps/organizations/permissions.py index 30d8988331..f37dc36757 100644 --- a/kobo/apps/organizations/permissions.py +++ b/kobo/apps/organizations/permissions.py @@ -1,14 +1,17 @@ from django.http import Http404 from rest_framework import permissions +from rest_framework.permissions import IsAuthenticated from kobo.apps.organizations.constants import ORG_EXTERNAL_ROLE +from kobo.apps.organizations.models import Organization from kpi.mixins.validation_password_permission import ValidationPasswordPermissionMixin from kpi.utils.object_permission import get_database_user -class IsOrgAdmin(ValidationPasswordPermissionMixin, permissions.BasePermission): +class IsOrgAdminPermission(ValidationPasswordPermissionMixin, IsAuthenticated): """ - Object-level permission to only allow admin members of an object to access it. + Object-level permission to only allow admin (and owner) members of an object + to access it. Assumes the model instance has an `is_admin` attribute. """ @@ -26,18 +29,22 @@ def has_object_permission(self, request, view, obj): return obj.is_admin(user) -class IsOrgAdminOrReadOnly(IsOrgAdmin): - """ - Object-level permission to only allow admin members of an object to edit it. - Assumes the model instance has an `is_admin` attribute. - """ +class HasOrgRolePermission(IsOrgAdminPermission): + def has_permission(self, request, view): + if not super().has_permission(request, view): + return False + + organization = Organization.objects.filter( + id=view.kwargs.get('organization_id') + ).first() + if organization and not self.has_object_permission( + request, view, organization + ): + return False + return True def has_object_permission(self, request, view, obj): - - # Read permissions are allowed to any request, - # so we'll always allow GET, HEAD or OPTIONS requests. - if request.method in permissions.SAFE_METHODS: + obj = obj if isinstance(obj, Organization) else obj.organization + if super().has_object_permission(request, view, obj): return True - - # Instance must have an attribute named `is_admin` - return obj.is_admin(request.user) + return request.method in permissions.SAFE_METHODS diff --git a/kobo/apps/organizations/serializers.py b/kobo/apps/organizations/serializers.py index 031545f220..ae13041394 100644 --- a/kobo/apps/organizations/serializers.py +++ b/kobo/apps/organizations/serializers.py @@ -1,4 +1,7 @@ +from django.contrib.auth import get_user_model +from django.utils.translation import gettext as t from rest_framework import serializers +from rest_framework.reverse import reverse from kobo.apps.organizations.models import ( Organization, @@ -12,10 +15,62 @@ class OrganizationUserSerializer(serializers.ModelSerializer): + user = serializers.HyperlinkedRelatedField( + queryset=get_user_model().objects.all(), + lookup_field='username', + view_name='user-kpi-detail', + ) + role = serializers.CharField() + user__has_mfa_enabled = serializers.BooleanField( + source='has_mfa_enabled', read_only=True + ) + url = serializers.SerializerMethodField() + date_joined = serializers.DateTimeField( + source='user.date_joined', format='%Y-%m-%dT%H:%M:%SZ' + ) + user__username = serializers.ReadOnlyField(source='user.username') + user__extra_details__name = serializers.ReadOnlyField( + source='user.extra_details.data.name' + ) + user__email = serializers.ReadOnlyField(source='user.email') + user__is_active = serializers.ReadOnlyField(source='user.is_active') class Meta: model = OrganizationUser - fields = ['user', 'organization'] + fields = [ + 'url', + 'user', + 'user__username', + 'user__email', + 'user__extra_details__name', + 'role', + 'user__has_mfa_enabled', + 'date_joined', + 'user__is_active' + ] + + def get_url(self, obj): + request = self.context.get('request') + return reverse( + 'organization-members-detail', + kwargs={ + 'organization_id': obj.organization.id, + 'user__username': obj.user.username + }, + request=request + ) + + def update(self, instance, validated_data): + if role := validated_data.get('role', None): + validated_data['is_admin'] = role == 'admin' + return super().update(instance, validated_data) + + def validate_role(self, role): + if role not in ['admin', 'member']: + raise serializers.ValidationError( + {'role': t("Invalid role. Only 'admin' or 'member' are allowed")} + ) + return role class OrganizationOwnerSerializer(serializers.ModelSerializer): diff --git a/kobo/apps/organizations/tests/test_organization_members_api.py b/kobo/apps/organizations/tests/test_organization_members_api.py new file mode 100644 index 0000000000..eb9e74b33b --- /dev/null +++ b/kobo/apps/organizations/tests/test_organization_members_api.py @@ -0,0 +1,131 @@ +from ddt import ddt, data, unpack +from django.urls import reverse +from rest_framework import status + +from kobo.apps.kobo_auth.shortcuts import User +from kobo.apps.organizations.tests.test_organizations_api import ( + BaseOrganizationAssetApiTestCase +) +from kpi.urls.router_api_v2 import URL_NAMESPACE + + +@ddt +class OrganizationMemberAPITestCase(BaseOrganizationAssetApiTestCase): + fixtures = ['test_data'] + URL_NAMESPACE = URL_NAMESPACE + + def setUp(self): + super().setUp() + self.organization = self.someuser.organization + self.owner_user = self.someuser + self.member_user = self.alice + self.admin_user = self.anotheruser + self.external_user = self.bob + + self.list_url = reverse( + self._get_endpoint('organization-members-list'), + kwargs={'organization_id': self.organization.id}, + ) + self.detail_url = lambda username: reverse( + self._get_endpoint('organization-members-detail'), + kwargs={ + 'organization_id': self.organization.id, + 'user__username': username + }, + ) + + @data( + ('owner', status.HTTP_200_OK), + ('admin', status.HTTP_200_OK), + ('member', status.HTTP_200_OK), + ('external', status.HTTP_404_NOT_FOUND), + ('anonymous', status.HTTP_401_UNAUTHORIZED), + ) + @unpack + def test_list_members_with_different_roles(self, user_role, expected_status): + if user_role == 'anonymous': + self.client.logout() + else: + user = getattr(self, f'{user_role}_user') + self.client.force_login(user) + response = self.client.get(self.list_url) + self.assertEqual(response.status_code, expected_status) + + @data( + ('owner', status.HTTP_200_OK), + ('admin', status.HTTP_200_OK), + ('member', status.HTTP_200_OK), + ('external', status.HTTP_404_NOT_FOUND), + ('anonymous', status.HTTP_401_UNAUTHORIZED), + ) + @unpack + def test_retrieve_member_details_with_different_roles( + self, user_role, expected_status + ): + if user_role == 'anonymous': + self.client.logout() + else: + user = getattr(self, f'{user_role}_user') + self.client.force_login(user) + response = self.client.get(self.detail_url(self.member_user)) + self.assertEqual(response.status_code, expected_status) + + @data( + ('owner', status.HTTP_200_OK), + ('admin', status.HTTP_200_OK), + ('member', status.HTTP_403_FORBIDDEN), + ('external', status.HTTP_404_NOT_FOUND), + ('anonymous', status.HTTP_401_UNAUTHORIZED), + ) + @unpack + def test_update_member_role_with_different_roles(self, user_role, expected_status): + if user_role == 'anonymous': + self.client.logout() + else: + user = getattr(self, f'{user_role}_user') + self.client.force_login(user) + data = {'role': 'admin'} + response = self.client.patch(self.detail_url(self.member_user), data) + self.assertEqual(response.status_code, expected_status) + + @data( + ('owner', status.HTTP_204_NO_CONTENT), + ('admin', status.HTTP_204_NO_CONTENT), + ('member', status.HTTP_403_FORBIDDEN), + ('external', status.HTTP_404_NOT_FOUND), + ('anonymous', status.HTTP_401_UNAUTHORIZED), + ) + @unpack + def test_delete_member_with_different_roles(self, user_role, expected_status): + if user_role == 'anonymous': + self.client.logout() + else: + user = getattr(self, f'{user_role}_user') + self.client.force_login(user) + response = self.client.delete(self.detail_url(self.member_user)) + self.assertEqual(response.status_code, expected_status) + if expected_status == status.HTTP_204_NO_CONTENT: + # Confirm deletion + response = self.client.get(self.detail_url(self.member_user)) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertFalse( + User.objects.filter(username=f'{user_role}_user').exists() + ) + + @data( + ('owner', status.HTTP_405_METHOD_NOT_ALLOWED), + ('admin', status.HTTP_405_METHOD_NOT_ALLOWED), + ('member', status.HTTP_403_FORBIDDEN), + ('external', status.HTTP_404_NOT_FOUND), + ('anonymous', status.HTTP_401_UNAUTHORIZED), + ) + @unpack + def test_post_request_is_not_allowed(self, user_role, expected_status): + if user_role == 'anonymous': + self.client.logout() + else: + user = getattr(self, f'{user_role}_user') + self.client.force_login(user) + data = {'role': 'admin'} + response = self.client.post(self.list_url, data) + self.assertEqual(response.status_code, expected_status) diff --git a/kobo/apps/organizations/tests/test_organizations_api.py b/kobo/apps/organizations/tests/test_organizations_api.py index c97ebef66e..a3fa635543 100644 --- a/kobo/apps/organizations/tests/test_organizations_api.py +++ b/kobo/apps/organizations/tests/test_organizations_api.py @@ -61,7 +61,12 @@ def test_anonymous_user(self): def test_create(self): data = {'name': 'my org'} res = self.client.post(self.url_list, data) - self.assertContains(res, data['name'], status_code=201) + self.assertEqual(res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + + def test_delete(self): + self._insert_data() + res = self.client.delete(self.url_detail) + self.assertEqual(res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) def test_list(self): self._insert_data() @@ -349,7 +354,7 @@ def test_can_list(self, username, expected_status_code): def test_list_not_found_as_anonymous(self): self.client.logout() response = self.client.get(self.org_assets_list_url) - assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.status_code == status.HTTP_401_UNAUTHORIZED def test_list_only_organization_assets(self): # The organization's assets endpoint only returns assets where the `owner` diff --git a/kobo/apps/organizations/views.py b/kobo/apps/organizations/views.py index 9be7689a27..7fd0e59470 100644 --- a/kobo/apps/organizations/views.py +++ b/kobo/apps/organizations/views.py @@ -1,6 +1,14 @@ from django.conf import settings from django.contrib.postgres.aggregates import ArrayAgg -from django.db.models import QuerySet +from django.db.models import ( + QuerySet, + Case, + When, + Value, + CharField, + OuterRef, +) +from django.db.models.expressions import Exists from django.utils.decorators import method_decorator from django.utils.http import http_date from django.views.decorators.cache import cache_page @@ -14,19 +22,17 @@ from kpi.constants import ASSET_TYPE_SURVEY from kpi.filters import AssetOrderingFilter, SearchFilter from kpi.models.asset import Asset -from kpi.paginators import AssetUsagePagination -from kpi.permissions import IsAuthenticated from kpi.serializers.v2.service_usage import ( CustomAssetUsageSerializer, ServiceUsageSerializer, ) from kpi.utils.object_permission import get_database_user from kpi.views.v2.asset import AssetViewSet - +from .models import Organization, OrganizationOwner, OrganizationUser +from .permissions import HasOrgRolePermission, IsOrgAdminPermission +from .serializers import OrganizationSerializer, OrganizationUserSerializer +from ..accounts.mfa.models import MfaMethod from ..stripe.constants import ACTIVE_STRIPE_STATUSES -from .models import Organization -from .permissions import IsOrgAdmin, IsOrgAdminOrReadOnly -from .serializers import OrganizationSerializer class OrganizationAssetViewSet(AssetViewSet): @@ -79,10 +85,12 @@ class OrganizationViewSet(viewsets.ModelViewSet): queryset = Organization.objects.all() serializer_class = OrganizationSerializer lookup_field = 'id' - permission_classes = (IsAuthenticated, IsOrgAdminOrReadOnly) - pagination_class = AssetUsagePagination + permission_classes = [HasOrgRolePermission] + http_method_names = ['get', 'patch'] - @action(detail=True, methods=['GET'], permission_classes=[IsOrgAdmin]) + @action( + detail=True, methods=['GET'], permission_classes=[IsOrgAdminPermission] + ) def assets(self, request: Request, *args, **kwargs): """ ### Retrieve Organization Assets @@ -263,3 +271,184 @@ def asset_usage(self, request, pk=None, *args, **kwargs): page, many=True, context=context ) return self.get_paginated_response(serializer.data) + + +class OrganizationMemberViewSet(viewsets.ModelViewSet): + """ + The API uses `ModelViewSet` instead of `NestedViewSetMixin` to maintain + explicit control over the queryset. + + ## Organization Members API + + This API allows authorized users to view and manage organization members and + their roles, including promoting or demoting members (eg. to admin). + + * Manage members and their roles within an organization. + * Update member roles (promote/demote). + + ### List Members + + Retrieves all members in the specified organization. + + <pre class="prettyprint"> + <b>GET</b> /api/v2/organizations/{organization_id}/members/ + </pre> + + > Example + > + > curl -X GET https://[kpi]/api/v2/organizations/org_12345/members/ + + > Response 200 + + > { + > "count": 2, + > "next": null, + > "previous": null, + > "results": [ + > { + > "url": "http://[kpi]/api/v2/organizations/org_12345/ \ + > members/foo_bar/", + > "user": "http://[kpi]/api/v2/users/foo_bar/", + > "user__username": "foo_bar", + > "user__email": "foo_bar@example.com", + > "user__name": "Foo Bar", + > "role": "owner", + > "user__has_mfa_enabled": true, + > "date_joined": "2024-08-11T12:36:32Z", + > "user__is_active": true + > }, + > { + > "url": "http://[kpi]/api/v2/organizations/org_12345/ \ + > members/john_doe/", + > "user": "http://[kpi]/api/v2/users/john_doe/", + > "user__username": "john_doe", + > "user__email": "john_doe@example.com", + > "user__name": "John Doe", + > "role": "admin", + > "user__has_mfa_enabled": false, + > "date_joined": "2024-10-21T06:38:45Z", + > "user__is_active": true + > } + > ] + > } + + + ### Retrieve Member Details + + Retrieves the details of a specific member within an organization by username. + + <pre class="prettyprint"> + <b>GET</b> /api/v2/organizations/{organization_id}/members/{username}/ + </pre> + + > Example + > + > curl -X GET https://[kpi]/api/v2/organizations/org_12345/members/foo_bar/ + + > Response 200 + + > { + > "url": "http://[kpi]/api/v2/organizations/org_12345/members/foo_bar/", + > "user": "http://[kpi]/api/v2/users/foo_bar/", + > "user__username": "foo_bar", + > "user__email": "foo_bar@example.com", + > "user__name": "Foo Bar", + > "role": "owner", + > "user__has_mfa_enabled": true, + > "date_joined": "2024-08-11T12:36:32Z", + > "user__is_active": true + > } + + ### Update Member Role + + Updates the role of a member within the organization to `admin` or + `member`. + + - **admin**: Grants the member admin privileges within the organization + - **member**: Revokes admin privileges, setting the member as a regular user + + <pre class="prettyprint"> + <b>PATCH</b> /api/v2/organizations/{organization_id}/members/{username}/ + </pre> + + > Example + > + > curl -X PATCH https://[kpi]/api/v2/organizations/org_12345/members/foo_bar/ + + > Payload + + > { + > "role": "admin" + > } + + > Response 200 + + > { + > "url": "http://[kpi]/api/v2/organizations/org_12345/members/foo_bar/", + > "user": "http://[kpi]/api/v2/users/foo_bar/", + > "user__username": "foo_bar", + > "user__email": "foo_bar@example.com", + > "user__name": "Foo Bar", + > "role": "admin", + > "user__has_mfa_enabled": true, + > "date_joined": "2024-08-11T12:36:32Z", + > "user__is_active": true + > } + + + ### Remove Member + + Delete an organization member. + + <pre class="prettyprint"> + <b>DELETE</b> /api/v2/organizations/{organization_id}/members/{username}/ + </pre> + + > Example + > + > curl -X DELETE https://[kpi]/api/v2/organizations/org_12345/members/foo_bar/ + + ## Permissions + + - The user must be authenticated to perform these actions. + - Owners and admins can manage members and roles. + - Members can view the list but cannot update roles or delete members. + + ## Notes + + - **Role Validation**: Only valid roles ('admin', 'member') are accepted + in updates. + """ + serializer_class = OrganizationUserSerializer + permission_classes = [HasOrgRolePermission] + http_method_names = ['get', 'patch', 'delete'] + lookup_field = 'user__username' + + def get_queryset(self): + organization_id = self.kwargs['organization_id'] + + # Subquery to check if the user has an active MFA method + mfa_subquery = MfaMethod.objects.filter( + user=OuterRef('user_id'), + is_active=True + ).values('pk') + + # Subquery to check if the user is the owner + owner_subquery = OrganizationOwner.objects.filter( + organization_id=organization_id, + organization_user=OuterRef('pk') + ).values('pk') + + # Annotate with role based on organization ownership and admin status + queryset = OrganizationUser.objects.filter( + organization_id=organization_id + ).select_related('user__extra_details').annotate( + role=Case( + When(Exists(owner_subquery), then=Value('owner')), + When(is_admin=True, then=Value('admin')), + default=Value('member'), + output_field=CharField() + ), + has_mfa_enabled=Exists(mfa_subquery) + ) + return queryset diff --git a/kpi/urls/router_api_v2.py b/kpi/urls/router_api_v2.py index 7c7dbd2575..411d77de2b 100644 --- a/kpi/urls/router_api_v2.py +++ b/kpi/urls/router_api_v2.py @@ -6,7 +6,7 @@ from kobo.apps.hook.views.v2.hook import HookViewSet from kobo.apps.hook.views.v2.hook_log import HookLogViewSet from kobo.apps.languages.urls import router as language_router -from kobo.apps.organizations.views import OrganizationViewSet +from kobo.apps.organizations.views import OrganizationViewSet, OrganizationMemberViewSet from kobo.apps.project_ownership.urls import router as project_ownership_router from kobo.apps.project_views.views import ProjectViewViewSet from kpi.views.v2.asset import AssetViewSet @@ -140,6 +140,12 @@ def get_urls(self, *args, **kwargs): router_api_v2.register(r'imports', ImportTaskViewSet) router_api_v2.register(r'organizations', OrganizationViewSet, basename='organizations',) +router_api_v2.register( + r'organizations/(?P<organization_id>[^/.]+)/members', + OrganizationMemberViewSet, + basename='organization-members', +) + router_api_v2.register(r'permissions', PermissionViewSet) router_api_v2.register(r'project-views', ProjectViewViewSet) router_api_v2.register(r'service_usage', From 1386a155f48e6677308b023ab47235b24b781ed2 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak <leszek@magicznyleszek.xyz> Date: Mon, 25 Nov 2024 17:37:09 +0100 Subject: [PATCH 15/19] feat(organizations): members table TASK-980 (#5261) Add MembersRoute component that renders things at `#/account/organization/members` (a header and a table of organization members). Add `membersQuery` that defines `OrganizationMember` TS interface and adds way for getting organization members. Change minimum column size in UniversalTable props to 60 (smaller then previously). --- .../js/account/organization/MembersRoute.tsx | 102 ++++++++++++++++++ jsapp/js/account/organization/membersQuery.ts | 85 +++++++++++++++ .../organization/membersRoute.module.scss | 26 +++++ jsapp/js/account/routes.constants.ts | 3 + jsapp/js/account/routes.tsx | 3 +- jsapp/js/api.endpoints.ts | 1 + jsapp/js/query/queryKeys.ts | 1 + .../universalTable.component.tsx | 2 +- 8 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 jsapp/js/account/organization/MembersRoute.tsx create mode 100644 jsapp/js/account/organization/membersQuery.ts create mode 100644 jsapp/js/account/organization/membersRoute.module.scss diff --git a/jsapp/js/account/organization/MembersRoute.tsx b/jsapp/js/account/organization/MembersRoute.tsx new file mode 100644 index 0000000000..dc7708e35f --- /dev/null +++ b/jsapp/js/account/organization/MembersRoute.tsx @@ -0,0 +1,102 @@ +// Libraries +import React from 'react'; + +// Partial components +import PaginatedQueryUniversalTable from 'js/universalTable/paginatedQueryUniversalTable.component'; +import LoadingSpinner from 'js/components/common/loadingSpinner'; +import Avatar from 'js/components/common/avatar'; +import Badge from 'jsapp/js/components/common/badge'; + +// Stores, hooks and utilities +import {formatTime} from 'js/utils'; +import {useOrganizationQuery} from './organizationQuery'; +import useOrganizationMembersQuery from './membersQuery'; + +// Constants and types +import type {OrganizationMember} from './membersQuery'; + +// Styles +import styles from './membersRoute.module.scss'; + +export default function MembersRoute() { + const orgQuery = useOrganizationQuery(); + + if (!orgQuery.data?.id) { + return ( + <LoadingSpinner /> + ); + } + + return ( + <div className={styles.membersRouteRoot}> + <header className={styles.header}> + <h2 className={styles.headerText}>{t('Members')}</h2> + </header> + + <PaginatedQueryUniversalTable<OrganizationMember> + queryHook={useOrganizationMembersQuery} + columns={[ + { + key: 'user__extra_details__name', + label: t('Name'), + cellFormatter: (member: OrganizationMember) => ( + <Avatar + size='m' + username={member.user__username} + isUsernameVisible + email={member.user__email} + // We pass `undefined` for the case it's an empty string + fullName={member.user__extra_details__name || undefined} + /> + ), + size: 360, + }, + { + key: 'invite', + label: t('Status'), + size: 120, + cellFormatter: (member: OrganizationMember) => { + if (member.invite?.status) { + return member.invite.status; + } else { + return <Badge color='light-green' size='s' label={t('Active')} />; + } + return null; + }, + }, + { + key: 'date_joined', + label: t('Date added'), + size: 140, + cellFormatter: (member: OrganizationMember) => formatTime(member.date_joined), + }, + { + key: 'role', + label: t('Role'), + size: 120, + }, + { + key: 'user__has_mfa_enabled', + label: t('2FA'), + size: 90, + cellFormatter: (member: OrganizationMember) => { + if (member.user__has_mfa_enabled) { + return <Badge size='s' color='light-blue' icon='check' />; + } + return <Badge size='s' color='light-storm' icon='minus' />; + }, + }, + { + // We use `url` here, but the cell would contain interactive UI + // element + key: 'url', + label: '', + size: 64, + // TODO: this will be added soon + cellFormatter: () => (' '), + }, + ]} + /> + </div> + ); +} diff --git a/jsapp/js/account/organization/membersQuery.ts b/jsapp/js/account/organization/membersQuery.ts new file mode 100644 index 0000000000..1bec128e80 --- /dev/null +++ b/jsapp/js/account/organization/membersQuery.ts @@ -0,0 +1,85 @@ +import {keepPreviousData, useQuery} from '@tanstack/react-query'; +import {endpoints} from 'js/api.endpoints'; +import type {PaginatedResponse} from 'js/dataInterface'; +import {fetchGet} from 'js/api'; +import {QueryKeys} from 'js/query/queryKeys'; +import {useOrganizationQuery, type OrganizationUserRole} from './organizationQuery'; + +export interface OrganizationMember { + /** + * The url to the member within the organization + * `/api/v2/organizations/<organization_uid>/members/<username>/` + */ + url: string; + /** `/api/v2/users/<username>/` */ + user: string; + user__username: string; + /** can be an empty string in some edge cases */ + user__email: string | ''; + /** can be an empty string in some edge cases */ + user__extra_details__name: string | ''; + role: OrganizationUserRole; + user__has_mfa_enabled: boolean; + user__is_active: boolean; + /** yyyy-mm-dd HH:MM:SS */ + date_joined: string; + invite?: { + /** '/api/v2/organizations/<organization_uid>/invites/<invite_uid>/' */ + url: string; + /** yyyy-mm-dd HH:MM:SS */ + date_created: string; + /** yyyy-mm-dd HH:MM:SS */ + date_modified: string; + status: 'sent' | 'accepted' | 'expired' | 'declined'; + }; +} + +/** + * Fetches paginated list of members for given organization. + * This is mainly needed for `useOrganizationMembersQuery`, so you most probably + * would use it through that hook rather than directly. + */ +async function getOrganizationMembers( + limit: number, + offset: number, + orgId: string +) { + const params = new URLSearchParams({ + limit: limit.toString(), + offset: offset.toString(), + }); + + const apiUrl = endpoints.ORGANIZATION_MEMBERS_URL.replace(':organization_id', orgId); + + return fetchGet<PaginatedResponse<OrganizationMember>>( + apiUrl + '?' + params, + { + errorMessageDisplay: t('There was an error getting the list.'), + } + ); +} + +/** + * A hook that gives you paginated list of organization members. Uses + * `useOrganizationQuery` to get the id. + */ +export default function useOrganizationMembersQuery( + itemLimit: number, + pageOffset: number +) { + const orgQuery = useOrganizationQuery(); + const orgId = orgQuery.data?.id; + + return useQuery({ + queryKey: [QueryKeys.organizationMembers, itemLimit, pageOffset, orgId], + // `orgId!` because it's ensured to be there in `enabled` property :ok: + queryFn: () => getOrganizationMembers(itemLimit, pageOffset, orgId!), + placeholderData: keepPreviousData, + enabled: !!orgId, + // We might want to improve this in future, for now let's not retry + retry: false, + // The `refetchOnWindowFocus` option is `true` by default, I'm setting it + // here so we don't forget about it. + refetchOnWindowFocus: true, + }); +} diff --git a/jsapp/js/account/organization/membersRoute.module.scss b/jsapp/js/account/organization/membersRoute.module.scss new file mode 100644 index 0000000000..d100d36428 --- /dev/null +++ b/jsapp/js/account/organization/membersRoute.module.scss @@ -0,0 +1,26 @@ +@use 'scss/colors'; +@use 'scss/breakpoints'; + +.membersRouteRoot { + padding: 20px; + overflow-y: auto; + height: 100%; +} + +.header { + margin-bottom: 20px; +} + +h2.headerText { + color: colors.$kobo-storm; + text-transform: uppercase; + font-size: 18px; + font-weight: 700; + margin: 0; +} + +@include breakpoints.breakpoint(mediumAndUp) { + .membersRouteRoot { + padding: 50px; + } +} diff --git a/jsapp/js/account/routes.constants.ts b/jsapp/js/account/routes.constants.ts index 9d29ccebed..4a4e49b6ea 100644 --- a/jsapp/js/account/routes.constants.ts +++ b/jsapp/js/account/routes.constants.ts @@ -19,6 +19,9 @@ export const AccountSettings = React.lazy( export const DataStorage = React.lazy( () => import(/* webpackPrefetch: true */ './usage/usageTopTabs') ); +export const MembersRoute = React.lazy( + () => import(/* webpackPrefetch: true */ './organization/MembersRoute') +); export const OrganizationSettingsRoute = React.lazy( () => import(/* webpackPrefetch: true */ './organization/OrganizationSettingsRoute') ); diff --git a/jsapp/js/account/routes.tsx b/jsapp/js/account/routes.tsx index 57044c76ec..b6646f9360 100644 --- a/jsapp/js/account/routes.tsx +++ b/jsapp/js/account/routes.tsx @@ -11,6 +11,7 @@ import { DataStorage, PlansRoute, SecurityRoute, + MembersRoute, OrganizationSettingsRoute, } from 'js/account/routes.constants'; import {useFeatureFlag, FeatureFlag} from 'js/featureFlags'; @@ -121,7 +122,7 @@ export default function routes() { mmoOnly redirectRoute={ACCOUNT_ROUTES.ACCOUNT_SETTINGS} > - <div>Organization members view to be implemented</div> + <MembersRoute /> </RequireOrgPermissions> </RequireAuth> } diff --git a/jsapp/js/api.endpoints.ts b/jsapp/js/api.endpoints.ts index 3a4c1a1485..a6096989f1 100644 --- a/jsapp/js/api.endpoints.ts +++ b/jsapp/js/api.endpoints.ts @@ -5,6 +5,7 @@ export const endpoints = { PRODUCTS_URL: '/api/v2/stripe/products/', SUBSCRIPTION_URL: '/api/v2/stripe/subscriptions/', ADD_ONS_URL: '/api/v2/stripe/addons/', + ORGANIZATION_MEMBERS_URL: '/api/v2/organizations/:organization_id/members/', /** Expected parameters: price_id and organization_id **/ CHECKOUT_URL: '/api/v2/stripe/checkout-link', /** Expected parameter: organization_id **/ diff --git a/jsapp/js/query/queryKeys.ts b/jsapp/js/query/queryKeys.ts index 3cd3dc26e4..1ded3e7116 100644 --- a/jsapp/js/query/queryKeys.ts +++ b/jsapp/js/query/queryKeys.ts @@ -11,4 +11,5 @@ export enum QueryKeys { activityLogs = 'activityLogs', activityLogsFilter = 'activityLogsFilter', organization = 'organization', + organizationMembers = 'organizationMembers', } diff --git a/jsapp/js/universalTable/universalTable.component.tsx b/jsapp/js/universalTable/universalTable.component.tsx index eaa29bacb2..8b3dc480da 100644 --- a/jsapp/js/universalTable/universalTable.component.tsx +++ b/jsapp/js/universalTable/universalTable.component.tsx @@ -86,7 +86,7 @@ interface UniversalTableProps<DataItem> { const DEFAULT_COLUMN_SIZE = { size: 200, // starting column size - minSize: 100, // enforced during column resizing + minSize: 60, // enforced during column resizing maxSize: 600, // enforced during column resizing }; From fe8e7ecd492c503f6d6058a8077e85b635622929 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak <leszek@magicznyleszek.xyz> Date: Mon, 25 Nov 2024 17:56:26 +0100 Subject: [PATCH 16/19] refactor(usage): rename ##placeholder## to :placeholder in urls for consistency sake (#5298) Internal code improvement --- jsapp/js/account/usage/usage.api.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/jsapp/js/account/usage/usage.api.ts b/jsapp/js/account/usage/usage.api.ts index 2bbab18bf6..ca49a3b624 100644 --- a/jsapp/js/account/usage/usage.api.ts +++ b/jsapp/js/account/usage/usage.api.ts @@ -55,17 +55,15 @@ export interface UsageResponse { } const USAGE_URL = '/api/v2/service_usage/'; -const ORGANIZATION_USAGE_URL = - '/api/v2/organizations/##ORGANIZATION_ID##/service_usage/'; +const ORGANIZATION_USAGE_URL = '/api/v2/organizations/:organization_id/service_usage/'; const ASSET_USAGE_URL = '/api/v2/asset_usage/'; -const ORGANIZATION_ASSET_USAGE_URL = - '/api/v2/organizations/##ORGANIZATION_ID##/asset_usage/'; +const ORGANIZATION_ASSET_USAGE_URL = '/api/v2/organizations/:organization_id/asset_usage/'; export async function getUsage(organization_id: string | null = null) { if (organization_id) { return fetchGet<UsageResponse>( - ORGANIZATION_USAGE_URL.replace('##ORGANIZATION_ID##', organization_id), + ORGANIZATION_USAGE_URL.replace(':organization_id', organization_id), { includeHeaders: true, errorMessageDisplay: t('There was an error fetching usage data.'), @@ -95,10 +93,7 @@ export async function getAssetUsageForOrganization( return await getAssetUsage(ASSET_USAGE_URL); } - const apiUrl = ORGANIZATION_ASSET_USAGE_URL.replace( - '##ORGANIZATION_ID##', - organizationId - ); + const apiUrl = ORGANIZATION_ASSET_USAGE_URL.replace(':organization_id', organizationId); const params = new URLSearchParams({ page: pageNumber.toString(), From 7e6bb6725bb743a1be46a079ddb806287fb015bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20L=C3=A9ger?= <olivierleger@gmail.com> Date: Tue, 26 Nov 2024 13:41:16 -0500 Subject: [PATCH 17/19] chore: remove deprecated imports (#5301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### đŸ“Ŗ Summary This PR removes imports that: - Were marked as deprecated in previous versions in Python 3.8 - Are no longer supported in the Python 3.1x --- kobo/apps/accounts/tests/test_backend.py | 2 +- kobo/apps/audit_log/tests/test_models.py | 2 +- kobo/apps/audit_log/tests/test_signals.py | 2 +- kobo/apps/help/views.py | 5 ----- .../openrosa/apps/api/viewsets/xform_list_api.py | 6 +----- .../apps/logger/tests/models/test_instance.py | 4 +--- .../apps/logger/tests/test_form_submission.py | 2 +- kobo/apps/openrosa/apps/viewer/tasks.py | 5 +---- kobo/apps/openrosa/libs/data/tests/test_tools.py | 2 +- .../openrosa/libs/mixins/openrosa_headers_mixin.py | 6 +----- kobo/apps/openrosa/libs/utils/logger_tools.py | 6 +----- kobo/apps/stripe/tests/test_organization_usage.py | 7 +------ kobo/apps/subsequences/actions/base.py | 6 +----- kpi/deployment_backends/openrosa_backend.py | 6 +----- kpi/models/import_export_task.py | 13 ++++--------- kpi/serializers/v2/user.py | 6 +----- kpi/tests/test_asset_versions.py | 7 +------ kpi/tests/test_deployment_backends.py | 3 ++- kpi/tests/test_export_tasks.py | 4 ++-- kpi/tests/test_mock_data_exports.py | 9 ++------- kpi/utils/mongo_helper.py | 5 ++--- kpi/views/v2/open_rosa.py | 6 +----- 22 files changed, 28 insertions(+), 86 deletions(-) diff --git a/kobo/apps/accounts/tests/test_backend.py b/kobo/apps/accounts/tests/test_backend.py index 23de46ceaa..49acfe9fae 100644 --- a/kobo/apps/accounts/tests/test_backend.py +++ b/kobo/apps/accounts/tests/test_backend.py @@ -102,5 +102,5 @@ def test_keep_django_auth_backend_with_sso(self, mock_verify_and_unstash_state): self.assertTrue(response.wsgi_request.user.is_authenticated) # Ensure there is a record of the login audit_log: AuditLog = AuditLog.objects.filter(user=response.wsgi_request.user).first() - self.assertEquals(audit_log.action, AuditAction.AUTH) + self.assertEqual(audit_log.action, AuditAction.AUTH) assert response.wsgi_request.user.backend == settings.AUTHENTICATION_BACKENDS[0] diff --git a/kobo/apps/audit_log/tests/test_models.py b/kobo/apps/audit_log/tests/test_models.py index ae98bc484f..eb997a970f 100644 --- a/kobo/apps/audit_log/tests/test_models.py +++ b/kobo/apps/audit_log/tests/test_models.py @@ -80,7 +80,7 @@ def test_create_access_log_sets_standard_fields(self): date_created=yesterday, ) self._check_common_fields(log, AccessLogModelTestCase.super_user) - self.assertEquals(log.date_created, yesterday) + self.assertEqual(log.date_created, yesterday) self.assertDictEqual(log.metadata, {'foo': 'bar'}) @patch('kobo.apps.audit_log.models.logging.warning') diff --git a/kobo/apps/audit_log/tests/test_signals.py b/kobo/apps/audit_log/tests/test_signals.py index ed99d46080..7c476bb0a9 100644 --- a/kobo/apps/audit_log/tests/test_signals.py +++ b/kobo/apps/audit_log/tests/test_signals.py @@ -76,7 +76,7 @@ def test_login_with_email_verification(self): } self.client.post(reverse('kobo_login'), data=data, follow=True) # no audit log should be created yet because the email has not been verified - self.assertEquals(AuditLog.objects.count(), 0) + self.assertEqual(AuditLog.objects.count(), 0) # verify the email and try again email: EmailAddress = EmailAddress.objects.filter(user=user).first() email.verified = True diff --git a/kobo/apps/help/views.py b/kobo/apps/help/views.py index 3aba1eee0e..44b17cd3c0 100644 --- a/kobo/apps/help/views.py +++ b/kobo/apps/help/views.py @@ -1,8 +1,3 @@ -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo - from django.db.models import Q from django.utils import timezone from private_storage.views import PrivateStorageView diff --git a/kobo/apps/openrosa/apps/api/viewsets/xform_list_api.py b/kobo/apps/openrosa/apps/api/viewsets/xform_list_api.py index f425451b89..f9886e01f5 100644 --- a/kobo/apps/openrosa/apps/api/viewsets/xform_list_api.py +++ b/kobo/apps/openrosa/apps/api/viewsets/xform_list_api.py @@ -1,9 +1,5 @@ from datetime import datetime - -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo +from zoneinfo import ZoneInfo from django.conf import settings from django.http import Http404 diff --git a/kobo/apps/openrosa/apps/logger/tests/models/test_instance.py b/kobo/apps/openrosa/apps/logger/tests/models/test_instance.py index 805dce72c6..fb3575ad4d 100644 --- a/kobo/apps/openrosa/apps/logger/tests/models/test_instance.py +++ b/kobo/apps/openrosa/apps/logger/tests/models/test_instance.py @@ -1,12 +1,10 @@ -# coding: utf-8 import os import reversion from datetime import datetime, timedelta, timezone +from unittest.mock import patch from dateutil import parser from django_digest.test import DigestAuth -from mock import patch - from kobo.apps.openrosa.apps.main.tests.test_base import TestBase from kobo.apps.openrosa.apps.logger.models import XForm, Instance from kobo.apps.openrosa.apps.logger.models.instance import ( diff --git a/kobo/apps/openrosa/apps/logger/tests/test_form_submission.py b/kobo/apps/openrosa/apps/logger/tests/test_form_submission.py index 0f7f493ebc..fb8e5bcb54 100644 --- a/kobo/apps/openrosa/apps/logger/tests/test_form_submission.py +++ b/kobo/apps/openrosa/apps/logger/tests/test_form_submission.py @@ -1,12 +1,12 @@ # coding: utf-8 import os import re +from unittest.mock import patch from django.http import Http404 from django_digest.test import DigestAuth from django_digest.test import Client as DigestClient from kobo.apps.openrosa.libs.utils.guardian import assign_perm -from mock import patch from kobo.apps.openrosa.apps.main.models.user_profile import UserProfile from kobo.apps.openrosa.apps.main.tests.test_base import TestBase diff --git a/kobo/apps/openrosa/apps/viewer/tasks.py b/kobo/apps/openrosa/apps/viewer/tasks.py index 4a728c866e..779d82d2b7 100644 --- a/kobo/apps/openrosa/apps/viewer/tasks.py +++ b/kobo/apps/openrosa/apps/viewer/tasks.py @@ -3,10 +3,7 @@ import re import sys from datetime import datetime, timedelta -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo +from zoneinfo import ZoneInfo from celery import shared_task from django.conf import settings diff --git a/kobo/apps/openrosa/libs/data/tests/test_tools.py b/kobo/apps/openrosa/libs/data/tests/test_tools.py index 391cb1d368..d80b201e29 100644 --- a/kobo/apps/openrosa/libs/data/tests/test_tools.py +++ b/kobo/apps/openrosa/libs/data/tests/test_tools.py @@ -1,9 +1,9 @@ # coding: utf-8 import os from datetime import datetime, timedelta, date +from unittest.mock import patch from django.conf import settings -from mock import patch from kobo.apps.openrosa.apps.logger.models.instance import Instance from kobo.apps.openrosa.apps.main.tests.test_base import TestBase diff --git a/kobo/apps/openrosa/libs/mixins/openrosa_headers_mixin.py b/kobo/apps/openrosa/libs/mixins/openrosa_headers_mixin.py index 7ce6862e6d..7d37cfa94f 100644 --- a/kobo/apps/openrosa/libs/mixins/openrosa_headers_mixin.py +++ b/kobo/apps/openrosa/libs/mixins/openrosa_headers_mixin.py @@ -1,9 +1,5 @@ -# coding: utf-8 from datetime import datetime -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo +from zoneinfo import ZoneInfo from django.conf import settings diff --git a/kobo/apps/openrosa/libs/utils/logger_tools.py b/kobo/apps/openrosa/libs/utils/logger_tools.py index 30d0ae0e41..6570659a44 100644 --- a/kobo/apps/openrosa/libs/utils/logger_tools.py +++ b/kobo/apps/openrosa/libs/utils/logger_tools.py @@ -10,11 +10,7 @@ from typing import Generator, Optional, Union from xml.etree import ElementTree as ET from xml.parsers.expat import ExpatError - -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo +from zoneinfo import ZoneInfo from wsgiref.util import FileWrapper from xml.dom import Node diff --git a/kobo/apps/stripe/tests/test_organization_usage.py b/kobo/apps/stripe/tests/test_organization_usage.py index e07773be59..788122553e 100644 --- a/kobo/apps/stripe/tests/test_organization_usage.py +++ b/kobo/apps/stripe/tests/test_organization_usage.py @@ -1,11 +1,6 @@ import timeit - -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo - from datetime import datetime +from zoneinfo import ZoneInfo import pytest from dateutil.relativedelta import relativedelta diff --git a/kobo/apps/subsequences/actions/base.py b/kobo/apps/subsequences/actions/base.py index 14feafe035..4401a8c172 100644 --- a/kobo/apps/subsequences/actions/base.py +++ b/kobo/apps/subsequences/actions/base.py @@ -1,9 +1,5 @@ import datetime - -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo +from zoneinfo import ZoneInfo from django.utils import timezone diff --git a/kpi/deployment_backends/openrosa_backend.py b/kpi/deployment_backends/openrosa_backend.py index 78fe35d2b7..a680c0ff33 100644 --- a/kpi/deployment_backends/openrosa_backend.py +++ b/kpi/deployment_backends/openrosa_backend.py @@ -5,11 +5,7 @@ from datetime import date, datetime from typing import Generator, Literal, Optional, Union from urllib.parse import urlparse - -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo +from zoneinfo import ZoneInfo import redis.exceptions import requests diff --git a/kpi/models/import_export_task.py b/kpi/models/import_export_task.py index 6c901fa1b4..e6a0c9c98f 100644 --- a/kpi/models/import_export_task.py +++ b/kpi/models/import_export_task.py @@ -1,4 +1,3 @@ -# coding: utf-8 import base64 import datetime import os @@ -9,15 +8,10 @@ from io import BytesIO from os.path import split, splitext from typing import Dict, Generator, List, Optional, Tuple - -import dateutil.parser - -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo +from zoneinfo import ZoneInfo import constance +import dateutil.parser import formpack import requests from django.conf import settings @@ -26,6 +20,7 @@ from django.db import models, transaction from django.db.models import F from django.urls import reverse +from django.utils import timezone from django.utils.translation import gettext as t from formpack.constants import KOBO_LOCK_SHEET from formpack.schema.fields import ( @@ -489,7 +484,7 @@ class ProjectViewExportTask(ImportExportTask): def _build_export_filename( self, export_type: str, username: str, view: str ) -> str: - time = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + time = timezone.now().strftime('%Y-%m-%dT%H:%M:%SZ') return f'{export_type}-{username}-view_{view}-{time}.csv' def _run_task(self, messages: list) -> None: diff --git a/kpi/serializers/v2/user.py b/kpi/serializers/v2/user.py index 3271ab871e..01b12571ea 100644 --- a/kpi/serializers/v2/user.py +++ b/kpi/serializers/v2/user.py @@ -1,8 +1,4 @@ -# coding: utf-8 -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo +from zoneinfo import ZoneInfo from django.conf import settings from django_request_cache import cache_for_request diff --git a/kpi/tests/test_asset_versions.py b/kpi/tests/test_asset_versions.py index 711151a51e..f3d66a3b2c 100644 --- a/kpi/tests/test_asset_versions.py +++ b/kpi/tests/test_asset_versions.py @@ -1,12 +1,7 @@ -# coding: utf-8 import json from copy import deepcopy from datetime import datetime - -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo +from zoneinfo import ZoneInfo from django.test import TestCase from django.utils import timezone diff --git a/kpi/tests/test_deployment_backends.py b/kpi/tests/test_deployment_backends.py index a3064d57c9..8325da5bc3 100644 --- a/kpi/tests/test_deployment_backends.py +++ b/kpi/tests/test_deployment_backends.py @@ -184,7 +184,8 @@ def test_sync_media_files(self): ).first() assert default_kobocat_storage.exists(str(meta_data.data_file)) - assert not default_storage.exists(str(meta_data.data_file)) + if default_storage.__class__.__name__ == 'FileSystemStorage': + assert not default_storage.exists(str(meta_data.data_file)) with default_kobocat_storage.open( str(meta_data.data_file), 'r' diff --git a/kpi/tests/test_export_tasks.py b/kpi/tests/test_export_tasks.py index 9072d65a28..ce0896ed0e 100644 --- a/kpi/tests/test_export_tasks.py +++ b/kpi/tests/test_export_tasks.py @@ -1,9 +1,9 @@ -import datetime from unittest.mock import Mock, patch from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.test import TestCase +from django.utils import timezone from kobo.apps.kobo_auth.shortcuts import User from kpi.models.import_export_task import ProjectViewExportTask @@ -46,7 +46,7 @@ def test_export_task_success(self, mock_get_project_view, mock_send_mail): root_url, self.user.username, self.user.username, - datetime.datetime.now().strftime('%Y-%m-%dT%H%M%SZ'), + timezone.now().strftime('%Y-%m-%dT%H%M%SZ'), ) mock_send_mail.assert_called_once_with( subject='Project View Report Complete', diff --git a/kpi/tests/test_mock_data_exports.py b/kpi/tests/test_mock_data_exports.py index 2e62a7083c..23791f00e7 100644 --- a/kpi/tests/test_mock_data_exports.py +++ b/kpi/tests/test_mock_data_exports.py @@ -1,15 +1,10 @@ # flake8: noqa: F401 +import datetime import os import zipfile from collections import defaultdict - -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo - -import datetime from unittest import mock +from zoneinfo import ZoneInfo import openpyxl from django.conf import settings diff --git a/kpi/utils/mongo_helper.py b/kpi/utils/mongo_helper.py index 6b6dedfba0..4d9245f78d 100644 --- a/kpi/utils/mongo_helper.py +++ b/kpi/utils/mongo_helper.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from typing import Any, Dict, Optional, Union +from typing import Any, Optional, Union from django.conf import settings @@ -9,8 +9,7 @@ from kpi.constants import NESTED_MONGO_RESERVED_ATTRIBUTES from kpi.utils.strings import base64_encodestring -# use `dict` when Python 3.8 is dropped -PermissionFilter = Dict[str, Any] +PermissionFilter = dict[str, Any] def drop_mock_only(func): diff --git a/kpi/views/v2/open_rosa.py b/kpi/views/v2/open_rosa.py index 2a19619031..56f733c4c7 100644 --- a/kpi/views/v2/open_rosa.py +++ b/kpi/views/v2/open_rosa.py @@ -1,9 +1,5 @@ -# coding: utf-8 from datetime import datetime -try: - from zoneinfo import ZoneInfo -except ImportError: - from backports.zoneinfo import ZoneInfo +from zoneinfo import ZoneInfo from django.conf import settings from rest_framework import status From a11ca28ee7d1cdcf50d31b5eb599dd62ba2e5b39 Mon Sep 17 00:00:00 2001 From: Guillermo <lgar89@gmail.com> Date: Tue, 26 Nov 2024 13:13:13 -0600 Subject: [PATCH 18/19] feat(projectHistoryLogs): project history logs for bulk actions TASK-1229 (#5270) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 📖 Description This PR adds support for bulk actions logging. ### 👀 Preview steps 1. Create two new projects 2. Deploy the projects 3. Execute the bulk actions for the 2 projects (archive, unarchive) 4. To test the archive and unarchive, the UI may not allow you to perform these actions, however you can use curl: ``` $ curl -X POST -H "Authorization: Token xxx" -d "{\"payload\": {\"action\": \"unarchive\", \"asset_uids\": [\"ae6hQt9s2phfjRnsNphLdA\", \"auYiwftdP2g8axKkiUB8cE\"]}}" -H "Content-Type: application/json" http://kf.kobo.local/api/v2/assets/bulk/ {"detail":"2 projects have been unarchived"} $ curl -X POST -H "Authorization: Token xxx" -d "{\"payload\": {\"action\": \"archive\", \"asset_uids\": [\"ae6hQt9s2phfjRnsNphLdA\", \"auYiwftdP2g8axKkiUB8cE\"]}}" -H "Content-Type: application/json" http://kf.kobo.local/api/v2/assets/bulk/ {"detail":"2 projects have been archived"} ``` 5. Check the logs were created through the django shell ``` $ ./run.py -cf exec kpi bash $ ./manage.py shell_plus ... In[]: us = User.objects.get(username="super_admin") In[]: for log in ProjectHistoryLog.objects.filter(user=us).order_by('-date_created')[0:8]: print(log.date_created, log.action, log.metadata['asset_uid']) 2024-11-14 23:50:19.482422+00:00 archive auYiwftdP2g8axKkiUB8cE 2024-11-14 23:50:19.468558+00:00 archive ae6hQt9s2phfjRnsNphLdA 2024-11-14 23:50:03.891367+00:00 unarchive auYiwftdP2g8axKkiUB8cE 2024-11-14 23:50:03.848762+00:00 unarchive ae6hQt9s2phfjRnsNphLdA 2024-11-14 18:36:23.187022+00:00 archive ae6hQt9s2phfjRnsNphLdA ... ``` ### 💭 Notes --- kobo/apps/audit_log/base_views.py | 1 + kobo/apps/audit_log/middleware.py | 7 ++- kobo/apps/audit_log/models.py | 43 ++++++++++++++ .../tests/test_project_history_logs.py | 59 +++++++++++++++++++ kpi/serializers/v2/asset.py | 4 +- 5 files changed, 111 insertions(+), 3 deletions(-) diff --git a/kobo/apps/audit_log/base_views.py b/kobo/apps/audit_log/base_views.py index f697f267ca..4970417c5e 100644 --- a/kobo/apps/audit_log/base_views.py +++ b/kobo/apps/audit_log/base_views.py @@ -39,6 +39,7 @@ class AuditLoggedViewSet(viewsets.GenericViewSet): def initialize_request(self, request, *args, **kwargs): request = super().initialize_request(request, *args, **kwargs) request._request.log_type = self.log_type + request._request._data = request.data.copy() return request def get_object(self): diff --git a/kobo/apps/audit_log/middleware.py b/kobo/apps/audit_log/middleware.py index f092cf9443..d978de84c5 100644 --- a/kobo/apps/audit_log/middleware.py +++ b/kobo/apps/audit_log/middleware.py @@ -1,3 +1,5 @@ +from rest_framework import status + from kobo.apps.audit_log.models import AuditType, ProjectHistoryLog @@ -7,7 +9,10 @@ def create_audit_logs(request): if request.method in ['GET', 'HEAD']: return response log_type = getattr(request, 'log_type', None) - if log_type == AuditType.PROJECT_HISTORY: + if ( + status.is_success(response.status_code) and + log_type == AuditType.PROJECT_HISTORY + ): ProjectHistoryLog.create_from_request(request) return response diff --git a/kobo/apps/audit_log/models.py b/kobo/apps/audit_log/models.py index 1778aaaa59..140f6eb852 100644 --- a/kobo/apps/audit_log/models.py +++ b/kobo/apps/audit_log/models.py @@ -335,6 +335,7 @@ def create_from_request(cls, request): 'asset-file-list': cls.create_from_file_request, 'asset-export-list': cls.create_from_export_request, 'exporttask-list': cls.create_from_v1_export, + 'asset-bulk': cls.create_from_bulk_request, } url_name = request.resolver_match.url_name method = url_name_to_action.get(url_name, None) @@ -342,6 +343,48 @@ def create_from_request(cls, request): return method(request) + @staticmethod + def create_from_bulk_request(request): + try: + payload = request._data['payload'] + action = payload['action'] + asset_uids = payload['asset_uids'] + except KeyError: + return # Incorrect payload + + if type(asset_uids) is not list or len(asset_uids) == 0: # Nothing to do + return + + bulk_action_to_audit_action = { + 'archive': AuditAction.ARCHIVE, + 'unarchive': AuditAction.UNARCHIVE, + } + audit_action = bulk_action_to_audit_action.get(action) + if audit_action is None: + return # Unsupported action + + source = get_human_readable_client_user_agent(request) + client_ip = get_client_ip(request) + + assets = Asset.optimize_queryset_for_list( + Asset.all_objects.filter(uid__in=asset_uids) + ) + for asset in assets: + object_id = asset.id + metadata = { + 'asset_uid': asset.uid, + 'log_subtype': PROJECT_HISTORY_LOG_PROJECT_SUBTYPE, + 'ip_address': client_ip, + 'source': source, + 'latest_version_uid': asset.prefetched_latest_versions[0].uid, + } + ProjectHistoryLog.objects.create( + user=request.user, + object_id=object_id, + action=audit_action, + metadata=metadata, + ) + @staticmethod def create_from_deployment_request(request): audit_log_info = getattr(request, 'additional_audit_log_info', None) diff --git a/kobo/apps/audit_log/tests/test_project_history_logs.py b/kobo/apps/audit_log/tests/test_project_history_logs.py index 693a072eef..3b684a85f9 100644 --- a/kobo/apps/audit_log/tests/test_project_history_logs.py +++ b/kobo/apps/audit_log/tests/test_project_history_logs.py @@ -8,6 +8,7 @@ from ddt import data, ddt, unpack from django.test import override_settings from django.urls import reverse +from rest_framework.response import Response from rest_framework.reverse import reverse as drf_reverse from kobo.apps.audit_log.audit_actions import AuditAction @@ -90,6 +91,23 @@ def _base_project_history_log_test( self._check_common_metadata(log.metadata, expected_subtype) return log.metadata + def _make_bulk_request(self, asset_uids, action) -> Response: + """ + Make a bulk action request for a list of asset uid's and an action name + + asset_uids: [list_of_uids] + action: [archive, unarchive, delete, undelete] + """ + payload = { + 'payload': { + 'asset_uids': asset_uids, + 'action': action, + } + } + url = reverse(self._get_endpoint('asset-bulk')) + response = self.client.post(url, data=payload, format='json') + return response + def test_first_time_deployment_creates_log(self): post_data = { 'active': True, @@ -862,3 +880,44 @@ def test_export_v1_creates_log(self): log = log_query.first() self._check_common_metadata(log.metadata, PROJECT_HISTORY_LOG_PROJECT_SUBTYPE) self.assertEqual(log.object_id, self.asset.id) + + @data( + ('archive', AuditAction.ARCHIVE), + ('unarchive', AuditAction.UNARCHIVE), + ('undelete', None), + ('delete', None), + ) + @unpack + def test_bulk_actions(self, bulk_action, audit_action): + assets = [Asset.objects.create( + content={ + 'survey': [ + { + 'type': 'text', + 'label': 'Question 1', + 'name': 'q1', + '$kuid': 'abc', + }, + ] + }, + owner=self.user, + asset_type='survey', + ) for i in range(0, 2)] + + for asset in assets: + asset.deploy(backend='mock', active=True) + + uids = [asset.uid for asset in assets] + + if bulk_action == 'undelete': + self._make_bulk_request(uids, 'delete') + + self._make_bulk_request(uids, bulk_action) + + if audit_action is None: + self.assertEqual(ProjectHistoryLog.objects.count(), 0) + else: + project_hist_logs = ProjectHistoryLog.objects.filter( + object_id__in=[asset.id for asset in assets], action=audit_action + ) + self.assertEqual(project_hist_logs.count(), 2) diff --git a/kpi/serializers/v2/asset.py b/kpi/serializers/v2/asset.py index 6fb44a56f4..74778aaffb 100644 --- a/kpi/serializers/v2/asset.py +++ b/kpi/serializers/v2/asset.py @@ -130,8 +130,8 @@ def to_representation(self, instance): if delete_request: if put_back_: message = nt( - f'%(count)d project has been undeleted', - f'%(count)d projects have been undeleted', + '%(count)d project has been undeleted', + '%(count)d projects have been undeleted', instance['project_counts'], ) % {'count': instance['project_counts']} else: From 0474aedfbebf0bc9d19e581eedaec09edea4ccb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20L=C3=A9ger?= <olivierleger@gmail.com> Date: Tue, 26 Nov 2024 14:33:03 -0500 Subject: [PATCH 19/19] fix(OpenRosa): correct detection of root node tag name for XForm surveys TASK-1258 (#5272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### đŸ“Ŗ Summary Fixes the detection of the root node tag name of the survey in the XForm XML, respecting the name field in XLSForm settings ### 📖 Description This bugfix addresses two issues caused by wrong detection of the root node tag name in the "survey" section of XForm XML: - an empty `formhub/uuid` on submission creation - a 500 error when a form is open in Enketo (preview/collection) when a disclaimer is set The update ensures that the name field, if provided in the XLSForm settings, is respected as the root node's tag name. ### 👀 Preview steps Bug template: 1. Log in as a regular user 2. Create a project (add `name` field with custom value in the `settings` worksheet) 3. Deploy and submit data 1. 🔴 [on main] notice that this `formhub/uuid` is empty 2. đŸŸĸ [on PR] notice that this is `formhub/uuid` matches XForm uuid 1. Go to admin and add a disclaimer 2. Try to open the project in Enketo (preview and collection) 1. 🔴 [on main] notice that it raises a 500 error 2. đŸŸĸ [on PR] notice that the form opens ### 💭 Notes This PR relies on the fact that `id_string` and `name` are always provided in the XForm.json field (according to existing data on production servers). In case of preview, If the `name` field is not supplied, the fallback mechanism uses the asset's UID to determine the tag name (as it is in `main`). --- .../apps/openrosa/apps/logger/models/xform.py | 12 +++++ .../logger/tests/test_simple_submission.py | 2 +- .../management/commands/mark_start_times.py | 2 +- .../management/commands/set_uuid_in_xml.py | 2 +- .../apps/viewer/models/data_dictionary.py | 46 ++++++++----------- kobo/apps/openrosa/libs/utils/logger_tools.py | 24 +++------- kpi/models/asset_snapshot.py | 15 ++++++ kpi/utils/xml.py | 18 +++++--- 8 files changed, 68 insertions(+), 53 deletions(-) diff --git a/kobo/apps/openrosa/apps/logger/models/xform.py b/kobo/apps/openrosa/apps/logger/models/xform.py index b52d6296b6..e4ae64448e 100644 --- a/kobo/apps/openrosa/apps/logger/models/xform.py +++ b/kobo/apps/openrosa/apps/logger/models/xform.py @@ -291,6 +291,18 @@ def url(self): 'download_xform', kwargs={'username': self.user.username, 'pk': self.pk} ) + @property + def xform_root_node_name(self): + """ + Retrieves the name of the XML tag representing the root node of the "survey" + in the XForm XML structure. + + It should always be present in `self.json`. + """ + + form_json = json.loads(self.json) + return form_json['name'] + @property def xml_with_disclaimer(self): return XMLFormWithDisclaimer(self).get_object().xml diff --git a/kobo/apps/openrosa/apps/logger/tests/test_simple_submission.py b/kobo/apps/openrosa/apps/logger/tests/test_simple_submission.py index 371ee04f30..d378b247bf 100644 --- a/kobo/apps/openrosa/apps/logger/tests/test_simple_submission.py +++ b/kobo/apps/openrosa/apps/logger/tests/test_simple_submission.py @@ -32,7 +32,7 @@ def _get_xml_for_form(self, xform): builder = SurveyElementBuilder() sss = builder.create_survey_element_from_json(xform.json) xform.xml = sss.to_xml() - xform._mark_start_time_boolean() + xform.mark_start_time_boolean() xform.save() def _submit_at_hour(self, hour): diff --git a/kobo/apps/openrosa/apps/viewer/management/commands/mark_start_times.py b/kobo/apps/openrosa/apps/viewer/management/commands/mark_start_times.py index 22cf41326e..074890a7ec 100644 --- a/kobo/apps/openrosa/apps/viewer/management/commands/mark_start_times.py +++ b/kobo/apps/openrosa/apps/viewer/management/commands/mark_start_times.py @@ -11,7 +11,7 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): for dd in DataDictionary.objects.all(): try: - dd._mark_start_time_boolean() + dd.mark_start_time_boolean() dd.save() except Exception: print ("Could not mark start time for DD: %(data)s" % { diff --git a/kobo/apps/openrosa/apps/viewer/management/commands/set_uuid_in_xml.py b/kobo/apps/openrosa/apps/viewer/management/commands/set_uuid_in_xml.py index 6ebb0cba88..076b55d492 100644 --- a/kobo/apps/openrosa/apps/viewer/management/commands/set_uuid_in_xml.py +++ b/kobo/apps/openrosa/apps/viewer/management/commands/set_uuid_in_xml.py @@ -14,7 +14,7 @@ def handle(self, *args, **kwargs): for i, dd in enumerate( queryset_iterator(DataDictionary.objects.all())): if dd.xls: - dd.set_uuid_in_xml(id_string=dd.id_string) + dd.set_uuid_in_xml() super(DataDictionary, dd).save() if (i + 1) % 10 == 0: print('Updated %(nb)d XForms...' % {'nb': i}) diff --git a/kobo/apps/openrosa/apps/viewer/models/data_dictionary.py b/kobo/apps/openrosa/apps/viewer/models/data_dictionary.py index 6a9922315a..fe19add6c4 100644 --- a/kobo/apps/openrosa/apps/viewer/models/data_dictionary.py +++ b/kobo/apps/openrosa/apps/viewer/models/data_dictionary.py @@ -1,4 +1,3 @@ -# coding: utf-8 import os import re from xml.dom import Node @@ -65,17 +64,11 @@ def __init__(self, *args, **kwargs): self.instances_for_export = lambda d: d.instances.all() super().__init__(*args, **kwargs) - def set_uuid_in_xml(self, file_name=None, id_string=None): + def set_uuid_in_xml(self): """ Add bind to automatically set UUID node in XML. """ - - if id_string: - root_node = id_string - else: - if not file_name: - file_name = self.file_name() - root_node, _ = os.path.splitext(file_name) + root_node = self.xform_root_node_name doc = clean_and_parse_xml(self.xml) model_nodes = doc.getElementsByTagName("model") @@ -98,19 +91,21 @@ def set_uuid_in_xml(self, file_name=None, id_string=None): instance_node = instance_nodes[0] # get the first child whose id attribute matches our id_string - survey_nodes = [node for node in instance_node.childNodes - if node.nodeType == Node.ELEMENT_NODE and - (node.tagName == root_node or - node.attributes.get('id'))] - + survey_nodes = [ + node + for node in instance_node.childNodes + if node.nodeType == Node.ELEMENT_NODE + and node.tagName == root_node + ] if len(survey_nodes) != 1: - raise Exception( - "Multiple survey nodes with the id '{}'".format(root_node)) + raise Exception(f'Multiple survey nodes `{root_node}`') survey_node = survey_nodes[0] - formhub_nodes = [n for n in survey_node.childNodes - if n.nodeType == Node.ELEMENT_NODE and - n.tagName == "formhub"] + formhub_nodes = [ + n + for n in survey_node.childNodes + if n.nodeType == Node.ELEMENT_NODE and n.tagName == 'formhub' + ] if len(formhub_nodes) > 1: raise Exception( @@ -130,10 +125,9 @@ def set_uuid_in_xml(self, file_name=None, id_string=None): if len(formhub_nodes) == 0: # append the calculate bind node calculate_node = doc.createElement("bind") - calculate_node.setAttribute( - "nodeset", "/%s/formhub/uuid" % root_node) - calculate_node.setAttribute("type", "string") - calculate_node.setAttribute("calculate", "'%s'" % self.uuid) + calculate_node.setAttribute('nodeset', f'/{root_node}/formhub/uuid') + calculate_node.setAttribute('type', 'string') + calculate_node.setAttribute('calculate', f"'{self.uuid}'") model_node.appendChild(calculate_node) self.xml = smart_str(doc.toprettyxml(indent=" ", encoding='utf-8')) @@ -166,9 +160,9 @@ def save(self, *args, **kwargs): survey.name = survey.id_string self.json = survey.to_json() self.xml = survey.to_xml() - self._mark_start_time_boolean() + self.mark_start_time_boolean() set_uuid(self) - self.set_uuid_in_xml(id_string=survey.id_string) + self.set_uuid_in_xml() super().save(*args, **kwargs) def file_name(self): @@ -418,7 +412,7 @@ def get_data_for_excel(self): self._expand_geocodes(d, key, e) yield d - def _mark_start_time_boolean(self): + def mark_start_time_boolean(self): starttime_substring = 'jr:preloadParams="start"' if self.xml.find(starttime_substring) != -1: self.has_start_time = True diff --git a/kobo/apps/openrosa/libs/utils/logger_tools.py b/kobo/apps/openrosa/libs/utils/logger_tools.py index 6570659a44..598c449127 100644 --- a/kobo/apps/openrosa/libs/utils/logger_tools.py +++ b/kobo/apps/openrosa/libs/utils/logger_tools.py @@ -523,26 +523,16 @@ def publish_xls_form(xls_file, user, id_string=None): return dd -def publish_xml_form(xml_file, user, id_string=None): +def publish_xml_form(xml_file, user): xml = smart_str(xml_file.read()) survey = create_survey_element_from_xml(xml) form_json = survey.to_json() - if id_string: - dd = DataDictionary.objects.get(user=user, id_string=id_string) - dd.xml = xml - dd.json = form_json - dd._mark_start_time_boolean() - set_uuid(dd) - dd.set_uuid_in_xml() - dd.save() - return dd - else: - dd = DataDictionary(user=user, xml=xml, json=form_json) - dd._mark_start_time_boolean() - set_uuid(dd) - dd.set_uuid_in_xml(file_name=xml_file.name) - dd.save() - return dd + dd = DataDictionary(user=user, xml=xml, json=form_json) + dd.mark_start_time_boolean() + set_uuid(dd) + dd.set_uuid_in_xml() + dd.save() + return dd def report_exception(subject, info, exc_info=None): diff --git a/kpi/models/asset_snapshot.py b/kpi/models/asset_snapshot.py index a97d435b8b..ecd6d3c5ce 100644 --- a/kpi/models/asset_snapshot.py +++ b/kpi/models/asset_snapshot.py @@ -242,3 +242,18 @@ def generate_xml_from_source(self, 'warnings': warnings, }) return xml, details + + @property + def xform_root_node_name(self): + """ + Retrieves the name of the XML tag representing the root node of the "survey" + in the XForm XML structure. + + This method uses the `name` setting from the XLSForm to determine the tag name. + If no name is provided, it falls back to using the asset UID. + """ + + try: + return self.asset.content['settings']['name'] + except KeyError: + return self.asset.uid diff --git a/kpi/utils/xml.py b/kpi/utils/xml.py index 202d8771ca..1f84c0392f 100644 --- a/kpi/utils/xml.py +++ b/kpi/utils/xml.py @@ -1,4 +1,3 @@ -# coding: utf-8 from __future__ import annotations import re @@ -373,6 +372,10 @@ class XMLFormWithDisclaimer: def __init__(self, obj: Union['kpi.AssetSnapshot', 'logger.XForm']): self._object = obj self._unique_id = obj.asset.uid + + # Avoid accessing the `xform_root_node_name` property immediately to prevent + # extra database queries. It will be set only when it is actually needed. + self._root_tag_name = None self._add_disclaimer() def get_object(self): @@ -391,6 +394,7 @@ def _add_disclaimer(self): translated, disclaimers_dict, default_language_code = value self._root_node = minidom.parseString(self._object.xml) + self._root_tag_name = self._object.xform_root_node_name if translated: self._add_translation_nodes(disclaimers_dict, default_language_code) @@ -413,7 +417,7 @@ def _add_instance_and_bind_nodes(self): # Inject <bind nodeset /> inside <model odk:xforms-version="1.0.0"> bind_node = self._root_node.createElement('bind') bind_node.setAttribute( - 'nodeset', f'/{self._unique_id}/_{self._unique_id}__disclaimer' + 'nodeset', f'/{self._root_tag_name}/_{self._unique_id}__disclaimer' ) bind_node.setAttribute('readonly', 'true()') bind_node.setAttribute('required', 'false()') @@ -421,9 +425,9 @@ def _add_instance_and_bind_nodes(self): bind_node.setAttribute('relevant', 'false()') model_node.appendChild(bind_node) - # Inject note node inside <{self._unique_id}> + # Inject note node inside <{self._root_tag_name}> instance_node = model_node.getElementsByTagName('instance')[0] - instance_node = instance_node.getElementsByTagName(self._unique_id)[0] + instance_node = instance_node.getElementsByTagName(self._root_tag_name)[0] instance_node.appendChild( self._root_node.createElement(f'_{self._unique_id}__disclaimer') ) @@ -442,11 +446,11 @@ def _add_disclaimer_input( disclaimer_input_label = self._root_node.createElement('label') disclaimer_input.setAttribute('appearance', 'kobo-disclaimer') disclaimer_input.setAttribute( - 'ref', f'/{self._unique_id}/_{self._unique_id}__disclaimer' + 'ref', f'/{self._root_tag_name}/_{self._unique_id}__disclaimer' ) if translated: - itext = f'/{self._unique_id}/_{self._unique_id}__disclaimer:label' + itext = f'/{self._root_tag_name}/_{self._unique_id}__disclaimer:label' disclaimer_input_label.setAttribute( 'ref', f"jr:itext('{itext}')", @@ -474,7 +478,7 @@ def _add_translation_nodes( disclaimer_translation = self._root_node.createElement('text') disclaimer_translation.setAttribute( 'id', - f'/{self._unique_id}/_{self._unique_id}__disclaimer:label', + f'/{self._root_tag_name}/_{self._unique_id}__disclaimer:label', ) value = self._root_node.createElement('value') language = n.getAttribute('lang').lower().strip()