Skip to content

Commit

Permalink
feat: send emails by teams pluggable
Browse files Browse the repository at this point in the history
  • Loading branch information
johnvente committed Jan 4, 2024
1 parent a6dfabb commit bcd7e63
Show file tree
Hide file tree
Showing 15 changed files with 757 additions and 6 deletions.
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,18 @@
"@openedx-plugins/communications-app-schedule-section": "file:plugins/communications-app/ScheduleSection",
"@openedx-plugins/communications-app-subject-form": "file:plugins/communications-app/SubjectForm",
"@openedx-plugins/communications-app-task-alert-modal": "file:plugins/communications-app/TaskAlertModalForm",
"@openedx-plugins/communications-app-team-emails": "file:plugins/communications-app/TeamEmails",
"@openedx-plugins/communications-app-test-component": "file:plugins/communications-app/TestComponent",
"@tinymce/tinymce-react": "3.14.0",
"axios": "0.27.2",
"classnames": "2.3.2",
"core-js": "3.26.1",
"humps": "^2.0.1",
"jquery": "3.6.1",
"popper.js": "1.16.1",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-bootstrap-typeahead": "^6.3.2",
"uuid": "^9.0.1",
"react-dom": "17.0.2",
"react-helmet": "^6.1.0",
"react-redux": "7.2.9",
Expand All @@ -74,7 +75,8 @@
"redux": "4.2.0",
"regenerator-runtime": "0.13.11",
"tinymce": "5.10.7",
"use-deep-compare-effect": "^1.8.1"
"use-deep-compare-effect": "^1.8.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"@edx/browserslist-config": "^1.2.0",
Expand Down
11 changes: 10 additions & 1 deletion plugins/communications-app/RecipientsForm/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const RecipientsForm = ({ cohorts: additionalCohorts, courseId }) => {
emailRecipients,
isFormSubmitted,
emailLearnersList = [],
teamsList = [],
} = formData;

const [selectedGroups, setSelectedGroups] = useState([]);
Expand Down Expand Up @@ -61,6 +62,8 @@ const RecipientsForm = ({ cohorts: additionalCohorts, courseId }) => {
dispatch(formActions.updateForm({ emailLearnersList: setEmailLearnersListUpdated }));
};

const isInvalidRecipients = teamsList.length === 0 && selectedGroups.length === 0;

useEffect(() => {
setSelectedGroups(emailRecipients);
}, [isEditMode, emailRecipients.length, emailRecipients]);
Expand Down Expand Up @@ -194,7 +197,13 @@ const RecipientsForm = ({ cohorts: additionalCohorts, courseId }) => {
/>
)}

