Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds sorting and search functionality #1378

Closed
wants to merge 12 commits into from
2 changes: 1 addition & 1 deletion src/components/AdvanceAnalyticsV2/tabs/Leaderboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const Leaderboard = ({ startDate, endDate, enterpriseId }) => {
})}
tableSubtitle={intl.formatMessage({
id: 'advance.analytics.leaderboard.tab.datatable.leaderboard.subtitle',
defaultMessage: 'See the top learners by different measures of engagement. The results are defaulted to sort by learning hours. Download the full CSV below to sort by other metrics.',
defaultMessage: 'Explore the top learners ranked by engagement metrics. The list is sorted by learning hours by default. To dive deeper, download the full CSV to explore and sort by other metrics. *Only learners who have passed the course and completed at least one engagement activity (watching a video, submitting a problem, or posting in forums) are included.',
description: 'Subtitle for the leaderboard datatable.',
})}
startDate={startDate}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('Leaderboard Component', () => {
{
className: '.leaderboard-datatable-container',
title: 'Leaderboard',
subtitle: 'See the top learners by different measures of engagement. The results are defaulted to sort by learning hours. Download the full CSV below to sort by other metrics.',
subtitle: 'Explore the top learners ranked by engagement metrics. The list is sorted by learning hours by default. To dive deeper, download the full CSV to explore and sort by other metrics. *Only learners who have passed the course and completed at least one engagement activity (watching a video, submitting a problem, or posting in forums) are included.',
},
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { Stack, Icon } from '@openedx/paragon';
import { Error } from '@openedx/paragon/icons';

const AddMemberModalSummaryDuplicate = () => (
<Stack className="duplicate-warning" direction="horizontal" gap={3}>
<Icon className="text-info-500" src={Error} />
<div>
<div className="h4 mb-1">Only 1 invite per email address will be sent.</div>
<span className="small">One or more duplicate emails were detected. Ensure that your entry is correct before proceeding.</span>
</div>
</Stack>
);

export default AddMemberModalSummaryDuplicate;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';

const AddMemberModalSummaryEmptyState = () => (
<>
<div className="h4 mb-0">You haven&apos;t uploaded any members yet.</div>
<span className="small">Upload a CSV file or select members to get started.</span>
</>
);

export default AddMemberModalSummaryEmptyState;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { Stack, Icon } from '@openedx/paragon';
import { Error } from '@openedx/paragon/icons';

const AddMemberModalSummaryErrorState = () => (
<Stack direction="horizontal" gap={3}>
<Icon className="text-danger" src={Error} />
<div>
<div className="h4 mb-0">Members can&apos;t be added as entered.</div>
<span className="small">Please check your file and try again.</span>
</div>
</Stack>
);

export default AddMemberModalSummaryErrorState;
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { v4 as uuidv4 } from 'uuid';
import {
Button, Stack, Icon,
} from '@openedx/paragon';
import { Person } from '@openedx/paragon/icons';

import { MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT } from '../constants';
import { hasLearnerEmailsSummaryListTruncation } from '../utils';

const AddMemberModalSummaryLearnerList = ({
learnerEmails,
}) => {
const [isTruncated, setIsTruncated] = useState(hasLearnerEmailsSummaryListTruncation(learnerEmails));
const truncatedLearnerEmails = learnerEmails.slice(0, MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT);
const displayedLearnerEmails = isTruncated ? truncatedLearnerEmails : learnerEmails;

useEffect(() => {
setIsTruncated(hasLearnerEmailsSummaryListTruncation(learnerEmails));
}, [learnerEmails]);

const expandCollapseMessage = isTruncated
? `Show ${learnerEmails.length - MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT} more`
: 'Show less';

return (
<ul className="list-unstyled mb-0">
<Stack gap={2.5}>
{displayedLearnerEmails.map((emailAddress) => (
<li key={uuidv4()} className="small">
<div className="d-flex justify-content-between">
<div style={{ maxWidth: '85%' }}>
<Stack direction="horizontal" gap={2} className="align-items-center">
<Icon size="sm" src={Person} />
<div
className="text-nowrap overflow-hidden font-weight-bold"
style={{ textOverflow: 'ellipsis' }}
title={emailAddress}
data-hj-suppress
>
{emailAddress}
</div>
</Stack>
</div>
</div>
</li>
))}
</Stack>
{hasLearnerEmailsSummaryListTruncation(learnerEmails) && (
<Button
variant="link"
size="sm"
className="mt-2.5"
onClick={() => setIsTruncated(prevState => !prevState)}
>
{expandCollapseMessage}
</Button>
)}
</ul>
);
};

AddMemberModalSummaryLearnerList.propTypes = {
learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired,
};

export default AddMemberModalSummaryLearnerList;
136 changes: 136 additions & 0 deletions src/components/PeopleManagement/AddMembersModal/AddMembersModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React, { useCallback, useState, useEffect } from 'react';
import { logError } from '@edx/frontend-platform/logging';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { useQueryClient } from '@tanstack/react-query';
import { useIntl } from '@edx/frontend-platform/i18n';
import { snakeCaseObject } from '@edx/frontend-platform/utils';
import {
ActionRow, Button, FullscreenModal, StatefulButton, useToggle,
} from '@openedx/paragon';
import LmsApiService from '../../../data/services/LmsApiService';
import SystemErrorAlertModal from '../../learner-credit-management/cards/assignment-allocation-status-modals/SystemErrorAlertModal';
import AddMembersModalContent from './AddMembersModalContent';
import { learnerCreditManagementQueryKeys } from '../../learner-credit-management/data';
import { useAllEnterpriseGroupLearners } from '../data/hooks';

const AddMembersModal = ({
isModalOpen,
closeModal,
enterpriseUUID,
groupName,
groupUuid,
}) => {
const intl = useIntl();
const [learnerEmails, setLearnerEmails] = useState([]);
const [addButtonState, setAddButtonState] = useState('default');
const [canAddMembers, setCanAddMembersGroup] = useState(false);
const [isSystemErrorModalOpen, openSystemErrorModal, closeSystemErrorModal] = useToggle(false);
const handleCloseAddMembersModal = () => {
closeModal();
setAddButtonState('default');
};
const queryClient = useQueryClient();
const {
isLoading,
enterpriseGroupLearners,
} = useAllEnterpriseGroupLearners(groupUuid);

const handleAddMembers = async () => {
setAddButtonState('pending');
try {
const requestBody = snakeCaseObject({
learnerEmails,
});
await LmsApiService.inviteEnterpriseLearnersToGroup(groupUuid, requestBody);
queryClient.invalidateQueries({
queryKey: learnerCreditManagementQueryKeys.group(groupUuid),
});
setAddButtonState('complete');
handleCloseAddMembersModal();
} catch (err) {
logError(err);
setAddButtonState('error');
openSystemErrorModal();
}
};

const handleEmailAddressesChange = useCallback((
value,
{ canInvite = false } = {},
) => {
setLearnerEmails(value);
setCanAddMembersGroup(canInvite);
}, []);

useEffect(() => {
setCanAddMembersGroup(false);
if (canAddMembers) {
setCanAddMembersGroup(true);
}
}, [canAddMembers]);
return (
<div>
{!isLoading ? (
<div>
<FullscreenModal
className="stepper-modal bg-light-200"
isOpen={isModalOpen}
onClose={handleCloseAddMembersModal}
title={intl.formatMessage({
id: 'peopleManagement.tab.add.members.modal.title',
defaultMessage: 'Add members',
description: 'Title for adding members modal',
})}
footerNode={(
<ActionRow>
<ActionRow.Spacer />
<Button variant="tertiary" onClick={handleCloseAddMembersModal}>Cancel</Button>
<StatefulButton
labels={{
default: 'Add',
pending: 'Adding...',
complete: 'Added',
error: 'Try again',
}}
variant="primary"
state={addButtonState}
disabled={!canAddMembers}
onClick={handleAddMembers}
/>
</ActionRow>
)}
>
<AddMembersModalContent
groupName={groupName}
onEmailAddressesChange={handleEmailAddressesChange}
isGroupInvite
enterpriseUUID={enterpriseUUID}
enterpriseGroupLearners={enterpriseGroupLearners}
/>
</FullscreenModal>
<SystemErrorAlertModal
isErrorModalOpen={isSystemErrorModalOpen}
closeErrorModal={closeSystemErrorModal}
closeAssignmentModal={handleCloseAddMembersModal}
retry={handleAddMembers}
/>
</div>
) : null}
</div>
);
};

AddMembersModal.propTypes = {
enterpriseUUID: PropTypes.string.isRequired,
isModalOpen: PropTypes.bool.isRequired,
closeModal: PropTypes.func.isRequired,
groupUuid: PropTypes.string.isRequired,
groupName: PropTypes.string,
};

const mapStateToProps = state => ({
enterpriseUUID: state.portalConfiguration.enterpriseId,
});

export default connect(mapStateToProps)(AddMembersModal);
Loading