From abf205190fa6da67ac5eb425ea73ad498a97ade0 Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Wed, 29 Jan 2025 21:59:50 +0000 Subject: [PATCH 1/3] fix: bug in datatable with multi select --- .../AddMembersModalContent.jsx | 4 +-- .../CreateGroupModalContent.jsx | 4 +-- ...sx => EnterpriseCustomerUserDataTable.jsx} | 34 +++++++++---------- .../GroupDetailPage/AddMembersBulkAction.jsx | 8 +++-- .../GroupDetailPage/GroupMembersTable.jsx | 1 + .../RemoveMembersBulkAction.jsx | 6 +++- .../tests/AddMembersModal.test.jsx | 6 ++-- .../tests/CreateGroupModal.test.jsx | 7 ++-- 8 files changed, 39 insertions(+), 31 deletions(-) rename src/components/PeopleManagement/{EnterpriseCustomerUserDatatable.jsx => EnterpriseCustomerUserDataTable.jsx} (84%) diff --git a/src/components/PeopleManagement/AddMembersModal/AddMembersModalContent.jsx b/src/components/PeopleManagement/AddMembersModal/AddMembersModalContent.jsx index 4556fa8d3a..367fe13d6d 100644 --- a/src/components/PeopleManagement/AddMembersModal/AddMembersModalContent.jsx +++ b/src/components/PeopleManagement/AddMembersModal/AddMembersModalContent.jsx @@ -12,7 +12,7 @@ import AddMembersModalSummary from './AddMembersModalSummary'; import InviteSummaryCount from '../../learner-credit-management/invite-modal/InviteSummaryCount'; import FileUpload from '../../learner-credit-management/invite-modal/FileUpload'; import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isInviteEmailAddressesInputValueValid } from '../../learner-credit-management/cards/data'; -import EnterpriseCustomerUserDatatable from '../EnterpriseCustomerUserDatatable'; +import EnterpriseCustomerUserDataTable from '../EnterpriseCustomerUserDataTable'; import { useEnterpriseLearners } from '../../learner-credit-management/data'; const AddMembersModalContent = ({ @@ -129,7 +129,7 @@ const AddMembersModalContent = ({
- - { }; const FilterStatus = (rest) => ; -const EnterpriseCustomerUserDatatable = ({ +const EnterpriseCustomerUserDataTable = ({ enterpriseId, learnerEmails, onHandleAddMembersBulkAction, @@ -47,6 +47,12 @@ const EnterpriseCustomerUserDatatable = ({ fetchEnterpriseMembersTableData, } = useEnterpriseMembersTableData({ enterpriseId }); + const selectColumn = { + id: 'selection', + Header: DataTable.ControlledSelectHeader, + Cell: DataTable.ControlledSelect, + disableSortBy: true, + }; return ( row.enterpriseCustomerUser.userId.toString(), }} pageCount={enterpriseMembersTableData.pageCount} - manualSelectColumn={ - { - id: 'selection', - Header: DataTable.ControlledSelectHeader, - /* eslint-disable react/no-unstable-nested-components */ - Cell: (props) => , - disableSortBy: true, - } - } + manualSelectColumn={selectColumn} + SelectionStatusComponent={DataTable.ControlledSelectionStatus} /> ); }; -EnterpriseCustomerUserDatatable.defaultProps = { +EnterpriseCustomerUserDataTable.defaultProps = { enterpriseGroupLearners: [], }; -EnterpriseCustomerUserDatatable.propTypes = { +EnterpriseCustomerUserDataTable.propTypes = { enterpriseId: PropTypes.string.isRequired, learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired, onHandleRemoveMembersBulkAction: PropTypes.func.isRequired, @@ -129,7 +128,6 @@ BaseSelectWithContext.propTypes = { getToggleRowSelectedProps: PropTypes.func.isRequired, id: PropTypes.string, }).isRequired, - contextKey: PropTypes.string.isRequired, enterpriseGroupLearners: PropTypes.arrayOf(PropTypes.shape({})), }; @@ -137,4 +135,4 @@ const mapStateToProps = state => ({ enterpriseId: state.portalConfiguration.enterpriseId, }); -export default connect(mapStateToProps)(EnterpriseCustomerUserDatatable); +export default connect(mapStateToProps)(EnterpriseCustomerUserDataTable); diff --git a/src/components/PeopleManagement/GroupDetailPage/AddMembersBulkAction.jsx b/src/components/PeopleManagement/GroupDetailPage/AddMembersBulkAction.jsx index cd1d6a4d95..1bbce7b450 100644 --- a/src/components/PeopleManagement/GroupDetailPage/AddMembersBulkAction.jsx +++ b/src/components/PeopleManagement/GroupDetailPage/AddMembersBulkAction.jsx @@ -59,12 +59,16 @@ const AddMembersBulkAction = ({ ); }; +AddMembersBulkAction.defaultProps = { + selectedFlatRows: [], +}; + AddMembersBulkAction.propTypes = { - selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired, + selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()), enterpriseId: PropTypes.string.isRequired, onHandleAddMembersBulkAction: PropTypes.func.isRequired, isEntireTableSelected: PropTypes.bool, - enterpriseGroupLearners: PropTypes.arrayOf(PropTypes.string), + enterpriseGroupLearners: PropTypes.arrayOf(PropTypes.shape({})), }; export default AddMembersBulkAction; diff --git a/src/components/PeopleManagement/GroupDetailPage/GroupMembersTable.jsx b/src/components/PeopleManagement/GroupDetailPage/GroupMembersTable.jsx index c212bd9c93..02864bf8ac 100644 --- a/src/components/PeopleManagement/GroupDetailPage/GroupMembersTable.jsx +++ b/src/components/PeopleManagement/GroupDetailPage/GroupMembersTable.jsx @@ -49,6 +49,7 @@ const KabobMenu = ({ src={MoreVert} iconAs={Icon} variant="primary" + alt="Kabob menu dropdown" /> diff --git a/src/components/PeopleManagement/RemoveMembersBulkAction.jsx b/src/components/PeopleManagement/RemoveMembersBulkAction.jsx index 99da098997..70c97c90ff 100644 --- a/src/components/PeopleManagement/RemoveMembersBulkAction.jsx +++ b/src/components/PeopleManagement/RemoveMembersBulkAction.jsx @@ -23,9 +23,13 @@ const RemoveMembersBulkAction = ({ ); }; +RemoveMembersBulkAction.defaultProps = { + selectedFlatRows: null, +}; + RemoveMembersBulkAction.propTypes = { learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired, - selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired, + selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()), onHandleRemoveMembersBulkAction: PropTypes.func.isRequired, isEntireTableSelected: PropTypes.bool, }; diff --git a/src/components/PeopleManagement/tests/AddMembersModal.test.jsx b/src/components/PeopleManagement/tests/AddMembersModal.test.jsx index ae24d146de..b1a865b275 100644 --- a/src/components/PeopleManagement/tests/AddMembersModal.test.jsx +++ b/src/components/PeopleManagement/tests/AddMembersModal.test.jsx @@ -191,9 +191,9 @@ describe('', () => { }, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 }); // testing interaction with adding members from the datatable - const membersCheckbox = screen.getAllByTitle('Toggle row selected'); - userEvent.click(membersCheckbox[0]); - userEvent.click(membersCheckbox[1]); + const membersCheckboxes = screen.getAllByRole('checkbox'); + userEvent.click(membersCheckboxes[1]); + userEvent.click(membersCheckboxes[2]); const addMembersButton = screen.getAllByText('Add')[0]; userEvent.click(addMembersButton); diff --git a/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx b/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx index c6c1c22db7..1c5de9002f 100644 --- a/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx +++ b/src/components/PeopleManagement/tests/CreateGroupModal.test.jsx @@ -179,9 +179,10 @@ describe('', () => { }, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 }); // testing interaction with adding members from the datatable - const membersCheckbox = screen.getAllByTitle('Toggle row selected'); - userEvent.click(membersCheckbox[0]); - userEvent.click(membersCheckbox[1]); + const membersCheckboxes = screen.getAllByRole('checkbox'); + // skipping first one because its the select all checkbox + userEvent.click(membersCheckboxes[1]); + userEvent.click(membersCheckboxes[2]); const addMembersButton = screen.getByText('Add'); userEvent.click(addMembersButton); From e800d19e2c82a064f0207dc82893dba8e763402d Mon Sep 17 00:00:00 2001 From: Kira Miller Date: Tue, 4 Feb 2025 19:28:58 +0000 Subject: [PATCH 2/3] fix: in progress file --- .../EnterpriseCustomerUserDataTable.jsx | 74 ++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/src/components/PeopleManagement/EnterpriseCustomerUserDataTable.jsx b/src/components/PeopleManagement/EnterpriseCustomerUserDataTable.jsx index 8de37e01b2..9e088c61e0 100644 --- a/src/components/PeopleManagement/EnterpriseCustomerUserDataTable.jsx +++ b/src/components/PeopleManagement/EnterpriseCustomerUserDataTable.jsx @@ -1,8 +1,11 @@ +import { useContext } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { - CheckboxControl, DataTable, TextFilter, + Button, CheckboxControl, DataTable, DataTableContext, TextFilter, } from '@openedx/paragon'; + +import BaseSelectionStatus from '../BulkEnrollmentPage/table/BaseSelectionStatus'; import { GROUP_MEMBERS_TABLE_DEFAULT_PAGE, GROUP_MEMBERS_TABLE_PAGE_SIZE, } from './constants'; @@ -34,6 +37,7 @@ export const BaseSelectWithContext = ({ row, enterpriseGroupLearners }) => { }; const FilterStatus = (rest) => ; + const EnterpriseCustomerUserDataTable = ({ enterpriseId, learnerEmails, @@ -47,10 +51,76 @@ const EnterpriseCustomerUserDataTable = ({ fetchEnterpriseMembersTableData, } = useEnterpriseMembersTableData({ enterpriseId }); + + const SelectionStatus = () => { + + // const { page, toggleAllRowsSelected, state, controlledTableSelections, + // } = useContext(DataTableContext); + // const numSelectedRowsOnPage = page.filter(r => r.isSelected).length; + + // const { selectedRowIds } = state; + // const numSelectedRows = Object.keys(selectedRowIds || {}).length; + // // const numSelectedRows = enterpriseGroupLearners.length; + // console.log(state) + // console.log(controlledTableSelections) + + + // const handleClearSelection = () => { + // toggleAllRowsSelected(false); + // }; + + // return ( + // <> + // {numSelectedRows} selected ({numSelectedRowsOnPage} shown below) + // {numSelectedRows > 0 && ( + // + // )} + // + // ); + const { + itemCount, + page, + controlledTableSelections: [{ selectedRows, isEntireTableSelected }, dispatch], + } = useContext(DataTableContext); + + useEffect( + () => { + if (isEntireTableSelected) { + const selectedRowIds = getRowIds(selectedRows); + // const unselectedPageRows = getUnselectedPageRows(selectedRowIds, page); + // if (unselectedPageRows.length) { + // dispatch(setSelectedRowsAction(unselectedPageRows, itemCount)); + // } + } + }, + [isEntireTableSelected, selectedRows, itemCount, page, dispatch], + ); + + const numSelectedRows = isEntireTableSelected ? itemCount : selectedRows.length; + const numSelectedRowsOnPage = (page || []).filter(r => r.isSelected).length; + + const selectionStatusProps = { + className, + numSelectedRows, + numSelectedRowsOnPage, + clearSelectionText, + onSelectAll: () => dispatch(setSelectAllRowsAllPagesAction()), + onClear: () => dispatch(clearSelectionAction()), + }; + return ; + }; + const selectColumn = { id: 'selection', Header: DataTable.ControlledSelectHeader, - Cell: DataTable.ControlledSelect, + // Cell: DataTable.ControlledSelect, + Cell: (props) => , disableSortBy: true, }; return ( From fcbfff1bdb3684390a7260244d45e461c37da227 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Tue, 4 Feb 2025 18:20:51 -0500 Subject: [PATCH 3/3] fix: tooltip icons for existing members --- .../EnterpriseCustomerUserDataTable.jsx | 278 ++++++++++-------- 1 file changed, 153 insertions(+), 125 deletions(-) diff --git a/src/components/PeopleManagement/EnterpriseCustomerUserDataTable.jsx b/src/components/PeopleManagement/EnterpriseCustomerUserDataTable.jsx index 9e088c61e0..7a28b26e00 100644 --- a/src/components/PeopleManagement/EnterpriseCustomerUserDataTable.jsx +++ b/src/components/PeopleManagement/EnterpriseCustomerUserDataTable.jsx @@ -1,11 +1,10 @@ -import { useContext } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { - Button, CheckboxControl, DataTable, DataTableContext, TextFilter, + Button, + CheckboxControl, DataTable, DataTableContext, Icon, IconButtonWithTooltip, Table, TextFilter, } from '@openedx/paragon'; -import BaseSelectionStatus from '../BulkEnrollmentPage/table/BaseSelectionStatus'; import { GROUP_MEMBERS_TABLE_DEFAULT_PAGE, GROUP_MEMBERS_TABLE_PAGE_SIZE, } from './constants'; @@ -14,6 +13,9 @@ import AddMembersBulkAction from './GroupDetailPage/AddMembersBulkAction'; import RemoveMembersBulkAction from './RemoveMembersBulkAction'; import MemberJoinedDateCell from './MemberJoinedDateCell'; import { useEnterpriseMembersTableData } from './data/hooks'; +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import classNames from 'classnames'; +import { HelpOutline } from '@openedx/paragon/icons'; export const BaseSelectWithContext = ({ row, enterpriseGroupLearners }) => { const { @@ -27,8 +29,7 @@ export const BaseSelectWithContext = ({ row, enterpriseGroupLearners }) => { @@ -37,6 +38,87 @@ export const BaseSelectWithContext = ({ row, enterpriseGroupLearners }) => { }; const FilterStatus = (rest) => ; +const EnterpriseCustomerUserDataTableContext = createContext(); + +const useCheckboxControlProps = (props) => { + const updatedProps = useMemo( + () => { + const { indeterminate, ...rest } = props; + return { isIndeterminate: indeterminate, ...rest }; + }, + [props], + ); + return updatedProps; +}; + +const addSelectedRowAction = (row, itemCount) => ({ + type: 'ADD ROW', + row, + itemCount, +}); + +const deleteSelectedRowAction = (rowId) => ({ + type: 'DELETE ROW', + rowId, +}); + +function getSelectedRowsFromGroupLearners(enterpriseGroupLearners) { + const selections = enterpriseGroupLearners.reduce((acc, learner) => { + acc[learner.lmsUserId] = true; + return acc; + }, {}); + return selections; +} + +const CustomSelectColumnCell = ({ row }) => { + const { enterpriseGroupLearners } = useContext(EnterpriseCustomerUserDataTableContext); + const [isAddedMember, setIsAddedMember] = useState(false); + const { + itemCount, + controlledTableSelections: [, dispatch], + } = useContext(DataTableContext); + + const toggleSelected = useCallback( + () => { + if (row.isSelected) { + dispatch(deleteSelectedRowAction(row.id)); + } else { + dispatch(addSelectedRowAction(row, itemCount)); + } + }, + [itemCount, row, dispatch], + ); + + useEffect(() => { + setIsAddedMember(!!enterpriseGroupLearners.find(learner => learner.lmsUserId === Number(row.id))); + }, [enterpriseGroupLearners, row.id]); + + const checkboxControlProps = useCheckboxControlProps( + row.getToggleRowSelectedProps(), + ); + + if (isAddedMember) { + return ( + This learner is already a member of this group.} + src={HelpOutline} + iconAs={Icon} + size="inline" + /> + ); + } + + return ( +
+ +
+ ); +}; const EnterpriseCustomerUserDataTable = ({ enterpriseId, @@ -51,133 +133,79 @@ const EnterpriseCustomerUserDataTable = ({ fetchEnterpriseMembersTableData, } = useEnterpriseMembersTableData({ enterpriseId }); - - const SelectionStatus = () => { - - // const { page, toggleAllRowsSelected, state, controlledTableSelections, - // } = useContext(DataTableContext); - // const numSelectedRowsOnPage = page.filter(r => r.isSelected).length; - - // const { selectedRowIds } = state; - // const numSelectedRows = Object.keys(selectedRowIds || {}).length; - // // const numSelectedRows = enterpriseGroupLearners.length; - // console.log(state) - // console.log(controlledTableSelections) - - - // const handleClearSelection = () => { - // toggleAllRowsSelected(false); - // }; - - // return ( - // <> - // {numSelectedRows} selected ({numSelectedRowsOnPage} shown below) - // {numSelectedRows > 0 && ( - // - // )} - // - // ); - const { - itemCount, - page, - controlledTableSelections: [{ selectedRows, isEntireTableSelected }, dispatch], - } = useContext(DataTableContext); - - useEffect( - () => { - if (isEntireTableSelected) { - const selectedRowIds = getRowIds(selectedRows); - // const unselectedPageRows = getUnselectedPageRows(selectedRowIds, page); - // if (unselectedPageRows.length) { - // dispatch(setSelectedRowsAction(unselectedPageRows, itemCount)); - // } - } - }, - [isEntireTableSelected, selectedRows, itemCount, page, dispatch], - ); - - const numSelectedRows = isEntireTableSelected ? itemCount : selectedRows.length; - const numSelectedRowsOnPage = (page || []).filter(r => r.isSelected).length; - - const selectionStatusProps = { - className, - numSelectedRows, - numSelectedRowsOnPage, - clearSelectionText, - onSelectAll: () => dispatch(setSelectAllRowsAllPagesAction()), - onClear: () => dispatch(clearSelectionAction()), - }; - return ; - }; - const selectColumn = { id: 'selection', Header: DataTable.ControlledSelectHeader, // Cell: DataTable.ControlledSelect, - Cell: (props) => , + Cell: CustomSelectColumnCell, disableSortBy: true, }; + + const contextValue = useMemo(() => ({ + enterpriseGroupLearners, + }), [enterpriseGroupLearners]); + return ( - , - , - ]} - columns={[ - { - Header: 'Member details', - accessor: 'name', - Cell: MemberDetailsCell, - }, - { - Header: 'Joined organization', - accessor: 'joinedOrg', - Cell: MemberJoinedDateCell, - disableFilters: true, - }, - ]} - initialState={{ - pageIndex: GROUP_MEMBERS_TABLE_DEFAULT_PAGE, - pageSize: GROUP_MEMBERS_TABLE_PAGE_SIZE, - sortBy: [ - { id: 'name', desc: true }, - ], - filters: [], - }} - data={enterpriseMembersTableData.results} - defaultColumnValues={{ Filter: TextFilter }} - FilterStatusComponent={FilterStatus} - fetchData={fetchEnterpriseMembersTableData} - isFilterable - isLoading={isLoading} - isPaginated - isSelectable - itemCount={enterpriseMembersTableData.itemCount} - manualFilters - manualPagination - isSortable - manualSortBy - initialTableOptions={{ - getRowId: row => row.enterpriseCustomerUser.userId.toString(), - }} - pageCount={enterpriseMembersTableData.pageCount} - manualSelectColumn={selectColumn} - SelectionStatusComponent={DataTable.ControlledSelectionStatus} - /> + + , + , + ]} + columns={[ + { + Header: 'Member details', + accessor: 'name', + Cell: MemberDetailsCell, + }, + { + Header: 'Joined organization', + accessor: 'joinedOrg', + Cell: MemberJoinedDateCell, + disableFilters: true, + }, + ]} + initialState={{ + pageIndex: GROUP_MEMBERS_TABLE_DEFAULT_PAGE, + pageSize: GROUP_MEMBERS_TABLE_PAGE_SIZE, + sortBy: [ + { id: 'name', desc: true }, + ], + filters: [], + // selectedRowIds: getSelectedRowsFromGroupLearners(enterpriseGroupLearners), + }} + data={enterpriseMembersTableData.results} + defaultColumnValues={{ Filter: TextFilter }} + FilterStatusComponent={FilterStatus} + fetchData={fetchEnterpriseMembersTableData} + isFilterable + isLoading={isLoading} + isPaginated + isSelectable + itemCount={enterpriseMembersTableData.itemCount} + manualFilters + manualPagination + isSortable + manualSortBy + initialTableOptions={{ + getRowId: row => row.enterpriseCustomerUser.userId.toString(), + }} + pageCount={enterpriseMembersTableData.pageCount} + manualSelectColumn={selectColumn} + SelectionStatusComponent={DataTable.ControlledSelectionStatus} + // onSelectedRowsChanged={(selectedRows) => { + // console.log('DEBUG?! onSelectedRowsChanged selectedRows', selectedRows); + // }} + /> + ); };