Skip to content

Commit

Permalink
Merge branch 'master' into kiram15/ENT-9466
Browse files Browse the repository at this point in the history
  • Loading branch information
kiram15 committed Dec 4, 2024
2 parents 201367c + 0c35ab3 commit aa7c2f7
Show file tree
Hide file tree
Showing 23 changed files with 923 additions and 142 deletions.
40 changes: 40 additions & 0 deletions src/components/PeopleManagement/EnrollmentsTableColumnHeader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import {
OverlayTrigger,
Tooltip,
Stack,
Icon,
} from '@openedx/paragon';
import { InfoOutline } from '@openedx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

const EnrollmentsTableColumnHeader = () => (
<Stack gap={1} direction="horizontal">
<span data-testid="members-table-enrollments-column-header">
<FormattedMessage
id="people.management.groups.detail.page.learnersTable.enrollmentsColumn"
defaultMessage="Enrollments"
description="Enrollments column header in the Members table"
/>
</span>
<OverlayTrigger
key="enrollments-column-tooltip"
placement="top"
overlay={(
<Tooltip id="enrollments-column-tooltip">
<div>
<FormattedMessage
id="people.management.groups.detail.page.learnersTable.enrollmentsColumn.tooltip"
defaultMessage="Total number of enrollment originated from the budget"
description="Tooltip for the Enrollments column header in the Group Members table"
/>
</div>
</Tooltip>
)}
>
<Icon size="xs" src={InfoOutline} className="ml-1 d-inline-flex" />
</OverlayTrigger>
</Stack>
);

export default EnrollmentsTableColumnHeader;
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import {
} from '@openedx/paragon';
import { Delete, Edit } from '@openedx/paragon/icons';

import { useEnterpriseGroupUuid } from '../../learner-credit-management/data';
import { useEnterpriseGroupLearnersTableData, useEnterpriseGroupUuid } from '../data/hooks';
import { ROUTE_NAMES } from '../../EnterpriseApp/data/constants';
import DeleteGroupModal from './DeleteGroupModal';
import EditGroupNameModal from './EditGroupNameModal';
import formatDates from '../utils';
import GroupMembersTable from '../GroupMembersTable';

const GroupDetailPage = () => {
const intl = useIntl();
Expand All @@ -20,7 +21,11 @@ const GroupDetailPage = () => {
const [isEditModalOpen, openEditModal, closeEditModal] = useToggle(false);
const [isLoading, setIsLoading] = useState(true);
const [groupName, setGroupName] = useState(enterpriseGroup?.name);

const {
isLoading: isTableLoading,
enterpriseGroupLearnersTableData,
fetchEnterpriseGroupLearnersTableData,
} = useEnterpriseGroupLearnersTableData({ groupUuid });
const handleNameUpdate = (name) => {
setGroupName(name);
};
Expand Down Expand Up @@ -119,7 +124,29 @@ const GroupDetailPage = () => {
</Card.Footer>
</Card>
</>
) : <Skeleton className="mt-3" height={200} count={1} /> }
) : <Skeleton className="mt-3" height={200} count={1} />}
<div className="mb-4 mt-5">
<h4 className="mt-1">
<FormattedMessage
id="people.management.group.details.page.label"
defaultMessage="Group members"
description="Label for the groups detail page with members"
/>
</h4>
<p className="font-weight-light">
<FormattedMessage
id="people.management.group.details.page.description"
defaultMessage="Add and remove group members."
description="Description for the members table in the Groups detail page"
/>
</p>
</div>
<GroupMembersTable
isLoading={isTableLoading}
tableData={enterpriseGroupLearnersTableData}
fetchTableData={fetchEnterpriseGroupLearnersTableData}
groupUuid={groupUuid}
/>
</div>
);
};
Expand Down
141 changes: 141 additions & 0 deletions src/components/PeopleManagement/GroupMembersTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
DataTable, Dropdown, Icon, IconButton,
} from '@openedx/paragon';
import { MoreVert, RemoveCircle } from '@openedx/paragon/icons';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import TableTextFilter from '../learner-credit-management/TableTextFilter';
import CustomDataTableEmptyState from '../learner-credit-management/CustomDataTableEmptyState';
import MemberDetailsTableCell from '../learner-credit-management/members-tab/MemberDetailsTableCell';
import EnrollmentsTableColumnHeader from './EnrollmentsTableColumnHeader';
import { GROUP_MEMBERS_TABLE_DEFAULT_PAGE, GROUP_MEMBERS_TABLE_PAGE_SIZE } from './constants';
import RecentActionTableCell from './RecentActionTableCell';

