diff --git a/README.rst b/README.rst index 31b477a85..f00c3db4b 100644 --- a/README.rst +++ b/README.rst @@ -43,6 +43,12 @@ Usage ``import Header, { messages } from '@edx/frontend-component-header-edx';`` +Plugins +======= +This component can be customized using `Frontend Plugin Framework `_. + +The parts of this component that can be customized in that manner are documented `here `_. + Development =========== diff --git a/package-lock.json b/package-lock.json index 9e3be9586..9f211b915 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@fortawesome/free-regular-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.0", + "@openedx/frontend-plugin-framework": "^1.2.1", "@openedx/paragon": "^21.11.0", "@reduxjs/toolkit": "1.9.7", "axios-mock-adapter": "1.22.0", @@ -3582,6 +3583,43 @@ } } }, + "node_modules/@openedx/frontend-plugin-framework": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.2.1.tgz", + "integrity": "sha512-ooreh/Tbz9U+7vIDwgbw25Jw0HBPQZUgjh8f4EHxGJX7AJbNC+DykJToEGkqUlX/nm0LHI+60BX6DEncPsXAeg==", + "dependencies": { + "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", + "classnames": "^2.3.2", + "core-js": "3.36.0", + "react-redux": "7.2.9", + "redux": "4.2.1", + "regenerator-runtime": "0.14.1" + }, + "peerDependencies": { + "@edx/frontend-platform": "^7.0.0 || ^8.0.0", + "@openedx/paragon": "^21.0.0 || ^22.0.0", + "prop-types": "^15.8.0", + "react": "^17.0.0", + "react-dom": "^17.0.0", + "react-error-boundary": "^4.0.11" + } + }, + "node_modules/@openedx/frontend-plugin-framework/node_modules/@edx/brand": { + "name": "@openedx/brand-openedx", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@openedx/brand-openedx/-/brand-openedx-1.2.3.tgz", + "integrity": "sha512-Dn9CtpC8fovh++Xi4NF5NJoeR9yU2yXZnV9IujxIyGd/dn0Phq5t6dzJVfupwq09mpDnzJv7egA8Znz/3ljO+w==" + }, + "node_modules/@openedx/frontend-plugin-framework/node_modules/core-js": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.0.tgz", + "integrity": "sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/@openedx/paragon": { "version": "21.11.4", "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-21.11.4.tgz", @@ -14731,6 +14769,18 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", + "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", diff --git a/package.json b/package.json index 259b969e3..b0159597f 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@fortawesome/free-regular-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.0", + "@openedx/frontend-plugin-framework": "^1.2.1", "@openedx/paragon": "^21.11.0", "@reduxjs/toolkit": "1.9.7", "axios-mock-adapter": "1.22.0", diff --git a/src/DesktopHeader.jsx b/src/DesktopHeader.jsx index df5ac78dc..fb685f2d5 100644 --- a/src/DesktopHeader.jsx +++ b/src/DesktopHeader.jsx @@ -5,6 +5,8 @@ import { getConfig } from '@edx/frontend-platform'; import { AvatarButton, Dropdown } from '@openedx/paragon'; // Local Components +import UserMenuGroupItemSlot from './plugin-slots/UserMenuGroupItemSlot'; +import UserMenuGroupSlot from './plugin-slots/UserMenuGroupSlot'; import UserMenuItem from './common/UserMenuItem'; import { Menu, MenuTrigger, MenuContent } from './Menu'; import { LinkedLogo, Logo } from './Logo'; @@ -104,10 +106,12 @@ class DesktopHeader extends React.Component { /> )} + {userMenu.map((group, index) => ( // eslint-disable-next-line react/jsx-no-comment-textnodes,react/no-array-index-key {group.heading && {group.heading}} + {index === 0 && ()} {group.items.map(({ type, content, href, disabled, isActive, onClick, }) => ( diff --git a/src/MobileHeader.jsx b/src/MobileHeader.jsx index a304dfed4..d5f3da45b 100644 --- a/src/MobileHeader.jsx +++ b/src/MobileHeader.jsx @@ -5,6 +5,8 @@ import { getConfig } from '@edx/frontend-platform'; // Local Components import { AvatarButton } from '@openedx/paragon'; +import UserMenuGroupSlot from './plugin-slots/UserMenuGroupSlot'; +import UserMenuGroupItemSlot from './plugin-slots/UserMenuGroupItemSlot'; import { Menu, MenuTrigger, MenuContent } from './Menu'; import { LinkedLogo, Logo } from './Logo'; import UserMenuItem from './common/UserMenuItem'; @@ -97,7 +99,12 @@ class MobileHeader extends React.Component { )) )); - return userInfoItem ? [userInfoItem, ...userMenuItems] : userMenuItems; + const userMenuGroupSlot = ; + const userMenuGroupItemSlot = ; + + return userInfoItem + ? [userInfoItem, userMenuGroupSlot, userMenuGroupItemSlot, ...userMenuItems] + : [userMenuGroupSlot, userMenuGroupItemSlot, ...userMenuItems]; } renderLoggedOutItems() { diff --git a/src/plugin-slots/README.md b/src/plugin-slots/README.md new file mode 100644 index 000000000..92776fc44 --- /dev/null +++ b/src/plugin-slots/README.md @@ -0,0 +1,4 @@ +# `frontend-component-header-edx` Plugin Slots + +* [`header_user_menu_group_slot`](./UserMenuGroupSlot/) +* [`header_user_menu_group_item_slot`](./UserMenuGroupItemSlot/) diff --git a/src/plugin-slots/UserMenuGroupItemSlot/README.md b/src/plugin-slots/UserMenuGroupItemSlot/README.md new file mode 100644 index 000000000..764853da8 --- /dev/null +++ b/src/plugin-slots/UserMenuGroupItemSlot/README.md @@ -0,0 +1,72 @@ +# Header User Menu Group Slot + +### Slot ID: `header_user_menu_group_item_slot` + +## Description + +This slot allows you to insert a user menu item into a group within the header's user menu for both desktop and mobile screens. + +Note: Ensure the slot is provided with appropriate JSX that can render smoothly on both desktop and mobile screens. + +## Example + +The following ``env.config.jsx`` demonstrates how to insert a user menu item into a group within the header's user menu +for both desktop and mobile screens. + +**Default Behaviour:** + +Desktop: +![Screenshot of Default Header_User_Menu](./images/default_user_menu_desktop.png) + +Mobile: +![Screenshot of Default Header_User_Menu](./images/default_user_menu_mobile.png) + +**Inserted a user menu group:** + +Desktop: +![Screenshot of Inserted_User_Menu_Group](./images/inserted_user_menu_item_desktop.png) + +Mobile: +![Screenshot of Inserted_User_Menu_Group](./images/inserted_user_menu_item_mobile.png) + +```jsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { breakpoints, Dropdown, useWindowSize } from '@openedx/paragon'; + +const config = { + pluginSlots: { + header_user_menu_group_slot: { + plugins: [ + { + // Insert some user menu group item + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'user_menu_group', + type: DIRECT_PLUGIN, + RenderWidget: () => { + const { width } = useWindowSize(); + const isMobile = width <= breakpoints.small.maxWidth; + if (!isMobile) { + return ( + + User Menu Group Item + + ); + } + return ( +
  • + + User Menu Group Item + +
  • + ); + }, + }, + }, + ], + }, + }, +} + +export default config; +``` diff --git a/src/plugin-slots/UserMenuGroupItemSlot/images/default_user_menu_desktop.png b/src/plugin-slots/UserMenuGroupItemSlot/images/default_user_menu_desktop.png new file mode 100644 index 000000000..ad26802ce Binary files /dev/null and b/src/plugin-slots/UserMenuGroupItemSlot/images/default_user_menu_desktop.png differ diff --git a/src/plugin-slots/UserMenuGroupItemSlot/images/default_user_menu_mobile.png b/src/plugin-slots/UserMenuGroupItemSlot/images/default_user_menu_mobile.png new file mode 100644 index 000000000..e68c40485 Binary files /dev/null and b/src/plugin-slots/UserMenuGroupItemSlot/images/default_user_menu_mobile.png differ diff --git a/src/plugin-slots/UserMenuGroupItemSlot/images/inserted_user_menu_item_desktop.png b/src/plugin-slots/UserMenuGroupItemSlot/images/inserted_user_menu_item_desktop.png new file mode 100644 index 000000000..efd37c7e4 Binary files /dev/null and b/src/plugin-slots/UserMenuGroupItemSlot/images/inserted_user_menu_item_desktop.png differ diff --git a/src/plugin-slots/UserMenuGroupItemSlot/images/inserted_user_menu_item_mobile.png b/src/plugin-slots/UserMenuGroupItemSlot/images/inserted_user_menu_item_mobile.png new file mode 100644 index 000000000..b6d022f91 Binary files /dev/null and b/src/plugin-slots/UserMenuGroupItemSlot/images/inserted_user_menu_item_mobile.png differ diff --git a/src/plugin-slots/UserMenuGroupItemSlot/index.jsx b/src/plugin-slots/UserMenuGroupItemSlot/index.jsx new file mode 100644 index 000000000..7d6c95693 --- /dev/null +++ b/src/plugin-slots/UserMenuGroupItemSlot/index.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +import { PluginSlot } from '@openedx/frontend-plugin-framework'; + +const UserMenuGroupItemSlot = () => ( + +); + +export default UserMenuGroupItemSlot; diff --git a/src/plugin-slots/UserMenuGroupSlot/README.md b/src/plugin-slots/UserMenuGroupSlot/README.md new file mode 100644 index 000000000..47dbc65dc --- /dev/null +++ b/src/plugin-slots/UserMenuGroupSlot/README.md @@ -0,0 +1,86 @@ +# Header User Menu Group Slot + +### Slot ID: `header_user_menu_group_slot` + +## Description + +This slot is used to insert a user menu group in the header's user menu for both desktop and mobile screens. + +Note: Ensure the slot is provided with appropriate JSX that can render smoothly on both desktop and mobile screens. + +## Example + +The following ``env.config.jsx`` demonstrates how to insert a user menu group into the header's user menu +for both mobile and desktop screens. + +**Default Behaviour:** + +Desktop: +![Screenshot of Default Header_User_Menu_Desktop](./images/default_user_menu_desktop.png) + +Mobile: +![Screenshot of Default Header_User_Menu_Mobile](./images/default_user_menu_mobile.png) + +**Inserted a user menu group:** + +Desktop: +![Screenshot of Inserted_User_Menu_Group_Desktop](./images/inserted_user_menu_desktop.png) + +Mobile: +![Screenshot of Inserted_User_Menu_Group_Mobile](./images/inserted_user_menu_mobile.png) + +```jsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { breakpoints, Dropdown, useWindowSize } from '@openedx/paragon'; + +const config = { + pluginSlots: { + header_user_menu_group_slot: { + plugins: [ + { + // Insert some user menu group + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'user_menu_group', + type: DIRECT_PLUGIN, + RenderWidget: () => { + const { width } = useWindowSize(); + const isMobile = width <= breakpoints.small.maxWidth; + if (!isMobile) { + return ( + <> + User Menu Group Header + + User Menu Group Item - Active + + + User Menu Group Item 2 + + + + ); + } + return ( + <> +
  • + + User Menu Group Item - Active + +
  • +
  • + + User Menu Group Item 2 + +
  • + + ); + }, + }, + }, + ], + }, + }, +} + +export default config; +``` diff --git a/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_desktop.png b/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_desktop.png new file mode 100644 index 000000000..ad26802ce Binary files /dev/null and b/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_desktop.png differ diff --git a/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_mobile.png b/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_mobile.png new file mode 100644 index 000000000..133f2bb80 Binary files /dev/null and b/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_mobile.png differ diff --git a/src/plugin-slots/UserMenuGroupSlot/images/inserted_user_menu_desktop.png b/src/plugin-slots/UserMenuGroupSlot/images/inserted_user_menu_desktop.png new file mode 100644 index 000000000..a7d012ad1 Binary files /dev/null and b/src/plugin-slots/UserMenuGroupSlot/images/inserted_user_menu_desktop.png differ diff --git a/src/plugin-slots/UserMenuGroupSlot/images/inserted_user_menu_mobile.png b/src/plugin-slots/UserMenuGroupSlot/images/inserted_user_menu_mobile.png new file mode 100644 index 000000000..7bbec5d84 Binary files /dev/null and b/src/plugin-slots/UserMenuGroupSlot/images/inserted_user_menu_mobile.png differ diff --git a/src/plugin-slots/UserMenuGroupSlot/index.jsx b/src/plugin-slots/UserMenuGroupSlot/index.jsx new file mode 100644 index 000000000..8a06242e0 --- /dev/null +++ b/src/plugin-slots/UserMenuGroupSlot/index.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +import { PluginSlot } from '@openedx/frontend-plugin-framework'; + +const UserMenuGroupSlot = () => ( + +); + +export default UserMenuGroupSlot;