-
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
Changes from all commits
bed02db
6521fab
02b87df
a2751cb
69e9481
1168171
6e27721
5d2b035
7edb159
97e6f59
7b256f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| const CertificatesPage = () => { | ||
| return ( | ||
| <div> | ||
| <h3>Certificates</h3> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default CertificatesPage; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| const CourseTeamPage = () => { | ||
| return ( | ||
| <div> | ||
| <h3>Course Team</h3> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default CourseTeamPage; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,5 +6,6 @@ export const useCourseInfo = (courseId: string) => ( | |
| useQuery({ | ||
| queryKey: courseInfoQueryKeys.byCourse(courseId), | ||
| queryFn: () => getCourseInfo(courseId), | ||
| enabled: !!courseId, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is checking |
||
| }) | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| const DataDownloadsPage = () => { | ||
| return ( | ||
| <div> | ||
| <h3>Data Downloads</h3> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default DataDownloadsPage; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| const DateExtensionsPage = () => { | ||
| return ( | ||
| <div> | ||
| <h3>Date Extensions</h3> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default DateExtensionsPage; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| const EnrollmentsPage = () => { | ||
| return ( | ||
| <div> | ||
| <h3>Enrollments</h3> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default EnrollmentsPage; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| const GradingPage = () => { | ||
| return ( | ||
| <div> | ||
| <h3>Grading</h3> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default GradingPage; |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||||||
| import { useContext } from 'react'; | ||||||||||
| import { useNavigate, useParams } from 'react-router-dom'; | ||||||||||
| import { Tab, Tabs } from '@openedx/paragon'; | ||||||||||
| import { SlotContext, useWidgetsForId } from '@openedx/frontend-base'; | ||||||||||
| import { useCourseInfo } from '../data/apiHook'; | ||||||||||
|
|
||||||||||
| export interface TabProps { | ||||||||||
| tabId: string, | ||||||||||
| url: string, | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding a clarifying comment here would be great, something like:
Suggested change
|
||||||||||
| title: string, | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const extractWidgetProps = (widget: React.ReactNode): TabProps | null => { | ||||||||||
| if (widget && typeof widget === 'object' && 'props' in widget) { | ||||||||||
| const props = widget.props.children.props as TabProps; | ||||||||||
| if (props?.tabId && props?.url && props?.title) { | ||||||||||
| return props; | ||||||||||
| } | ||||||||||
| } | ||||||||||
| return null; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| const useWidgetProps = (slotId: string): TabProps[] => { | ||||||||||
| const widgets = useWidgetsForId(slotId); | ||||||||||
| return widgets.map(extractWidgetProps).filter((props): props is TabProps => props !== null); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| const InstructorTabs = () => { | ||||||||||
| const navigate = useNavigate(); | ||||||||||
| const { courseId, tabId } = useParams<{ courseId: string, tabId?: string }>(); | ||||||||||
| const { id: slotId } = useContext(SlotContext); | ||||||||||
| const { data: courseInfo, isLoading } = useCourseInfo(courseId ?? ''); | ||||||||||
| const widgetPropsArray = useWidgetProps(slotId); | ||||||||||
|
|
||||||||||
| const apiTabs: TabProps[] = courseInfo?.tabs ?? []; | ||||||||||
| const allTabs = [...apiTabs]; | ||||||||||
|
|
||||||||||
| widgetPropsArray.forEach(slotTab => { | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd be great to add a clarifying comment here, something like
Suggested change
|
||||||||||
| if (!apiTabs.find(apiTab => apiTab.tabId === slotTab.tabId)) { | ||||||||||
| allTabs.push(slotTab); | ||||||||||
| } else { | ||||||||||
| const indexToRemove = allTabs.findIndex(({ tabId }) => tabId === slotTab.tabId); | ||||||||||
| if (indexToRemove !== -1) { | ||||||||||
| allTabs.splice(indexToRemove, 1); | ||||||||||
| } | ||||||||||
| allTabs.push(slotTab); | ||||||||||
| } | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| const activeKey = tabId ?? 'course_info'; | ||||||||||
| const handleSelect = (eventKey: string | null) => { | ||||||||||
| if (eventKey && courseId) { | ||||||||||
| const selectedTab = allTabs.find(({ tabId }) => tabId === eventKey); | ||||||||||
| if (selectedTab) { | ||||||||||
| navigate(`/${courseId}/${eventKey}`); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| if (isLoading) { | ||||||||||
| return <div>Loading tabs...</div>; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (allTabs.length === 0) return null; | ||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <Tabs id="instructor-tabs" activeKey={activeKey} onSelect={handleSelect}> | ||||||||||
| {allTabs.map(({ tabId, title }) => ( | ||||||||||
| <Tab key={tabId} eventKey={tabId} title={title} /> | ||||||||||
| ))} | ||||||||||
| </Tabs> | ||||||||||
| ); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| export default InstructorTabs; | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| const OpenResponsesPage = () => { | ||
| return ( | ||
| <div> | ||
| <h3>Open Responses</h3> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default OpenResponsesPage; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { useIntl } from '@openedx/frontend-base'; | ||
| import messages from './messages'; | ||
| import InstructorTabsSlot from '../slots/instructorTabsSlot/InstructorTabsSlot'; | ||
|
|
||
| const PageWrapper = ({ children }: { children: React.ReactNode }) => { | ||
| const { formatMessage } = useIntl(); | ||
| return ( | ||
| <div className="container-xl"> | ||
| <h2>{formatMessage(messages.pageTitle)}</h2> | ||
| <InstructorTabsSlot /> | ||
| {children} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default PageWrapper; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { defineMessages } from '@openedx/frontend-base'; | ||
|
|
||
| const messages = defineMessages({ | ||
| pageTitle: { | ||
| id: 'pageWrapper.pageTitle', | ||
| defaultMessage: 'Instructor Dashboard', | ||
| description: 'Title for the instructor dashboard page', | ||
| }, | ||
| }); | ||
|
|
||
| export default messages; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,44 @@ | ||
| import { useParams, Navigate } from 'react-router-dom'; | ||
| import CertificatesPage from './certificates/CertificatesPage'; | ||
| import CohortsPage from './cohorts/CohortsPage'; | ||
| import CourseInfoPage from './courseInfo/CourseInfoPage'; | ||
| import CourseTeamPage from './courseTeam/CourseTeamPage'; | ||
| import DataDownloadsPage from './dataDownloads/DataDownloadsPage'; | ||
| import DateExtensionsPage from './dateExtensions/DateExtensionsPage'; | ||
| import EnrollmentsPage from './enrollments/EnrollmentsPage'; | ||
| import GradingPage from './grading/GradingPage'; | ||
| import Main from './Main'; | ||
| import OpenResponsesPage from './openResponses/OpenResponsesPage'; | ||
| import SpecialExamsPage from './specialExams/SpecialExamsPage'; | ||
|
|
||
| const TabContent = () => { | ||
| const { tabId } = useParams<{ tabId: string }>(); | ||
|
|
||
| switch (tabId) { | ||
| case 'course_info': | ||
| return <CourseInfoPage />; | ||
| case 'enrollments': | ||
| return <EnrollmentsPage />; | ||
| case 'course_team': | ||
| return <CourseTeamPage />; | ||
| case 'cohorts': | ||
| return <CohortsPage />; | ||
| case 'date_extensions': | ||
| return <DateExtensionsPage />; | ||
| case 'grading': | ||
| return <GradingPage />; | ||
| case 'data_downloads': | ||
| return <DataDownloadsPage />; | ||
| case 'special_exams': | ||
| return <SpecialExamsPage />; | ||
| case 'certificates': | ||
| return <CertificatesPage />; | ||
| case 'open_responses': | ||
| return <OpenResponsesPage />; | ||
| default: | ||
| return <Navigate to="course_info" replace />; | ||
| } | ||
| }; | ||
|
|
||
| const routes = [ | ||
| { | ||
|
|
@@ -12,41 +50,13 @@ const routes = [ | |
| Component: Main, | ||
| children: [ | ||
| { | ||
| path: 'course_info', | ||
| element: <CourseInfoPage /> | ||
| path: ':tabId', | ||
| element: <TabContent /> | ||
|
Comment on lines
+53
to
+54
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i remember adding this so we can get |
||
| }, | ||
| // { | ||
| // path: 'membership', | ||
| // element: <MembershipPage /> | ||
| // }, | ||
| { | ||
| path: 'cohorts', | ||
| element: <CohortsPage /> | ||
| }, | ||
| // { | ||
| // path: 'extensions', | ||
| // element: <ExtensionsPage /> | ||
| // }, | ||
| // { | ||
| // path: 'student_admin', | ||
| // element: <StudentAdminPage /> | ||
| // }, | ||
| // { | ||
| // path: 'data_download', | ||
| // element: <DataDownloadPage /> | ||
| // }, | ||
| // { | ||
| // path: 'special_exams', | ||
| // element: <SpecialExamsPage /> | ||
| // }, | ||
| // { | ||
| // path: 'certificates', | ||
| // element: <CertificatesPage /> | ||
| // }, | ||
| // { | ||
| // path: 'open_responses', | ||
| // element: <OpenResponsesPage /> | ||
| // } | ||
| path: '', | ||
| element: <Navigate to="course_info" replace /> | ||
| } | ||
| ] | ||
| } | ||
| ]; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { SlotOperation } from '@openedx/frontend-base'; | ||
|
|
||
| const slots: SlotOperation[] = []; | ||
|
|
||
| export default slots; |
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