Skip to content

Commit

Permalink
feat: add additional segment events (#1132)
Browse files Browse the repository at this point in the history
* feat: add addtional segment events

* feat: close and open chip modal events

* feat: naming for track events added

* feat: more segment eventing work

* feat: breadcrumbs and budget overview eventing added

* feat: individual cancel and remind modal events

* feat: cancel and remind submission events

* feat: bulk cancel/remind eventing

* feat: Add failed redemption, refator select all events

* fix: updates how assignment configuration is retrieved

* chore: tests

* chore: more tests

* chore: PR feedback

* feat: add additional metadata across all events

* chore: Update tests

* chore: PR feedback with abstractions

* chore: Update comments

* chore: PR fixes

* chore: remove extraneous code from test
  • Loading branch information
brobro10000 authored Jan 2, 2024
1 parent f57653e commit 2d7e691
Show file tree
Hide file tree
Showing 27 changed files with 972 additions and 129 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Chip } from '@edx/paragon';
import PropTypes from 'prop-types';
import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';
import { connect } from 'react-redux';
import FailedBadEmail from './assignments-status-chips/FailedBadEmail';
import FailedCancellation from './assignments-status-chips/FailedCancellation';
import FailedRedemption from './assignments-status-chips/FailedRedemption';
Expand All @@ -8,14 +10,61 @@ import FailedSystem from './assignments-status-chips/FailedSystem';
import NotifyingLearner from './assignments-status-chips/NotifyingLearner';
import WaitingForLearner from './assignments-status-chips/WaitingForLearner';
import { capitalizeFirstLetter } from '../../utils';
import { useBudgetId, useSubsidyAccessPolicy } from './data';

