feat: license widget (#132)
This commit is contained in:
14
package-lock.json
generated
14
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VideoEditor snapshots renders as expected with default behavior 1`] = `
|
||||
<Component
|
||||
value="hooks.errorsHook.error"
|
||||
>
|
||||
<EditorContainer
|
||||
onClose={[MockFunction props.onClose]}
|
||||
validateEntry={[MockFunction validateEntry]}
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
screenreadertext="loading"
|
||||
/>
|
||||
</EditorContainer>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`VideoEditor snapshots renders as expected with default behavior 2`] = `
|
||||
<Component
|
||||
value="hooks.errorsHook.error"
|
||||
>
|
||||
|
||||
@@ -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 (
|
||||
<CollapsibleFormWidget title="License">
|
||||
<div>License Widget</div>
|
||||
<p>License Type: {licenseType.formValue}</p>
|
||||
<p>Attribution: {licenseDetails.formValue.attribution ? 'True' : 'False'}</p>
|
||||
<p>Non-Commercial: {licenseDetails.formValue.noCommercial ? 'True' : 'False'}</p>
|
||||
<p>No-Derivatives: {licenseDetails.formValue.noDerivatives ? 'True' : 'False'}</p>
|
||||
<p>Share-Alike: {licenseDetails.formValue.shareAlike ? 'True' : 'False'}</p>
|
||||
</CollapsibleFormWidget>
|
||||
);
|
||||
};
|
||||
|
||||
export default LicenseWidget;
|
||||
@@ -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,
|
||||
}) => (
|
||||
<div className="d-flex flex-row flex-row">
|
||||
{license === LicenseTypes.allRightsReserved ? <Icon src={Copyright} /> : null}
|
||||
{license === LicenseTypes.creativeCommons ? <Icon src={Cc} /> : null}
|
||||
{details.attribution ? <Icon src={Attribution} /> : null}
|
||||
{details.noncommercial ? <Icon src={Nc} /> : null}
|
||||
{details.noDerivatives ? <Icon src={Nd} /> : null}
|
||||
{details.shareAlike ? <Icon src={Sa} /> : null}
|
||||
{license === LicenseTypes.allRightsReserved
|
||||
? <Form.Text><FormattedMessage {...messages.allRightsReservedIconsLabel} /></Form.Text>
|
||||
: null}
|
||||
{license === LicenseTypes.creativeCommons
|
||||
? <Form.Text><FormattedMessage {...messages.creativeCommonsIconsLabel} /></Form.Text>
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
|
||||
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);
|
||||
@@ -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(<LicenseBlurb {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with license equal to Creative Commons', () => {
|
||||
expect(
|
||||
shallow(<LicenseBlurb {...props} license="creative-commons" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected when details.attribution equal true', () => {
|
||||
expect(
|
||||
shallow(<LicenseBlurb {...props} license="creative-commons" details={{ attribution: true }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected when details.attribution and details.noncommercial equal true', () => {
|
||||
expect(
|
||||
shallow(<LicenseBlurb {...props} license="creative-commons" details={{ attribution: true, noncommercial: true }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected when details.attribution and details.noDerivatives equal true', () => {
|
||||
expect(
|
||||
shallow(<LicenseBlurb {...props} license="creative-commons" details={{ attribution: true, noDerivatives: true }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected when details.attribution and details.shareAlike equal true', () => {
|
||||
expect(
|
||||
shallow(<LicenseBlurb {...props} license="creative-commons" details={{ attribution: true, shareAlike: true }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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' ? (
|
||||
<div className="border-primary-100 border-top pb-3">
|
||||
<Form.Group>
|
||||
<Form.Label className="mt-3">
|
||||
<FormattedMessage {...messages.detailsSubsectionTitle} />
|
||||
</Form.Label>
|
||||
|
||||
{license === LicenseTypes.allRightsReserved
|
||||
? (
|
||||
<Form.Text>
|
||||
<FormattedMessage {...messages.allRightsReservedSectionMessage} />
|
||||
</Form.Text>
|
||||
)
|
||||
: null}
|
||||
|
||||
{license === LicenseTypes.creativeCommons
|
||||
? (
|
||||
<Stack gap={3}>
|
||||
<Card>
|
||||
<Card.Header
|
||||
title={(
|
||||
<div className="d-flex flex-row flex-nowrap">
|
||||
<Icon src={Attribution} />
|
||||
<FormattedMessage {...messages.attributionCheckboxLabel} />
|
||||
</div>
|
||||
)}
|
||||
actions={<Form.Checkbox checked disabled />}
|
||||
/>
|
||||
<Card.Section>
|
||||
<FormattedMessage {...messages.attributionSectionDescription} />
|
||||
</Card.Section>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
isClickable
|
||||
onClick={() => updateField({
|
||||
licenseDetails: {
|
||||
...details,
|
||||
noncommercial: !details.noncommercial,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Card.Header
|
||||
title={(
|
||||
<div className="d-flex flex-row flex-row">
|
||||
<Icon src={Nc} />
|
||||
<FormattedMessage {...messages.noncommercialCheckboxLabel} />
|
||||
</div>
|
||||
)}
|
||||
actions={(
|
||||
<Form.Checkbox
|
||||
checked={details.noncommercial}
|
||||
disabled={level !== LicenseLevel.block}
|
||||
onChange={(e) => updateField({
|
||||
licenseDetails: {
|
||||
...details,
|
||||
noncommercial: e.target.checked,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Section>
|
||||
<FormattedMessage {...messages.noncommercialSectionDescription} />
|
||||
</Card.Section>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
isClickable
|
||||
onClick={() => updateField({
|
||||
licenseDetails: {
|
||||
...details,
|
||||
noDerivatives: !details.noDerivatives,
|
||||
shareAlike: !details.noDerivatives ? false : details.shareAlike,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Card.Header
|
||||
title={(
|
||||
<div className="d-flex flex-row flex-row">
|
||||
<Icon src={Nd} />
|
||||
<FormattedMessage {...messages.noDerivativesCheckboxLabel} />
|
||||
</div>
|
||||
)}
|
||||
actions={(
|
||||
<Form.Checkbox
|
||||
checked={details.noDerivatives}
|
||||
disabled={level !== LicenseLevel.block}
|
||||
onChange={(e) => updateField({
|
||||
licenseDetails: {
|
||||
...details,
|
||||
noDerivatives: e.target.checked,
|
||||
shareAlike: e.target.checked ? false : details.shareAlike,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Section>
|
||||
<FormattedMessage {...messages.noDerivativesSectionDescription} />
|
||||
</Card.Section>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
isClickable
|
||||
onClick={() => updateField({
|
||||
licenseDetails: {
|
||||
...details,
|
||||
shareAlike: !details.shareAlike,
|
||||
noDerivatives: !details.shareAlike ? false : details.noDerivatives,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Card.Header
|
||||
title={(
|
||||
<div className="d-flex flex-row flex-row">
|
||||
<Icon src={Sa} />
|
||||
<FormattedMessage {...messages.shareAlikeCheckboxLabel} />
|
||||
</div>
|
||||
)}
|
||||
actions={(
|
||||
<Form.Checkbox
|
||||
checked={details.shareAlike}
|
||||
disabled={level !== LicenseLevel.block}
|
||||
onChange={(e) => updateField({
|
||||
licenseDetails: {
|
||||
...details,
|
||||
shareAlike: e.target.checked,
|
||||
noDerivatives: e.target.checked ? false : details.noDerivatives,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Section>
|
||||
<FormattedMessage {...messages.shareAlikeSectionDescription} />
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</Stack>
|
||||
)
|
||||
: null}
|
||||
|
||||
</Form.Group>
|
||||
</div>
|
||||
) : 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));
|
||||
@@ -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(<LicenseDetails {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with level set to library', () => {
|
||||
expect(
|
||||
shallow(<LicenseDetails {...props} level="library" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with level set to block and license set to select', () => {
|
||||
expect(
|
||||
shallow(<LicenseDetails {...props} level="block" license="select" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with level set to block and license set to all rights reserved', () => {
|
||||
expect(
|
||||
shallow(<LicenseDetails {...props} level="block" license="all-rights-reserved" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with level set to block and license set to Creative Commons', () => {
|
||||
expect(
|
||||
shallow(<LicenseDetails {...props} level="block" license="creative-commons" />),
|
||||
).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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<Stack gap={3} className="border-primary-100 border-top">
|
||||
<FormattedMessage {...messages.displaySubsectionTitle} />
|
||||
<Card className="mb-3">
|
||||
<Card.Header title={<LicenseBlurb license={license} details={details} />} />
|
||||
<Card.Section>{licenseDescription}</Card.Section>
|
||||
</Card>
|
||||
{level !== LicenseLevel.course ? (
|
||||
<Hyperlink destination="https://creativecommons.org/about" target="_blank">
|
||||
<FormattedMessage {...messages.viewLicenseDetailsLabel} />
|
||||
</Hyperlink>
|
||||
) : null }
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
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);
|
||||
@@ -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(<LicenseDisplay {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with level set to library', () => {
|
||||
expect(
|
||||
shallow(<LicenseDisplay {...props} level="library" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with level set to block', () => {
|
||||
expect(
|
||||
shallow(<LicenseDisplay {...props} level="block" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with level set to block and license set to select', () => {
|
||||
expect(
|
||||
shallow(<LicenseDisplay {...props} level="block" license="select" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with level set to block and license set to Creative Commons', () => {
|
||||
expect(
|
||||
shallow(<LicenseDisplay {...props} level="block" license="creative-commons" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<Form.Group className="mt-2 mx-2">
|
||||
<Form.Row className="mt-4.5">
|
||||
<Form.Control
|
||||
as="select"
|
||||
ref={ref}
|
||||
defaultValue={license}
|
||||
disabled={level !== LicenseLevel.block}
|
||||
floatingLabel={intl.formatMessage(messages.licenseTypeLabel)}
|
||||
onChange={(e) => onLicenseChange(e.target.value)}
|
||||
>
|
||||
{Object.entries(LicenseNames).map(([key, text]) => {
|
||||
if (license === key) { return (<option value={LicenseTypes[key]} selected>{text}</option>); }
|
||||
if (key === LicenseTypes.select) { return (<option hidden>{text}</option>); }
|
||||
return (<option value={LicenseTypes[key]}>{text}</option>);
|
||||
})}
|
||||
</Form.Control>
|
||||
{level === LicenseLevel.block ? (
|
||||
<IconButtonWithTooltip
|
||||
iconAs={Icon}
|
||||
src={Delete}
|
||||
onClick={() => {
|
||||
ref.current.value = courseLicenseType;
|
||||
updateField({ licenseType: '', licenseDetails: {} });
|
||||
}}
|
||||
tooltipPlacement="top"
|
||||
tooltipContent={<FormattedMessage {...messages.deleteLicenseSelection} />}
|
||||
/>
|
||||
) : null }
|
||||
</Form.Row>
|
||||
<Form.Text>{levelDescription}</Form.Text>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
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));
|
||||
@@ -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(<LicenseSelector {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with library level', () => {
|
||||
expect(
|
||||
shallow(<LicenseSelector {...props} level="library" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with block level', () => {
|
||||
expect(
|
||||
shallow(<LicenseSelector {...props} level="block" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with no license', () => {
|
||||
expect(
|
||||
shallow(<LicenseSelector {...props} license="" />),
|
||||
).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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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`] = `
|
||||
<div
|
||||
className="d-flex flex-row flex-row"
|
||||
>
|
||||
<Icon />
|
||||
<Icon />
|
||||
<Icon />
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="Some Rights Reserved"
|
||||
description="Label for row of creative common icons"
|
||||
id="authoring.videoeditor.license.creativeCommonsIcons.label"
|
||||
/>
|
||||
</Form.Text>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LicenseBlurb snapshots snapshots: renders as expected when details.attribution and details.noncommercial equal true 1`] = `
|
||||
<div
|
||||
className="d-flex flex-row flex-row"
|
||||
>
|
||||
<Icon />
|
||||
<Icon />
|
||||
<Icon />
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="Some Rights Reserved"
|
||||
description="Label for row of creative common icons"
|
||||
id="authoring.videoeditor.license.creativeCommonsIcons.label"
|
||||
/>
|
||||
</Form.Text>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LicenseBlurb snapshots snapshots: renders as expected when details.attribution and details.shareAlike equal true 1`] = `
|
||||
<div
|
||||
className="d-flex flex-row flex-row"
|
||||
>
|
||||
<Icon />
|
||||
<Icon />
|
||||
<Icon />
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="Some Rights Reserved"
|
||||
description="Label for row of creative common icons"
|
||||
id="authoring.videoeditor.license.creativeCommonsIcons.label"
|
||||
/>
|
||||
</Form.Text>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LicenseBlurb snapshots snapshots: renders as expected when details.attribution equal true 1`] = `
|
||||
<div
|
||||
className="d-flex flex-row flex-row"
|
||||
>
|
||||
<Icon />
|
||||
<Icon />
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="Some Rights Reserved"
|
||||
description="Label for row of creative common icons"
|
||||
id="authoring.videoeditor.license.creativeCommonsIcons.label"
|
||||
/>
|
||||
</Form.Text>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LicenseBlurb snapshots snapshots: renders as expected with default props 1`] = `
|
||||
<div
|
||||
className="d-flex flex-row flex-row"
|
||||
>
|
||||
<Icon />
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="All Rights Reserved"
|
||||
description="Label for row of all rights reserved icons"
|
||||
id="authoring.videoeditor.license.allRightsReservedIcons.label"
|
||||
/>
|
||||
</Form.Text>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LicenseBlurb snapshots snapshots: renders as expected with license equal to Creative Commons 1`] = `
|
||||
<div
|
||||
className="d-flex flex-row flex-row"
|
||||
>
|
||||
<Icon />
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="Some Rights Reserved"
|
||||
description="Label for row of creative common icons"
|
||||
id="authoring.videoeditor.license.creativeCommonsIcons.label"
|
||||
/>
|
||||
</Form.Text>
|
||||
</div>
|
||||
`;
|
||||
@@ -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`] = `
|
||||
<div
|
||||
className="border-primary-100 border-top pb-3"
|
||||
>
|
||||
<Form.Group>
|
||||
<Form.Label
|
||||
className="mt-3"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="License Details"
|
||||
description="Title for license detatils subsection"
|
||||
id="authoring.videoeditor.license.detailsSubsection.title"
|
||||
/>
|
||||
</Form.Label>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<Card>
|
||||
<Card.Header
|
||||
actions={
|
||||
<Form.Checkbox
|
||||
checked={true}
|
||||
disabled={true}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<div
|
||||
className="d-flex flex-row flex-nowrap"
|
||||
>
|
||||
<Icon />
|
||||
<FormattedMessage
|
||||
defaultMessage="Attribution"
|
||||
description="Label for attribution checkbox"
|
||||
id="authoring.videoeditor.license.attributionCheckboxLabel"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Card.Section>
|
||||
<FormattedMessage
|
||||
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"
|
||||
id="authoring.videoeditor.license.attributionSectionDescription"
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
<Card
|
||||
isClickable={true}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Card.Header
|
||||
actions={
|
||||
<Form.Checkbox
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<div
|
||||
className="d-flex flex-row flex-row"
|
||||
>
|
||||
<Icon />
|
||||
<FormattedMessage
|
||||
defaultMessage="Noncommercial"
|
||||
description="Label for noncommercial checkbox"
|
||||
id="authoring.videoeditor.license.noncommercialCheckboxLabel"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Card.Section>
|
||||
<FormattedMessage
|
||||
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"
|
||||
id="authoring.videoeditor.license.noncommercialSectionDescription"
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
<Card
|
||||
isClickable={true}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Card.Header
|
||||
actions={
|
||||
<Form.Checkbox
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<div
|
||||
className="d-flex flex-row flex-row"
|
||||
>
|
||||
<Icon />
|
||||
<FormattedMessage
|
||||
defaultMessage="No Derivatives"
|
||||
description="Label for No Derivatives checkbox"
|
||||
id="authoring.videoeditor.license.noDerivativesCheckboxLabel"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Card.Section>
|
||||
<FormattedMessage
|
||||
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"
|
||||
id="authoring.videoeditor.license.noDerivativesSectionDescription"
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
<Card
|
||||
isClickable={true}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Card.Header
|
||||
actions={
|
||||
<Form.Checkbox
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<div
|
||||
className="d-flex flex-row flex-row"
|
||||
>
|
||||
<Icon />
|
||||
<FormattedMessage
|
||||
defaultMessage="Share Alike"
|
||||
description="Label for Share Alike checkbox"
|
||||
id="authoring.videoeditor.license.shareAlikeCheckboxLabel"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Card.Section>
|
||||
<FormattedMessage
|
||||
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"
|
||||
id="authoring.videoeditor.license.shareAlikeSectionDescription"
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Form.Group>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LicenseDetails snapshots snapshots: renders as expected with level set to block and license set to all rights reserved 1`] = `
|
||||
<div
|
||||
className="border-primary-100 border-top pb-3"
|
||||
>
|
||||
<Form.Group>
|
||||
<Form.Label
|
||||
className="mt-3"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="License Details"
|
||||
description="Title for license detatils subsection"
|
||||
id="authoring.videoeditor.license.detailsSubsection.title"
|
||||
/>
|
||||
</Form.Label>
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="You reserve all rights for your work."
|
||||
description="All Rights Reserved section message"
|
||||
id="authoring.videoeditor.license.allRightsReservedSectionMessage"
|
||||
/>
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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`] = `""`;
|
||||
@@ -0,0 +1,145 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LicenseDisplay snapshots snapshots: renders as expected with default props 1`] = `
|
||||
<Stack
|
||||
className="border-primary-100 border-top"
|
||||
gap={3}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="License Display"
|
||||
description="Title for license display subsection"
|
||||
id="authoring.videoeditor.license.displaySubsection.title"
|
||||
/>
|
||||
<Card
|
||||
className="mb-3"
|
||||
>
|
||||
<Card.Header
|
||||
title={
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
license="all-rights-reserved"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Card.Section>
|
||||
FormattedMessage component with license description
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`LicenseDisplay snapshots snapshots: renders as expected with level set to block 1`] = `
|
||||
<Stack
|
||||
className="border-primary-100 border-top"
|
||||
gap={3}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="License Display"
|
||||
description="Title for license display subsection"
|
||||
id="authoring.videoeditor.license.displaySubsection.title"
|
||||
/>
|
||||
<Card
|
||||
className="mb-3"
|
||||
>
|
||||
<Card.Header
|
||||
title={
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
license="all-rights-reserved"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Card.Section>
|
||||
FormattedMessage component with license description
|
||||
</Card.Section>
|
||||
</Card>
|
||||
<Hyperlink
|
||||
destination="https://creativecommons.org/about"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="View license details"
|
||||
description="Label for view license details button"
|
||||
id="authoring.videoeditor.license.viewLicenseDetailsLabel.label"
|
||||
/>
|
||||
</Hyperlink>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`LicenseDisplay snapshots snapshots: renders as expected with level set to block and license set to Creative Commons 1`] = `
|
||||
<Stack
|
||||
className="border-primary-100 border-top"
|
||||
gap={3}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="License Display"
|
||||
description="Title for license display subsection"
|
||||
id="authoring.videoeditor.license.displaySubsection.title"
|
||||
/>
|
||||
<Card
|
||||
className="mb-3"
|
||||
>
|
||||
<Card.Header
|
||||
title={
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
license="creative-commons"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Card.Section>
|
||||
FormattedMessage component with license description
|
||||
</Card.Section>
|
||||
</Card>
|
||||
<Hyperlink
|
||||
destination="https://creativecommons.org/about"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="View license details"
|
||||
description="Label for view license details button"
|
||||
id="authoring.videoeditor.license.viewLicenseDetailsLabel.label"
|
||||
/>
|
||||
</Hyperlink>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
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`] = `
|
||||
<Stack
|
||||
className="border-primary-100 border-top"
|
||||
gap={3}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="License Display"
|
||||
description="Title for license display subsection"
|
||||
id="authoring.videoeditor.license.displaySubsection.title"
|
||||
/>
|
||||
<Card
|
||||
className="mb-3"
|
||||
>
|
||||
<Card.Header
|
||||
title={
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
license="all-rights-reserved"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Card.Section>
|
||||
FormattedMessage component with license description
|
||||
</Card.Section>
|
||||
</Card>
|
||||
<Hyperlink
|
||||
destination="https://creativecommons.org/about"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="View license details"
|
||||
description="Label for view license details button"
|
||||
id="authoring.videoeditor.license.viewLicenseDetailsLabel.label"
|
||||
/>
|
||||
</Hyperlink>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -0,0 +1,177 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LicenseSelector snapshots snapshots: renders as expected with block level 1`] = `
|
||||
<Form.Group
|
||||
className="mt-2 mx-2"
|
||||
>
|
||||
<Form.Row
|
||||
className="mt-4.5"
|
||||
>
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue="all-rights-reserved"
|
||||
disabled={false}
|
||||
floatingLabel="License Type"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<option
|
||||
hidden={true}
|
||||
>
|
||||
Select
|
||||
</option>
|
||||
<option
|
||||
value="all-rights-reserved"
|
||||
>
|
||||
All Rights Reserved
|
||||
</option>
|
||||
<option
|
||||
value="creative-commons"
|
||||
>
|
||||
Creative Commons
|
||||
</option>
|
||||
</Form.Control>
|
||||
<IconButtonWithTooltip
|
||||
iconAs="Icon"
|
||||
onClick={[Function]}
|
||||
tooltipContent={
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete"
|
||||
description="Message presented to user for action to delete license selection"
|
||||
id="authoring.videoeditor.license.deleteLicenseSelection"
|
||||
/>
|
||||
}
|
||||
tooltipPlacement="top"
|
||||
/>
|
||||
</Form.Row>
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="This license is set specifically for this video"
|
||||
description="Helper text for license type when choosing for a spcific video"
|
||||
id="authoring.videoeditor.license.defaultLevelDescription.helperText"
|
||||
/>
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`LicenseSelector snapshots snapshots: renders as expected with default props 1`] = `
|
||||
<Form.Group
|
||||
className="mt-2 mx-2"
|
||||
>
|
||||
<Form.Row
|
||||
className="mt-4.5"
|
||||
>
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue="all-rights-reserved"
|
||||
disabled={true}
|
||||
floatingLabel="License Type"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<option
|
||||
hidden={true}
|
||||
>
|
||||
Select
|
||||
</option>
|
||||
<option
|
||||
value="all-rights-reserved"
|
||||
>
|
||||
All Rights Reserved
|
||||
</option>
|
||||
<option
|
||||
value="creative-commons"
|
||||
>
|
||||
Creative Commons
|
||||
</option>
|
||||
</Form.Control>
|
||||
</Form.Row>
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="This license currently set at the course level"
|
||||
description="Helper text for license type when using course license"
|
||||
id="authoring.videoeditor.license.courseLevelDescription.helperText"
|
||||
/>
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`LicenseSelector snapshots snapshots: renders as expected with library level 1`] = `
|
||||
<Form.Group
|
||||
className="mt-2 mx-2"
|
||||
>
|
||||
<Form.Row
|
||||
className="mt-4.5"
|
||||
>
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue="all-rights-reserved"
|
||||
disabled={true}
|
||||
floatingLabel="License Type"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<option
|
||||
hidden={true}
|
||||
>
|
||||
Select
|
||||
</option>
|
||||
<option
|
||||
value="all-rights-reserved"
|
||||
>
|
||||
All Rights Reserved
|
||||
</option>
|
||||
<option
|
||||
value="creative-commons"
|
||||
>
|
||||
Creative Commons
|
||||
</option>
|
||||
</Form.Control>
|
||||
</Form.Row>
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="This license currently set at the library level"
|
||||
description="Helper text for license type when using library license"
|
||||
id="authoring.videoeditor.license.libraryLevelDescription.helperText"
|
||||
/>
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`LicenseSelector snapshots snapshots: renders as expected with no license 1`] = `
|
||||
<Form.Group
|
||||
className="mt-2 mx-2"
|
||||
>
|
||||
<Form.Row
|
||||
className="mt-4.5"
|
||||
>
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue=""
|
||||
disabled={true}
|
||||
floatingLabel="License Type"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<option
|
||||
hidden={true}
|
||||
>
|
||||
Select
|
||||
</option>
|
||||
<option
|
||||
value="all-rights-reserved"
|
||||
>
|
||||
All Rights Reserved
|
||||
</option>
|
||||
<option
|
||||
value="creative-commons"
|
||||
>
|
||||
Creative Commons
|
||||
</option>
|
||||
</Form.Control>
|
||||
</Form.Row>
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="This license currently set at the course level"
|
||||
description="Helper text for license type when using course license"
|
||||
id="authoring.videoeditor.license.courseLevelDescription.helperText"
|
||||
/>
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
`;
|
||||
@@ -0,0 +1,155 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LicenseWidget snapshots snapshots: renders as expected with default props 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
subtitle={
|
||||
<div>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
license="all-rights-reserved"
|
||||
/>
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="This license currently set at the course level"
|
||||
description="Helper text for license type when using course license"
|
||||
id="authoring.videoeditor.license.courseLevelDescription.helperText"
|
||||
/>
|
||||
</Form.Text>
|
||||
</div>
|
||||
}
|
||||
title="License"
|
||||
>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
level="course"
|
||||
license="all-rights-reserved"
|
||||
/>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
level="course"
|
||||
license="all-rights-reserved"
|
||||
/>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
level="course"
|
||||
license="all-rights-reserved"
|
||||
licenseDescription={
|
||||
<FormattedMessage
|
||||
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"
|
||||
id="authoring.videoeditor.license.courseLicenseDescription.message"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="border-primary-100 border-bottom"
|
||||
/>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a license for this video"
|
||||
description="Label for add license button"
|
||||
id="authoring.videoeditor.license.add.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
`;
|
||||
|
||||
exports[`LicenseWidget snapshots snapshots: renders as expected with isLibrary true 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
subtitle={
|
||||
<div>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
license="all-rights-reserved"
|
||||
/>
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="This license currently set at the library level"
|
||||
description="Helper text for license type when using library license"
|
||||
id="authoring.videoeditor.license.libraryLevelDescription.helperText"
|
||||
/>
|
||||
</Form.Text>
|
||||
</div>
|
||||
}
|
||||
title="License"
|
||||
>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
level="library"
|
||||
license="all-rights-reserved"
|
||||
/>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
level="library"
|
||||
license="all-rights-reserved"
|
||||
/>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
level="library"
|
||||
license="all-rights-reserved"
|
||||
licenseDescription={
|
||||
<FormattedMessage
|
||||
defaultMessage="Licenses set at the library level appear at the specific library video."
|
||||
description="Message explaining where library level licenses are set"
|
||||
id="authoring.videoeditor.license.libraryLicenseDescription.message"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
`;
|
||||
|
||||
exports[`LicenseWidget snapshots snapshots: renders as expected with licenseType defined 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
subtitle={
|
||||
<div>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
license="all-rights-reserved"
|
||||
/>
|
||||
<Form.Text>
|
||||
<FormattedMessage
|
||||
defaultMessage="This license is set specifically for this video"
|
||||
description="Helper text for license type when choosing for a spcific video"
|
||||
id="authoring.videoeditor.license.defaultLevelDescription.helperText"
|
||||
/>
|
||||
</Form.Text>
|
||||
</div>
|
||||
}
|
||||
title="License"
|
||||
>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
level="block"
|
||||
license="all-rights-reserved"
|
||||
/>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
level="block"
|
||||
license="all-rights-reserved"
|
||||
/>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
details={Object {}}
|
||||
level="block"
|
||||
license="all-rights-reserved"
|
||||
licenseDescription={
|
||||
<FormattedMessage
|
||||
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"
|
||||
id="authoring.videoeditor.license.defaultLicenseDescription.message"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
`;
|
||||
@@ -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 = <FormattedMessage {...messages.courseLevelDescription} />;
|
||||
licenseDescription = <FormattedMessage {...messages.courseLicenseDescription} />;
|
||||
break;
|
||||
case LicenseLevel.library:
|
||||
levelDescription = <FormattedMessage {...messages.libraryLevelDescription} />;
|
||||
licenseDescription = <FormattedMessage {...messages.libraryLicenseDescription} />;
|
||||
break;
|
||||
default: // default to block
|
||||
levelDescription = <FormattedMessage {...messages.defaultLevelDescription} />;
|
||||
licenseDescription = <FormattedMessage {...messages.defaultLicenseDescription} />;
|
||||
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,
|
||||
};
|
||||
@@ -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: <FormattedMessage {...messages.courseLevelDescription} />,
|
||||
licenseDescription: <FormattedMessage {...messages.courseLicenseDescription} />,
|
||||
});
|
||||
});
|
||||
it('returns expected level and license description for library level', () => {
|
||||
expect(module.determineText({ level: 'library' })).toEqual({
|
||||
levelDescription: <FormattedMessage {...messages.libraryLevelDescription} />,
|
||||
licenseDescription: <FormattedMessage {...messages.libraryLicenseDescription} />,
|
||||
});
|
||||
});
|
||||
it('returns expected level and license description for library level', () => {
|
||||
expect(module.determineText({ level: 'default' })).toEqual({
|
||||
levelDescription: <FormattedMessage {...messages.defaultLevelDescription} />,
|
||||
licenseDescription: <FormattedMessage {...messages.defaultLicenseDescription} />,
|
||||
});
|
||||
});
|
||||
});
|
||||
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 } });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<CollapsibleFormWidget
|
||||
subtitle={(
|
||||
<div>
|
||||
<LicenseBlurb license={license} details={details} />
|
||||
<Form.Text>{levelDescription}</Form.Text>
|
||||
</div>
|
||||
)}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
>
|
||||
<Stack gap={3}>
|
||||
|
||||
{license ? (
|
||||
<>
|
||||
<LicenseSelector license={license} level={level} />
|
||||
<LicenseDetails license={license} details={details} level={level} />
|
||||
<LicenseDisplay
|
||||
license={license}
|
||||
details={details}
|
||||
licenseDescription={licenseDescription}
|
||||
level={level}
|
||||
/>
|
||||
</>
|
||||
) : null }
|
||||
{!licenseType ? (
|
||||
<>
|
||||
<div className="border-primary-100 border-bottom" />
|
||||
<Button
|
||||
iconBefore={Add}
|
||||
variant="link"
|
||||
onClick={() => updateField({ licenseType: 'select', licenseDetails: {} })}
|
||||
>
|
||||
<FormattedMessage {...messages.addLicenseButtonLabel} />
|
||||
</Button>
|
||||
</>
|
||||
) : null }
|
||||
</Stack>
|
||||
</CollapsibleFormWidget>
|
||||
);
|
||||
};
|
||||
|
||||
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));
|
||||
@@ -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(<LicenseWidget {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with isLibrary true', () => {
|
||||
expect(
|
||||
shallow(<LicenseWidget {...props} isLibrary licenseType="all-rights-reserved" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with licenseType defined', () => {
|
||||
expect(
|
||||
shallow(<LicenseWidget {...props} licenseType="all-rights-reserved" />),
|
||||
).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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
>
|
||||
<div className="video-editor">
|
||||
<VideoEditorModal />
|
||||
</div>
|
||||
{studioViewFinished ? (
|
||||
<div className="video-editor">
|
||||
<VideoEditorModal />
|
||||
</div>
|
||||
) : (
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
screenreadertext={intl.formatMessage(messages.spinnerScreenReaderText)}
|
||||
/>
|
||||
)}
|
||||
</EditorContainer>
|
||||
</ErrorContext.Provider>
|
||||
);
|
||||
@@ -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));
|
||||
|
||||
@@ -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(<VideoEditor {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('renders as expected with default behavior', () => {
|
||||
expect(shallow(<VideoEditor {...props} studioViewFinished />)).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', () => {
|
||||
|
||||
9
src/editors/containers/VideoEditor/messages.js
Normal file
9
src/editors/containers/VideoEditor/messages.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export const messages = {
|
||||
spinnerScreenReaderText: {
|
||||
id: 'authoring.videoEditor.spinnerScreenReaderText',
|
||||
defaultMessage: 'loading',
|
||||
description: 'Loading message for spinner screenreader text.',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -19,4 +19,5 @@ export const RequestKeys = StrictDict({
|
||||
uploadThumbnail: 'uploadThumbnail',
|
||||
uploadTranscript: 'uploadTranscript',
|
||||
deleteTranscript: 'deleteTranscript',
|
||||
fetchCourseDetails: 'fetchCourseDetails',
|
||||
});
|
||||
|
||||
@@ -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 }),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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] }), {});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: '<p>Test prompt content</p>',
|
||||
data: '<p>Test prompt content</p> <div data-metadata="license, "value": "all-rights-reserved", "type": " />',
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,6 +88,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
ErrorContext: {
|
||||
Provider: 'ErrorContext.Provider',
|
||||
},
|
||||
Hyperlink: 'Hyperlink',
|
||||
Icon: 'Icon',
|
||||
IconButton: 'IconButton',
|
||||
IconButtonWithTooltip: 'IconButtonWithTooltip',
|
||||
|
||||
18
www/package-lock.json
generated
18
www/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user