-
Notifications
You must be signed in to change notification settings - Fork 81
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [FC-0044] group configurations MFE page (#929)
* 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
1 parent
7f668a6
commit 907ce50
Showing
62 changed files
with
4,818 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
Oops, something went wrong.