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 (
-
-
-
-
- {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)}
- |
-
-
-
- {Object.values(data).map(((block) => (
-
- {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={(
+
+
+
+ )}
+ />
+
+
+ );
+}
+
+EmbedORAModal.propTypes = {
+ usageKey: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ buttonText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
+};
+
+export default EmbedORAModal;
diff --git a/src/ora-dashboard/messages.js b/src/ora-dashboard/messages.js
new file mode 100644
index 0000000..cb9fbb8
--- /dev/null
+++ b/src/ora-dashboard/messages.js
@@ -0,0 +1,11 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ section_heading: {
+ id: 'ora.dashboard.heading',
+ defaultMessage: 'Open Responses',
+ description: 'Section heading',
+ },
+});
+
+export default messages;
diff --git a/src/ora-dashboard/summary-table/SummaryTable.jsx b/src/ora-dashboard/summary-table/SummaryTable.jsx
index d236c2d..d58329a 100644
--- a/src/ora-dashboard/summary-table/SummaryTable.jsx
+++ b/src/ora-dashboard/summary-table/SummaryTable.jsx
@@ -1,46 +1,58 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import React from 'react';
+import { Table } from '@edx/paragon';
import messages from './messages';
+import { oraSummaryDataShape } from '../../data/constants';
-function SummaryTable({ intl }) {
+function SummaryTable({ intl, data }) {
+ const columns = [
+ {
+ label: intl.formatMessage(messages.units),
+ key: 'units',
+ },
+ {
+ label: intl.formatMessage(messages.assessments),
+ key: 'assessments',
+ },
+ {
+ label: intl.formatMessage(messages.total_responses),
+ key: 'total',
+ },
+ {
+ label: intl.formatMessage(messages.training),
+ key: 'training',
+ },
+ {
+ label: intl.formatMessage(messages.peer),
+ key: 'peer',
+ },
+ {
+ label: intl.formatMessage(messages.self),
+ key: 'self',
+ },
+ {
+ label: intl.formatMessage(messages.waiting),
+ key: 'waiting',
+ },
+ {
+ label: intl.formatMessage(messages.staff),
+ key: 'staff',
+ },
+ {
+ label: intl.formatMessage(messages.final_grade_received),
+ key: 'done',
+ },
+ ];
return (
-
-
-
-
- {intl.formatMessage(messages.units)}
- |
-
- {intl.formatMessage(messages.assessments)}
- |
-
- {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)}
- |
-
-
-
+
);
}
SummaryTable.propTypes = {
intl: intlShape.isRequired,
+ data: oraSummaryDataShape.isRequired,
};
export default injectIntl(SummaryTable);