diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6ab04a8c0..a1007f8a7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -36,4 +36,4 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
- run: npx semantic-release
+ run: npx semantic-release@19.0.5
diff --git a/README.md b/README.md
index f80760868..81796b73c 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ This guide presumes you have a functioning devstack.
2. In devstack + venv, run `$ make dev.provision.lms+studio+frontend-app-course-authoring` to make up the required services. Minimum services required are lms, studio and frontend-app-course-authoring.
4. In [Studio Django Admin](http://localhost:18000/admin/waffle/flag/) turn on `new_core_editors.use_new_text_editor` flag for HTML editor, `new_core_editors.use_new_video_editor` flag for new video editor, and `new_core_editors.use_new_problem_editor` flag for problems. The list of supported flags is in [toggles.py. ](https://github.com/openedx/edx-platform/blob/master/cms/djangoapps/contentstore/toggles.py). you might have to add a flag for your xblock of choice.
2. Clone this repo into the [`${DEVSTACK_WORKSPACE}/src` directory](https://edx.readthedocs.io/projects/open-edx-devstack/en/latest/readme.html?highlight=DEVSTACK_WORKSPACE#id9) the sibling repo of your edx devstack `/src`.
-3. In the course authoring app, follow the guide to use your [local verison of frontend-lib-content-components. ](https://github.com/openedx/frontend-build#local-module-configuration-for-webpack) The moduleconfig.js in the frontend-app-course-authoring repo will be:
+3. In the course authoring app, follow the guide to use your [local verison of frontend-lib-content-components. ](https://github.com/openedx/frontend-build#local-module-configuration-for-webpack) The module.config.js in the frontend-app-course-authoring repo will be:
```
module.exports = {
diff --git a/docs/decisions/0005-internal-editor-testability-decisions.md b/docs/decisions/0005-internal-editor-testability-decisions.md
new file mode 100644
index 000000000..cd7e43455
--- /dev/null
+++ b/docs/decisions/0005-internal-editor-testability-decisions.md
@@ -0,0 +1,136 @@
+# Internal editor testability decision
+
+# Increased complexity for the sake of testability
+The internally-managed editors in this repo (as of now planned to include text, video, and problem types) follow a number of patterns that increase the complexity of parts of the code slightly, in favor of providing increased testability around their behavior.
+
+## Note - Strict Dictionaries
+Javacript is generally fairly lackadaisical with regards to dictionary access of missing/invalid keys. This is fine and expected in many cases, but also prevents us from using dictionary access on something like a key store to ensure we are calling something that actually exists.
+
+For this purpose, there are a pair of utilities in this repo called `StrictDict` and `keyStore`.
+
+`StrictDict` takes an object and returns a version that will complain (throw an error) if called with an invalid key.
+
+`keyStore` takes an object and returns a StrictDict of just the keys of that object. (this is useful particularly for mocking and spying on specific methods and fields)
+
+## Note - Self imports
+Javascript webpack imports can be problematic around the specific issue of attempting to mock a single method being used in another method in the same file.
+
+Problem: File A includes methodA and methodB (which calls methodA). We want to be able to test methodA and then test methodB *without* having to re-test methodA as part of that test. We want to be able to mock methodA *only* while we are testing methodB.
+
+Solution: Self-imports. By importing the module into itself (which webpack handles nicely, don't worry), we provide tests the ability to spy on and mock individual methods from that module separately on a per-test basis.
+
+Ex:
+```javascript
+// myFile.js
+import * as module from './myFile';
+
+export const methodA = (val) => // do something complex with val and return a number
+export const methodB = (val) => module.methodA(val) * 2;
+
+// myFile.test.js
+import * as module from './myFile';
+import { keyStore } from './utils';
+
+cosnt moduleKeys = keyStore(module);
+
+describe('myFile', () => {
+ describe('methodA', () => ...);
+ describe('methodB', () => {
+ const mockMethodA = (val) => val + 3
+ const testValue = 23;
+ beforeEach(() => {
+ jest.spyOn(module, moduleKeys).mockImplementationValueOnce(mockMethodA);
+ });
+ it('returns double the output of methodA with the given value', () => {
+ expect(module.methodB(testValue)).toEqual(mockMethodA(testValue) + 3);
+ });
+ });
+});
+```
+
+## Hooks and Snapshots - Separation from components for increased viability of snapshots
+As part of the testing of these internal editors, we are relying on snapshot testing to ensure stability of the display of the components themselves. This can be a fragile solution in certain situations where components are too large or complex to adequately snapshot and verify.
+
+For this purpose, we have opted for a general pattern of separating all of the behavior of components withing these editors into separate `hooks` files.
+
+These hook files contain methods that utilize both `react` and `react-redux` hooks, along with arguments passed directly into the component, in order to generate the resulting desired behaviors.
+
+From there, components are tested by mocking out the behavior of these hooks to return verifyable data in the snapshots.
+
+As part of this separation, there are a number of additional patterns that are followed
+
+### Snapshot considerations
+#### Callbacks
+Any callback that is included in render in a component should be separated such that is is either passed in as a prop or derived from a hook, and should be mocked with a `mockName` using jest, to ensure that they are uniquely identifyable in the snapshots.
+
+Ex:
+```javascript
+const props = {
+ onClick: jest.fn().mockName('props.onClick');
+}
+expect(shallow( )).toMatchSnapshot();
+```
+
+#### Imported components
+Imported compoents are mocked to return simple string components based on their existing name. This results in shallow renders that display the components by name, with passed props, but do not attempt to render *any* logic from those components.
+
+This is a bit more complex for components with sub-components, but we have provided a test utility in `src/testUtils` called `mockNestedComponent` that will allow you to easily mock these for your snapshots as well.
+
+Ex:
+```javascript
+jest.mock('componentModule', () => {
+ const { mockNestedComponent } = jest.requireActual('./testUtils');
+ return {
+ SimpleComponents: () => 'SimpleComponent',
+ NestedComponent: mockNestedComponent('NestedComponent', {
+ NestedChild1: 'NestedComponent.NestedChild1',
+ NestedChild2: 'NestedComponent.NestedChild2',
+ }),
+});
+```
+#### Top-level mocked imports
+We have mocked out all paragon components and icons being used in the repo, as well as a number of other common methods, hooks, and components in our module's `setupTests` file, which will ensure that those components show up reasonably in snapshots.
+
+### Hook Considerations
+#### useState and mockUseState
+React's useState hook is a very powerful alternative to class components, but is also somewhat problematic to test, as it returns different values based on where it is called in a hook, as well as based on previous runs of that hook.
+
+To resolve this, we are using a custom test utility to mock a hook modules state values for easy testing.
+
+This requires a particular structure to hook modules that use the useState, for the sake of enabling the mock process (which is documented with the utility).
+
+Ex:
+```javascript
+import * as module from './hooks';
+const state = {
+ myStateValue: (val) => useState(val),
+};
+const myHook = () => {
+ const [myStateValue, setMyStateValue] = module.state.myStateValue('initialValue');
+};
+```
+Examples on how to use this for testing are included with the mock class in `src/testUtils`
+
+#### useCallback, useEffect
+These hooks provide behavior that calls a method based on given prerequisite behaviors.
+For this reason, we use general-purpose mocks for these hooks that return an object containing the passed callback and prerequisites for easy test access.
+
+#### Additional Considrations
+*useIntl not available*
+
+We are using react-intl under the hood for our i18n support, but do not have access to some of the more recent features in that library due to the pinned version in frontend-platform. Specifically, this includes a `useIntl` hook available in later versions that is still unavailable to us, requiring us to use the older `injectIntl` pattern.
+
+*useDispatch*
+
+React-redux's `useDispatch` hook does not play nicely with being called in a method called by a component, and really wants to be called *in* the component. For this reason, the dispatch method is generated in components and passed through to hook components.
+
+## Notes for integration testing
+Because of the top-level mocks in setupTest, any integration tests will need to be sure to unmock most of these.
+
+Ex:
+```javascript
+jest.unmock('@edx/frontend-platform/i18n');
+jest.unmock('@edx/paragon');
+jest.unmock('@edx/paragon/icons');
+jest.unmock('react-redux');
+```
diff --git a/package-lock.json b/package-lock.json
index 1c73363eb..27075c386 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"license": "AGPL-3.0",
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
+ "@codemirror/lang-xml": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@reduxjs/toolkit": "^1.8.1",
@@ -35,9 +36,9 @@
"devDependencies": {
"@edx/frontend-build": "^11.0.2",
"@edx/frontend-platform": "2.4.0",
- "@edx/paragon": "^20.21.3",
+ "@edx/paragon": "^20.27.0",
"@testing-library/dom": "^8.13.0",
- "@testing-library/react": "12.1.1",
+ "@testing-library/react": "^12.1.1",
"@testing-library/user-event": "^13.5.0",
"codecov": "3.8.3",
"enzyme": "3.11.0",
@@ -192,15 +193,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/code-frame/node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@babel/code-frame/node_modules/@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
@@ -437,24 +429,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/core/node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/core/node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@babel/core/node_modules/@babel/helper-validator-option": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
@@ -478,18 +452,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/core/node_modules/@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true,
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@babel/core/node_modules/@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
@@ -525,20 +487,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/core/node_modules/@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "dependencies": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@babel/core/node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -638,6 +586,36 @@
"node": ">=0.10.0"
}
},
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.19.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
+ "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.19.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
+ "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz",
+ "integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==",
+ "dev": true,
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/@babel/plugin-proposal-class-properties": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.0.tgz",
@@ -796,36 +774,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true,
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
@@ -861,20 +809,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/plugin-proposal-class-properties/node_modules/@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "dependencies": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@babel/plugin-proposal-class-properties/node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -1444,24 +1378,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/preset-env/node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/preset-env/node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@babel/preset-env/node_modules/@babel/helper-validator-option": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
@@ -1486,18 +1402,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/preset-env/node_modules/@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true,
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@babel/preset-env/node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz",
@@ -2466,20 +2370,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/preset-env/node_modules/@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "dependencies": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@babel/preset-env/node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -2722,24 +2612,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/preset-react/node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/preset-react/node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@babel/preset-react/node_modules/@babel/helper-validator-option": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
@@ -2829,20 +2701,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/preset-react/node_modules/@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "dependencies": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@babel/runtime": {
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz",
@@ -2854,6 +2712,20 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/types": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz",
+ "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.19.4",
+ "@babel/helper-validator-identifier": "^7.19.1",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@codemirror/autocomplete": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.4.0.tgz",
@@ -2912,14 +2784,6 @@
"@lezer/javascript": "^1.0.0"
}
},
- "node_modules/@codemirror/lang-javascript/node_modules/@lezer/highlight": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz",
- "integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==",
- "dependencies": {
- "@lezer/common": "^1.0.0"
- }
- },
"node_modules/@codemirror/lang-javascript/node_modules/@lezer/javascript": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.0.tgz",
@@ -2929,12 +2793,16 @@
"@lezer/lr": "^1.0.0"
}
},
- "node_modules/@codemirror/lang-javascript/node_modules/@lezer/lr": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz",
- "integrity": "sha512-f9319YG1A/3ysgUE3bqCHEd7g+3ZZ71MWlwEc42mpnLVYXgfJJgtu1XAyBB4Kz8FmqmnFe9caopDqKeMMMAU6g==",
+ "node_modules/@codemirror/lang-xml": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.1.tgz",
+ "integrity": "sha512-0tvycUTElajCcRKgsszhKjWX+uuOogdu5+enpfqYA+j0gnP8ek7LRxujh2/XMPRdXt/hwOML4slJLE7r2eX3yQ==",
"dependencies": {
- "@lezer/common": "^1.0.0"
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/xml": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
@@ -2950,22 +2818,6 @@
"style-mod": "^4.0.0"
}
},
- "node_modules/@codemirror/language/node_modules/@lezer/highlight": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz",
- "integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==",
- "dependencies": {
- "@lezer/common": "^1.0.0"
- }
- },
- "node_modules/@codemirror/language/node_modules/@lezer/lr": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz",
- "integrity": "sha512-f9319YG1A/3ysgUE3bqCHEd7g+3ZZ71MWlwEc42mpnLVYXgfJJgtu1XAyBB4Kz8FmqmnFe9caopDqKeMMMAU6g==",
- "dependencies": {
- "@lezer/common": "^1.0.0"
- }
- },
"node_modules/@codemirror/language/node_modules/style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
@@ -3288,12 +3140,6 @@
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
"dev": true
},
- "node_modules/@edx/frontend-build/node_modules/@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"node_modules/@edx/frontend-build/node_modules/@types/webpack": {
"version": "5.28.0",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.0.tgz",
@@ -4481,9 +4327,9 @@
}
},
"node_modules/@edx/paragon": {
- "version": "20.21.5",
- "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.21.5.tgz",
- "integrity": "sha512-7+pRDS3MejiF3tsOFs9R6PC66zZJb8eN2nYObNWnyUmKZ7DfzzpHVsDuUYUg+J5cm3dYMY2imyIrMrqov6ettA==",
+ "version": "20.27.0",
+ "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.27.0.tgz",
+ "integrity": "sha512-jy62ZEBdAVlsP6tAm1/YDyMtc9fiD47H00whoW+y2Z+lLZqPsv6D5boIPQIcdBeg0W4f2gCU4TEy2+b2q8mYGA==",
"dev": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
@@ -4567,23 +4413,6 @@
"integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==",
"dev": true
},
- "node_modules/@edx/paragon/node_modules/@types/prop-types": {
- "version": "15.7.5",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
- "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
- "dev": true
- },
- "node_modules/@edx/paragon/node_modules/@types/react": {
- "version": "18.0.26",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
- "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
- "dev": true,
- "dependencies": {
- "@types/prop-types": "*",
- "@types/scheduler": "*",
- "csstype": "^3.0.2"
- }
- },
"node_modules/@edx/paragon/node_modules/@types/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -5242,7 +5071,7 @@
"@lezer/lr": "^1.0.0"
}
},
- "node_modules/@lezer/css/node_modules/@lezer/highlight": {
+ "node_modules/@lezer/highlight": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz",
"integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==",
@@ -5250,14 +5079,6 @@
"@lezer/common": "^1.0.0"
}
},
- "node_modules/@lezer/css/node_modules/@lezer/lr": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz",
- "integrity": "sha512-f9319YG1A/3ysgUE3bqCHEd7g+3ZZ71MWlwEc42mpnLVYXgfJJgtu1XAyBB4Kz8FmqmnFe9caopDqKeMMMAU6g==",
- "dependencies": {
- "@lezer/common": "^1.0.0"
- }
- },
"node_modules/@lezer/html": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.0.tgz",
@@ -5268,20 +5089,21 @@
"@lezer/lr": "^1.0.0"
}
},
- "node_modules/@lezer/html/node_modules/@lezer/highlight": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz",
- "integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==",
+ "node_modules/@lezer/lr": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.0.tgz",
+ "integrity": "sha512-rpvS+WPS/PlbJCiW+bzXPbIFIRXmzRiTEDzMvrvgpED05w5ZQO59AzH3BJen2AnHuJIlP3DcJRjsKLTrkknUNA==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
- "node_modules/@lezer/html/node_modules/@lezer/lr": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz",
- "integrity": "sha512-f9319YG1A/3ysgUE3bqCHEd7g+3ZZ71MWlwEc42mpnLVYXgfJJgtu1XAyBB4Kz8FmqmnFe9caopDqKeMMMAU6g==",
+ "node_modules/@lezer/xml": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.1.tgz",
+ "integrity": "sha512-jMDXrV953sDAUEMI25VNrI9dz94Ai96FfeglytFINhhwQ867HKlCE2jt3AwZTCT7M528WxdDWv/Ty8e9wizwmQ==",
"dependencies": {
- "@lezer/common": "^1.0.0"
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
}
},
"node_modules/@mapbox/node-pre-gyp": {
@@ -5635,10 +5457,36 @@
}
},
"node_modules/@svgr/babel-plugin-remove-jsx-attribute": {
- "dev": true
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.5.0.tgz",
+ "integrity": "sha512-8zYdkym7qNyfXpWvu4yq46k41pyNM9SOstoWhKlm+IfdCE1DdnRKeMUPsWIEO/DEkaWxJ8T9esNdG3QwQ93jBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
},
"node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": {
- "dev": true
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.5.0.tgz",
+ "integrity": "sha512-NFdxMq3xA42Kb1UbzCVxplUc0iqSyM9X8kopImvFnB+uSDdzIHOdbs1op8ofAvVRtbg4oZiyRl3fTYeKcOe9Iw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
},
"node_modules/@svgr/webpack": {
"version": "6.2.1",
@@ -5931,24 +5779,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@svgr/webpack/node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@svgr/webpack/node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@svgr/webpack/node_modules/@babel/helper-validator-option": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
@@ -5972,18 +5802,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@svgr/webpack/node_modules/@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true,
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@svgr/webpack/node_modules/@babel/plugin-syntax-typescript": {
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz",
@@ -6083,20 +5901,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@svgr/webpack/node_modules/@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "dependencies": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@svgr/webpack/node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -6585,15 +6389,6 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
- "node_modules/@svgr/webpack/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/@svgr/webpack/node_modules/svg-parser": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
@@ -6702,16 +6497,42 @@
"dev": true
},
"node_modules/@types/babel__generator": {
- "dev": true
+ "version": "7.6.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz",
+ "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
},
"node_modules/@types/babel__template": {
- "dev": true
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz",
+ "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
},
"node_modules/@types/babel__traverse": {
- "dev": true
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz",
+ "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.3.0"
+ }
},
"node_modules/@types/body-parser": {
- "dev": true
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
+ "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
+ "dev": true,
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
},
"node_modules/@types/cheerio": {
"version": "0.22.31",
@@ -6722,41 +6543,151 @@
"@types/node": "*"
}
},
+ "node_modules/@types/connect": {
+ "version": "3.4.35",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
+ "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/eslint": {
+ "version": "8.4.10",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz",
+ "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
+ "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
+ "dev": true
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
+ "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
"dev": true
},
"node_modules/@types/istanbul-lib-report": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
+ "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==",
+ "dev": true,
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.11",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
+ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
+ "dev": true
+ },
+ "node_modules/@types/mime": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
+ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==",
"dev": true
},
"node_modules/@types/minimatch": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
"node_modules/@types/node": {
+ "version": "18.11.18",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
+ "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
},
"node_modules/@types/prop-types": {
- "dev": true
+ "version": "15.7.5",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
+ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
},
"node_modules/@types/qs": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"node_modules/@types/range-parser": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
+ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
- "node_modules/@types/react": {},
+ "node_modules/@types/react": {
+ "version": "18.0.26",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
+ "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/@types/scheduler": {
- "dev": true
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
},
"node_modules/@types/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==",
+ "dev": true,
+ "dependencies": {
+ "@types/mime": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/source-list-map": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
+ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
"dev": true
},
"node_modules/@types/uglify-js": {
- "dev": true
+ "version": "3.17.1",
+ "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz",
+ "integrity": "sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g==",
+ "dev": true,
+ "dependencies": {
+ "source-map": "^0.6.1"
+ }
},
"node_modules/@types/webpack-sources": {
- "dev": true
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz",
+ "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/source-list-map": "*",
+ "source-map": "^0.7.3"
+ }
+ },
+ "node_modules/@types/webpack-sources/node_modules/source-map": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
},
"node_modules/@types/yargs-parser": {
+ "version": "21.0.0",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz",
+ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
"dev": true
},
"node_modules/@videojs/http-streaming": {
@@ -8333,36 +8264,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/babel-eslint/node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/babel-eslint/node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/babel-eslint/node_modules/@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true,
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/babel-eslint/node_modules/@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
@@ -8398,20 +8299,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/babel-eslint/node_modules/@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "dependencies": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/babel-eslint/node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -8512,50 +8399,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/babel-jest/node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/babel-jest/node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/babel-jest/node_modules/@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true,
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/babel-jest/node_modules/@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "dependencies": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/babel-jest/node_modules/@cnakazawa/watch": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz",
@@ -8652,12 +8495,6 @@
"@types/node": "*"
}
},
- "node_modules/babel-jest/node_modules/@types/istanbul-lib-coverage": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
- "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
- "dev": true
- },
"node_modules/babel-jest/node_modules/@types/istanbul-reports": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
@@ -9580,15 +9417,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/babel-jest/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/babel-jest/node_modules/source-map-resolve": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
@@ -9705,12 +9533,6 @@
"webpack": ">=2"
}
},
- "node_modules/babel-loader/node_modules/@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"node_modules/babel-loader/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -9903,50 +9725,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/babel-plugin-react-intl/node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/babel-plugin-react-intl/node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/babel-plugin-react-intl/node_modules/@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true,
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/babel-plugin-react-intl/node_modules/@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "dependencies": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/babel-plugin-react-intl/node_modules/@formatjs/ecma402-abstract": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.4.0.tgz",
@@ -10027,12 +9805,6 @@
"@types/node": "*"
}
},
- "node_modules/babel-plugin-react-intl/node_modules/@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"node_modules/babel-plugin-react-intl/node_modules/@types/schema-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/schema-utils/-/schema-utils-2.4.0.tgz",
@@ -10111,38 +9883,6 @@
"is-valid-path": "^0.1.1"
}
},
- "node_modules/babel-plugin-transform-imports/node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/babel-plugin-transform-imports/node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/babel-plugin-transform-imports/node_modules/@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "dependencies": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/babel-polyfill": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
@@ -10204,36 +9944,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/babel-preset-jest/node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/babel-preset-jest/node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/babel-preset-jest/node_modules/@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true,
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/babel-preset-jest/node_modules/@babel/plugin-syntax-async-generators": {
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
@@ -10395,20 +10105,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/babel-preset-jest/node_modules/@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "dependencies": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/babel-preset-jest/node_modules/@types/babel__core": {
"version": "7.1.20",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.20.tgz",
@@ -10422,15 +10118,6 @@
"@types/babel__traverse": "*"
}
},
- "node_modules/babel-preset-jest/node_modules/@types/babel__traverse": {
- "version": "7.18.3",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz",
- "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==",
- "dev": true,
- "dependencies": {
- "@babel/types": "^7.3.0"
- }
- },
"node_modules/babel-preset-jest/node_modules/babel-plugin-jest-hoist": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz",
@@ -11247,15 +10934,6 @@
"source-map": "^0.6.0"
}
},
- "node_modules/clean-webpack-plugin/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -11594,12 +11272,6 @@
"webpack": "^4.27.0 || ^5.0.0"
}
},
- "node_modules/css-loader/node_modules/@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"node_modules/css-loader/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -12496,15 +12168,6 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
- "node_modules/cssnano/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/cssnano/node_modules/stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -14755,12 +14418,6 @@
"webpack": "^4.0.0 || ^5.0.0"
}
},
- "node_modules/file-loader/node_modules/@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"node_modules/file-loader/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -16695,15 +16352,6 @@
"strip-ansi": "^6.0.1"
}
},
- "node_modules/html-webpack-plugin/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/html-webpack-plugin/node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@@ -16946,12 +16594,6 @@
"@types/node": "*"
}
},
- "node_modules/image-webpack-loader/node_modules/@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"node_modules/image-webpack-loader/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -20688,16 +20330,6 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
- "node_modules/imagemin-svgo/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "optional": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/imagemin-svgo/node_modules/svgo": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz",
@@ -22789,11 +22421,6 @@
}
}
},
- "node_modules/jest-resolve": {
- "dev": true,
- "optional": true,
- "peer": true
- },
"node_modules/jest/node_modules/@babel/generator": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz",
@@ -22863,36 +22490,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/jest/node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/jest/node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/jest/node_modules/@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true,
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/jest/node_modules/@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
@@ -22928,20 +22525,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/jest/node_modules/@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "dependencies": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/jest/node_modules/@bcoe/v8-coverage": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
@@ -23291,15 +22874,6 @@
"node": ">= 6"
}
},
- "node_modules/jest/node_modules/@types/babel__traverse": {
- "version": "7.18.3",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz",
- "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==",
- "dev": true,
- "dependencies": {
- "@babel/types": "^7.3.0"
- }
- },
"node_modules/jest/node_modules/@types/graceful-fs": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
@@ -23309,12 +22883,6 @@
"@types/node": "*"
}
},
- "node_modules/jest/node_modules/@types/istanbul-lib-coverage": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
- "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
- "dev": true
- },
"node_modules/jest/node_modules/@types/istanbul-reports": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
@@ -25560,15 +25128,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/jest/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/jest/node_modules/source-map-resolve": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
@@ -26501,12 +26060,6 @@
"webpack": "^4.4.0 || ^5.0.0"
}
},
- "node_modules/mini-css-extract-plugin/node_modules/@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -26566,15 +26119,6 @@
"integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
"dev": true
},
- "node_modules/mini-css-extract-plugin/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/mini-css-extract-plugin/node_modules/webpack-sources": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
@@ -28742,12 +28286,6 @@
"node": ">= 8"
}
},
- "node_modules/react-dev-utils/node_modules/@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"node_modules/react-dev-utils/node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -29440,17 +28978,6 @@
"hoist-non-react-statics": "^3.3.0"
}
},
- "node_modules/react-intl/node_modules/@types/react": {
- "version": "18.0.26",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
- "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
- "dev": true,
- "dependencies": {
- "@types/prop-types": "*",
- "@types/scheduler": "*",
- "csstype": "^3.0.2"
- }
- },
"node_modules/react-intl/node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -30009,15 +29536,6 @@
"integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==",
"dev": true
},
- "node_modules/resolve-url-loader/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/resolve/node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -30641,6 +30159,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-loader": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.4.tgz",
@@ -31292,12 +30819,6 @@
"webpack": "^4.0.0 || ^5.0.0"
}
},
- "node_modules/style-loader/node_modules/@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"node_modules/style-loader/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -32038,12 +31559,6 @@
}
}
},
- "node_modules/url-loader/node_modules/@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"node_modules/url-loader/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -32814,12 +32329,6 @@
"@types/node": "*"
}
},
- "node_modules/webpack-dev-server/node_modules/@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"node_modules/webpack-dev-server/node_modules/@types/serve-index": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz",
@@ -34387,13 +33896,6 @@
"dev": true,
"peer": true
},
- "node_modules/webpack/node_modules/@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true,
- "peer": true
- },
"node_modules/webpack/node_modules/@webassemblyjs/ast": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
@@ -34694,16 +34196,6 @@
"url": "https://opencollective.com/webpack"
}
},
- "node_modules/webpack/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/webpack/node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@@ -35144,12 +34636,6 @@
"@babel/highlight": "^7.18.6"
},
"dependencies": {
- "@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true
- },
"@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
@@ -35333,18 +34819,6 @@
"@babel/types": "^7.18.6"
}
},
- "@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true
- },
- "@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true
- },
"@babel/helper-validator-option": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
@@ -35362,12 +34836,6 @@
"@babel/types": "^7.20.5"
}
},
- "@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true
- },
"@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
@@ -35397,17 +34865,6 @@
"globals": "^11.1.0"
}
},
- "@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "requires": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- }
- },
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -35479,6 +34936,24 @@
}
}
},
+ "@babel/helper-string-parser": {
+ "version": "7.19.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
+ "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
+ "dev": true
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.19.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
+ "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
+ "dev": true
+ },
+ "@babel/parser": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz",
+ "integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==",
+ "dev": true
+ },
"@babel/plugin-proposal-class-properties": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.0.tgz",
@@ -35595,24 +35070,6 @@
"@babel/types": "^7.18.6"
}
},
- "@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true
- },
- "@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true
- },
- "@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true
- },
"@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
@@ -35642,17 +35099,6 @@
"globals": "^11.1.0"
}
},
- "@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "requires": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- }
- },
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -36090,18 +35536,6 @@
"@babel/types": "^7.18.6"
}
},
- "@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true
- },
- "@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true
- },
"@babel/helper-validator-option": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
@@ -36120,12 +35554,6 @@
"@babel/types": "^7.20.5"
}
},
- "@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true
- },
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz",
@@ -36755,17 +36183,6 @@
"globals": "^11.1.0"
}
},
- "@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "requires": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- }
- },
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -36951,18 +36368,6 @@
"integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==",
"dev": true
},
- "@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true
- },
- "@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true
- },
"@babel/helper-validator-option": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
@@ -37018,17 +36423,6 @@
"@babel/helper-annotate-as-pure": "^7.18.6",
"@babel/helper-plugin-utils": "^7.18.6"
}
- },
- "@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "requires": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- }
}
}
},
@@ -37040,6 +36434,17 @@
"regenerator-runtime": "^0.13.11"
}
},
+ "@babel/types": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz",
+ "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-string-parser": "^7.19.4",
+ "@babel/helper-validator-identifier": "^7.19.1",
+ "to-fast-properties": "^2.0.0"
+ }
+ },
"@codemirror/autocomplete": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.4.0.tgz",
@@ -37092,14 +36497,6 @@
"@lezer/javascript": "^1.0.0"
},
"dependencies": {
- "@lezer/highlight": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz",
- "integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==",
- "requires": {
- "@lezer/common": "^1.0.0"
- }
- },
"@lezer/javascript": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.0.tgz",
@@ -37108,17 +36505,21 @@
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
- },
- "@lezer/lr": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz",
- "integrity": "sha512-f9319YG1A/3ysgUE3bqCHEd7g+3ZZ71MWlwEc42mpnLVYXgfJJgtu1XAyBB4Kz8FmqmnFe9caopDqKeMMMAU6g==",
- "requires": {
- "@lezer/common": "^1.0.0"
- }
}
}
},
+ "@codemirror/lang-xml": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.1.tgz",
+ "integrity": "sha512-0tvycUTElajCcRKgsszhKjWX+uuOogdu5+enpfqYA+j0gnP8ek7LRxujh2/XMPRdXt/hwOML4slJLE7r2eX3yQ==",
+ "requires": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/xml": "^1.0.0"
+ }
+ },
"@codemirror/language": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.3.2.tgz",
@@ -37132,22 +36533,6 @@
"style-mod": "^4.0.0"
},
"dependencies": {
- "@lezer/highlight": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz",
- "integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==",
- "requires": {
- "@lezer/common": "^1.0.0"
- }
- },
- "@lezer/lr": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz",
- "integrity": "sha512-f9319YG1A/3ysgUE3bqCHEd7g+3ZZ71MWlwEc42mpnLVYXgfJJgtu1XAyBB4Kz8FmqmnFe9caopDqKeMMMAU6g==",
- "requires": {
- "@lezer/common": "^1.0.0"
- }
- },
"style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
@@ -37393,12 +36778,6 @@
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
"dev": true
},
- "@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"@types/webpack": {
"version": "5.28.0",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.0.tgz",
@@ -38316,9 +37695,9 @@
}
},
"@edx/paragon": {
- "version": "20.21.5",
- "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.21.5.tgz",
- "integrity": "sha512-7+pRDS3MejiF3tsOFs9R6PC66zZJb8eN2nYObNWnyUmKZ7DfzzpHVsDuUYUg+J5cm3dYMY2imyIrMrqov6ettA==",
+ "version": "20.27.0",
+ "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.27.0.tgz",
+ "integrity": "sha512-jy62ZEBdAVlsP6tAm1/YDyMtc9fiD47H00whoW+y2Z+lLZqPsv6D5boIPQIcdBeg0W4f2gCU4TEy2+b2q8mYGA==",
"dev": true,
"requires": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
@@ -38384,23 +37763,6 @@
"integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==",
"dev": true
},
- "@types/prop-types": {
- "version": "15.7.5",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
- "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
- "dev": true
- },
- "@types/react": {
- "version": "18.0.26",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
- "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
- "dev": true,
- "requires": {
- "@types/prop-types": "*",
- "@types/scheduler": "*",
- "csstype": "^3.0.2"
- }
- },
"@types/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -38875,24 +38237,14 @@
"requires": {
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
- },
- "dependencies": {
- "@lezer/highlight": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz",
- "integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==",
- "requires": {
- "@lezer/common": "^1.0.0"
- }
- },
- "@lezer/lr": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz",
- "integrity": "sha512-f9319YG1A/3ysgUE3bqCHEd7g+3ZZ71MWlwEc42mpnLVYXgfJJgtu1XAyBB4Kz8FmqmnFe9caopDqKeMMMAU6g==",
- "requires": {
- "@lezer/common": "^1.0.0"
- }
- }
+ }
+ },
+ "@lezer/highlight": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz",
+ "integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==",
+ "requires": {
+ "@lezer/common": "^1.0.0"
}
},
"@lezer/html": {
@@ -38903,24 +38255,23 @@
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
- },
- "dependencies": {
- "@lezer/highlight": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz",
- "integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==",
- "requires": {
- "@lezer/common": "^1.0.0"
- }
- },
- "@lezer/lr": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz",
- "integrity": "sha512-f9319YG1A/3ysgUE3bqCHEd7g+3ZZ71MWlwEc42mpnLVYXgfJJgtu1XAyBB4Kz8FmqmnFe9caopDqKeMMMAU6g==",
- "requires": {
- "@lezer/common": "^1.0.0"
- }
- }
+ }
+ },
+ "@lezer/lr": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.0.tgz",
+ "integrity": "sha512-rpvS+WPS/PlbJCiW+bzXPbIFIRXmzRiTEDzMvrvgpED05w5ZQO59AzH3BJen2AnHuJIlP3DcJRjsKLTrkknUNA==",
+ "requires": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "@lezer/xml": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.1.tgz",
+ "integrity": "sha512-jMDXrV953sDAUEMI25VNrI9dz94Ai96FfeglytFINhhwQ867HKlCE2jt3AwZTCT7M528WxdDWv/Ty8e9wizwmQ==",
+ "requires": {
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
}
},
"@mapbox/node-pre-gyp": {
@@ -39195,10 +38546,18 @@
}
},
"@svgr/babel-plugin-remove-jsx-attribute": {
- "dev": true
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.5.0.tgz",
+ "integrity": "sha512-8zYdkym7qNyfXpWvu4yq46k41pyNM9SOstoWhKlm+IfdCE1DdnRKeMUPsWIEO/DEkaWxJ8T9esNdG3QwQ93jBA==",
+ "dev": true,
+ "requires": {}
},
"@svgr/babel-plugin-remove-jsx-empty-expression": {
- "dev": true
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.5.0.tgz",
+ "integrity": "sha512-NFdxMq3xA42Kb1UbzCVxplUc0iqSyM9X8kopImvFnB+uSDdzIHOdbs1op8ofAvVRtbg4oZiyRl3fTYeKcOe9Iw==",
+ "dev": true,
+ "requires": {}
},
"@svgr/webpack": {
"version": "6.2.1",
@@ -39419,18 +38778,6 @@
"@babel/types": "^7.18.6"
}
},
- "@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true
- },
- "@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true
- },
"@babel/helper-validator-option": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
@@ -39448,12 +38795,6 @@
"@babel/types": "^7.20.5"
}
},
- "@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true
- },
"@babel/plugin-syntax-typescript": {
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz",
@@ -39523,17 +38864,6 @@
"globals": "^11.1.0"
}
},
- "@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "requires": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- }
- },
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -39842,12 +39172,6 @@
"boolbase": "^1.0.0"
}
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
"svg-parser": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
@@ -39928,16 +39252,42 @@
"dev": true
},
"@types/babel__generator": {
- "dev": true
+ "version": "7.6.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz",
+ "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.0.0"
+ }
},
"@types/babel__template": {
- "dev": true
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz",
+ "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==",
+ "dev": true,
+ "requires": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
},
"@types/babel__traverse": {
- "dev": true
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz",
+ "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.3.0"
+ }
},
"@types/body-parser": {
- "dev": true
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
+ "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
+ "dev": true,
+ "requires": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
},
"@types/cheerio": {
"version": "0.22.31",
@@ -39948,41 +39298,150 @@
"@types/node": "*"
}
},
+ "@types/connect": {
+ "version": "3.4.35",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
+ "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/eslint": {
+ "version": "8.4.10",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz",
+ "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==",
+ "dev": true,
+ "requires": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "@types/estree": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
+ "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
+ "dev": true
+ },
+ "@types/istanbul-lib-coverage": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
+ "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
"dev": true
},
"@types/istanbul-lib-report": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
+ "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==",
+ "dev": true,
+ "requires": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "@types/json-schema": {
+ "version": "7.0.11",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
+ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
+ "dev": true
+ },
+ "@types/mime": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
+ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==",
"dev": true
},
"@types/minimatch": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
"@types/node": {
+ "version": "18.11.18",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
+ "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
},
"@types/prop-types": {
- "dev": true
+ "version": "15.7.5",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
+ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
},
"@types/qs": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"@types/range-parser": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
+ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
- "@types/react": {},
+ "@types/react": {
+ "version": "18.0.26",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
+ "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
"@types/scheduler": {
- "dev": true
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
},
"@types/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==",
+ "dev": true,
+ "requires": {
+ "@types/mime": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/source-list-map": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
+ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
"dev": true
},
"@types/uglify-js": {
- "dev": true
+ "version": "3.17.1",
+ "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz",
+ "integrity": "sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g==",
+ "dev": true,
+ "requires": {
+ "source-map": "^0.6.1"
+ }
},
"@types/webpack-sources": {
- "dev": true
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz",
+ "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "@types/source-list-map": "*",
+ "source-map": "^0.7.3"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
+ "dev": true
+ }
+ }
},
"@types/yargs-parser": {
+ "version": "21.0.0",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz",
+ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
"dev": true
},
"@videojs/http-streaming": {
@@ -41126,24 +40585,6 @@
"@babel/types": "^7.18.6"
}
},
- "@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true
- },
- "@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true
- },
- "@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true
- },
"@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
@@ -41173,17 +40614,6 @@
"globals": "^11.1.0"
}
},
- "@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "requires": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- }
- },
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -41259,35 +40689,6 @@
"integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==",
"dev": true
},
- "@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true
- },
- "@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true
- },
- "@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true
- },
- "@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "requires": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- }
- },
"@cnakazawa/watch": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz",
@@ -41369,12 +40770,6 @@
"@types/node": "*"
}
},
- "@types/istanbul-lib-coverage": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
- "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
- "dev": true
- },
"@types/istanbul-reports": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
@@ -42125,12 +41520,6 @@
}
}
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
"source-map-resolve": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
@@ -42227,12 +41616,6 @@
"schema-utils": "^2.6.5"
},
"dependencies": {
- "@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -42375,35 +41758,6 @@
"integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==",
"dev": true
},
- "@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true
- },
- "@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true
- },
- "@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true
- },
- "@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "requires": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- }
- },
"@formatjs/ecma402-abstract": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.4.0.tgz",
@@ -42477,12 +41831,6 @@
"@types/node": "*"
}
},
- "@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"@types/schema-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/schema-utils/-/schema-utils-2.4.0.tgz",
@@ -42547,31 +41895,6 @@
"requires": {
"@babel/types": "^7.4",
"is-valid-path": "^0.1.1"
- },
- "dependencies": {
- "@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true
- },
- "@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true
- },
- "@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "requires": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- }
- }
}
},
"babel-polyfill": {
@@ -42628,24 +41951,6 @@
"integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==",
"dev": true
},
- "@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true
- },
- "@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true
- },
- "@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true
- },
"@babel/plugin-syntax-async-generators": {
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
@@ -42765,17 +42070,6 @@
"@babel/types": "^7.18.10"
}
},
- "@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "requires": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- }
- },
"@types/babel__core": {
"version": "7.1.20",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.20.tgz",
@@ -42789,15 +42083,6 @@
"@types/babel__traverse": "*"
}
},
- "@types/babel__traverse": {
- "version": "7.18.3",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz",
- "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==",
- "dev": true,
- "requires": {
- "@babel/types": "^7.3.0"
- }
- },
"babel-plugin-jest-hoist": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz",
@@ -43418,12 +42703,6 @@
"anymatch": "^3.0.0",
"source-map": "^0.6.0"
}
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
}
}
},
@@ -43706,12 +42985,6 @@
"semver": "^7.3.5"
},
"dependencies": {
- "@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -44302,12 +43575,6 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
"stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -46041,12 +45308,6 @@
"schema-utils": "^3.0.0"
},
"dependencies": {
- "@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -47528,12 +46789,6 @@
"strip-ansi": "^6.0.1"
}
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
"source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@@ -47726,12 +46981,6 @@
"@types/node": "*"
}
},
- "@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -50736,13 +49985,6 @@
"boolbase": "^1.0.0"
}
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "optional": true
- },
"svgo": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz",
@@ -52423,24 +51665,6 @@
"@babel/types": "^7.18.6"
}
},
- "@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
- "dev": true
- },
- "@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
- "dev": true
- },
- "@babel/parser": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
- "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
- "dev": true
- },
"@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
@@ -52470,17 +51694,6 @@
"globals": "^11.1.0"
}
},
- "@babel/types": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
- "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
- "dev": true,
- "requires": {
- "@babel/helper-string-parser": "^7.19.4",
- "@babel/helper-validator-identifier": "^7.19.1",
- "to-fast-properties": "^2.0.0"
- }
- },
"@bcoe/v8-coverage": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
@@ -52773,15 +51986,6 @@
"integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
"dev": true
},
- "@types/babel__traverse": {
- "version": "7.18.3",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz",
- "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==",
- "dev": true,
- "requires": {
- "@babel/types": "^7.3.0"
- }
- },
"@types/graceful-fs": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
@@ -52791,12 +51995,6 @@
"@types/node": "*"
}
},
- "@types/istanbul-lib-coverage": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
- "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
- "dev": true
- },
"@types/istanbul-reports": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
@@ -54588,12 +53786,6 @@
}
}
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
"source-map-resolve": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
@@ -54931,11 +54123,6 @@
"dev": true,
"requires": {}
},
- "jest-resolve": {
- "dev": true,
- "optional": true,
- "peer": true
- },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -55358,12 +54545,6 @@
"webpack-sources": "^1.1.0"
},
"dependencies": {
- "@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -55405,12 +54586,6 @@
"integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
"dev": true
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
"webpack-sources": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
@@ -57027,12 +56202,6 @@
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true
},
- "@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -57555,17 +56724,6 @@
"hoist-non-react-statics": "^3.3.0"
}
},
- "@types/react": {
- "version": "18.0.26",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
- "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
- "dev": true,
- "requires": {
- "@types/prop-types": "*",
- "@types/scheduler": "*",
- "csstype": "^3.0.2"
- }
- },
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -58037,12 +57195,6 @@
"resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz",
"integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==",
"dev": true
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
}
}
},
@@ -58488,6 +57640,12 @@
"sort-keys": "^1.0.0"
}
},
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
"source-map-loader": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.4.tgz",
@@ -58974,12 +58132,6 @@
"schema-utils": "^3.0.0"
},
"dependencies": {
- "@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -59562,12 +58714,6 @@
"schema-utils": "^3.0.0"
},
"dependencies": {
- "@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -59862,13 +59008,6 @@
"dev": true,
"peer": true
},
- "@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true,
- "peer": true
- },
"@webassemblyjs/ast": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
@@ -60137,13 +59276,6 @@
"ajv-keywords": "^3.5.2"
}
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "peer": true
- },
"source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@@ -60566,12 +59698,6 @@
"@types/node": "*"
}
},
- "@types/json-schema": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
- },
"@types/serve-index": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz",
diff --git a/package.json b/package.json
index 7b99ac4ab..fc5f9b095 100644
--- a/package.json
+++ b/package.json
@@ -36,9 +36,9 @@
"devDependencies": {
"@edx/frontend-build": "^11.0.2",
"@edx/frontend-platform": "2.4.0",
- "@edx/paragon": "^20.21.3",
+ "@edx/paragon": "^20.27.0",
"@testing-library/dom": "^8.13.0",
- "@testing-library/react": "12.1.1",
+ "@testing-library/react": "^12.1.1",
"@testing-library/user-event": "^13.5.0",
"codecov": "3.8.3",
"enzyme": "3.11.0",
@@ -58,6 +58,7 @@
},
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
+ "@codemirror/lang-xml": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@reduxjs/toolkit": "^1.8.1",
diff --git a/src/editors/Editor.jsx b/src/editors/Editor.jsx
index d760c72de..6668e5eb3 100644
--- a/src/editors/Editor.jsx
+++ b/src/editors/Editor.jsx
@@ -30,7 +30,7 @@ export const Editor = ({
const EditorComponent = supportedEditors[blockType];
return (
-
+
TextEditor) 1`] = `
+
+
+
+
+ }
+ footerAction={null}
+ isOpen={false}
+ size="md"
+ title="Exit the editor?"
+ >
+
+
@@ -23,30 +56,22 @@ exports[`EditorContainer component render snapshot: initialized. enable save and
>
-
- My test content
-
+
+
+ My test content
+
+
+
+
+
+
+ }
+ footerAction={null}
+ isOpen={false}
+ size="md"
+ title="Exit the editor?"
+ >
+
+
@@ -83,27 +141,18 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav
>
+
(
-
+
{saveFailed && (
diff --git a/src/editors/containers/EditorContainer/hooks.js b/src/editors/containers/EditorContainer/hooks.js
index 7bb918f77..aa5466560 100644
--- a/src/editors/containers/EditorContainer/hooks.js
+++ b/src/editors/containers/EditorContainer/hooks.js
@@ -1,9 +1,12 @@
import { useSelector } from 'react-redux';
+import { useState } from 'react';
import { RequestKeys } from '../../data/constants/requests';
import { selectors } from '../../data/redux';
import * as appHooks from '../../hooks';
+import * as module from './hooks';
import analyticsEvt from '../../data/constants/analyticsEvt';
+import { StrictDict } from '../../utils';
export const {
navigateCallback,
@@ -11,6 +14,10 @@ export const {
saveBlock,
} = appHooks;
+export const state = StrictDict({
+ isCancelConfirmModalOpen: (val) => useState(val),
+});
+
export const handleSaveClicked = ({ dispatch, getContent, validateEntry }) => {
const destination = useSelector(selectors.app.returnUrl);
const analytics = useSelector(selectors.app.analytics);
@@ -23,7 +30,16 @@ export const handleSaveClicked = ({ dispatch, getContent, validateEntry }) => {
validateEntry,
});
};
-export const handleCancelClicked = ({ onClose }) => {
+export const cancelConfirmModalToggle = () => {
+ const [isCancelConfirmOpen, setIsOpen] = module.state.isCancelConfirmModalOpen(false);
+ return {
+ isCancelConfirmOpen,
+ openCancelConfirmModal: () => setIsOpen(true),
+ closeCancelConfirmModal: () => setIsOpen(false),
+ };
+};
+
+export const handleCancel = ({ onClose }) => {
if (onClose) {
return onClose;
}
@@ -34,6 +50,6 @@ export const handleCancelClicked = ({ onClose }) => {
});
};
export const isInitialized = () => useSelector(selectors.app.isInitialized);
-export const saveFailed = () => useSelector((state) => (
- selectors.requests.isFailed(state, { requestKey: RequestKeys.saveBlock })
+export const saveFailed = () => useSelector((rootState) => (
+ selectors.requests.isFailed(rootState, { requestKey: RequestKeys.saveBlock })
));
diff --git a/src/editors/containers/EditorContainer/hooks.test.jsx b/src/editors/containers/EditorContainer/hooks.test.jsx
index 098302fba..01b9412a7 100644
--- a/src/editors/containers/EditorContainer/hooks.test.jsx
+++ b/src/editors/containers/EditorContainer/hooks.test.jsx
@@ -1,4 +1,5 @@
import * as reactRedux from 'react-redux';
+import { MockUseState } from '../../../testUtils';
import { RequestKeys } from '../../data/constants/requests';
import { selectors } from '../../data/redux';
@@ -7,6 +8,8 @@ import * as appHooks from '../../hooks';
import * as hooks from './hooks';
import analyticsEvt from '../../data/constants/analyticsEvt';
+const hookState = new MockUseState(hooks);
+
jest.mock('../../data/redux', () => ({
selectors: {
app: {
@@ -67,9 +70,46 @@ describe('EditorContainer hooks', () => {
});
});
});
- describe('handleCancelClicked', () => {
+
+ describe('cancelConfirmModalToggle', () => {
+ const hookKey = hookState.keys.isCancelConfirmModalOpen;
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ describe('state hook', () => {
+ hookState.testGetter(hookKey);
+ });
+ describe('using state', () => {
+ beforeEach(() => {
+ hookState.mock();
+ });
+ afterEach(() => {
+ hookState.restore();
+ });
+
+ describe('cancelConfirmModalToggle', () => {
+ let hook;
+ beforeEach(() => {
+ hook = hooks.cancelConfirmModalToggle();
+ });
+ test('isCancelConfirmOpen: state value', () => {
+ expect(hook.isCancelConfirmOpen).toEqual(hookState.stateVals[hookKey]);
+ });
+ test('openCancelConfirmModal: calls setter with true', () => {
+ hook.openCancelConfirmModal();
+ expect(hookState.setState[hookKey]).toHaveBeenCalledWith(true);
+ });
+ test('closeCancelConfirmModal: calls setter with false', () => {
+ hook.closeCancelConfirmModal();
+ expect(hookState.setState[hookKey]).toHaveBeenCalledWith(false);
+ });
+ });
+ });
+ });
+
+ describe('handleCancel', () => {
it('calls navigateCallback to returnUrl if onClose is not passed', () => {
- expect(hooks.handleCancelClicked({})).toEqual(
+ expect(hooks.handleCancel({})).toEqual(
appHooks.navigateCallback({
destination: reactRedux.useSelector(selectors.app.returnUrl),
analyticsEvent: analyticsEvt.editorCancelClick,
@@ -79,7 +119,7 @@ describe('EditorContainer hooks', () => {
});
it('calls onClose and not navigateCallback if onClose is passed', () => {
const onClose = () => 'my close value';
- expect(hooks.handleCancelClicked({ onClose })).toEqual(onClose);
+ expect(hooks.handleCancel({ onClose })).toEqual(onClose);
expect(appHooks.navigateCallback).not.toHaveBeenCalled();
});
});
diff --git a/src/editors/containers/EditorContainer/index.jsx b/src/editors/containers/EditorContainer/index.jsx
index b8e5fc9f4..bd3658801 100644
--- a/src/editors/containers/EditorContainer/index.jsx
+++ b/src/editors/containers/EditorContainer/index.jsx
@@ -2,24 +2,50 @@ import React from 'react';
import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
-import { Icon, ModalDialog, IconButton } from '@edx/paragon';
+import {
+ Icon, ModalDialog, IconButton, Button,
+} from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
+import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import EditorFooter from './components/EditorFooter';
import TitleHeader from './components/TitleHeader';
import * as hooks from './hooks';
+import BaseModal from '../TextEditor/components/BaseModal';
+import messages from './messages';
export const EditorContainer = ({
children,
getContent,
onClose,
validateEntry,
+ // injected
+ intl,
}) => {
const dispatch = useDispatch();
const isInitialized = hooks.isInitialized();
- const handleCancelClicked = hooks.handleCancelClicked({ onClose });
+ const { isCancelConfirmOpen, openCancelConfirmModal, closeCancelConfirmModal } = hooks.cancelConfirmModalToggle();
+ const handleCancel = hooks.handleCancel({ onClose });
return (
-
+
+
+
+
+ )}
+ isOpen={isCancelConfirmOpen}
+ close={closeCancelConfirmModal}
+ title={intl.formatMessage(messages.cancelConfirmTitle)}
+ >
+
+
- {isInitialized && children}
+
+ {isInitialized && children}
+
({
isInitialized: jest.fn().mockReturnValue(true),
- handleCancelClicked: (args) => ({ handleCancelClicked: args }),
+ handleCancel: (args) => ({ handleCancel: args }),
handleSaveClicked: (args) => ({ handleSaveClicked: args }),
saveFailed: jest.fn().mockName('hooks.saveFailed'),
+ cancelConfirmModalToggle: jest.fn(() => ({
+ isCancelConfirmOpen: false,
+ openCancelConfirmModal: jest.fn().mockName('openCancelConfirmModal'),
+ closeCancelConfirmModal: jest.fn().mockName('closeCancelConfirmModal'),
+ })),
}));
let el;
@@ -39,23 +46,13 @@ describe('EditorContainer component', () => {
el = shallow({testContent} );
});
- test('close behavior is linked to modal onClose', () => {
- const expected = hooks.handleCancelClicked({ onClose: props.onClose });
- expect(el.find(IconButton)
- .props().onClick).toEqual(expected);
- });
- test('close behavior is linked to footer onCancel', () => {
- const expected = hooks.handleCancelClicked({ onClose: props.onClose });
- expect(el.children().at(2)
- .props().onCancel).toEqual(expected);
- });
test('save behavior is linked to footer onSave', () => {
const expected = hooks.handleSaveClicked({
dispatch: useDispatch(),
getContent: props.getContent,
validateEntry: props.validateEntry,
});
- expect(el.children().at(2)
+ expect(el.children().at(3)
.props().onSave).toEqual(expected);
});
});
diff --git a/src/editors/containers/EditorContainer/messages.js b/src/editors/containers/EditorContainer/messages.js
new file mode 100644
index 000000000..c9eb65dce
--- /dev/null
+++ b/src/editors/containers/EditorContainer/messages.js
@@ -0,0 +1,19 @@
+export const messages = {
+ cancelConfirmTitle: {
+ id: 'authoring.editorContainer.cancelConfirm.title',
+ defaultMessage: 'Exit the editor?',
+ description: 'Label for modal confirming cancellation',
+ },
+ cancelConfirmDescription: {
+ id: 'authoring.editorContainer.cancelConfirm.description',
+ defaultMessage: 'Are you sure you want to exit the editor? Any unsaved changes will be lost.',
+ description: 'Description text for modal confirming cancellation',
+ },
+ okButtonLabel: {
+ id: 'authoring.editorContainer.okButton.label',
+ defaultMessage: 'OK',
+ description: 'Label for OK button',
+ },
+};
+
+export default messages;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx
index a8e867ed7..73f07b3ea 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx
@@ -2,57 +2,21 @@ import React, { memo } from 'react';
import { connect, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import {
- Col, Collapsible, Icon, IconButton, Form, Row,
+ Collapsible,
+ Icon,
+ IconButton,
+ Form,
} from '@edx/paragon';
-import { AddComment, Delete } from '@edx/paragon/icons';
-import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { FeedbackOutline, DeleteOutline } from '@edx/paragon/icons';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
import { selectors } from '../../../../../data/redux';
import { answerOptionProps } from '../../../../../data/services/cms/types';
-import { ProblemTypeKeys } from '../../../../../data/constants/problem';
+import Checker from './components/Checker';
+import { FeedbackBox } from './components/Feedback';
import * as hooks from './hooks';
-const Checker = ({
- hasSingleAnswer, answer, setAnswer,
-}) => {
- let CheckerType = Form.Checkbox;
- if (hasSingleAnswer) {
- CheckerType = Form.Radio;
- }
- return (
- setAnswer({ correct: e.target.checked })}
- checked={answer.correct}
- >
- {answer.id}
-
- );
-};
-
-const FeedbackControl = ({
- feedback, onChange, labelMessage, labelMessageBoldUnderline, key, answer, intl,
-}) => (
-
-
- ,
- }}
- />
-
-
-
-);
-
export const AnswerOption = ({
answer,
hasSingleAnswer,
@@ -66,91 +30,55 @@ export const AnswerOption = ({
const setAnswer = hooks.setAnswer({ answer, hasSingleAnswer, dispatch });
const { isFeedbackVisible, toggleFeedback } = hooks.prepareFeedback(answer);
- const displayFeedbackControl = (answerObject) => {
- if (problemType !== ProblemTypeKeys.MULTISELECT) {
- return FeedbackControl({
- key: `feedback-${answerObject.id}`,
- feedback: answerObject.feedback,
- onChange: (e) => setAnswer({ feedback: e.target.value }),
- labelMessage: messages.selectedFeedbackLabel,
- labelMessageBoldUnderline: messages.selectedFeedbackLabelBoldUnderlineText,
- answer: answerObject,
- intl,
- });
- }
- return [
- FeedbackControl({
- key: `selectedfeedback-${answerObject.id}`,
- feedback: answerObject.selectedFeedback,
- onChange: (e) => setAnswer({ selectedFeedback: e.target.value }),
- labelMessage: messages.selectedFeedbackLabel,
- labelMessageBoldUnderline: messages.selectedFeedbackLabelBoldUnderlineText,
- answer: answerObject,
- intl,
- }),
- FeedbackControl({
- key: `unselectedfeedback-${answerObject.id}`,
- feedback: answerObject.unselectedFeedback,
- onChange: (e) => setAnswer({ unselectedFeedback: e.target.value }),
- labelMessage: messages.unSelectedFeedbackLabel,
- labelMessageBoldUnderline: messages.unSelectedFeedbackLabelBoldUnderlineText,
- answer: answerObject,
- intl,
- }),
- ];
- };
-
return (
-
-
-
-
+
+
+
+
{ setAnswer({ title: e.target.value }); }}
+ placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)}
+ />
+
+
-
-
-
- { setAnswer({ title: e.target.value }); }}
- placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)}
- />
-
-
-
- {displayFeedbackControl(answer)}
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
+
+
+
);
};
@@ -164,22 +92,6 @@ AnswerOption.propTypes = {
problemType: PropTypes.string.isRequired,
};
-FeedbackControl.propTypes = {
- feedback: PropTypes.string.isRequired,
- onChange: PropTypes.func.isRequired,
- labelMessage: PropTypes.string.isRequired,
- labelMessageBoldUnderline: PropTypes.string.isRequired,
- key: PropTypes.string.isRequired,
- answer: answerOptionProps.isRequired,
- intl: intlShape.isRequired,
-};
-
-Checker.propTypes = {
- hasSingleAnswer: PropTypes.bool.isRequired,
- answer: answerOptionProps.isRequired,
- setAnswer: PropTypes.func.isRequired,
-};
-
export const mapStateToProps = (state) => ({
problemType: selectors.problem.problemType(state),
});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.jsx
index 5e40a528c..439c597a3 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.jsx
@@ -1,14 +1,23 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../testUtils';
-import { AnswerOption } from './AnswerOption';
+import { selectors } from '../../../../../data/redux';
+import { AnswerOption, mapStateToProps } from './AnswerOption';
+
+jest.mock('../../../../../data/redux', () => ({
+ selectors: {
+ problem: {
+ problemType: jest.fn(state => ({ problemType: state })),
+ },
+ },
+}));
describe('AnswerOption', () => {
const answerWithOnlyFeedback = {
id: 'A',
title: 'Answer 1',
correct: true,
- feedback: 'some feedback',
+ selectedFeedback: 'some feedback',
};
const answerWithSelectedUnselectedFeedback = {
id: 'A',
@@ -23,15 +32,23 @@ describe('AnswerOption', () => {
answer: answerWithOnlyFeedback,
// inject
intl: { formatMessage },
- deleteAnswer: jest.fn(),
- updateAnswer: jest.fn(),
+ // redux
+ problemType: 'multiplechoiceresponse',
};
describe('render', () => {
test('snapshot: renders correct option with feedback', () => {
expect(shallow( )).toMatchSnapshot();
});
test('snapshot: renders correct option with selected unselected feedback', () => {
- expect(shallow( )).toMatchSnapshot();
+ expect(shallow( )).toMatchSnapshot();
+ });
+ });
+ describe('mapStateToProps', () => {
+ const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
+ test('problemType from problem.problemType', () => {
+ expect(
+ mapStateToProps(testState).problemType,
+ ).toEqual(selectors.problem.problemType(testState));
});
});
});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.jsx
index d85b9ea2e..42c079a4f 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.jsx
@@ -6,7 +6,7 @@ import { Add } from '@edx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';
-import { initializeAnswerContainer } from '../../../hooks';
+import { useAnswerContainer, isSingleAnswerProblem } from './hooks';
import { actions, selectors } from '../../../../../data/redux';
import { answerOptionProps } from '../../../../../data/services/cms/types';
import AnswerOption from './AnswerOption';
@@ -16,11 +16,14 @@ export const AnswersContainer = ({
// Redux
answers,
addAnswer,
+ updateField,
}) => {
- const { hasSingleAnswer } = initializeAnswerContainer(problemType);
+ const hasSingleAnswer = isSingleAnswerProblem(problemType);
+
+ useAnswerContainer({ answers, problemType, updateField });
return (
-
+
{answers.map((answer) => (
))}
({
@@ -52,6 +56,7 @@ export const mapStateToProps = (state) => ({
export const mapDispatchToProps = {
addAnswer: actions.problem.addAnswer,
+ updateField: actions.problem.updateField,
};
export default connect(mapStateToProps, mapDispatchToProps)(AnswersContainer);
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.test.jsx
new file mode 100644
index 000000000..fa3f42d55
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.test.jsx
@@ -0,0 +1,90 @@
+/* eslint-disable react/prop-types */
+import React from 'react';
+import { shallow } from 'enzyme';
+import { act, render, waitFor } from '@testing-library/react';
+
+import { actions, selectors } from '../../../../../data/redux';
+
+import * as module from './AnswersContainer';
+
+import { AnswersContainer as AnswersContainerWithoutHOC } from './AnswersContainer';
+
+jest.mock('@edx/frontend-platform/i18n', () => ({
+ FormattedMessage: ({ defaultMessage }) => ({defaultMessage}
),
+ injectIntl: (args) => args,
+ intlShape: {},
+}));
+
+jest.mock('./AnswerOption', () => () => MockAnswerOption
);
+
+jest.mock('../../../../../data/redux', () => ({
+ actions: {
+ problem: {
+ updateField: jest.fn().mockName('actions.problem.updateField'),
+ addAnswer: jest.fn().mockName('actions.problem.addAnswer'),
+ },
+ },
+ selectors: {
+ problem: {
+ answers: jest.fn(state => ({ answers: state })),
+ },
+ },
+}));
+describe('AnswersContainer', () => {
+ const props = {
+ answers: [],
+ updateField: jest.fn(),
+ addAnswer: jest.fn(),
+ };
+ describe('render', () => {
+ test('snapshot: renders correct default', () => {
+ act(() => {
+ expect(shallow( )).toMatchSnapshot();
+ });
+ });
+ test('snapshot: renders correctly with answers', () => {
+ act(() => {
+ expect(shallow(
+ ,
+ )).toMatchSnapshot();
+ });
+ });
+
+ test('with react-testing-library', async () => {
+ let container = null;
+ await act(async () => {
+ const wrapper = render(
+ ,
+ );
+ container = wrapper.container;
+ });
+
+ await waitFor(() => expect(container.querySelector('button')).toBeTruthy());
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ expect(props.updateField).toHaveBeenCalledWith(expect.objectContaining({ correctAnswerCount: 2 }));
+ });
+ });
+ describe('mapStateToProps', () => {
+ const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
+ test('answers from problem.answers', () => {
+ expect(
+ module.mapStateToProps(testState).answers,
+ ).toEqual(selectors.problem.answers(testState));
+ });
+ });
+ describe('mapDispatchToProps', () => {
+ test('updateField from actions.problem.updateField', () => {
+ expect(module.mapDispatchToProps.updateField).toEqual(actions.problem.updateField);
+ });
+ test('updateField from actions.problem.addAnswer', () => {
+ expect(module.mapDispatchToProps.addAnswer).toEqual(actions.problem.addAnswer);
+ });
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/__snapshots__/AnswerOption.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/__snapshots__/AnswerOption.test.jsx.snap
index 5aa379eaa..fb4a9c8fa 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/__snapshots__/AnswerOption.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/__snapshots__/AnswerOption.test.jsx.snap
@@ -2,113 +2,115 @@
exports[`AnswerOption render snapshot: renders correct option with feedback 1`] = `
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
`;
exports[`AnswerOption render snapshot: renders correct option with selected unselected feedback 1`] = `
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/__snapshots__/AnswersContainer.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/__snapshots__/AnswersContainer.test.jsx.snap
new file mode 100644
index 000000000..07078350c
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/__snapshots__/AnswersContainer.test.jsx.snap
@@ -0,0 +1,59 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AnswersContainer render snapshot: renders correct default 1`] = `
+
+
+
+
+
+`;
+
+exports[`AnswersContainer render snapshot: renders correctly with answers 1`] = `
+
+
+
+
+
+
+
+`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Checker/__snapshots__/index.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Checker/__snapshots__/index.test.jsx.snap
new file mode 100644
index 000000000..a9692438d
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Checker/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Checker component with multiple answers 1`] = `
+
+ A
+
+`;
+
+exports[`Checker component with single answer 1`] = `
+
+ A
+
+`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Checker/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Checker/index.jsx
new file mode 100644
index 000000000..2ac2a4d7a
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Checker/index.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Form } from '@edx/paragon';
+
+const Checker = ({
+ hasSingleAnswer, answer, setAnswer,
+}) => {
+ let CheckerType = Form.Checkbox;
+ if (hasSingleAnswer) {
+ CheckerType = Form.Radio;
+ }
+ return (
+ setAnswer({ correct: e.target.checked })}
+ checked={answer.correct}
+ >
+ {answer.id}
+
+ );
+};
+Checker.propTypes = {
+ hasSingleAnswer: PropTypes.bool.isRequired,
+ answer: PropTypes.shape({
+ correct: PropTypes.bool,
+ id: PropTypes.number,
+ }).isRequired,
+ setAnswer: PropTypes.func.isRequired,
+};
+
+export default Checker;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Checker/index.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Checker/index.test.jsx
new file mode 100644
index 000000000..19998a536
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Checker/index.test.jsx
@@ -0,0 +1,22 @@
+import { shallow } from 'enzyme';
+import Checker from '.';
+
+const props = {
+ hasSingleAnswer: true,
+ answer: {
+ id: 'A',
+ title: 'Answer 1',
+ correct: true,
+ selectedFeedback: 'some feedback',
+ },
+ setAnswer: jest.fn(),
+};
+describe('Checker component', () => {
+ test('with single answer', () => {
+ expect(shallow( )).toMatchSnapshot();
+ });
+
+ test('with multiple answers', () => {
+ expect(shallow( )).toMatchSnapshot();
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/FeedbackBox.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/FeedbackBox.jsx
new file mode 100644
index 000000000..347f4cd07
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/FeedbackBox.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
+import { answerOptionProps } from '../../../../../../../data/services/cms/types';
+import FeedbackControl from './FeedbackControl';
+import { messages } from './messages';
+
+export const FeedbackBox = ({
+ answer, setAnswer, intl,
+}) => {
+ const props = {
+ onChange: (e) => setAnswer({ selectedFeedback: e.target.value }),
+ answer,
+ intl,
+ };
+
+ return (
+
+
+
+
+ );
+};
+FeedbackBox.propTypes = {
+ answer: answerOptionProps.isRequired,
+ setAnswer: PropTypes.func.isRequired,
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(FeedbackBox);
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/FeedbackBox.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/FeedbackBox.test.jsx
new file mode 100644
index 000000000..117c48255
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/FeedbackBox.test.jsx
@@ -0,0 +1,22 @@
+import { shallow } from 'enzyme';
+import { FeedbackBox } from './FeedbackBox';
+
+const answerWithFeedback = {
+ id: 'A',
+ title: 'Answer 1',
+ correct: true,
+ selectedFeedback: 'some feedback',
+ unselectedFeedback: 'unselectedFeedback',
+};
+
+const props = {
+ answer: answerWithFeedback,
+ intl: {},
+ setAnswer: jest.fn(),
+};
+
+describe('FeedbackBox component', () => {
+ test('renders', () => {
+ expect(shallow( )).toMatchSnapshot();
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/FeedbackControl.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/FeedbackControl.jsx
new file mode 100644
index 000000000..12ff8091d
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/FeedbackControl.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
+import { Form } from '@edx/paragon';
+
+import { answerOptionProps } from '../../../../../../../data/services/cms/types';
+import messages from './messages';
+
+const FeedbackControl = ({
+ feedback, onChange, labelMessage, labelMessageBoldUnderline, answer, intl,
+}) => (
+
+
+ ,
+ }}
+ />
+
+
+
+);
+FeedbackControl.propTypes = {
+ feedback: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ labelMessage: PropTypes.string.isRequired,
+ labelMessageBoldUnderline: PropTypes.string.isRequired,
+ answer: answerOptionProps.isRequired,
+ intl: intlShape.isRequired,
+};
+
+export default FeedbackControl;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/FeedbackControl.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/FeedbackControl.test.jsx
new file mode 100644
index 000000000..93d6499d4
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/FeedbackControl.test.jsx
@@ -0,0 +1,26 @@
+import { shallow } from 'enzyme';
+import FeedbackControl from './FeedbackControl';
+
+const answerWithFeedback = {
+ id: 'A',
+ title: 'Answer 1',
+ correct: true,
+ selectedFeedback: 'some feedback',
+ unselectedFeedback: 'unselectedFeedback',
+};
+
+const props = {
+ answer: answerWithFeedback,
+ intl: { formatMessage: jest.fn() },
+ setAnswer: jest.fn(),
+ feedback: 'feedback',
+ onChange: jest.fn(),
+ labelMessage: 'msg',
+ labelMessageBoldUnderline: 'msg',
+};
+
+describe('FeedbackControl component', () => {
+ test('renders', () => {
+ expect(shallow( )).toMatchSnapshot();
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/__snapshots__/FeedbackBox.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/__snapshots__/FeedbackBox.test.jsx.snap
new file mode 100644
index 000000000..4c9113846
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/__snapshots__/FeedbackBox.test.jsx.snap
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FeedbackBox component renders 1`] = `
+
+
+
+
+`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/__snapshots__/FeedbackControl.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/__snapshots__/FeedbackControl.test.jsx.snap
new file mode 100644
index 000000000..40a7fde43
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/__snapshots__/FeedbackControl.test.jsx.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FeedbackControl component renders 1`] = `
+
+
+
+
+
+
+ ,
+ }
+ }
+ />
+
+
+
+`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/index.jsx
new file mode 100644
index 000000000..bce02835a
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/index.jsx
@@ -0,0 +1,2 @@
+export { default as FeedbackBox } from './FeedbackBox';
+export { default as FeedbackControl } from './FeedbackControl';
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/messages.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/messages.js
new file mode 100644
index 000000000..a580c9c7e
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/components/Feedback/messages.js
@@ -0,0 +1,34 @@
+export const messages = {
+ feedbackPlaceholder: {
+ id: 'authoring.answerwidget.feedback.placeholder',
+ defaultMessage: 'Feedback message',
+ description: 'Placeholder text for feedback text',
+ },
+ feedbackToggleIconAltText: {
+ id: 'authoring.answerwidget.feedback.icon.alt',
+ defaultMessage: 'Toggle feedback',
+ description: 'Alt text for feedback toggle icon',
+ },
+ selectedFeedbackLabel: {
+ id: 'authoring.answerwidget.feedback.selected.label',
+ defaultMessage: 'Show following feedback when {answerId} {boldunderline}:',
+ description: 'Label text for feedback if option is selected',
+ },
+ selectedFeedbackLabelBoldUnderlineText: {
+ id: 'authoring.answerwidget.feedback.selected.label.boldunderline',
+ defaultMessage: 'is selected',
+ description: 'Bold & underlined text for feedback if option is selected',
+ },
+ unSelectedFeedbackLabel: {
+ id: 'authoring.answerwidget.feedback.unselected.label',
+ defaultMessage: 'Show following feedback when {answerId} {boldunderline}:',
+ description: 'Label text for feedback if option is not selected',
+ },
+ unSelectedFeedbackLabelBoldUnderlineText: {
+ id: 'authoring.answerwidget.feedback.unselected.label.boldunderline',
+ defaultMessage: 'is not selected',
+ description: 'Bold & underlined text for feedback if option is not selected',
+ },
+};
+
+export default messages;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hook.test.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hook.test.js
index 892e21af4..ff5d51abe 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hook.test.js
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hook.test.js
@@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { MockUseState } from '../../../../../../testUtils';
+import { ProblemTypeKeys } from '../../../../../data/constants/problem';
import * as module from './hooks';
jest.mock('react', () => {
@@ -52,4 +53,15 @@ describe('Answer Options Hooks', () => {
expect(state.setState[key]).toHaveBeenCalledWith(true);
});
});
+ describe('isSingleAnswerProblem()', () => {
+ test('singleSelect', () => {
+ expect(module.isSingleAnswerProblem(ProblemTypeKeys.SINGLESELECT)).toBe(true);
+ });
+ test('multiSelect', () => {
+ expect(module.isSingleAnswerProblem(ProblemTypeKeys.MULTISELECT)).toBe(false);
+ });
+ test('dropdown', () => {
+ expect(module.isSingleAnswerProblem(ProblemTypeKeys.DROPDOWN)).toBe(true);
+ });
+ });
});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js
index 5559b5c70..74f141637 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { StrictDict } from '../../../../../utils';
import * as module from './hooks';
import { actions } from '../../../../../data/redux';
+import { ProblemTypeKeys } from '../../../../../data/constants/problem';
export const state = StrictDict({
isFeedbackVisible: (val) => useState(val),
@@ -37,6 +38,22 @@ export const prepareFeedback = (answer) => {
};
};
-export default {
- state, removeAnswer, setAnswer, prepareFeedback,
+export const isSingleAnswerProblem = (problemType) => (
+ problemType === ProblemTypeKeys.DROPDOWN || problemType === ProblemTypeKeys.SINGLESELECT
+);
+
+export const useAnswerContainer = ({ answers, updateField }) => {
+ useEffect(() => {
+ let answerCount = 0;
+ answers.forEach(answer => {
+ if (answer.correct) {
+ answerCount += 1;
+ }
+ });
+ updateField({ correctAnswerCount: answerCount });
+ }, []);
+};
+
+export default {
+ state, removeAnswer, setAnswer, prepareFeedback, isSingleAnswerProblem, useAnswerContainer,
};
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/index.jsx
index 7e68f2cc9..e9c9bd928 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/index.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/index.jsx
@@ -15,11 +15,11 @@ const AnswerWidget = ({
const problemStaticData = ProblemTypes[problemType];
return (
-
-
+
+
-
+
{problemStaticData.description}
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/index.scss b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/index.scss
index 622a591a9..59ad4d855 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/index.scss
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/index.scss
@@ -1,11 +1,32 @@
-.problem-answer {
- padding: 12px;
- color: #00262B;
- .problem-answer-title {
- font-weight: bold;
- }
+.answer-option {
+ .answer-option-flex-item-1 {
+ flex-basis: 8.33%;
+ }
- .problem-answer-description {
- font-size: 0.9rem;
+ .answer-option-flex-item-2 {
+ flex-basis: 83.34%;
+ }
+
+ .answer-option-flex-item-3 {
+ flex-basis: 8.33%;
+ }
+
+ .answer-option-textarea {
+ textarea {
+ border: none;
+ resize: none;
+
+ &:active, &:hover, &:focus, &:focus-visible {
+ border: none;
+ border-right: none;
+ border-left: none;
+ border-radius: 0;
+ box-shadow: none;
+ }
+
+ &:focus, &:hover {
+ border-bottom: 1px solid #000;
+ }
}
+ }
}
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.jsx
index 8eceb0277..1e31b2f99 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.jsx
@@ -6,6 +6,7 @@ import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import * as hooks from '../../../hooks';
import { selectors, actions } from '../../../../../data/redux';
import { messages } from './messages';
+import './index.scss';
// This widget should be connected, grab all questions from store, update them as needed.
export const QuestionWidget = ({
@@ -15,12 +16,11 @@ export const QuestionWidget = ({
const { editorRef, refReady, setEditorRef } = hooks.prepareEditorRef();
if (!refReady) { return null; }
return (
-
);
};
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.scss b/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.scss
new file mode 100644
index 000000000..9292bef5f
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.scss
@@ -0,0 +1,28 @@
+.question-widget {
+ .tox-tinymce {
+ border-radius: 0.375rem;
+ }
+
+ .tox {
+ .tox-toolbar__primary {
+ background: none;
+ }
+ }
+
+ .tox-statusbar {
+ border-top: none;
+ }
+
+ .tox-toolbar__group:not(:last-of-type) {
+ // TODO: Find a way to override the border without !important
+ border-right: none !important;
+
+ &:after {
+ content: '';
+ position: relative;
+ left: 5px;
+ border: 1px solid #eae6e5;
+ height: 24px;
+ }
+ }
+}
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/SettingsOption.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/SettingsOption.jsx
index f74e04217..d3cc22bfa 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/SettingsOption.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/SettingsOption.jsx
@@ -12,7 +12,7 @@ export const SettingsOption = ({
const { isCardCollapsed, toggleCardCollapse } = showFullCard();
return (
-
+
+
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/__snapshots__/index.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/__snapshots__/index.test.jsx.snap
index 346860fd2..432523260 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/__snapshots__/index.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/__snapshots__/index.test.jsx.snap
@@ -2,172 +2,182 @@
exports[`SettingsWidget snapshot snapshot: renders Settings widget page 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-`;
-
-exports[`SettingsWidget snapshot snapshot: renders Settings widget page advanced settings visible 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`SettingsWidget snapshot snapshot: renders Settings widget page advanced settings visible 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js
index e77ab6196..47c88e6e7 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js
@@ -3,7 +3,7 @@ import { useState, useEffect } from 'react';
import _ from 'lodash-es';
import * as module from './hooks';
import messages from './messages';
-import { ShowAnswerTypesKeys } from '../../../../../data/constants/problem';
+import { ProblemTypeKeys, ShowAnswerTypesKeys } from '../../../../../data/constants/problem';
export const state = {
showAdvanced: (val) => useState(val),
@@ -113,14 +113,14 @@ export const resetCardHooks = (updateSettings) => {
export const scoringCardHooks = (scoring, updateSettings) => {
const handleMaxAttemptChange = (event) => {
- let unlimitedAttempts = true;
+ let unlimitedAttempts = false;
let attemptNumber = parseInt(event.target.value);
if (_.isNaN(attemptNumber)) {
+ attemptNumber = null;
+ unlimitedAttempts = true;
+ } else if (attemptNumber < 0) {
attemptNumber = 0;
}
- if (attemptNumber > 0) {
- unlimitedAttempts = false;
- }
updateSettings({ scoring: { ...scoring, attempts: { number: attemptNumber, unlimited: unlimitedAttempts } } });
};
@@ -182,11 +182,23 @@ export const timerCardHooks = (updateSettings) => ({
},
});
-export const typeRowHooks = (typeKey, updateField) => {
+export const typeRowHooks = ({
+ answers,
+ correctAnswerCount,
+ typeKey,
+ updateField,
+ updateAnswer,
+}) => {
const onClick = () => {
+ if (typeKey === ProblemTypeKeys.SINGLESELECT || typeKey === ProblemTypeKeys.DROPDOWN) {
+ if (correctAnswerCount > 1) {
+ answers.forEach(answer => {
+ updateAnswer({ ...answer, correct: false });
+ });
+ }
+ }
updateField({ problemType: typeKey });
};
-
return {
onClick,
};
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.test.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.test.js
index 9d9a41b9d..556004402 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.test.js
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.test.js
@@ -18,6 +18,7 @@ jest.mock('../../../../../data/redux', () => ({
problem: {
updateSettings: (args) => ({ updateSettings: args }),
updateField: (args) => ({ updateField: args }),
+ updateAnswer: (args) => ({ updateAnswer: args }),
},
},
}));
@@ -177,7 +178,31 @@ describe('Problem settings hooks', () => {
const value = 0;
output.handleMaxAttemptChange({ target: { value } });
expect(updateSettings)
- .toHaveBeenCalledWith({ scoring: { ...scoring, attempts: { number: value, unlimited: true } } });
+ .toHaveBeenCalledWith({ scoring: { ...scoring, attempts: { number: value, unlimited: false } } });
+ });
+ test('test handleMaxAttemptChange set attempts to null value', () => {
+ const value = null;
+ output.handleMaxAttemptChange({ target: { value } });
+ expect(updateSettings)
+ .toHaveBeenCalledWith({ scoring: { ...scoring, attempts: { number: null, unlimited: true } } });
+ });
+ test('test handleMaxAttemptChange set attempts to empty string', () => {
+ const value = '';
+ output.handleMaxAttemptChange({ target: { value } });
+ expect(updateSettings)
+ .toHaveBeenCalledWith({ scoring: { ...scoring, attempts: { number: null, unlimited: true } } });
+ });
+ test('test handleMaxAttemptChange set attempts to non-numeric value', () => {
+ const value = 'abc';
+ output.handleMaxAttemptChange({ target: { value } });
+ expect(updateSettings)
+ .toHaveBeenCalledWith({ scoring: { ...scoring, attempts: { number: null, unlimited: true } } });
+ });
+ test('test handleMaxAttemptChange set attempts to negative value', () => {
+ const value = -1;
+ output.handleMaxAttemptChange({ target: { value } });
+ expect(updateSettings)
+ .toHaveBeenCalledWith({ scoring: { ...scoring, attempts: { number: 0, unlimited: false } } });
});
test('test handleWeightChange', () => {
const value = 2;
@@ -217,10 +242,27 @@ describe('Problem settings hooks', () => {
describe('Type row hooks', () => {
test('test onClick', () => {
- const typekey = 'TEXTINPUT';
+ const typekey = 'multiplechoiceresponse';
const updateField = jest.fn();
- output = hooks.typeRowHooks(typekey, updateField);
+ const updateAnswer = jest.fn();
+ const answers = [{
+ correct: true,
+ id: 'a',
+ },
+ {
+ correct: true,
+ id: 'b',
+ }];
+ output = hooks.typeRowHooks({
+ answers,
+ correctAnswerCount: 2,
+ typeKey: typekey,
+ updateField,
+ updateAnswer,
+ });
output.onClick();
+ expect(updateAnswer).toHaveBeenNthCalledWith(1, { ...answers[0], correct: false });
+ expect(updateAnswer).toHaveBeenNthCalledWith(2, { ...answers[1], correct: false });
expect(updateField).toHaveBeenCalledWith({ problemType: typekey });
});
});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx
index 90e5f904a..b8f0aa374 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx
@@ -13,6 +13,7 @@ import ResetCard from './settingsComponents/ResetCard';
import MatlabCard from './settingsComponents/MatlabCard';
import TimerCard from './settingsComponents/TimerCard';
import TypeCard from './settingsComponents/TypeCard';
+import SwitchToAdvancedEditorCard from './settingsComponents/SwitchToAdvancedEditorCard';
import messages from './messages';
import { showAdvancedSettingsCards } from './hooks';
@@ -22,72 +23,90 @@ import './index.scss';
export const SettingsWidget = ({
problemType,
// redux
+ answers,
+ correctAnswerCount,
settings,
updateSettings,
updateField,
+ updateAnswer,
}) => {
const { isAdvancedCardsVisible, showAdvancedCards } = showAdvancedSettingsCards();
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
SettingsWidget.propTypes = {
+ answers: PropTypes.arrayOf(PropTypes.shape({
+ correct: PropTypes.bool,
+ id: PropTypes.string,
+ selectedFeedback: PropTypes.string,
+ title: PropTypes.string,
+ unselectedFeedback: PropTypes.string,
+ })).isRequired,
+ correctAnswerCount: PropTypes.number.isRequired,
problemType: PropTypes.string.isRequired,
+ updateAnswer: PropTypes.func.isRequired,
updateField: PropTypes.func.isRequired,
updateSettings: PropTypes.func.isRequired,
// eslint-disable-next-line
@@ -96,11 +115,14 @@ SettingsWidget.propTypes = {
const mapStateToProps = (state) => ({
settings: selectors.problem.settings(state),
+ answers: selectors.problem.answers(state),
+ correctAnswerCount: selectors.problem.correctAnswerCount(state),
});
export const mapDispatchToProps = {
updateSettings: actions.problem.updateSettings,
updateField: actions.problem.updateField,
+ updateAnswer: actions.problem.updateAnswer,
};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SettingsWidget));
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.js
index 4539decc3..ce597208a 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.js
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.js
@@ -36,7 +36,7 @@ export const messages = {
},
noHintSummary: {
id: 'authoring.problemeditor.settings.hint.noHintSummary',
- defaultMessage: 'No Hints',
+ defaultMessage: 'None',
description: 'Summary text for no hints',
},
hintSummary: {
@@ -104,10 +104,35 @@ export const messages = {
defaultMessage: 'Points',
description: 'Scoring weight input label',
},
- scoringSummary: {
- id: 'authoring.problemeditor.settings.scoring.summary',
- defaultMessage: '{attempts, plural, =0 {Unlimited} other {#}} attempts - {weight, plural, =0 {Ungraded} other {# points}}',
- description: 'Summary text for scoring settings',
+ unlimitedAttemptsSummary: {
+ id: 'authoring.problemeditor.settings.scoring.unlimited',
+ defaultMessage: 'Unlimited attempts',
+ description: 'Summary text for unlimited attempts',
+ },
+ attemptsSummary: {
+ id: 'authoring.problemeditor.settings.scoring.attempts',
+ defaultMessage: '{attempts, plural, =1 {# attempt} other {# attempts}}',
+ description: 'Summary text for number of attempts',
+ },
+ weightSummary: {
+ id: 'authoring.problemeditor.settings.scoring.weight',
+ defaultMessage: '{weight, plural, =0 {Ungraded} other {# points}}',
+ description: 'Summary text for scoring weight',
+ },
+ scoringSettingsLabel: {
+ id: 'authoring.problemeditor.settings.scoring.label',
+ defaultMessage: 'Specify point weight and the number of answer attempts',
+ description: 'Descriptive text for scoring settings',
+ },
+ attemptsHint: {
+ id: 'authoring.problemeditor.settings.scoring.attempts.hint',
+ defaultMessage: 'If a value is not set, unlimited attempts are allowed',
+ description: 'Summary text for scoring weight',
+ },
+ weightHint: {
+ id: 'authoring.problemeditor.settings.scoring.weight.hint',
+ defaultMessage: 'If a value is not set, the problem is worth one point',
+ description: 'Summary text for scoring weight',
},
showAnswerSettingsTitle: {
id: 'authoring.problemeditor.settings.showAnswer.title',
@@ -149,6 +174,26 @@ export const messages = {
defaultMessage: 'Type',
description: 'Type settings card title',
},
+ SwitchButtonLabel: {
+ id: 'authoring.problemeditor.settings.switchtoadvancededitor.label',
+ defaultMessage: 'Switch To Advanced Editor',
+ description: 'button to switch to the advanced mode of the editor.',
+ },
+ ConfirmSwitchMessage: {
+ id: 'authoring.problemeditor.settings.switchtoadvancededitor.message',
+ defaultMessage: 'If you use the advanced editor, this problem will be converted to OLX and you will not be able to return to the simple editor.',
+ description: 'message to confirm that a user wants to use the advanced editor',
+ },
+ ConfirmSwitchMessageTitle: {
+ id: 'authoring.problemeditor.settings.switchtoadvancededitor.message',
+ defaultMessage: 'Convert to OLX?',
+ description: 'message to confirm that a user wants to use the advanced editor',
+ },
+ ConfirmSwitchButtonLabel: {
+ id: 'authoring.problemeditor.settings.switchtoadvancededitor.message',
+ defaultMessage: 'Switch To Advanced Editor',
+ description: 'message to confirm that a user wants to use the advanced editor',
+ },
};
export default messages;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/HintsCard.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/HintsCard.jsx
index ef2fff7e0..088fd3eff 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/HintsCard.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/HintsCard.jsx
@@ -29,7 +29,7 @@ export const HintsCard = ({
/>
))}
{
const { handleMaxAttemptChange, handleWeightChange } = scoringCardHooks(scoring, updateSettings);
+ const getScoringSummary = (attempts, unlimited, weight) => {
+ let summary = unlimited
+ ? intl.formatMessage(messages.unlimitedAttemptsSummary)
+ : intl.formatMessage(messages.attemptsSummary, { attempts });
+ summary += ` ${String.fromCharCode(183)} `;
+ summary += intl.formatMessage(messages.weightSummary, { weight });
+ return summary;
+ };
+
return (
+
+
+
+
+
+
+
+
+
);
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ShowAnswerCard.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ShowAnswerCard.jsx
index da15c2fbc..cdd8186ac 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ShowAnswerCard.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ShowAnswerCard.jsx
@@ -1,9 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
import { Form, Hyperlink } from '@edx/paragon';
import SettingsOption from '../SettingsOption';
import { ShowAnswerTypes, ShowAnswerTypesKeys } from '../../../../../../data/constants/problem';
+import { selectors } from '../../../../../../data/redux';
import messages from '../messages';
import { showAnswerCardHooks } from '../hooks';
@@ -12,6 +14,9 @@ export const ShowAnswerCard = ({
updateSettings,
// inject
intl,
+ // redux
+ studioEndpointUrl,
+ learningContextId,
}) => {
const {
handleShowAnswerChange,
@@ -29,7 +34,7 @@ export const ShowAnswerCard = ({
-
+
@@ -49,7 +54,7 @@ export const ShowAnswerCard = ({
))}
- { showAttempts
+ {showAttempts
&& (
({
+ studioEndpointUrl: selectors.app.studioEndpointUrl(state),
+ learningContextId: selectors.app.learningContextId(state),
+});
+
+export const mapDispatchToProps = {};
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ShowAnswerCard));
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ShowAnswerCard.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ShowAnswerCard.test.jsx
index e727746e2..63d2e2bfe 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ShowAnswerCard.test.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ShowAnswerCard.test.jsx
@@ -1,13 +1,23 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../../testUtils';
-import { ShowAnswerCard } from './ShowAnswerCard';
+import { selectors } from '../../../../../../data/redux';
+import { ShowAnswerCard, mapStateToProps, mapDispatchToProps } from './ShowAnswerCard';
import { showAnswerCardHooks } from '../hooks';
jest.mock('../hooks', () => ({
showAnswerCardHooks: jest.fn(),
}));
+jest.mock('../../../../../../data/redux', () => ({
+ selectors: {
+ app: {
+ studioEndpointUrl: jest.fn(state => ({ studioEndpointUrl: state })),
+ learningContextId: jest.fn(state => ({ learningContextId: state })),
+ },
+ },
+}));
+
describe('ShowAnswerCard', () => {
const showAnswer = {
on: 'after_attempts',
@@ -17,7 +27,11 @@ describe('ShowAnswerCard', () => {
};
const props = {
showAnswer,
+ // injected
intl: { formatMessage },
+ // redux
+ studioEndpointUrl: 'SoMEeNDpOinT',
+ learningContextId: 'sOMEcouRseId',
};
const showAnswerCardHooksProps = {
@@ -39,4 +53,22 @@ describe('ShowAnswerCard', () => {
expect(shallow( )).toMatchSnapshot();
});
});
+ describe('mapStateToProps', () => {
+ const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
+ test('studioEndpointUrl from app.studioEndpointUrl', () => {
+ expect(
+ mapStateToProps(testState).studioEndpointUrl,
+ ).toEqual(selectors.app.studioEndpointUrl(testState));
+ });
+ test('learningContextId from app.learningContextId', () => {
+ expect(
+ mapStateToProps(testState).learningContextId,
+ ).toEqual(selectors.app.learningContextId(testState));
+ });
+ });
+ describe('mapDispatchToProps', () => {
+ test('equal an empty object', () => {
+ expect(mapDispatchToProps).toEqual({});
+ });
+ });
});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchToAdvancedEditorCard.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchToAdvancedEditorCard.jsx
new file mode 100644
index 000000000..e68e10351
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchToAdvancedEditorCard.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
+import { Button, Card } from '@edx/paragon';
+import PropTypes from 'prop-types';
+import messages from '../messages';
+import { thunkActions } from '../../../../../../data/redux';
+import BaseModal from '../../../../../TextEditor/components/BaseModal';
+
+export const SwitchToAdvancedEditorCard = ({
+ switchToAdvancedEditor,
+}) => {
+ const [isConfirmOpen, setConfirmOpen] = React.useState(false);
+ return (
+
+ { setConfirmOpen(false); }}
+ title={( )}
+ confirmAction={(
+
+
+
+ )}
+ size="md"
+ >
+
+
+ { setConfirmOpen(true); }}
+ >
+
+
+
+ );
+};
+
+SwitchToAdvancedEditorCard.propTypes = {
+ switchToAdvancedEditor: PropTypes.func.isRequired,
+};
+
+export const mapStateToProps = () => ({
+});
+export const mapDispatchToProps = {
+ switchToAdvancedEditor: thunkActions.problem.switchToAdvancedEditor,
+};
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SwitchToAdvancedEditorCard));
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchToAdvancedEditorCard.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchToAdvancedEditorCard.test.jsx
new file mode 100644
index 000000000..96d0b4a5c
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchToAdvancedEditorCard.test.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { SwitchToAdvancedEditorCard } from './SwitchToAdvancedEditorCard';
+
+describe('SwitchToAdvancedEditorCard snapshot', () => {
+ const mockSwitchToAdvancedEditor = jest.fn().mockName('switchToAdvancedEditor');
+ test('snapshot: SwitchToAdvancedEditorCard', () => {
+ expect(
+ shallow( ),
+ ).toMatchSnapshot();
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.jsx
index 06a085db2..c73779a04 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.jsx
@@ -7,12 +7,17 @@ import messages from '../messages';
import TypeRow from './TypeRow';
export const TypeCard = ({
+ answers,
+ correctAnswerCount,
problemType,
updateField,
+ updateAnswer,
// inject
intl,
}) => {
- const problemTypeKeysArray = Object.values(ProblemTypeKeys);
+ const problemTypeKeysArray = Object.values(ProblemTypeKeys).filter(key => key !== ProblemTypeKeys.ADVANCED);
+
+ if (problemType === ProblemTypeKeys.ADVANCED) { return null; }
return (
{problemTypeKeysArray.map((typeKey, i) => (
))}
@@ -34,9 +42,19 @@ export const TypeCard = ({
};
TypeCard.propTypes = {
- intl: intlShape.isRequired,
+ answers: PropTypes.arrayOf(PropTypes.shape({
+ correct: PropTypes.bool,
+ id: PropTypes.string,
+ selectedFeedback: PropTypes.string,
+ title: PropTypes.string,
+ unselectedFeedback: PropTypes.string,
+ })).isRequired,
+ correctAnswerCount: PropTypes.number.isRequired,
problemType: PropTypes.string.isRequired,
updateField: PropTypes.func.isRequired,
+ updateAnswer: PropTypes.func.isRequired,
+ // injected
+ intl: intlShape.isRequired,
};
export default injectIntl(TypeCard);
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.test.jsx
index 1886661a5..d2a2f8518 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.test.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.test.jsx
@@ -6,8 +6,12 @@ import { ProblemTypeKeys } from '../../../../../../data/constants/problem';
describe('TypeCard', () => {
const props = {
+ answers: [],
+ correctAnswerCount: 0,
problemType: ProblemTypeKeys.TEXTINPUT,
updateField: jest.fn().mockName('args.updateField'),
+ updateAnswer: jest.fn().mockName('args.updateAnswer'),
+ // injected
intl: { formatMessage },
};
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.jsx
index 71810543c..811499d43 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.jsx
@@ -5,13 +5,22 @@ import { Check } from '@edx/paragon/icons';
import { typeRowHooks } from '../hooks';
export const TypeRow = ({
+ answers,
+ correctAnswerCount,
typeKey,
label,
selected,
lastRow,
updateField,
+ updateAnswer,
}) => {
- const { onClick } = typeRowHooks(typeKey, updateField);
+ const { onClick } = typeRowHooks({
+ answers,
+ correctAnswerCount,
+ typeKey,
+ updateField,
+ updateAnswer,
+ });
return (
<>
@@ -25,10 +34,19 @@ export const TypeRow = ({
};
TypeRow.propTypes = {
+ answers: PropTypes.arrayOf(PropTypes.shape({
+ correct: PropTypes.bool,
+ id: PropTypes.string,
+ selectedFeedback: PropTypes.string,
+ title: PropTypes.string,
+ unselectedFeedback: PropTypes.string,
+ })).isRequired,
+ correctAnswerCount: PropTypes.number.isRequired,
typeKey: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
selected: PropTypes.bool.isRequired,
lastRow: PropTypes.bool.isRequired,
+ updateAnswer: PropTypes.func.isRequired,
updateField: PropTypes.func.isRequired,
};
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.test.jsx
index 72c4579e8..2065e57c0 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.test.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.test.jsx
@@ -10,11 +10,14 @@ jest.mock('../hooks', () => ({
describe('TypeRow', () => {
const typeKey = 'TEXTINPUT';
const props = {
+ answers: [],
+ correctAnswerCount: 0,
typeKey,
label: 'Text Input Problem',
selected: true,
lastRow: false,
updateField: jest.fn().mockName('args.updateField'),
+ updateAnswer: jest.fn().mockName('args.updateAnswer'),
};
const typeRowHooksProps = {
@@ -26,7 +29,13 @@ describe('TypeRow', () => {
describe('behavior', () => {
it(' calls typeRowHooks when initialized', () => {
shallow( );
- expect(typeRowHooks).toHaveBeenCalledWith(typeKey, props.updateField);
+ expect(typeRowHooks).toHaveBeenCalledWith({
+ answers: props.answers,
+ correctAnswerCount: props.correctAnswerCount,
+ typeKey,
+ updateField: props.updateField,
+ updateAnswer: props.updateAnswer,
+ });
});
});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/HintRow.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/HintRow.test.jsx.snap
index ea0adb253..a106c71a8 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/HintRow.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/HintRow.test.jsx.snap
@@ -5,7 +5,7 @@ exports[`HintRow snapshot snapshot: renders hints row 1`] = `
fluid={true}
>
-
@@ -15,8 +15,8 @@ exports[`HintRow snapshot snapshot: renders hints row 1`] = `
value="hint_1"
/>
-
-
+
-
+
`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/HintsCard.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/HintsCard.test.jsx.snap
index f25b722aa..2863895dd 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/HintsCard.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/HintsCard.test.jsx.snap
@@ -20,7 +20,7 @@ exports[`HintsCard snapshot snapshot: renders hints setting card multiple hints
value=""
/>
@@ -35,11 +35,11 @@ exports[`HintsCard snapshot snapshot: renders hints setting card multiple hints
exports[`HintsCard snapshot snapshot: renders hints setting card no hints 1`] = `
@@ -65,7 +65,7 @@ exports[`HintsCard snapshot snapshot: renders hints setting card one hint 1`] =
value="hint1"
/>
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/ScoringCard.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/ScoringCard.test.jsx.snap
index e97e3d4f7..566d13fe5 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/ScoringCard.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/ScoringCard.test.jsx.snap
@@ -2,9 +2,18 @@
exports[`ScoringCard snapshot snapshot: scoring setting card 1`] = `
+
+
+
+
+
+
+
+
+
`;
exports[`ScoringCard snapshot snapshot: scoring setting card max attempts 1`] = `
+
+
+
+
+
+
+
+
+
`;
exports[`ScoringCard snapshot snapshot: scoring setting card zero zero weight 1`] = `
+
+
+
+
+
+
+
+
+
`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/ShowAnswerCard.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/ShowAnswerCard.test.jsx.snap
index 8a750c7b3..7e4b65d7e 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/ShowAnswerCard.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/ShowAnswerCard.test.jsx.snap
@@ -20,7 +20,7 @@ exports[`ShowAnswerCard snapshot snapshot: show answer setting card 1`] = `
className="spacedMessage"
>
+
+
+
+ }
+ footerAction={null}
+ isOpen={false}
+ size="md"
+ title={
+
+ }
+ >
+
+
+
+
+
+
+`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/TypeCard.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/TypeCard.test.jsx.snap
index 251289f10..9daac71fe 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/TypeCard.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/TypeCard.test.jsx.snap
@@ -2,55 +2,62 @@
exports[`TypeCard snapshot snapshot: renders type setting card 1`] = `
-
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/__snapshots__/index.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/__snapshots__/index.test.jsx.snap
new file mode 100644
index 000000000..f5b33aaa9
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,64 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EditorProblemView component renders raw editor 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`EditorProblemView component renders simple view 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/hooks.js
new file mode 100644
index 000000000..80298bc56
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/hooks.js
@@ -0,0 +1,14 @@
+import ReactStateSettingsParser from '../../data/ReactStateSettingsParser';
+import ReactStateOLXParser from '../../data/ReactStateOLXParser';
+
+// eslint-disable-next-line import/prefer-default-export
+export const parseState = (problem, isAdvanced, ref) => () => {
+ const reactSettingsParser = new ReactStateSettingsParser(problem);
+ const reactOLXParser = new ReactStateOLXParser({ problem });
+ const rawOLX = ref?.current?.state.doc.toString();
+
+ return {
+ settings: reactSettingsParser.getSettings(),
+ olx: isAdvanced ? rawOLX : reactOLXParser.buildOLX(),
+ };
+};
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/hooks.test.js b/src/editors/containers/ProblemEditor/components/EditProblemView/hooks.test.js
new file mode 100644
index 000000000..9cfc8a42f
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/hooks.test.js
@@ -0,0 +1,25 @@
+import * as hooks from './hooks';
+
+const mockRawOLX = 'rawOLX';
+const mockBuiltOLX = 'builtOLX';
+
+jest.mock('../../data/ReactStateOLXParser', () => (
+ jest.fn().mockImplementation(() => ({
+ buildOLX: () => mockBuiltOLX,
+ }))
+));
+jest.mock('../../data/ReactStateSettingsParser');
+
+describe('EditProblemView hooks parseState', () => {
+ const toStringMock = () => mockRawOLX;
+ const refMock = { current: { state: { doc: { toString: toStringMock } } } };
+
+ test('default problem', () => {
+ const res = hooks.parseState('problem', false, refMock)();
+ expect(res.olx).toBe(mockBuiltOLX);
+ });
+ test('advanced problem', () => {
+ const res = hooks.parseState('problem', true, refMock)();
+ expect(res.olx).toBe(mockRawOLX);
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx
index 5600f9e3a..f43ddd1d2 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/index.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
@@ -6,34 +6,35 @@ import { Col, Container, Row } from '@edx/paragon';
import AnswerWidget from './AnswerWidget';
import SettingsWidget from './SettingsWidget';
import QuestionWidget from './QuestionWidget';
-import { EditorContainer } from '../../../EditorContainer';
+import EditorContainer from '../../../EditorContainer';
import { selectors } from '../../../../data/redux';
-import ReactStateSettingsParser from '../../data/ReactStateSettingsParser';
-import ReactStateOLXParser from '../../data/ReactStateOLXParser';
-import { AdvanceProblemKeys } from '../../../../data/constants/problem';
+import RawEditor from '../../../../sharedComponents/RawEditor';
+import { ProblemTypeKeys } from '../../../../data/constants/problem';
+
+import { parseState } from './hooks';
export const EditProblemView = ({
problemType,
problemState,
}) => {
- const parseState = (problem) => () => {
- const reactSettingsParser = new ReactStateSettingsParser(problem);
- const reactOLXParser = new ReactStateOLXParser({ problem });
- return {
- settings: reactSettingsParser.getSettings(),
- olx: reactOLXParser.buildOLX(),
- };
- };
- if (Object.values(AdvanceProblemKeys).includes(problemType)) {
- return `hello raw editor with ${problemType}`;
- }
+ const editorRef = useRef(null);
+ const isAdvancedProblemType = problemType === ProblemTypeKeys.ADVANCED;
+
+ const getContent = parseState(problemState, isAdvancedProblemType, editorRef);
+
return (
-
-
+
+
-
-
+ {isAdvancedProblemType ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/index.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/index.test.jsx
new file mode 100644
index 000000000..05e934a07
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/index.test.jsx
@@ -0,0 +1,20 @@
+import { shallow } from 'enzyme';
+import { EditProblemView } from '.';
+import AnswerWidget from './AnswerWidget';
+import { ProblemTypeKeys } from '../../../../data/constants/problem';
+import RawEditor from '../../../../sharedComponents/RawEditor';
+
+describe('EditorProblemView component', () => {
+ test('renders simple view', () => {
+ const wrapper = shallow( );
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find(AnswerWidget).length).toBe(1);
+ });
+
+ test('renders raw editor', () => {
+ const wrapper = shallow( );
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find(AnswerWidget).length).toBe(0);
+ expect(wrapper.find(RawEditor).length).toBe(1);
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/SelectTypeFooter.jsx b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/SelectTypeFooter.jsx
index e7f279fe7..1d0e8f870 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/SelectTypeFooter.jsx
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/SelectTypeFooter.jsx
@@ -17,12 +17,12 @@ export const SelectTypeFooter = ({
onCancel,
selected,
// redux
- setProblemType,
updateField,
+ setBlockTitle,
// injected,
intl,
}) => (
-
+
@@ -35,7 +35,7 @@ export const SelectTypeFooter = ({
@@ -52,8 +52,8 @@ SelectTypeFooter.defaultProps = {
SelectTypeFooter.propTypes = {
onCancel: PropTypes.func.isRequired,
selected: PropTypes.string,
- setProblemType: PropTypes.func.isRequired,
updateField: PropTypes.func.isRequired,
+ setBlockTitle: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
};
@@ -62,8 +62,8 @@ export const mapStateToProps = () => ({
});
export const mapDispatchToProps = {
- setProblemType: actions.problem.setProblemType,
updateField: actions.problem.updateField,
+ setBlockTitle: actions.app.setBlockTitle,
};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SelectTypeFooter));
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/SelectTypeFooter.test.jsx b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/SelectTypeFooter.test.jsx
index 9d22bbf52..fb73c3911 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/SelectTypeFooter.test.jsx
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/SelectTypeFooter.test.jsx
@@ -16,7 +16,7 @@ describe('SelectTypeFooter', () => {
onCancel: jest.fn().mockName('onCancel'),
selected: null,
// redux
- setProblemType: jest.fn().mockName('setProblemType'),
+ updateField: jest.fn().mockName('UpdateField'),
// inject
intl: { formatMessage },
};
@@ -36,7 +36,7 @@ describe('SelectTypeFooter', () => {
.toEqual(expected);
});
test('select behavior is linked to modal onSelect', () => {
- const expected = hooks.onSelect(props.setProblemType, props.selected);
+ const expected = hooks.onSelect(props.selected, props.updateField);
expect(el.find(Button).last().props().onClick)
.toEqual(expected);
});
@@ -48,8 +48,8 @@ describe('SelectTypeFooter', () => {
});
});
describe('mapDispatchToProps', () => {
- test('loads setProblemType from problem.setProblemType actions', () => {
- expect(module.mapDispatchToProps.setProblemType).toEqual(actions.problem.setProblemType);
+ test('loads updateField from problem.updateField actions', () => {
+ expect(module.mapDispatchToProps.updateField).toEqual(actions.problem.updateField);
});
});
});
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/__snapshots__/SelectTypeFooter.test.jsx.snap b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/__snapshots__/SelectTypeFooter.test.jsx.snap
index 760a9d35c..5d136ff50 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/__snapshots__/SelectTypeFooter.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/__snapshots__/SelectTypeFooter.test.jsx.snap
@@ -2,12 +2,7 @@
exports[`SelectTypeFooter snapshot 1`] = `
-
- Select Problem type
-
+
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/index.jsx b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/index.jsx
index b8c63786d..f40e50260 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/index.jsx
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/index.jsx
@@ -1,29 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
-
+import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Icon, ModalDialog, IconButton } from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
import SelectTypeFooter from './SelectTypeFooter';
import * as hooks from '../../../../EditorContainer/hooks';
+import messages from './messages';
export const SelectTypeWrapper = ({
children,
onClose,
selected,
}) => {
- const handleCancelClicked = hooks.handleCancelClicked({ onClose });
+ const handleCancel = hooks.handleCancel({ onClose });
return (
- Select Problem type
+
@@ -33,7 +34,7 @@ export const SelectTypeWrapper = ({
);
@@ -48,4 +49,4 @@ SelectTypeWrapper.propTypes = {
onClose: PropTypes.func,
};
-export default SelectTypeWrapper;
+export default injectIntl(SelectTypeWrapper);
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/index.test.jsx b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/index.test.jsx
index 342d06ced..e5e6c2c49 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/index.test.jsx
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/index.test.jsx
@@ -2,10 +2,10 @@ import React from 'react';
import { shallow } from 'enzyme';
import { IconButton } from '@edx/paragon';
import * as module from '.';
-import { handleCancelClicked } from '../../../../EditorContainer/hooks';
+import { handleCancel } from '../../../../EditorContainer/hooks';
jest.mock('../../../../EditorContainer/hooks', () => ({
- handleCancelClicked: jest.fn().mockName('handleCancelClicked'),
+ handleCancel: jest.fn().mockName('handleCancel'),
}));
describe('SelectTypeWrapper', () => {
@@ -25,7 +25,7 @@ describe('SelectTypeWrapper', () => {
el = shallow(
);
});
test('close behavior is linked to modal onClose', () => {
- const expected = handleCancelClicked({ onClose: props.onClose });
+ const expected = handleCancel({ onClose: props.onClose });
expect(el.find(IconButton).props().onClick)
.toEqual(expected);
});
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/messages.js b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/messages.js
index e4e34350f..5dbb562a8 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/messages.js
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/SelectTypeWrapper/messages.js
@@ -1,4 +1,9 @@
export const messages = {
+ selectTypeTitle: {
+ id: 'authoring.problemEditor.selectType.title',
+ defaultMessage: 'Select problem type',
+ description: 'Title for select problem type modal',
+ },
cancelButtonLabel: {
id: 'authoring.problemeditor.selecttype.cancelButton.label',
defaultMessage: 'Cancel',
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/__snapshots__/index.test.jsx.snap b/src/editors/containers/ProblemEditor/components/SelectTypeModal/__snapshots__/index.test.jsx.snap
index c111eeb42..e3f98ca29 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/__snapshots__/index.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/__snapshots__/index.test.jsx.snap
@@ -1,24 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectTypeModal snapshot 1`] = `
-
-
+
-
-
-
+
-
+
`;
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/AdvanceTypeSelect.jsx b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/AdvanceTypeSelect.jsx
index 2ecfa2568..e61eef086 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/AdvanceTypeSelect.jsx
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/AdvanceTypeSelect.jsx
@@ -8,6 +8,8 @@ import {
Icon,
OverlayTrigger,
Tooltip,
+ Hyperlink,
+ Col,
} from '@edx/paragon';
import { ArrowBack } from '@edx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -22,8 +24,8 @@ export const AdvanceTypeSelect = ({
}) => {
const handleChange = e => { setSelected(e.target.value); };
return (
-
-
+
+
setSelected(ProblemTypeKeys.SINGLESELECT)} />
@@ -56,7 +58,7 @@ export const AdvanceTypeSelect = ({
)}
>
-
+
{intl.formatMessage(messages.problemSupportStatus, { supportStatus: data.status })}
@@ -74,7 +76,13 @@ export const AdvanceTypeSelect = ({
})}
-
+
+
+
+
);
};
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/Preview.jsx b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/Preview.jsx
index b76b9b9b0..a4d04a216 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/Preview.jsx
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/Preview.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Hyperlink, Image } from '@edx/paragon';
+import { Hyperlink, Image, Container } from '@edx/paragon';
import {
FormattedMessage,
injectIntl,
@@ -19,7 +19,7 @@ export const Preview = ({
}
const data = ProblemTypes[problemType];
return (
-
+
{intl.formatMessage(messages.previewTitle, { previewTitle: data.title })}
@@ -38,7 +38,7 @@ export const Preview = ({
>
-
+
);
};
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/ProblemTypeSelect.jsx b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/ProblemTypeSelect.jsx
index e2c8fea06..427967026 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/ProblemTypeSelect.jsx
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/ProblemTypeSelect.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Button, SelectableBox } from '@edx/paragon';
+import { Button, Container, SelectableBox } from '@edx/paragon';
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
import { ProblemTypes, ProblemTypeKeys, AdvanceProblemKeys } from '../../../../../data/constants/problem';
import messages from './messages';
@@ -14,7 +14,7 @@ export const ProblemTypeSelect = ({
const settings = { 'aria-label': 'checkbox', type: 'radio' };
return (
- <>
+
(
key !== 'advanced'
? (
-
+
{ProblemTypes[key].title}
)
@@ -34,7 +39,7 @@ export const ProblemTypeSelect = ({
- >
+
);
};
ProblemTypeSelect.propTypes = {
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/__snapshots__/AdvanceTypeSelect.test.jsx.snap b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/__snapshots__/AdvanceTypeSelect.test.jsx.snap
index 040a7ca10..971ee0749 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/__snapshots__/AdvanceTypeSelect.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/__snapshots__/AdvanceTypeSelect.test.jsx.snap
@@ -1,11 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with default props 1`] = `
-
- Blank advance problem
+ Blank advanced problem
@@ -80,7 +82,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with default
}
placement="right"
>
-
+
Not supported
@@ -133,53 +137,13 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with default
}
placement="right"
>
-
+
Provisional
-
-
- Drag and drop (deprecated version)
-
-
-
-
- {supportStatus, select,
- Provisional {Provisionally supported tools might lack the robustness of functionality
- that your courses require. edX does not have control over the quality of the software,
- or of the content that can be provided using these tools.
-
-
-
- Test these tools thoroughly before using them in your course, especially in graded
- sections. Complete documentstion might not be available for provisionally supported
- tools, or documentation might be available from sources other than edX.}
- Not_supported {Tools with no support are not maintained by edX, and might be deprecated
- in the future. They are not recommened for use in courses due to non-compliance with one
- or more of the base requirements, such as testing, accessibility, internationalization,
- and documentation.}
- other { }
- }
-
-
- }
- placement="right"
- >
-
- Not supported
-
-
-
@@ -217,7 +181,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with default
}
placement="right"
>
-
+
Not supported
@@ -270,22 +236,36 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with default
}
placement="right"
>
-
+
+
+
+
`;
exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problemType is circuitschematic 1`] = `
-
- Blank advance problem
+ Blank advanced problem
@@ -360,7 +340,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -413,53 +395,13 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Provisional
-
-
- Drag and drop (deprecated version)
-
-
-
-
- {supportStatus, select,
- Provisional {Provisionally supported tools might lack the robustness of functionality
- that your courses require. edX does not have control over the quality of the software,
- or of the content that can be provided using these tools.
-
-
-
- Test these tools thoroughly before using them in your course, especially in graded
- sections. Complete documentstion might not be available for provisionally supported
- tools, or documentation might be available from sources other than edX.}
- Not_supported {Tools with no support are not maintained by edX, and might be deprecated
- in the future. They are not recommened for use in courses due to non-compliance with one
- or more of the base requirements, such as testing, accessibility, internationalization,
- and documentation.}
- other { }
- }
-
-
- }
- placement="right"
- >
-
- Not supported
-
-
-
@@ -497,7 +439,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -550,22 +494,36 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
+
+
+
`;
exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problemType is customgrader 1`] = `
-
- Blank advance problem
+ Blank advanced problem
@@ -640,7 +598,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -693,53 +653,13 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Provisional
-
-
- Drag and drop (deprecated version)
-
-
-
-
- {supportStatus, select,
- Provisional {Provisionally supported tools might lack the robustness of functionality
- that your courses require. edX does not have control over the quality of the software,
- or of the content that can be provided using these tools.
-
-
-
- Test these tools thoroughly before using them in your course, especially in graded
- sections. Complete documentstion might not be available for provisionally supported
- tools, or documentation might be available from sources other than edX.}
- Not_supported {Tools with no support are not maintained by edX, and might be deprecated
- in the future. They are not recommened for use in courses due to non-compliance with one
- or more of the base requirements, such as testing, accessibility, internationalization,
- and documentation.}
- other { }
- }
-
-
- }
- placement="right"
- >
-
- Not supported
-
-
-
@@ -777,7 +697,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -830,22 +752,36 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
+
+
+
`;
exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problemType is drag_and_drop 1`] = `
-
- Blank advance problem
+ Blank advanced problem
@@ -920,7 +856,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -973,53 +911,13 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Provisional
-
-
- Drag and drop (deprecated version)
-
-
-
-
- {supportStatus, select,
- Provisional {Provisionally supported tools might lack the robustness of functionality
- that your courses require. edX does not have control over the quality of the software,
- or of the content that can be provided using these tools.
-
-
-
- Test these tools thoroughly before using them in your course, especially in graded
- sections. Complete documentstion might not be available for provisionally supported
- tools, or documentation might be available from sources other than edX.}
- Not_supported {Tools with no support are not maintained by edX, and might be deprecated
- in the future. They are not recommened for use in courses due to non-compliance with one
- or more of the base requirements, such as testing, accessibility, internationalization,
- and documentation.}
- other { }
- }
-
-
- }
- placement="right"
- >
-
- Not supported
-
-
-
@@ -1057,7 +955,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -1110,22 +1010,36 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
+
+
+
`;
exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problemType is formularesponse 1`] = `
-
- Blank advance problem
+ Blank advanced problem
@@ -1200,7 +1114,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -1253,53 +1169,13 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Provisional
-
-
- Drag and drop (deprecated version)
-
-
-
-
- {supportStatus, select,
- Provisional {Provisionally supported tools might lack the robustness of functionality
- that your courses require. edX does not have control over the quality of the software,
- or of the content that can be provided using these tools.
-
-
-
- Test these tools thoroughly before using them in your course, especially in graded
- sections. Complete documentstion might not be available for provisionally supported
- tools, or documentation might be available from sources other than edX.}
- Not_supported {Tools with no support are not maintained by edX, and might be deprecated
- in the future. They are not recommened for use in courses due to non-compliance with one
- or more of the base requirements, such as testing, accessibility, internationalization,
- and documentation.}
- other { }
- }
-
-
- }
- placement="right"
- >
-
- Not supported
-
-
-
@@ -1337,7 +1213,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -1390,22 +1268,36 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
+
+
+
`;
exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problemType is imageresponse 1`] = `
-
- Blank advance problem
+ Blank advanced problem
@@ -1480,7 +1372,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -1533,53 +1427,13 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Provisional
-
-
- Drag and drop (deprecated version)
-
-
-
-
- {supportStatus, select,
- Provisional {Provisionally supported tools might lack the robustness of functionality
- that your courses require. edX does not have control over the quality of the software,
- or of the content that can be provided using these tools.
-
-
-
- Test these tools thoroughly before using them in your course, especially in graded
- sections. Complete documentstion might not be available for provisionally supported
- tools, or documentation might be available from sources other than edX.}
- Not_supported {Tools with no support are not maintained by edX, and might be deprecated
- in the future. They are not recommened for use in courses due to non-compliance with one
- or more of the base requirements, such as testing, accessibility, internationalization,
- and documentation.}
- other { }
- }
-
-
- }
- placement="right"
- >
-
- Not supported
-
-
-
@@ -1617,7 +1471,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -1670,22 +1526,36 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
+
+
+
`;
exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problemType is jsinput_response 1`] = `
-
- Blank advance problem
+ Blank advanced problem
@@ -1760,7 +1630,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -1813,53 +1685,13 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Provisional
-
-
- Drag and drop (deprecated version)
-
-
-
-
- {supportStatus, select,
- Provisional {Provisionally supported tools might lack the robustness of functionality
- that your courses require. edX does not have control over the quality of the software,
- or of the content that can be provided using these tools.
-
-
-
- Test these tools thoroughly before using them in your course, especially in graded
- sections. Complete documentstion might not be available for provisionally supported
- tools, or documentation might be available from sources other than edX.}
- Not_supported {Tools with no support are not maintained by edX, and might be deprecated
- in the future. They are not recommened for use in courses due to non-compliance with one
- or more of the base requirements, such as testing, accessibility, internationalization,
- and documentation.}
- other { }
- }
-
-
- }
- placement="right"
- >
-
- Not supported
-
-
-
@@ -1897,7 +1729,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -1950,22 +1784,36 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
+
+
+
`;
exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problemType is problem_with_hint 1`] = `
-
- Blank advance problem
+ Blank advanced problem
@@ -2040,7 +1888,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -2093,53 +1943,13 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Provisional
-
-
- Drag and drop (deprecated version)
-
-
-
-
- {supportStatus, select,
- Provisional {Provisionally supported tools might lack the robustness of functionality
- that your courses require. edX does not have control over the quality of the software,
- or of the content that can be provided using these tools.
-
-
-
- Test these tools thoroughly before using them in your course, especially in graded
- sections. Complete documentstion might not be available for provisionally supported
- tools, or documentation might be available from sources other than edX.}
- Not_supported {Tools with no support are not maintained by edX, and might be deprecated
- in the future. They are not recommened for use in courses due to non-compliance with one
- or more of the base requirements, such as testing, accessibility, internationalization,
- and documentation.}
- other { }
- }
-
-
- }
- placement="right"
- >
-
- Not supported
-
-
-
@@ -2177,7 +1987,9 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
Not supported
@@ -2230,12 +2042,24 @@ exports[`AdvanceTypeSelect snapshots snapshots: renders as expected with problem
}
placement="right"
>
-
+
+
+
+
`;
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/__snapshots__/Preview.test.jsx.snap b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/__snapshots__/Preview.test.jsx.snap
index 8dd2e23bf..9cc84af14 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/__snapshots__/Preview.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/__snapshots__/Preview.test.jsx.snap
@@ -3,13 +3,19 @@
exports[`Preview snapshots snapshots: renders as expected with default props 1`] = `""`;
exports[`Preview snapshots snapshots: renders as expected with problemType is choiceresponse 1`] = `
-
- Multi Select Problem
+ Multi-select problem
-
+
`;
exports[`Preview snapshots snapshots: renders as expected with problemType is multiplechoiceresponse 1`] = `
-
- Single Select Problem
+ Single select problem
-
+
`;
exports[`Preview snapshots snapshots: renders as expected with problemType is numericalresponse 1`] = `
-
- Numeric Response Problem
+ Numerical input problem
-
+
`;
exports[`Preview snapshots snapshots: renders as expected with problemType is optionresponse 1`] = `
-
- Dropdown Problem
+ Dropdown problem
-
+
`;
exports[`Preview snapshots snapshots: renders as expected with problemType is stringresponse 1`] = `
-
- Text Input Problem
+ Text input problem
-
+
`;
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/__snapshots__/ProblemTypeSelect.test.jsx.snap b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/__snapshots__/ProblemTypeSelect.test.jsx.snap
index bf2f8389e..775965cf3 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/__snapshots__/ProblemTypeSelect.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/__snapshots__/ProblemTypeSelect.test.jsx.snap
@@ -1,7 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProblemTypeSelect snapshot DROPDOWN 1`] = `
-
+
- Single Select Problem
+ Single select
- Multi Select Problem
+ Multi-select
- Dropdown Problem
+ Dropdown
- Numeric Response Problem
+ Numerical input
- Text Input Problem
+ Text input
-
+
`;
exports[`ProblemTypeSelect snapshot MULTISELECT 1`] = `
-
+
- Single Select Problem
+ Single select
- Multi Select Problem
+ Multi-select
- Dropdown Problem
+ Dropdown
- Numeric Response Problem
+ Numerical input
- Text Input Problem
+ Text input
-
+
`;
exports[`ProblemTypeSelect snapshot NUMERIC 1`] = `
-
+
- Single Select Problem
+ Single select
- Multi Select Problem
+ Multi-select
- Dropdown Problem
+ Dropdown
- Numeric Response Problem
+ Numerical input
- Text Input Problem
+ Text input
-
+
`;
exports[`ProblemTypeSelect snapshot SINGLESELECT 1`] = `
-
+
- Single Select Problem
+ Single select
- Multi Select Problem
+ Multi-select
- Dropdown Problem
+ Dropdown
- Numeric Response Problem
+ Numerical input
- Text Input Problem
+ Text input
-
+
`;
exports[`ProblemTypeSelect snapshot TEXTINPUT 1`] = `
-
+
- Single Select Problem
+ Single select
- Multi Select Problem
+ Multi-select
- Dropdown Problem
+ Dropdown
- Numeric Response Problem
+ Numerical input
- Text Input Problem
+ Text input
-
+
`;
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/messages.js b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/messages.js
index dae097bb7..9d220f1a1 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/messages.js
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/content/messages.js
@@ -1,7 +1,7 @@
export const messages = {
advanceProblemButtonLabel: {
id: 'authoring.problemEditor.problemSelect.advanceButton.label',
- defaultMessage: 'Advance problem types',
+ defaultMessage: 'Advanced problem types',
description: 'Button label for advance problem types option',
},
advanceMenuTitle: {
@@ -39,7 +39,7 @@ export const messages = {
},
previewTitle: {
id: 'authoring.problemEditor.preview.title',
- defaultMessage: '{previewTitle}',
+ defaultMessage: '{previewTitle} problem',
description: 'Title for the problem preview column',
},
previewAltText: {
@@ -61,8 +61,13 @@ export const messages = {
},
learnMoreButtonLabel: {
id: 'authoring.problemEditor.learnMoreButtonLabel.label',
- defaultMessage: 'Learn More',
- description: 'Label for Learn More button',
+ defaultMessage: 'Learn more',
+ description: 'Label for Learn more button',
+ },
+ learnMoreAdvancedButtonLabel: {
+ id: 'authoring.problemEditor.advanceProblem.learnMoreButtonLabel.label',
+ defaultMessage: 'Learn more about advanced problem types',
+ description: 'Label for Learn more about advanced problem types button',
},
};
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/hooks.js b/src/editors/containers/ProblemEditor/components/SelectTypeModal/hooks.js
index f1dace285..f6cf10143 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/hooks.js
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/hooks.js
@@ -4,6 +4,7 @@ import {
} from '../../../../data/constants/problem';
import { StrictDict } from '../../../../utils';
import * as module from './hooks';
+import { getDataFromOlx } from '../../../../data/redux/thunkActions/problem';
export const state = StrictDict({
selected: (val) => useState(val),
@@ -17,11 +18,16 @@ export const selectHooks = () => {
};
};
-export const onSelect = (setProblemType, selected, updateField) => () => {
+export const onSelect = ({ selected, updateField, setBlockTitle }) => () => {
if (Object.values(AdvanceProblemKeys).includes(selected)) {
- updateField({ rawOLX: AdvanceProblems[selected].template });
+ updateField({ problemType: ProblemTypeKeys.ADVANCED, rawOLX: AdvanceProblems[selected].template });
+ setBlockTitle(AdvanceProblems[selected].title);
+ } else {
+ const newOLX = ProblemTypes[selected].template;
+ const { settings, ...newState } = getDataFromOlx({ rawOLX: newOLX, rawSettings: {} });
+ updateField({ ...newState });
+ setBlockTitle(ProblemTypes[selected].title);
}
- setProblemType({ selected });
};
export const useArrowNav = (selected, setSelected) => {
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/hooks.test.js b/src/editors/containers/ProblemEditor/components/SelectTypeModal/hooks.test.js
index 50829221e..b44cc4eef 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/hooks.test.js
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/hooks.test.js
@@ -2,7 +2,8 @@
import React from 'react';
import { MockUseState } from '../../../../../testUtils';
import * as module from './hooks';
-import { AdvanceProblems, ProblemTypeKeys } from '../../../../data/constants/problem';
+import { AdvanceProblems, ProblemTypeKeys, ProblemTypes } from '../../../../data/constants/problem';
+import { OLXParser } from '../../data/OLXParser';
jest.mock('react', () => ({
...jest.requireActual('react'),
@@ -11,11 +12,11 @@ jest.mock('react', () => ({
}));
const state = new MockUseState(module);
-const mockSetProblemType = jest.fn().mockName('setProblemType');
const mockUpdateField = jest.fn().mockName('updateField');
-const mockSelected = 'vAl';
-const mockAdvancedSelected = 'blankadvanced';
+const mockSelected = 'multiplechoiceresponse';
+const mockAdvancedSelected = 'circuitschematic';
const mockSetSelected = jest.fn().mockName('setSelected');
+const mocksetBlockTitle = jest.fn().mockName('setBlockTitle');
let hook;
@@ -45,14 +46,26 @@ describe('SelectTypeModal hooks', () => {
describe('onSelect', () => {
test('updateField is called with selected templated if selected is an Advanced Problem', () => {
- module.onSelect(mockSetProblemType, mockAdvancedSelected, mockUpdateField)();
+ module.onSelect({
+ selected: mockAdvancedSelected,
+ updateField: mockUpdateField,
+ setBlockTitle: mocksetBlockTitle,
+ })();
expect(mockUpdateField).toHaveBeenCalledWith({
+ problemType: ProblemTypeKeys.ADVANCED,
rawOLX: AdvanceProblems[mockAdvancedSelected].template,
});
+ expect(mocksetBlockTitle).toHaveBeenCalledWith(AdvanceProblems[mockAdvancedSelected].title);
});
- test('setProblemType is called with selected', () => {
- module.onSelect(mockSetProblemType, mockSelected, mockUpdateField)();
- expect(mockSetProblemType).toHaveBeenCalledWith({ selected: mockSelected });
+ test('updateField is called with selected on visual propblems', () => {
+ module.onSelect({ selected: mockSelected, updateField: mockUpdateField, setBlockTitle: mocksetBlockTitle })();
+ const testOlXParser = new OLXParser(ProblemTypes[mockSelected].template);
+ const { settings, ...testState } = testOlXParser.getParsedOLXData();
+ expect(mockUpdateField).toHaveBeenCalledWith({
+ ...testState,
+ rawOLX: ProblemTypes[mockSelected].template,
+ });
+ expect(mocksetBlockTitle).toHaveBeenCalledWith(ProblemTypes[mockSelected].title);
});
});
diff --git a/src/editors/containers/ProblemEditor/components/SelectTypeModal/index.jsx b/src/editors/containers/ProblemEditor/components/SelectTypeModal/index.jsx
index 49d2ca7b7..0c02e0647 100644
--- a/src/editors/containers/ProblemEditor/components/SelectTypeModal/index.jsx
+++ b/src/editors/containers/ProblemEditor/components/SelectTypeModal/index.jsx
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Col, Row } from '@edx/paragon';
+import { Row, Stack } from '@edx/paragon';
import ProblemTypeSelect from './content/ProblemTypeSelect';
import Preview from './content/Preview';
import AdvanceTypeSelect from './content/AdvanceTypeSelect';
@@ -17,16 +17,12 @@ export const SelectTypeModal = ({
return (
-
+
{(!Object.values(AdvanceProblemKeys).includes(selected)) ? (
- <>
-
-
-
-
-
-
- >
+
+
+
+
) : }
diff --git a/src/editors/containers/ProblemEditor/data/OLXParser.js b/src/editors/containers/ProblemEditor/data/OLXParser.js
index 18126a039..865a14999 100644
--- a/src/editors/containers/ProblemEditor/data/OLXParser.js
+++ b/src/editors/containers/ProblemEditor/data/OLXParser.js
@@ -280,7 +280,7 @@ export class OLXParser {
the parsed OLX.
*/
const tagMap = {
- label: 'bold',
+ label: 'strong',
description: 'em',
};
@@ -331,10 +331,16 @@ export class OLXParser {
getProblemType() {
const problemKeys = Object.keys(this.problem);
const intersectedProblems = _.intersection(Object.values(ProblemTypeKeys), problemKeys);
-
if (intersectedProblems.length === 0) {
- return null;
+ // a blank problem is a problem which contains only `
` as it's olx.
+ // blank problems are not given types, so that a type may be selected.
+ if (problemKeys.length === 1 && problemKeys[0] === '#text' && this.problem[problemKeys[0]] === '') {
+ return null;
+ }
+ // if we have no matching problem type, the problem is advanced.
+ return ProblemTypeKeys.ADVANCED;
}
+ // make sure compound problems are treated as advanced
if (intersectedProblems.length > 1) {
return ProblemTypeKeys.ADVANCED;
}
@@ -369,7 +375,10 @@ export class OLXParser {
answersObject = this.parseMultipleChoiceAnswers(ProblemTypeKeys.SINGLESELECT, 'choicegroup', 'choice');
break;
case ProblemTypeKeys.ADVANCED:
- break;
+ return {
+ problemType,
+ settings: {},
+ };
default:
// if problem is unset, return null
return {};
diff --git a/src/editors/containers/ProblemEditor/data/OLXParser.test.js b/src/editors/containers/ProblemEditor/data/OLXParser.test.js
index 3ae946638..5823e4b6e 100644
--- a/src/editors/containers/ProblemEditor/data/OLXParser.test.js
+++ b/src/editors/containers/ProblemEditor/data/OLXParser.test.js
@@ -7,6 +7,8 @@ import {
textInputWithFeedbackAndHintsOLX,
mutlipleChoiceWithFeedbackAndHintsOLX,
textInputWithFeedbackAndHintsOLXWithMultipleAnswers,
+ advancedProblemOlX,
+ blankProblemOLX,
} from './mockData/olxTestData';
import { ProblemTypeKeys } from '../../../data/constants/problem';
@@ -36,6 +38,16 @@ describe('Check OLXParser problem type', () => {
const problemType = olxparser.getProblemType();
expect(problemType).toBe(ProblemTypeKeys.TEXTINPUT);
});
+ test('Test Advanced Problem Type', () => {
+ const olxparser = new OLXParser(advancedProblemOlX.rawOLX);
+ const problemType = olxparser.getProblemType();
+ expect(problemType).toBe(ProblemTypeKeys.ADVANCED);
+ });
+ test('Test Blank Problem Type', () => {
+ const olxparser = new OLXParser(blankProblemOLX.rawOLX);
+ const problemType = olxparser.getProblemType();
+ expect(problemType).toBe(null);
+ });
});
describe('Check OLXParser hints', () => {
diff --git a/src/editors/containers/ProblemEditor/data/SettingsParser.js b/src/editors/containers/ProblemEditor/data/SettingsParser.js
index 8c038efe7..1de0708a0 100644
--- a/src/editors/containers/ProblemEditor/data/SettingsParser.js
+++ b/src/editors/containers/ProblemEditor/data/SettingsParser.js
@@ -16,10 +16,7 @@ export const parseScoringSettings = (metadata) => {
let attempts = popuplateItem({}, 'max_attempts', 'number', metadata);
if (!_.isEmpty(attempts)) {
- let unlimited = true;
- if (attempts.number > 0) {
- unlimited = false;
- }
+ const unlimited = _.isNaN(attempts.number);
attempts = { ...attempts, unlimited };
scoring = { ...scoring, attempts };
}
diff --git a/src/editors/containers/ProblemEditor/data/SettingsParser.test.js b/src/editors/containers/ProblemEditor/data/SettingsParser.test.js
index cf3ddc763..871262aa7 100644
--- a/src/editors/containers/ProblemEditor/data/SettingsParser.test.js
+++ b/src/editors/containers/ProblemEditor/data/SettingsParser.test.js
@@ -4,7 +4,7 @@ import {
dropdownWithFeedbackHints,
numericWithHints,
textInputWithHints,
- sigleSelectWithHints,
+ singleSelectWithHints,
} from './mockData/problemTestData';
describe('Test Settings to State Parser', () => {
@@ -38,7 +38,7 @@ describe('Test Settings to State Parser', () => {
});
test('Test score settings missing', () => {
- const settings = parseSettings(sigleSelectWithHints.metadata);
+ const settings = parseSettings(singleSelectWithHints.metadata);
expect(settings.scoring).toBeUndefined();
});
diff --git a/src/editors/containers/ProblemEditor/data/mockData/olxTestData.js b/src/editors/containers/ProblemEditor/data/mockData/olxTestData.js
index cc9e2de9d..187ce6514 100644
--- a/src/editors/containers/ProblemEditor/data/mockData/olxTestData.js
+++ b/src/editors/containers/ProblemEditor/data/mockData/olxTestData.js
@@ -82,11 +82,11 @@ export const checkboxesOLXWithFeedbackAndHintsOLX = {
},
],
},
- question: '
You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
+ question: '
You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
buildOLX: `
You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.
- Add the question text, or prompt, here. This text is required.
+ Add the question text, or prompt, here. This text is required.
You can add an optional tip or note related to the prompt like this.
@@ -160,11 +160,11 @@ export const dropdownOLXWithFeedbackAndHintsOLX = {
},
],
},
- question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
+ question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
buildOLX: `
You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.
- Add the question text, or prompt, here. This text is required.
+ Add the question text, or prompt, here. This text is required.
You can add an optional tip or note related to the prompt like this.
@@ -233,11 +233,11 @@ export const mutlipleChoiceWithFeedbackAndHintsOLX = {
},
],
},
- question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for multiple choice with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
+ question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for multiple choice with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
buildOLX: `
You can use this template as a guide to the simple editor markdown and OLX markup to use for multiple choice with hints and feedback problems. Edit this component to replace this template with your own assessment.
- Add the question text, or prompt, here. This text is required.
+ Add the question text, or prompt, here. This text is required.
You can add an optional tip or note related to the prompt like this.
@@ -299,10 +299,10 @@ export const numericInputWithFeedbackAndHintsOLX = {
},
],
},
- question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
+ question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
buildOLX: `
You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.
- Add the question text, or prompt, here. This text is required.
+ Add the question text, or prompt, here. This text is required.
You can add an optional tip or note related to the prompt like this.
@@ -373,11 +373,11 @@ export const textInputWithFeedbackAndHintsOLX = {
},
},
},
- question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
+ question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
buildOLX: `
You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.
- Add the question text, or prompt, here. This text is required.
+ Add the question text, or prompt, here. This text is required.
You can add an optional tip or note related to the prompt like this.
You can specify optional feedback like this, which appears after this answer is submitted.
@@ -452,11 +452,11 @@ export const textInputWithFeedbackAndHintsOLXWithMultipleAnswers = {
},
},
},
- question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
+ question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
buildOLX: `
You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.
- Add the question text, or prompt, here. This text is required.
+ Add the question text, or prompt, here. This text is required.
You can add an optional tip or note related to the prompt like this.
You can specify optional feedback like this, which appears after this answer is submitted.
@@ -531,10 +531,10 @@ export const numericInputWithFeedbackAndHintsOLXException = {
},
],
},
- question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
+ question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.
Add the question text, or prompt, here. This text is required. You can add an optional tip or note related to the prompt like this. ',
buildOLX: `
You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.
- Add the question text, or prompt, here. This text is required.
+ Add the question text, or prompt, here. This text is required.
You can add an optional tip or note related to the prompt like this.
@@ -553,3 +553,17 @@ export const numericInputWithFeedbackAndHintsOLXException = {
`,
};
+export const advancedProblemOlX = {
+ rawOLX: `
+
+ You can use this template as a guide to the OLX markup to use for math expression problems. Edit this component to replace the example with your own assessment.
+ Add the question text, or prompt, here. This text is required. Example: Write an expression for the product of R_1, R_2, and the inverse of R_3.
+ You can add an optional tip or note related to the prompt like this. Example: To test this example, the correct answer is R_1*R_2/R_3
+
+
+
+ `,
+};
+export const blankProblemOLX = {
+ rawOLX: ' ',
+};
diff --git a/src/editors/containers/ProblemEditor/data/mockData/problemTestData.js b/src/editors/containers/ProblemEditor/data/mockData/problemTestData.js
index fbd7a6a11..46e2ebf43 100644
--- a/src/editors/containers/ProblemEditor/data/mockData/problemTestData.js
+++ b/src/editors/containers/ProblemEditor/data/mockData/problemTestData.js
@@ -217,7 +217,7 @@ export const numericWithHints = {
scoring: {
weight: 2.5,
attempts: {
- unlimited: true,
+ unlimited: false,
number: 0,
},
},
@@ -288,7 +288,7 @@ export const textInputWithHints = {
scoring: {
weight: 2.5,
attempts: {
- unlimited: true,
+ unlimited: false,
number: 0,
},
},
@@ -315,7 +315,7 @@ not=optional incorrect answer such as a frequent misconception {{You can specify
},
};
-export const sigleSelectWithHints = {
+export const singleSelectWithHints = {
state: {
rawOLX: '\nYou can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.
\n\nAdd the question text, or prompt, here. This text is required. \nYou can add an optional tip or note related to the prompt like this. \n\n \n a correct answer selected: You can specify optional feedback that appears after the learner selects and submits this answer. }, { unselected: You can specify optional feedback that appears after the learner clears and submits this answer. \n an incorrect answer \n an incorrect answer selected: You can specify optional feedback for none, all, or a subset of the answers. }, { unselected: You can specify optional feedback for selected answers, cleared answers, or both. \n an incorrect answer again \n \n \n\n \n You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted. \n You can specify optional feedback for one, several, or all answer combinations. \n \n \n\n\n\n You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button. \n If you add more than one hint, a different hint appears each time learners select the hint button. \n \n ',
problemType: 'SINGLESELECT',
@@ -362,7 +362,7 @@ export const sigleSelectWithHints = {
weight: 0,
attempts: {
unlimited: true,
- number: 0,
+ number: null,
},
},
timeBetween: 0,
diff --git a/src/editors/containers/ProblemEditor/hooks.js b/src/editors/containers/ProblemEditor/hooks.js
index 78bf14ca6..63ab97f6b 100644
--- a/src/editors/containers/ProblemEditor/hooks.js
+++ b/src/editors/containers/ProblemEditor/hooks.js
@@ -1,7 +1,7 @@
import {
useRef, useCallback, useState, useEffect,
} from 'react';
-import { ProblemTypeKeys } from '../../data/constants/problem';
+import tinyMCEStyles from '../../data/constants/tinyMCEStyles';
import { StrictDict } from '../../utils';
import * as module from './hooks';
@@ -19,6 +19,13 @@ export const problemEditorConfig = ({
setEditorRef(editor);
},
initialValue: question || '',
+ init: {
+ skin: false,
+ content_css: false,
+ content_style: tinyMCEStyles,
+ menubar: false,
+ branding: false,
+ },
onFocusOut: () => {
const content = editorRef.current.getContent();
updateQuestion(content);
@@ -34,8 +41,3 @@ export const prepareEditorRef = () => {
useEffect(() => setRefReady(true), [setRefReady]);
return { editorRef, refReady, setEditorRef };
};
-
-export const initializeAnswerContainer = (problemType) => {
- const hasSingleAnswer = problemType === ProblemTypeKeys.DROPDOWN || problemType === ProblemTypeKeys.SINGLESELECT;
- return { hasSingleAnswer };
-};
diff --git a/src/editors/containers/ProblemEditor/index.jsx b/src/editors/containers/ProblemEditor/index.jsx
index 542fcabaa..b96a400c7 100644
--- a/src/editors/containers/ProblemEditor/index.jsx
+++ b/src/editors/containers/ProblemEditor/index.jsx
@@ -17,8 +17,6 @@ export const ProblemEditor = ({
blockValue,
initializeProblemEditor,
}) => {
- React.useEffect(() => initializeProblemEditor(blockValue), [blockValue]);
- // TODO: INTL MSG, Add LOAD FAILED ERROR using BLOCKFAILED
if (!blockFinished || !studioViewFinished) {
return (
@@ -31,6 +29,8 @@ export const ProblemEditor = ({
);
}
// once data is loaded, init store
+ React.useEffect(() => initializeProblemEditor(blockValue), [blockValue]);
+ // TODO: INTL MSG, Add LOAD FAILED ERROR using BLOCKFAILED
if (problemType === null) {
return ( );
diff --git a/src/editors/containers/TextEditor/__snapshots__/index.test.jsx.snap b/src/editors/containers/TextEditor/__snapshots__/index.test.jsx.snap
index 085a3400e..b7b79cd3c 100644
--- a/src/editors/containers/TextEditor/__snapshots__/index.test.jsx.snap
+++ b/src/editors/containers/TextEditor/__snapshots__/index.test.jsx.snap
@@ -236,6 +236,13 @@ exports[`TextEditor snapshots loaded, raw editor 1`] = `
/>
diff --git a/src/editors/containers/TextEditor/components/CodeEditor/index.jsx b/src/editors/containers/TextEditor/components/CodeEditor/index.jsx
deleted file mode 100644
index f4a465bd2..000000000
--- a/src/editors/containers/TextEditor/components/CodeEditor/index.jsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import React, { useEffect, useRef } from 'react';
-import PropTypes from 'prop-types';
-
-import {
- Button,
-} from '@edx/paragon';
-
-import { basicSetup } from 'codemirror';
-import { EditorState } from '@codemirror/state';
-import { EditorView } from '@codemirror/view';
-import { html } from '@codemirror/lang-html';
-
-import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import alphanumericMap from './constants';
-import * as module from './index';
-import messages from './messages';
-import './index.scss';
-
-export const hooks = {
-
- state: {
- showBtnEscapeHTML: (val) => React.useState(val),
- },
-
- prepareShowBtnEscapeHTML: () => {
- const [visibility, setVisibility] = hooks.state.showBtnEscapeHTML(true);
- const hide = () => setVisibility(false);
- return { showBtnEscapeHTML: visibility, hideBtn: hide };
- },
-
- createCodeMirrorDomNode: ({ ref, initialText, upstreamRef }) => {
- useEffect(() => {
- const cleanText = hooks.cleanHTML({ initialText });
- const state = EditorState.create({
- doc: cleanText,
- extensions: [basicSetup, html(), EditorView.lineWrapping],
- });
- const view = new EditorView({ state, parent: ref.current });
- // eslint-disable-next-line no-param-reassign
- upstreamRef.current = view;
- view.focus();
- return () => {
- // called on cleanup
- view.destroy();
- };
- }, []);
- },
- cleanHTML: ({ initialText }) => {
- const translateRegex = new RegExp(`&(${Object.keys(alphanumericMap).join('|')});`, 'g');
- const translator = ($0, $1) => alphanumericMap[$1];
- return initialText.replace(translateRegex, translator);
- },
- escapeHTMLSpecialChars: ({ ref, hideBtn }) => {
- const text = ref.current.state.doc.toString(); let
- pos = 0;
- const changes = [];
- Object.keys(alphanumericMap).forEach(
- (escapedKeyword) => {
- // eslint-disable-next-line no-cond-assign
- for (let next; (next = text.indexOf(alphanumericMap[escapedKeyword], pos)) > -1;) {
- changes.push({ from: next, to: next + 1, insert: `&${escapedKeyword};` });
- pos = next + 1;
- }
- },
- );
- ref.current.dispatch({ changes });
- hideBtn();
- },
-
-};
-
-export const CodeEditor = ({
- innerRef,
- value,
- // injected
- intl,
-}) => {
- const DOMref = useRef();
- const btnRef = useRef();
- module.hooks.createCodeMirrorDomNode({ ref: DOMref, initialText: value, upstreamRef: innerRef });
- const { showBtnEscapeHTML, hideBtn } = module.hooks.prepareShowBtnEscapeHTML();
-
- return (
-
-
- {showBtnEscapeHTML && (
-
module.hooks.escapeHTMLSpecialChars({ ref: innerRef, hideBtn })}
- >
-
-
- )}
-
- );
-};
-
-CodeEditor.propTypes = {
- innerRef: PropTypes.oneOfType([
- PropTypes.func,
- PropTypes.shape({ current: PropTypes.any }),
- ]).isRequired,
- value: PropTypes.string.isRequired,
- intl: intlShape.isRequired,
-};
-
-export default injectIntl(CodeEditor);
diff --git a/src/editors/containers/TextEditor/components/RawEditor/index.jsx b/src/editors/containers/TextEditor/components/RawEditor/index.jsx
deleted file mode 100644
index 57a3f7515..000000000
--- a/src/editors/containers/TextEditor/components/RawEditor/index.jsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { Alert } from '@edx/paragon';
-
-import CodeEditor from '../CodeEditor';
-
-export const RawEditor = ({
- editorRef,
- text,
-}) => (
-
-
- You are using the raw HTML editor.
-
- { text && text.data.data ? (
-
- ) : null}
-
-
-);
-RawEditor.defaultProps = {
- editorRef: null,
- text: null,
-};
-RawEditor.propTypes = {
- editorRef: PropTypes.oneOfType([
- PropTypes.func,
- PropTypes.shape({ current: PropTypes.any }),
- ]),
- text: PropTypes.shape({
- data: PropTypes.shape({ data: PropTypes.string }),
- }),
-};
-
-export default RawEditor;
diff --git a/src/editors/containers/TextEditor/components/SourceCodeModal/index.jsx b/src/editors/containers/TextEditor/components/SourceCodeModal/index.jsx
index 590e10c1e..3241fd9bb 100644
--- a/src/editors/containers/TextEditor/components/SourceCodeModal/index.jsx
+++ b/src/editors/containers/TextEditor/components/SourceCodeModal/index.jsx
@@ -12,7 +12,7 @@ import messages from './messages';
import hooks from './hooks';
import BaseModal from '../BaseModal';
-import CodeEditor from '../CodeEditor';
+import CodeEditor from '../../../../sharedComponents/CodeEditor';
export const SourceCodeModal = ({
isOpen,
diff --git a/src/editors/containers/TextEditor/index.jsx b/src/editors/containers/TextEditor/index.jsx
index 450071b7d..b3f3d16c4 100644
--- a/src/editors/containers/TextEditor/index.jsx
+++ b/src/editors/containers/TextEditor/index.jsx
@@ -32,7 +32,7 @@ import { RequestKeys } from '../../data/constants/requests';
import EditorContainer from '../EditorContainer';
import ImageUploadModal from './components/ImageUploadModal';
import SourceCodeModal from './components/SourceCodeModal';
-import RawEditor from './components/RawEditor';
+import RawEditor from '../../sharedComponents/RawEditor';
import * as hooks from './hooks';
import messages from './messages';
@@ -64,7 +64,7 @@ export const TextEditor = ({
return (
);
}
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget.jsx
deleted file mode 100644
index 779266af6..000000000
--- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget.jsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import React from 'react';
-import { useDispatch } from 'react-redux';
-
-import { Col, Form } from '@edx/paragon';
-
-import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
-import { keyStore } from '../../../../../utils';
-import CollapsibleFormWidget from './CollapsibleFormWidget';
-import hooks from './hooks';
-import { durationFromValue } from './duration';
-import messages from './messages';
-
-/**
- * Collapsible Form widget controlling video start and end times
- * Also displays the total run time of the video.
- */
-export const DurationWidget = ({
- // injected
- intl,
-}) => {
- const dispatch = useDispatch();
- const { duration } = hooks.widgetValues({
- dispatch,
- fields: { [hooks.selectorKeys.duration]: hooks.durationWidget },
- });
- const timeKeys = keyStore(duration.formValue);
-
- const getTotalLabel = (startTime, stopTime, subtitle) => {
- if (!stopTime) {
- if (!startTime) {
- return intl.formatMessage(messages.fullVideoLength);
- }
- if (subtitle) {
- return intl.formatMessage(messages.startsAt, { startTime: durationFromValue(startTime) });
- }
- return null;
- }
- const total = stopTime - (startTime || 0);
- return intl.formatMessage(messages.total, { total: durationFromValue(total) });
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {getTotalLabel(duration.formValue.startTime, duration.formValue.stopTime)}
-
-
- );
-};
-
-DurationWidget.propTypes = {
- // injected
- intl: intlShape.isRequired,
-};
-
-export default injectIntl(DurationWidget);
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget.test.jsx
deleted file mode 100644
index 68a9f8e57..000000000
--- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget.test.jsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import { formatMessage } from '../../../../../../testUtils';
-import { DurationWidget } from './DurationWidget';
-
-describe('DurationWidget', () => {
- const props = {
- isError: false,
- subtitle: 'SuBTItle',
- title: 'tiTLE',
- // inject
- intl: { formatMessage },
- };
- describe('render', () => {
- test('snapshots: renders as expected with default props', () => {
- expect(
- shallow( ),
- ).toMatchSnapshot();
- });
- });
-});
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/__snapshots__/DurationWidget.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/__snapshots__/index.test.jsx.snap
similarity index 92%
rename from src/editors/containers/VideoEditor/components/VideoSettingsModal/components/__snapshots__/DurationWidget.test.jsx.snap
rename to src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/__snapshots__/index.test.jsx.snap
index 53fdb4043..22bb051c5 100644
--- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/__snapshots__/DurationWidget.test.jsx.snap
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/__snapshots__/index.test.jsx.snap
@@ -3,7 +3,7 @@
exports[`DurationWidget render snapshots: renders as expected with default props 1`] = `
-
+
-
+
- Full video length
+ Total: 00:00:00
`;
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.js
new file mode 100644
index 000000000..9a9960307
--- /dev/null
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.js
@@ -0,0 +1,216 @@
+import { useEffect, useState } from 'react';
+
+import messages from '../messages';
+
+import * as module from './hooks';
+
+const durationMatcher = /^(\d{0,2}):?(\d{0,2})?:?(\d{0,2})?$/i;
+
+export const durationWidget = ({ duration, updateField }) => {
+ const setDuration = (val) => updateField({ duration: val });
+ const initialState = module.durationString(duration);
+ const [unsavedDuration, setUnsavedDuration] = useState(initialState);
+
+ useEffect(() => {
+ setUnsavedDuration(module.durationString(duration));
+ }, [duration]);
+
+ return {
+ unsavedDuration,
+ onBlur: (index) => (
+ (e) => module.updateDuration({
+ duration,
+ setDuration,
+ unsavedDuration,
+ setUnsavedDuration,
+ index,
+ inputString: e.target.value,
+ })
+ ),
+ onChange: (index) => (
+ (e) => setUnsavedDuration(module.onDurationChange(unsavedDuration, index, e.target.value))
+ ),
+ onKeyDown: (index) => (
+ (e) => setUnsavedDuration(module.onDurationKeyDown(unsavedDuration, index, e))
+ ),
+ getTotalLabel: ({ durationString, subtitle, intl }) => {
+ if (!durationString.stopTime) {
+ if (!durationString.startTime) {
+ return intl.formatMessage(messages.fullVideoLength);
+ }
+ if (subtitle) {
+ return intl.formatMessage(
+ messages.startsAt,
+ { startTime: module.durationStringFromValue(durationString.startTime) },
+ );
+ }
+ return null;
+ }
+ const total = durationString.stopTime - (durationString.startTime || 0);
+ return intl.formatMessage(messages.total, { total: module.durationStringFromValue(total) });
+ },
+ };
+};
+
+/**
+ * durationString(duration)
+ * Returns the display value for embedded start and stop times
+ * @param {object} duration - object containing startTime and stopTime millisecond values
+ * @return {object} - start and stop time from incoming object mapped to duration strings.
+ */
+export const durationString = (duration) => ({
+ startTime: module.durationStringFromValue(duration.startTime),
+ stopTime: module.durationStringFromValue(duration.stopTime),
+});
+
+/**
+ * durationStringFromValue(value)
+ * Returns a duration string in 'hh:mm:ss' format from the given ms value
+ * @param {number} value - duration (in milliseconds)
+ * @return {string} - duration in 'hh:mm:ss' format
+ */
+export const durationStringFromValue = (value) => {
+ // return 'why';
+ if (!value || typeof value !== 'number' || value <= 0) {
+ return '00:00:00';
+ }
+ const seconds = Math.floor((value / 1000) % 60);
+ const minutes = Math.floor((value / 60000) % 60);
+ const hours = Math.floor((value / 3600000) % 60);
+ const zeroPad = (num) => String(num).padStart(2, '0');
+ return [hours, minutes, seconds].map(zeroPad).join(':');
+};
+
+/**
+ * updateDuration({ duration, unsavedDuration, setUnsavedDuration, setDuration })
+ * Returns a memoized callback based on inputs that updates unsavedDuration value and form value
+ * if the new string is valid (duration stores a number, unsavedDuration stores a string).
+ * If the duration string is invalid, resets the unsavedDuration value to the latest good value.
+ * @param {object} duration - redux-stored durations in milliseconds
+ * @param {object} unsavedDuration - hook-stored duration in 'hh:mm:ss' format
+ * @param {func} setDuration - set form value
+ * @param {func} setUnsavedDuration - set unsavedDuration object
+ * @param {string} index - startTime or stopTime
+ * @param {string} inputString - string value of user input for either the start or stop time fields
+ * @return {func} - callback to update duration unsavedDurationly and in redux
+ * updateDuration(args)(index, durationString)
+ */
+export const updateDuration = ({
+ duration,
+ unsavedDuration,
+ setDuration,
+ setUnsavedDuration,
+ index,
+ inputString,
+}) => {
+ let newDurationString = inputString;
+ let newValue = module.valueFromDuration(newDurationString);
+ // maxTime is 23:59:59 or 86399 seconds
+ if (newValue > 86399000) {
+ newValue = 86399000;
+ }
+ // stopTime must be at least 1 second, if not zero
+ if (index === 'stopTime' && newValue > 0 && newValue < 1000) {
+ newValue = 1000;
+ }
+ // stopTime must be at least 1 second after startTime, except 0 means no custom stopTime
+ if (index === 'stopTime' && newValue > 0 && newValue < (duration.startTime + 1000)) {
+ newValue = duration.startTime + 1000;
+ }
+ // startTime must be at least 1 second before stopTime, except when stopTime is less than a second
+ // (stopTime should only be less than a second if it's zero, but we're being paranoid)
+ if (index === 'startTime' && duration.stopTime >= 1000 && newValue > (duration.stopTime - 1000)) {
+ newValue = duration.stopTime - 1000;
+ }
+ newDurationString = module.durationStringFromValue(newValue);
+ setUnsavedDuration({ ...unsavedDuration, [index]: newDurationString });
+ setDuration({ ...duration, [index]: newValue });
+};
+
+/**
+ * onDurationChange(duration)
+ * Returns a new duration value based on onChange event
+ * @param {object} duration - object containing startTime and stopTime millisecond values
+ * @param {string} index - 'startTime or 'stopTime'
+ * @param {string} val - duration in 'hh:mm:ss' format
+ * @return {object} duration - object containing startTime and stopTime millisecond values
+ */
+export const onDurationChange = (duration, index, val) => {
+ const match = val.trim().match(durationMatcher);
+ if (!match) {
+ return duration;
+ }
+
+ const caretPos = document.activeElement.selectionStart;
+ let newDuration = val;
+ if (caretPos === newDuration.length && (newDuration.length === 2 || newDuration.length === 5)) {
+ newDuration += ':';
+ }
+
+ return {
+ ...duration,
+ [index]: newDuration,
+ };
+};
+
+/**
+ * onDurationKeyDown(duration)
+ * Returns a new duration value based on onKeyDown event
+ * @param {object} duration - object containing startTime and stopTime millisecond values
+ * @param {string} index - 'startTime or 'stopTime'
+ * @param {Event} event - event from onKeyDown
+ * @return {object} duration - object containing startTime and stopTime millisecond values
+ */
+export const onDurationKeyDown = (duration, index, event) => {
+ const caretPos = document.activeElement.selectionStart;
+ let newDuration = duration[index];
+
+ switch (event.key) {
+ case 'Enter':
+ document.activeElement.blur();
+ break;
+ case 'Backspace':
+ if (caretPos === newDuration.length && newDuration.slice(-1) === ':') {
+ newDuration = newDuration.slice(0, -1);
+ }
+ break;
+ default:
+ break;
+ }
+
+ return {
+ ...duration,
+ [index]: newDuration,
+ };
+};
+
+/**
+ * valueFromDuration(duration)
+ * Returns a millisecond duration value from the given 'hh:mm:ss' format string
+ * @param {string} duration - duration in 'hh:mm:ss' format
+ * @return {number} - duration in milliseconds. Returns null if duration is invalid.
+ */
+export const valueFromDuration = (duration) => {
+ let matches = duration.trim().match(durationMatcher);
+ if (!matches) {
+ return 0;
+ }
+ matches = matches.slice(1).filter(v => v !== undefined);
+ if (matches.length < 3) {
+ for (let i = 0; i <= 3 - matches.length; i++) {
+ matches.unshift(0);
+ }
+ }
+ const [hours, minutes, seconds] = matches.map(x => parseInt(x, 10) || 0);
+ return ((hours * 60 + minutes) * 60 + seconds) * 1000;
+};
+
+export default {
+ durationWidget,
+ durationString,
+ durationStringFromValue,
+ updateDuration,
+ onDurationChange,
+ onDurationKeyDown,
+ valueFromDuration,
+};
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.test.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.test.js
new file mode 100644
index 000000000..782558829
--- /dev/null
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.test.js
@@ -0,0 +1,308 @@
+import React from 'react';
+
+import * as hooks from './hooks';
+import messages from '../messages';
+
+jest.mock('react', () => {
+ const updateState = jest.fn();
+ return {
+ ...jest.requireActual('react'),
+ updateState,
+ useState: jest.fn(val => ([{ state: val }, (newVal) => updateState({ val, newVal })])),
+ useCallback: (cb, prereqs) => ({ useCallback: { cb, prereqs } }),
+ useEffect: jest.fn(),
+ };
+});
+
+let testMethod;
+const intl = {
+ formatMessage: jest.fn(val => val),
+};
+
+const [h, m, s] = [3600000, 60000, 1000];
+const durationPairs = [
+ [0, '00:00:00'],
+ [5000, '00:00:05'],
+ [60000, '00:01:00'],
+ [3600000, '01:00:00'],
+ [3665000, '01:01:05'],
+];
+const trickyDurations = [
+ ['10:00', 600000],
+ ['23', 23000],
+ ['99:99:99', 99 * (m + s + h)],
+ ['23:42:81', 23 * h + 42 * m + 81 * s],
+];
+let props;
+const e = {
+ target: {
+ value: 'vAlUE',
+ },
+};
+
+describe('Video Settings DurationWidget hooks', () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+ describe('durationWidget', () => {
+ const duration = {
+ startTime: '00:00:00',
+ stopTime: '00:00:10',
+ };
+ const updateField = jest.fn();
+ beforeEach(() => {
+ testMethod = hooks.durationWidget({ duration, updateField });
+ });
+ describe('behavior', () => {
+ describe('initialization', () => {
+ test('useEffect memoized on duration', () => {
+ hooks.durationWidget({ duration, updateField });
+ expect(React.useEffect).toHaveBeenCalled();
+ expect(React.useEffect.mock.calls[0][1]).toEqual([duration]);
+ });
+ test('calls setUnsavedDuration with durationString(duration)', () => {
+ hooks.durationWidget({ duration, updateField });
+ React.useEffect.mock.calls[0][0]();
+ expect(React.updateState).toHaveBeenCalled();
+ });
+ });
+ });
+ describe('returns', () => {
+ testMethod = hooks.durationWidget({ duration, updateField });
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+ describe('unsavedDuration, defaulted to duration', () => {
+ expect(testMethod.unsavedDuration).toEqual({ state: hooks.durationString(duration) });
+ });
+ describe('onBlur, calls updateDuration', () => {
+ jest.spyOn(hooks, 'updateDuration').mockImplementation(jest.fn());
+ testMethod.onBlur('IndEX')(e);
+ expect(hooks.updateDuration).toHaveBeenCalled();
+ });
+ describe('onChange', () => {
+ testMethod.onChange('IndEX')(e);
+ expect(React.updateState).toHaveBeenCalled();
+ });
+ describe('onKeyDown', () => {
+ testMethod.onKeyDown('iNDex')(e);
+ expect(React.updateState).toHaveBeenCalled();
+ });
+ describe('getTotalLabel', () => {
+ describe('returns fullVideoLength message when no startTime and no stop Time are set', () => {
+ expect(testMethod.getTotalLabel({
+ durationString: {},
+ subtitle: true,
+ intl,
+ })).toEqual(messages.fullVideoLength);
+ });
+ describe('returns startAt message for subtitle when only startTime is set', () => {
+ expect(testMethod.getTotalLabel({
+ durationString: {
+ startTime: '00:00:00',
+ },
+ subtitle: true,
+ intl,
+ })).toEqual(messages.startsAt);
+ });
+ describe('returns null for widget (not subtitle) when there only startTime is set', () => {
+ expect(testMethod.getTotalLabel({
+ durationString: {
+ startTime: '00:00:00',
+ },
+ subtitle: false,
+ intl,
+ })).toEqual(null);
+ });
+ describe('returns total message when at least stopTime is set', () => {
+ expect(testMethod.getTotalLabel({
+ durationString: {
+ startTime: '00:00:00',
+ stopTime: '00:00:10',
+ },
+ subtitle: true,
+ intl,
+ })).toEqual(messages.total);
+ });
+ });
+ });
+ });
+ describe('durationString', () => {
+ beforeEach(() => {
+ testMethod = hooks.durationString;
+ });
+ it('returns an object that maps durationStringFromValue to the passed duration keys', () => {
+ const testDuration = { startTime: 1000, stopTime: 2000, other: 'values' };
+ expect(testMethod(testDuration)).toEqual({
+ startTime: '00:00:01',
+ stopTime: '00:00:02',
+ });
+ });
+ });
+ describe('durationStringFromValue', () => {
+ beforeEach(() => {
+ testMethod = hooks.durationStringFromValue;
+ });
+ it('returns 00:00:00 if given a bad value', () => {
+ const badChecks = ['a', '', null, -1];
+ badChecks.forEach(val => expect(testMethod(val)).toEqual('00:00:00'));
+ });
+ it('translates milliseconds into hh:mm:ss format', () => {
+ durationPairs.forEach(
+ ([val, dur]) => expect(testMethod(val)).toEqual(dur),
+ );
+ });
+ });
+ describe('updateDuration', () => {
+ const testValidIndex = 'startTime';
+ const testStopIndex = 'stopTime';
+ const testValidDuration = '00:00:00';
+ const testValidValue = 0;
+ const testInvalidDuration = 'abc';
+ beforeEach(() => {
+ testMethod = hooks.updateDuration;
+ props = {
+ duration: { startTime: 23000, stopTime: 600000 },
+ unsavedDuration: { startTime: '00:00:23', stopTime: '00:10:00' },
+ setDuration: jest.fn(),
+ setUnsavedDuration: jest.fn(),
+ index: 'startTime',
+ inputString: '01:23:45',
+ };
+ });
+ describe('if the passed durationString is valid', () => {
+ it('sets the unsavedDuration to updated strings and duration to new timestamp value', () => {
+ testMethod({
+ ...props,
+ index: testValidIndex,
+ inputString: testValidDuration,
+ });
+ expect(props.setUnsavedDuration).toHaveBeenCalledWith({
+ ...props.unsavedDuration,
+ [testValidIndex]: testValidDuration,
+ });
+ expect(props.setDuration).toHaveBeenCalledWith({
+ ...props.duration,
+ [testValidIndex]: testValidValue,
+ });
+ });
+ });
+ describe('if the passed durationString is not valid', () => {
+ it('updates unsavedDuration values to 0 (the default)', () => {
+ testMethod({
+ ...props,
+ index: testValidIndex,
+ inputString: testInvalidDuration,
+ });
+ expect(props.setUnsavedDuration).toHaveBeenCalledWith({
+ ...props.unsavedDuration,
+ [testValidIndex]: testValidDuration,
+ });
+ expect(props.setDuration).toHaveBeenCalledWith({
+ ...props.duration,
+ [testValidIndex]: testValidValue,
+ });
+ });
+ });
+ describe('if the passed startTime is after (or equal to) the stored non-zero stopTime', () => {
+ it('updates unsavedDuration startTime values to 1 second before stopTime', () => {
+ testMethod({
+ ...props,
+ index: testValidIndex,
+ inputString: '00:10:00',
+ });
+ expect(props.setUnsavedDuration).toHaveBeenCalledWith({
+ ...props.unsavedDuration,
+ [testValidIndex]: '00:09:59',
+ });
+ expect(props.setDuration).toHaveBeenCalledWith({
+ ...props.duration,
+ [testValidIndex]: 599000,
+ });
+ });
+ });
+ describe('if the passed stopTime is before (or equal to) the stored startTime', () => {
+ it('updates unsavedDuration stopTime values to 1 second after startTime', () => {
+ testMethod({
+ ...props,
+ index: testStopIndex,
+ inputString: '00:00:22',
+ });
+ expect(props.setUnsavedDuration).toHaveBeenCalledWith({
+ ...props.unsavedDuration,
+ [testStopIndex]: '00:00:24',
+ });
+ expect(props.setDuration).toHaveBeenCalledWith({
+ ...props.duration,
+ [testStopIndex]: 24000,
+ });
+ });
+ });
+ });
+ describe('onDurationChange', () => {
+ beforeEach(() => {
+ props = {
+ duration: { startTime: '00:00:00' },
+ index: 'startTime',
+ val: 'vAl',
+ };
+ testMethod = hooks.onDurationChange;
+ });
+ it('returns duration with no change if duration[index] does not match HH:MM:SS format', () => {
+ const badChecks = [
+ 'ab:cd:ef', // non-digit characters
+ '12:34:567', // characters past max length
+ ];
+ badChecks.forEach(val => expect(testMethod(props.duration, props.index, val)).toEqual(props.duration));
+ });
+ it('returns duration with an added \':\' after 2 characters when caret is at end', () => {
+ props.duration = { startTime: '0' };
+ props.val = '00';
+ document.activeElement.selectionStart = props.duration[props.index].length + 1;
+ expect(testMethod(props.duration, props.index, props.val)).toEqual({ startTime: '00:' });
+ });
+ it('returns duration with an added \':\' after 5 characters when caret is at end', () => {
+ props.duration = { startTime: '00:0' };
+ props.val = '00:00';
+ document.activeElement.selectionStart = props.duration[props.index].length + 1;
+ expect(testMethod(props.duration, props.index, props.val)).toEqual({ startTime: '00:00:' });
+ });
+ });
+ describe('onDurationKeyDown', () => {
+ beforeEach(() => {
+ props = {
+ duration: { startTime: '00:00:00' },
+ index: 'startTime',
+ event: 'eVeNt',
+ };
+ testMethod = hooks.onDurationKeyDown;
+ });
+ it('enter event: calls blur()', () => {
+ props.event = { key: 'Enter' };
+ const blurSpy = jest.spyOn(document.activeElement, 'blur');
+ testMethod(props.duration, props.index, props.event);
+ expect(blurSpy).toHaveBeenCalled();
+ });
+ it('backspace event: returns duration with deleted end character when that character is \':\' and caret is at end', () => {
+ props.duration = { startTime: '00:' };
+ props.event = { key: 'Backspace' };
+ document.activeElement.selectionStart = props.duration[props.index].length;
+ expect(testMethod(props.duration, props.index, props.event)).toEqual({ startTime: '00' });
+ });
+ });
+ describe('valueFromDuration', () => {
+ beforeEach(() => {
+ testMethod = hooks.valueFromDuration;
+ });
+ it('returns 0 if given a bad duration string', () => {
+ const badChecks = ['a', '00:00:1f', '0adg:00:04'];
+ badChecks.forEach(dur => expect(testMethod(dur)).toEqual(0));
+ });
+ it('returns simple durations', () => {
+ durationPairs.forEach(([val, dur]) => expect(testMethod(dur)).toEqual(val));
+ });
+ it('returns tricky durations, prepending zeros and expanding out sections', () => {
+ trickyDurations.forEach(([dur, val]) => expect(testMethod(dur)).toEqual(val));
+ });
+ });
+});
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.jsx
new file mode 100644
index 000000000..7b2b1144c
--- /dev/null
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.jsx
@@ -0,0 +1,99 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+
+import { Col, Form } from '@edx/paragon';
+import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
+
+import { actions, selectors } from '../../../../../../data/redux';
+import { keyStore } from '../../../../../../utils';
+import CollapsibleFormWidget from '../CollapsibleFormWidget';
+import hooks from './hooks';
+import messages from '../messages';
+
+/**
+ * Collapsible Form widget controlling video start and end times
+ * Also displays the total run time of the video.
+ */
+export const DurationWidget = ({
+ // redux
+ duration,
+ updateField,
+ // injected
+ intl,
+}) => {
+ const {
+ unsavedDuration,
+ onBlur,
+ onChange,
+ onKeyDown,
+ getTotalLabel,
+ } = hooks.durationWidget({ duration, updateField });
+
+ const timeKeys = keyStore(duration);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {getTotalLabel({
+ durationString: duration,
+ subtitle: false,
+ intl,
+ })}
+
+
+ );
+};
+
+DurationWidget.propTypes = {
+ // redux
+ duration: PropTypes.objectOf(PropTypes.number).isRequired,
+ updateField: PropTypes.func.isRequired,
+ // injected
+ intl: intlShape.isRequired,
+};
+
+export const mapStateToProps = (state) => ({
+ duration: selectors.video.duration(state),
+});
+
+export const mapDispatchToProps = {
+ updateField: actions.video.updateField,
+};
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(DurationWidget));
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.test.jsx
new file mode 100644
index 000000000..ddb9e8df9
--- /dev/null
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.test.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { actions, selectors } from '../../../../../../data/redux';
+import { formatMessage } from '../../../../../../../testUtils';
+import { DurationWidget, mapStateToProps, mapDispatchToProps } from '.';
+
+jest.mock('../../../../../../data/redux', () => ({
+ actions: {
+ video: {
+ updateField: jest.fn().mockName('actions.video.updateField'),
+ },
+ },
+ selectors: {
+ video: {
+ duration: jest.fn(state => ({ duration: state })),
+ },
+ },
+}));
+
+describe('DurationWidget', () => {
+ const props = {
+ duration: {
+ startTime: '00:00:00',
+ stopTime: '00:00:10',
+ },
+ updateField: jest.fn().mockName('updateField'),
+ // inject
+ intl: { formatMessage },
+ };
+ describe('render', () => {
+ test('snapshots: renders as expected with default props', () => {
+ expect(
+ shallow( ),
+ ).toMatchSnapshot();
+ });
+ });
+ describe('mapStateToProps', () => {
+ const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
+ test('duration from video.duration', () => {
+ expect(
+ mapStateToProps(testState).duration,
+ ).toEqual(selectors.video.duration(testState));
+ });
+ });
+ describe('mapDispatchToProps', () => {
+ test('updateField from actions.video.updateField', () => {
+ expect(mapDispatchToProps.updateField).toEqual(actions.video.updateField);
+ });
+ });
+});
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/hooks.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/hooks.jsx
index d68d1cde2..90a8fce8c 100644
--- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/hooks.jsx
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/hooks.jsx
@@ -1,70 +1,19 @@
import { actions } from '../../../../../../data/redux';
-import { isEdxVideo } from '../../../../../../data/services/cms/api';
-/**
- * updateVideoId({ dispatch })({e, source})
- * updateVideoId takes the current onBlur event, the current object of the video
- * source, and dispatch method, and updates the redux value for all the fields to
- * their default values except videoId, fallbackVideos, and handouts.
- * @param {event} e - object for onBlur event
- * @param {func} dispatch - redux dispatch method
- * @param {object} source - object for the Video Source field functions and values
- */
-export const updateVideoId = ({ dispatch }) => ({ e, source }) => {
- if (source.local !== '') {
- if (source.formValue !== e.target.value) {
- source.onBlur(e);
- let videoId;
- let videoSource;
- if (isEdxVideo(source.local)) {
- videoId = source.local;
- videoSource = '';
- } else if (source.local.includes('youtu.be') || source.local.includes('youtube')) {
- videoId = '';
- videoSource = source.local;
- } else {
- videoId = '';
- videoSource = source.local;
- }
- dispatch(actions.video.updateField({
- videoId,
- videoSource,
- allowVideoDownloads: false,
- thumbnail: null,
- transcripts: [],
- allowTranscriptDownloads: false,
- showTranscriptByDefault: false,
- duration: {
- startTime: '00:00:00',
- stopTime: '00:00:00',
- total: '00:00:00',
- },
- licenseType: null,
- }));
- }
- }
+export const sourceHooks = ({ dispatch }) => ({
+ updateVideoURL: (e) => dispatch(actions.video.updateField({ videoSource: e.target.value })),
+ updateVideoId: (e) => dispatch(actions.video.updateField({ videoId: e.target.value })),
+});
+
+export const fallbackHooks = ({ fallbackVideos, dispatch }) => ({
+ addFallbackVideo: () => dispatch(actions.video.updateField({ fallbackVideos: [...fallbackVideos, ''] })),
+ deleteFallbackVideo: (videoUrl) => {
+ const updatedFallbackVideos = fallbackVideos.splice(fallbackVideos.indexOf(videoUrl), 1);
+ dispatch(actions.video.updateField({ fallbackVideos: updatedFallbackVideos }));
+ },
+});
+
+export default {
+ sourceHooks,
+ fallbackHooks,
};
-
-/**
- * deleteFallbackVideo({ fallbackVideos, dispatch })(videoUrl)
- * deleteFallbackVideo takes the current array of fallback videos, string of
- * deleted video URL and dispatch method, and updates the redux value for
- * fallbackVideos.
- * @param {array} fallbackVideos - array of current fallback videos
- * @param {func} dispatch - redux dispatch method
- * @param {string} videoUrl - string of the video URL for the fallabck video that needs to be deleted
- */
-export const deleteFallbackVideo = ({ fallbackVideos, dispatch }) => (videoUrl) => {
- const updatedFallbackVideos = [];
- let firstOccurence = true;
- fallbackVideos.forEach(item => {
- if (item === videoUrl && firstOccurence) {
- firstOccurence = false;
- } else {
- updatedFallbackVideos.push(item);
- }
- });
- dispatch(actions.video.updateField({ fallbackVideos: updatedFallbackVideos }));
-};
-
-export default { deleteFallbackVideo };
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/hooks.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/hooks.test.jsx
index 4f21f183e..34181e523 100644
--- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/hooks.test.jsx
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/hooks.test.jsx
@@ -6,6 +6,7 @@ jest.mock('react-redux', () => {
const dispatchFn = jest.fn();
return {
...jest.requireActual('react-redux'),
+ useSelector: jest.fn(),
dispatch: dispatchFn,
useDispatch: jest.fn(() => dispatchFn),
};
@@ -20,78 +21,61 @@ jest.mock('../../../../../../data/redux', () => ({
}));
describe('VideoEditorHandout hooks', () => {
- describe('updateVideoId', () => {
- const sourceEdxVideo = {
- onBlur: jest.fn(),
- local: '06b15030-7df0-4e70-b979-326e02dbcbe0',
- };
- const sourceYouTube = {
- onBlur: jest.fn(),
- local: 'youtu.be',
- };
- const sourceHtml5Source = {
- onBlur: jest.fn(),
- local: 'sOMEranDomfILe.mp4',
- };
- const mockState = {
- videoId: '',
- videoSource: '',
- allowVideoDownloads: false,
- thumbnail: null,
- transcripts: [],
- allowTranscriptDownloads: false,
- showTranscriptByDefault: false,
- duration: {
- startTime: '00:00:00',
- stopTime: '00:00:00',
- total: '00:00:00',
- },
- licenseType: null,
- };
- it('returns dispatches updateField action with default state and edxVideo Id', () => {
- hooks.updateVideoId({ dispatch })({ e: { target: { value: sourceEdxVideo.local } }, source: sourceEdxVideo });
- expect(dispatch).toHaveBeenCalledWith(
- actions.video.updateField({
- ...mockState,
- videoId: sourceEdxVideo.local,
- }),
- );
+ let hook;
+
+ describe('sourceHooks', () => {
+ const e = { target: { value: 'soMEvALuE' } };
+ beforeEach(() => {
+ hook = hooks.sourceHooks({ dispatch });
});
- it('returns dispatches updateField action with default state and YouTube video', () => {
- hooks.updateVideoId({ dispatch })({
- e: { target: { value: sourceYouTube.local } },
- source: sourceYouTube,
+ describe('updateVideoURL', () => {
+ it('dispatches updateField action with new videoSource', () => {
+ hook.updateVideoURL(e);
+ expect(dispatch).toHaveBeenCalledWith(
+ actions.video.updateField({
+ videoSource: e.target.value,
+ }),
+ );
});
- expect(dispatch).toHaveBeenCalledWith(
- actions.video.updateField({
- ...mockState,
- }),
- );
});
- it('returns dispatches updateField action with default state and html5source video', () => {
- hooks.updateVideoId({ dispatch })({
- e: { target: { value: sourceHtml5Source.local } },
- source: sourceHtml5Source,
+ describe('updateVideoId', () => {
+ it('dispatches updateField action with new videoId', () => {
+ hook.updateVideoId(e);
+ expect(dispatch).toHaveBeenCalledWith(
+ actions.video.updateField({
+ videoId: e.target.value,
+ }),
+ );
});
- expect(dispatch).toHaveBeenCalledWith(
- actions.video.updateField({
- ...mockState,
- }),
- );
});
});
- describe('deleteFallbackVideo', () => {
+ describe('fallbackHooks', () => {
const videoUrl = 'sOmERAndoMuRl1';
const fallbackVideos = ['sOmERAndoMuRl1', 'sOmERAndoMuRl2', 'sOmERAndoMuRl1', ''];
- const updatedFallbackVideos = ['sOmERAndoMuRl2', 'sOmERAndoMuRl1', ''];
- it('returns dispatches updateField action with updatedFallbackVideos', () => {
- hooks.deleteFallbackVideo({ fallbackVideos, dispatch })(videoUrl);
- expect(dispatch).toHaveBeenCalledWith(
- actions.video.updateField({
- fallbackVideos: updatedFallbackVideos,
- }),
- );
+ beforeEach(() => {
+ hook = hooks.fallbackHooks({ fallbackVideos, dispatch });
+ });
+ describe('addFallbackVideo', () => {
+ it('dispatches updateField action with updated array appended by a new empty element', () => {
+ hook.addFallbackVideo();
+ expect(dispatch).toHaveBeenCalledWith(
+ actions.video.updateField({
+ fallbackVideos: [...fallbackVideos, ''],
+ }),
+ );
+ });
+ });
+ describe('deleteFallbackVideo', () => {
+ it('dispatches updateField action with updated array with videoUrl removed', () => {
+ const updatedFallbackVideos = ['sOmERAndoMuRl2', 'sOmERAndoMuRl1', ''];
+ hook.deleteFallbackVideo(videoUrl);
+ expect(dispatch).toHaveBeenCalledWith(
+ actions.video.updateField({
+ fallbackVideos: updatedFallbackVideos,
+ }),
+ );
+ });
});
});
});
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/index.jsx
index 7dc2762d5..915f4281e 100644
--- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/index.jsx
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/index.jsx
@@ -1,6 +1,5 @@
import React from 'react';
-import { connect, useDispatch } from 'react-redux';
-import PropTypes from 'prop-types';
+import { useDispatch } from 'react-redux';
import {
Form,
@@ -20,9 +19,8 @@ import {
} from '@edx/frontend-platform/i18n';
import * as widgetHooks from '../hooks';
-import * as module from './hooks';
+import * as hooks from './hooks';
import messages from './messages';
-import { actions } from '../../../../../../data/redux';
import CollapsibleFormWidget from '../CollapsibleFormWidget';
@@ -32,8 +30,6 @@ import CollapsibleFormWidget from '../CollapsibleFormWidget';
export const VideoSourceWidget = ({
// injected
intl,
- // redux
- updateField,
}) => {
const dispatch = useDispatch();
const {
@@ -50,8 +46,11 @@ export const VideoSourceWidget = ({
[widgetHooks.selectorKeys.allowVideoDownloads]: widgetHooks.genericWidget,
},
});
- const deleteFallbackVideo = module.deleteFallbackVideo({ fallbackVideos: fallbackVideos.formValue, dispatch });
- const updateVideoId = module.updateVideoId({ dispatch });
+ const { updateVideoId, updateVideoURL } = hooks.sourceHooks({ dispatch });
+ const {
+ addFallbackVideo,
+ deleteFallbackVideo,
+ } = hooks.fallbackHooks({ fallbackVideos: fallbackVideos.formValue, dispatch });
return (
updateVideoId({ e, source: videoId })}
+ onBlur={updateVideoId}
value={videoId.local}
/>
@@ -72,7 +71,7 @@ export const VideoSourceWidget = ({
updateVideoId({ e, source })}
+ onBlur={updateVideoURL}
value={source.local}
/>
@@ -134,7 +133,7 @@ export const VideoSourceWidget = ({
size="sm"
iconBefore={Add}
variant="link"
- onClick={() => updateField({ fallbackVideos: [...fallbackVideos.formValue, ''] })}
+ onClick={() => addFallbackVideo()}
>
@@ -144,12 +143,6 @@ export const VideoSourceWidget = ({
VideoSourceWidget.propTypes = {
// injected
intl: intlShape.isRequired,
- // redux
- updateField: PropTypes.func.isRequired,
};
-export const mapStateToProps = () => ({});
-export const mapDispatchToProps = (dispatch) => ({
- updateField: (stateUpdate) => dispatch(actions.video.updateField(stateUpdate)),
-});
-export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(VideoSourceWidget));
+export default injectIntl(VideoSourceWidget);
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/index.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/index.test.jsx
index fcc48d57a..88d5825db 100644
--- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/index.test.jsx
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/index.test.jsx
@@ -1,25 +1,19 @@
import React from 'react';
+import { dispatch } from 'react-redux';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../../testUtils';
-import { actions } from '../../../../../../data/redux';
-import { VideoSourceWidget, mapDispatchToProps } from '.';
+import { VideoSourceWidget } from '.';
+import * as hooks from './hooks';
-jest.mock('../../../../../../data/redux', () => ({
- actions: {
- video: {
- updateField: jest.fn().mockName('actions.video.updateField'),
- },
- },
- selectors: {
- video: {
- videoSource: jest.fn(state => ({ videoSource: state })),
- videoId: jest.fn(state => ({ videoId: state })),
- fallbackVideos: jest.fn(state => ({ fallbackVideos: state })),
- allowVideoDownloads: jest.fn(state => ({ allowVideoDownloads: state })),
- },
- },
-}));
+jest.mock('react-redux', () => {
+ const dispatchFn = jest.fn();
+ return {
+ ...jest.requireActual('react-redux'),
+ dispatch: dispatchFn,
+ useDispatch: jest.fn(() => dispatchFn),
+ };
+});
jest.mock('../hooks', () => ({
selectorKeys: ['soMEkEy'],
@@ -36,14 +30,21 @@ jest.mock('../hooks', () => ({
}),
}));
+jest.mock('./hooks', () => ({
+ sourceHooks: jest.fn().mockReturnValue({
+ updateVideoId: (args) => ({ updateVideoId: args }),
+ updateVideoURL: (args) => ({ updateVideoURL: args }),
+ }),
+ fallbackHooks: jest.fn().mockReturnValue({
+ addFallbackVideo: jest.fn().mockName('addFallbackVideo'),
+ deleteFallbackVideo: jest.fn().mockName('deleteFallbackVideo'),
+ }),
+}));
+
describe('VideoSourceWidget', () => {
const props = {
- error: {},
- title: 'tiTLE',
// inject
intl: { formatMessage },
- // redux
- updateField: jest.fn().mockName('args.updateField'),
};
describe('snapshots', () => {
@@ -53,10 +54,27 @@ describe('VideoSourceWidget', () => {
).toMatchSnapshot();
});
});
- describe('mapDispatchToProps', () => {
- const dispatch = jest.fn();
- test('updateField from actions.video.updateField', () => {
- expect(mapDispatchToProps.updateField).toEqual(dispatch(actions.video.updateField));
+
+ describe('behavior inspection', () => {
+ let el;
+ let hook;
+ beforeEach(() => {
+ el = shallow( );
+ hook = hooks.sourceHooks({ dispatch });
+ });
+ test('updateVideoId is tied to id field onBlur', () => {
+ const expected = hook.updateVideoId;
+ expect(el
+ // eslint-disable-next-line
+ .children().at(0).children().at(0).children().at(0)
+ .props().onBlur).toEqual(expected);
+ });
+ test('updateVideoURL is tied to url field onBlur', () => {
+ const expected = hook.updateVideoURL;
+ expect(el
+ // eslint-disable-next-line
+ .children().at(0).children().at(0).children().at(2)
+ .props().onBlur).toEqual(expected);
});
});
});
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/duration.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/duration.js
deleted file mode 100644
index 3f7c65176..000000000
--- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/duration.js
+++ /dev/null
@@ -1,155 +0,0 @@
-import { useCallback } from 'react';
-import * as module from './duration';
-
-const durationMatcher = /^(\d{0,2}):?(\d{0,2})?:?(\d{0,2})?$/i;
-
-/**
- * onDurationChange(duration)
- * Returns a new duration value based on onChange event
- * @param {object} duration - object containing startTime and stopTime millisecond values
- * @param {string} index - 'startTime or 'stopTime'
- * @param {string} val - duration in 'hh:mm:ss' format
- * @return {object} duration - object containing startTime and stopTime millisecond values
- */
-export const onDurationChange = (duration, index, val) => {
- const match = val.trim().match(durationMatcher);
- if (!match) {
- return duration;
- }
-
- const caretPos = document.activeElement.selectionStart;
- let newDuration = val;
- if (caretPos === newDuration.length && (newDuration.length === 2 || newDuration.length === 5)) {
- newDuration += ':';
- }
-
- return {
- ...duration,
- [index]: newDuration,
- };
-};
-
-/**
- * onDurationKeyDown(duration)
- * Returns a new duration value based on onKeyDown event
- * @param {object} duration - object containing startTime and stopTime millisecond values
- * @param {string} index - 'startTime or 'stopTime'
- * @param {Event} event - event from onKeyDown
- * @return {object} duration - object containing startTime and stopTime millisecond values
- */
-export const onDurationKeyDown = (duration, index, event) => {
- const caretPos = document.activeElement.selectionStart;
- let newDuration = duration[index];
-
- switch (event.key) {
- case 'Enter':
- document.activeElement.blur();
- break;
- case 'Backspace':
- if (caretPos === newDuration.length && newDuration.slice(-1) === ':') {
- newDuration = newDuration.slice(0, -1);
- }
- break;
- default:
- break;
- }
-
- return {
- ...duration,
- [index]: newDuration,
- };
-};
-
-/**
- * durationFromValue(value)
- * Returns a duration string in 'hh:mm:ss' format from the given ms value
- * @param {number} value - duration (in milliseconds)
- * @return {string} - duration in 'hh:mm:ss' format
- */
-export const durationFromValue = (value) => {
- if (!value || typeof value !== 'number' || value <= 0) {
- return '00:00:00';
- }
- const seconds = Math.floor((value / 1000) % 60);
- const minutes = Math.floor((value / 60000) % 60);
- const hours = Math.floor((value / 3600000) % 60);
- const zeroPad = (num) => String(num).padStart(2, '0');
- return [hours, minutes, seconds].map(zeroPad).join(':');
-};
-
-/**
- * valueFromDuration(duration)
- * Returns a millisecond duration value from the given 'hh:mm:ss' format string
- * @param {string} duration - duration in 'hh:mm:ss' format
- * @return {number} - duration in milliseconds. Returns null if duration is invalid.
- */
-export const valueFromDuration = (duration) => {
- let matches = duration.trim().match(durationMatcher);
- if (!matches) {
- return 0;
- }
- matches = matches.slice(1).filter(v => v !== undefined);
- if (matches.length < 3) {
- for (let i = 0; i <= 3 - matches.length; i++) {
- matches.unshift(0);
- }
- }
- const [hours, minutes, seconds] = matches.map(x => parseInt(x, 10) || 0);
- return ((hours * 60 + minutes) * 60 + seconds) * 1000;
-};
-
-/**
- * durationValue(duration)
- * Returns the display value for embedded start and stop times
- * @param {object} duration - object containing startTime and stopTime millisecond values
- * @return {object} - start and stop time from incoming object mapped to duration strings.
- */
-export const durationValue = (duration) => ({
- startTime: module.durationFromValue(duration.startTime),
- stopTime: module.durationFromValue(duration.stopTime),
-});
-
-/**
- * updateDuration({ formValue, local, setLocal, setFormValue })
- * Returns a memoized callback based on inputs that updates local value and form value
- * if the new string is valid (formValue stores a number, local stores a string).
- * If the duration string is invalid, resets the local value to the latest good value.
- * @param {object} formValue - redux-stored durations in milliseconds
- * @param {object} local - hook-stored duration in 'hh:mm:ss' format
- * @param {func} setFormValue - set form value
- * @param {func} setLocal - set local object
- * @return {func} - callback to update duration locally and in redux
- * updateDuration(args)(index, durationString)
- */
-export const updateDuration = ({
- formValue,
- local,
- setFormValue,
- setLocal,
-}) => useCallback(
- (index, durationString) => {
- let newDurationString = durationString;
- let newValue = module.valueFromDuration(newDurationString);
- // maxTime is 23:59:59 or 86399 seconds
- if (newValue > 86399000) {
- newValue = 86399000;
- }
- // stopTime must be at least 1 second, if not zero
- if (index === 'stopTime' && newValue > 0 && newValue < 1000) {
- newValue = 1000;
- }
- // stopTime must be at least 1 second after startTime, except 0 means no custom stopTime
- if (index === 'stopTime' && newValue > 0 && newValue < (formValue.startTime + 1000)) {
- newValue = formValue.startTime + 1000;
- }
- // startTime must be at least 1 second before stopTime, except when stopTime is less than a second
- // (stopTime should only be less than a second if it's zero, but we're being paranoid)
- if (index === 'startTime' && formValue.stopTime >= 1000 && newValue > (formValue.stopTime - 1000)) {
- newValue = formValue.stopTime - 1000;
- }
- newDurationString = module.durationFromValue(newValue);
- setLocal({ ...local, [index]: newDurationString });
- setFormValue({ ...formValue, [index]: newValue });
- },
- [formValue, local, setLocal, setFormValue],
-);
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/duration.test.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/duration.test.js
deleted file mode 100644
index 533289ec3..000000000
--- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/duration.test.js
+++ /dev/null
@@ -1,216 +0,0 @@
-import { keyStore } from '../../../../../utils';
-import * as duration from './duration';
-
-jest.mock('react', () => ({
- ...jest.requireActual('react'),
- useCallback: (cb, prereqs) => ({ useCallback: { cb, prereqs } }),
-}));
-
-let hook;
-const durationKeys = keyStore(duration);
-const [h, m, s] = [3600000, 60000, 1000];
-const durationPairs = [
- [0, '00:00:00'],
- [5000, '00:00:05'],
- [60000, '00:01:00'],
- [3600000, '01:00:00'],
- [3665000, '01:01:05'],
-];
-const trickyDurations = [
- ['10:00', 600000],
- ['23', 23000],
- ['99:99:99', 99 * (m + s + h)],
- ['23:42:81', 23 * h + 42 * m + 81 * s],
-];
-let spies = {};
-let props;
-let cb;
-let prereqs;
-let oldDuration;
-describe('Video Settings Modal duration hooks', () => {
- beforeEach(() => {
- spies = {};
- oldDuration = { ...jest.requireActual('./duration') };
- });
- afterEach(() => {
- Object.keys(oldDuration).forEach((key) => {
- duration[key] = oldDuration[key];
- });
- Object.keys(spies).forEach((key) => {
- spies[key].mockRestore();
- });
- });
-
- describe('onDurationChange', () => {
- beforeEach(() => {
- props = {
- duration: { startTime: '00:00:00' },
- index: 'startTime',
- val: 'vAl',
- };
- hook = duration.onDurationChange;
- });
- it('returns duration with no change if duration[index] does not match HH:MM:SS format', () => {
- const badChecks = [
- 'ab:cd:ef', // non-digit characters
- '12:34:567', // characters past max length
- ];
- badChecks.forEach(val => expect(hook(props.duration, props.index, val)).toEqual(props.duration));
- });
- it('returns duration with an added \':\' after 2 characters when caret is at end', () => {
- props.duration = { startTime: '0' };
- props.val = '00';
- document.activeElement.selectionStart = props.duration[props.index].length + 1;
- expect(hook(props.duration, props.index, props.val)).toEqual({ startTime: '00:' });
- });
- it('returns duration with an added \':\' after 5 characters when caret is at end', () => {
- props.duration = { startTime: '00:0' };
- props.val = '00:00';
- document.activeElement.selectionStart = props.duration[props.index].length + 1;
- expect(hook(props.duration, props.index, props.val)).toEqual({ startTime: '00:00:' });
- });
- });
- describe('onDurationKeyDown', () => {
- beforeEach(() => {
- props = {
- duration: { startTime: '00:00:00' },
- index: 'startTime',
- event: 'eVeNt',
- };
- hook = duration.onDurationKeyDown;
- });
- it('enter event: calls blur()', () => {
- props.event = { key: 'Enter' };
- const blurSpy = jest.spyOn(document.activeElement, 'blur');
- hook(props.duration, props.index, props.event);
- expect(blurSpy).toHaveBeenCalled();
- });
- it('backspace event: returns duration with deleted end character when that character is \':\' and caret is at end', () => {
- props.duration = { startTime: '00:' };
- props.event = { key: 'Backspace' };
- document.activeElement.selectionStart = props.duration[props.index].length;
- expect(hook(props.duration, props.index, props.event)).toEqual({ startTime: '00' });
- });
- });
- describe('durationFromValue', () => {
- beforeEach(() => {
- hook = duration.durationFromValue;
- });
- it('returns 00:00:00 if given a bad value', () => {
- const badChecks = ['a', '', null, -1];
- badChecks.forEach(val => expect(hook(val)).toEqual('00:00:00'));
- });
- it('translates milliseconds into hh:mm:ss format', () => {
- durationPairs.forEach(
- ([val, dur]) => expect(hook(val)).toEqual(dur),
- );
- });
- });
- describe('valueFromDuration', () => {
- beforeEach(() => {
- hook = duration.valueFromDuration;
- });
- it('returns 0 if given a bad duration string', () => {
- const badChecks = ['a', '00:00:1f', '0adg:00:04'];
- badChecks.forEach(dur => expect(hook(dur)).toEqual(0));
- });
- it('returns simple durations', () => {
- durationPairs.forEach(([val, dur]) => expect(hook(dur)).toEqual(val));
- });
- it('returns tricky durations, prepending zeros and expanding out sections', () => {
- trickyDurations.forEach(([dur, val]) => expect(hook(dur)).toEqual(val));
- });
- });
- describe('durationValue', () => {
- const mock = jest.fn(v => ({ duration: v }));
- beforeEach(() => {
- jest.spyOn(duration, durationKeys.durationFromValue).mockImplementation(mock);
- });
- it('returns an object that maps durationFromValue to the passed duration keys', () => {
- const testDuration = { startTime: 1, stopTime: 2, other: 'values' };
- expect(duration.durationValue(testDuration)).toEqual({
- startTime: mock(testDuration.startTime),
- stopTime: mock(testDuration.stopTime),
- });
- });
- });
- describe('updateDuration', () => {
- const testValidIndex = 'startTime';
- const testStopIndex = 'stopTime';
- const testValidDuration = '00:00:00';
- const testValidValue = 0;
- const testInvalidDuration = 'abc';
- beforeEach(() => {
- props = {
- formValue: { startTime: 23000, stopTime: 600000 },
- local: { startTime: '00:00:23', stopTime: '00:10:00' },
- setLocal: jest.fn(),
- setFormValue: jest.fn(),
- };
- hook = duration.updateDuration;
- ({ cb, prereqs } = hook(props).useCallback);
- });
- it('returns a useCallback field based on the passed args', () => {
- expect(prereqs).toEqual([
- props.formValue,
- props.local,
- props.setLocal,
- props.setFormValue,
- ]);
- });
- describe('callback', () => {
- describe('if the passed durationString is valid', () => {
- it('sets the local value to updated strings and form value to new timestamp value', () => {
- cb(testValidIndex, testValidDuration);
- expect(props.setLocal).toHaveBeenCalledWith({
- ...props.local,
- [testValidIndex]: testValidDuration,
- });
- expect(props.setFormValue).toHaveBeenCalledWith({
- ...props.formValue,
- [testValidIndex]: testValidValue,
- });
- });
- });
- describe('if the passed durationString is not valid', () => {
- it('updates local values to 0 (the default)', () => {
- hook(props).useCallback.cb(testValidIndex, testInvalidDuration);
- expect(props.setLocal).toHaveBeenCalledWith({
- ...props.local,
- [testValidIndex]: testValidDuration,
- });
- expect(props.setFormValue).toHaveBeenCalledWith({
- ...props.formValue,
- [testValidIndex]: testValidValue,
- });
- });
- });
- describe('if the passed startTime is after (or equal to) the stored non-zero stopTime', () => {
- it('updates local startTime values to 1 second before stopTime', () => {
- hook(props).useCallback.cb(testValidIndex, '00:10:00');
- expect(props.setLocal).toHaveBeenCalledWith({
- ...props.local,
- [testValidIndex]: '00:09:59',
- });
- expect(props.setFormValue).toHaveBeenCalledWith({
- ...props.formValue,
- [testValidIndex]: 599000,
- });
- });
- });
- describe('if the passed stopTime is before (or equal to) the stored startTime', () => {
- it('updates local stopTime values to 1 second after startTime', () => {
- hook(props).useCallback.cb(testStopIndex, '00:00:22');
- expect(props.setLocal).toHaveBeenCalledWith({
- ...props.local,
- [testStopIndex]: '00:00:24',
- });
- expect(props.setFormValue).toHaveBeenCalledWith({
- ...props.formValue,
- [testStopIndex]: 24000,
- });
- });
- });
- });
- });
-});
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.js
index 97400b049..621b079a2 100644
--- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.js
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.js
@@ -2,7 +2,6 @@ import {
useCallback,
useState,
useEffect,
- useMemo,
} from 'react';
import { useSelector } from 'react-redux';
@@ -10,18 +9,9 @@ import { StrictDict, keyStore } from '../../../../../utils';
import { actions, selectors } from '../../../../../data/redux';
import {
- updateDuration,
- durationValue,
- onDurationChange,
- onDurationKeyDown,
-} from './duration';
-
-import {
- handleIndexEvent,
handleIndexTransformEvent,
onValue,
onChecked,
- onEvent,
} from './handlers';
import * as module from './hooks';
@@ -88,21 +78,6 @@ export const updateFormField = ({ dispatch, key }) => useCallback(
[],
);
-/**
- * currentValue({ key, formValue })
- * Returns the current display value based on the form value.
- * If duration, uses durationValue to transform the formValue
- * @param {string} key - redux video state key
- * @param {any} formValue - current value in the redux
- * @return {any} - to-local translation of formValue
- */
-export const currentValue = ({ key, formValue }) => {
- if (key === selectorKeys.duration) {
- return durationValue(formValue);
- }
- return formValue;
-};
-
/**
* valueHooks({ dispatch, key })
* returns local and redux state associated with the given data key, as well as methods
@@ -118,16 +93,11 @@ export const currentValue = ({ key, formValue }) => {
*/
export const valueHooks = ({ dispatch, key }) => {
const formValue = useSelector(selectors.video[key]);
- const initialValue = useMemo(() => module.currentValue({ key, formValue }), []);
- const [local, setLocal] = module.state[key](initialValue);
+ const [local, setLocal] = module.state[key](formValue);
const setFormValue = module.updateFormField({ dispatch, key });
useEffect(() => {
- if (key === selectorKeys.duration) {
- setLocal(durationValue(formValue));
- } else {
- setLocal(formValue);
- }
+ setLocal(formValue);
}, [formValue]);
const setAll = useCallback(
@@ -264,62 +234,6 @@ export const objectWidget = ({ dispatch, key }) => {
};
};
-/**
- * durationWidget({ dispatch, key })
- * Returns the value-tied hooks for the video duration widget.
- * Includes onChange, and onBlur. blur changes local and redux state, on-change affects
- * only local state.
- * The creators from this widget will require an index to provide the final event-handler.
- * @param {func} dispatch - redux dispatch method
- * @param {string} key - redux video shape key
- * @return {object} - state hooks
- * formValue - value state in redux
- * setFormValue - sets form field in redux
- * local - value state in hook
- * setLocal - sets form field in hook
- * setAll - sets form field in hook AND redux
- * onChange(index) - handle input change by updating local state
- * onBlur(index) - handle input blur by updating local and redux states
- * onClear(index) - handle clear event by setting value to empty string
- */
-export const durationWidget = ({ dispatch }) => {
- const widget = module.valueHooks({ dispatch, key: selectorKeys.duration });
- const {
- formValue,
- local,
- setFormField,
- setLocal,
- } = widget;
- return {
- ...widget,
- onBlur: useCallback(
- handleIndexEvent({
- handler: onValue,
- transform: updateDuration(widget),
- }),
- [formValue, local, setFormField],
- ),
- onChange: useCallback(
- handleIndexTransformEvent({
- handler: onValue,
- setter: setLocal,
- transform: onDurationChange,
- local,
- }),
- [local],
- ),
- onKeyDown: useCallback(
- handleIndexTransformEvent({
- handler: onEvent,
- setter: setLocal,
- transform: onDurationKeyDown,
- local,
- }),
- [local],
- ),
- };
-};
-
/**
* widgetValues({ fields, dispatch })
* widget value populator, that takes a fields mapping (dataKey: widgetFn) and dispatch
@@ -338,7 +252,6 @@ export const widgetValues = ({ fields, dispatch }) => Object.keys(fields).reduce
export default {
arrayWidget,
- durationWidget,
genericWidget,
objectWidget,
selectorKeys,
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.test.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.test.js
index 571ff57cd..9f6d8a4e5 100644
--- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.test.js
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.test.js
@@ -5,7 +5,6 @@ import { keyStore } from '../../../../../utils';
import { actions, selectors } from '../../../../../data/redux';
import { MockUseState } from '../../../../../../testUtils';
-import * as duration from './duration';
import * as handlers from './handlers';
import * as hooks from './hooks';
@@ -17,13 +16,6 @@ jest.mock('react', () => ({
useMemo: jest.fn((cb, prereqs) => ({ useMemo: { cb, prereqs } })),
}));
-jest.mock('./duration', () => ({
- onDurationChange: jest.fn(value => ({ onDurationChange: value })),
- onDurationKeyDown: jest.fn(value => ({ onDurationKeyDown: value })),
- updateDuration: jest.fn(value => ({ updateDuration: value })),
- durationValue: jest.fn(value => ({ durationValue: value })),
-}));
-
jest.mock('./handlers', () => ({
handleIndexEvent: jest.fn(args => ({ handleIndexEvent: args })),
handleIndexTransformEvent: jest.fn(args => ({ handleIndexTransformEvent: args })),
@@ -56,8 +48,6 @@ jest.mock('../../../../../data/redux', () => ({
}));
const keys = {
- duration: keyStore(duration),
- handlers: keyStore(handlers),
hooks: keyStore(hooks),
selectors: hooks.selectorKeys,
};
@@ -126,18 +116,6 @@ describe('Video Settings modal hooks', () => {
}));
});
});
- describe('currentValue', () => {
- it('returns duration display of form value if is duration key', () => {
- expect(
- hooks.currentValue({ key: keys.selectors.duration, formValue: testValue }),
- ).toEqual(duration.durationValue(testValue));
- });
- it('returns the raw formValue by default', () => {
- expect(
- hooks.currentValue({ key: testKey, formValue: testValue }),
- ).toEqual(testValue);
- });
- });
describe('valueHooks', () => {
let formValue;
beforeEach(() => {
@@ -150,13 +128,6 @@ describe('Video Settings modal hooks', () => {
expect(useEffect).toHaveBeenCalled();
expect(useEffect.mock.calls[0][1]).toEqual([formValue]);
});
- test('calls setLocal with durationValue(formValue) if is duration', () => {
- hooks.valueHooks({ dispatch, key: keys.selectors.duration });
- useEffect.mock.calls[0][0]();
- expect(state.setState[keys.selectors.duration]).toHaveBeenCalledWith(
- duration.durationValue(useSelector(selectors.video.duration)),
- );
- });
test('calls setLocal with formValue by default', () => {
hooks.valueHooks({ dispatch, key: testKey });
useEffect.mock.calls[0][0]();
@@ -165,13 +136,10 @@ describe('Video Settings modal hooks', () => {
});
});
describe('returned object', () => {
- const mockCurrentValue = (args) => ({ currentValue: args });
const mockUpdateFormField = (args) => jest.fn(
(val) => ({ updateFormField: { args, val } }),
);
beforeEach(() => {
- jest.spyOn(hooks, keys.hooks.currentValue)
- .mockImplementationOnce(mockCurrentValue);
jest.spyOn(hooks, keys.hooks.updateFormField)
.mockImplementationOnce(mockUpdateFormField);
out = hooks.valueHooks({ dispatch, key: testKey });
@@ -180,10 +148,9 @@ describe('Video Settings modal hooks', () => {
expect(out.formValue).toEqual(useSelector(selectors.video[testKey]));
});
describe('local and setLocal', () => {
- test('keyed to state, initialized with memo of currentValue that never updates', () => {
+ test('keyed to state, initialized with formValue', () => {
const { local, setLocal } = out;
- expect(local.useMemo.cb()).toEqual(mockCurrentValue({ key: testKey, formValue }));
- expect(local.useMemo.prereqs).toEqual([]);
+ expect(local).toEqual(formValue);
setLocal(testValue);
expect(state.setState[testKey]).toHaveBeenCalledWith(testValue);
});
@@ -315,60 +282,6 @@ describe('Video Settings modal hooks', () => {
}));
});
});
- describe('durationWidget', () => {
- beforeEach(() => {
- out = hooks.durationWidget({ dispatch });
- });
- it('forwards widget values', () => {
- expect(out.formValue).toEqual(widgetValues.formValue);
- expect(out.local).toEqual(widgetValues.local);
- });
- describe('onBlur', () => {
- test('memoized callback based on formValue, local, and setFormValue from widget', () => {
- expect(out.onBlur.useCallback.prereqs).toEqual(
- [widgetValues.formValue, widgetValues.local, widgetValues.setFormField],
- );
- });
- test('calls handleIndexEvent with updateDuration', () => {
- expect(out.onBlur.useCallback.cb).toEqual(
- handlers.handleIndexEvent({
- handler: handlers.onValue,
- transform: duration.updateDuration(widgetValues),
- }),
- );
- });
- });
- describe('onChange', () => {
- test('memoized callback based on local from widget', () => {
- expect(out.onChange.useCallback.prereqs).toEqual([widgetValues.local]);
- });
- test('calls handleIndexTransformEvent with setLocal', () => {
- expect(out.onChange.useCallback.cb).toEqual(
- handlers.handleIndexTransformEvent({
- handler: handlers.onValue,
- setter: widgetValues.setLocal,
- transform: duration.onDurationChange,
- local: widgetValues.local,
- }),
- );
- });
- });
- describe('onKeyDown', () => {
- test('memoized callback based on local from widget', () => {
- expect(out.onKeyDown.useCallback.prereqs).toEqual([widgetValues.local]);
- });
- test('calls handleIndexTransformEvent with setLocal', () => {
- expect(out.onKeyDown.useCallback.cb).toEqual(
- handlers.handleIndexTransformEvent({
- handler: handlers.onEvent,
- setter: widgetValues.setLocal,
- transform: duration.onDurationKeyDown,
- local: widgetValues.local,
- }),
- );
- });
- });
- });
});
describe('widgetValues', () => {
describe('returned object', () => {
diff --git a/src/editors/data/constants/olxTemplates/circuitschematic.js b/src/editors/data/constants/advancedOlxTemplates/circuitschematic.js
similarity index 99%
rename from src/editors/data/constants/olxTemplates/circuitschematic.js
rename to src/editors/data/constants/advancedOlxTemplates/circuitschematic.js
index 571b7c484..6dc5beb85 100644
--- a/src/editors/data/constants/olxTemplates/circuitschematic.js
+++ b/src/editors/data/constants/advancedOlxTemplates/circuitschematic.js
@@ -89,4 +89,4 @@ export const circuitSchematic = `
`;
-export default { circuitSchematic };
+export default circuitSchematic;
diff --git a/src/editors/data/constants/olxTemplates/customgrader.js b/src/editors/data/constants/advancedOlxTemplates/customgrader.js
similarity index 99%
rename from src/editors/data/constants/olxTemplates/customgrader.js
rename to src/editors/data/constants/advancedOlxTemplates/customgrader.js
index 914fe3bb3..cd0f50555 100644
--- a/src/editors/data/constants/olxTemplates/customgrader.js
+++ b/src/editors/data/constants/advancedOlxTemplates/customgrader.js
@@ -76,4 +76,4 @@ export const customGrader = `
`;
-export default { customGrader };
+export default customGrader;
diff --git a/src/editors/data/constants/olxTemplates/formularesponse.js b/src/editors/data/constants/advancedOlxTemplates/formularesponse.js
similarity index 96%
rename from src/editors/data/constants/olxTemplates/formularesponse.js
rename to src/editors/data/constants/advancedOlxTemplates/formularesponse.js
index ea5d43987..b77a166ae 100644
--- a/src/editors/data/constants/olxTemplates/formularesponse.js
+++ b/src/editors/data/constants/advancedOlxTemplates/formularesponse.js
@@ -13,4 +13,4 @@ export const formulaResponse = `
`;
-export default { formulaResponse };
+export default formulaResponse;
diff --git a/src/editors/data/constants/olxTemplates/imageresponse.js b/src/editors/data/constants/advancedOlxTemplates/imageresponse.js
similarity index 98%
rename from src/editors/data/constants/olxTemplates/imageresponse.js
rename to src/editors/data/constants/advancedOlxTemplates/imageresponse.js
index b3b7acb6a..36ac9e2c6 100644
--- a/src/editors/data/constants/olxTemplates/imageresponse.js
+++ b/src/editors/data/constants/advancedOlxTemplates/imageresponse.js
@@ -30,4 +30,4 @@ export const imageResponse = `
`;
-export default { imageResponse };
+export default imageResponse;
diff --git a/src/editors/data/constants/advancedOlxTemplates/index.js b/src/editors/data/constants/advancedOlxTemplates/index.js
new file mode 100644
index 000000000..16602efdb
--- /dev/null
+++ b/src/editors/data/constants/advancedOlxTemplates/index.js
@@ -0,0 +1,11 @@
+import { StrictDict } from '../../../utils';
+import circuitSchematic from './circuitschematic';
+import customGrader from './customgrader';
+import formulaResponse from './formularesponse';
+import imageResponse from './imageresponse';
+import jsInputResponse from './jsinput_response';
+import problemWithHint from './problem_with_hint';
+
+export default StrictDict({
+ circuitSchematic, customGrader, formulaResponse, imageResponse, jsInputResponse, problemWithHint,
+});
diff --git a/src/editors/data/constants/olxTemplates/jsinput_response.js b/src/editors/data/constants/advancedOlxTemplates/jsinput_response.js
similarity index 99%
rename from src/editors/data/constants/olxTemplates/jsinput_response.js
rename to src/editors/data/constants/advancedOlxTemplates/jsinput_response.js
index 3af5a3761..356a96d66 100644
--- a/src/editors/data/constants/olxTemplates/jsinput_response.js
+++ b/src/editors/data/constants/advancedOlxTemplates/jsinput_response.js
@@ -78,4 +78,4 @@ export const jsInputResponse = `
`;
-export default { jsInputResponse };
+export default jsInputResponse;
diff --git a/src/editors/data/constants/olxTemplates/problem_with_hint.js b/src/editors/data/constants/advancedOlxTemplates/problem_with_hint.js
similarity index 98%
rename from src/editors/data/constants/olxTemplates/problem_with_hint.js
rename to src/editors/data/constants/advancedOlxTemplates/problem_with_hint.js
index 10be3531c..fb79a28d2 100644
--- a/src/editors/data/constants/olxTemplates/problem_with_hint.js
+++ b/src/editors/data/constants/advancedOlxTemplates/problem_with_hint.js
@@ -43,4 +43,4 @@ export const problemWithHint = `
`;
-export default { problemWithHint };
+export default problemWithHint;
diff --git a/src/editors/data/constants/basicOlxTemplates/dropdown.js b/src/editors/data/constants/basicOlxTemplates/dropdown.js
new file mode 100644
index 000000000..9fa1853a1
--- /dev/null
+++ b/src/editors/data/constants/basicOlxTemplates/dropdown.js
@@ -0,0 +1,13 @@
+/* eslint-disable */
+export const dropdown = `
+
+ Enter your question
+
+ an incorrect answer
+ the correct answer
+ an incorrect answer
+
+
+ `
+
+export default dropdown;
\ No newline at end of file
diff --git a/src/editors/data/constants/basicOlxTemplates/index.js b/src/editors/data/constants/basicOlxTemplates/index.js
new file mode 100644
index 000000000..977d3b7a5
--- /dev/null
+++ b/src/editors/data/constants/basicOlxTemplates/index.js
@@ -0,0 +1,10 @@
+import { StrictDict } from '../../../utils';
+import dropdown from './dropdown';
+import multiSelect from './multiSelect';
+import numeric from './numeric';
+import singleSelect from './singleSelect';
+import textInput from './textInput';
+
+export default StrictDict({
+ dropdown, multiSelect, numeric, singleSelect, textInput,
+});
diff --git a/src/editors/data/constants/basicOlxTemplates/multiSelect.js b/src/editors/data/constants/basicOlxTemplates/multiSelect.js
new file mode 100644
index 000000000..4e439917b
--- /dev/null
+++ b/src/editors/data/constants/basicOlxTemplates/multiSelect.js
@@ -0,0 +1,13 @@
+/* eslint-disable */
+ const multiSelect= `
+
+Enter your question
+
+ a correct answer
+ an incorrect answer
+ an incorrect answer
+ a correct answer
+
+
+ `
+export default multiSelect;
\ No newline at end of file
diff --git a/src/editors/data/constants/basicOlxTemplates/numeric.js b/src/editors/data/constants/basicOlxTemplates/numeric.js
new file mode 100644
index 000000000..22a3f7852
--- /dev/null
+++ b/src/editors/data/constants/basicOlxTemplates/numeric.js
@@ -0,0 +1,10 @@
+/* eslint-disable */
+export const numeric = `
+
+Enter your question
+
+
+
+ `
+
+export default numeric;
\ No newline at end of file
diff --git a/src/editors/data/constants/basicOlxTemplates/singleSelect.js b/src/editors/data/constants/basicOlxTemplates/singleSelect.js
new file mode 100644
index 000000000..de402b531
--- /dev/null
+++ b/src/editors/data/constants/basicOlxTemplates/singleSelect.js
@@ -0,0 +1,12 @@
+/* eslint-disable */
+export const singleSelect = `
+
+Enter your question
+
+ an incorrect answer
+ the correct answer
+ an incorrect answer
+
+
+ `
+export default singleSelect;
\ No newline at end of file
diff --git a/src/editors/data/constants/basicOlxTemplates/textInput.js b/src/editors/data/constants/basicOlxTemplates/textInput.js
new file mode 100644
index 000000000..ceee90835
--- /dev/null
+++ b/src/editors/data/constants/basicOlxTemplates/textInput.js
@@ -0,0 +1,9 @@
+/* eslint-disable */
+const textInput =`
+
+Enter your question
+
+
+
+ `
+export default textInput;
\ No newline at end of file
diff --git a/src/editors/data/constants/olxTemplates/drag_and_drop.js b/src/editors/data/constants/olxTemplates/drag_and_drop.js
deleted file mode 100644
index 40d313652..000000000
--- a/src/editors/data/constants/olxTemplates/drag_and_drop.js
+++ /dev/null
@@ -1,88 +0,0 @@
-/* eslint-disable */
-// ---
-// metadata:
-// display_name: Drag and Drop (Deprecated Version)
-// markdown: !!null
-// showanswer: never
-// data: |
-export const dragAndDrop = `
- In drag and drop problems, students respond to a question by dragging text or objects to a specific location on an image.
-
- For more information, see
-
- Drag and Drop Problem (Deprecated) in Building and Running an edX Course .
-
-
- When you add the problem, be sure to select Settings
- to specify a Display Name and other values that apply.
-
- You can use the following example problems as models.
-
-
- Simple Drag and Drop
- Drag each word in the scrollbar to the bucket that matches the number of letters in the word.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- correct_answer = {
- '1': [[70, 150], 121],
- '6': [[190, 150], 121],
- '8': [[190, 150], 121],
- '2': [[310, 150], 121],
- '9': [[310, 150], 121],
- '11': [[310, 150], 121],
- '4': [[420, 150], 121],
- '7': [[420, 150], 121],
- '3': [[550, 150], 121],
- '5': [[550, 150], 121],
- '10': [[550, 150], 121]}
- if draganddrop.grade(submission[0], correct_answer):
- correct = ['correct']
- else:
- correct = ['incorrect']
-
-
-
-
- Drag and Drop with Outline
- Label the hydrogen atoms connected with the left carbon atom.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- correct_answer = [{
- 'draggables': ['1', '2'],
- 'targets': ['t2', 't3', 't4' ],
- 'rule':'anyof'
- }]
- if draganddrop.grade(submission[0], correct_answer):
- correct = ['correct']
- else:
- correct = ['incorrect']
-
-
- `;
-
-export default { dragAndDrop };
diff --git a/src/editors/data/constants/problem.js b/src/editors/data/constants/problem.js
index cc491a9d8..20bca15ef 100644
--- a/src/editors/data/constants/problem.js
+++ b/src/editors/data/constants/problem.js
@@ -4,13 +4,8 @@ import multiSelect from '../images/multiSelect.png';
import dropdown from '../images/dropdown.png';
import numericalInput from '../images/numericalInput.png';
import textInput from '../images/textInput.png';
-import { circuitSchematic } from './olxTemplates/circuitschematic';
-import { customGrader } from './olxTemplates/customgrader';
-import { dragAndDrop } from './olxTemplates/drag_and_drop';
-import { formulaResponse } from './olxTemplates/formularesponse';
-import { imageResponse } from './olxTemplates/imageresponse';
-import { jsInputResponse } from './olxTemplates/jsinput_response';
-import { problemWithHint } from './olxTemplates/problem_with_hint';
+import advancedOlxTemplates from './advancedOlxTemplates';
+import basicOlxTemplates from './basicOlxTemplates';
export const ProblemTypeKeys = StrictDict({
SINGLESELECT: 'multiplechoiceresponse',
@@ -23,44 +18,50 @@ export const ProblemTypeKeys = StrictDict({
export const ProblemTypes = StrictDict({
[ProblemTypeKeys.SINGLESELECT]: {
- title: 'Single Select Problem',
+ title: 'Single select',
preview: singleSelect,
description: 'Specify one correct answer from a list of possible options',
helpLink: 'https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/multiple_choice.html',
prev: ProblemTypeKeys.TEXTINPUT,
next: ProblemTypeKeys.MULTISELECT,
+ template: basicOlxTemplates.singleSelect,
+
},
[ProblemTypeKeys.MULTISELECT]: {
- title: 'Multi Select Problem',
+ title: 'Multi-select',
preview: multiSelect,
description: 'Specify one or more correct answers from a list of possible options.',
helpLink: 'https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/checkbox.html',
next: ProblemTypeKeys.DROPDOWN,
prev: ProblemTypeKeys.SINGLESELECT,
+ template: basicOlxTemplates.multiSelect,
},
[ProblemTypeKeys.DROPDOWN]: {
- title: 'Dropdown Problem',
+ title: 'Dropdown',
preview: dropdown,
description: 'Specify one correct answer from a list of possible options, selected in a dropdown menu.',
helpLink: 'https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/dropdown.html',
next: ProblemTypeKeys.NUMERIC,
prev: ProblemTypeKeys.MULTISELECT,
+ template: basicOlxTemplates.dropdown,
},
[ProblemTypeKeys.NUMERIC]: {
- title: 'Numeric Response Problem',
+ title: 'Numerical input',
preview: numericalInput,
description: 'Specify one or more correct numeric answers, submitted in a response field.',
helpLink: 'https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/numerical_input.html',
next: ProblemTypeKeys.TEXTINPUT,
prev: ProblemTypeKeys.DROPDOWN,
+ template: basicOlxTemplates.numeric,
},
[ProblemTypeKeys.TEXTINPUT]: {
- title: 'Text Input Problem',
+ title: 'Text input',
preview: textInput,
description: 'Specify one or more correct text answers, including numbers and special characters, submitted in a response field.',
helpLink: 'https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/text_input.html',
prev: ProblemTypeKeys.NUMERIC,
next: ProblemTypeKeys.SINGLESELECT,
+ template: basicOlxTemplates.textInput,
},
[ProblemTypeKeys.ADVANCED]: {
title: 'Advanced Problem',
@@ -75,7 +76,6 @@ export const AdvanceProblemKeys = StrictDict({
CIRCUITSCHEMATIC: 'circuitschematic',
JSINPUT: 'jsinputresponse',
CUSTOMGRADER: 'customgrader',
- DRAGANDDROP: 'draganddrop',
IMAGE: 'imageresponse',
FORMULA: 'formularesponse',
PROBLEMWITHHINT: 'problemwithhint',
@@ -83,44 +83,39 @@ export const AdvanceProblemKeys = StrictDict({
export const AdvanceProblems = StrictDict({
[AdvanceProblemKeys.BLANK]: {
- title: 'Blank advance problem',
+ title: 'Blank advanced problem',
status: '',
template: ' ',
},
[AdvanceProblemKeys.CIRCUITSCHEMATIC]: {
title: 'Circuit schematic builder',
status: 'Not supported',
- template: circuitSchematic,
+ template: advancedOlxTemplates.circuitSchematic,
},
[AdvanceProblemKeys.JSINPUT]: {
title: 'Custom JavaScript display and grading',
status: '',
- template: jsInputResponse,
+ template: advancedOlxTemplates.jsInputResponse,
},
[AdvanceProblemKeys.CUSTOMGRADER]: {
title: 'Custom Python-evaluated input',
status: 'Provisional',
- template: customGrader,
- },
- [AdvanceProblemKeys.DRAGANDDROP]: {
- title: 'Drag and drop (deprecated version)',
- status: 'Not supported',
- template: dragAndDrop,
+ template: advancedOlxTemplates.customGrader,
},
[AdvanceProblemKeys.IMAGE]: {
title: 'Image mapped input',
status: 'Not supported',
- template: imageResponse,
+ template: advancedOlxTemplates.imageResponse,
},
[AdvanceProblemKeys.FORMULA]: {
title: 'Math expression input',
status: '',
- template: formulaResponse,
+ template: advancedOlxTemplates.formulaResponse,
},
[AdvanceProblemKeys.PROBLEMWITHHINT]: {
title: 'Problem with adaptive hint',
status: 'Not supported',
- template: problemWithHint,
+ template: advancedOlxTemplates.problemWithHint,
},
});
diff --git a/src/editors/data/constants/tinyMCEStyles.js b/src/editors/data/constants/tinyMCEStyles.js
index a5c9f4f55..d201a86fd 100644
--- a/src/editors/data/constants/tinyMCEStyles.js
+++ b/src/editors/data/constants/tinyMCEStyles.js
@@ -1,214 +1,220 @@
-export default `@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400;1,600;1,700&display=swap");
+const getStyles = () => (
+ `@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400;1,600;1,700&display=swap");
-.mce-content-body *[contentEditable=false] {
- cursor: default;
-}
-.mce-content-body *[contentEditable=true] {
- cursor: text;
-}
-.mce-content-body div.mce-resizehandle {
- background-color: #4099ff;
- border-color: #4099ff;
- border-style: solid;
- border-width: 1px;
- box-sizing: border-box;
- height: 10px;
- position: absolute;
- width: 10px;
- z-index: 1298;
-}
-.mce-content-body div.mce-resizehandle:hover {
- background-color: #4099ff;
-}
-.mce-content-body div.mce-resizehandle:nth-of-type(1) {
- cursor: nwse-resize;
-}
-.mce-content-body div.mce-resizehandle:nth-of-type(2) {
- cursor: nesw-resize;
-}
-.mce-content-body div.mce-resizehandle:nth-of-type(3) {
- cursor: nwse-resize;
-}
-.mce-content-body div.mce-resizehandle:nth-of-type(4) {
- cursor: nesw-resize;
-}
-.mce-content-body .mce-resize-backdrop {
- z-index: 10000;
-}
-.mce-content-body .mce-clonedresizable {
- cursor: default;
- opacity: 0.5;
- outline: 1px dashed black;
- position: absolute;
- z-index: 10001;
-}
-.mce-content-body .mce-clonedresizable.mce-resizetable-columns th,
-.mce-content-body .mce-clonedresizable.mce-resizetable-columns td {
- border: 0;
-}
-.mce-content-body .mce-resize-helper {
- background: #555;
- background: rgba(0, 0, 0, 0.75);
- border: 1px;
- border-radius: 3px;
- color: white;
- display: none;
- font-family: sans-serif;
- font-size: 12px;
- line-height: 14px;
- margin: 5px 10px;
- padding: 5px;
- position: absolute;
- white-space: nowrap;
- z-index: 10002;
-}
-.mce-content-body img[data-mce-selected],
-.mce-content-body video[data-mce-selected],
-.mce-content-body audio[data-mce-selected],
-.mce-content-body object[data-mce-selected],
-.mce-content-body embed[data-mce-selected],
-.mce-content-body table[data-mce-selected] {
- outline: 3px solid #b4d7ff;
-}
-.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus {
- outline: 3px solid #b4d7ff;
-}
-.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover {
- outline: 3px solid #b4d7ff;
-}
-.mce-content-body *[contentEditable=false][data-mce-selected] {
- cursor: not-allowed;
- outline: 3px solid #b4d7ff;
-}
-.mce-content-body.mce-content-readonly *[contentEditable=true]:focus,
-.mce-content-body.mce-content-readonly *[contentEditable=true]:hover {
- outline: none;
-}
-.mce-content-body *[data-mce-selected="inline-boundary"] {
- background-color: #b4d7ff;
-}
-.mce-content-body .mce-edit-focus {
- outline: 3px solid #b4d7ff;
-}
-.mce-content-body img::-moz-selection {
- background: none;
-}
-.mce-content-body img::selection {
- background: none;
-}
-.mce-content-body {
- padding: 10px;
- background-color: #fff;
- font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif;
- font-size: 16px;
- line-height: 1.6;
- color: #3c3c3c;
- scrollbar-3dlight-color: #F0F0EE;
- scrollbar-arrow-color: #676662;
- scrollbar-base-color: #F0F0EE;
- scrollbar-darkshadow-color: #DDDDDD;
- scrollbar-face-color: #E0E0DD;
- scrollbar-highlight-color: #F0F0EE;
- scrollbar-shadow-color: #F0F0EE;
- scrollbar-track-color: #F5F5F5;
-}
-.mce-content-body h1,
-.mce-content-body .hd-1 {
- color: #3c3c3c;
- font-weight: normal;
- font-size: 2em;
- line-height: 1.4em;
- margin: 0 0 1.41575em 0;
-}
-.mce-content-body h2,
-.mce-content-body .hd-2 {
- letter-spacing: 1px;
- margin-bottom: 15px;
- color: #646464;
- font-weight: 300;
- font-size: 1.2em;
- line-height: 1.2em;
- text-transform: uppercase;
-}
-.mce-content-body h3,
-.mce-content-body .hd-3 {
- margin: 0 0 10px 0;
- font-size: 1.1125em;
- font-weight: 400;
- text-transform: initial;
-}
-.mce-content-body .hd-3,
-.mce-content-body h4,
-.mce-content-body .hd-4,
-.mce-content-body h5,
-.mce-content-body .hd-5,
-.mce-content-body h6,
-.mce-content-body .hd-6 {
- margin: 0 0 10px 0;
- font-weight: 600;
-}
-.mce-content-body h4,
-.mce-content-body .hd-4 {
- font-size: 1em;
-}
-.mce-content-body h5,
-.mce-content-body .hd-5 {
- font-size: 0.83em;
-}
-.mce-content-body h6,
-.mce-content-body .hd-6 {
- font-size: 0.75em;
-}
-.mce-content-body p {
- margin-bottom: 1.416em;
- font-size: 1em;
- line-height: 1.6em !important;
- color: #3c3c3c;
-}
-.mce-content-body em, .mce-content-body i {
- font-style: italic;
-}
-.mce-content-body strong, .mce-content-body b {
- font-weight: bold;
-}
-.mce-content-body p + p, .mce-content-body ul + p, .mce-content-body ol + p {
- margin-top: 20px;
-}
-.mce-content-body ol, .mce-content-body ul {
- margin: 1em 0;
- padding: 0 0 0 1em;
- color: #3c3c3c;
-}
-.mce-content-body ol li, .mce-content-body ul li {
- margin-bottom: 0.708em;
-}
-.mce-content-body ol {
- list-style: decimal outside none;
- margin: 0;
-}
-.mce-content-body ul {
- list-style: disc outside none;
- margin: 0;
-}
-.mce-content-body a, .mce-content-body a:link, .mce-content-body a:visited, .mce-content-body a:hover, .mce-content-body a:active {
- color: #0075b4;
- text-decoration: none;
-}
-.mce-content-body img {
- max-width: 100%;
- height: auto;
-}
-.mce-content-body pre {
- margin: 1em 0;
- color: #3c3c3c;
- font-family: monospace, serif;
- font-size: 1em;
- white-space: pre-wrap;
- word-wrap: break-word;
-}
-.mce-content-body code {
- font-family: monospace, serif;
+ .mce-content-body *[contentEditable=false] {
+ cursor: default;
+ }
+ .mce-content-body *[contentEditable=true] {
+ cursor: text;
+ }
+ .mce-content-body div.mce-resizehandle {
+ background-color: #4099ff;
+ border-color: #4099ff;
+ border-style: solid;
+ border-width: 1px;
+ box-sizing: border-box;
+ height: 10px;
+ position: absolute;
+ width: 10px;
+ z-index: 1298;
+ }
+ .mce-content-body div.mce-resizehandle:hover {
+ background-color: #4099ff;
+ }
+ .mce-content-body div.mce-resizehandle:nth-of-type(1) {
+ cursor: nwse-resize;
+ }
+ .mce-content-body div.mce-resizehandle:nth-of-type(2) {
+ cursor: nesw-resize;
+ }
+ .mce-content-body div.mce-resizehandle:nth-of-type(3) {
+ cursor: nwse-resize;
+ }
+ .mce-content-body div.mce-resizehandle:nth-of-type(4) {
+ cursor: nesw-resize;
+ }
+ .mce-content-body .mce-resize-backdrop {
+ z-index: 10000;
+ }
+ .mce-content-body .mce-clonedresizable {
+ cursor: default;
+ opacity: 0.5;
+ outline: 1px dashed black;
+ position: absolute;
+ z-index: 10001;
+ }
+ .mce-content-body .mce-clonedresizable.mce-resizetable-columns th,
+ .mce-content-body .mce-clonedresizable.mce-resizetable-columns td {
+ border: 0;
+ }
+ .mce-content-body .mce-resize-helper {
+ background: #555;
+ background: rgba(0, 0, 0, 0.75);
+ border: 1px;
+ border-radius: 3px;
+ color: white;
+ display: none;
+ font-family: sans-serif;
+ font-size: 12px;
+ line-height: 14px;
+ margin: 5px 10px;
+ padding: 5px;
+ position: absolute;
+ white-space: nowrap;
+ z-index: 10002;
+ }
+ .mce-content-body img[data-mce-selected],
+ .mce-content-body video[data-mce-selected],
+ .mce-content-body audio[data-mce-selected],
+ .mce-content-body object[data-mce-selected],
+ .mce-content-body embed[data-mce-selected],
+ .mce-content-body table[data-mce-selected] {
+ outline: 3px solid #b4d7ff;
+ }
+ .mce-content-body *[contentEditable=false] *[contentEditable=true]:focus {
+ outline: 3px solid #b4d7ff;
+ }
+ .mce-content-body *[contentEditable=false] *[contentEditable=true]:hover {
+ outline: 3px solid #b4d7ff;
+ }
+ .mce-content-body *[contentEditable=false][data-mce-selected] {
+ cursor: not-allowed;
+ outline: 3px solid #b4d7ff;
+ }
+ .mce-content-body.mce-content-readonly *[contentEditable=true]:focus,
+ .mce-content-body.mce-content-readonly *[contentEditable=true]:hover {
+ outline: none;
+ }
+ .mce-content-body *[data-mce-selected="inline-boundary"] {
+ background-color: #b4d7ff;
+ }
+ .mce-content-body .mce-edit-focus {
+ outline: 3px solid #b4d7ff;
+ }
+ .mce-content-body img::-moz-selection {
background: none;
+ }
+ .mce-content-body img::selection {
+ background: none;
+ }
+ .mce-content-body {
+ padding: 10px;
+ background-color: #fff;
+ font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif;
+ font-size: 16px;
+ line-height: 1.6;
+ color: #3c3c3c;
+ scrollbar-3dlight-color: #F0F0EE;
+ scrollbar-arrow-color: #676662;
+ scrollbar-base-color: #F0F0EE;
+ scrollbar-darkshadow-color: #DDDDDD;
+ scrollbar-face-color: #E0E0DD;
+ scrollbar-highlight-color: #F0F0EE;
+ scrollbar-shadow-color: #F0F0EE;
+ scrollbar-track-color: #F5F5F5;
+ }
+ .mce-content-body h1,
+ .mce-content-body .hd-1 {
+ color: #3c3c3c;
+ font-weight: normal;
+ font-size: 2em;
+ line-height: 1.4em;
+ margin: 0 0 1.41575em 0;
+ }
+ .mce-content-body h2,
+ .mce-content-body .hd-2 {
+ letter-spacing: 1px;
+ margin-bottom: 15px;
+ color: #646464;
+ font-weight: 300;
+ font-size: 1.2em;
+ line-height: 1.2em;
+ text-transform: uppercase;
+ }
+ .mce-content-body h3,
+ .mce-content-body .hd-3 {
+ margin: 0 0 10px 0;
+ font-size: 1.1125em;
+ font-weight: 400;
+ text-transform: initial;
+ }
+ .mce-content-body .hd-3,
+ .mce-content-body h4,
+ .mce-content-body .hd-4,
+ .mce-content-body h5,
+ .mce-content-body .hd-5,
+ .mce-content-body h6,
+ .mce-content-body .hd-6 {
+ margin: 0 0 10px 0;
+ font-weight: 600;
+ }
+ .mce-content-body h4,
+ .mce-content-body .hd-4 {
+ font-size: 1em;
+ }
+ .mce-content-body h5,
+ .mce-content-body .hd-5 {
+ font-size: 0.83em;
+ }
+ .mce-content-body h6,
+ .mce-content-body .hd-6 {
+ font-size: 0.75em;
+ }
+ .mce-content-body p {
+ margin-bottom: 1.416em;
+ font-size: 1em;
+ line-height: 1.6em !important;
+ color: #3c3c3c;
+ }
+ .mce-content-body em, .mce-content-body i {
+ font-style: italic;
+ }
+ .mce-content-body strong, .mce-content-body b {
+ font-weight: bold;
+ }
+ .mce-content-body p + p, .mce-content-body ul + p, .mce-content-body ol + p {
+ margin-top: 20px;
+ }
+ .mce-content-body ol, .mce-content-body ul {
+ margin: 1em 0;
+ padding: 0 0 0 1em;
+ color: #3c3c3c;
+ }
+ .mce-content-body ol li, .mce-content-body ul li {
+ margin-bottom: 0.708em;
+ }
+ .mce-content-body ol {
+ list-style: decimal outside none;
+ margin: 0;
+ }
+ .mce-content-body ul {
+ list-style: disc outside none;
+ margin: 0;
+ }
+ .mce-content-body a, .mce-content-body a:link, .mce-content-body a:visited, .mce-content-body a:hover, .mce-content-body a:active {
+ color: #0075b4;
+ text-decoration: none;
+ }
+ .mce-content-body img {
+ max-width: 100%;
+ height: auto;
+ }
+ .mce-content-body pre {
+ margin: 1em 0;
color: #3c3c3c;
- padding: 0;
-}`;
+ font-family: monospace, serif;
+ font-size: 1em;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ }
+ .mce-content-body code {
+ font-family: monospace, serif;
+ background: none;
+ color: #3c3c3c;
+ padding: 0;
+ }`
+);
+
+export { getStyles };
+
+export default getStyles({});
diff --git a/src/editors/data/redux/problem/reducers.js b/src/editors/data/redux/problem/reducers.js
index 765c26ede..454f44725 100644
--- a/src/editors/data/redux/problem/reducers.js
+++ b/src/editors/data/redux/problem/reducers.js
@@ -10,6 +10,7 @@ const initialState = {
problemType: null,
question: '',
answers: [],
+ correctAnswerCount: 0,
groupFeedbackList: [],
additionalAttributes: {},
settings: {
@@ -17,7 +18,7 @@ const initialState = {
weight: 0,
attempts: {
unlimited: true,
- number: 0,
+ number: null,
},
},
hints: [],
@@ -46,8 +47,17 @@ const problem = createSlice({
}),
updateAnswer: (state, { payload }) => {
const { id, hasSingleAnswer, ...answer } = payload;
+ let { correctAnswerCount } = state;
const answers = state.answers.map(obj => {
if (obj.id === id) {
+ if (_.has(answer, 'correct') && payload.correct) {
+ correctAnswerCount += 1;
+ return { ...obj, ...answer };
+ }
+ if (_.has(answer, 'correct') && payload.correct === false) {
+ correctAnswerCount -= 1;
+ return { ...obj, ...answer };
+ }
return { ...obj, ...answer };
}
// set other answers as incorrect if problem only has one answer correct
@@ -59,14 +69,19 @@ const problem = createSlice({
});
return {
...state,
+ correctAnswerCount,
answers,
};
},
deleteAnswer: (state, { payload }) => {
- const { id } = payload;
+ const { id, correct } = payload;
if (state.answers.length <= 1) {
return state;
}
+ let { correctAnswerCount } = state;
+ if (correct) {
+ correctAnswerCount -= 1;
+ }
const answers = state.answers.filter(obj => obj.id !== id).map((answer, index) => {
const newId = indexToLetterMap[index];
if (answer.id === newId) {
@@ -76,6 +91,7 @@ const problem = createSlice({
});
return {
...state,
+ correctAnswerCount,
answers,
};
},
@@ -89,15 +105,12 @@ const problem = createSlice({
title: '',
selectedFeedback: undefined,
unselectedFeedback: undefined,
- feedback: undefined,
correct: false,
};
if (state.problemType === ProblemTypeKeys.MULTISELECT) {
- newOption.selectedFeedback = '';
newOption.unselectedFeedback = '';
- } else {
- newOption.feedback = '';
}
+ newOption.selectedFeedback = '';
const answers = [
...currAnswers,
newOption,
@@ -128,10 +141,6 @@ const problem = createSlice({
...state,
problemType: null,
}),
- setProblemType: (state, { payload: { selected } }) => ({
- ...state,
- problemType: selected,
- }),
},
});
diff --git a/src/editors/data/redux/problem/reducers.test.js b/src/editors/data/redux/problem/reducers.test.js
new file mode 100644
index 000000000..737404097
--- /dev/null
+++ b/src/editors/data/redux/problem/reducers.test.js
@@ -0,0 +1,129 @@
+import { initialState, actions, reducer } from './reducers';
+
+const testingState = {
+ ...initialState,
+ arbitraryField: 'arbitrary',
+};
+
+describe('problem reducer', () => {
+ it('has initial state', () => {
+ expect(reducer(undefined, {})).toEqual(initialState);
+ });
+
+ const testValue = 'roll for initiative';
+
+ describe('handling actions', () => {
+ const setterTest = (action, target) => {
+ describe(action, () => {
+ it(`load ${target} from payload`, () => {
+ expect(reducer(testingState, actions[action](testValue))).toEqual({
+ ...testingState,
+ [target]: testValue,
+ });
+ });
+ });
+ };
+ [
+ ['updateQuestion', 'question'],
+ ].map(args => setterTest(...args));
+ describe('load', () => {
+ it('sets answers', () => {
+ const answer = {
+ id: 'A',
+ correct: false,
+ selectedFeedback: '',
+ title: '',
+ unselectedFeedback: undefined,
+ };
+ expect(reducer(testingState, actions.addAnswer(answer))).toEqual({
+ ...testingState,
+ answers: [answer],
+ });
+ });
+ });
+ describe('updateField', () => {
+ it('sets given parameter', () => {
+ const payload = { problemType: 'soMePRoblEMtYPe' };
+ expect(reducer(testingState, actions.updateField(payload))).toEqual({
+ ...testingState,
+ ...payload,
+ });
+ });
+ });
+ describe('updateSettings', () => {
+ it('sets given settings parameter', () => {
+ const payload = { hints: ['soMehInt'] };
+ expect(reducer(testingState, actions.updateSettings(payload))).toEqual({
+ ...testingState,
+ settings: {
+ ...testingState.settings,
+ ...payload,
+ },
+ });
+ });
+ });
+ describe('addAnswer', () => {
+ it('sets answers', () => {
+ const answer = {
+ id: 'A',
+ correct: false,
+ selectedFeedback: '',
+ title: '',
+ unselectedFeedback: '',
+ };
+ expect(reducer({ ...testingState, problemType: 'choiceresponse' }, actions.addAnswer())).toEqual({
+ ...testingState,
+ problemType: 'choiceresponse',
+ answers: [answer],
+ });
+ });
+ });
+ describe('updateAnswer', () => {
+ it('sets answers, as well as setting the correctAnswerCount ', () => {
+ const answer = { id: 'A', correct: true };
+ expect(reducer(
+ {
+ ...testingState,
+ answers: [{
+ id: 'A',
+ correct: false,
+ }],
+ },
+ actions.updateAnswer(answer),
+ )).toEqual({
+ ...testingState,
+ correctAnswerCount: 1,
+ answers: [{ id: 'A', correct: true }],
+ });
+ });
+ });
+ describe('deleteAnswer', () => {
+ it('sets answers, as well as setting the correctAnswerCount ', () => {
+ const answer = { id: 'A' };
+ expect(reducer(
+ {
+ ...testingState,
+ correctAnswerCount: 1,
+ answers: [{
+ id: 'A',
+ correct: false,
+ },
+ {
+ id: 'B',
+ correct: true,
+ }],
+ },
+ actions.deleteAnswer(answer),
+ )).toEqual({
+ ...testingState,
+ correctAnswerCount: 1,
+ answers: [
+ {
+ id: 'A',
+ correct: true,
+ }],
+ });
+ });
+ });
+ });
+});
diff --git a/src/editors/data/redux/problem/selectors.js b/src/editors/data/redux/problem/selectors.js
index 532bc5289..a9c267600 100644
--- a/src/editors/data/redux/problem/selectors.js
+++ b/src/editors/data/redux/problem/selectors.js
@@ -6,6 +6,7 @@ const mkSimpleSelector = (cb) => createSelector([module.problemState], cb);
export const simpleSelectors = {
problemType: mkSimpleSelector(problemData => problemData.problemType),
answers: mkSimpleSelector(problemData => problemData.answers),
+ correctAnswerCount: mkSimpleSelector(problemData => problemData.correctAnswerCount),
settings: mkSimpleSelector(problemData => problemData.settings),
question: mkSimpleSelector(problemData => problemData.question),
completeState: mkSimpleSelector(problemData => problemData),
diff --git a/src/editors/data/redux/problem/selectors.test.js b/src/editors/data/redux/problem/selectors.test.js
new file mode 100644
index 000000000..b4d36bb13
--- /dev/null
+++ b/src/editors/data/redux/problem/selectors.test.js
@@ -0,0 +1,52 @@
+// import * in order to mock in-file references
+import { keyStore } from '../../../utils';
+import * as selectors from './selectors';
+
+jest.mock('reselect', () => ({
+ createSelector: jest.fn((preSelectors, cb) => ({ preSelectors, cb })),
+}));
+
+const testState = { some: 'arbitraryValue' };
+const testValue = 'my VALUE';
+
+describe('problem selectors unit tests', () => {
+ const {
+ problemState,
+ simpleSelectors,
+ } = selectors;
+ describe('problemState', () => {
+ it('returns the problem data', () => {
+ expect(problemState({ ...testState, problem: testValue })).toEqual(testValue);
+ });
+ });
+ describe('simpleSelectors', () => {
+ const testSimpleSelector = (key) => {
+ test(`${key} simpleSelector returns its value from the problem store`, () => {
+ const { preSelectors, cb } = simpleSelectors[key];
+ expect(preSelectors).toEqual([problemState]);
+ expect(cb({ ...testState, [key]: testValue })).toEqual(testValue);
+ });
+ };
+ const simpleKeys = keyStore(simpleSelectors);
+ describe('simple selectors link their values from problem store', () => {
+ [
+ simpleKeys.problemType,
+ simpleKeys.answers,
+ simpleKeys.correctAnswerCount,
+ simpleKeys.settings,
+ simpleKeys.question,
+ ].map(testSimpleSelector);
+ });
+ test('simple selector completeState equals the entire state', () => {
+ const { preSelectors, cb } = simpleSelectors[simpleKeys.completeState];
+ expect(preSelectors).toEqual([problemState]);
+ expect(cb({
+ ...testState,
+ [simpleKeys.completeState]: testValue,
+ })).toEqual({
+ ...testState,
+ [simpleKeys.completeState]: testValue,
+ });
+ });
+ });
+});
diff --git a/src/editors/data/redux/thunkActions/problem.js b/src/editors/data/redux/thunkActions/problem.js
index d1902d46f..82caf7928 100644
--- a/src/editors/data/redux/thunkActions/problem.js
+++ b/src/editors/data/redux/thunkActions/problem.js
@@ -1,31 +1,55 @@
import _ from 'lodash-es';
-import * as requests from './requests';
import { actions } from '..';
import { OLXParser } from '../../../containers/ProblemEditor/data/OLXParser';
import { parseSettings } from '../../../containers/ProblemEditor/data/SettingsParser';
+import { ProblemTypeKeys } from '../../constants/problem';
+import ReactStateOLXParser from '../../../containers/ProblemEditor/data/ReactStateOLXParser';
+import { blankProblemOLX } from '../../../containers/ProblemEditor/data/mockData/olxTestData';
+
+export const switchToAdvancedEditor = () => (dispatch, getState) => {
+ const state = getState();
+ const reactOLXParser = new ReactStateOLXParser({ problem: state.problem });
+ const rawOlx = reactOLXParser.buildOLX();
+ dispatch(actions.problem.updateField({ problemType: ProblemTypeKeys.ADVANCED, rawOlx }));
+};
+
+export const isBlankProblem = ({ rawOLX }) => {
+ if (rawOLX === blankProblemOLX.rawOLX) {
+ return true;
+ }
+ return false;
+};
+
+export const getDataFromOlx = ({ rawOLX, rawSettings }) => {
+ let olxParser;
+ let parsedProblem;
+ try {
+ olxParser = new OLXParser(rawOLX);
+ parsedProblem = olxParser.getParsedOLXData();
+ } catch {
+ console.error('The Problem Could Not Be Parsed from OLX. redirecting to Advanced editor.');
+ return { problemType: ProblemTypeKeys.ADVANCED, rawOLX, settings: parseSettings(rawSettings) };
+ }
+ if (parsedProblem?.problemType === ProblemTypeKeys.ADVANCED) {
+ return { problemType: ProblemTypeKeys.ADVANCED, rawOLX, settings: parseSettings(rawSettings) };
+ }
+ const { settings, ...data } = parsedProblem;
+ const parsedSettings = { ...settings, ...parseSettings(rawSettings) };
+ if (!_.isEmpty(rawOLX) && !_.isEmpty(data)) {
+ return { ...data, rawOLX, settings: parsedSettings };
+ }
+ return {};
+};
export const initializeProblem = (blockValue) => (dispatch) => {
const rawOLX = _.get(blockValue, 'data.data', {});
- const olxParser = new OLXParser(rawOLX);
+ const rawSettings = _.get(blockValue, 'data.metadata', {});
- const parsedProblem = olxParser.getParsedOLXData();
- if (_.isEmpty(parsedProblem)) {
- // if problem is blank, enable selection.
+ if (isBlankProblem({ rawOLX })) {
dispatch(actions.problem.setEnableTypeSelection());
+ } else {
+ dispatch(actions.problem.load(getDataFromOlx({ rawOLX, rawSettings })));
}
- const { settings, ...data } = parsedProblem;
- const parsedSettings = { ...settings, ...parseSettings(_.get(blockValue, 'data.metadata', {})) };
- if (!_.isEmpty(rawOLX) && !_.isEmpty(data)) {
- dispatch(actions.problem.load({ ...data, rawOLX, settings: parsedSettings }));
- }
- dispatch(requests.fetchAdvanceSettings({
- onSuccess: (response) => {
- console.log(response);
- if (response.data.allow_unsupported_xblocks.value) {
- console.log(response.allow_unsupported_xblocks.value);
- }
- },
- }));
};
-export default { initializeProblem };
+export default { initializeProblem, switchToAdvancedEditor };
diff --git a/src/editors/data/redux/thunkActions/problem.test.js b/src/editors/data/redux/thunkActions/problem.test.js
new file mode 100644
index 000000000..c48c82bd8
--- /dev/null
+++ b/src/editors/data/redux/thunkActions/problem.test.js
@@ -0,0 +1,51 @@
+import { actions } from '..';
+import { initializeProblem, switchToAdvancedEditor } from './problem';
+import { checkboxesOLXWithFeedbackAndHintsOLX, advancedProblemOlX, blankProblemOLX } from '../../../containers/ProblemEditor/data/mockData/olxTestData';
+import { ProblemTypeKeys } from '../../constants/problem';
+
+const mockOlx = 'SOmEVALue';
+const mockBuildOlx = jest.fn(() => mockOlx);
+jest.mock('../../../containers/ProblemEditor/data/ReactStateOLXParser', () => jest.fn().mockImplementation(() => ({ buildOLX: mockBuildOlx })));
+
+jest.mock('..', () => ({
+ actions: {
+ problem: {
+ load: () => {},
+ setEnableTypeSelection: () => {},
+ updateField: (args) => args,
+ },
+ },
+}));
+
+describe('problem thunkActions', () => {
+ let dispatch;
+ let getState;
+ beforeEach(() => {
+ dispatch = jest.fn((action) => ({ dispatch: action }));
+ getState = jest.fn(() => ({
+ problem: {
+ },
+ }));
+ });
+ test('initializeProblem visual Problem :', () => {
+ const blockValue = { data: { data: checkboxesOLXWithFeedbackAndHintsOLX.rawOLX } };
+ initializeProblem(blockValue)(dispatch);
+ expect(dispatch).toHaveBeenCalledWith(actions.problem.load());
+ });
+ test('initializeProblem advanced Problem', () => {
+ const blockValue = { data: { data: advancedProblemOlX.rawOLX } };
+ initializeProblem(blockValue)(dispatch);
+ expect(dispatch).toHaveBeenCalledWith(actions.problem.load());
+ });
+ test('initializeProblem blank Problem', () => {
+ const blockValue = { data: { data: blankProblemOLX.rawOLX } };
+ initializeProblem(blockValue)(dispatch);
+ expect(dispatch).toHaveBeenCalledWith(actions.problem.setEnableTypeSelection());
+ });
+ test('switchToAdvancedEditor visual Problem', () => {
+ switchToAdvancedEditor()(dispatch, getState);
+ expect(dispatch).toHaveBeenCalledWith(
+ actions.problem.updateField({ problemType: ProblemTypeKeys.ADVANCED, rawOlx: mockOlx }),
+ );
+ });
+});
diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js
index f36ecd86d..500ba856a 100644
--- a/src/editors/data/redux/thunkActions/video.js
+++ b/src/editors/data/redux/thunkActions/video.js
@@ -2,7 +2,7 @@ import { actions, selectors } from '..';
import { removeItemOnce } from '../../../utils';
import * as requests from './requests';
import * as module from './video';
-import { valueFromDuration } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/duration';
+import { valueFromDuration } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks';
import { parseYoutubeId } from '../../services/cms/api';
export const loadVideoData = () => (dispatch, getState) => {
@@ -11,10 +11,10 @@ export const loadVideoData = () => (dispatch, getState) => {
const courseLicenseData = state.app.courseDetails.data ? state.app.courseDetails.data : {};
const studioView = state.app.studioView?.data?.html;
const {
- videoSource,
videoId,
+ videoUrl,
fallbackVideos,
- } = module.determineVideoSource({
+ } = module.determineVideoSources({
edxVideoId: rawVideoData.edx_video_id,
youtubeId: rawVideoData.youtube_id_1_0,
html5Sources: rawVideoData.html5_sources,
@@ -27,7 +27,7 @@ export const loadVideoData = () => (dispatch, getState) => {
});
dispatch(actions.video.load({
- videoSource,
+ videoSource: videoUrl || '',
videoId,
fallbackVideos,
allowVideoDownloads: rawVideoData.download_video,
@@ -61,7 +61,7 @@ export const loadVideoData = () => (dispatch, getState) => {
allowThumbnailUpload: response.data.allowThumbnailUpload,
})),
}));
- const youTubeId = parseYoutubeId(videoSource);
+ const youTubeId = parseYoutubeId(videoUrl);
if (youTubeId) {
dispatch(requests.checkTranscriptsForImport({
videoId,
@@ -77,33 +77,23 @@ export const loadVideoData = () => (dispatch, getState) => {
}
};
-export const determineVideoSource = ({
+export const determineVideoSources = ({
edxVideoId,
youtubeId,
html5Sources,
}) => {
- // videoSource should be the edx_video_id, the youtube url or the first fallback url in that order.
- // If we are falling back to the first fallback url, remove it from the list of fallback urls for display.
const youtubeUrl = `https://youtu.be/${youtubeId}`;
- const videoId = edxVideoId || '';
- let videoSource = '';
- let fallbackVideos = [];
+ let videoUrl;
+ let fallbackVideos;
if (youtubeId) {
- // videoSource = youtubeUrl;
- // fallbackVideos = html5Sources;
- [videoSource, fallbackVideos] = [youtubeUrl, html5Sources];
- } else if (edxVideoId) {
- // fallbackVideos = html5Sources;
- fallbackVideos = html5Sources;
+ [videoUrl, fallbackVideos] = [youtubeUrl, html5Sources];
} else if (Array.isArray(html5Sources) && html5Sources[0]) {
- // videoSource = html5Sources[0];
- // fallbackVideos = html5Sources.slice(1);
- [videoSource, fallbackVideos] = [html5Sources[0], html5Sources.slice(1)];
+ [videoUrl, fallbackVideos] = [html5Sources[0], html5Sources.slice(1)];
}
return {
- videoSource,
- videoId,
- fallbackVideos,
+ videoId: edxVideoId || '',
+ videoUrl: videoUrl || '',
+ fallbackVideos: fallbackVideos || [],
};
};
@@ -343,7 +333,7 @@ export const replaceTranscript = ({ newFile, newFilename, language }) => (dispat
export default {
loadVideoData,
- determineVideoSource,
+ determineVideoSources,
parseLicense,
saveVideoData,
uploadThumbnail,
diff --git a/src/editors/data/redux/thunkActions/video.test.js b/src/editors/data/redux/thunkActions/video.test.js
index 895d4f425..eb28c61a2 100644
--- a/src/editors/data/redux/thunkActions/video.test.js
+++ b/src/editors/data/redux/thunkActions/video.test.js
@@ -99,8 +99,8 @@ describe('video thunkActions', () => {
let dispatchedAction1;
let dispatchedAction2;
beforeEach(() => {
- jest.spyOn(thunkActions, thunkActionsKeys.determineVideoSource).mockReturnValue({
- videoSource: 'videOsOurce',
+ jest.spyOn(thunkActions, thunkActionsKeys.determineVideoSources).mockReturnValue({
+ videoUrl: 'videOsOurce',
videoId: 'videOiD',
fallbackVideos: 'fALLbACKvIDeos',
});
@@ -176,69 +176,82 @@ describe('video thunkActions', () => {
}));
});
});
- describe('determineVideoSource', () => {
+ describe('determineVideoSources', () => {
const edxVideoId = 'EDxviDEoiD';
const youtubeId = 'yOuTuBEiD';
const youtubeUrl = `https://youtu.be/${youtubeId}`;
const html5Sources = ['htmLOne', 'hTMlTwo', 'htMLthrEE'];
+ describe('when edx id, youtube id and source values are null', () => {
+ it('returns empty strings for ids and an empty array for sources', () => {
+ expect(thunkActions.determineVideoSources({
+ edxVideoId: null,
+ youtubeId: null,
+ html5Sources: null,
+ })).toEqual({
+ videoUrl: '',
+ videoId: '',
+ fallbackVideos: [],
+ });
+ });
+ });
describe('when there is an edx video id, youtube id and html5 sources', () => {
- it('returns the youtube id for video source and html5 sources for fallback videos', () => {
- expect(thunkActions.determineVideoSource({
+ it('returns all three with the youtube id wrapped in url', () => {
+ expect(thunkActions.determineVideoSources({
edxVideoId,
youtubeId,
html5Sources,
})).toEqual({
- videoSource: youtubeUrl,
+ videoUrl: youtubeUrl,
videoId: edxVideoId,
fallbackVideos: html5Sources,
});
});
});
- describe('when there is an edx video id', () => {
+ describe('when there is only an edx video id', () => {
it('returns the edx video id for video source', () => {
- expect(thunkActions.determineVideoSource({
+ expect(thunkActions.determineVideoSources({
edxVideoId,
youtubeId: '',
html5Sources: '',
})).toEqual({
- videoSource: '',
+ videoUrl: '',
videoId: edxVideoId,
- fallbackVideos: '',
+ fallbackVideos: [],
});
});
});
describe('when there is no edx video id', () => {
it('returns the youtube url for video source and html5 sources for fallback videos', () => {
- expect(thunkActions.determineVideoSource({
+ expect(thunkActions.determineVideoSources({
edxVideoId: '',
youtubeId,
html5Sources,
})).toEqual({
- videoSource: youtubeUrl,
+ videoUrl: youtubeUrl,
videoId: '',
fallbackVideos: html5Sources,
});
});
});
describe('when there is no edx video id and no youtube id', () => {
- it('returns the first html5 source for video source and the rest for fallback videos', () => {
- expect(thunkActions.determineVideoSource({
+ it('returns the first html5 source for video url and the rest for fallback videos', () => {
+ expect(thunkActions.determineVideoSources({
edxVideoId: '',
youtubeId: '',
html5Sources,
})).toEqual({
- videoSource: 'htmLOne',
+ videoUrl: 'htmLOne',
videoId: '',
fallbackVideos: ['hTMlTwo', 'htMLthrEE'],
});
});
it('returns the html5 source for video source and an array with 2 empty values for fallback videos', () => {
- expect(thunkActions.determineVideoSource({
+ expect(thunkActions.determineVideoSources({
edxVideoId: '',
youtubeId: '',
html5Sources: ['htmlOne'],
})).toEqual({
- videoSource: 'htmlOne',
+ videoUrl: 'htmlOne',
videoId: '',
fallbackVideos: [],
});
@@ -246,12 +259,12 @@ describe('video thunkActions', () => {
});
describe('when there is no edx video id, no youtube id and no html5 sources', () => {
it('returns an empty string for video source and an array with 2 empty values for fallback videos', () => {
- expect(thunkActions.determineVideoSource({
+ expect(thunkActions.determineVideoSources({
edxVideoId: '',
youtubeId: '',
html5Sources: [],
})).toEqual({
- videoSource: '',
+ videoUrl: '',
videoId: '',
fallbackVideos: [],
});
diff --git a/src/editors/data/services/cms/api.js b/src/editors/data/services/cms/api.js
index 434df60f5..d666cb6aa 100644
--- a/src/editors/data/services/cms/api.js
+++ b/src/editors/data/services/cms/api.js
@@ -3,7 +3,7 @@ import * as urls from './urls';
import { get, post, deleteObject } from './utils';
import * as module from './api';
import * as mockApi from './mockApi';
-import { durationFromValue } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/duration';
+import { durationStringFromValue } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks';
export const apiMethods = {
fetchBlockById: ({ blockId, studioEndpointUrl }) => get(
@@ -157,7 +157,7 @@ export const apiMethods = {
youtubeId,
} = module.processVideoIds({
videoId: content.videoId,
- videoSource: content.videoSource,
+ videoUrl: content.videoSource,
fallbackVideos: content.fallbackVideos,
});
response = {
@@ -176,8 +176,8 @@ export const apiMethods = {
track: '', // TODO Downloadable Transcript URL. Backend expects a file name, for example: "something.srt"
show_captions: content.showTranscriptByDefault,
handout: content.handout,
- start_time: durationFromValue(content.duration.startTime),
- end_time: durationFromValue(content.duration.stopTime),
+ start_time: durationStringFromValue(content.duration.startTime),
+ end_time: durationStringFromValue(content.duration.stopTime),
license: module.processLicense(content.licenseType, content.licenseDetails),
},
};
@@ -217,21 +217,18 @@ export const loadImages = (rawImages) => camelizeKeys(rawImages).reduce(
export const processVideoIds = ({
videoId,
- videoSource,
+ videoUrl,
fallbackVideos,
- edxVideoId,
}) => {
- let newEdxVideoId = edxVideoId;
let youtubeId = '';
const html5Sources = [];
- // overwrite videoId if source is changed.
- if (module.isEdxVideo(videoId)) {
- newEdxVideoId = videoId;
- } else if (module.parseYoutubeId(videoSource)) {
- youtubeId = module.parseYoutubeId(videoSource);
- } else if (videoSource) {
- html5Sources.push(videoSource);
+ if (videoUrl) {
+ if (module.parseYoutubeId(videoUrl)) {
+ youtubeId = module.parseYoutubeId(videoUrl);
+ } else {
+ html5Sources.push(videoUrl);
+ }
}
if (fallbackVideos) {
@@ -239,7 +236,7 @@ export const processVideoIds = ({
}
return {
- edxVideoId: newEdxVideoId,
+ edxVideoId: videoId,
html5Sources,
youtubeId,
};
diff --git a/src/editors/data/services/cms/api.test.js b/src/editors/data/services/cms/api.test.js
index 6426f93d5..004f660dc 100644
--- a/src/editors/data/services/cms/api.test.js
+++ b/src/editors/data/services/cms/api.test.js
@@ -333,7 +333,8 @@ describe('cms api', () => {
});
describe('processVideoIds', () => {
const edxVideoId = 'eDXviDEoid';
- const youtubeId = 'yOuTuBeID';
+ const youtubeId = 'yOuTuBeUrL';
+ const youtubeUrl = `https://youtu.be/${youtubeId}`;
const html5Sources = [
'sOuRce1',
'sourCE2',
@@ -341,15 +342,14 @@ describe('cms api', () => {
afterEach(() => {
jest.restoreAllMocks();
});
- describe('if the videoSource is an edx video id', () => {
+ describe('if there is a video id', () => {
beforeEach(() => {
jest.spyOn(api, 'isEdxVideo').mockReturnValue(true);
- jest.spyOn(api, 'parseYoutubeId').mockReturnValue(null);
+ jest.spyOn(api, 'parseYoutubeId').mockReturnValue(youtubeId);
});
it('returns edxVideoId when there are no fallbackVideos', () => {
expect(api.processVideoIds({
- edxVideoId,
- videoSource: '',
+ videoUrl: '',
fallbackVideos: [],
videoId: edxVideoId,
})).toEqual({
@@ -360,42 +360,39 @@ describe('cms api', () => {
});
it('returns edxVideoId and html5Sources when there are fallbackVideos', () => {
expect(api.processVideoIds({
- edxVideoId,
- videoSource: 'edxVideoId',
+ videoUrl: youtubeUrl,
fallbackVideos: html5Sources,
videoId: edxVideoId,
})).toEqual({
edxVideoId,
html5Sources,
- youtubeId: '',
+ youtubeId,
});
});
});
- describe('if the videoSource is a youtube url', () => {
+ describe('if there is a youtube url', () => {
beforeEach(() => {
jest.spyOn(api, 'isEdxVideo').mockReturnValue(false);
jest.spyOn(api, 'parseYoutubeId').mockReturnValue(youtubeId);
});
it('returns youtubeId when there are no fallbackVideos', () => {
expect(api.processVideoIds({
- edxVideoId,
- videoSource: edxVideoId,
+ videoUrl: youtubeUrl,
fallbackVideos: [],
videoId: '',
})).toEqual({
- edxVideoId,
+ edxVideoId: '',
html5Sources: [],
youtubeId,
});
});
it('returns youtubeId and html5Sources when there are fallbackVideos', () => {
expect(api.processVideoIds({
- edxVideoId,
- videoSource: edxVideoId,
+ videoUrl: youtubeUrl,
fallbackVideos: html5Sources,
videoId: '',
})).toEqual({
- edxVideoId,
+ edxVideoId: '',
html5Sources,
youtubeId,
});
@@ -408,24 +405,22 @@ describe('cms api', () => {
});
it('returns html5Sources when there are no fallbackVideos', () => {
expect(api.processVideoIds({
- edxVideoId,
- videoSource: html5Sources[0],
+ videoUrl: html5Sources[0],
fallbackVideos: [],
videoId: '',
})).toEqual({
- edxVideoId,
+ edxVideoId: '',
html5Sources: [html5Sources[0]],
youtubeId: '',
});
});
it('returns html5Sources when there are fallbackVideos', () => {
expect(api.processVideoIds({
- edxVideoId,
- videoSource: html5Sources[0],
+ videoUrl: html5Sources[0],
fallbackVideos: [html5Sources[1]],
videoId: '',
})).toEqual({
- edxVideoId,
+ edxVideoId: '',
html5Sources,
youtubeId: '',
});
diff --git a/src/editors/data/services/cms/mockApi.js b/src/editors/data/services/cms/mockApi.js
index 94be91054..68dea813d 100644
--- a/src/editors/data/services/cms/mockApi.js
+++ b/src/editors/data/services/cms/mockApi.js
@@ -9,7 +9,8 @@ export const fetchBlockById = ({ blockId, studioEndpointUrl }) => {
let data = {};
if (blockId === 'html-block-id') {
data = {
- data: 'Test prompt content
',
+ data: `
+ `,
display_name: 'My Text Prompt',
metadata: {
display_name: 'Welcome!',
@@ -286,3 +287,7 @@ export const fetchStudioView = ({ blockId, studioEndpointUrl }) => {
},
});
};
+
+export const checkTranscriptsForImport = () => mockPromise({});
+
+export const uploadTranscript = () => mockPromise({});
diff --git a/src/editors/containers/TextEditor/components/CodeEditor/__snapshots__/index.test.jsx.snap b/src/editors/sharedComponents/CodeEditor/__snapshots__/index.test.jsx.snap
similarity index 100%
rename from src/editors/containers/TextEditor/components/CodeEditor/__snapshots__/index.test.jsx.snap
rename to src/editors/sharedComponents/CodeEditor/__snapshots__/index.test.jsx.snap
diff --git a/src/editors/containers/TextEditor/components/CodeEditor/constants.js b/src/editors/sharedComponents/CodeEditor/constants.js
similarity index 100%
rename from src/editors/containers/TextEditor/components/CodeEditor/constants.js
rename to src/editors/sharedComponents/CodeEditor/constants.js
diff --git a/src/editors/sharedComponents/CodeEditor/hooks.js b/src/editors/sharedComponents/CodeEditor/hooks.js
new file mode 100644
index 000000000..3c99b0f1e
--- /dev/null
+++ b/src/editors/sharedComponents/CodeEditor/hooks.js
@@ -0,0 +1,71 @@
+import React, { useEffect } from 'react';
+
+import { basicSetup } from 'codemirror';
+import { EditorState } from '@codemirror/state';
+import { EditorView } from '@codemirror/view';
+import { html } from '@codemirror/lang-html';
+import { xml } from '@codemirror/lang-xml';
+
+import alphanumericMap from './constants';
+import './index.scss';
+
+const CODEMIRROR_LANGUAGES = { HTML: 'html', XML: 'xml' };
+
+export const state = {
+ showBtnEscapeHTML: (val) => React.useState(val),
+};
+
+export const prepareShowBtnEscapeHTML = () => {
+ const [visibility, setVisibility] = state.showBtnEscapeHTML(true);
+ const hide = () => setVisibility(false);
+ return { showBtnEscapeHTML: visibility, hideBtn: hide };
+};
+
+export const cleanHTML = ({ initialText }) => {
+ const translateRegex = new RegExp(`&(${Object.keys(alphanumericMap).join('|')});`, 'g');
+ const translator = ($0, $1) => alphanumericMap[$1];
+ return initialText.replace(translateRegex, translator);
+};
+
+export const createCodeMirrorDomNode = ({
+ ref,
+ initialText,
+ upstreamRef,
+ lang,
+}) => {
+ useEffect(() => {
+ const languageExtension = lang === CODEMIRROR_LANGUAGES.HTML ? html() : xml();
+ const cleanText = cleanHTML({ initialText });
+ const newState = EditorState.create({
+ doc: cleanText,
+ extensions: [basicSetup, languageExtension, EditorView.lineWrapping],
+ });
+ const view = new EditorView({ state: newState, parent: ref.current });
+ // eslint-disable-next-line no-param-reassign
+ upstreamRef.current = view;
+ view.focus();
+
+ return () => {
+ // called on cleanup
+ view.destroy();
+ };
+ }, []);
+};
+
+export const escapeHTMLSpecialChars = ({ ref, hideBtn }) => {
+ const text = ref.current.state.doc.toString();
+ let pos = 0;
+ const changes = [];
+ Object.keys(alphanumericMap).forEach(
+ (escapedKeyword) => {
+ // eslint-disable-next-line no-cond-assign
+ for (let next; (next = text.indexOf(alphanumericMap[escapedKeyword], pos)) > -1;) {
+ changes.push({ from: next, to: next + 1, insert: `&${escapedKeyword};` });
+ pos = next + 1;
+ }
+ },
+ );
+
+ ref.current.dispatch({ changes });
+ hideBtn();
+};
diff --git a/src/editors/sharedComponents/CodeEditor/index.jsx b/src/editors/sharedComponents/CodeEditor/index.jsx
new file mode 100644
index 000000000..49155ae92
--- /dev/null
+++ b/src/editors/sharedComponents/CodeEditor/index.jsx
@@ -0,0 +1,55 @@
+import React, { useRef } from 'react';
+import PropTypes from 'prop-types';
+
+import {
+ Button,
+} from '@edx/paragon';
+
+import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import messages from './messages';
+import './index.scss';
+
+import * as hooks from './hooks';
+
+export const CodeEditor = ({
+ innerRef,
+ value,
+ lang,
+ // injected
+ intl,
+}) => {
+ const DOMref = useRef();
+ const btnRef = useRef();
+ hooks.createCodeMirrorDomNode({
+ ref: DOMref, initialText: value, upstreamRef: innerRef, lang,
+ });
+ const { showBtnEscapeHTML, hideBtn } = hooks.prepareShowBtnEscapeHTML();
+
+ return (
+
+
+ {showBtnEscapeHTML && (
+
hooks.escapeHTMLSpecialChars({ ref: innerRef, hideBtn })}
+ >
+
+
+ )}
+
+ );
+};
+
+CodeEditor.propTypes = {
+ innerRef: PropTypes.oneOfType([
+ PropTypes.func,
+ PropTypes.shape({ current: PropTypes.any }),
+ ]).isRequired,
+ value: PropTypes.string.isRequired,
+ intl: intlShape.isRequired,
+ lang: PropTypes.string.isRequired,
+};
+
+export default injectIntl(CodeEditor);
diff --git a/src/editors/containers/TextEditor/components/CodeEditor/index.scss b/src/editors/sharedComponents/CodeEditor/index.scss
similarity index 100%
rename from src/editors/containers/TextEditor/components/CodeEditor/index.scss
rename to src/editors/sharedComponents/CodeEditor/index.scss
diff --git a/src/editors/containers/TextEditor/components/CodeEditor/index.test.jsx b/src/editors/sharedComponents/CodeEditor/index.test.jsx
similarity index 81%
rename from src/editors/containers/TextEditor/components/CodeEditor/index.test.jsx
rename to src/editors/sharedComponents/CodeEditor/index.test.jsx
index 918818b99..7004a25ab 100644
--- a/src/editors/containers/TextEditor/components/CodeEditor/index.test.jsx
+++ b/src/editors/sharedComponents/CodeEditor/index.test.jsx
@@ -4,9 +4,10 @@ import { shallow } from 'enzyme';
import { EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { html } from '@codemirror/lang-html';
-import { formatMessage, MockUseState } from '../../../../../testUtils';
+import { formatMessage, MockUseState } from '../../../testUtils';
import alphanumericMap from './constants';
import * as module from './index';
+import * as hooks from './hooks';
jest.mock('@codemirror/view');
@@ -28,11 +29,15 @@ jest.mock('@codemirror/lang-html', () => ({
html: jest.fn(),
}));
+jest.mock('@codemirror/lang-xml', () => ({
+ xml: jest.fn(),
+}));
+
jest.mock('codemirror', () => ({
basicSetup: 'bAsiCSetUp',
}));
-const state = new MockUseState(module.hooks);
+const state = new MockUseState(hooks);
describe('CodeEditor', () => {
describe('Hooks', () => {
@@ -45,7 +50,7 @@ describe('CodeEditor', () => {
state.mock();
});
it('prepareShowBtnEscapeHTML', () => {
- const hook = module.hooks.prepareShowBtnEscapeHTML();
+ const hook = hooks.prepareShowBtnEscapeHTML();
expect(state.stateVals.showBtnEscapeHTML).toEqual(hook.showBtnEscapeHTML);
hook.hideBtn();
expect(state.setState.showBtnEscapeHTML).toHaveBeenCalledWith(false);
@@ -58,7 +63,7 @@ describe('CodeEditor', () => {
const cleanText = `${Object.values(alphanumericMap).join(' , ')}`;
it('escapes alphanumerics and sets them to be literals', () => {
- expect(module.hooks.cleanHTML({ initialText: dirtyText })).toEqual(cleanText);
+ expect(hooks.cleanHTML({ initialText: dirtyText })).toEqual(cleanText);
});
});
@@ -79,7 +84,7 @@ describe('CodeEditor', () => {
};
const mockHideBtn = jest.fn();
it('unescapes literals and sets them to be alphanumerics', () => {
- module.hooks.escapeHTMLSpecialChars({ ref, hideBtn: mockHideBtn });
+ hooks.escapeHTMLSpecialChars({ ref, hideBtn: mockHideBtn });
expect(mockDispatch).toHaveBeenCalled();
expect(mockHideBtn).toHaveBeenCalled();
});
@@ -90,13 +95,14 @@ describe('CodeEditor', () => {
ref: {
current: 'sOmEvAlUe',
},
+ lang: 'html',
initialText: 'sOmEhTmL',
upstreamRef: {
current: 'sOmEotHERvAlUe',
},
};
beforeEach(() => {
- module.hooks.createCodeMirrorDomNode(props);
+ hooks.createCodeMirrorDomNode(props);
});
it('calls useEffect and sets up codemirror objects', () => {
const [cb, prereqs] = React.useEffect.mock.calls[0];
@@ -118,18 +124,19 @@ describe('CodeEditor', () => {
innerRef: {
current: 'sOmEvALUE',
},
+ lang: 'html',
value: 'mOcKhTmL',
};
- jest.spyOn(module.hooks, 'createCodeMirrorDomNode').mockImplementation(() => ({}));
+ jest.spyOn(hooks, 'createCodeMirrorDomNode').mockImplementation(() => ({}));
});
afterAll(() => {
jest.clearAllMocks();
});
test('Renders and calls Hooks ', () => {
- jest.spyOn(module.hooks, 'prepareShowBtnEscapeHTML').mockImplementation(() => ({ showBtnEscapeHTML: true, hideBtn: mockHideBtn }));
+ jest.spyOn(hooks, 'prepareShowBtnEscapeHTML').mockImplementation(() => ({ showBtnEscapeHTML: true, hideBtn: mockHideBtn }));
// Note: ref won't show up as it is not acutaly a DOM attribute.
expect(shallow( )).toMatchSnapshot();
- expect(module.hooks.createCodeMirrorDomNode).toHaveBeenCalled();
+ expect(hooks.createCodeMirrorDomNode).toHaveBeenCalled();
});
});
});
diff --git a/src/editors/containers/TextEditor/components/CodeEditor/messages.js b/src/editors/sharedComponents/CodeEditor/messages.js
similarity index 100%
rename from src/editors/containers/TextEditor/components/CodeEditor/messages.js
rename to src/editors/sharedComponents/CodeEditor/messages.js
diff --git a/src/editors/containers/TextEditor/components/RawEditor/__snapshots__/index.test.jsx.snap b/src/editors/sharedComponents/RawEditor/__snapshots__/index.test.jsx.snap
similarity index 72%
rename from src/editors/containers/TextEditor/components/RawEditor/__snapshots__/index.test.jsx.snap
rename to src/editors/sharedComponents/RawEditor/__snapshots__/index.test.jsx.snap
index 32899800f..056066354 100644
--- a/src/editors/containers/TextEditor/components/RawEditor/__snapshots__/index.test.jsx.snap
+++ b/src/editors/sharedComponents/RawEditor/__snapshots__/index.test.jsx.snap
@@ -1,18 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RawEditor renders as expected with default behavior 1`] = `
-
+
- You are using the raw HTML editor.
+ You are using the raw
+ html
+ editor.
diff --git a/src/editors/sharedComponents/RawEditor/index.jsx b/src/editors/sharedComponents/RawEditor/index.jsx
new file mode 100644
index 000000000..05bbc9401
--- /dev/null
+++ b/src/editors/sharedComponents/RawEditor/index.jsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Alert } from '@edx/paragon';
+
+import CodeEditor from '../CodeEditor';
+
+function getValue(content) {
+ if (!content) { return null; }
+ if (typeof content === 'string') { return content; }
+ return content.data?.data;
+}
+
+export const RawEditor = ({
+ editorRef,
+ content,
+ lang,
+}) => {
+ const value = getValue(content);
+
+ return (
+
+
+ You are using the raw {lang} editor.
+
+ { value ? (
+
+ ) : null}
+
+
+ );
+};
+RawEditor.defaultProps = {
+ editorRef: null,
+ content: null,
+ lang: 'html',
+};
+RawEditor.propTypes = {
+ editorRef: PropTypes.oneOfType([
+ PropTypes.func,
+ PropTypes.shape({ current: PropTypes.any }),
+ ]),
+ content: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.shape({
+ data: PropTypes.shape({ data: PropTypes.string }),
+ }),
+ ]),
+ lang: PropTypes.string,
+};
+
+export default RawEditor;
diff --git a/src/editors/containers/TextEditor/components/RawEditor/index.test.jsx b/src/editors/sharedComponents/RawEditor/index.test.jsx
similarity index 87%
rename from src/editors/containers/TextEditor/components/RawEditor/index.test.jsx
rename to src/editors/sharedComponents/RawEditor/index.test.jsx
index 7c403ed07..868f7b47d 100644
--- a/src/editors/containers/TextEditor/components/RawEditor/index.test.jsx
+++ b/src/editors/sharedComponents/RawEditor/index.test.jsx
@@ -10,7 +10,7 @@ describe('RawEditor', () => {
value: 'Ref Value',
},
},
- text: { data: { data: 'eDiTablE Text' } },
+ content: { data: { data: 'eDiTablE Text' } },
};
test('renders as expected with default behavior', () => {
expect(shallow(
)).toMatchSnapshot();
diff --git a/src/index.scss b/src/index.scss
index c4301db28..e69de29bb 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -1 +0,0 @@
-/* styles go here */
diff --git a/src/setupTest.js b/src/setupTest.js
index cc8c8f7d3..93f427002 100644
--- a/src/setupTest.js
+++ b/src/setupTest.js
@@ -80,6 +80,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
Footer: 'Card.Footer',
Body: 'Card.Body',
},
+ Col: 'Col',
Container: 'Container',
Dropdown: {
Item: 'Dropdown.Item',
diff --git a/www/package-lock.json b/www/package-lock.json
index e1efceca8..a251014fc 100644
--- a/www/package-lock.json
+++ b/www/package-lock.json
@@ -14,7 +14,7 @@
"@edx/frontend-build": "^11.0.0",
"@edx/frontend-lib-content-components": "file:..",
"@edx/frontend-platform": "2.5.1",
- "@edx/paragon": "20.13.0",
+ "@edx/paragon": "^20.27.0",
"core-js": "^3.21.1",
"dotenv": "^16.0.0",
"prop-types": "^15.5.10",
@@ -26,10 +26,12 @@
}
},
"..": {
+ "name": "@edx/frontend-lib-content-components",
"version": "1.0.0-semantically-released",
"license": "AGPL-3.0",
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
+ "@codemirror/lang-xml": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@reduxjs/toolkit": "^1.8.1",
@@ -55,7 +57,7 @@
"devDependencies": {
"@edx/frontend-build": "^11.0.2",
"@edx/frontend-platform": "2.4.0",
- "@edx/paragon": "^20.21.3",
+ "@edx/paragon": "^20.27.0",
"@testing-library/dom": "^8.13.0",
"@testing-library/react": "12.1.1",
"@testing-library/user-event": "^13.5.0",
@@ -1986,9 +1988,9 @@
}
},
"node_modules/@edx/paragon": {
- "version": "20.13.0",
- "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.13.0.tgz",
- "integrity": "sha512-Zp4nU3YwGviapT9P77I2KV2HSV/5wSip/k2MHPZO235P5usmsJ4zG5UaIkD7X9ciYB3JPrTBfSP05FU2/k2o2g==",
+ "version": "20.27.0",
+ "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.27.0.tgz",
+ "integrity": "sha512-jy62ZEBdAVlsP6tAm1/YDyMtc9fiD47H00whoW+y2Z+lLZqPsv6D5boIPQIcdBeg0W4f2gCU4TEy2+b2q8mYGA==",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
@@ -2012,7 +2014,8 @@
"react-table": "^7.7.0",
"react-transition-group": "^4.4.2",
"tabbable": "^5.3.3",
- "uncontrollable": "^7.2.1"
+ "uncontrollable": "^7.2.1",
+ "uuid": "^9.0.0"
},
"peerDependencies": {
"react": "^16.8.6 || ^17.0.0",
@@ -2057,6 +2060,14 @@
"node": ">=10"
}
},
+ "node_modules/@edx/paragon/node_modules/uuid": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
+ "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/@formatjs/ecma402-abstract": {
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
@@ -22117,11 +22128,12 @@
"version": "file:..",
"requires": {
"@codemirror/lang-html": "^6.0.0",
+ "@codemirror/lang-xml": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@edx/frontend-build": "^11.0.2",
"@edx/frontend-platform": "2.4.0",
- "@edx/paragon": "^20.21.3",
+ "@edx/paragon": "^20.27.0",
"@reduxjs/toolkit": "^1.8.1",
"@testing-library/dom": "^8.13.0",
"@testing-library/react": "12.1.1",
@@ -22219,9 +22231,9 @@
}
},
"@edx/paragon": {
- "version": "20.13.0",
- "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.13.0.tgz",
- "integrity": "sha512-Zp4nU3YwGviapT9P77I2KV2HSV/5wSip/k2MHPZO235P5usmsJ4zG5UaIkD7X9ciYB3JPrTBfSP05FU2/k2o2g==",
+ "version": "20.27.0",
+ "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.27.0.tgz",
+ "integrity": "sha512-jy62ZEBdAVlsP6tAm1/YDyMtc9fiD47H00whoW+y2Z+lLZqPsv6D5boIPQIcdBeg0W4f2gCU4TEy2+b2q8mYGA==",
"requires": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
@@ -22245,7 +22257,8 @@
"react-table": "^7.7.0",
"react-transition-group": "^4.4.2",
"tabbable": "^5.3.3",
- "uncontrollable": "^7.2.1"
+ "uncontrollable": "^7.2.1",
+ "uuid": "^9.0.0"
},
"dependencies": {
"brace-expansion": {
@@ -22275,6 +22288,11 @@
"requires": {
"brace-expansion": "^2.0.1"
}
+ },
+ "uuid": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
+ "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
}
}
},
diff --git a/www/package.json b/www/package.json
index 15fc6aaa6..fc163e96c 100644
--- a/www/package.json
+++ b/www/package.json
@@ -15,7 +15,7 @@
"@edx/frontend-build": "^11.0.0",
"@edx/frontend-lib-content-components": "file:..",
"@edx/frontend-platform": "2.5.1",
- "@edx/paragon": "20.13.0",
+ "@edx/paragon": "^20.27.0",
"core-js": "^3.21.1",
"dotenv": "^16.0.0",
"prop-types": "^15.5.10",