Skip to content

Commit

Permalink
feat: [FC-0044] group configurations MFE page (#929)
Browse files Browse the repository at this point in the history
* feat: group configurations - index page

* feat: [AXIMST-63] Index group configurations page

* fix: resolve discussions

* fix: resolve second round discussions

* feat: group configurations - content group actions

* feat: [AXIMST-75, AXIMST-69, AXIMST-81] Content group actions

* fix: resolve conversations

* feat: group configurations - sidebar

* feat: [AXIMST-87] group-configuration page sidebar

* refactor: [AXIMST-87] add changes after review

* refactor: [AXIMST-87] add changes after review

* refactor: [AXIMST-87] add changes ater review

---------

Co-authored-by: Kyrylo Hudym-Levkovych <[email protected]>

* fix: group configurations - the page reloads after the user saves changes

* feat: group configurations - experiment groups

* feat: [AXIMST-93, 99, 105] Group configuration - Experiment Groups

* fix: [AXIMST-518, 537] Group configuration - resolve bugs

* fix: review discussions

* fix: revert classname case

* fix: group configurations - resolve discussions

fix: [AXIMST-714] icon is aligned with text (#210)

* fix: add hook tests

* fix: add thunk tests

* fix: add slice tests

* chore: group configurations - messages

* fix: group configurations - remove delete in edit mode

---------

Co-authored-by: Kyr <[email protected]>
Co-authored-by: Kyrylo Hudym-Levkovych <[email protected]>
Co-authored-by: monteri <lansevermore>
  • Loading branch information
3 people authored Apr 23, 2024
1 parent 7f668a6 commit 907ce50
Show file tree
Hide file tree
Showing 62 changed files with 4,818 additions and 4 deletions.
5 changes: 5 additions & 0 deletions src/CourseAuthoringRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import CourseExportPage from './export-page/CourseExportPage';
import CourseImportPage from './import-page/CourseImportPage';
import { DECODED_ROUTES } from './constants';
import CourseChecklist from './course-checklist';
import GroupConfigurations from './group-configurations';

/**
* As of this writing, these routes are mounted at a path prefixed with the following:
Expand Down Expand Up @@ -100,6 +101,10 @@ const CourseAuthoringRoutes = () => {
path="course_team"
element={<PageWrap><CourseTeam courseId={courseId} /></PageWrap>}
/>
<Route
path="group_configurations"
element={<PageWrap><GroupConfigurations courseId={courseId} /></PageWrap>}
/>
<Route
path="settings/advanced"
element={<PageWrap><AdvancedSettings courseId={courseId} /></PageWrap>}
Expand Down
24 changes: 24 additions & 0 deletions src/generic/prompt-if-dirty/PromptIfDirty.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect } from 'react';
import PropTypes from 'prop-types';

const PromptIfDirty = ({ dirty }) => {
useEffect(() => {
// eslint-disable-next-line consistent-return
const handleBeforeUnload = (event) => {
if (dirty) {
event.preventDefault();
}
};
window.addEventListener('beforeunload', handleBeforeUnload);

return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [dirty]);

return null;
};
PromptIfDirty.propTypes = {
dirty: PropTypes.bool.isRequired,
};
export default PromptIfDirty;
72 changes: 72 additions & 0 deletions src/generic/prompt-if-dirty/PromptIfDirty.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { act } from 'react-dom/test-utils';
import PromptIfDirty from './PromptIfDirty';

describe('PromptIfDirty', () => {
let container = null;
let mockEvent = null;

beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
mockEvent = new Event('beforeunload');
jest.spyOn(window, 'addEventListener');
jest.spyOn(window, 'removeEventListener');
jest.spyOn(mockEvent, 'preventDefault');
Object.defineProperty(mockEvent, 'returnValue', { writable: true });
mockEvent.returnValue = '';
});

afterEach(() => {
window.addEventListener.mockRestore();
window.removeEventListener.mockRestore();
mockEvent.preventDefault.mockRestore();
mockEvent = null;
unmountComponentAtNode(container);
container.remove();
container = null;
});

it('should add event listener on mount', () => {
act(() => {
render(<PromptIfDirty dirty />, container);
});

expect(window.addEventListener).toHaveBeenCalledWith('beforeunload', expect.any(Function));
});

it('should remove event listener on unmount', () => {
act(() => {
render(<PromptIfDirty dirty />, container);
});
act(() => {
unmountComponentAtNode(container);
});

expect(window.removeEventListener).toHaveBeenCalledWith('beforeunload', expect.any(Function));
});

it('should call preventDefault and set returnValue when dirty is true', () => {
act(() => {
render(<PromptIfDirty dirty />, container);
});
act(() => {
window.dispatchEvent(mockEvent);
});

expect(mockEvent.preventDefault).toHaveBeenCalled();
expect(mockEvent.returnValue).toBe('');
});

it('should not call preventDefault when dirty is false', () => {
act(() => {
render(<PromptIfDirty dirty={false} />, container);
});
act(() => {
window.dispatchEvent(mockEvent);
});

expect(mockEvent.preventDefault).not.toHaveBeenCalled();
});
});
130 changes: 130 additions & 0 deletions src/group-configurations/GroupConfigurations.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
@import "./empty-placeholder/EmptyPlaceholder";

.configuration-section-name {
text-transform: lowercase;

&::first-letter {
text-transform: capitalize;
}

.group-percentage-container {
width: 1rem;
}
}

.configuration-card {
@include pgn-box-shadow(1, "down");

background: $white;
border-radius: .375rem;
padding: map-get($spacers, 4);
margin-bottom: map-get($spacers, 4);

.configuration-card-header {
display: flex;
align-items: center;
align-content: center;
justify-content: space-between;

.configuration-card-header__button {
display: flex;
align-items: flex-start;
padding: 0;
height: auto;
color: $black;

&:focus::before {
display: none;
}

.pgn__icon {
display: inline-block;
margin-right: map-get($spacers, 1);
margin-bottom: map-get($spacers, 2\.5);
}

.pgn__hstack {
align-items: baseline;
}

&:hover {
background: transparent;
}
}

.configuration-card-header__title {
text-align: left;

h3 {
margin-bottom: map-get($spacers, 2);
}
}

.configuration-card-header__badge {
display: flex;
padding: .125rem map-get($spacers, 2);
justify-content: center;
align-items: center;
border-radius: $border-radius;
border: .063rem solid $light-300;
background: $white;

&:first-child {
margin-left: map-get($spacers, 2\.5);
}

& span:last-child {
color: $primary-700;
}
}

.configuration-card-header__delete-tooltip {
pointer-events: all;
}
}

.configuration-card-content {
margin: 0 map-get($spacers, 2) 0 map-get($spacers, 4);

.configuration-card-content__experiment-stack {
display: flex;
justify-content: space-between;
padding: map-get($spacers, 2\.5) 0;
margin: 0;
color: $primary-500;
gap: $spacer;

&:not(:last-child) {
border-bottom: .063rem solid $light-400;
}
}
}

.pgn__form-control-decorator-group {
margin-inline-end: 0;
}

.configuration-form-group {
.pgn__form-label {
font: normal $font-weight-bold .875rem/1.25rem $font-family-base;
color: $gray-700;
margin-bottom: .875rem;
}

.pgn__form-control-description,
.pgn__form-text {
font: normal $font-weight-normal .75rem/1.25rem $font-family-base;
color: $gray-500;
margin-top: .625rem;
}

.pgn__form-text-invalid {
color: $form-feedback-invalid-color;
}
}

.experiment-configuration-form-percentage {
width: 5rem;
text-align: center;
}
}
106 changes: 106 additions & 0 deletions src/group-configurations/GroupConfigurations.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import MockAdapter from 'axios-mock-adapter';
import { render, waitFor, within } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

import { RequestStatus } from '../data/constants';
import initializeStore from '../store';
import { executeThunk } from '../utils';
import { getContentStoreApiUrl } from './data/api';
import { fetchGroupConfigurationsQuery } from './data/thunk';
import { groupConfigurationResponseMock } from './__mocks__';
import messages from './messages';
import experimentMessages from './experiment-configurations-section/messages';
import contentGroupsMessages from './content-groups-section/messages';
import GroupConfigurations from '.';

let axiosMock;
let store;
const courseId = 'course-v1:org+101+101';
const enrollmentTrackGroups = groupConfigurationResponseMock.allGroupConfigurations[0];
const contentGroups = groupConfigurationResponseMock.allGroupConfigurations[1];

const renderComponent = () => render(
<AppProvider store={store}>
<IntlProvider locale="en">
<GroupConfigurations courseId={courseId} />
</IntlProvider>
</AppProvider>,
);

describe('<GroupConfigurations />', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});

store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getContentStoreApiUrl(courseId))
.reply(200, groupConfigurationResponseMock);
await executeThunk(fetchGroupConfigurationsQuery(courseId), store.dispatch);
});

it('renders component correctly', async () => {
const { getByText, getAllByText, getByTestId } = renderComponent();

await waitFor(() => {
const mainContent = getByTestId('group-configurations-main-content-wrapper');
const groupConfigurationsElements = getAllByText(messages.headingTitle.defaultMessage);
const groupConfigurationsTitle = groupConfigurationsElements[0];

expect(groupConfigurationsTitle).toBeInTheDocument();
expect(
getByText(messages.headingSubtitle.defaultMessage),
).toBeInTheDocument();
expect(
within(mainContent).getByText(contentGroupsMessages.addNewGroup.defaultMessage),
).toBeInTheDocument();
expect(
within(mainContent).getByText(experimentMessages.addNewGroup.defaultMessage),
).toBeInTheDocument();
expect(
within(mainContent).getByText(experimentMessages.title.defaultMessage),
).toBeInTheDocument();
expect(getByText(contentGroups.name)).toBeInTheDocument();
expect(getByText(enrollmentTrackGroups.name)).toBeInTheDocument();
});
});

it('does not render an empty section for enrollment track groups if it is empty', () => {
const shouldNotShowEnrollmentTrackResponse = {
...groupConfigurationResponseMock,
shouldShowEnrollmentTrack: false,
};
axiosMock
.onGet(getContentStoreApiUrl(courseId))
.reply(200, shouldNotShowEnrollmentTrackResponse);

const { queryByTestId } = renderComponent();
expect(
queryByTestId('group-configurations-empty-placeholder'),
).not.toBeInTheDocument();
});

it('updates loading status if request fails', async () => {
axiosMock
.onGet(getContentStoreApiUrl(courseId))
.reply(404, groupConfigurationResponseMock);

renderComponent();

await executeThunk(fetchGroupConfigurationsQuery(courseId), store.dispatch);

expect(store.getState().groupConfigurations.loadingStatus).toBe(
RequestStatus.FAILED,
);
});
});
44 changes: 44 additions & 0 deletions src/group-configurations/__mocks__/contentGroupsMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module.exports = {
active: true,
description: 'The groups in this configuration can be mapped to cohorts in the Instructor Dashboard.',
groups: [
{
id: 593758473,
name: 'My Content Group 1',
usage: [],
version: 1,
},
{
id: 256741177,
name: 'My Content Group 2',
usage: [
{
label: 'Unit / Blank Problem',
url: '/container/block-v1:org+101+101+type@vertical+block@3d6d82348e2743b6ac36ac4af354de0e',
},
{
label: 'Unit / Drag and Drop',
url: '/container/block-v1:org+101+101+type@vertical+block@3d6d82348w2743b6ac36ac4af354de0e',
},
],
version: 1,
},
{
id: 646686987,
name: 'My Content Group 3',
usage: [
{
label: 'Unit / Drag and Drop',
url: '/container/block-v1:org+101+101+type@vertical+block@3d6d82348e2743b6ac36ac4af354de0e',
},
],
version: 1,
},
],
id: 1791848226,
name: 'Content Groups',
parameters: {},
readOnly: false,
scheme: 'cohort',
version: 3,
};
Loading

0 comments on commit 907ce50

Please sign in to comment.