add entitlement support creation and reissue

Learner-3925
This commit is contained in:
Alex Dusenbery
2018-03-22 12:27:37 -04:00
committed by Michael LoTurco
parent 187e39ff54
commit 10be8bcd4e
21 changed files with 419 additions and 66 deletions

View File

@@ -1,6 +1,7 @@
{
"plugins": [
"transform-object-assign"
"transform-object-assign",
"transform-object-rest-spread"
],
"presets": [
[

View File

@@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import { createEntitlement, reissueEntitlement } from '../../data/actions/entitlement';
import { closeForm } from '../../data/actions/form';
import EntitlementForm from './index.jsx';
const mapStateToProps = state => ({
formType: state.form.formType,
isOpen: state.form.isOpen,
entitlement: state.form.activeEntitlement,
});
const mapDispatchToProps = dispatch => ({
createEntitlement: ({ username, courseUuid, mode, comments }) =>
dispatch(createEntitlement({ username, courseUuid, mode, comments })),
reissueEntitlement: ({ entitlement, comments }) =>
dispatch(reissueEntitlement({ entitlement, comments })),
closeForm: () => dispatch(closeForm()),
});
const EntitlementFormContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(EntitlementForm);
export default EntitlementFormContainer;

View File

@@ -0,0 +1,163 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, InputSelect, InputText, TextArea } from '@edx/paragon';
import { formTypes } from '../../data/constants/formTypes';
class EntitlementForm extends React.Component {
constructor(props) {
super(props);
if (props.formType === formTypes.REISSUE) {
const { courseUuid, mode, user } = props.entitlement;
this.state = {
courseUuid,
mode,
username: user,
comments: '',
};
} else {
this.state = {
courseUuid: '',
mode: '',
username: '',
comments: '',
};
}
this.onClose = this.onClose.bind(this);
this.handleCourseUUIDChange = this.handleCourseUUIDChange.bind(this);
this.handleUsernameChange = this.handleUsernameChange.bind(this);
this.handleModeChange = this.handleModeChange.bind(this);
this.handleCommentsChange = this.handleCommentsChange.bind(this);
this.submitForm = this.submitForm.bind(this);
}
onClose() {
this.props.closeForm();
}
handleCourseUUIDChange(courseUuid) {
this.setState({ courseUuid });
}
handleUsernameChange(username) {
this.setState({ username });
}
handleModeChange(mode) {
this.setState({ mode });
}
handleCommentsChange(comments) {
this.setState({ comments });
}
submitForm() {
const { courseUuid, username, mode, comments } = this.state;
const { formType, entitlement } = this.props;
if (formType === formTypes.REISSUE) { // if there is an active entitlement we are updating an entitlement
this.props.reissueEntitlement({ entitlement, comments });
} else { // if there is no active entitlement we are creating a new entitlement
this.props.createEntitlement({ courseUuid, username, mode, comments });
}
}
render() {
const { courseUuid, username, mode, comments } = this.state;
const isReissue = this.props.formType === formTypes.REISSUE;
const title = isReissue ? 'Re-issue Entitlement' : 'Create Entitlement';
const body = (
<div>
<h3> {title} </h3>
<InputText
disabled={isReissue}
name="courseUuid"
label="Course UUID"
value={courseUuid}
onChange={this.handleCourseUUIDChange}
/>
<InputText
disabled={isReissue}
name="username"
label="Username"
value={username}
onChange={this.handleUsernameChange}
/>
<InputSelect
disabled={isReissue}
name="mode"
label="Mode"
value={mode}
options={[
{ label: '--', value: '' },
{ label: 'Verified', value: 'verified' },
{ label: 'Professional', value: 'professional' },
]}
onChange={this.handleModeChange}
/>
<TextArea
name="comments"
label="Comments"
value={comments}
onChange={this.handleCommentsChange}
/>
<div>
<Button
className={['btn', 'btn-secondary']}
label="Close"
onClick={this.onClose}
/>
<Button
className={['btn', 'btn-primary']}
label="Submit"
onClick={this.submitForm}
/>
</div>
</div>
);
return this.props.isOpen && body;
}
}
EntitlementForm.propTypes = {
formType: PropTypes.string.isRequired,
isOpen: PropTypes.bool.isRequired,
entitlement: PropTypes.shape({
uuid: PropTypes.string.isRequired,
courseUuid: PropTypes.string.isRequired,
created: PropTypes.string.isRequired,
modified: PropTypes.string.isRequired,
expiredAt: PropTypes.string,
mode: PropTypes.string.isRequired,
orderNumber: PropTypes.string,
supportDetails: PropTypes.arrayOf(PropTypes.shape({
supportUser: PropTypes.string,
action: PropTypes.string,
comments: PropTypes.string,
unenrolledRun: PropTypes.string,
})),
user: PropTypes.string.isRequired,
}),
createEntitlement: PropTypes.func.isRequired,
reissueEntitlement: PropTypes.func.isRequired,
closeForm: PropTypes.func.isRequired,
};
EntitlementForm.defaultProps = {
entitlement: {
uuid:'',
courseUuid: '',
created: '',
modified: '',
expiredAt: '',
mode: 'verified',
orderNumber: '',
supportDetails: [],
user: '',
}
};
export default EntitlementForm;

View File

@@ -1,9 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StatusAlert } from '@edx/paragon';
import { Button, StatusAlert } from '@edx/paragon';
import SearchContainer from '../Search/SearchContainer.jsx';
import EntitlementSupportTableContainer from '../Table/EntitlementSupportTableContainer.jsx';
import EntitlementFormContainer from '../EntitlementForm/container.jsx';
const Main = props => (
<div>
@@ -14,17 +15,45 @@ const Main = props => (
open={!!props.errorMessage}
/>
<h2>
Entitlement Support Page
Student Support: Entitlement
</h2>
<SearchContainer />
<EntitlementSupportTableContainer ecommerceUrl={props.ecommerceUrl} />
<MainContent
isFormOpen={props.isFormOpen}
ecommerceUrl={props.ecommerceUrl}
openCreationForm={props.openCreationForm}
/>
</div>
);
const MainContent = (props) => {
if (props.isFormOpen) {
return <EntitlementFormContainer />;
}
return (
<div>
<SearchContainer />
<Button
className={['btn', 'btn-primary']}
label="Create New Entitlement"
onClick={props.openCreationForm}
/>
<EntitlementSupportTableContainer ecommerceUrl={props.ecommerceUrl} />
</div>
);
};
Main.propTypes = {
errorMessage: PropTypes.string.isRequired,
dismissErrorMessage: PropTypes.func.isRequired,
openCreationForm: PropTypes.func.isRequired,
ecommerceUrl: PropTypes.string.isRequired,
isFormOpen: PropTypes.bool.isRequired,
};
MainContent.propTypes = {
openCreationForm: PropTypes.func.isRequired,
ecommerceUrl: PropTypes.string.isRequired,
isFormOpen: PropTypes.bool.isRequired,
};
export default Main;

View File

@@ -1,15 +1,19 @@
import { connect } from 'react-redux';
import { dismissError } from '../../data/actions/error';
import { openCreationForm } from '../../data/actions/form';
import Main from './Main.jsx';
const mapStateToProps = state => ({
errorMessage: state.error,
isFormOpen: state.form.isOpen,
});
const mapDispatchToProps = dispatch => ({
dismissErrorMessage: () => dispatch(dismissError()),
openCreationForm: () => dispatch(openCreationForm()),
});
const MainContainer = connect(

View File

@@ -2,7 +2,7 @@ import React from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
import { Hyperlink, Table } from '@edx/paragon';
import { Button, Hyperlink, Table } from '@edx/paragon';
const entitlementColumns = [
{
@@ -13,14 +13,14 @@ const entitlementColumns = [
label: 'Course UUID',
key: 'courseUuid',
},
{
label: 'Enrollment',
key: 'enrollmentCourseRun',
},
{
label: 'Mode',
key: 'mode',
},
{
label: 'Enrollment',
key: 'enrollmentCourseRun',
},
{
label: 'Expired At',
key: 'expiredAt',
@@ -45,9 +45,9 @@ const entitlementColumns = [
},
];
const parseEntitlementData = (entitlements, ecommerceUrl) =>
const parseEntitlementData = (entitlements, ecommerceUrl, openReissueForm) =>
entitlements.map((entitlement) => {
const { expiredAt, created, modified, orderNumber } = entitlement;
const { expiredAt, created, modified, orderNumber, enrollmentCourseRun } = entitlement;
return Object.assign({}, entitlement, {
expiredAt: expiredAt ? moment(expiredAt).format('lll') : '',
createdAt: moment(created).format('lll'),
@@ -56,13 +56,18 @@ const parseEntitlementData = (entitlements, ecommerceUrl) =>
destination={`${ecommerceUrl}${orderNumber}/`}
content={orderNumber || ''}
/>,
button: <div> No Actions Currently Available </div>,
button: <Button
disabled={!enrollmentCourseRun}
className={['btn', 'btn-primary']}
label="Reissue"
onClick={() => openReissueForm(entitlement)}
/>,
});
});
const EntitlementSupportTable = props => (
<Table
data={parseEntitlementData(props.entitlements, props.ecommerceUrl)}
data={parseEntitlementData(props.entitlements, props.ecommerceUrl, props.openReissueForm)}
columns={entitlementColumns}
/>
);
@@ -86,6 +91,7 @@ EntitlementSupportTable.propTypes = {
user: PropTypes.string.isRequired,
})).isRequired,
ecommerceUrl: PropTypes.string.isRequired,
openReissueForm: PropTypes.func.isRequired,
};
export default EntitlementSupportTable;

View File

@@ -1,13 +1,19 @@
import { connect } from 'react-redux';
import { openReissueForm } from '../../data/actions/form';
import EntitlementSupportTable from './EntitlementSupportTable.jsx';
const mapStateToProps = state => ({
entitlements: state.entitlements,
});
const mapDispatchToProps = dispatch => ({
openReissueForm: entitlement => dispatch(openReissueForm(entitlement)),
});
const EntitlementSupportTableContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(EntitlementSupportTable);
export default EntitlementSupportTableContainer;

View File

@@ -1,12 +0,0 @@
export const entitlementActions = {
fetch: {
SUCCESS: 'FETCH_ENTITLEMENTS_SUCCESS',
FAILURE: 'FETCH_ENTITLEMENTS_FAILURE',
},
};
export const errorActions = {
DISPLAY_ERROR: 'DISPLAY_ERROR',
DISMISS_ERROR: 'DISMISS_ERROR',
};

View File

@@ -1,7 +1,7 @@
import camelize from 'camelize';
import { getEntitlements } from '../api/client';
import { entitlementActions } from './constants';
import { getEntitlements, patchEntitlement, postEntitlement } from '../api/client';
import { entitlementActions } from '../constants/actionTypes';
import { displayError } from './error';
const fetchEntitlementsSuccess = entitlements => ({
@@ -28,8 +28,73 @@ const fetchEntitlements = username =>
);
};
const reissueEntitlementSuccess = entitlement => ({
type: entitlementActions.reissue.SUCCESS,
entitlement,
});
const reissueEntitlementFailure = error =>
dispatch =>
dispatch(displayError('Error Reissuing Entitlement', error));
const reissueEntitlement = ({ entitlement, comments }) =>
(dispatch) => {
patchEntitlement({
uuid: entitlement.uuid,
action: 'REISSUE',
unenrolledRun: entitlement.enrollmentCourseRun,
comments,
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error(response);
})
.then(
json => dispatch(reissueEntitlementSuccess(camelize(json))),
error => dispatch(reissueEntitlementFailure(error)),
);
};
const createEntitlementSuccess = entitlement => ({
type: entitlementActions.create.SUCCESS,
entitlement,
});
const createEntitlementFailure = error =>
dispatch =>
dispatch(displayError('Error Creating Entitlement', error));
const createEntitlement = ({ username, courseUuid, mode, comments }) =>
(dispatch) => {
postEntitlement({
username,
courseUuid,
mode,
comments,
action: 'CREATE',
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error(response);
})
.then(
json => dispatch(createEntitlementSuccess(camelize(json))),
error => dispatch(createEntitlementFailure(error)),
);
};
export {
fetchEntitlements,
fetchEntitlementsSuccess,
fetchEntitlementsFailure,
createEntitlement,
createEntitlementSuccess,
createEntitlementFailure,
reissueEntitlement,
reissueEntitlementSuccess,
reissueEntitlementFailure,
};

View File

@@ -1,4 +1,4 @@
import { errorActions } from './constants';
import { errorActions } from '../constants/actionTypes';
const displayError = (message, error) => ({
type: errorActions.DISPLAY_ERROR,

View File

@@ -0,0 +1,21 @@
import { formActions } from '../constants/actionTypes';
const openReissueForm = entitlement => ({
type: formActions.OPEN_REISSUE_FORM,
entitlement,
});
const openCreationForm = () => ({
type: formActions.OPEN_CREATION_FORM,
});
const closeForm = () => ({
type: formActions.CLOSE_FORM,
});
export {
openReissueForm,
openCreationForm,
closeForm,
};

View File

@@ -10,13 +10,13 @@ const HEADERS = {
};
const getEntitlements = username => fetch(
`${entitlementApi}/?user=${username}`, {
`${entitlementApi}?user=${username}`, {
credentials: 'same-origin',
method: 'get',
},
);
const createEntitlement = ({ username, courseUuid, mode, action, comments = null }) => fetch(
const postEntitlement = ({ username, courseUuid, mode, action, comments = null }) => fetch(
`${entitlementApi}`, {
credentials: 'same-origin',
method: 'post',
@@ -33,14 +33,13 @@ const createEntitlement = ({ username, courseUuid, mode, action, comments = null
},
);
const updateEntitlement = ({ uuid, action, unenrolledRun = null, comments = null }) => fetch(
`${entitlementApi}/${uuid}`, {
const patchEntitlement = ({ uuid, action, unenrolledRun = null, comments = null }) => fetch(
`${entitlementApi}${uuid}`, {
credentials: 'same-origin',
method: 'patch',
headers: HEADERS,
body: JSON.stringify({
expired_at: null,
enrollment_run: null,
support_details: [{
unenrolled_run: unenrolledRun,
action,
@@ -52,6 +51,6 @@ const updateEntitlement = ({ uuid, action, unenrolledRun = null, comments = null
export {
getEntitlements,
createEntitlement,
updateEntitlement,
postEntitlement,
patchEntitlement,
};

View File

@@ -1,4 +1,4 @@
const entitlementApi = '/api/entitlements/v1/entitlements';
const entitlementApi = '/api/entitlements/v1/entitlements/';
export {
entitlementApi,

View File

@@ -0,0 +1,25 @@
export const entitlementActions = {
fetch: {
SUCCESS: 'FETCH_ENTITLEMENTS_SUCCESS',
FAILURE: 'FETCH_ENTITLEMENTS_FAILURE',
},
reissue: {
SUCCESS: 'REISSUE_ENTITLEMENT_SUCCESS',
FAILURE: 'REISSUE_ENTITLEMENT_FAILURE',
},
create: {
SUCCESS: 'CREATE_ENTITLEMENT_SUCCESS',
FAILURE: 'CREATE_ENTITLEMENT_FAILURE',
},
};
export const errorActions = {
DISPLAY_ERROR: 'DISPLAY_ERROR',
DISMISS_ERROR: 'DISMISS_ERROR',
};
export const formActions = {
OPEN_REISSUE_FORM: 'OPEN_REISSUE_FORM',
OPEN_CREATION_FORM: 'OPEN_CREATION_FORM',
CLOSE_FORM: 'CLOSE_FORM',
};

View File

@@ -0,0 +1,4 @@
export const formTypes = {
REISSUE: 'reissue',
CREATE: 'create',
};

View File

@@ -1,9 +1,18 @@
import { entitlementActions } from '../actions/constants';
import { entitlementActions } from '../constants/actionTypes';
const entitlements = (state = [], action) => {
switch (action.type) {
case entitlementActions.fetch.SUCCESS:
return action.entitlements;
case entitlementActions.create.SUCCESS:
return [...state, action.entitlement];
case entitlementActions.reissue.SUCCESS:
return state.map((entitlement) => {
if (entitlement.uuid === action.entitlement.uuid) {
return action.entitlement;
}
return entitlement;
});
default:
return state;
}

View File

@@ -1,4 +1,4 @@
import { errorActions, entitlementActions } from '../actions/constants';
import { errorActions, entitlementActions } from '../constants/actionTypes';
const error = (state = '', action) => {
switch (action.type) {

View File

@@ -0,0 +1,24 @@
import { formActions, entitlementActions } from '../constants/actionTypes';
import { formTypes } from '../constants/formTypes'
const clearFormState = {
formType: '',
isOpen: false,
activeEntitlement: null,
};
const form = (state = {}, action) => {
switch (action.type) {
case formActions.OPEN_REISSUE_FORM:
return { ...state, type: formTypes.REISSUE, isOpen: true, activeEntitlement: action.entitlement };
case formActions.OPEN_CREATION_FORM:
return { ...state, type: formTypes.CREATE, isOpen: true, activeEntitlement: null };
case formActions.CLOSE_FORM:
case entitlementActions.reissue.SUCCESS:
case entitlementActions.create.SUCCESS:
return clearFormState;
default:
return state;
}
};
export default form;

View File

@@ -2,7 +2,8 @@ import { combineReducers } from 'redux';
import entitlements from './entitlements';
import error from './error';
import form from './form';
const rootReducer = combineReducers({ entitlements, error });
const rootReducer = combineReducers({ entitlements, error, form });
export default rootReducer;

View File

@@ -6,6 +6,11 @@ import rootReducer from './reducers/index';
const defaultState = {
entitlements: [],
error: '',
form: {
formType: '',
isOpen: false,
activeEntitlement: null,
},
};
const configureStore = initialState =>

28
package-lock.json generated
View File

@@ -31,34 +31,10 @@
"classnames": "2.2.5",
"font-awesome": "4.7.0",
"prop-types": "15.6.0",
"react": "16.2.0",
"react-dom": "16.2.0",
"react": "16.1.0",
"react-dom": "16.1.0",
"react-element-proptypes": "1.0.0",
"react-proptype-conditional-require": "1.0.4"
},
"dependencies": {
"react": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.2.0.tgz",
"integrity": "sha512-ZmIomM7EE1DvPEnSFAHZn9Vs9zJl5A9H7el0EGTE6ZbW9FKe/14IYAlPbC8iH25YarEQxZL+E8VW7Mi7kfQrDQ==",
"requires": {
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
"object-assign": "4.1.1",
"prop-types": "15.6.0"
}
},
"react-dom": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.2.0.tgz",
"integrity": "sha512-zpGAdwHVn9K0091d+hr+R0qrjoJ84cIBFL2uU60KvWBPfZ7LPSrfqviTxGHWN0sjPZb2hxWzMexwrvJdKePvjg==",
"requires": {
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
"object-assign": "4.1.1",
"prop-types": "15.6.0"
}
}
}
},
"@edx/studio-frontend": {