From c9693eb09d95c3f9c8db1283b9a72928dc2d83e9 Mon Sep 17 00:00:00 2001 From: "kshitij.sobti" Date: Sun, 26 Apr 2026 00:41:04 +0530 Subject: [PATCH 1/2] feat: Introduce slot for customizing course tab navigation Adds a new plugin slot (`CourseTabsNavigationSlot`) to enable customization, modification, or hiding of the course tab navigation. Updated relevant components to integrate with the new slot. --- src/course-tabs/CourseTabsNavigation.tsx | 15 +++--- src/course-tabs/{index.js => index.ts} | 2 +- .../CourseTabsNavigationSlot/README.md | 38 ++++++++++++++ .../course-tabs-navigation-shadow.png | Bin 0 -> 5140 bytes .../CourseTabsNavigationSlot/index.tsx | 8 +++ .../{LoadedTabPage.jsx => LoadedTabPage.tsx} | 48 ++++++++---------- 6 files changed, 74 insertions(+), 37 deletions(-) rename src/course-tabs/{index.js => index.ts} (50%) create mode 100644 src/plugin-slots/CourseTabsNavigationSlot/README.md create mode 100644 src/plugin-slots/CourseTabsNavigationSlot/course-tabs-navigation-shadow.png create mode 100644 src/plugin-slots/CourseTabsNavigationSlot/index.tsx rename src/tab-page/{LoadedTabPage.jsx => LoadedTabPage.tsx} (73%) diff --git a/src/course-tabs/CourseTabsNavigation.tsx b/src/course-tabs/CourseTabsNavigation.tsx index 87a1b92c4a..d9d315c1dd 100644 --- a/src/course-tabs/CourseTabsNavigation.tsx +++ b/src/course-tabs/CourseTabsNavigation.tsx @@ -1,16 +1,14 @@ import React from 'react'; -import classNames from 'classnames'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { CourseTabLinksSlot } from '../plugin-slots/CourseTabLinksSlot'; -import { CoursewareSearch, CoursewareSearchToggle } from '../course-home/courseware-search'; -import { useCoursewareSearchState } from '../course-home/courseware-search/hooks'; -import Tabs from '../generic/tabs/Tabs'; +import { CoursewareSearch, CoursewareSearchToggle } from '@src/course-home/courseware-search'; +import { useCoursewareSearchState } from '@src/course-home/courseware-search/hooks'; +import { CourseTabLinksSlot } from '@src/plugin-slots/CourseTabLinksSlot'; +import Tabs from '@src/generic/tabs/Tabs'; import messages from './messages'; -interface CourseTabsNavigationProps { +export interface CourseTabsNavigationProps { activeTabSlug?: string; - className?: string | null; tabs: Array<{ title: string; slug: string; @@ -20,14 +18,13 @@ interface CourseTabsNavigationProps { const CourseTabsNavigation = ({ activeTabSlug = undefined, - className = null, tabs, }:CourseTabsNavigationProps) => { const intl = useIntl(); const { show } = useCoursewareSearchState(); return ( -
+
diff --git a/src/course-tabs/index.js b/src/course-tabs/index.ts similarity index 50% rename from src/course-tabs/index.js rename to src/course-tabs/index.ts index e2236ee726..801de27856 100644 --- a/src/course-tabs/index.js +++ b/src/course-tabs/index.ts @@ -1,2 +1,2 @@ -/* eslint-disable import/prefer-default-export */ export { default as CourseTabsNavigation } from './CourseTabsNavigation'; +export type { CourseTabsNavigationProps } from './CourseTabsNavigation'; diff --git a/src/plugin-slots/CourseTabsNavigationSlot/README.md b/src/plugin-slots/CourseTabsNavigationSlot/README.md new file mode 100644 index 0000000000..084bf07b02 --- /dev/null +++ b/src/plugin-slots/CourseTabsNavigationSlot/README.md @@ -0,0 +1,38 @@ +# Course Tab Navigation Slot + +### Slot ID: `org.openedx.frontend.learning.course_tabs_navigation.v1` + +### Props: +NONE + +## Description + +This slot is used to replace/modify/hide the entire course tab navigation. + +## Example + +### Added a drop shadow to Course Tabs bar +![Added a drop shadow to Course Tabs bar](./course-tabs-navigation-shadow.png) + +The following `env.config.jsx` will add a new course tab call "Custom Tab". + +```js +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; + +const config = { + pluginSlots: { + "org.openedx.frontend.learning.course_tab_navigation.v1": { + keepDefault: true, + plugins: [ + { + op: PLUGIN_OPERATIONS.Wrap, + widgetId: 'default_contents', + wrapper: ({component}) => (
{component}
) + }, + ], + }, + }, +} + +export default config; +``` diff --git a/src/plugin-slots/CourseTabsNavigationSlot/course-tabs-navigation-shadow.png b/src/plugin-slots/CourseTabsNavigationSlot/course-tabs-navigation-shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..eb3c6617be2f09ad7213766378dd63cd8a6d3988 GIT binary patch literal 5140 zcmcgvc|26__a9p+d$Nm4)|4nk5@lbqGZI6IvhR#7L-w^S5!qrSYxZp{jeW~bWE-+? zGZ@Tbe6Nqs>-GEn|686vp8MQ;&OOh$&v~EsxhFzLONH(t=S2_*M5m^zs0Uo2z&ekb z0+_cr4{AXm@@XAS1ErIb6B3DZe0)qG5RQ(H_V@R& z`uh6%`}^D5+dDfuH#axa)6-pDU2SY^0s;a&JUkj28eCjl^7He3e0=Qe>|Vcq{r>&? zxVX6Q-@j{WYC<58=;-L;;^MNhGDk;8Q&ZD7Z{C2M(R5zu-J_H7)#IdAoLMfmU;uwa zI1volMa~ep1279aB;l~#WekZ2hH055*8e8dj1#KI2|YbMzkdCyt*uQ!;_B<`ot&J$ zeft(08=IGxmz$ewYim0+H1zZ5PZSC@H8nLcF)=naHak1JxVZS|&!5T3$>88%dwctk zkdUyju&Ai0n3$N4A3r80CMG2%rKP2P`SK+-HFbV|etLR(e0+Rxa4N(hK8!Ds(=6f-PqVzTU$e; z(K|aktE;OE3k##8qXPp2!^6WlIXSOhz0%Us`tad{v$JzVM1*R}zI@`oT*6*ZP*6!p zNn~VXXlN(`fpBwkgTY`H78XrSO}@UqMn*<2Uc5+1NN8zk+1}pX+uOrnFb4+*hlht? zF!=fN=gG;**4EZ3DJd%}D?fhxfWzURKY#x8>65Xsv6`BizP`SJfq|!|r%>oFttWc# z=wx#JIIRMD*bay%+jc#IAVM9s?A;xlihxBZf8{S>+!T4&v{{?7e&UPFGjg!NH-hu&}$k zyP%-J+uOUjxj8T}Ff%i=w6yen-BED;QAI^Xd3m{)mseF)m9nz3ySsZ)Q4tb}Y;0`I z%F3#%tE;K0>FVn8_xFGH?3t31l8TDT+}xawj!s5KhPJl0y1KfFiHV<|pOuvr9*-vy zi8ve%i^XPVXTN>>c71(4f~<%a1Y+D%Q+#3oo!b0UqBZFouIX(n9h;>XvSoi z7;+FubtWwCV|gJMSYBP34TwxMBX9vy>BA+r+)aE8ul|JWdur-OGqXin=W8EydGH1_I5mozyHcCwz>r z!-Xsn;$9g83enNA&$JEoC1fgUFLMC9pAMnd7XZhEY@5n~Bxj^(e_AnVe^~+$$mdMj zE0+y!&>@j8)V?6K*aXx3`VK23_KS3j0^YI=E%A?@wd~L}tG`dGOnuF@+8VyiObJhb ziar&>^7USsUU^U@>Z2gs@SrG@H=x$1T*BlcTiEghkKg?i^sr6%K>>|%!UMh_H~U@9 zCrU@eXpLJvDT78_akp;wtpzH@KfQzAMlrrf)SIkqs$7@XhYy>`JvL)0sl_Pil@!(E zL!DnQhL%|5rBf7r$T=xY-9s4=hFTv08BuCiV~?n?yxL?*e)l3g)@_?I*;hII#qCZCX3 zY&@3X5G$5V;VGptCT&hrVYWM)6HGEx9S&Mvq)`~#XZ;iB=t-&v_lioZ%jAU{OFyvw z*|$VhLB(}$uQf>7ICYOz(SwO5cTiaJANpo683Xvf7Ke>sbx+kdF7UH(6dJ!tj?Ty} zMb;wcx^*vIEAA}rCy@_-1(SdFWEsp$OcGS3xrGgzn57@4x26&g5<^VGday{lQC#<0 zu9CWPSn`SYNe2Sm<5Q7P`F`>ym)mN0Fto>;I6vYdFO>2gU>~6ye}_Mjr77Ya&y<@x5`rQ z=&*HF3TCP3Sy4)v%~2?Z?xPAz>B&nG>-Ijb?BTb<$+d5WGx?zmFCex3u;#SI?+#rLC6tLg_BX6+uZzlo0+!4u7O|Pg}U~>*)E95(7#ziEHHf zQif3Y^9L{`HLPRAQ(XaOZ&Yph1H<_cFmd%GU&0q*c2fb5q@Qc<9>F5;*$LP~8;-$G z{2m?(4p&iz^ze8{Algc8heBTk(NVZ_!+ko@b5wy4d{r480yg5d_CYPl$?@jTiY|3n zI2%B58HY}NG^O40Lyk5Ji}LRT5CuG&W5KTTA6t;U0fBL36q9;jTGltn!p1w_mvcZH z)#~knS+X20uPFYd6MdcQna|s5daHOZ?(jge`&}Em{OXoV%;&Ce1=6{i2|h$+=;!f2 zATy1xhf&BRma{<3V!yY@<@US${#}?T41p%yPR`-HuAn|iS@glWl^ecq9^US##@{02 z$kzU5HtHs{9!1|S;OINLFB&3CZu8Fr zpt#IK2jGi*!sW6O8Tg#VnIVV5R%~b&i&N-L=j91_5d~J=$!U+q>85B}?yA>O7G1O` zBReCH<+ULc7Gw&Z`X#jF#ZVfh?)&oZgcEGZb?iyEY%XnCq6jxzf*u<>B$PQ%LiPu!vdLYAWlR@lcj}$`}2G(F7r=OG3R1KS;lrET0$~uigh9E2* z*fgD+4F%&)4uk{9l0QS1pG1A!E-GBYdIU3q!urWf9}%xuU1l`HAU6Du{_^N`xY<{1 z6L~jXRfUSrN9KCVC(9v*Im)@D+y@P*+~9|S=&_qO%a_(=LHF+aJ3sn?E;whBC{fv* zcP-ZPI2>&YD}P^t)#2&_PWqwvf(pWKMf=?PU(8v0OQ5Qz2=9;tSejQTmUkj-=d0H!Ay3mbUj?BM7!?6rLv&$(R=D^m+89 zaROinc0Y%>d)E8T$`d=p6ae>LFVe9*Mmr=qfaj!R7?s2znq()9J~j@SSfvqt{A5Zy3=E2* z`*2oI_|fKvjF8xcyh%_PfuA2e$g0@$)*af9Mi%P&Y;?BTiFwtRB&OHgU!V@)pXlek zE_E)9{6gv8Am7>(iv#$yq@6Vmc{5EWB#8Yd8^laL;WVNFj}d-*M-P@mCLm&nCaxSb zr+;9BM)tz^dEbdgxDgb~f8~v$(BO~1Mf@YjbGGcm+F{9dZrw`x$?mCcyEOL^J=rd8 zHJlI}#!1qYidaJooh^;^MP+^QCoq5{pJ+B*f|}`Z^r_d6;E@{QQUbhA!64*@@cd&j zAVMdbHJtbrj8h-MsC5V7B-zLNj~k`(ed)Uqne?R~R<0YTRh`{(oau_5GLM1ApLulG zg9+%xgBe+bYigR?->%z1X2aYmcdAj6EGa^r!FT$SZc~H+3GwL;@#?^`VWAOL#L5n@ zG55U6w(AP%;hk$Rn>XY);TKx?XCx7cpy)p1bdPF1y zMJI+J4iN4yH`Xa%CJBR?(JjUyKwUGWsln19ZvtrPK0^y1IuhYnYp%zHxmK5<^`M{i zNI7C6J7=R+ob9WsEzToGtak9W7xyJ30| zs-j+3-VNfT7tC5@mH0EJl$0Nm8E#W(@wWsJOf&ke<#4z<8MlT8STXQlR!rY>cF}d* z2Ae-Ubd)Z5xwv9?J2Qs4UtfJ(*_a5~hF>S5;DYmQ0W;HtS*4VmM1S3{DsJHA8l|_7 z9t)r(%eaJYMSs3hql7 zEu)UzySen1;_YgS3q-gbgmF|$mI-v8^uGFqZIHTOUWzqpQRv}qF@$pF2?wJ?G}%yN zd2;@uTU-j(}q$)<}Jot3PgR1T<3G*h%C#?L)7QG)33-Mbrokms_C+_=3GS14HQ zo4{@7;xT*9wZ}Rv%|98ChX8eQ>HA38QR|Y2wCgh;2TQ|y_ZG9Q>+scy;9!l3Vl%!OOQ_P# z$XN)g*u8hraLejhu}6!Q*YdXq9SyxBG(HPDDo1VY8fg?<*&JZv>!136_{9+0??}gC z%$TnD7tDzV^U(zdZN0t8SZyB+BO4Vj^>XnUVD^SC{Yh=FCg&o>c%n~Ee z7grsR&;IzteFGUTOP$eCw79Qh4^}(N@X6h?D6zW{6`>XtqjTr_bmsrn?+_)QVi;8p zEKs|caduW6DKEBi`HgHv|6vVanj~#4W;-k4R!-*LnX!nl%9DW}4b13eGkv&NTP06X zLPTBu{l|jtO!|hxR-nl)ej&2uhRE;jwbr&iUJv=mF8S_J(EqMw|| literal 0 HcmV?d00001 diff --git a/src/plugin-slots/CourseTabsNavigationSlot/index.tsx b/src/plugin-slots/CourseTabsNavigationSlot/index.tsx new file mode 100644 index 0000000000..eda1e470b2 --- /dev/null +++ b/src/plugin-slots/CourseTabsNavigationSlot/index.tsx @@ -0,0 +1,8 @@ +import { CourseTabsNavigation, type CourseTabsNavigationProps } from '@src/course-tabs'; +import { PluginSlot } from '@openedx/frontend-plugin-framework'; + +export const CourseTabsNavigationSlot = ({ tabs, activeTabSlug }: CourseTabsNavigationProps) => ( + + + +); diff --git a/src/tab-page/LoadedTabPage.jsx b/src/tab-page/LoadedTabPage.tsx similarity index 73% rename from src/tab-page/LoadedTabPage.jsx rename to src/tab-page/LoadedTabPage.tsx index 67a1b86595..2ff80fc84b 100644 --- a/src/tab-page/LoadedTabPage.jsx +++ b/src/tab-page/LoadedTabPage.tsx @@ -1,27 +1,35 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import { getConfig } from '@edx/frontend-platform'; import { useToggle } from '@openedx/paragon'; -import { CourseTabsNavigation } from '../course-tabs'; -import { useModel } from '../generic/model-store'; -import { AlertList } from '../generic/user-messages'; -import StreakModal from '../shared/streak-celebration'; -import InstructorToolbar from '../instructor-toolbar'; -import useEnrollmentAlert from '../alerts/enrollment-alert'; -import useLogistrationAlert from '../alerts/logistration-alert'; +import { CourseTabsNavigationSlot } from '@src/plugin-slots/CourseTabsNavigationSlot'; + +import { useModel } from '@src/generic/model-store'; +import { AlertList } from '@src/generic/user-messages'; +import useEnrollmentAlert from '@src/alerts/enrollment-alert'; +import useLogistrationAlert from '@src/alerts/logistration-alert'; +import StreakModal from '@src/shared/streak-celebration'; +import InstructorToolbar from '@src/instructor-toolbar'; import ProductTours from '../product-tours/ProductTours'; +interface LoadedTabPageProps { + activeTabSlug: string; + children: React.ReactNode; + courseId: string; + metadataModel: string; + unitId: string | null; +} + const LoadedTabPage = ({ activeTabSlug, - children, + children = null, courseId, - metadataModel, - unitId, -}) => { + metadataModel = 'courseHomeMeta', + unitId = null, +}: LoadedTabPageProps) => { const { celebrations, org, @@ -80,7 +88,7 @@ const LoadedTabPage = ({ ...logistrationAlert, }} /> - +
{children}
@@ -89,18 +97,4 @@ const LoadedTabPage = ({ ); }; -LoadedTabPage.propTypes = { - activeTabSlug: PropTypes.string.isRequired, - children: PropTypes.node, - courseId: PropTypes.string.isRequired, - metadataModel: PropTypes.string, - unitId: PropTypes.string, -}; - -LoadedTabPage.defaultProps = { - children: null, - metadataModel: 'courseHomeMeta', - unitId: null, -}; - export default LoadedTabPage; From ba411525fb101009663276a2d250aad061fec646 Mon Sep 17 00:00:00 2001 From: Kshitij Sobti Date: Fri, 29 May 2026 08:13:11 +0530 Subject: [PATCH 2/2] fixup! feat: Introduce slot for customizing course tab navigation --- src/tab-page/LoadedTabPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tab-page/LoadedTabPage.tsx b/src/tab-page/LoadedTabPage.tsx index 2ff80fc84b..f1c3e8271f 100644 --- a/src/tab-page/LoadedTabPage.tsx +++ b/src/tab-page/LoadedTabPage.tsx @@ -17,17 +17,17 @@ import ProductTours from '../product-tours/ProductTours'; interface LoadedTabPageProps { activeTabSlug: string; - children: React.ReactNode; + children?: React.ReactNode; courseId: string; metadataModel: string; - unitId: string | null; + unitId?: string | null; } const LoadedTabPage = ({ activeTabSlug, children = null, courseId, - metadataModel = 'courseHomeMeta', + metadataModel, unitId = null, }: LoadedTabPageProps) => { const {