const AssignmentStatusTableCell = ({ row }) => {
const AssignmentStatusTableCell = ({ enterpriseId, row }) => {
const { original } = row;
const {
learnerEmail,
learnerState,
errorReason,
} = original;
const { subsidyAccessPolicyId } = useBudgetId();
const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId);
const {
subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates,
} = subsidyAccessPolicy;

const sharedTrackEventMetadata = {
learnerState,
subsidyUuid,
assignmentConfiguration,
isSubsidyActive,
isAssignable,
catalogUuid,
aggregates,
};

const sendGenericTrackEvent = (eventName, eventMetadata = {}) => {
sendEnterpriseTrackEvent(
enterpriseId,
eventName,
{
...sharedTrackEventMetadata,
...eventMetadata,
},
);
};

const sendErrorStateTrackEvent = (eventName, eventMetadata = {}) => {
const errorReasonMetadata = {
erroredAction: {
errorReason: errorReason?.errorReason || null,
actionType: errorReason?.actionType || null,
},
};
const errorStateMetadata = {
...sharedTrackEventMetadata,
...errorReasonMetadata,
...eventMetadata,
};
sendEnterpriseTrackEvent(
enterpriseId,
eventName,
errorStateMetadata,
);
};

// Learner state is not available for this assignment, so don't display anything.
if (!learnerState) {
return null;
Expand All @@ -24,50 +73,50 @@ const AssignmentStatusTableCell = ({ row }) => {
// Display the appropriate status chip based on the learner state.
if (learnerState === 'notifying') {
return (
<NotifyingLearner learnerEmail={learnerEmail} />
<NotifyingLearner learnerEmail={learnerEmail} trackEvent={sendGenericTrackEvent} />
);
}

if (learnerState === 'waiting') {
return (
<WaitingForLearner learnerEmail={learnerEmail} />
<WaitingForLearner learnerEmail={learnerEmail} trackEvent={sendGenericTrackEvent} />
);
}

if (learnerState === 'failed') {
// If learnerState is failed but no top-level error reason is defined, return a failed system chip.
if (!errorReason) {
return <FailedSystem />;
return <FailedSystem trackEvent={sendErrorStateTrackEvent} />;
}
// Determine which failure chip to display based on the top level errorReason. In most cases, the actual errorReason
// code is ignored, in which case we key off the actionType.
if (errorReason.actionType === 'notified') {
if (errorReason.errorReason === 'email_error') {
return (
<FailedBadEmail learnerEmail={learnerEmail} />
<FailedBadEmail learnerEmail={learnerEmail} trackEvent={sendErrorStateTrackEvent} />
);
}
// non-email errors on failed notifications should NOT use the FailedBadEmail chip.
return <FailedSystem />;
return <FailedSystem trackEvent={sendErrorStateTrackEvent} />;
}
if (errorReason.actionType === 'cancelled') {
return <FailedCancellation />;
return <FailedCancellation trackEvent={sendErrorStateTrackEvent} />;
}
if (errorReason.actionType === 'reminded') {
return <FailedReminder />;
return <FailedReminder trackEvent={sendErrorStateTrackEvent} />;
}
if (errorReason.actionType === 'redeemed') {
return <FailedRedemption />;
return <FailedRedemption trackEvent={sendErrorStateTrackEvent} />;
}
// In all other unexpected cases, return a failed system chip.
return <FailedSystem />;
return <FailedSystem trackEvent={sendErrorStateTrackEvent} />;
}

// Note: The given `learnerState` not officially supported with a `ModalPopup`, but display it anyway.
return <Chip>{`${capitalizeFirstLetter(learnerState)}`}</Chip>;
};

AssignmentStatusTableCell.propTypes = {
enterpriseId: PropTypes.string.isRequired,
row: PropTypes.shape({
original: PropTypes.shape({
learnerEmail: PropTypes.string,
Expand All @@ -84,4 +133,8 @@ AssignmentStatusTableCell.propTypes = {
}).isRequired,
};

export default AssignmentStatusTableCell;
const mapStateToProps = state => ({
enterpriseId: state.portalConfiguration.enterpriseId,
});

export default connect(mapStateToProps)(AssignmentStatusTableCell);
101 changes: 94 additions & 7 deletions src/components/learner-credit-management/AssignmentTableCancel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { DoNotDisturbOn } from '@edx/paragon/icons';
import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';
import { connect } from 'react-redux';
import CancelAssignmentModal from './CancelAssignmentModal';
import useCancelContentAssignments from './data/hooks/useCancelContentAssignments';
import { transformSelectedRows, useBudgetId, useSubsidyAccessPolicy } from './data';
import EVENT_NAMES from '../../eventTracking';
import { getActiveTableColumnFilters } from '../../utils';

const calculateTotalToCancel = ({
Expand All @@ -17,9 +21,23 @@ const calculateTotalToCancel = ({
return assignmentUuids.length;
};

const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected, tableInstance }) => {
const assignmentUuids = selectedFlatRows.map(row => row.id);
const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration;
const AssignmentTableCancelAction = ({
selectedFlatRows, isEntireTableSelected, learnerStateCounts, tableInstance, enterpriseId,
}) => {
const { subsidyAccessPolicyId } = useBudgetId();
const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId);
const {
subsidyUuid, assignmentConfiguration, isSubsidyActive, isAssignable, catalogUuid, aggregates,
} = subsidyAccessPolicy;

const {
uniqueLearnerState,
uniqueAssignmentState,
uniqueContentKeys,
totalContentQuantity,
assignmentUuids,
totalSelectedRows,
} = transformSelectedRows(selectedFlatRows);

const activeFilters = getActiveTableColumnFilters(tableInstance.columns);

Expand All @@ -32,7 +50,66 @@ const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected,
close,
isOpen,
open,
} = useCancelContentAssignments(assignmentConfigurationUuid, assignmentUuids, shouldCancelAll);
} = useCancelContentAssignments(assignmentConfiguration.uuid, assignmentUuids, shouldCancelAll);

const {
BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_CANCEL_MODAL,
BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_CANCEL_MODAL,
BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_CANCEL,
} = EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT;

const trackEvent = (eventName) => {
// constructs a learner state object for the select all state to match format of select all on page metadata
const learnerStateObject = {};
learnerStateCounts.forEach((learnerState) => {
learnerStateObject[learnerState.learnerState] = learnerState.count;
});

const selectedRowsMetadata = isEntireTableSelected
? { uniqueLearnerState: learnerStateObject, totalSelectedRows: tableInstance.itemCount }
: {
uniqueLearnerState, uniqueAssignmentState, uniqueContentKeys, totalContentQuantity, totalSelectedRows,
};

const trackEventMetadata = {
...selectedRowsMetadata,
isAssignable,
isSubsidyActive,
subsidyUuid,
catalogUuid,
isEntireTableSelected,
assignmentUuids,
aggregates,
assignmentConfiguration,
isOpen: !isOpen,
};

sendEnterpriseTrackEvent(
enterpriseId,
eventName,
trackEventMetadata,
);
};

const openModal = () => {
open();
trackEvent(
BUDGET_DETAILS_ASSIGNED_DATATABLE_OPEN_BULK_CANCEL_MODAL,
);
};

const closeModal = () => {
close();
trackEvent(
BUDGET_DETAILS_ASSIGNED_DATATABLE_CLOSE_BULK_CANCEL_MODAL,
);
};

const cancellationTrackEvent = () => {
trackEvent(
BUDGET_DETAILS_ASSIGNED_DATATABLE_BULK_CANCEL,
);
};

const tableItemCount = tableInstance.itemCount;
const totalToCancel = calculateTotalToCancel({
Expand All @@ -43,27 +120,37 @@ const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected,

return (
<>
<Button variant="danger" iconBefore={DoNotDisturbOn} onClick={open}>
<Button variant="danger" iconBefore={DoNotDisturbOn} onClick={openModal}>
{`Cancel (${totalToCancel})`}
</Button>
<CancelAssignmentModal
cancelContentAssignments={cancelContentAssignments}
close={close}
close={closeModal}
isOpen={isOpen}
cancelButtonState={cancelButtonState}
trackEvent={cancellationTrackEvent}
uuidCount={totalToCancel}
/>
</>
);
};

AssignmentTableCancelAction.propTypes = {
enterpriseId: PropTypes.string.isRequired,
selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired,
isEntireTableSelected: PropTypes.bool.isRequired,
learnerStateCounts: PropTypes.arrayOf(PropTypes.shape({
learnerState: PropTypes.string.isRequired,
count: PropTypes.number.isRequired,
})).isRequired,
tableInstance: PropTypes.shape({
itemCount: PropTypes.number.isRequired,
columns: PropTypes.arrayOf(PropTypes.shape()).isRequired,
}).isRequired,
};

export default AssignmentTableCancelAction;
const mapStateToProps = state => ({
enterpriseId: state.portalConfiguration.enterpriseId,
});

export default connect(mapStateToProps)(AssignmentTableCancelAction);
Loading

0 comments on commit 2d7e691

Please sign in to comment.