-
Notifications
You must be signed in to change notification settings - Fork 75
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: [FC-0070] implement move xblock modal #1422
base: master
Are you sure you want to change the base?
feat: [FC-0070] implement move xblock modal #1422
Conversation
Thanks for the pull request, @ihor-romaniuk! What's next?Please work through the following steps to get your changes ready for engineering review: 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. 🔘 Let us know that your PR is ready for review:Who will review my changes?This repository is currently maintained by Where can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:
When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
c917065
to
1a9145e
Compare
1a9145e
to
de9f97c
Compare
0d617b7
to
062e439
Compare
Sandbox deployment successful 🚀 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[inform]: Regarding the failing tests. Apparently after merging one of the last PRs we had problems with tests for the library functionality. I have already fixed this in a new PR
#1493
src/course-unit/CourseUnit.test.jsx
Outdated
)).toBeInTheDocument(); | ||
|
||
const currentSection = courseOutlineInfoMock.child_info.children[1]; | ||
const currentSectionItemText = getByRole('button', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[optional]: We are looking for a button here, aren't we? Let's call this variable currentSectionItemBtn
const currentSectionItemText = getByRole('button', { | |
const currentSectionItemBtn = getByRole('button', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
src/course-unit/CourseUnit.test.jsx
Outdated
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`, | ||
}); | ||
expect(currentSectionItemText).toBeInTheDocument(); | ||
fireEvent.click(currentSectionItemText); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[optional]: I see that in this file there are tests using fireEvent
, but let's use a more correct approach with userEvent
https://blog.mimacom.com/react-testing-library-fireevent-vs-userevent/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
src/course-unit/CourseUnit.test.jsx
Outdated
fireEvent.click(currentSectionItemText); | ||
|
||
const currentSubsection = currentSection.child_info.children[0]; | ||
const currentSubsectionText = getByRole('button', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const currentSubsectionText = getByRole('button', { | |
const currentSectionItemBtn = getByRole('button', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
src/course-unit/move-modal/index.tsx
Outdated
<span | ||
className="category-text" | ||
aria-hidden="true" | ||
data-testId="move-xblock-modal-category" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit]: data-testid
instead of data-testId
data-testId="move-xblock-modal-category" | |
data-testid="move-xblock-modal-category" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
src/course-unit/CourseUnit.test.jsx
Outdated
await waitFor(() => { | ||
expect(getByText(unitDisplayName)).toBeInTheDocument(); | ||
}); | ||
|
||
axiosMock | ||
.onGet(getOutlineInfo(courseId)) | ||
.reply(200, courseOutlineInfoMock); | ||
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch); | ||
|
||
window.dispatchEvent(messageEvent); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[optional]: Since we have an asynchronous state update in this section of code, we will most likely get a warning in the test related to wrapping such sections of code in act()
. Let's wrap the blocks that asynchronously update states in this test and the following ones in this PR.
Something like this:
await waitFor(() => { | |
expect(getByText(unitDisplayName)).toBeInTheDocument(); | |
}); | |
axiosMock | |
.onGet(getOutlineInfo(courseId)) | |
.reply(200, courseOutlineInfoMock); | |
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch); | |
window.dispatchEvent(messageEvent); | |
await act(async () => { | |
await waitFor(() => { | |
expect(getByText(unitDisplayName)).toBeInTheDocument(); | |
}); | |
axiosMock.onGet(getOutlineInfo(courseId)).reply(200, courseOutlineInfoMock); | |
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch); | |
window.dispatchEvent(messageEvent); | |
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(sections[1].display_name, 'i') }))); | ||
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(subsections[1].display_name, 'i') }))); | ||
await waitFor(() => userEvent.click(within(breadcrumbs).getByText(sections[1].display_name))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(sections[1].display_name, 'i') }))); | |
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(subsections[1].display_name, 'i') }))); | |
await waitFor(() => userEvent.click(within(breadcrumbs).getByText(sections[1].display_name))); | |
await waitFor(() => { | |
userEvent.click(getByRole('button', { name: new RegExp(sections[1].display_name, 'i') })); | |
userEvent.click(getByRole('button', { name: new RegExp(subsections[1].display_name, 'i') })); | |
userEvent.click(within(breadcrumbs).getByText(sections[1].display_name)); | |
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
it('correctly navigates through the structure list', async () => { | ||
const { getByText, getByRole, getByTestId } = renderComponent(); | ||
const breadcrumbs: HTMLElement = getByTestId('move-xblock-modal-breadcrumbs'); | ||
const categoryIndicator: HTMLElement = getByTestId('move-xblock-modal-category'); | ||
|
||
expect(within(breadcrumbs).getByText(messages.moveModalBreadcrumbsBaseCategory.defaultMessage)).toBeInTheDocument(); | ||
expect( | ||
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsSections.defaultMessage), | ||
).toBeInTheDocument(); | ||
sections.map((section) => ( | ||
expect(getByText(section.display_name)).toBeInTheDocument() | ||
)); | ||
|
||
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(sections[1].display_name, 'i') }))); | ||
await waitFor(() => { | ||
expect( | ||
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsSubsections.defaultMessage), | ||
).toBeInTheDocument(); | ||
expect(within(breadcrumbs).getByText(sections[1].display_name)).toBeInTheDocument(); | ||
subsections.map((subsection) => ( | ||
expect(getByRole('button', { name: new RegExp(subsection.display_name, 'i') })).toBeInTheDocument() | ||
)); | ||
}); | ||
|
||
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(subsections[1].display_name, 'i') }))); | ||
await waitFor(() => { | ||
expect( | ||
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsUnits.defaultMessage), | ||
).toBeInTheDocument(); | ||
expect(within(breadcrumbs).getByText(subsections[1].display_name)).toBeInTheDocument(); | ||
units.map((unit) => ( | ||
expect(getByRole('button', { name: new RegExp(unit.display_name, 'i') })).toBeInTheDocument() | ||
)); | ||
}); | ||
|
||
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(units[0].display_name, 'i') }))); | ||
await waitFor(() => { | ||
expect( | ||
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsComponents.defaultMessage), | ||
).toBeInTheDocument(); | ||
expect(within(breadcrumbs).getByText(units[0].display_name)).toBeInTheDocument(); | ||
components.forEach((component) => { | ||
if (component.display_name) { | ||
expect(getByText(component.display_name)).toBeInTheDocument(); | ||
} | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[optional]: I tried locally to remove some waitFor
and unnecessary types and it works. Please check if you need to use waitFor so often in other tests in this file.
it('correctly navigates through the structure list', async () => { | |
const { getByText, getByRole, getByTestId } = renderComponent(); | |
const breadcrumbs: HTMLElement = getByTestId('move-xblock-modal-breadcrumbs'); | |
const categoryIndicator: HTMLElement = getByTestId('move-xblock-modal-category'); | |
expect(within(breadcrumbs).getByText(messages.moveModalBreadcrumbsBaseCategory.defaultMessage)).toBeInTheDocument(); | |
expect( | |
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsSections.defaultMessage), | |
).toBeInTheDocument(); | |
sections.map((section) => ( | |
expect(getByText(section.display_name)).toBeInTheDocument() | |
)); | |
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(sections[1].display_name, 'i') }))); | |
await waitFor(() => { | |
expect( | |
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsSubsections.defaultMessage), | |
).toBeInTheDocument(); | |
expect(within(breadcrumbs).getByText(sections[1].display_name)).toBeInTheDocument(); | |
subsections.map((subsection) => ( | |
expect(getByRole('button', { name: new RegExp(subsection.display_name, 'i') })).toBeInTheDocument() | |
)); | |
}); | |
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(subsections[1].display_name, 'i') }))); | |
await waitFor(() => { | |
expect( | |
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsUnits.defaultMessage), | |
).toBeInTheDocument(); | |
expect(within(breadcrumbs).getByText(subsections[1].display_name)).toBeInTheDocument(); | |
units.map((unit) => ( | |
expect(getByRole('button', { name: new RegExp(unit.display_name, 'i') })).toBeInTheDocument() | |
)); | |
}); | |
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(units[0].display_name, 'i') }))); | |
await waitFor(() => { | |
expect( | |
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsComponents.defaultMessage), | |
).toBeInTheDocument(); | |
expect(within(breadcrumbs).getByText(units[0].display_name)).toBeInTheDocument(); | |
components.forEach((component) => { | |
if (component.display_name) { | |
expect(getByText(component.display_name)).toBeInTheDocument(); | |
} | |
}); | |
}); | |
}); | |
it('correctly navigates through the structure list', async () => { | |
const { getByText, getByRole, getByTestId } = renderComponent(); | |
const breadcrumbs = getByTestId('move-xblock-modal-breadcrumbs'); | |
const categoryIndicator = getByTestId('move-xblock-modal-category'); | |
expect(within(breadcrumbs).getByText(messages.moveModalBreadcrumbsBaseCategory.defaultMessage)).toBeInTheDocument(); | |
expect( | |
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsSections.defaultMessage) | |
).toBeInTheDocument(); | |
sections.forEach((section) => { | |
expect(getByText(section.display_name)).toBeInTheDocument(); | |
}); | |
userEvent.click(getByRole('button', { name: new RegExp(sections[1].display_name, 'i') })); | |
await waitFor(() => { | |
expect( | |
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsSubsections.defaultMessage) | |
).toBeInTheDocument(); | |
expect(within(breadcrumbs).getByText(sections[1].display_name)).toBeInTheDocument(); | |
subsections.forEach((subsection) => { | |
expect(getByRole('button', { name: new RegExp(subsection.display_name, 'i') })).toBeInTheDocument(); | |
}); | |
}); | |
userEvent.click(getByRole('button', { name: new RegExp(subsections[1].display_name, 'i') })); | |
await waitFor(() => { | |
expect( | |
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsUnits.defaultMessage) | |
).toBeInTheDocument(); | |
expect(within(breadcrumbs).getByText(subsections[1].display_name)).toBeInTheDocument(); | |
units.forEach((unit) => { | |
expect(getByRole('button', { name: new RegExp(unit.display_name, 'i') })).toBeInTheDocument(); | |
}); | |
}); | |
userEvent.click(getByRole('button', { name: new RegExp(units[0].display_name, 'i') })); | |
await waitFor(() => { | |
expect( | |
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsComponents.defaultMessage) | |
).toBeInTheDocument(); | |
expect(within(breadcrumbs).getByText(units[0].display_name)).toBeInTheDocument(); | |
components.forEach((component) => { | |
if (component.display_name) { | |
expect(getByText(component.display_name)).toBeInTheDocument(); | |
} | |
}); | |
}); | |
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(sections[1].display_name, 'i') }))); | ||
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(subsections[1].display_name, 'i') }))); | ||
await waitFor(() => userEvent.click(getByRole('button', { name: new RegExp(units[7].display_name, 'i') }))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[optional]: It seems to me that a wrapper of one waitFor
will work the same as 3.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
import MoveModal from './index'; | ||
import messages from './messages'; | ||
|
||
interface CourseOutlineChildInfo { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[question]: Just a clarification, can we use these TS types inside the component or maybe we are already using them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reused from the component
src/course-unit/move-modal/utils.ts
Outdated
export const getXBlockType = (category: string): string => { | ||
switch (true) { | ||
case category === CATEGORIES_KEYS.chapter: | ||
return CATEGORIES_KEYS.section; | ||
case category === CATEGORIES_KEYS.sequential: | ||
return CATEGORIES_KEYS.subsection; | ||
case category === CATEGORIES_KEYS.vertical: | ||
return CATEGORIES_KEYS.unit; | ||
default: | ||
return category; | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[optional]: I consider switch(true)
an anti-pattern if you also suggest using the concatenated approach :)
export const getXBlockType = (category: string): string => { | |
switch (true) { | |
case category === CATEGORIES_KEYS.chapter: | |
return CATEGORIES_KEYS.section; | |
case category === CATEGORIES_KEYS.sequential: | |
return CATEGORIES_KEYS.subsection; | |
case category === CATEGORIES_KEYS.vertical: | |
return CATEGORIES_KEYS.unit; | |
default: | |
return category; | |
} | |
}; | |
export const getXBlockType = (category: string): string => { | |
const categoryMap: { [key: string]: string } = { | |
[CATEGORIES_KEYS.chapter]: CATEGORIES_KEYS.section, | |
[CATEGORIES_KEYS.sequential]: CATEGORIES_KEYS.subsection, | |
[CATEGORIES_KEYS.vertical]: CATEGORIES_KEYS.unit, | |
}; | |
return categoryMap[category] || category; | |
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
64a02a0
to
54ddf6b
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1422 +/- ##
==========================================
+ Coverage 93.01% 93.02% +0.01%
==========================================
Files 1048 1059 +11
Lines 20480 20789 +309
Branches 4402 4402
==========================================
+ Hits 19049 19339 +290
- Misses 1361 1382 +21
+ Partials 70 68 -2 ☔ View full report in Codecov by Sentry. |
Sandbox deployment successful 🚀 |
Sandbox deployment successful 🚀 |
src/course-unit/data/api.js
Outdated
/** | ||
* Get an object containing course outline data. | ||
* @param {string} courseId - The identifier of the course. | ||
* @returns {Promise<Object>} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible for you to specify the actual shape of the data returned, instead of just Object
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
src/course-unit/data/api.js
Outdated
* Move a unit item to new unit. | ||
* @param {string} sourceLocator - The ID of the item to be moved. | ||
* @param {string} targetParentLocator - The ID of the XBlock associated with the item. | ||
* @returns {Promise<Object>} - A promise that resolves to the response data from the server. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible for you to specify the actual shape of the data returned, instead of just Object
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
EmptyMessage.propTypes = { | ||
category: PropTypes.string.isRequired, | ||
categoryText: PropTypes.string.isRequired, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can omit propTypes
from .tsx
files if you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
54ddf6b
to
7429b68
Compare
Sandbox deployment successful 🚀 |
🚨 Dependencies:
Settings
Description
This PR introduces the ability to move an xBlock from one unit to another within a course using a modal window. The feature enables users to select a target unit for the xBlock and confirm the move.
move-xblock.mov
Related Pull Requests
Testing instructions