Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions src/components/Admin/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import AIAnalyticsSummarySkeleton from './AIAnalyticsSummarySkeleton';
import BudgetExpiryAlertAndModal from '../BudgetExpiryAlertAndModal';
import ModuleActivityReport from './tabs/ModuleActivityReport';
import EnrollmentsTablePOC from '../EnrollmentsTable/EnrollmentsTable';

class Admin extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -290,7 +291,7 @@
.some(item => item !== null);
}

hasEmptyData() {

Check failure on line 294 in src/components/Admin/index.jsx

View workflow job for this annotation

GitHub Actions / tests

Unused method or property "hasEmptyData" of class "Admin"
const {
numberOfUsers,
courseCompletions,
Expand Down Expand Up @@ -545,28 +546,27 @@
>
<div className="row">
<div className="col">
{!error && !loading && !this.hasEmptyData() && (
<>
<div className="row pb-3 mt-2">
<div className="col-12 col-md-12 col-xl-12">
{this.renderDownloadButton()}
</div>
</div>
{this.displaySearchBar() && (
<AdminSearchForm
searchParams={searchParams}
searchEnrollmentsList={() => this.props.searchEnrollmentsList()}
tableData={this.getTableData() ? this.getTableData().results : []}
budgets={budgets}
groups={groups}
enterpriseId={enterpriseId}
/>
)}
</>
{/* {!error && !loading && !this.hasEmptyData() && ( */}
<div className="row pb-3 mt-2">
<div className="col-12 col-md-12 col-xl-12">
{this.renderDownloadButton()}
</div>
</div>
{this.displaySearchBar() && (
<AdminSearchForm
searchParams={searchParams}
searchEnrollmentsList={() => this.props.searchEnrollmentsList()}
tableData={this.getTableData() ? this.getTableData().results : []}
budgets={budgets}
groups={groups}
enterpriseId={enterpriseId}
/>
)}
{/* )} */}
{csvErrorMessage && this.renderCsvErrorMessage(csvErrorMessage)}
<div className="mt-3 mb-5">
{enterpriseId && tableMetadata.component}
<EnrollmentsTablePOC searchParams={searchParams} />
{/* {enterpriseId && tableMetadata.component} */}
</div>
</div>
</div>
Expand Down
150 changes: 150 additions & 0 deletions src/components/Datatable/hooks/useDatatable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { useMemo, useState } from 'react';
import { logError } from '@edx/frontend-platform/logging';
import { v4 as uuidv4 } from 'uuid';
import snakeCase from 'lodash/snakeCase';
import { useLocation, useNavigate } from 'react-router-dom';
import isArray from 'lodash/isArray';
import { isObject } from 'lodash';

// We should attempt to include sane defaults for null values
const DEFAULT_PAGE_SIZE = 10;
const DEFAULT_PAGE_INDEX = 0;
const shortIdentifier = uuidv4().slice(0, 8);

// Handle filter and sorting
const handleFilters = (id, filters, customFilters = {}) => {
const filterObj = {};
// custom filter from external search input
for (const key in customFilters) {
if (customFilters[key]) {
filterObj[`${id}_${snakeCase(key)}`] = customFilters[key];
}
}

// default filter
filters.forEach((filter) => {
filterObj[`${id}${filter}`] = snakeCase(filter);
});
return filterObj;
};

const handleSortBy = (id, sortBy) => {
const sortByObj = { };
sortBy.forEach((sortByItem) => {
sortByObj[`${id}_ordering`] = sortByItem.desc ? `-${snakeCase(sortByItem.id)}` : `${snakeCase(sortByItem.id)}`;
});
return sortByObj;
};

// A wrapper to the fetchData call to include any necessary calls required for any datatable
const fetchDataWrapper = async (args) => {

Check failure on line 40 in src/components/Datatable/hooks/useDatatable.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected to return a value at the end of async arrow function
try {
const sortByObj = handleSortBy(args.dataTableId, args.sortBy);
const filtersObj = handleFilters(args.dataTableId, args.filters, args.customFilters);
const newSearchParams = { ...filtersObj, ...sortByObj };
const updatedArgs = { ...args, searchParams: newSearchParams };
const resolvedData = await args.fetchData(updatedArgs);
return resolvedData;
} catch (error) {
logError(error);
}
};

const useDatatable = (args) => {
const [data, setData] = useState([]);
const [originalResponse, setOriginalResponse] = useState(null);
const navigate = useNavigate();
const location = useLocation();

// Instantiate with values from args or sane defaults
const datatableProps = useMemo(() => ({
id: args.tableId || shortIdentifier,
initialState: args.initialState || {},
itemCount: args.itemCount || 0,
pageCount: args.pageCount || 1,
columns: args.columns || [],
fetchData: args.fetchData || null,
data: args.data || [],
isPaginated: args.isPaginated || false,
isSortable: args.isSortable || false,
isFilterable: args.isFilterable || false,
}), [args]);
const initialState = {
pageSize: args.initialState.pageSize || DEFAULT_PAGE_SIZE,
pageIndex: args.initialState.pageIndex || DEFAULT_PAGE_INDEX,
filters: args.initialState.filters || [],
sortBy: args.initialState.sortBy || [],
};

// Initial State
datatableProps.initialState = args.initialState;
// columns
datatableProps.columns = args.columns || [];

// manual pagination
if (args.manualPagination) {
datatableProps.manualPagination = true;
datatableProps.isPaginated = true;
datatableProps.initialState.pageSize = initialState.pageSize;
datatableProps.initialState.pageIndex = initialState.pageIndex;
}
// manual sort
if (args.manualSortBy) {
datatableProps.manualSortBy = true;
datatableProps.isSortable = true;
}

// manual filters
if (args.manualFilters) {
datatableProps.manualFilters = true;
datatableProps.isFilterable = true;
}

// Datatable data and fetch data
if (args.fetchData) {
datatableProps.fetchData = async (fetchDataArgs) => {
try {
const response = await fetchDataWrapper({
...fetchDataArgs,
fetchData: args.fetchData,
dataTableId: datatableProps.id,
customFilters: args.customFilters || {},
navigate,
location,
});
if (response) {
// TODO: Update conditionals
if (isArray(response) && data.length === 0) {
setData(response);
} else if (isObject(response) && originalResponse === null) {
setOriginalResponse(response);
const resultsData = args.dataAccessor ? response[args.dataAccessor] : response;
setData(resultsData);
}
return response;
}
return [];
} catch (error) {
logError(error);
}
return [];
};
// Data attribute
datatableProps.data = data[args.dataAccessor] || data || args.data || [];

// item and page count
if (datatableProps.isPaginated && originalResponse) {
if (originalResponse.count) {
datatableProps.itemCount = originalResponse.count;
}
if (originalResponse.numPages) {
datatableProps.pageCount = originalResponse.numPages;
} else if (datatableProps.itemCount) {
datatableProps.pageCount = Math.ceil(datatableProps.itemCount / datatableProps.data.length);
}
}
}
return useMemo(() => datatableProps, [datatableProps]);
};

export default useDatatable;
191 changes: 191 additions & 0 deletions src/components/EnrollmentsTable/EnrollmentsTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { DataTable, TextFilter } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useCallback, useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { camelCaseObject } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { useLocation, useNavigate } from 'react-router-dom';
import {
formatPercentage,
formatPrice,
i18nFormatPassedTimestamp,
i18nFormatProgressStatus,
i18nFormatTimestamp,
updateUrl,
} from '../../utils';
import useDatatable from '../Datatable/hooks/useDatatable';
import { analyticsEnrollmentsResponsePage1 } from '../../data/services/POCHelper';

const retrieveData = async (datatableProps, setDataset) => {

Check failure on line 19 in src/components/EnrollmentsTable/EnrollmentsTable.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected to return a value at the end of async arrow function
try {
const results = await datatableProps.data;
setDataset(results);
return results;
} catch (error) {
logError(error);
}
};

const EnrollmentsTablePOC = (props) => {
const [dataset, setDataset] = useState([]);
const intl = useIntl();
const navigate = useNavigate();
const location = useLocation();
const datasetFn = useCallback(async (args) => {
// Goal is to keep the URL updated with the filter and sortby metadata keyed to the datatable id
if (args.searchParams) {
updateUrl(navigate, location.pathname, args.searchParams);
}
if (!args) {
const results = await camelCaseObject(analyticsEnrollmentsResponsePage1());
return results;
}
let page = analyticsEnrollmentsResponsePage1();
if (args?.pageIndex === 1) {
page = await analyticsEnrollmentsResponsePage1().next();
}
return camelCaseObject(page);
}, []);

Check failure on line 48 in src/components/EnrollmentsTable/EnrollmentsTable.jsx

View workflow job for this annotation

GitHub Actions / tests

React Hook useCallback has missing dependencies: 'location.pathname' and 'navigate'. Either include them or remove the dependency array
const datatableProps = useDatatable({
tableId: 'enrollments',
initialState: {
pageSize: 50,
pageIndex: 0,
sortBy: [
{ id: 'lastActivityDate', desc: true },
],
filters: [],
},
manualPagination: true,
manualSortBy: true,
manualFilters: true,
fetchData: datasetFn,
customFilters: props.searchParams,

Check failure on line 63 in src/components/EnrollmentsTable/EnrollmentsTable.jsx

View workflow job for this annotation

GitHub Actions / tests

'searchParams' is missing in props validation
dataAccessor: 'results',
columns: [
{
Header: intl.formatMessage({
id: 'adminPortal.enrollmentsTable.user_email',
defaultMessage: 'Email',
}),
accessor: 'userEmail',
},
{
Header: intl.formatMessage({
id: 'adminPortal.enrollmentsTable.user_first_name',
defaultMessage: 'First Name',
description: 'Title for the first name column in the enrollments table',
}),
accessor: 'userFirstName',
disableFilters: true,
},
{
Header: intl.formatMessage({
id: 'adminPortal.enrollmentsTable.user_last_name',
defaultMessage: 'Last Name',
description: 'Title for the last name column in the enrollments table',
}),
accessor: 'userLastName',
disableFilters: true,
},
{
Header: intl.formatMessage({
id: 'adminPortal.enrollmentsTable.courseTitle',
defaultMessage: 'Course Title',
}),
accessor: 'courseTitle',
},
{
Header: intl.formatMessage({
id: 'adminPortal.enrollmentsTable.courseListPrice',
defaultMessage: 'Course Price',
}),
accessor: 'courseListPrice',
Cell: ({ row }) => formatPrice(row.values.courseListPrice),
disableFilters: true,
},
{
Header: intl.formatMessage({
id: 'adminPortal.enrollmentsTable.courseStartDate',
defaultMessage: 'Start Date',
}),
accessor: 'courseStartDate',
Cell: ({ row }) => i18nFormatTimestamp({ intl, timestamp: row.values.courseStartDate }),
},
{
Header: intl.formatMessage({
id: 'adminPortal.enrollmentsTable.courseEndDate',
defaultMessage: 'End Date',
}),
accessor: 'courseEndDate',
Cell: ({ row }) => i18nFormatTimestamp({ intl, timestamp: row.values.courseEndDate }),
disableFilters: true,
},
{
Header: intl.formatMessage({
id: 'adminPortal.enrollmentsTable.passedDate',
defaultMessage: 'Passed Date',
}),
accessor: 'passedDate',
Cell: ({ row }) => i18nFormatPassedTimestamp({ intl, timestamp: row.values.passedDate }),
disableFilters: true,
},
{
Header: intl.formatMessage({
id: 'adminPortal.enrollmentsTable.currentGrade',
defaultMessage: 'Current Grade',
}),
accessor: 'currentGrade',
Cell: ({ row }) => formatPercentage({ decimal: row.values.currentGrade }),
disableFilters: true,
},
{
Header: intl.formatMessage({
id: 'adminPortal.enrollmentsTable.progressStatus',
defaultMessage: 'Progress Status',
}),
accessor: 'progressStatus',
Cell: ({ row }) => i18nFormatProgressStatus({ intl, progressStatus: row.values.progressStatus }),
disableFilters: true,
},
{
Header: intl.formatMessage({
id: 'adminPortal.enrollmentsTable.lastActivityDate',
defaultMessage: 'Last Activity Date',
}),
accessor: 'lastActivityDate',
Cell: ({ row }) => i18nFormatTimestamp({ intl, timestamp: row.values.lastActivityDate }),
disableFilters: true,
},
],
});

useEffect(() => {
retrieveData(datatableProps, setDataset);
}, [datatableProps, datasetFn, setDataset]);

return (
<div id={datatableProps.tableId} className="enrollments" data-testid="enrollments-table">
<DataTable
{...datatableProps}
defaultColumnValues={{ Filter: TextFilter }}
data={dataset || []}
/>
</div>

);
};

const mapStateToProps = ((state, ownProps) => {
const tableState = state.table[ownProps.id] || {};
return {
enterpriseId: state.portalConfiguration.enterpriseId,
data: tableState.data && tableState.data.results,
currentPage: tableState.data && tableState.data.current_page,
pageCount: tableState.data && tableState.data.num_pages,
};
});

export default connect(
mapStateToProps,
)(EnrollmentsTablePOC);
Loading
Loading