const FilterStatus = (rest) => <DataTable.FilterStatus showFilteredFields={false} {...rest} />;

const KabobMenu = () => (
<Dropdown drop="top">
<Dropdown.Toggle
id="kabob-menu-dropdown"
data-testid="kabob-menu-dropdown"
as={IconButton}
src={MoreVert}
iconAs={Icon}
variant="primary"
/>
<Dropdown.Menu>
<Dropdown.Item>
<Icon src={RemoveCircle} className="mr-2 text-danger-500" />
<FormattedMessage
id="people.management.budgetDetail.membersTab.kabobMenu.removeMember"
defaultMessage="Remove member"
description="Remove member option in the kabob menu"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);

const selectColumn = {
id: 'selection',
Header: DataTable.ControlledSelectHeader,
Cell: DataTable.ControlledSelect,
disableSortBy: true,
};

const GroupMembersTable = ({
isLoading,
tableData,
fetchTableData,
groupUuid,
}) => {
const intl = useIntl();
return (
<span className="budget-detail-assignments">
<DataTable
isSortable
manualSortBy
isSelectable
SelectionStatusComponent={DataTable.ControlledSelectionStatus}
manualSelectColumn={selectColumn}
isPaginated
manualPagination
isFilterable
manualFilters
isLoading={isLoading}
defaultColumnValues={{ Filter: TableTextFilter }}
FilterStatusComponent={FilterStatus}
numBreakoutFilters={2}
columns={[
{
Header: intl.formatMessage({
id: 'people.management.groups.detail.page.members.columns.memberDetails',
defaultMessage: 'Member details',
description: 'Column header for the Member details column in the People management Groups detail page',
}),
accessor: 'memberDetails',
Cell: MemberDetailsTableCell,
},
{
Header: intl.formatMessage({
id: 'people.management.groups.detail.page.members.columns.recentAction',
defaultMessage: 'Recent action',
description: 'Column header for the Recent action column in the People management Groups detail page',
}),
accessor: 'recentAction',
Cell: RecentActionTableCell,
disableFilters: true,
},
{
Header: EnrollmentsTableColumnHeader,
accessor: 'enrollmentCount',
Cell: ({ row }) => row.original.enrollments,
disableFilters: true,
},
]}
initialTableOptions={{
getRowId: row => row?.memberDetails.userEmail,
autoResetPage: true,
}}
initialState={{
pageSize: GROUP_MEMBERS_TABLE_PAGE_SIZE,
pageIndex: GROUP_MEMBERS_TABLE_DEFAULT_PAGE,
sortBy: [
{ id: 'memberDetails', desc: true },
],
filters: [],
}}
additionalColumns={[
{
id: 'action',
Header: '',
// eslint-disable-next-line react/no-unstable-nested-components
Cell: (props) => (
<KabobMenu {...props} groupUuid={groupUuid} />
),
},
]}
fetchData={fetchTableData}
data={tableData.results}
itemCount={tableData.itemCount}
pageCount={tableData.pageCount}
EmptyTableComponent={CustomDataTableEmptyState}
/>
</span>
);
};

GroupMembersTable.propTypes = {
isLoading: PropTypes.bool.isRequired,
tableData: PropTypes.shape({
results: PropTypes.arrayOf(PropTypes.shape({
})),
itemCount: PropTypes.number.isRequired,
pageCount: PropTypes.number.isRequired,
}).isRequired,
fetchTableData: PropTypes.func.isRequired,
groupUuid: PropTypes.string.isRequired,
};

