diff --git a/package-lock.json b/package-lock.json index 474be4816..c14e8dae0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "devDependencies": { "@edx/frontend-build": "^11.0.2", "@edx/frontend-platform": "2.4.0", - "@edx/paragon": "^20.13.0", + "@edx/paragon": "^20.18.0", "@testing-library/dom": "^8.13.0", "@testing-library/react": "12.1.1", "@testing-library/user-event": "^13.5.0", @@ -2329,9 +2329,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.18.0", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.18.0.tgz", + "integrity": "sha512-8J7iDNjX7MPfLUWWuUU6K/ZwBojuvfdycOF16aV1+Kb2xg08E8HhevsHPevlXVjX7d6o4hTdlPZAvOlPFdxHVQ==", "dev": true, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.1.1", @@ -26408,9 +26408,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.18.0", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.18.0.tgz", + "integrity": "sha512-8J7iDNjX7MPfLUWWuUU6K/ZwBojuvfdycOF16aV1+Kb2xg08E8HhevsHPevlXVjX7d6o4hTdlPZAvOlPFdxHVQ==", "dev": true, "requires": { "@fortawesome/fontawesome-svg-core": "^6.1.1", diff --git a/package.json b/package.json index b50708552..a0249e7e7 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "devDependencies": { "@edx/frontend-build": "^11.0.2", "@edx/frontend-platform": "2.4.0", - "@edx/paragon": "^20.13.0", + "@edx/paragon": "^20.18.0", "@testing-library/dom": "^8.13.0", "@testing-library/react": "12.1.1", "@testing-library/user-event": "^13.5.0", diff --git a/src/editors/containers/VideoEditor/__snapshots__/index.test.jsx.snap b/src/editors/containers/VideoEditor/__snapshots__/index.test.jsx.snap index be2090bb2..4228e5ac6 100644 --- a/src/editors/containers/VideoEditor/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/VideoEditor/__snapshots__/index.test.jsx.snap @@ -1,6 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`VideoEditor snapshots renders as expected with default behavior 1`] = ` + + + + + +`; + +exports[`VideoEditor snapshots renders as expected with default behavior 2`] = ` diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget.jsx deleted file mode 100644 index f32840b84..000000000 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { useDispatch } from 'react-redux'; -// import PropTypes from 'prop-types'; - -import hooks from './hooks'; -import CollapsibleFormWidget from './CollapsibleFormWidget'; - -/** - * Collapsible Form widget controlling videe licence type and details - */ -export const LicenseWidget = () => { - const dispatch = useDispatch(); - const { licenseType, licenseDetails } = hooks.widgetValues({ - dispatch, - fields: { - [hooks.selectorKeys.licenseType]: hooks.genericWidget, - [hooks.selectorKeys.licenseDetails]: hooks.objectWidget, - }, - }); - return ( - -
License Widget
-

License Type: {licenseType.formValue}

-

Attribution: {licenseDetails.formValue.attribution ? 'True' : 'False'}

-

Non-Commercial: {licenseDetails.formValue.noCommercial ? 'True' : 'False'}

-

No-Derivatives: {licenseDetails.formValue.noDerivatives ? 'True' : 'False'}

-

Share-Alike: {licenseDetails.formValue.shareAlike ? 'True' : 'False'}

-
- ); -}; - -export default LicenseWidget; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseBlurb.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseBlurb.jsx new file mode 100644 index 000000000..7cfa2a553 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseBlurb.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + FormattedMessage, + injectIntl, +} from '@edx/frontend-platform/i18n'; +import { Form, Icon } from '@edx/paragon'; +import { + Attribution, + Copyright, + Cc, + Nd, + Nc, + Sa, +} from '@edx/paragon/icons'; + +import messages from './messages'; +import { LicenseTypes } from '../../../../../../data/constants/licenses'; + +export const LicenseBlurb = ({ + license, + details, +}) => ( +
+ {license === LicenseTypes.allRightsReserved ? : null} + {license === LicenseTypes.creativeCommons ? : null} + {details.attribution ? : null} + {details.noncommercial ? : null} + {details.noDerivatives ? : null} + {details.shareAlike ? : null} + {license === LicenseTypes.allRightsReserved + ? + : null} + {license === LicenseTypes.creativeCommons + ? + : null} +
+); + +LicenseBlurb.propTypes = { + license: PropTypes.string.isRequired, + details: PropTypes.shape({ + attribution: PropTypes.bool.isRequired, + noncommercial: PropTypes.bool.isRequired, + noDerivatives: PropTypes.bool.isRequired, + shareAlike: PropTypes.bool.isRequired, + }).isRequired, +}; + +export default injectIntl(LicenseBlurb); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseBlurb.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseBlurb.test.jsx new file mode 100644 index 000000000..4b4d87970 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseBlurb.test.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { LicenseBlurb } from './LicenseBlurb'; + +describe('LicenseBlurb', () => { + const props = { + license: 'all-rights-reserved', + details: {}, + }; + + describe('snapshots', () => { + test('snapshots: renders as expected with default props', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with license equal to Creative Commons', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected when details.attribution equal true', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected when details.attribution and details.noncommercial equal true', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected when details.attribution and details.noDerivatives equal true', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected when details.attribution and details.shareAlike equal true', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDetails.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDetails.jsx new file mode 100644 index 000000000..83ceb8549 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDetails.jsx @@ -0,0 +1,198 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { + FormattedMessage, + injectIntl, +} from '@edx/frontend-platform/i18n'; +import { + Card, + Form, + Icon, + Stack, +} from '@edx/paragon'; +import { + Attribution, + Nd, + Sa, + Nc, +} from '@edx/paragon/icons'; + +import { actions } from '../../../../../../data/redux'; +import { LicenseLevel, LicenseTypes } from '../../../../../../data/constants/licenses'; +import { messages } from './messages'; + +export const LicenseDetails = ({ + license, + details, + level, + // redux + updateField, +}) => ( + level === LicenseLevel.block && details && license !== 'select' ? ( +
+ + + + + + {license === LicenseTypes.allRightsReserved + ? ( + + + + ) + : null} + + {license === LicenseTypes.creativeCommons + ? ( + + + + + +
+ )} + actions={} + /> + + + + + + updateField({ + licenseDetails: { + ...details, + noncommercial: !details.noncommercial, + }, + })} + > + + + + + )} + actions={( + updateField({ + licenseDetails: { + ...details, + noncommercial: e.target.checked, + }, + })} + /> + )} + /> + + + + + + updateField({ + licenseDetails: { + ...details, + noDerivatives: !details.noDerivatives, + shareAlike: !details.noDerivatives ? false : details.shareAlike, + }, + })} + > + + + + + )} + actions={( + updateField({ + licenseDetails: { + ...details, + noDerivatives: e.target.checked, + shareAlike: e.target.checked ? false : details.shareAlike, + }, + })} + /> + )} + /> + + + + + + updateField({ + licenseDetails: { + ...details, + shareAlike: !details.shareAlike, + noDerivatives: !details.shareAlike ? false : details.noDerivatives, + }, + })} + > + + + + + )} + actions={( + updateField({ + licenseDetails: { + ...details, + shareAlike: e.target.checked, + noDerivatives: e.target.checked ? false : details.noDerivatives, + }, + })} + /> + )} + /> + + + + + + ) + : null} + + + + ) : null +); + +LicenseDetails.propTypes = { + license: PropTypes.string.isRequired, + details: PropTypes.shape({ + attribution: PropTypes.bool.isRequired, + noncommercial: PropTypes.bool.isRequired, + noDerivatives: PropTypes.bool.isRequired, + shareAlike: PropTypes.bool.isRequired, + }).isRequired, + level: PropTypes.string.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)(LicenseDetails)); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDetails.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDetails.test.jsx new file mode 100644 index 000000000..09ae9183b --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDetails.test.jsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { actions } from '../../../../../../data/redux'; +import { LicenseDetails, mapStateToProps, mapDispatchToProps } from './LicenseDetails'; + +jest.mock('react', () => { + const updateState = jest.fn(); + return { + ...jest.requireActual('react'), + updateState, + useContext: jest.fn(() => ({ license: ['error.license', jest.fn().mockName('error.setLicense')] })), + }; +}); + +jest.mock('../../../../../../data/redux', () => ({ + actions: { + video: { + updateField: jest.fn().mockName('actions.video.updateField'), + }, + }, +})); + +describe('LicenseDetails', () => { + const props = { + license: null, + details: {}, + level: 'course', + updateField: jest.fn().mockName('args.updateField'), + }; + + describe('snapshots', () => { + test('snapshots: renders as expected with default props', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with level set to library', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with level set to block and license set to select', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with level set to block and license set to all rights reserved', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with level set to block and license set to Creative Commons', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + }); + describe('mapStateToProps', () => { + const testState = { A: 'pple', B: 'anana', C: 'ucumber' }; + test('mapStateToProps equals an empty object', () => { + expect(mapStateToProps(testState)).toEqual({}); + }); + }); + describe('mapDispatchToProps', () => { + const dispatch = jest.fn(); + test('updateField from actions.video.updateField', () => { + expect(mapDispatchToProps.updateField).toEqual(dispatch(actions.video.updateField)); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDisplay.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDisplay.jsx new file mode 100644 index 000000000..21e41b46b --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDisplay.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + FormattedMessage, + injectIntl, +} from '@edx/frontend-platform/i18n'; +import { + Card, + Stack, + Hyperlink, +} from '@edx/paragon'; + +import { LicenseLevel, LicenseTypes } from '../../../../../../data/constants/licenses'; + +import LicenseBlurb from './LicenseBlurb'; +import { messages } from './messages'; + +export const LicenseDisplay = ({ + license, + details, + licenseDescription, + level, +}) => { + if (license !== LicenseTypes.select) { + return ( + + + + } /> + {licenseDescription} + + {level !== LicenseLevel.course ? ( + + + + ) : null } + + ); + } + return null; +}; + +LicenseDisplay.propTypes = { + license: PropTypes.string.isRequired, + details: PropTypes.shape({ + attribution: PropTypes.bool.isRequired, + noncommercial: PropTypes.bool.isRequired, + noDerivatives: PropTypes.bool.isRequired, + shareAlike: PropTypes.bool.isRequired, + }).isRequired, + level: PropTypes.string.isRequired, + licenseDescription: PropTypes.func.isRequired, +}; + +export default injectIntl(LicenseDisplay); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDisplay.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDisplay.test.jsx new file mode 100644 index 000000000..6f4535067 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDisplay.test.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { LicenseDisplay } from './LicenseDisplay'; + +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn(() => ({ license: ['error.license', jest.fn().mockName('error.setLicense')] })), +})); + +describe('LicenseDisplay', () => { + const props = { + license: 'all-rights-reserved', + details: {}, + licenseDescription: 'FormattedMessage component with license description', + level: 'course', + }; + + describe('snapshots', () => { + test('snapshots: renders as expected with default props', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with level set to library', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with level set to block', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with level set to block and license set to select', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with level set to block and license set to Creative Commons', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseSelector.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseSelector.jsx new file mode 100644 index 000000000..5e54c381f --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseSelector.jsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { connect, useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import { + FormattedMessage, + injectIntl, + intlShape, +} from '@edx/frontend-platform/i18n'; +import { + Form, + Icon, + IconButtonWithTooltip, +} from '@edx/paragon'; +import { Delete } from '@edx/paragon/icons'; + +import { actions, selectors } from '../../../../../../data/redux'; +import hooks from './hooks'; +import messages from './messages'; +import { LicenseLevel, LicenseNames, LicenseTypes } from '../../../../../../data/constants/licenses'; + +export const LicenseSelector = ({ + license, + level, + // injected + intl, + // redux + courseLicenseType, + updateField, +}) => { + const { levelDescription } = hooks.determineText({ level }); + const onLicenseChange = hooks.onSelectLicense({ dispatch: useDispatch() }); + const ref = React.useRef(); + return ( + + + onLicenseChange(e.target.value)} + > + {Object.entries(LicenseNames).map(([key, text]) => { + if (license === key) { return (); } + if (key === LicenseTypes.select) { return (); } + return (); + })} + + {level === LicenseLevel.block ? ( + { + ref.current.value = courseLicenseType; + updateField({ licenseType: '', licenseDetails: {} }); + }} + tooltipPlacement="top" + tooltipContent={} + /> + ) : null } + + {levelDescription} + + ); +}; + +LicenseSelector.propTypes = { + license: PropTypes.string.isRequired, + level: PropTypes.string.isRequired, + // injected + intl: intlShape.isRequired, + // redux + courseLicenseType: PropTypes.string.isRequired, + updateField: PropTypes.func.isRequired, +}; + +export const mapStateToProps = (state) => ({ + courseLicenseType: selectors.video.courseLicenseType(state), +}); + +export const mapDispatchToProps = (dispatch) => ({ + updateField: (stateUpdate) => dispatch(actions.video.updateField(stateUpdate)), +}); + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LicenseSelector)); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseSelector.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseSelector.test.jsx new file mode 100644 index 000000000..e49fc14a8 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseSelector.test.jsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { formatMessage } from '../../../../../../../testUtils'; +import { actions, selectors } from '../../../../../../data/redux'; +import { LicenseSelector, mapStateToProps, mapDispatchToProps } from './LicenseSelector'; + +jest.mock('react', () => { + const updateState = jest.fn(); + return { + ...jest.requireActual('react'), + updateState, + useContext: jest.fn(() => ({ license: ['error.license', jest.fn().mockName('error.setLicense')] })), + }; +}); + +jest.mock('react-redux', () => { + const dispatchFn = jest.fn(); + return { + ...jest.requireActual('react-redux'), + dispatch: dispatchFn, + useDispatch: jest.fn(() => dispatchFn), + }; +}); + +jest.mock('../../../../../../data/redux', () => ({ + actions: { + video: { + updateField: jest.fn().mockName('actions.video.updateField'), + }, + }, + selectors: { + video: { + courseLicenseType: jest.fn(state => ({ courseLicenseType: state })), + }, + }, +})); + +describe('LicenseSelector', () => { + const props = { + intl: { formatMessage }, + license: 'all-rights-reserved', + level: 'course', + courseLicenseType: 'all-rights-reserved', + updateField: jest.fn().mockName('args.updateField'), + }; + describe('snapshots', () => { + test('snapshots: renders as expected with default props', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with library level', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with block level', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with no license', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + }); + describe('mapStateToProps', () => { + const testState = { A: 'pple', B: 'anana', C: 'ucumber' }; + test('courseLicenseType from video.courseLicenseType', () => { + expect( + mapStateToProps(testState).courseLicenseType, + ).toEqual(selectors.video.courseLicenseType(testState)); + }); + }); + describe('mapDispatchToProps', () => { + const dispatch = jest.fn(); + test('updateField from actions.video.updateField', () => { + expect(mapDispatchToProps.updateField).toEqual(dispatch(actions.video.updateField)); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseBlurb.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseBlurb.test.jsx.snap new file mode 100644 index 000000000..43b363547 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseBlurb.test.jsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LicenseBlurb snapshots snapshots: renders as expected when details.attribution and details.noDerivatives equal true 1`] = ` +
+ + + + + + +
+`; + +exports[`LicenseBlurb snapshots snapshots: renders as expected when details.attribution and details.noncommercial equal true 1`] = ` +
+ + + + + + +
+`; + +exports[`LicenseBlurb snapshots snapshots: renders as expected when details.attribution and details.shareAlike equal true 1`] = ` +
+ + + + + + +
+`; + +exports[`LicenseBlurb snapshots snapshots: renders as expected when details.attribution equal true 1`] = ` +
+ + + + + +
+`; + +exports[`LicenseBlurb snapshots snapshots: renders as expected with default props 1`] = ` +
+ + + + +
+`; + +exports[`LicenseBlurb snapshots snapshots: renders as expected with license equal to Creative Commons 1`] = ` +
+ + + + +
+`; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseDetails.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseDetails.test.jsx.snap new file mode 100644 index 000000000..4ac890f39 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseDetails.test.jsx.snap @@ -0,0 +1,179 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LicenseDetails snapshots snapshots: renders as expected with default props 1`] = `""`; + +exports[`LicenseDetails snapshots snapshots: renders as expected with level set to block and license set to Creative Commons 1`] = ` +
+ + + + + + + + } + title={ +
+ + +
+ } + /> + + + +
+ + + } + title={ +
+ + +
+ } + /> + + + +
+ + + } + title={ +
+ + +
+ } + /> + + + +
+ + + } + title={ +
+ + +
+ } + /> + + + +
+
+
+
+`; + +exports[`LicenseDetails snapshots snapshots: renders as expected with level set to block and license set to all rights reserved 1`] = ` +
+ + + + + + + + +
+`; + +exports[`LicenseDetails snapshots snapshots: renders as expected with level set to block and license set to select 1`] = `""`; + +exports[`LicenseDetails snapshots snapshots: renders as expected with level set to library 1`] = `""`; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseDisplay.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseDisplay.test.jsx.snap new file mode 100644 index 000000000..0b292b989 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseDisplay.test.jsx.snap @@ -0,0 +1,145 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LicenseDisplay snapshots snapshots: renders as expected with default props 1`] = ` + + + + + } + /> + + FormattedMessage component with license description + + + +`; + +exports[`LicenseDisplay snapshots snapshots: renders as expected with level set to block 1`] = ` + + + + + } + /> + + FormattedMessage component with license description + + + + + + +`; + +exports[`LicenseDisplay snapshots snapshots: renders as expected with level set to block and license set to Creative Commons 1`] = ` + + + + + } + /> + + FormattedMessage component with license description + + + + + + +`; + +exports[`LicenseDisplay snapshots snapshots: renders as expected with level set to block and license set to select 1`] = `""`; + +exports[`LicenseDisplay snapshots snapshots: renders as expected with level set to library 1`] = ` + + + + + } + /> + + FormattedMessage component with license description + + + + + + +`; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseSelector.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseSelector.test.jsx.snap new file mode 100644 index 000000000..838820b4d --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseSelector.test.jsx.snap @@ -0,0 +1,177 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LicenseSelector snapshots snapshots: renders as expected with block level 1`] = ` + + + + + + + + + } + tooltipPlacement="top" + /> + + + + + +`; + +exports[`LicenseSelector snapshots snapshots: renders as expected with default props 1`] = ` + + + + + + + + + + + + +`; + +exports[`LicenseSelector snapshots snapshots: renders as expected with library level 1`] = ` + + + + + + + + + + + + +`; + +exports[`LicenseSelector snapshots snapshots: renders as expected with no license 1`] = ` + + + + + + + + + + + + +`; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/index.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/index.test.jsx.snap new file mode 100644 index 000000000..265b62223 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/index.test.jsx.snap @@ -0,0 +1,155 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LicenseWidget snapshots snapshots: renders as expected with default props 1`] = ` + + + + + + + } + title="License" +> + + + + + } + /> +
+ + + +`; + +exports[`LicenseWidget snapshots snapshots: renders as expected with isLibrary true 1`] = ` + + + + + +
+ } + title="License" +> + + + + + } + /> + +
+`; + +exports[`LicenseWidget snapshots snapshots: renders as expected with licenseType defined 1`] = ` + + + + + + + } + title="License" +> + + + + + } + /> + + +`; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/hooks.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/hooks.jsx new file mode 100644 index 000000000..87f64fd54 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/hooks.jsx @@ -0,0 +1,84 @@ +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import messages from './messages'; +import { actions } from '../../../../../../data/redux'; +import { LicenseLevel, LicenseTypes } from '../../../../../../data/constants/licenses'; + +export const determineLicense = ({ + isLibrary, + licenseType, + licenseDetails, + courseLicenseType, + courseLicenseDetails, +}) => { + let level = LicenseLevel.course; + if (licenseType) { + if (isLibrary) { + level = LicenseLevel.library; + } else { + level = LicenseLevel.block; + } + } + + return { + license: licenseType || courseLicenseType, + details: licenseType ? licenseDetails : courseLicenseDetails, + level, + }; +}; + +export const determineText = ({ level }) => { + let levelDescription = ''; + let licenseDescription = ''; + switch (level) { + case LicenseLevel.course: + levelDescription = ; + licenseDescription = ; + break; + case LicenseLevel.library: + levelDescription = ; + licenseDescription = ; + break; + default: // default to block + levelDescription = ; + licenseDescription = ; + break; + } + + return { + levelDescription, + licenseDescription, + }; +}; + +export const onSelectLicense = ({ + dispatch, +}) => (license) => { + switch (license) { + case LicenseTypes.allRightsReserved: + dispatch(actions.video.updateField({ + licenseType: LicenseTypes.allRightsReserved, + licenseDetails: {}, + })); + break; + case LicenseTypes.creativeCommons: + dispatch(actions.video.updateField({ + licenseType: LicenseTypes.creativeCommons, + licenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, + })); + break; + default: + dispatch(actions.video.updateField({ licenseType: LicenseTypes.select })); + break; + } +}; + +export default { + determineLicense, + determineText, + onSelectLicense, +}; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/hooks.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/hooks.test.jsx new file mode 100644 index 000000000..09efff5c6 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/hooks.test.jsx @@ -0,0 +1,131 @@ +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { actions } from '../../../../../../data/redux'; +import { LicenseTypes } from '../../../../../../data/constants/licenses'; +import * as module from './hooks'; +import { messages } from './messages'; + +jest.mock('../../../../../../data/redux', () => ({ + actions: { + video: { + updateField: jest.fn(args => ({ updateField: args })).mockName('actions.video.updateField'), + }, + }, +})); + +describe('VideoEditorTranscript hooks', () => { + describe('determineLicense', () => { + const courseLicenseProps = { + isLibrary: false, + licenseType: '', + licenseDetails: {}, + courseLicenseType: 'sOMEliCENse', + courseLicenseDetails: {}, + }; + const libraryLicenseProps = { + isLibrary: true, + licenseType: 'sOMEliCENse', + licenseDetails: {}, + courseLicenseType: '', + courseLicenseDetails: {}, + }; + const blockLicenseProps = { + isLibrary: false, + licenseType: 'sOMEliCENse', + licenseDetails: {}, + courseLicenseType: '', + courseLicenseDetails: {}, + }; + it('returns expected license, details and level for course set license', () => { + expect(module.determineLicense(courseLicenseProps)).toEqual({ + license: 'sOMEliCENse', + details: {}, + level: 'course', + }); + }); + it('returns expected license, details and level for library set license', () => { + expect(module.determineLicense(libraryLicenseProps)).toEqual({ + license: 'sOMEliCENse', + details: {}, + level: 'library', + }); + }); + it('returns expected license, details and level for block set license', () => { + expect(module.determineLicense(blockLicenseProps)).toEqual({ + license: 'sOMEliCENse', + details: {}, + level: 'block', + }); + }); + }); + describe('determineText', () => { + it('returns expected level and license description for course level', () => { + expect(module.determineText({ level: 'course' })).toEqual({ + levelDescription: , + licenseDescription: , + }); + }); + it('returns expected level and license description for library level', () => { + expect(module.determineText({ level: 'library' })).toEqual({ + levelDescription: , + licenseDescription: , + }); + }); + it('returns expected level and license description for library level', () => { + expect(module.determineText({ level: 'default' })).toEqual({ + levelDescription: , + licenseDescription: , + }); + }); + }); + describe('onSelectLicense', () => { + // const mockEvent = { target: { value: mockLangValue } }; + const mockDispatch = jest.fn(); + test('it dispatches the correct thunk for all rights reserved', () => { + const mockLicenseValue = 'all-rights-reserved'; + const callBack = module.onSelectLicense({ dispatch: mockDispatch }); + callBack(mockLicenseValue); + expect(actions.video.updateField).toHaveBeenCalledWith({ + licenseType: LicenseTypes.allRightsReserved, + licenseDetails: {}, + }); + expect(mockDispatch).toHaveBeenCalledWith({ + updateField: { + licenseType: LicenseTypes.allRightsReserved, + licenseDetails: {}, + }, + }); + }); + test('it dispatches the correct thunk for creative commons', () => { + const mockLicenseValue = 'creative-commons'; + const callBack = module.onSelectLicense({ dispatch: mockDispatch }); + callBack(mockLicenseValue); + expect(actions.video.updateField).toHaveBeenCalledWith({ + licenseType: LicenseTypes.creativeCommons, + licenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, + }); + expect(mockDispatch).toHaveBeenCalledWith({ + updateField: { + licenseType: LicenseTypes.creativeCommons, + licenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, + }, + }); + }); + test('it dispatches the correct thunk for no license type', () => { + const mockLicenseValue = 'sOMEliCENse'; + const callBack = module.onSelectLicense({ dispatch: mockDispatch }); + callBack(mockLicenseValue); + expect(actions.video.updateField).toHaveBeenCalledWith({ licenseType: LicenseTypes.select }); + expect(mockDispatch).toHaveBeenCalledWith({ updateField: { licenseType: LicenseTypes.select } }); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/index.jsx new file mode 100644 index 000000000..065433e38 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/index.jsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { + FormattedMessage, + injectIntl, + intlShape, +} from '@edx/frontend-platform/i18n'; +import { + Button, + Form, + Stack, +} from '@edx/paragon'; +import { Add } from '@edx/paragon/icons'; + +import { actions, selectors } from '../../../../../../data/redux'; +import hooks from './hooks'; +import messages from './messages'; +import CollapsibleFormWidget from '../CollapsibleFormWidget'; +import LicenseBlurb from './LicenseBlurb'; +import LicenseSelector from './LicenseSelector'; +import LicenseDetails from './LicenseDetails'; +import LicenseDisplay from './LicenseDisplay'; + +/** + * Collapsible Form widget controlling video license type and details + */ +export const LicenseWidget = ({ + // injected + intl, + // redux + isLibrary, + licenseType, + licenseDetails, + courseLicenseType, + courseLicenseDetails, + updateField, +}) => { + const { license, details, level } = hooks.determineLicense({ + isLibrary, + licenseType, + licenseDetails, + courseLicenseType, + courseLicenseDetails, + }); + const { licenseDescription, levelDescription } = hooks.determineText({ level }); + return ( + + + {levelDescription} + + )} + title={intl.formatMessage(messages.title)} + > + + + {license ? ( + <> + + + + + ) : null } + {!licenseType ? ( + <> +
+ + + ) : null } + + + ); +}; + +LicenseWidget.propTypes = { + // injected + intl: intlShape.isRequired, + // redux + isLibrary: PropTypes.bool.isRequired, + licenseType: PropTypes.string.isRequired, + licenseDetails: PropTypes.shape({}).isRequired, + courseLicenseType: PropTypes.string.isRequired, + courseLicenseDetails: PropTypes.shape({}).isRequired, + updateField: PropTypes.func.isRequired, +}; + +export const mapStateToProps = (state) => ({ + isLibrary: selectors.app.isLibrary(state), + licenseType: selectors.video.licenseType(state), + licenseDetails: selectors.video.licenseDetails(state), + courseLicenseType: selectors.video.courseLicenseType(state), + courseLicenseDetails: selectors.video.courseLicenseDetails(state), +}); + +export const mapDispatchToProps = (dispatch) => ({ + updateField: (stateUpdate) => dispatch(actions.video.updateField(stateUpdate)), +}); + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LicenseWidget)); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/index.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/index.test.jsx new file mode 100644 index 000000000..41fdeb9ef --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/index.test.jsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { formatMessage } from '../../../../../../../testUtils'; +import { actions, selectors } from '../../../../../../data/redux'; +import { LicenseWidget, mapStateToProps, mapDispatchToProps } from '.'; + +jest.mock('react', () => { + const updateState = jest.fn(); + return { + ...jest.requireActual('react'), + updateState, + useContext: jest.fn(() => ({ license: ['error.license', jest.fn().mockName('error.setLicense')] })), + }; +}); + +jest.mock('../../../../../../data/redux', () => ({ + actions: { + video: { + updateField: jest.fn().mockName('actions.video.updateField'), + }, + }, + selectors: { + app: { + isLibrary: jest.fn(state => ({ isLibrary: state })), + }, + video: { + licenseType: jest.fn(state => ({ licenseType: state })), + licenseDetails: jest.fn(state => ({ licenseDetails: state })), + courseLicenseType: jest.fn(state => ({ courseLicenseType: state })), + courseLicenseDetails: jest.fn(state => ({ courseLicenseDetails: state })), + }, + }, +})); + +describe('LicenseWidget', () => { + const props = { + error: {}, + subtitle: 'SuBTItle', + title: 'tiTLE', + intl: { formatMessage }, + isLibrary: false, + licenseType: null, + licenseDetails: {}, + courseLicenseType: 'all-rights-reserved', + courseLicenseDetails: {}, + updateField: jest.fn().mockName('args.updateField'), + }; + + describe('snapshots', () => { + // determineLicense.mockReturnValue({ + // license: false, + // details: jest.fn().mockName('modal.openModal'), + // level: 'course', + // }); + // determineText.mockReturnValue({ + // isSourceCodeOpen: false, + // openSourceCodeModal: jest.fn().mockName('modal.openModal'), + // closeSourceCodeModal: jest.fn().mockName('modal.closeModal'), + // }); + test('snapshots: renders as expected with default props', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with isLibrary true', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with licenseType defined', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + }); + describe('mapStateToProps', () => { + const testState = { A: 'pple', B: 'anana', C: 'ucumber' }; + test('isLibrary from app.isLibrary', () => { + expect( + mapStateToProps(testState).isLibrary, + ).toEqual(selectors.app.isLibrary(testState)); + }); + test('licenseType from video.licenseType', () => { + expect( + mapStateToProps(testState).licenseType, + ).toEqual(selectors.video.licenseType(testState)); + }); + test('licenseDetails from video.licenseDetails', () => { + expect( + mapStateToProps(testState).licenseDetails, + ).toEqual(selectors.video.licenseDetails(testState)); + }); + test('courseLicenseType from video.courseLicenseType', () => { + expect( + mapStateToProps(testState).courseLicenseType, + ).toEqual(selectors.video.courseLicenseType(testState)); + }); + test('courseLicenseDetails from video.courseLicenseDetails', () => { + expect( + mapStateToProps(testState).courseLicenseDetails, + ).toEqual(selectors.video.courseLicenseDetails(testState)); + }); + }); + describe('mapDispatchToProps', () => { + const dispatch = jest.fn(); + test('updateField from actions.video.updateField', () => { + expect(mapDispatchToProps.updateField).toEqual(dispatch(actions.video.updateField)); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/messages.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/messages.js new file mode 100644 index 000000000..af08c2c7f --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/messages.js @@ -0,0 +1,124 @@ +export const messages = { + title: { + id: 'authoring.videoeditor.license.title', + defaultMessage: 'License', + description: 'Title for license widget', + }, + licenseTypeLabel: { + id: 'authoring.videoeditor.license.licenseType.label', + defaultMessage: 'License Type', + description: 'Label for license type selection field', + }, + detailsSubsectionTitle: { + id: 'authoring.videoeditor.license.detailsSubsection.title', + defaultMessage: 'License Details', + description: 'Title for license detatils subsection', + }, + displaySubsectionTitle: { + id: 'authoring.videoeditor.license.displaySubsection.title', + defaultMessage: 'License Display', + description: 'Title for license display subsection', + }, + addLicenseButtonLabel: { + id: 'authoring.videoeditor.license.add.label', + defaultMessage: 'Add a license for this video', + description: 'Label for add license button', + }, + deleteLicenseSelection: { + id: 'authoring.videoeditor.license.deleteLicenseSelection', + defaultMessage: 'Delete', + description: 'Message presented to user for action to delete license selection', + }, + allRightsReservedIconsLabel: { + id: 'authoring.videoeditor.license.allRightsReservedIcons.label', + defaultMessage: 'All Rights Reserved', + description: 'Label for row of all rights reserved icons', + }, + creativeCommonsIconsLabel: { + id: 'authoring.videoeditor.license.creativeCommonsIcons.label', + defaultMessage: 'Some Rights Reserved', + description: 'Label for row of creative common icons', + }, + viewLicenseDetailsLabel: { + id: 'authoring.videoeditor.license.viewLicenseDetailsLabel.label', + defaultMessage: 'View license details', + description: 'Label for view license details button', + }, + courseLevelDescription: { + id: 'authoring.videoeditor.license.courseLevelDescription.helperText', + defaultMessage: 'This license currently set at the course level', + description: 'Helper text for license type when using course license', + }, + courseLicenseDescription: { + id: 'authoring.videoeditor.license.courseLicenseDescription.message', + defaultMessage: 'Licenses set at the course level appear at the bottom of courseware pages within your course.', + description: 'Message explaining where course level licenses are set', + }, + libraryLevelDescription: { + id: 'authoring.videoeditor.license.libraryLevelDescription.helperText', + defaultMessage: 'This license currently set at the library level', + description: 'Helper text for license type when using library license', + }, + libraryLicenseDescription: { + id: 'authoring.videoeditor.license.libraryLicenseDescription.message', + defaultMessage: 'Licenses set at the library level appear at the specific library video.', + description: 'Message explaining where library level licenses are set', + }, + defaultLevelDescription: { + id: 'authoring.videoeditor.license.defaultLevelDescription.helperText', + defaultMessage: 'This license is set specifically for this video', + description: 'Helper text for license type when choosing for a spcific video', + }, + defaultLicenseDescription: { + id: 'authoring.videoeditor.license.defaultLicenseDescription.message', + defaultMessage: 'When a video has a different license than the course as a whole, learners see the license at the bottom right of the video player.', + description: 'Message explaining where video specific licenses are seen by users', + }, + attributionCheckboxLabel: { + id: 'authoring.videoeditor.license.attributionCheckboxLabel', + defaultMessage: 'Attribution', + description: 'Label for attribution checkbox', + }, + attributionSectionDescription: { + id: 'authoring.videoeditor.license.attributionSectionDescription', + defaultMessage: 'Allow others to copy, distribute, display and perform your copyrighted work but only if they give credit the way you request. Currently, this option is required.', + description: 'Attribution card section defining attribution license', + }, + noncommercialCheckboxLabel: { + id: 'authoring.videoeditor.license.noncommercialCheckboxLabel', + defaultMessage: 'Noncommercial', + description: 'Label for noncommercial checkbox', + }, + noncommercialSectionDescription: { + id: 'authoring.videoeditor.license.noncommercialSectionDescription', + defaultMessage: 'Allow others to copy, distribute, display and perform your work - and derivative works based upon it - but for noncommercial purposes only.', + description: 'Noncommercial card section defining noncommercial license', + }, + noDerivativesCheckboxLabel: { + id: 'authoring.videoeditor.license.noDerivativesCheckboxLabel', + defaultMessage: 'No Derivatives', + description: 'Label for No Derivatives checkbox', + }, + noDerivativesSectionDescription: { + id: 'authoring.videoeditor.license.noDerivativesSectionDescription', + defaultMessage: 'Allow others to copy, distribute, display and perform only verbatim copies of your work, not derivative works based upon it. This option is incompatible with "Share Alike".', + description: 'No Derivatives card section defining no derivatives license', + }, + shareAlikeCheckboxLabel: { + id: 'authoring.videoeditor.license.shareAlikeCheckboxLabel', + defaultMessage: 'Share Alike', + description: 'Label for Share Alike checkbox', + }, + shareAlikeSectionDescription: { + id: 'authoring.videoeditor.license.shareAlikeSectionDescription', + defaultMessage: 'Allow others to distribute derivative works only under a license identical to the license that governs your work. This option is incompatible with "No Derivatives".', + description: 'Share Alike card section defining no derivatives license', + }, + allRightsReservedSectionMessage: { + id: 'authoring.videoeditor.license.allRightsReservedSectionMessage', + defaultMessage: 'You reserve all rights for your work.', + description: 'All Rights Reserved section message', + }, +}; + +export default messages; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/hooks.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/hooks.js similarity index 100% rename from src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/hooks.jsx rename to src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoSourceWidget/hooks.js diff --git a/src/editors/containers/VideoEditor/index.jsx b/src/editors/containers/VideoEditor/index.jsx index 3696884f5..d11c1543e 100644 --- a/src/editors/containers/VideoEditor/index.jsx +++ b/src/editors/containers/VideoEditor/index.jsx @@ -2,12 +2,25 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import { + Spinner, +} from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + +import { selectors } from '../../data/redux'; +import { RequestKeys } from '../../data/constants/requests'; + import EditorContainer from '../EditorContainer'; import VideoEditorModal from './components/VideoEditorModal'; import { ErrorContext, errorsHook, fetchVideoContent } from './hooks'; +import { messages } from './messages'; export const VideoEditor = ({ onClose, + // injected + intl, + // redux + studioViewFinished, }) => { const { error, @@ -20,9 +33,17 @@ export const VideoEditor = ({ onClose={onClose} validateEntry={validateEntry} > -
- -
+ {studioViewFinished ? ( +
+ +
+ ) : ( + + )} ); @@ -33,10 +54,16 @@ VideoEditor.defaultProps = { }; VideoEditor.propTypes = { onClose: PropTypes.func, + // injected + intl: intlShape.isRequired, + // redux + studioViewFinished: PropTypes.bool.isRequired, }; -export const mapStateToProps = () => {}; +export const mapStateToProps = (state) => ({ + studioViewFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchStudioView }), +}); export const mapDispatchToProps = {}; -export default connect(mapStateToProps, mapDispatchToProps)(VideoEditor); +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(VideoEditor)); diff --git a/src/editors/containers/VideoEditor/index.test.jsx b/src/editors/containers/VideoEditor/index.test.jsx index 80f775830..dcb403226 100644 --- a/src/editors/containers/VideoEditor/index.test.jsx +++ b/src/editors/containers/VideoEditor/index.test.jsx @@ -1,7 +1,10 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { VideoEditor, mapDispatchToProps } from '.'; +import { formatMessage } from '../../../testUtils'; +import { selectors } from '../../data/redux'; +import { RequestKeys } from '../../data/constants/requests'; +import { VideoEditor, mapStateToProps, mapDispatchToProps } from '.'; jest.mock('../EditorContainer', () => 'EditorContainer'); jest.mock('./components/VideoEditorModal', () => 'VideoEditorModal'); @@ -15,14 +18,35 @@ jest.mock('./hooks', () => ({ fetchVideoContent: jest.fn().mockName('fetchVideoContent'), })); +jest.mock('../../data/redux', () => ({ + selectors: { + requests: { + isFinished: jest.fn((state, params) => ({ isFailed: { state, params } })), + }, + }, +})); + describe('VideoEditor', () => { const props = { onClose: jest.fn().mockName('props.onClose'), + intl: { formatMessage }, + studioViewFinished: false, }; describe('snapshots', () => { test('renders as expected with default behavior', () => { expect(shallow()).toMatchSnapshot(); }); + test('renders as expected with default behavior', () => { + expect(shallow()).toMatchSnapshot(); + }); + }); + describe('mapStateToProps', () => { + const testState = { A: 'pple', B: 'anana', C: 'ucumber' }; + test('studioViewFinished from requests.isFinished', () => { + expect( + mapStateToProps(testState).studioViewFinished, + ).toEqual(selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchStudioView })); + }); }); describe('mapDispatchToProps', () => { test('is empty', () => { diff --git a/src/editors/containers/VideoEditor/messages.js b/src/editors/containers/VideoEditor/messages.js new file mode 100644 index 000000000..488c8c7c1 --- /dev/null +++ b/src/editors/containers/VideoEditor/messages.js @@ -0,0 +1,9 @@ +export const messages = { + spinnerScreenReaderText: { + id: 'authoring.videoEditor.spinnerScreenReaderText', + defaultMessage: 'loading', + description: 'Loading message for spinner screenreader text.', + }, +}; + +export default messages; diff --git a/src/editors/data/constants/licenses.js b/src/editors/data/constants/licenses.js index a8bd10c82..9b021aca1 100644 --- a/src/editors/data/constants/licenses.js +++ b/src/editors/data/constants/licenses.js @@ -1,9 +1,26 @@ import { StrictDict } from '../../utils'; -const LicenseTypes = StrictDict({ - creativeCommons: 'creative-commons', - allRightsReserved: 'all-rights-reserved', - publicDomainDedication: 'public-domain-dedication', +export const LicenseNames = StrictDict({ + select: 'Select', + allRightsReserved: 'All Rights Reserved', + creativeCommons: 'Creative Commons', }); -export default LicenseTypes; +export const LicenseTypes = StrictDict({ + allRightsReserved: 'all-rights-reserved', + creativeCommons: 'creative-commons', + select: 'select', + // publicDomainDedication: 'public-domain-dedication', // future? +}); + +export const LicenseLevel = StrictDict({ + block: 'block', + course: 'course', + library: 'library', +}); + +export default { + LicenseLevel, + LicenseNames, + LicenseTypes, +}; diff --git a/src/editors/data/constants/requests.js b/src/editors/data/constants/requests.js index b1d098780..6cd0d5d07 100644 --- a/src/editors/data/constants/requests.js +++ b/src/editors/data/constants/requests.js @@ -19,4 +19,5 @@ export const RequestKeys = StrictDict({ uploadThumbnail: 'uploadThumbnail', uploadTranscript: 'uploadTranscript', deleteTranscript: 'deleteTranscript', + fetchCourseDetails: 'fetchCourseDetails', }); diff --git a/src/editors/data/redux/app/reducer.js b/src/editors/data/redux/app/reducer.js index 8dba8dbba..c2311cbc3 100644 --- a/src/editors/data/redux/app/reducer.js +++ b/src/editors/data/redux/app/reducer.js @@ -16,6 +16,7 @@ const initialState = { studioEndpointUrl: null, lmsEndpointUrl: null, assets: {}, + courseDetails: {}, }; // eslint-disable-next-line no-unused-vars @@ -37,15 +38,13 @@ const app = createSlice({ blockValue: payload, blockTitle: payload.data.display_name, }), - setStudioView: (state, { payload }) => ({ - ...state, - studioView: payload, - }), + setStudioView: (state, { payload }) => ({ ...state, studioView: payload }), setBlockContent: (state, { payload }) => ({ ...state, blockContent: payload }), setBlockTitle: (state, { payload }) => ({ ...state, blockTitle: payload }), setSaveResponse: (state, { payload }) => ({ ...state, saveResponse: payload }), initializeEditor: (state) => ({ ...state, editorInitialized: true }), setAssets: (state, { payload }) => ({ ...state, assets: payload }), + setCourseDetails: (state, { payload }) => ({ ...state, courseDetails: payload }), }, }); diff --git a/src/editors/data/redux/app/reducer.test.js b/src/editors/data/redux/app/reducer.test.js index 6d48a3de2..09b1bccdb 100644 --- a/src/editors/data/redux/app/reducer.test.js +++ b/src/editors/data/redux/app/reducer.test.js @@ -48,6 +48,7 @@ describe('app reducer', () => { ['setBlockTitle', 'blockTitle'], ['setSaveResponse', 'saveResponse'], ['setAssets', 'assets'], + ['setCourseDetails', 'courseDetails'], ].map(args => setterTest(...args)); describe('setBlockValue', () => { it('sets blockValue, as well as setting the blockTitle from data.display_name', () => { diff --git a/src/editors/data/redux/requests/reducer.js b/src/editors/data/redux/requests/reducer.js index 2df4b3830..6c1b80819 100644 --- a/src/editors/data/redux/requests/reducer.js +++ b/src/editors/data/redux/requests/reducer.js @@ -14,6 +14,7 @@ const initialState = { [RequestKeys.uploadThumbnail]: { status: RequestStates.inactive }, [RequestKeys.uploadTranscript]: { status: RequestStates.inactive }, [RequestKeys.deleteTranscript]: { status: RequestStates.inactive }, + [RequestKeys.fetchCourseDetails]: { status: RequestStates.inactive }, [RequestKeys.fetchAssets]: { status: RequestStates.inactive }, }; diff --git a/src/editors/data/redux/thunkActions/app.js b/src/editors/data/redux/thunkActions/app.js index 4ffdd7410..536ea296f 100644 --- a/src/editors/data/redux/thunkActions/app.js +++ b/src/editors/data/redux/thunkActions/app.js @@ -29,6 +29,14 @@ export const fetchAssets = () => (dispatch) => { onSuccess: (response) => dispatch(actions.app.setAssets(response)), })); }; + +export const fetchCourseDetails = () => (dispatch) => { + dispatch(requests.fetchCourseDetails({ + onSuccess: (response) => dispatch(actions.app.setCourseDetails(response)), + onFailure: (e) => dispatch(actions.app.setCourseDetails(e)), + })); +}; + /** * @param {string} studioEndpointUrl * @param {string} blockId @@ -41,6 +49,7 @@ export const initialize = (data) => (dispatch) => { dispatch(module.fetchUnit()); dispatch(module.fetchStudioView()); dispatch(module.fetchAssets()); + dispatch(module.fetchCourseDetails()); }; /** @@ -71,11 +80,12 @@ export const fetchVideos = ({ onSuccess }) => (dispatch) => { export default StrictDict({ fetchBlock, + fetchCourseDetails, + fetchStudioView, fetchUnit, + fetchVideos, initialize, saveBlock, fetchAssets, uploadImage, - fetchVideos, - fetchStudioView, }); diff --git a/src/editors/data/redux/thunkActions/app.test.js b/src/editors/data/redux/thunkActions/app.test.js index 49fa5c5c6..01373e9af 100644 --- a/src/editors/data/redux/thunkActions/app.test.js +++ b/src/editors/data/redux/thunkActions/app.test.js @@ -9,6 +9,7 @@ jest.mock('./requests', () => ({ uploadAsset: (args) => ({ uploadAsset: args }), fetchStudioView: (args) => ({ fetchStudioView: args }), fetchAssets: (args) => ({ fetchAssets: args }), + fetchCourseDetails: (args) => ({ fetchCourseDetails: args }), })); jest.mock('../../../utils', () => ({ @@ -78,6 +79,25 @@ describe('app thunkActions', () => { expect(dispatch).toHaveBeenCalledWith(actions.app.setUnitUrl(testValue)); }); }); + describe('fetchCourseDetails', () => { + beforeEach(() => { + thunkActions.fetchCourseDetails()(dispatch); + [[dispatchedAction]] = dispatch.mock.calls; + }); + it('dispatches fetchUnit action', () => { + expect(dispatchedAction.fetchCourseDetails).not.toEqual(undefined); + }); + it('dispatches actions.app.setUnitUrl on success', () => { + dispatch.mockClear(); + dispatchedAction.fetchCourseDetails.onSuccess(testValue); + expect(dispatch).toHaveBeenCalledWith(actions.app.setCourseDetails(testValue)); + }); + it('dispatches actions.app.setUnitUrl on failure', () => { + dispatch.mockClear(); + dispatchedAction.fetchCourseDetails.onFailure(testValue); + expect(dispatch).toHaveBeenCalledWith(actions.app.setCourseDetails(testValue)); + }); + }); describe('initialize', () => { it('dispatches actions.app.initialize, and then fetches both block and unit', () => { const { @@ -85,11 +105,13 @@ describe('app thunkActions', () => { fetchUnit, fetchStudioView, fetchAssets, + fetchCourseDetails, } = thunkActions; thunkActions.fetchBlock = () => 'fetchBlock'; thunkActions.fetchUnit = () => 'fetchUnit'; thunkActions.fetchStudioView = () => 'fetchStudioView'; thunkActions.fetchAssets = () => 'fetchAssets'; + thunkActions.fetchCourseDetails = () => 'fetchCourseDetails'; thunkActions.initialize(testValue)(dispatch); expect(dispatch.mock.calls).toEqual([ [actions.app.initialize(testValue)], @@ -97,11 +119,13 @@ describe('app thunkActions', () => { [thunkActions.fetchUnit()], [thunkActions.fetchStudioView()], [thunkActions.fetchAssets()], + [thunkActions.fetchCourseDetails()], ]); thunkActions.fetchBlock = fetchBlock; thunkActions.fetchUnit = fetchUnit; thunkActions.fetchStudioView = fetchStudioView; thunkActions.fetchAssets = fetchAssets; + thunkActions.fetchCourseDetails = fetchCourseDetails; }); }); describe('saveBlock', () => { diff --git a/src/editors/data/redux/thunkActions/requests.js b/src/editors/data/redux/thunkActions/requests.js index 5b5898b46..69c383286 100644 --- a/src/editors/data/redux/thunkActions/requests.js +++ b/src/editors/data/redux/thunkActions/requests.js @@ -134,6 +134,7 @@ export const fetchAssets = ({ ...rest }) => (dispatch, getState) => { ...rest, })); }; + export const allowThumbnailUpload = ({ ...rest }) => (dispatch, getState) => { dispatch(module.networkRequest({ requestKey: RequestKeys.allowThumbnailUpload, @@ -143,6 +144,7 @@ export const allowThumbnailUpload = ({ ...rest }) => (dispatch, getState) => { ...rest, })); }; + export const uploadThumbnail = ({ thumbnail, videoId, ...rest }) => (dispatch, getState) => { dispatch(module.networkRequest({ requestKey: RequestKeys.uploadThumbnail, @@ -155,6 +157,7 @@ export const uploadThumbnail = ({ thumbnail, videoId, ...rest }) => (dispatch, g ...rest, })); }; + export const deleteTranscript = ({ language, videoId, ...rest }) => (dispatch, getState) => { dispatch(module.networkRequest({ requestKey: RequestKeys.deleteTranscript, @@ -187,6 +190,18 @@ export const uploadTranscript = ({ })); }; +export const fetchCourseDetails = ({ ...rest }) => (dispatch, getState) => { + dispatch(module.networkRequest({ + requestKey: RequestKeys.fetchCourseDetails, + promise: api + .fetchCourseDetails({ + studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), + learningContextId: selectors.app.learningContextId(getState()), + }), + ...rest, + })); +}; + export default StrictDict({ fetchBlock, fetchStudioView, @@ -198,4 +213,5 @@ export default StrictDict({ uploadThumbnail, deleteTranscript, uploadTranscript, + fetchCourseDetails, }); diff --git a/src/editors/data/redux/thunkActions/requests.test.js b/src/editors/data/redux/thunkActions/requests.test.js index c9f345b24..e5b7007ae 100644 --- a/src/editors/data/redux/thunkActions/requests.test.js +++ b/src/editors/data/redux/thunkActions/requests.test.js @@ -24,6 +24,7 @@ jest.mock('../../services/cms/api', () => ({ fetchBlockById: ({ id, url }) => ({ id, url }), fetchStudioView: ({ id, url }) => ({ id, url }), fetchByUnitId: ({ id, url }) => ({ id, url }), + fetchCourseDetails: (args) => args, saveBlock: (args) => args, fetchAssets: ({ id, url }) => ({ id, url }), uploadAsset: (args) => args, @@ -215,6 +216,21 @@ describe('requests thunkActions module', () => { }, }); }); + describe('fetchCourseDetails', () => { + testNetworkRequestAction({ + action: requests.fetchCourseDetails, + args: fetchParams, + expectedString: 'with fetchCourseDetails promise', + expectedData: { + ...fetchParams, + requestKey: RequestKeys.fetchCourseDetails, + promise: api.fetchCourseDetails({ + studioEndpointUrl: selectors.app.studioEndpointUrl(testState), + learningContextId: selectors.app.learningContextId(testState), + }), + }, + }); + }); describe('fetchAssets', () => { let fetchAssets; let loadImages; diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index 8b034eea8..55f557953 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -5,6 +5,8 @@ import * as module from './video'; export const loadVideoData = () => (dispatch, getState) => { const state = getState(); const rawVideoData = state.app.blockValue.data.metadata ? state.app.blockValue.data.metadata : {}; + const courseLicenseData = state.app.courseDetails.data ? state.app.courseDetails.data : {}; + const licenseData = state.app.studioView?.data?.html; const { videoSource, videoType, @@ -15,9 +17,11 @@ export const loadVideoData = () => (dispatch, getState) => { youtubeId: rawVideoData.youtube_id_1_0, html5Sources: rawVideoData.html5_sources, }); - // we don't appear to want to parse license version - const [licenseType, licenseOptions] = module.parseLicense(rawVideoData.license); - + const [licenseType, licenseOptions] = module.parseLicense({ licenseData, level: 'block' }); + const [courseLicenseType, courseLicenseDetails] = module.parseLicense({ + licenseData: courseLicenseData.license, + level: 'course', + }); dispatch(actions.video.load({ videoSource, videoType, @@ -40,6 +44,13 @@ export const loadVideoData = () => (dispatch, getState) => { noDerivatives: licenseOptions.nd, shareAlike: licenseOptions.sa, }, + courseLicenseType, + courseLicenseDetails: { + attribution: courseLicenseDetails.by, + noncommercial: courseLicenseDetails.nc, + noDerivatives: courseLicenseDetails.nd, + shareAlike: courseLicenseDetails.sa, + }, thumbnail: rawVideoData.thumbnail, })); dispatch(requests.allowThumbnailUpload({ @@ -88,28 +99,35 @@ export const determineVideoSource = ({ }; }; -// copied from frontend-app-learning/src/courseware/course/course-license/CourseLicense.jsx -// in the long run, should be shared (perhaps one day the learning MFE will depend on this repo) -export const parseLicense = (license) => { - if (!license) { - // Default to All Rights Reserved if no license - // is detected - return ['all-rights-reserved', {}]; +// partially copied from frontend-app-learning/src/courseware/course/course-license/CourseLicense.jsx +export const parseLicense = ({ licenseData, level }) => { + if (!licenseData) { + return [null, {}]; } - - // Search for a colon character denoting the end - // of the license type and start of the options - const colonIndex = license.indexOf(':'); - if (colonIndex === -1) { + let license = licenseData; + if (level === 'block') { + const metadataArr = licenseData.split('data-metadata'); + metadataArr.forEach(arr => { + const parsedStr = arr.replace(/"/g, '"'); + if (parsedStr.includes('license')) { + license = parsedStr.substring(parsedStr.indexOf('"value"'), parsedStr.indexOf(', "type"')).replace(/"value": |"/g, ''); + } + }); + } + if (!license || license.includes('null')) { + return [null, {}]; + } + if (license === 'all-rights-reserved') { // no options, so the entire thing is the license type return [license, {}]; } - + // Search for a colon character denoting the end + // of the license type and start of the options + const colonIndex = license.lastIndexOf(':'); // Split the license on the colon const licenseType = license.slice(0, colonIndex).trim(); const optionStr = license.slice(colonIndex + 1).trim(); - - let options = {}; + const options = {}; let version = ''; // Set the defaultVersion to 4.0 @@ -136,19 +154,6 @@ export const parseLicense = (license) => { } }); - // No options - if (Object.keys(options).length === 0) { - // If no other options are set for the - // license, set version to 1.0 - version = '1.0'; - - // Set the `zero` option so the link - // works correctly - options = { - zero: true, - }; - } - // Set the version to whatever was included, // using `defaultVersion` as a fallback if unset version = version || defaultVersion; @@ -157,8 +162,6 @@ export const parseLicense = (license) => { }; export const saveVideoData = () => (dispatch, getState) => { - // dispatch(actions.app.setBlockContent) - // dispatch(requests.saveBlock({ }); const state = getState(); return selectors.video.videoSettings(state); }; diff --git a/src/editors/data/redux/thunkActions/video.test.js b/src/editors/data/redux/thunkActions/video.test.js index dec480432..ad9371048 100644 --- a/src/editors/data/redux/thunkActions/video.test.js +++ b/src/editors/data/redux/thunkActions/video.test.js @@ -10,6 +10,9 @@ jest.mock('..', () => ({ }, }, selectors: { + app: { + courseDetails: (state) => ({ courseDetails: state }), + }, video: { videoId: (state) => ({ videoId: state }), videoSettings: (state) => ({ videoSettings: state }), @@ -44,6 +47,7 @@ const testMetadata = { show_captions: 'shOWcapTIONS', start_time: 'stARtTiME', transcripts: { la: 'test VALUE' }, + thumbnail: 'thuMBNaIl', }; const testState = { transcripts: { la: 'test VALUE' }, @@ -69,6 +73,8 @@ describe('video thunkActions', () => { blockId: 'soMEBloCk', blockValue: { data: { metadata: { ...testMetadata } } }, studioEndpointUrl: 'soMEeNDPoiNT', + courseDetails: { data: { license: null } }, + studioView: { data: { html: 'sOMeHTml' } }, }, video: testState, })); @@ -76,28 +82,11 @@ describe('video thunkActions', () => { describe('loadVideoData', () => { let dispatchedLoad; beforeEach(() => { - thunkActions.loadVideoData()(dispatch, getState); - [[dispatchedLoad], [dispatchedAction]] = dispatch.mock.calls; - }); - afterEach(() => { - jest.restoreAllMocks(); - }); - it('dispatches allowThumbnailUpload action', () => { - expect(dispatchedLoad).not.toEqual(undefined); - expect(dispatchedAction.allowThumbnailUpload).not.toEqual(undefined); - }); - it('dispatches actions.video.updateField on success', () => { - dispatch.mockClear(); - dispatchedAction.allowThumbnailUpload.onSuccess(mockAllowThumbnailUpload); - expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({ - allowThumbnailUpload: mockAllowThumbnailUpload.data.allowThumbnailUpload, - })); - }); - it('dispatches actions.video.load', () => { jest.spyOn(thunkActions, thunkActionsKeys.determineVideoSource).mockReturnValue({ videoSource: 'videOsOurce', videoId: 'videOiD', fallbackVideos: 'fALLbACKvIDeos', + videoType: 'viDEOtyPE', }); jest.spyOn(thunkActions, thunkActionsKeys.parseLicense).mockReturnValue([ 'liCENSEtyPe', @@ -109,10 +98,21 @@ describe('video thunkActions', () => { }, ]); thunkActions.loadVideoData()(dispatch, getState); - expect(dispatch).toHaveBeenCalledWith(actions.video.load({ + [[dispatchedLoad], [dispatchedAction]] = dispatch.mock.calls; + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('dispatches allowThumbnailUpload action', () => { + expect(dispatchedLoad).not.toEqual(undefined); + expect(dispatchedAction.allowThumbnailUpload).not.toEqual(undefined); + }); + it('dispatches actions.video.load', () => { + expect(dispatchedLoad.load).toEqual({ videoSource: 'videOsOurce', videoId: 'videOiD', fallbackVideos: 'fALLbACKvIDeos', + videoType: 'viDEOtyPE', allowVideoDownloads: testMetadata.download_video, transcripts: testMetadata.transcripts, allowTranscriptDownloads: testMetadata.download_track, @@ -130,6 +130,21 @@ describe('video thunkActions', () => { noDerivatives: true, shareAlike: false, }, + courseLicenseType: 'liCENSEtyPe', + courseLicenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, + thumbnail: testMetadata.thumbnail, + }); + }); + it('dispatches actions.video.updateField on success', () => { + dispatch.mockClear(); + dispatchedAction.allowThumbnailUpload.onSuccess(mockAllowThumbnailUpload); + expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({ + allowThumbnailUpload: mockAllowThumbnailUpload.data.allowThumbnailUpload, })); }); }); @@ -222,23 +237,42 @@ describe('video thunkActions', () => { }); }); describe('parseLicense', () => { - let license; - it('returns all-rights-reserved when there is no license', () => { - expect(thunkActions.parseLicense(license)).toEqual([ + const emptyLicenseData = null; + const noLicense = 'sOMeHTml data-metadata "license" "value"= null, "type"'; + it('returns expected values for a license with no course license', () => { + expect(thunkActions.parseLicense({ + licenseData: emptyLicenseData, + level: 'sOMElevEL', + })).toEqual([ + null, + {}, + ]); + }); + it('returns expected values for a license with no block license', () => { + expect(thunkActions.parseLicense({ + licenseData: noLicense, + level: 'block', + })).toEqual([ + null, + {}, + ]); + }); + it('returns expected values for a license with all rights reserved', () => { + const license = 'sOMeHTml data-metadata "license" "value": "all-rights-reserved", "type"'; + expect(thunkActions.parseLicense({ + licenseData: license, + level: 'block', + })).toEqual([ 'all-rights-reserved', {}, ]); }); - it('returns expected values for a license with no options', () => { - license = 'sOmeLIcense'; - expect(thunkActions.parseLicense(license)).toEqual([ - license, - {}, - ]); - }); it('returns expected type and options for creative commons', () => { - license = 'creative-commons: ver=4.0 BY NC ND'; - expect(thunkActions.parseLicense(license)).toEqual([ + const license = 'sOMeHTml data-metadata "license" "value": "creative-commons: ver=4.0 BY NC ND", "type"'; + expect(thunkActions.parseLicense({ + licenseData: license, + level: 'block', + })).toEqual([ 'creative-commons', { by: true, diff --git a/src/editors/data/redux/video/reducer.js b/src/editors/data/redux/video/reducer.js index e21aac79f..17a94564f 100644 --- a/src/editors/data/redux/video/reducer.js +++ b/src/editors/data/redux/video/reducer.js @@ -23,7 +23,14 @@ const initialState = { handout: null, licenseType: null, licenseDetails: { - attribution: false, + attribution: true, + noncommercial: false, + noDerivatives: false, + shareAlike: false, + }, + courseLicenseType: null, + courseLicenseDetails: { + attribution: true, noncommercial: false, noDerivatives: false, shareAlike: false, diff --git a/src/editors/data/redux/video/selectors.js b/src/editors/data/redux/video/selectors.js index b6c071e26..037b91d8e 100644 --- a/src/editors/data/redux/video/selectors.js +++ b/src/editors/data/redux/video/selectors.js @@ -25,6 +25,8 @@ export const simpleSelectors = [ stateKeys.handout, stateKeys.licenseType, stateKeys.licenseDetails, + stateKeys.courseLicenseType, + stateKeys.courseLicenseDetails, stateKeys.allowThumbnailUpload, stateKeys.videoType, ].reduce((obj, key) => ({ ...obj, [key]: state => state.video[key] }), {}); diff --git a/src/editors/data/services/cms/api.js b/src/editors/data/services/cms/api.js index a750da973..c40e6db3e 100644 --- a/src/editors/data/services/cms/api.js +++ b/src/editors/data/services/cms/api.js @@ -17,6 +17,9 @@ export const apiMethods = { fetchAssets: ({ learningContextId, studioEndpointUrl }) => get( urls.courseAssets({ studioEndpointUrl, learningContextId }), ), + fetchCourseDetails: ({ studioEndpointUrl, learningContextId }) => get( + urls.courseDetailsUrl({ studioEndpointUrl, learningContextId }), + ), uploadAsset: ({ learningContextId, studioEndpointUrl, @@ -196,15 +199,18 @@ export const parseYoutubeId = (src) => { }; export const processLicense = (licenseType, licenseDetails) => { + if (licenseType === 'creative-commons') { + return 'creative-commons: ver=4.0'.concat( + (licenseDetails.attribution ? ' BY' : ''), + (licenseDetails.noncommercial ? ' NC' : ''), + (licenseDetails.noDerivatives ? ' ND' : ''), + (licenseDetails.shareAlike ? ' SA' : ''), + ); + } if (licenseType === 'all-rights-reserved') { return 'all-rights-reserved'; } - return 'creative-commons: ver=4.0'.concat( - (licenseDetails.attribution ? ' BY' : ''), - (licenseDetails.noncommercial ? ' NC' : ''), - (licenseDetails.noDerivatives ? ' ND' : ''), - (licenseDetails.shareAlike ? ' SA' : ''), - ); + return ''; }; export const checkMockApi = (key) => { diff --git a/src/editors/data/services/cms/api.test.js b/src/editors/data/services/cms/api.test.js index 50d37bbd1..49e449eac 100644 --- a/src/editors/data/services/cms/api.test.js +++ b/src/editors/data/services/cms/api.test.js @@ -433,6 +433,25 @@ describe('cms api', () => { expect(api.parseYoutubeId(badURL)).toEqual(null); }); }); - // TODO FOR LICENSE - describe('processLicense', () => {}); + describe('processLicense', () => { + it('returns empty string when licenseType is empty or not a valid licnese type', () => { + expect(api.processLicense('', {})).toEqual(''); + expect(api.processLicense('LiCeNsETYpe', {})).toEqual(''); + }); + it('returns empty string when licenseType equals creative commons', () => { + const licenseType = 'creative-commons'; + const licenseDetails = { + attribution: true, + noncommercial: false, + noDerivatives: true, + shareAlike: false, + }; + expect(api.processLicense(licenseType, licenseDetails)).toEqual('creative-commons: ver=4.0 BY ND'); + }); + it('returns empty string when licenseType equals creative commons', () => { + const licenseType = 'all-rights-reserved'; + const licenseDetails = {}; + expect(api.processLicense(licenseType, licenseDetails)).toEqual('all-rights-reserved'); + }); + }); }); diff --git a/src/editors/data/services/cms/mockApi.js b/src/editors/data/services/cms/mockApi.js index 98c5f0984..25d6e606e 100644 --- a/src/editors/data/services/cms/mockApi.js +++ b/src/editors/data/services/cms/mockApi.js @@ -39,7 +39,7 @@ export const fetchStudioView = ({ blockId, studioEndpointUrl }) => mockPromise({ data: { // The following is sent for 'raw' editors. html: blockId.includes('mockRaw') ? 'data-editor="raw"' : '', - data: '

Test prompt content

', + data: '

Test prompt content

', display_name: 'My Text Prompt', metadata: { display_name: 'Welcome!', @@ -120,7 +120,13 @@ export const fetchAssets = ({ learningContextId, studioEndpointUrl }) => mockPro ], }, }); - +// eslint-disable-next-line +export const fetchCourseDetails = ({ studioEndpointUrl, learningContextId }) => mockPromise({ + data: { + // license: "creative-commons: ver=4.0 BY NC", + license: 'all-rights-reserved', + }, +}); // eslint-disable-next-line export const allowThumbnailUpload = ({ studioEndpointUrl }) => mockPromise({ data: true, diff --git a/src/editors/data/services/cms/mockVideoData.js b/src/editors/data/services/cms/mockVideoData.js index 10be6619f..0bc13fd80 100644 --- a/src/editors/data/services/cms/mockVideoData.js +++ b/src/editors/data/services/cms/mockVideoData.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import LicenseTypes from '../../constants/licenses'; +import { LicenseTypes } from '../../constants/licenses'; export const videoDataProps = { videoSource: PropTypes.string, @@ -48,7 +48,7 @@ export const singleVideoData = { handout: 'my-handout-url', licenseType: LicenseTypes.creativeCommons, licenseDetails: { - attribution: false, + attribution: true, noncommercial: false, noDerivatives: false, shareAlike: false, diff --git a/src/editors/data/services/cms/urls.js b/src/editors/data/services/cms/urls.js index cf615d513..389a0b775 100644 --- a/src/editors/data/services/cms/urls.js +++ b/src/editors/data/services/cms/urls.js @@ -50,3 +50,7 @@ export const downloadVideoTranscriptURL = ({ studioEndpointUrl, blockId, languag export const downloadVideoHandoutUrl = ({ studioEndpointUrl, handout }) => ( `${studioEndpointUrl}${handout}` ); + +export const courseDetailsUrl = ({ studioEndpointUrl, learningContextId }) => ( + `${studioEndpointUrl}/settings/details/${learningContextId}` +); diff --git a/src/editors/data/services/cms/urls.test.js b/src/editors/data/services/cms/urls.test.js index d02ea245a..35ad230a2 100644 --- a/src/editors/data/services/cms/urls.test.js +++ b/src/editors/data/services/cms/urls.test.js @@ -6,9 +6,12 @@ import { blockAncestor, blockStudioView, courseAssets, + allowThumbnailUpload, + thumbnailUpload, downloadVideoTranscriptURL, videoTranscripts, downloadVideoHandoutUrl, + courseDetailsUrl, } from './urls'; describe('cms url methods', () => { @@ -19,6 +22,7 @@ describe('cms url methods', () => { const libraryV1Id = 'library-v1:libaryId123'; const language = 'la'; const handout = '/aSSet@hANdoUt'; + const videoId = '123-SOmeVidEOid-213'; describe('return to learning context urls', () => { const unitUrl = { data: { @@ -75,6 +79,18 @@ describe('cms url methods', () => { .toEqual(`${studioEndpointUrl}/assets/${learningContextId}/?page_size=500`); }); }); + describe('allowThumbnailUpload', () => { + it('returns url with studioEndpointUrl', () => { + expect(allowThumbnailUpload({ studioEndpointUrl })) + .toEqual(`${studioEndpointUrl}/video_images_upload_enabled`); + }); + }); + describe('thumbnailUpload', () => { + it('returns url with studioEndpointUrl, learningContextId, and videoId', () => { + expect(thumbnailUpload({ studioEndpointUrl, learningContextId, videoId })) + .toEqual(`${studioEndpointUrl}/video_images/${learningContextId}/${videoId}`); + }); + }); describe('videoTranscripts', () => { it('returns url with studioEndpointUrl and blockId', () => { expect(videoTranscripts({ studioEndpointUrl, blockId })) @@ -93,4 +109,10 @@ describe('cms url methods', () => { .toEqual(`${studioEndpointUrl}${handout}`); }); }); + describe('courseDetailsUrl', () => { + it('returns url with studioEndpointUrl and courseKey', () => { + expect(courseDetailsUrl({ studioEndpointUrl, learningContextId })) + .toEqual(`${studioEndpointUrl}/settings/details/${learningContextId}`); + }); + }); }); diff --git a/src/setupTest.js b/src/setupTest.js index ba8c8fceb..e5bb3980d 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -88,6 +88,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon ErrorContext: { Provider: 'ErrorContext.Provider', }, + Hyperlink: 'Hyperlink', Icon: 'Icon', IconButton: 'IconButton', IconButtonWithTooltip: 'IconButtonWithTooltip', diff --git a/www/package-lock.json b/www/package-lock.json index 57878ba7d..3e1067f34 100644 --- a/www/package-lock.json +++ b/www/package-lock.json @@ -14,7 +14,7 @@ "@edx/frontend-build": "^9.1.1", "@edx/frontend-lib-content-components": "file:..", "@edx/frontend-platform": "^3.0.1", - "@edx/paragon": "^20.13.0", + "@edx/paragon": "^20.18.0", "core-js": "^3.21.1", "dotenv": "^16.0.0", "prop-types": "^15.5.10", @@ -54,7 +54,7 @@ "devDependencies": { "@edx/frontend-build": "^11.0.2", "@edx/frontend-platform": "2.4.0", - "@edx/paragon": "^20.13.0", + "@edx/paragon": "^20.18.0", "@testing-library/dom": "^8.13.0", "@testing-library/react": "12.1.1", "@testing-library/user-event": "^13.5.0", @@ -19790,9 +19790,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.18.0", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.18.0.tgz", + "integrity": "sha512-8J7iDNjX7MPfLUWWuUU6K/ZwBojuvfdycOF16aV1+Kb2xg08E8HhevsHPevlXVjX7d6o4hTdlPZAvOlPFdxHVQ==", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", @@ -39607,7 +39607,7 @@ "@codemirror/view": "^6.0.0", "@edx/frontend-build": "^11.0.2", "@edx/frontend-platform": "2.4.0", - "@edx/paragon": "^20.13.0", + "@edx/paragon": "^20.18.0", "@reduxjs/toolkit": "^1.8.1", "@testing-library/dom": "^8.13.0", "@testing-library/react": "12.1.1", @@ -57906,9 +57906,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.18.0", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.18.0.tgz", + "integrity": "sha512-8J7iDNjX7MPfLUWWuUU6K/ZwBojuvfdycOF16aV1+Kb2xg08E8HhevsHPevlXVjX7d6o4hTdlPZAvOlPFdxHVQ==", "requires": { "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", diff --git a/www/package.json b/www/package.json index a1fa0a7e8..0b2a25993 100644 --- a/www/package.json +++ b/www/package.json @@ -15,7 +15,7 @@ "@edx/frontend-build": "^9.1.1", "@edx/frontend-lib-content-components": "file:..", "@edx/frontend-platform": "^3.0.1", - "@edx/paragon": "^20.13.0", + "@edx/paragon": "^20.18.0", "core-js": "^3.21.1", "dotenv": "^16.0.0", "prop-types": "^15.5.10",