{ isFormSubmitted && selectedGroups.length === 0 && (
<PluggableComponent
id="team-emails"
as="communications-app-team-emails"
courseId={courseId}
/>

{ isFormSubmitted && isInvalidRecipients && (
<Form.Control.Feedback
className="px-3"
type="invalid"
Expand Down
11 changes: 8 additions & 3 deletions plugins/communications-app/TaskAlertModalForm/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,19 @@ const TaskAlertModalForm = ({
isScheduleButtonClicked = false,
isFormSubmitted = false,
emailLearnersList = [],
teamsList = [],
teamsListFullData = [],
} = formData;

const changeFormStatus = (status) => dispatchForm(formActions.updateForm({ formStatus: status }));
const handleResetFormValues = () => dispatchForm(formActions.resetForm());

const handlePostEmailTask = async () => {
const emailRecipientsFormat = emailRecipients.filter((recipient) => recipient !== 'individual-learners');
const teamsNames = teamsListFullData.map(({ name }) => name);
const invalidRecipients = ['individual-learners', ...teamsNames];
const emailRecipientsFormat = emailRecipients.filter((recipient) => !invalidRecipients.includes(recipient));
const emailsLearners = emailLearnersList.map(({ email }) => email);
const extraTargets = { emails: emailsLearners };
const extraTargets = { emails: emailsLearners, teams: teamsList };
const emailData = new FormData();
emailData.append('action', 'send');
emailData.append('send_to', JSON.stringify(emailRecipientsFormat));
Expand Down Expand Up @@ -93,7 +97,8 @@ const TaskAlertModalForm = ({
const isScheduleValid = isScheduled ? scheduleDate.length > 0 && scheduleTime.length > 0 : true;
const isIndividualEmailsValid = (emailRecipients.includes('individual-learners') && emailLearnersList.length > 0)
|| !emailRecipients.includes('individual-learners');
const isFormValid = emailRecipients.length > 0 && subject.length > 0
const isValidRecipients = emailRecipients.length > 0 || teamsList.length > 0;
const isFormValid = isValidRecipients && subject.length > 0
&& body.length > 0 && isScheduleValid && isIndividualEmailsValid;

if (isFormValid && isEditMode) {
Expand Down
56 changes: 56 additions & 0 deletions plugins/communications-app/TeamEmails/ListTeams.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import PropTypes from 'prop-types';
import { Form } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

import './ListTeams.scss';

const recipientsTeamFormDescription = 'A selectable choice from a list of potential email team recipients';

const ListTeams = ({ teams, onChangeCheckBox, teamsSelected }) => (
<div className="flex-wrap flex-row w-100">
{teams.map(({ id, name }) => (
<Form.Checkbox
key={`team:${name}_${id}`}
value={id}
className="mr-2 team-checkbox"
data-testid={`team:${id}`}
onChange={onChangeCheckBox}
checked={teamsSelected.includes(id)}
>
<FormattedMessage
id={`bulk.email.form.recipients.teams.${name}`}
defaultMessage={name}
description={recipientsTeamFormDescription}
/>
</Form.Checkbox>
))}
</div>
);

ListTeams.defaultProps = {
onChangeCheckBox: () => {},
teamsSelected: [],
};

ListTeams.propTypes = {
onChangeCheckBox: PropTypes.func,
teamsSelected: PropTypes.arrayOf(PropTypes.string),
teams: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
discussionTopicId: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
courseId: PropTypes.string.isRequired,
topicId: PropTypes.string.isRequired,
dateCreated: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
country: PropTypes.string.isRequired,
language: PropTypes.string.isRequired,
lastActivityAt: PropTypes.string.isRequired,
membership: PropTypes.arrayOf(PropTypes.shape()),
organizationProtected: PropTypes.bool.isRequired,
}),
).isRequired,
};

export default ListTeams;
7 changes: 7 additions & 0 deletions plugins/communications-app/TeamEmails/ListTeams.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.team-checkbox {
label {
overflow-wrap: break-word;
display: block !important;
max-width: 300px;
}
}
85 changes: 85 additions & 0 deletions plugins/communications-app/TeamEmails/ListTeams.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { IntlProvider } from 'react-intl';

import ListTeams from './ListTeams';

describe('ListTeams component', () => {
// eslint-disable-next-line no-unused-vars, react/prop-types
const IntlProviderWrapper = ({ children }) => (
<IntlProvider locale="en" messages={{}}>
{children}
</IntlProvider>
);
const teamsData = [
{
id: '1',
discussionTopicId: 'topic1',
name: 'Team 1',
courseId: 'course1',
topicId: 'topic1',
dateCreated: '2024-01-02T23:21:16.321434Z',
description: 'Description 1',
country: '',
language: '',
lastActivityAt: '2024-01-02T23:20:13Z',
membership: [],
organizationProtected: false,
},
];

test('renders checkboxes for each team', () => {
const { getAllByTestId } = render(
<IntlProviderWrapper>
<ListTeams teams={teamsData} />
</IntlProviderWrapper>,
);
const teamCheckboxes = getAllByTestId(/team:/i);
expect(teamCheckboxes).toHaveLength(teamsData.length);
});

test('displays team names in checkboxes', () => {
const { getByText } = render(
<IntlProviderWrapper>
<ListTeams teams={teamsData} />
</IntlProviderWrapper>,
);
teamsData.forEach(({ name }) => {
const teamNameElement = getByText(name);
expect(teamNameElement).toBeInTheDocument();
});
});

test('renders no checkboxes when teams array is empty', () => {
const { queryByTestId } = render(<ListTeams teams={[]} />);
const teamCheckboxes = queryByTestId(/team:/i);
expect(teamCheckboxes).toBeNull();
});

test('calls onChangeCheckBox function when a checkbox is clicked', () => {
const onChangeMock = jest.fn();
const { getByTestId } = render(
<IntlProviderWrapper>
<ListTeams teams={teamsData} onChangeCheckBox={onChangeMock} />
</IntlProviderWrapper>,
);

const checkbox = getByTestId('team:1');
fireEvent.click(checkbox);

expect(onChangeMock).toHaveBeenCalledTimes(1);
});

test('renders checkboxes with checked status for selected teams', () => {
const selectedTeams = ['1'];
const { getByTestId } = render(
<IntlProviderWrapper>
<ListTeams teams={teamsData} teamsSelected={selectedTeams} />
</IntlProviderWrapper>,
);

const checkbox = getByTestId('team:1');
expect(checkbox).toBeInTheDocument();
expect(checkbox).toBeChecked();
});
});
14 changes: 14 additions & 0 deletions plugins/communications-app/TeamEmails/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

export async function getTeamsList(courseId, page = 1, pageSize = 100) {
const endpointUrl = `${
getConfig().LMS_BASE_URL
}/platform-plugin-teams/${courseId}/api/topics/?page=${page}&pageSize=${pageSize}`;
try {
const response = await getAuthenticatedHttpClient().get(endpointUrl);
return response;
} catch (error) {
throw new Error(error);
}
}
52 changes: 52 additions & 0 deletions plugins/communications-app/TeamEmails/api.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';

import { getTeamsList } from './api';

jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));

describe('getTeamsList function', () => {
const mockCourseId = 'course123';
const mockResponseData = { data: 'someData' };
const mockConfig = { LMS_BASE_URL: 'http://localhost' };

beforeEach(() => {
getConfig.mockReturnValue(mockConfig);
getAuthenticatedHttpClient.mockReturnValue({
get: jest.fn().mockResolvedValue(mockResponseData),
});
});

test('successfully fetches teams list with default parameters', async () => {
const response = await getTeamsList(mockCourseId);

expect(response).toEqual(mockResponseData);
expect(getAuthenticatedHttpClient().get).toHaveBeenCalledWith(
`http://localhost/platform-plugin-teams/${mockCourseId}/api/topics/?page=1&pageSize=100`,
);
});

test('successfully fetches teams list with custom page and pageSize', async () => {
const customPage = 2;
const customPageSize = 50;

const response = await getTeamsList(mockCourseId, customPage, customPageSize);

expect(response).toEqual(mockResponseData);
expect(getAuthenticatedHttpClient().get).toHaveBeenCalledWith(
`http://localhost/platform-plugin-teams/${mockCourseId}/api/topics/?page=${customPage}&pageSize=${customPageSize}`,
);
});

test('handles an error', async () => {
const errorMessage = 'Network error';
getAuthenticatedHttpClient().get.mockRejectedValue(new Error(errorMessage));

await expect(getTeamsList(mockCourseId)).rejects.toThrow(errorMessage);
});
});
Loading

0 comments on commit bcd7e63

Please sign in to comment.