@edx/studio-frontend
Advanced tools
Comparing version 0.7.1 to 0.8.0
{ | ||
"name": "@edx/studio-frontend", | ||
"version": "0.7.1", | ||
"version": "0.8.0", | ||
"description": "The frontend for the Open edX platform", | ||
@@ -16,3 +16,3 @@ "repository": "edx/studio-frontend", | ||
"@edx/edx-bootstrap": "^0.4.3", | ||
"@edx/paragon": "^1.6.0", | ||
"@edx/paragon": "^1.7.1", | ||
"babel-polyfill": "^6.26.0", | ||
@@ -19,0 +19,0 @@ "classnames": "^2.2.5", |
@@ -24,8 +24,21 @@ import React from 'react'; | ||
it('correct number of filters', () => { | ||
expect(wrapper.find('li')).toHaveLength(5); | ||
const checkBoxGroup = wrapper.find('CheckBoxGroup'); | ||
expect(checkBoxGroup).toHaveLength(1); | ||
expect(checkBoxGroup.find('[type="checkbox"]')).toHaveLength(5); | ||
}); | ||
it('correct styling', () => { | ||
expect(wrapper.find('ul').hasClass('filter-set')).toEqual(true); | ||
expect(wrapper.find('h4')).toHaveLength(1); | ||
expect(wrapper.find('h4').hasClass('filter-heading')).toEqual(true); | ||
expect(wrapper.find('div').at(1).hasClass('filter-set')).toEqual(true); | ||
}); | ||
it('handles onChange callback correctly', () => { | ||
const checkBoxGroup = wrapper.find('CheckBoxGroup'); | ||
let checkBoxes = checkBoxGroup.find('[type="checkbox"]'); | ||
checkBoxes.first().simulate('change', { target: { checked: true, type: 'checkbox' } }); | ||
checkBoxes = checkBoxGroup.find('[type="checkbox"]'); | ||
expect(checkBoxes.first().html()).toContain('checked'); | ||
}); | ||
}); | ||
}); |
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import CheckBoxGroup from '@edx/paragon/src/CheckBoxGroup'; | ||
import CheckBox from '@edx/paragon/src/CheckBox'; | ||
@@ -33,14 +34,19 @@ import { connect } from 'react-redux'; | ||
export const AssetsFilters = ({ assetsFilters, updateFilter }) => ( | ||
<ul className={styles['filter-set']}> | ||
{ASSET_TYPES.map(type => ( | ||
<li key={type.key}> | ||
<CheckBox | ||
name={type.key} | ||
label={type.displayName} | ||
checked={assetsFilters[type.key]} | ||
onChange={checked => updateFilter(type.key, checked)} | ||
/> | ||
</li> | ||
))} | ||
</ul> | ||
<div role="group" aria-labelledby="filter-label"> | ||
<h4 id="filter-label" className={styles['filter-heading']}>Filter by File Type</h4> | ||
<div className={styles['filter-set']}> | ||
<CheckBoxGroup> | ||
{ASSET_TYPES.map(type => ( | ||
<CheckBox | ||
key={type.key} | ||
id={type.key} | ||
name={type.key} | ||
label={type.displayName} | ||
checked={assetsFilters[type.key]} | ||
onChange={checked => updateFilter(type.key, checked)} | ||
/> | ||
))} | ||
</CheckBoxGroup> | ||
</div> | ||
</div> | ||
); | ||
@@ -47,0 +53,0 @@ |
@@ -188,3 +188,3 @@ import React from 'react'; | ||
} else { | ||
expect(row.containsMatchingElement(<td>Preview not available</td>)).toEqual(true); | ||
expect(row.find('.no-image-preview')).toHaveLength(1); | ||
} | ||
@@ -525,4 +525,4 @@ }); | ||
describe('Lock asset', () => { | ||
const getLockedButtons = () => wrapper.find('button > .fa-lock'); | ||
const getUnlockedButtons = () => wrapper.find('button > .fa-unlock'); | ||
const getLockedButtons = () => wrapper.find('button .fa-lock'); | ||
const getUnlockedButtons = () => wrapper.find('button .fa-unlock'); | ||
const getLockingButtons = () => wrapper.find('button > .fa-spinner'); | ||
@@ -529,0 +529,0 @@ beforeEach(() => { |
@@ -12,2 +12,3 @@ import React from 'react'; | ||
import FontAwesomeStyles from 'font-awesome/css/font-awesome.min.css'; | ||
import styles from './AssetsTable.scss'; | ||
import { assetActions } from '../../data/constants/actionTypes'; | ||
@@ -121,7 +122,10 @@ import { assetLoading } from '../../data/constants/loadingTypes'; | ||
const baseUrl = this.props.courseDetails.base_url || ''; | ||
return thumbnail ? (<img src={`${baseUrl}${thumbnail}`} alt="Description not available" />) : 'Preview not available'; | ||
if (thumbnail) { | ||
return (<img src={`${baseUrl}${thumbnail}`} alt="Description not available" />); | ||
} | ||
return (<div className={styles['no-image-preview']}>Preview not available</div>); | ||
} | ||
getLockButton(asset) { | ||
const classes = [FontAwesomeStyles.fa]; | ||
const classes = [FontAwesomeStyles.fa, styles['button-primary-outline']]; | ||
let lockState; | ||
@@ -136,3 +140,4 @@ if (asset.locked) { | ||
return (<Button | ||
label={(<span className={classNames(...classes)} />)} | ||
className={classes} | ||
label={''} | ||
data-asset-id={asset.id} | ||
@@ -165,5 +170,7 @@ aria-label={`${lockState} ${asset.display_name}`} | ||
getLoadingLockButton(asset) { | ||
const classes = [FontAwesomeStyles.fa, FontAwesomeStyles['fa-spinner'], FontAwesomeStyles['fa-spin']]; | ||
// spinner classes are applied to the span to keep the whole button from spinning | ||
const spinnerClasses = [FontAwesomeStyles.fa, FontAwesomeStyles['fa-spinner'], FontAwesomeStyles['fa-spin']]; | ||
return (<Button | ||
label={(<span className={classNames(...classes)} />)} | ||
className={[styles['button-primary-outline']]} | ||
label={(<span className={classNames(...spinnerClasses)} />)} | ||
aria-label={`Updating lock status for ${asset.display_name}`} | ||
@@ -238,3 +245,3 @@ />); | ||
<span> | ||
{studioUrl && this.getCopyUrlButton(assetDisplayName, studioUrl, 'Studio')} | ||
{studioUrl && this.getCopyUrlButton(assetDisplayName, studioUrl, 'Studio', [styles['studio-copy-button']])} | ||
{webUrl && this.getCopyUrlButton(assetDisplayName, webUrl, 'Web')} | ||
@@ -245,6 +252,6 @@ </span> | ||
getCopyUrlButton(assetDisplayName, url, label) { | ||
getCopyUrlButton(assetDisplayName, url, label, classes = []) { | ||
const buttonLabel = ( | ||
<span> | ||
<span className={classNames(FontAwesomeStyles.fa, FontAwesomeStyles['fa-files-o'])} aria-hidden /> | ||
<span className={classNames(FontAwesomeStyles.fa, FontAwesomeStyles['fa-files-o'], styles['copy-icon'])} aria-hidden /> | ||
{label} | ||
@@ -258,2 +265,3 @@ </span> | ||
label={buttonLabel} | ||
className={classes} | ||
textToCopy={url} | ||
@@ -278,3 +286,3 @@ onCopyButtonClick={this.onCopyButtonClick} | ||
key={currentAsset.id} | ||
className={[FontAwesomeStyles.fa, FontAwesomeStyles['fa-trash']]} | ||
className={[FontAwesomeStyles.fa, FontAwesomeStyles['fa-trash'], styles['button-primary-outline']]} | ||
label={''} | ||
@@ -281,0 +289,0 @@ aria-label={`Delete ${currentAsset.display_name}`} |
@@ -64,3 +64,3 @@ import React from 'react'; | ||
<Button | ||
className={[styles['copy-button']]} | ||
className={[...this.props.className, styles['copy-button']]} | ||
aria-label={this.props.ariaLabel} | ||
@@ -84,2 +84,3 @@ label={label} | ||
onCopyButtonClick: PropTypes.func, | ||
className: PropTypes.arrayOf(PropTypes.string), | ||
}; | ||
@@ -90,2 +91,3 @@ | ||
ariaLabel: 'Copy', | ||
className: [], | ||
}; |
@@ -6,3 +6,3 @@ import configureStore from 'redux-mock-store'; | ||
import endpoints from '../api/endpoints'; | ||
import { submitAccessibilityForm, clearAccessibilityStatus } from './accessibility'; | ||
import * as actionCreators from './accessibility'; | ||
import { accessibilityActions } from '../../data/constants/actionTypes'; | ||
@@ -20,3 +20,7 @@ | ||
describe('Accessibility Action Creator', () => { | ||
describe('Accessibility Action Creators', () => { | ||
beforeEach(() => { | ||
store = mockStore(initialState); | ||
}); | ||
afterEach(() => { | ||
@@ -26,3 +30,30 @@ fetchMock.reset(); | ||
}); | ||
it('returns expected state from submitAccessibilityFormSuccess', () => { | ||
const response = { | ||
status: 200, | ||
}; | ||
const expectedAction = { | ||
statusCode: response.status, | ||
type: accessibilityActions.ACCESSIBILITY_FORM_SUBMIT_SUCCESS, | ||
}; | ||
expect(store.dispatch( | ||
actionCreators.submitAccessibilityFormSuccess(response))).toEqual(expectedAction); | ||
}); | ||
it('returns expected state from submitAccessibilityFormRateLimitFailure', () => { | ||
const response = { | ||
status: 429, | ||
detail: 'You have hit the rate limit!', | ||
}; | ||
const expectedAction = { | ||
statusCode: 429, | ||
failureDetails: response.detail, | ||
type: accessibilityActions.ACCESSIBILITY_FORM_SUBMIT_RATE_LIMIT_FAILURE, | ||
}; | ||
expect(store.dispatch( | ||
actionCreators.submitAccessibilityFormRateLimitFailure(response))).toEqual(expectedAction); | ||
}); | ||
it('returns expected state from success', () => { | ||
@@ -33,4 +64,4 @@ fetchMock.once(zendeskEndpoint, 201); | ||
]; | ||
store = mockStore(initialState); | ||
return store.dispatch(submitAccessibilityForm()).then(() => { | ||
return store.dispatch(actionCreators.submitAccessibilityForm()).then(() => { | ||
// return of async actions | ||
@@ -51,4 +82,4 @@ expect(store.getActions()).toEqual(expectedActions); | ||
}]; | ||
store = mockStore(initialState); | ||
return store.dispatch(submitAccessibilityForm()).then(() => { | ||
return store.dispatch(actionCreators.submitAccessibilityForm()).then(() => { | ||
// return of async actions | ||
@@ -61,5 +92,4 @@ expect(store.getActions()).toEqual(expectedActions); | ||
const expectedAction = { type: accessibilityActions.CLEAR_ACCESSIBILITY_STATUS }; | ||
store = mockStore(initialState); | ||
expect(store.dispatch(clearAccessibilityStatus())).toEqual(expectedAction); | ||
expect(store.dispatch(actionCreators.clearAccessibilityStatus())).toEqual(expectedAction); | ||
}); | ||
}); |
@@ -18,5 +18,5 @@ import * as clientApi from '../api/client'; | ||
export const assetDeleteFailure = response => ({ | ||
type: assetActions.DELETE_ASSET_FAILURE, | ||
response, | ||
export const requestAssetsFailure = response => ({ | ||
type: assetActions.REQUEST_ASSETS_FAILURE, | ||
data: response, | ||
}); | ||
@@ -45,3 +45,3 @@ | ||
.catch((error) => { | ||
dispatch(assetDeleteFailure(error)); | ||
dispatch(requestAssetsFailure(error)); | ||
}); | ||
@@ -64,16 +64,23 @@ | ||
export const deleteAssetSuccess = (assetId, response) => ({ | ||
export const deleteAssetSuccess = assetId => ({ | ||
type: assetActions.DELETE_ASSET_SUCCESS, | ||
assetId, | ||
response, | ||
}); | ||
export const deleteAssetFailure = assetId => ({ | ||
type: assetActions.DELETE_ASSET_FAILURE, | ||
assetId, | ||
}); | ||
export const deleteAsset = (assetId, courseDetails) => | ||
dispatch => | ||
clientApi.requestDeleteAsset(courseDetails.id, assetId) | ||
// since the API returns 204 on success and 404 on failure, neither of which have | ||
// content, we don't json-ify the response | ||
.then((response) => { | ||
if (response.ok) { | ||
dispatch(deleteAssetSuccess(assetId, response)); | ||
dispatch(deleteAssetSuccess(assetId)); | ||
} else { | ||
dispatch(assetDeleteFailure(response)); | ||
dispatch(deleteAssetFailure(assetId)); | ||
} | ||
@@ -101,16 +108,16 @@ }); | ||
dispatch(togglingLockAsset(asset)); | ||
clientApi.requestToggleLockAsset(courseDetails.id, asset) | ||
return clientApi.requestToggleLockAsset(courseDetails.id, asset) | ||
.then((response) => { | ||
if (response.ok) { | ||
dispatch(toggleLockAssetSuccess(asset)); | ||
} else { | ||
dispatch(toggleLockAssetFailure(asset, response)); | ||
if (!response.ok) { | ||
throw new Error(response); | ||
} | ||
}) | ||
.then(() => { | ||
dispatch(toggleLockAssetSuccess(asset)); | ||
}) | ||
.catch((error) => { | ||
dispatch(toggleLockAssetFailure(asset, error)); | ||
}); | ||
}; | ||
export const clearAssetsStatus = () => | ||
dispatch => | ||
dispatch({ type: assetActions.CLEAR_ASSETS_STATUS }); | ||
export const uploadingAssets = count => ({ | ||
@@ -117,0 +124,0 @@ type: assetActions.UPLOADING_ASSETS, |
@@ -7,17 +7,15 @@ import configureStore from 'redux-mock-store'; | ||
import * as actionCreators from './assets'; | ||
import { requestInitial } from '../reducers/assets'; | ||
import { assetActions } from '../../data/constants/actionTypes'; | ||
const initialState = { | ||
request: { | ||
assetTypes: {}, | ||
start: 0, | ||
end: 0, | ||
page: 0, | ||
pageSize: 50, | ||
totalCount: 0, | ||
sort: 'date_added', | ||
direction: 'desc', | ||
}, | ||
request: { ...requestInitial }, | ||
}; | ||
const courseDetails = { | ||
id: 'edX', | ||
}; | ||
const assetId = 'asset'; | ||
const assetsEndpoint = endpoints.assets; | ||
@@ -28,3 +26,3 @@ const middlewares = [thunk]; | ||
describe('Assets Action Creator', () => { | ||
describe('Assets Action Creators', () => { | ||
beforeEach(() => { | ||
@@ -34,2 +32,7 @@ store = mockStore(initialState); | ||
afterEach(() => { | ||
fetchMock.reset(); | ||
fetchMock.restore(); | ||
}); | ||
it('returns expected state from requestAssetsSuccess', () => { | ||
@@ -40,24 +43,35 @@ const expectedAction = { data: 'response', type: assetActions.REQUEST_ASSETS_SUCCESS }; | ||
it('returns expected state from assetDeleteFailure', () => { | ||
const expectedAction = { response: 'response', type: assetActions.DELETE_ASSET_FAILURE }; | ||
expect(store.dispatch(actionCreators.assetDeleteFailure('response'))).toEqual(expectedAction); | ||
const expectedAction = { assetId, type: assetActions.DELETE_ASSET_FAILURE }; | ||
expect(store.dispatch(actionCreators.deleteAssetFailure(assetId))).toEqual(expectedAction); | ||
}); | ||
it('returns expected state from getAssets success', () => { | ||
const request = { | ||
page: 0, | ||
assetTypes: {}, | ||
sort: 'date_added', | ||
direction: 'desc', | ||
}; | ||
const request = requestInitial; | ||
const response = request; | ||
const courseDetails = { | ||
id: 'edX', | ||
fetchMock.once(`begin:${assetsEndpoint}`, response); | ||
const expectedActions = [ | ||
{ type: assetActions.REQUEST_ASSETS_SUCCESS, data: response }, | ||
]; | ||
return store.dispatch(actionCreators.getAssets(request, courseDetails)).then(() => { | ||
// return of async actions | ||
expect(store.getActions()).toEqual(expectedActions); | ||
}); | ||
}); | ||
it('returns expected state from getAssets failure', () => { | ||
const request = requestInitial; | ||
const response = { | ||
status: 400, | ||
body: request, | ||
}; | ||
const errorResponse = new Error(response); | ||
fetchMock.once(`begin:${assetsEndpoint}`, response); | ||
const expectedActions = [ | ||
{ type: assetActions.REQUEST_ASSETS_SUCCESS, data: response }, | ||
{ type: assetActions.REQUEST_ASSETS_FAILURE, data: errorResponse }, | ||
]; | ||
store = mockStore(initialState); | ||
return store.dispatch(actionCreators.getAssets(request, courseDetails)).then(() => { | ||
@@ -68,2 +82,20 @@ // return of async actions | ||
}); | ||
it('returns expected state from getAssets if response metadata does not match request', () => { | ||
const request = { | ||
...requestInitial, | ||
direction: 'asc', | ||
}; | ||
const response = request; | ||
fetchMock.once(`begin:${assetsEndpoint}`, response); | ||
// if the response is not the same as the request, we expect nothing | ||
const expectedActions = []; | ||
return store.dispatch(actionCreators.getAssets(request, courseDetails)).then(() => { | ||
// return of async actions | ||
expect(store.getActions()).toEqual(expectedActions); | ||
}); | ||
}); | ||
it('returns expected state from filterUpdate', () => { | ||
@@ -81,6 +113,35 @@ const expectedAction = { data: { filter: true }, type: assetActions.FILTER_UPDATED }; | ||
}); | ||
it('returns expected state from deleteAssetSuccess', () => { | ||
const expectedAction = { assetId: 'assetId', response: 'response', type: assetActions.DELETE_ASSET_SUCCESS }; | ||
expect(store.dispatch(actionCreators.deleteAssetSuccess('assetId', 'response'))).toEqual(expectedAction); | ||
it('returns expected state from deleteAsset success', () => { | ||
const response = { | ||
status: 204, | ||
body: {}, | ||
}; | ||
fetchMock.once(`begin:${assetsEndpoint}`, response); | ||
const expectedActions = [ | ||
{ type: assetActions.DELETE_ASSET_SUCCESS, assetId }, | ||
]; | ||
return store.dispatch(actionCreators.deleteAsset(assetId, courseDetails)).then(() => { | ||
// return of async actions | ||
expect(store.getActions()).toEqual(expectedActions); | ||
}); | ||
}); | ||
it('returns expected state from deleteAsset failure', () => { | ||
const response = { | ||
status: 400, | ||
}; | ||
fetchMock.once(`begin:${assetsEndpoint}`, response); | ||
const expectedActions = [ | ||
{ type: assetActions.DELETE_ASSET_FAILURE, assetId }, | ||
]; | ||
return store.dispatch(actionCreators.deleteAsset(assetId, courseDetails)).then(() => { | ||
// return of async actions | ||
expect(store.getActions()).toEqual(expectedActions); | ||
}); | ||
}); | ||
it('returns expected state from togglingLockAsset', () => { | ||
@@ -87,0 +148,0 @@ const expectedAction = { asset: 'asset', type: assetActions.TOGGLING_LOCK_ASSET_SUCCESS }; |
/* ASSETS ACTION TYPES */ | ||
export const assetActions = { | ||
REQUEST_ASSETS_SUCCESS: 'REQUEST_ASSETS_SUCCESS', | ||
REQUEST_ASSETS_FAILURE: 'REQUEST_ASSETS_FAILURE', | ||
FILTER_UPDATED: 'FILTER_UPDATED', | ||
@@ -5,0 +6,0 @@ DELETE_ASSET_SUCCESS: 'DELETE_ASSET_SUCCESS', |
@@ -9,7 +9,7 @@ import { combineReducers } from 'redux'; | ||
const filtersInitial = { | ||
export const filtersInitial = { | ||
assetTypes: {}, | ||
}; | ||
const paginationInitial = { | ||
export const paginationInitial = { | ||
start: 0, | ||
@@ -22,3 +22,3 @@ end: 0, | ||
const sortInitial = { | ||
export const sortInitial = { | ||
sort: 'date_added', | ||
@@ -28,3 +28,3 @@ direction: 'desc', | ||
const requestInitial = { | ||
export const requestInitial = { | ||
...filtersInitial, | ||
@@ -36,3 +36,3 @@ ...paginationInitial, | ||
const filters = (state = filtersInitial, action) => { | ||
export const filters = (state = filtersInitial, action) => { | ||
switch (action.type) { | ||
@@ -67,3 +67,3 @@ case assetActions.REQUEST_ASSETS_SUCCESS: | ||
const pagination = (state = paginationInitial, action) => { | ||
export const pagination = (state = paginationInitial, action) => { | ||
switch (action.type) { | ||
@@ -83,3 +83,3 @@ case assetActions.REQUEST_ASSETS_SUCCESS: | ||
const sort = (state = sortInitial, action) => { | ||
export const sort = (state = sortInitial, action) => { | ||
switch (action.type) { | ||
@@ -96,4 +96,14 @@ case assetActions.REQUEST_ASSETS_SUCCESS: | ||
const status = (state = {}, action) => { | ||
export const status = (state = {}, action) => { | ||
switch (action.type) { | ||
case assetActions.REQUEST_ASSETS_SUCCESS: | ||
return { | ||
response: action.response, | ||
type: action.type, | ||
}; | ||
case assetActions.REQUEST_ASSETS_FAILURE: | ||
return { | ||
response: action.response, | ||
type: action.type, | ||
}; | ||
case assetActions.CLEAR_ASSETS_STATUS: | ||
@@ -100,0 +110,0 @@ return {}; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 3 instances in 1 package
6
185409
62
4068
Updated@edx/paragon@^1.7.1