export default GroupMembersTable;
19 changes: 19 additions & 0 deletions src/components/PeopleManagement/RecentActionTableCell.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import formatDates from './utils';

const RecentActionTableCell = ({
row,
}) => (
<div>Added: {formatDates(row.original.activatedAt)}</div>
);

RecentActionTableCell.propTypes = {
row: PropTypes.shape({
original: PropTypes.shape({
activatedAt: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
};

export default RecentActionTableCell;
3 changes: 3 additions & 0 deletions src/components/PeopleManagement/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export const GROUP_TYPE_FLEX = 'flex';

export const GROUP_DROPDOWN_TEXT = 'Select group';

export const GROUP_MEMBERS_TABLE_PAGE_SIZE = 10;
export const GROUP_MEMBERS_TABLE_DEFAULT_PAGE = 0; // `DataTable` uses zero-index array

// Query Key factory for the people management module, intended to be used with `@tanstack/react-query`.
// Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories.
export const peopleManagementQueryKeys = {
Expand Down
3 changes: 3 additions & 0 deletions src/components/PeopleManagement/data/hooks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as useEnterpriseGroupUuid } from './useEnterpriseGroupUuid';
export { default as useEnterpriseGroupLearnersTableData } from './useEnterpriseGroupLearnersTableData';
export { default as useEnterpriseMembersTableData } from './useEnterpriseMembersTableData';
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
useCallback, useMemo, useState,
} from 'react';
import _ from 'lodash';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import { logError } from '@edx/frontend-platform/logging';
import debounce from 'lodash.debounce';

import LmsApiService from '../../../../data/services/LmsApiService';

const useEnterpriseGroupLearnersTableData = ({ groupUuid }) => {
const [isLoading, setIsLoading] = useState(true);
const [enterpriseGroupLearnersTableData, setEnterpriseGroupLearnersTableData] = useState({
itemCount: 0,
pageCount: 0,
results: [],
});
const fetchEnterpriseGroupLearnersData = useCallback((args) => {
const fetch = async () => {
try {
setIsLoading(true);
const options = {};
if (args?.sortBy.length > 0) {
const sortByValue = args.sortBy[0].id;
options.sort_by = _.snakeCase(sortByValue);
if (!args.sortBy[0].desc) {
options.is_reversed = !args.sortBy[0].desc;
}
}
args.filters.forEach((filter) => {
const { id, value } = filter;
if (id === 'status') {
options.show_removed = value;
} else if (id === 'memberDetails') {
options.user_query = value;
}
});

options.page = args.pageIndex + 1;
const response = await LmsApiService.fetchEnterpriseGroupLearners(groupUuid, options);
const data = camelCaseObject(response.data);

setEnterpriseGroupLearnersTableData({
itemCount: data.count,
pageCount: data.numPages ?? Math.floor(data.count / options.pageSize),
results: data.results.filter(result => result.activatedAt),
});
} catch (error) {
logError(error);
} finally {
setIsLoading(false);
}
};
fetch();
}, [groupUuid]);

const debouncedFetchEnterpriseGroupLearnersData = useMemo(
() => debounce(fetchEnterpriseGroupLearnersData, 300),
[fetchEnterpriseGroupLearnersData],
);

return {
isLoading,
enterpriseGroupLearnersTableData,
fetchEnterpriseGroupLearnersTableData: debouncedFetchEnterpriseGroupLearnersData,
};
};

export default useEnterpriseGroupLearnersTableData;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { camelCaseObject } from '@edx/frontend-platform/utils';

import { learnerCreditManagementQueryKeys } from '../constants';
import { learnerCreditManagementQueryKeys } from '../../../learner-credit-management/data/constants';
import LmsApiService from '../../../../data/services/LmsApiService';

/**
Expand Down
Loading

0 comments on commit aa7c2f7

Please sign in to comment.