-
Notifications
You must be signed in to change notification settings - Fork 6
Instructor tabs #32
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
base: main
Are you sure you want to change the base?
Instructor tabs #32
Conversation
|
Thanks for the pull request, @diana-villalvazo-wgu! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are 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. DetailsWhere 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. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #32 +/- ##
==========================================
+ Coverage 0.00% 54.96% +54.96%
==========================================
Files 5 39 +34
Lines 7 322 +315
Branches 0 78 +78
==========================================
+ Hits 0 177 +177
- Misses 7 145 +138 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Are we not making the tabs plugin-based? |
What do you mean by plugin-based? 🤔 |
|
Aren't we going to make the tab bar a plugin slot, and each tab ("Course Info", "Enrollments", "Course Team", etc.) a plugin? So that people can make custom plugins to add new tabs to the instructor dashboard without having to fork and modify this MFE? |
I tried this approach of having a slot per tab, but it seems I can't use Tab component through a slot config, i receive this error msg |
Could you share the code that led to that situation? My guess is something about how that was configured led to <Tabs>
<SomethingThatIsNotTab>
<Tab />
</SomethingThatIsNotTab>
</Tabs>I'm return (
<SlotContext.Provider value={{ id, children, ...props }}>
{layoutElement}
</SlotContext.Provider>
);So I think things to look into are:
I'm not sure how viable any of these options are, but if I were to do some digging those are the places I'd start looking. |
I tried with this code: i will look into the suggested options and will try to get a way to support this 🤔 |
80b8171 to
3cf3870
Compare
bradenmacdonald
left a comment
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.
Nice work finding a way to get Tabs working with slots! I think we definitely need to make that easier by improving Paragon's <Tab> component directly.
src/instructorTabs/app.tsx
Outdated
| slotId: `org.openedx.frontend.slot.instructor.tabs.v1`, | ||
| id: `org.openedx.frontend.widget.instructor.tab.${tab_id}`, | ||
| op: WidgetOperationTypes.APPEND, | ||
| element: <TabSlot tab_id={tab_id} title={title} url={url} />, |
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.
What prevents us from using element: <Tab eventKey={tab_id} title={title} /> directly? Why do we need to have a "fake" <TabSlot> element and then convert it to <Tab> later ?
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.
I guess nothing prevents it, we need to use TabSlot placeholder because if you use Tab directly the app breaks, due to the error explained in prev comments The Tab component is not meant to be rendered! It's an abstract component that is only valid as a direct Child of the Tabs Component.
1fed473 to
25163bb
Compare
25163bb to
a5cbad5
Compare
882acc7 to
4ac0580
Compare
brian-smith-tcril
left a comment
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.
I really like the solution you came up with for using a dummy component to support adding tabs to InstructorTabs!
I left a comment about the naming of the TabSlot component, I'd like to hear your thoughts on that one.
It's also not fully clear what parts of this PR are actually related to instructor tabs. For example, the added test in api.test.ts is just checking for a course_name, which feels completely unrelated to adding the tabs.
It'd be great to split this PR up a bit, I'd be happy to review/land a couple small "add some api tests" PRs so this can be rebased and the diff can be more focused on the changes specific for instructor tabs.
| // Since we are using a slot-based architecture and Paragon is passing Tabs/Tab through | ||
| // We can't have context provider between Tabs and Tab when rendering it should be direct parent/children relation | ||
|
|
||
| const TabSlot = (_props: TabProps) => { |
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.
The name TabSlot feels a bit confusing to me. To me it implies "a slot that a tab goes in," but the README example has an app.tsx where the element going into the InstructorTabsSlot is a TabSlot.
I don't know what a perfect name for this would be, the first thing that comes to mind would be InstructorTab.
To keep the intention of the component clear, I think it might make sense to combine this file and InstructorTabsSlot.tsx into something like
import { Slot } from '@openedx/frontend-base';
import InstructorTabs, { TabProps } from '../../instructorTabs/InstructorTabs';
export const InstructorTabsSlot = () => (
<Slot id="org.openedx.frontend.slot.instructor.tabs.v1">
<InstructorTabs />
</Slot>
);
// This component will be a placeholder/dummy component just to retrieve Tab props
// Since we are using a slot-based architecture and Paragon is passing Tabs/Tab through
// We can't have context provider between Tabs and Tab when rendering it should be direct parent/children relation
export const InstructorTab = (_props: TabProps) => {
return null;
}
export default InstructorTabsSlot;and then in the README example it could be
import { SlotOperation, WidgetOperationTypes } from '@openedx/frontend-base';
import { InstructorTab } from '../slots/instructorTabsSlot/InstructorTabSlot';
// Tab configuration data
const tabData = { tab_id: 'course_info', url: 'course_info', title: 'Course Info' };
// Create slot operations
export const tabSlots: SlotOperation[] = [{
slotId: `org.openedx.frontend.slot.instructor.tabs.v1`,
id: `org.openedx.frontend.widget.instructor.tab.${tab_id}`,
op: WidgetOperationTypes.APPEND,
element: <InstructorTab tab_id={tabData.tab_id} title={tabData.title} url={tabData.url} />,
}];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.
Thanks Brian, yes after the changes i forgot to update Read me file, but this sounds great, i updated it! 👍
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.
and regarding the api tests, i just added a couple of tests since i need course info api call to retrieve the tabs response :)
0f7d18e to
c4f6ea2
Compare
|
I'll let @brian-smith-tcril do the remaining review here, but let me know if there was any specific input you wanted from me. Thanks for this :) |
| const apiTabs: TabProps[] = courseInfo?.tabs ?? []; | ||
| const allTabs = [...apiTabs]; | ||
|
|
||
| widgetPropsArray.forEach(slotTab => { | ||
| if (!apiTabs.find(apiTab => apiTab.tab_id === slotTab.tab_id)) { | ||
| allTabs.push(slotTab); | ||
| } | ||
| }); |
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.
Couple of questions about the logic here.
My understanding of the current logic is that it
- Grabs tabs from the API, puts those first in the array
- Grabs tabs added to the
InstructorTabsSlot- If the id matches a tab the API is already providing, the slot tab is ignored
- If the id doesn't match a tab the API is already providing, the slot tab is added to the end of the array
So my open questions are:
- How do we want to handle ordering of tabs?
- Do we always want the API provided tabs to take priority over slot provided tabs?
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.
right now we are taking the ordering from backend response, based on figma, and as you may know, there are some cases that some tabs will not show up, etc, so we rely on the response since we don't want to cause some coupling in case we change tab name at some point, we discussed on backend PR
And for the second one, i was in doubt on which one should take priority, only reason i keep it this way is because if we override through the widget it will just render tab in different order and tab content will be same that we provide instead of what user may expect, so if its same tab name and they want change tab content, they should do it in routes instead of adding a new slot if that is the need 🤔
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.
I'm not sure I'm following. I'd like to think through a few examples to ensure we're covering use cases properly.
- A site operator wants to put a new custom tab first
- A site operator wants to put a custom tab somewhere in the middle of the existing tabs
- A site operator wants to hide one or more tabs coming from the API
- A site operator wants to rename a tab
- A site operator wants to reorder the tabs coming from the API
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.
For the cases 1, 2 and 5, are possible if i change this implementation and instead of giving priority to apiTabs we allow to delete and add it from slotTabs if they have same tab_id, for example in slot tabs they can give the same as api tabs but in a different order, and we preserve slot tabs order.
Same for case 4 if we preserve giving priority to slotTabs they can provide same tab_id but change title (that is the displayed name)
For case 3 with current implementation we can't suppress a tab, we should implement a new logic for that
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.
I was thinking doing something like this:
widgetPropsArray.forEach(slotTab => {
if (!apiTabs.find(apiTab => apiTab.tab_id === slotTab.tab_id)) {
allTabs.push(slotTab);
} else {
const indexToRemove = allTabs.findIndex(({ tab_id }) => tab_id === slotTab.tab_id);
if (indexToRemove !== -1) {
allTabs.splice(indexToRemove, 1);
}
allTabs.push(slotTab);
}
});
d0abfbf to
0008c6f
Compare
0008c6f to
84d6171
Compare
|
Bumping this, @brian-smith-tcril |
brian-smith-tcril
left a comment
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.
This isn't a complete review as I'm still thinking through how all of this works together. From what I can tell it's looking pretty great, and the documentation is very nice!
I left a couple minor comments for now, and I'll review more ASAP.
| const mockCourseData = { course_name: 'Test Course' }; | ||
| const mockCamelCaseData = { courseName: 'Test Course' }; | ||
| const mockCourseData = { course_name: 'Test Course', tabs: [{ tab_id: 'course_info', title: 'Course Information', url: 'https://test-lms.com/courses/test-course-123/info' }] }; | ||
| const mockCamelCasedCourseData = { courseName: 'Test Course', tabs: [{ tabId: 'course_info', title: 'Course Information', url: 'https://test-lms.com/courses/test-course-123/info' }] }; |
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.
I'm not sure what patterns we're using in other tests, but it'd be nice to reduce the duplication here a bit as the data we're mocking grows. Maybe something like
const mockCourseData = { course_name: 'Test Course', [...] }
const mockCamelCasedCourseData = { courseName: mockCourseData.course_name, [...] }| useQuery({ | ||
| queryKey: courseInfoQueryKeys.byCourse(courseId), | ||
| queryFn: () => getCourseInfo(courseId), | ||
| enabled: !!courseId, |
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.
Is checking courseId for truthiness here enough validation? My first impression is that this would lead to enabled being true when we have an invalid (but truthy) courseId.
| path: ':tabId', | ||
| element: <TabContent /> |
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.
I'm curious about the motivation behind this change. I don't immediately see a need for these pages to have loaders or actions (and therefore errorElements), but moving this logic to a component feels like "locking in" on that decision.
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.
i remember adding this so we can get tabId from useParams for InstructorTabs component, otherwise i didn't get that info, but if you know a better approach to do this glad to hear about it
brian-smith-tcril
left a comment
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.
After looking through this again I think I have a fuller sense of how things are working.
I'd love to see some clarifying inline comments in here. I left some comments with examples, but overall it'd be great to have the comments as a place to discuss what the code is doing and why, and the code as a place discuss how it is being done.
The other major open question I have is: how can a site operator add content (read: a new page) to a custom tab? It seems like that functionality isn't implemented yet, and I think the answer to that question will inform how we want to define the slots we're building here.
This is definitely a complex thing to build out. Thank you so much for taking it on and working through this review process with me!
| const apiTabs: TabProps[] = courseInfo?.tabs ?? []; | ||
| const allTabs = [...apiTabs]; | ||
|
|
||
| widgetPropsArray.forEach(slotTab => { |
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.
It'd be great to add a clarifying comment here, something like
| widgetPropsArray.forEach(slotTab => { | |
| // Tabs added via slot take priority over (read: replace) tabs from the API | |
| // All tabs added via slot are placed at the end of the tabs array | |
| widgetPropsArray.forEach(slotTab => { |
|
|
||
| export interface TabProps { | ||
| tabId: string, | ||
| url: string, |
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.
Adding a clarifying comment here would be great, something like:
| url: string, | |
| url: string, // This is not used by frontend-app-instruct, it is a holdover from how the legacy instructor dash used the API |
84d6171 to
7b256f4
Compare
Description
Components basic implementation for instructor tabs.
Demo
Screen.Recording.2025-10-09.at.2.30.20.p.m.mov
Support Information
Closes #23