diff --git a/src/components/value-or-loading/ValueOrLoading.jsx b/src/components/value-or-loading/ValueOrLoading.jsx index 4eff4cc..8508fba 100644 --- a/src/components/value-or-loading/ValueOrLoading.jsx +++ b/src/components/value-or-loading/ValueOrLoading.jsx @@ -11,7 +11,7 @@ function LoadingOrValue({ value }) { } LoadingOrValue.propTypes = { - value: PropType.element, + value: PropType.node, }; LoadingOrValue.defaultProps = { diff --git a/src/data/constants.js b/src/data/constants.js index a601533..01c65aa 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -31,3 +31,15 @@ export const oraDataShape = PropTypes.objectOf(PropTypes.shape({ done: PropTypes.number, status: PropTypes.string, })); + +export const oraSummaryDataShape = PropTypes.objectOf(PropTypes.shape({ + units: PropTypes.number, + assessments: PropTypes.number, + total: PropTypes.number, + training: PropTypes.number, + peer: PropTypes.number, + self: PropTypes.number, + waiting: PropTypes.number, + staff: PropTypes.number, + done: PropTypes.number, +})); diff --git a/src/ora-dashboard/OraDashboard.jsx b/src/ora-dashboard/OraDashboard.jsx index 2707ad3..e490199 100644 --- a/src/ora-dashboard/OraDashboard.jsx +++ b/src/ora-dashboard/OraDashboard.jsx @@ -1,26 +1,37 @@ +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import React from 'react'; +import { useSelector } from 'react-redux'; +import { Spinner } from '@edx/paragon'; import SummaryTable from './summary-table/SummaryTable'; import DataTable from './data-table/DataTable'; -import { oraDataShape } from '../data/constants'; +import { oraDataShape, oraSummaryDataShape, RequestStatus } from '../data/constants'; +import { loadingStatus } from './data/selectors'; +import messages from './messages'; + +function OraDashboard({ intl, data, summary }) { + const status = useSelector(loadingStatus); -function OraDashboard({ data }) { return (
-

Open Responses

- - +

{intl.formatMessage(messages.section_heading)}

+ {status === RequestStatus.IN_PROGRESS && } + {status === RequestStatus.SUCCESSFUL && ( + <> + + + + )} +
); } OraDashboard.propTypes = { - data: oraDataShape, -}; - -OraDashboard.defaultProps = { - data: {}, + intl: intlShape.isRequired, + data: oraDataShape.isRequired, + summary: oraSummaryDataShape.isRequired, }; -export default OraDashboard; +export default injectIntl(OraDashboard); diff --git a/src/ora-dashboard/OraDashboardContainer.jsx b/src/ora-dashboard/OraDashboardContainer.jsx index 8d44f72..e40188e 100644 --- a/src/ora-dashboard/OraDashboardContainer.jsx +++ b/src/ora-dashboard/OraDashboardContainer.jsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router'; -import { selectOraBlocks } from './data/selectors'; +import { selectOraBlocks, selectSummary } from './data/selectors'; import { fetchOraBlocks } from './data/thunks'; import OraDashboard from './OraDashboard'; @@ -9,12 +9,14 @@ export default function OraDashboardContainer() { const { courseId } = useParams(); const dispatch = useDispatch(); const oraBlocks = useSelector(selectOraBlocks); + const summary = useSelector(selectSummary); + useEffect(() => { // The courseId from the URL is the course we WANT to load. dispatch(fetchOraBlocks(courseId)); }, [courseId]); return ( - + ); } diff --git a/src/ora-dashboard/data-table/DataTable.jsx b/src/ora-dashboard/data-table/DataTable.jsx index 3d7a83c..720046f 100644 --- a/src/ora-dashboard/data-table/DataTable.jsx +++ b/src/ora-dashboard/data-table/DataTable.jsx @@ -1,67 +1,118 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import React, { useEffect } from 'react'; +import { Table } from '@edx/paragon'; import { useDispatch } from 'react-redux'; import { useParams } from 'react-router'; -import LoadingOrValue from '../../components/value-or-loading/ValueOrLoading'; import messages from './messages'; import { fetchOraReports } from '../data/thunks'; import { oraDataShape } from '../../data/constants'; +import EmbedORAModal from '../embed-ora-modal/EmbedORAModal'; + +/** + * Sort implementation for Paragon Table + * @param {any} firstElement + * @param {any} secondElement + * @param {string} key + * @param {string} direction + */ +function sort(firstElement, secondElement, key, direction) { + const directionIsAsc = direction === 'asc'; + + if (firstElement[key] > secondElement[key]) { + return directionIsAsc ? 1 : -1; + } if (firstElement[key] < secondElement[key]) { + return directionIsAsc ? -1 : 1; + } + return 0; +} function DataTable({ intl, data }) { const { courseId } = useParams(); const dispatch = useDispatch(); useEffect(() => { - Object.keys(data).map((blockId) => data[blockId].status || dispatch(fetchOraReports(courseId, blockId))); + if (Object.keys(data).length > 0) { + const blockId = Object.keys(data)[0]; + if (!data[blockId].status) { + dispatch(fetchOraReports(courseId, blockId)); + } + } }, [courseId, data]); + + // create a copy of data for sortable Table + const sortableData = Object.values(data).slice().map(item => { + const action = ; + return { ...item, actions: action }; + }); + + // define Table columns + const columns = [ + { + label: intl.formatMessage(messages.unit_name), + key: 'vertical', + columnSortable: true, + }, + { + label: intl.formatMessage(messages.assessment), + key: 'name', + columnSortable: true, + }, + { + label: intl.formatMessage(messages.total_responses), + key: 'total', + columnSortable: true, + }, + { + label: intl.formatMessage(messages.training), + key: 'training', + columnSortable: true, + }, + { + label: intl.formatMessage(messages.peer), + key: 'peer', + columnSortable: true, + }, + { + label: intl.formatMessage(messages.self), + key: 'self', + columnSortable: true, + }, + { + label: intl.formatMessage(messages.waiting), + key: 'waiting', + columnSortable: true, + }, + { + label: intl.formatMessage(messages.staff), + key: 'staff', + columnSortable: true, + }, + { + label: intl.formatMessage(messages.final_grade_received), + key: 'done', + columnSortable: true, + }, + { + label: '', + key: 'actions', + }, + ]; + return ( - - - - - - - - - - - - - - - - {Object.values(data).map(((block) => ( - - - - - - - - - - - - )))} - -
- {intl.formatMessage(messages.unit_name)} - - {intl.formatMessage(messages.assessment)} - - {intl.formatMessage(messages.total_responses)} - - {intl.formatMessage(messages.training)} - - {intl.formatMessage(messages.peer)} - - {intl.formatMessage(messages.self)} - - {intl.formatMessage(messages.waiting)} - - {intl.formatMessage(messages.staff)} - - {intl.formatMessage(messages.final_grade_received)} -
{block.vertical}{block.name}
+
+ ({ + ...column, + onSort(direction) { + sortableData.sort( + (firstElement, secondElement) => sort(firstElement, secondElement, column.key, direction), + ); + }, + }))} + tableSortable + /> + {/* {iframes} */} + ); } DataTable.propTypes = { diff --git a/src/ora-dashboard/data/selectors.js b/src/ora-dashboard/data/selectors.js index ff5d036..b7da726 100644 --- a/src/ora-dashboard/data/selectors.js +++ b/src/ora-dashboard/data/selectors.js @@ -1,4 +1,6 @@ /* eslint-disable import/prefer-default-export */ export const selectOraBlocks = state => state.blocks; +export const selectSummary = state => state.summary; + export const loadingStatus = state => state.status; diff --git a/src/ora-dashboard/data/slices.js b/src/ora-dashboard/data/slices.js index b0fba67..4906ab8 100644 --- a/src/ora-dashboard/data/slices.js +++ b/src/ora-dashboard/data/slices.js @@ -33,11 +33,61 @@ function oraBlocksFromResponse(response) { return oraBlocks; } +/** + * Given a get_ora2_responses API response, set statistics for each ORA Block. + * Also assign default value if there is no statistics for any block. + * @param {array} blockIds - list of blockIdx in current course + * @param {object} payload - get_ora2_responses api response + */ +function oraBlockStatisticsFromPayload(blockIds, payload) { + const blocks = {}; + const defaultValues = { + status: RequestStatus.SUCCESSFUL, + total: 0, + training: 0, + peer: 0, + self: 0, + waiting: 0, + staff: 0, + done: 0, + }; + blockIds.forEach(blockId => { + blocks[blockId] = { ...defaultValues, ...payload[blockId] }; + }); + return blocks; +} + +/** + * Given current state.oraBlocks object, prepares summary. + * @param {object} data - state.oraBlocks + */ +function prepareStatisticsSummary(data) { + const dataKeys = Object.keys(data); + const dataItemArr = dataKeys.map((key) => data[key]); + + const sumByKey = (key) => dataItemArr + .map((item) => (item[key] ? item[key] : 0)) + .reduce((result, val) => result + val, 0); + + const fields = ['total', 'training', 'peer', 'self', 'waiting', 'staff', 'done']; + const summaryData = { + units: dataKeys.length, + assessments: dataKeys.length, + }; + + fields.forEach(field => { + summaryData[field] = sumByKey(field); + }); + + return summaryData; +} + const oraSlice = createSlice({ name: 'ora', initialState: { status: RequestStatus.IN_PROGRESS, blocks: {}, + summary: {}, }, reducers: { fetchOraBlocksRequest: (state) => { @@ -57,10 +107,12 @@ const oraSlice = createSlice({ state.blocks[payload.blockId].status = RequestStatus.IN_PROGRESS; }, fetchOraReportSuccess: (state, { payload }) => { - Object.keys(payload).forEach((blockId) => { - state.blocks[blockId].status = RequestStatus.SUCCESSFUL; - Object.assign(state.blocks[blockId], payload[blockId]); + const statistics = oraBlockStatisticsFromPayload(Object.keys(state.blocks), payload); + Object.keys(state.blocks).forEach(blockId => { + state.blocks[blockId] = { ...state.blocks[blockId], ...statistics[blockId] }; }); + // as statistics updated, update summary too. + state.summary = prepareStatisticsSummary(state.blocks); }, fetchOraReportFailed: (state, { payload }) => { state.blocks[payload.blockId].status = RequestStatus.FAILED; diff --git a/src/ora-dashboard/embed-ora-modal/EmbedORAModal.jsx b/src/ora-dashboard/embed-ora-modal/EmbedORAModal.jsx new file mode 100644 index 0000000..cc2641f --- /dev/null +++ b/src/ora-dashboard/embed-ora-modal/EmbedORAModal.jsx @@ -0,0 +1,37 @@ +import { Button, Modal } from '@edx/paragon'; +import { getConfig } from '@edx/frontend-platform'; +import React, { useState } from 'react'; + +import PropTypes from 'prop-types'; + +const { LMS_BASE_URL } = getConfig(); + +function EmbedORAModal({ usageKey, title, buttonText }) { + const [open, setOpen] = useState(false); + + return ( +
+ setOpen(false)} + body={( +
+