feat: license widget (#132)

This commit is contained in:
Raymond Zhou
2022-11-02 07:41:40 -07:00
committed by GitHub
parent 9c397d8802
commit 79ae64b562
48 changed files with 2328 additions and 144 deletions

14
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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"
>

View File

@@ -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;

View File

@@ -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);

View File

@@ -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();
});
});
});

View File

@@ -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));

View File

@@ -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));
});
});
});

View File

@@ -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);

View File

@@ -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();
});
});
});

View File

@@ -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));

View File

@@ -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));
});
});
});

View File

@@ -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>
`;

View File

@@ -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`] = `""`;

View File

@@ -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>
`;

View File

@@ -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>
`;

View File

@@ -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)>
`;

View File

@@ -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,
};

View File

@@ -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 } });
});
});
});

View File

@@ -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));

View File

@@ -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));
});
});
});

View File

@@ -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;

View File

@@ -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));

View File

@@ -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', () => {

View 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;

View File

@@ -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,
};

View File

@@ -19,4 +19,5 @@ export const RequestKeys = StrictDict({
uploadThumbnail: 'uploadThumbnail',
uploadTranscript: 'uploadTranscript',
deleteTranscript: 'deleteTranscript',
fetchCourseDetails: 'fetchCourseDetails',
});

View File

@@ -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 }),
},
});

View File

@@ -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', () => {

View File

@@ -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 },
};

View File

@@ -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,
});

View File

@@ -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', () => {

View File

@@ -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,
});

View File

@@ -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;

View File

@@ -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(/&#34;/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);
};

View File

@@ -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 &#34;license&#34; &#34;value&#34;= null, &#34;type&#34;';
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 &#34;license&#34; &#34;value&#34;: &#34;all-rights-reserved&#34;, &#34;type&#34;';
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 &#34;license&#34; &#34;value&#34;: &#34;creative-commons: ver=4.0 BY NC ND&#34;, &#34;type&#34;';
expect(thunkActions.parseLicense({
licenseData: license,
level: 'block',
})).toEqual([
'creative-commons',
{
by: true,

View File

@@ -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,

View File

@@ -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] }), {});

View File

@@ -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) => {

View File

@@ -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');
});
});
});

View File

@@ -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,

View File

@@ -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,

View File

@@ -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}`
);

View File

@@ -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}`);
});
});
});

View File

@@ -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
View File

@@ -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",

View File

@@ -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",