From 4e425353e8ccda876edbb44c2ff3265de426ced1 Mon Sep 17 00:00:00 2001 From: Shimul Chowdhury Date: Sun, 22 Nov 2020 21:20:08 +0600 Subject: [PATCH 1/5] Summary and Datatable both showing data properly --- .../value-or-loading/ValueOrLoading.jsx | 2 +- src/data/constants.js | 12 ++++ src/ora-dashboard/OraDashboard.jsx | 13 ++-- src/ora-dashboard/OraDashboardContainer.jsx | 6 +- src/ora-dashboard/data-table/DataTable.jsx | 8 ++- src/ora-dashboard/data/selectors.js | 2 + src/ora-dashboard/data/slices.js | 60 ++++++++++++++++++- .../summary-table/SummaryTable.jsx | 18 +++++- 8 files changed, 105 insertions(+), 16 deletions(-) diff --git a/src/components/value-or-loading/ValueOrLoading.jsx b/src/components/value-or-loading/ValueOrLoading.jsx index 4eff4cc..76e012d 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.oneOfType([PropType.element, PropType.number]), }; 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..a443987 100644 --- a/src/ora-dashboard/OraDashboard.jsx +++ b/src/ora-dashboard/OraDashboard.jsx @@ -1,14 +1,14 @@ import React from 'react'; import SummaryTable from './summary-table/SummaryTable'; import DataTable from './data-table/DataTable'; -import { oraDataShape } from '../data/constants'; +import { oraDataShape, oraSummaryDataShape } from '../data/constants'; -function OraDashboard({ data }) { +function OraDashboard({ data, summary }) { return (

Open Responses

- +
@@ -16,11 +16,8 @@ function OraDashboard({ data }) { } OraDashboard.propTypes = { - data: oraDataShape, -}; - -OraDashboard.defaultProps = { - data: {}, + data: oraDataShape.isRequired, + summary: oraSummaryDataShape.isRequired, }; export default 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..0854d34 100644 --- a/src/ora-dashboard/data-table/DataTable.jsx +++ b/src/ora-dashboard/data-table/DataTable.jsx @@ -11,8 +11,14 @@ 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]); + return ( 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..e6e5fad 100644 --- a/src/ora-dashboard/data/slices.js +++ b/src/ora-dashboard/data/slices.js @@ -33,11 +33,63 @@ 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]).reduce((result, val) => result + val, 0); + const fields = ['total', 'training', 'peer', 'self', 'waiting', 'staff', 'done']; + const summaryData = { + units: dataKeys.length, + assessments: dataKeys.length, + }; + + if (dataKeys.length > 0 && data[dataKeys[0]].status === RequestStatus.SUCCESSFUL) { + fields.forEach(field => { + summaryData[field] = sumByKey(field); + }); + } else if (dataKeys.length === 0) { + // there is no ORA block in the course. + fields.forEach(field => { + summaryData[field] = 0; + }); + } + return summaryData; +} + const oraSlice = createSlice({ name: 'ora', initialState: { status: RequestStatus.IN_PROGRESS, blocks: {}, + summary: {}, }, reducers: { fetchOraBlocksRequest: (state) => { @@ -57,10 +109,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/summary-table/SummaryTable.jsx b/src/ora-dashboard/summary-table/SummaryTable.jsx index d236c2d..de60ba7 100644 --- a/src/ora-dashboard/summary-table/SummaryTable.jsx +++ b/src/ora-dashboard/summary-table/SummaryTable.jsx @@ -1,8 +1,10 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import React from 'react'; import messages from './messages'; +import LoadingOrValue from '../../components/value-or-loading/ValueOrLoading'; +import { oraSummaryDataShape } from '../../data/constants'; -function SummaryTable({ intl }) { +function SummaryTable({ intl, data }) { return (
@@ -36,11 +38,25 @@ function SummaryTable({ intl }) { + + + + + + + + + + + + +
); } SummaryTable.propTypes = { intl: intlShape.isRequired, + data: oraSummaryDataShape.isRequired, }; export default injectIntl(SummaryTable); From c86d95f98ce25a6575e197b2ce777781f4a282a6 Mon Sep 17 00:00:00 2001 From: Shimul Chowdhury Date: Mon, 23 Nov 2020 18:23:10 +0600 Subject: [PATCH 2/5] use paragon components and allow sorting --- src/ora-dashboard/OraDashboard.jsx | 17 ++- src/ora-dashboard/data-table/DataTable.jsx | 131 +++++++++++------- src/ora-dashboard/data/slices.js | 20 ++- .../summary-table/SummaryTable.jsx | 90 ++++++------ 4 files changed, 148 insertions(+), 110 deletions(-) diff --git a/src/ora-dashboard/OraDashboard.jsx b/src/ora-dashboard/OraDashboard.jsx index a443987..fa63ed2 100644 --- a/src/ora-dashboard/OraDashboard.jsx +++ b/src/ora-dashboard/OraDashboard.jsx @@ -1,15 +1,26 @@ 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, oraSummaryDataShape } from '../data/constants'; +import { oraDataShape, oraSummaryDataShape, RequestStatus } from '../data/constants'; +import { loadingStatus } from './data/selectors'; function OraDashboard({ data, summary }) { + const status = useSelector(loadingStatus); + return (

Open Responses

- - + {status === RequestStatus.IN_PROGRESS && } + {status === RequestStatus.SUCCESSFUL && ( +
+ + +
+ )} +
); diff --git a/src/ora-dashboard/data-table/DataTable.jsx b/src/ora-dashboard/data-table/DataTable.jsx index 0854d34..43fdba0 100644 --- a/src/ora-dashboard/data-table/DataTable.jsx +++ b/src/ora-dashboard/data-table/DataTable.jsx @@ -1,12 +1,30 @@ 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'; +/** + * Sort implementation for Paragon Table + * @param {any} firstElement + * @param {any} secondElement + * @param {string} key + * @param {string} direction + */ +const sort = 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(); @@ -19,55 +37,70 @@ function DataTable({ intl, data }) { } }, [courseId, data]); + // create a copy of data for sortable Table + const sortableData = Object.values(data).slice(); + + // 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, + }, + ]; + 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) { + console.log('Sort in direction ', direction, column); + sortableData.sort((firstElement, secondElement) => sort(firstElement, secondElement, column.key, direction)); + }, + }))} + tableSortable + /> ); } DataTable.propTypes = { diff --git a/src/ora-dashboard/data/slices.js b/src/ora-dashboard/data/slices.js index e6e5fad..4906ab8 100644 --- a/src/ora-dashboard/data/slices.js +++ b/src/ora-dashboard/data/slices.js @@ -64,23 +64,21 @@ function oraBlockStatisticsFromPayload(blockIds, payload) { function prepareStatisticsSummary(data) { const dataKeys = Object.keys(data); const dataItemArr = dataKeys.map((key) => data[key]); - const sumByKey = (key) => dataItemArr.map((item) => item[key]).reduce((result, val) => result + val, 0); + + 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, }; - if (dataKeys.length > 0 && data[dataKeys[0]].status === RequestStatus.SUCCESSFUL) { - fields.forEach(field => { - summaryData[field] = sumByKey(field); - }); - } else if (dataKeys.length === 0) { - // there is no ORA block in the course. - fields.forEach(field => { - summaryData[field] = 0; - }); - } + fields.forEach(field => { + summaryData[field] = sumByKey(field); + }); + return summaryData; } diff --git a/src/ora-dashboard/summary-table/SummaryTable.jsx b/src/ora-dashboard/summary-table/SummaryTable.jsx index de60ba7..d58329a 100644 --- a/src/ora-dashboard/summary-table/SummaryTable.jsx +++ b/src/ora-dashboard/summary-table/SummaryTable.jsx @@ -1,57 +1,53 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import React from 'react'; +import { Table } from '@edx/paragon'; import messages from './messages'; -import LoadingOrValue from '../../components/value-or-loading/ValueOrLoading'; import { oraSummaryDataShape } from '../../data/constants'; 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 = { From 3caf1254db967653d63c81a4b80cd06bba4e91e1 Mon Sep 17 00:00:00 2001 From: Shimul Chowdhury Date: Mon, 23 Nov 2020 23:54:53 +0600 Subject: [PATCH 3/5] Embed ORA block --- src/ora-dashboard/data-table/DataTable.jsx | 36 ++++++++++++------ .../embed-ora-modal/EmbedORAModal.jsx | 37 +++++++++++++++++++ 2 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 src/ora-dashboard/embed-ora-modal/EmbedORAModal.jsx diff --git a/src/ora-dashboard/data-table/DataTable.jsx b/src/ora-dashboard/data-table/DataTable.jsx index 43fdba0..4d0dfec 100644 --- a/src/ora-dashboard/data-table/DataTable.jsx +++ b/src/ora-dashboard/data-table/DataTable.jsx @@ -6,6 +6,7 @@ import { useParams } from 'react-router'; 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 @@ -38,7 +39,7 @@ function DataTable({ intl, data }) { }, [courseId, data]); // create a copy of data for sortable Table - const sortableData = Object.values(data).slice(); + const sortableData = Object.values(data).slice().map(item => ({ ...item, actions: () })); // define Table columns const columns = [ @@ -87,20 +88,31 @@ function DataTable({ intl, data }) { key: 'done', columnSortable: true, }, + { + label: '', + key: 'actions', + }, ]; + console.log(sortableData.map(i => i.id)); + return ( -
({ - ...column, - onSort(direction) { - console.log('Sort in direction ', direction, column); - sortableData.sort((firstElement, secondElement) => sort(firstElement, secondElement, column.key, direction)); - }, - }))} - tableSortable - /> +
+
({ + ...column, + onSort(direction) { + console.log('Sort in direction ', direction, column); + sortableData.sort( + (firstElement, secondElement) => sort(firstElement, secondElement, column.key, direction), + ); + }, + }))} + tableSortable + /> + {/* {iframes} */} + ); } DataTable.propTypes = { 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={( +
+