Compare commits

...

30 Commits

Author SHA1 Message Date
renovate[bot]
85881f5857 chore(deps): update actions/setup-node action to v6 2026-01-27 14:15:01 +00:00
Brian Smith
af6fe9fcdb chore(ci): update semantic release workflow to use OIDC (#660) 2026-01-27 09:14:11 -05:00
renovate[bot]
1118dca6c8 chore(deps): update dependency @openedx/paragon to v23.18.2 (#661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 06:40:46 +00:00
renovate[bot]
59ac43b1cc chore(deps): update dependency ts-jest to v29.4.6 (#659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 05:54:58 +00:00
renovate[bot]
aaa849a697 chore(deps): update dependency @openedx/paragon to v23.18.1 (#658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 08:59:11 +00:00
renovate[bot]
840b042606 chore(deps): update dependency @openedx/paragon to v23.18.0 (#657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 05:03:19 +00:00
renovate[bot]
93d62e35e8 chore(deps): update dependency react-router-dom to v6.30.2 (#656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 06:10:50 +00:00
renovate[bot]
0c305651b0 chore(deps): update dependency @openedx/paragon to v23.16.0 (#654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 06:36:21 +00:00
renovate[bot]
dfad1edcb9 chore(deps): update dependency redux-saga to v1.4.2 (#652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 07:02:31 +00:00
renovate[bot]
af46ef756e chore(deps): update dependency @openedx/paragon to v23.15.2 (#651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 07:02:10 +00:00
Farhaan Bukhsh
c86165180f feat: Moves search button into a slot (#644)
Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>
2025-10-27 10:51:45 -04:00
renovate[bot]
97063850c6 chore(deps): update dependency @openedx/paragon to v23.15.0 (#650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 04:33:28 +00:00
renovate[bot]
af252d2d3f chore(deps): update dependency @edx/frontend-platform to v8.5.2 (#649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 04:33:12 +00:00
renovate[bot]
9e850c3229 chore(deps): update dependency @openedx/paragon to v23.14.9 (#647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 05:52:53 +00:00
Braden MacDonald
413454b6e6 refactor: update propTypes -> TypeScript in Studio Header (#643) 2025-10-14 09:43:33 -07:00
renovate[bot]
ed7cc8a36e chore(deps): update dependency ts-jest to v29.4.5 (#646)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 04:27:12 +00:00
renovate[bot]
a8a4ad59ed chore(deps): update dependency @openedx/paragon to v23.14.8 (#645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 04:27:04 +00:00
Kyle McCormick
9f2b2a4026 feat: Remove Studio Maintenance link (empty commit for release CI)
This was already merged in
6d424edbd5,
but we are pushing this empty commit with BREAKING CHANGE message
in order to trigger the release of a new semantic version.

BREAKING CHANGE: The Studio "Maintenance" link has been removed
as part of https://github.com/openedx/edx-platform/issues/36263
2025-10-07 15:28:43 -04:00
Feanil Patel
21953ec96d Merge pull request #629 from openedx/renovate/actions-checkout-5.x
chore(deps): update actions/checkout action to v5
2025-10-07 14:06:33 -04:00
renovate[bot]
57671449b1 chore(deps): update actions/checkout action to v5 2025-10-07 17:59:08 +00:00
Feanil Patel
66012905b2 Merge pull request #641 from openedx/renovate/actions-setup-node-5.x
chore(deps): update actions/setup-node action to v5
2025-10-07 13:57:38 -04:00
renovate[bot]
209d52ed60 chore(deps): update actions/setup-node action to v5 2025-10-07 17:55:09 +00:00
Kyle McCormick
6d424edbd5 feat!: Remove Studio Maintenance link (#642)
This reverts commit 48c49fe0b2.

The original commit (a229c34535)
was merged prematurely, so it was reverted. However, now that
we have properly DEPR'd the underlying feature, we can revert
that revert.

Part of: https://github.com/openedx/edx-platform/issues/36263
2025-10-07 13:54:30 -04:00
renovate[bot]
5a4279a7f3 chore(deps): update dependency @openedx/paragon to v23.14.4 (#640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 13:54:01 -04:00
Peter Kulko
ef0ea1378d docs: [FC-86] Restructure Modify Items section with Add/Replace examples (#598)
* docs: example of adding Marketing links via the plugin slots

* refactor: code refactoring

* feat: added logoDestination props for Header component

* refactor: after rebase

* refactor: updated tests

* refactor: after review

* refactor: removed logoDestination prop

* docs: Custom Marketing links mobile example

* refactor: after review
2025-09-29 12:44:28 -04:00
oleksandr.buhaienko
b8b39bffbc test: Remove support for Node 20 2025-09-29 12:26:17 -03:00
renovate[bot]
7f778adda9 chore(deps): update jest monorepo to v30.2.0 (#637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 06:50:45 +00:00
renovate[bot]
4cf48d8ba8 chore(deps): update dependency @openedx/paragon to v23.14.3 (#636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 06:50:27 +00:00
Feanil Patel
2e82ba910d build: remove unused @edx/reactifex package
Remove @edx/reactifex package from devDependencies as it is no longer
needed. Translation extraction functionality has been verified to work
correctly without these dependencies.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 09:58:55 -03:00
oleksandr.buhaienko
02533b0474 build: Upgrade to Node 24 2025-09-26 09:18:14 -03:00
27 changed files with 7947 additions and 4079 deletions

View File

@@ -9,19 +9,15 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [20, 24]
continue-on-error: ${{ matrix.node == 24 }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Nodejs
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
- name: Validate package-lock.json changes

View File

@@ -4,19 +4,26 @@ on:
branches:
- master
- alpha
permissions:
contents: read # for checkout
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write # to be able to publish a GitHub release
issues: write # to be able to comment on released issues
pull-requests: write # to be able to comment on released pull requests
id-token: write # to enable use of OIDC for trusted publishing and npm provenance
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VER }}
- name: Install dependencies
@@ -39,9 +46,6 @@ jobs:
- name: Build
run: npm run build
- name: Release
uses: cycjimmy/semantic-release-action@v3
with:
semantic_version: 16
env:
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
run: npx semantic-release@25

2
.nvmrc
View File

@@ -1 +1 @@
20
24

View File

@@ -72,9 +72,9 @@ Cloning and Startup
``git clone https://github.com/openedx/frontend-component-header.git``
2. Use node v18.x.
2. Use node v24.x.
The current version of the micro-frontend build scripts support node 18.
The current version of the micro-frontend build scripts support node 24.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes an .nvmrc file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.

11391
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,22 +34,22 @@
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-platform": "^8.3.1",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "^14.3.2",
"@openedx/paragon": "^23.0.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "^16.2.0",
"jest": "30.1.3",
"jest": "30.2.0",
"jest-environment-jsdom": "^30.0.0",
"prop-types": "15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "^8.1.1",
"react-router-dom": "6.30.1",
"react-router-dom": "6.30.2",
"react-test-renderer": "^18.3.1",
"redux": "4.2.1",
"redux-saga": "1.3.0"
"redux-saga": "1.4.2",
"ts-jest": "^29.4.4"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.7.2",
@@ -71,4 +71,3 @@
"react-router-dom": "^6.14.2"
}
}

View File

@@ -61,11 +61,6 @@ const messages = defineMessages({
defaultMessage: 'Studio Home',
description: 'Link to the Studio Home',
},
'header.user.menu.studio.maintenance': {
id: 'header.user.menu.studio.maintenance',
defaultMessage: 'Maintenance',
description: 'Link to the Studio Maintenance',
},
'header.label.account.nav': {
id: 'header.label.account.nav',
defaultMessage: 'Account',

View File

@@ -13,9 +13,11 @@ This slot is used to replace/modify/hide the desktop main menu.
### Modify Items
The following `env.config.jsx` will modify the items in the desktop main menu.
#### Replace All Items
![Screenshot of modified items](./images/desktop_main_menu_modify_items.png)
The following `env.config.jsx` will replace all items in the desktop main menu.
![Screenshot of modified items](./images/desktop_main_menu_replace_all_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
@@ -59,6 +61,58 @@ const config = {
export default config;
```
#### Add Items
The following `env.config.jsx` will add items in the desktop main menu.
![Screenshot of custom marketing links](./images/desktop_main_menu_add_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifyMainMenu = (widget) => {
const existingMenu = widget.RenderWidget.props.menu || [];
const newMarketingLinks = [
{
type: 'item',
href: 'https://example.com/how-it-works',
content: 'How it works',
},
{
type: 'item',
href: 'https://example.com/courses',
content: 'Courses',
},
{
type: 'item',
href: 'https://example.com/schools',
content: 'Schools',
}
];
widget.content.menu = [...existingMenu, ...newMarketingLinks];
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyMainMenu,
},
]
},
},
}
export default config;
```
### Replace Menu with Custom Component
The following `env.config.jsx` will replace the desktop main menu entirely (in this case with a centered 🗺️ `h1`)
@@ -134,4 +188,3 @@ const config = {
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -13,9 +13,11 @@ This slot is used to replace/modify/hide the mobile main menu.
### Modify Items
The following `env.config.jsx` will modify the items in the mobile main menu.
#### Replace All Items
![Screenshot of modified items](./images/mobile_main_menu_modify_items.png)
The following `env.config.jsx` will replace all items in the mobile main menu.
![Screenshot of modified items](./images/mobile_main_menu_replace_all_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
@@ -59,6 +61,58 @@ const config = {
export default config;
```
#### Add Items
The following `env.config.jsx` will add items in the mobile main menu.
![Screenshot of custom marketing links](./images/mobile_main_menu_add_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifyMainMenu = (widget) => {
const existingMenu = widget.RenderWidget.props.menu || [];
const newMarketingLinks = [
{
type: 'item',
href: 'https://example.com/how-it-works',
content: 'How it works',
},
{
type: 'item',
href: 'https://example.com/courses',
content: 'Courses',
},
{
type: 'item',
href: 'https://example.com/schools',
content: 'Schools',
}
];
widget.content.menu = [...existingMenu, ...newMarketingLinks];
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_mobile_main_menu.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyMainMenu,
},
]
},
},
}
export default config;
```
### Replace Menu with Custom Component
The following `env.config.jsx` will replace the mobile main menu entirely (in this case with a centered 🗺️ `h1`)

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -24,3 +24,6 @@
* [`org.openedx.frontend.layout.header_mobile_main_menu.v1`](./MobileMainMenuSlot/)
* [`org.openedx.frontend.layout.header_mobile_user_menu.v1`](./MobileUserMenuSlot/)
* [`org.openedx.frontend.layout.header_mobile_user_menu_trigger.v1`](./MobileUserMenuToggleSlot/)
### Studio Header
* [`org.openedx.frontend.layout.studio_header_search_button_slot.v1`](./StudioHeaderSearchButtonSlot/)

View File

@@ -0,0 +1,89 @@
# Studio Header Search Button Slot
### Slot ID: `org.openedx.frontend.layout.studio_header_search_button_slot.v1`
## Description
This slot is used to replace/modify/hide the search button in the studio header.
## Examples
### Replace search with custom component
The following `env.config.jsx` will replace the search button entirely (in this case with a custom emoji icon):
![Search button being replaced](./images/studio_header_search_button_replace.png)
```jsx
import {
DIRECT_PLUGIN,
PLUGIN_OPERATIONS,
} from "@openedx/frontend-plugin-framework";
const config = {
pluginSlots: {
"org.openedx.frontend.layout.studio_header_search_button_slot.v1": {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: "custom_notification_tray",
type: DIRECT_PLUGIN,
RenderWidget: () => <span>🔔</span>,
},
},
],
},
},
};
export default config;
```
### Add custom component before and after search button
The following `env.config.jsx` will insert emoji after and before the search button
![Add custom component before and after search button](./images/studio_header_search_button_before_after.png)
```jsx
import {
DIRECT_PLUGIN,
PLUGIN_OPERATIONS,
} from "@openedx/frontend-plugin-framework";
const config = {
pluginSlots: {
"org.openedx.frontend.layout.studio_header_search_button_slot.v1": {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
priority: 10,
id: 'custom_notification_tray_before',
type: DIRECT_PLUGIN,
RenderWidget: () => <span>🔔</span>,
},
},
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
priority: 90,
id: 'custom_notification_tray_after',
type: DIRECT_PLUGIN,
RenderWidget: () => <span>🔕</span>,
},
},
],
},
};
export default config;
```
## API
- **Slot ID:** `org.openedx.frontend.layout.studio_header_search_button_slot.v1`
- **Component:** Uses the [PluginSlot](https://github.com/openedx/frontend-plugin-framework#pluginslot) from the Open edX Frontend Plugin Framework for plugin injection.

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { Nav, IconButton, Icon } from '@openedx/paragon';
import { Search } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../../studio-header/messages';
const StudioHeaderSearchButtonSlot = ({ searchButtonAction }) => {
const intl = useIntl();
return (
<PluginSlot
id="org.openedx.frontend.layout.studio_header_search_button_slot.v1"
>
{searchButtonAction && (
<Nav>
<IconButton
src={Search}
iconAs={Icon}
onClick={searchButtonAction}
aria-label={intl.formatMessage(messages['header.label.search.nav'])}
alt={intl.formatMessage(messages['header.label.search.nav'])}
/>
</Nav>
)}
</PluginSlot>
);
};
StudioHeaderSearchButtonSlot.propTypes = {
searchButtonAction: PropTypes.func,
};
export default StudioHeaderSearchButtonSlot;

View File

@@ -1,8 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import React, { type FunctionComponent } from 'react';
import { Link } from 'react-router-dom';
const BrandNav = ({
interface Props {
studioBaseUrl: string;
logo: string;
logoAltText: string;
}
const BrandNav: FunctionComponent<Props> = ({
studioBaseUrl,
logo,
logoAltText,
@@ -16,10 +21,4 @@ const BrandNav = ({
</Link>
);
BrandNav.propTypes = {
studioBaseUrl: PropTypes.string.isRequired,
logo: PropTypes.string.isRequired,
logoAltText: PropTypes.string.isRequired,
};
export default BrandNav;

View File

@@ -1,5 +1,4 @@
import React from 'react';
import PropTypes from 'prop-types';
import React, { type FunctionComponent } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
OverlayTrigger,
@@ -9,14 +8,19 @@ import { Link } from 'react-router-dom';
import messages from './messages';
const CourseLockUp = (
{
outlineLink,
org,
number,
title,
},
) => {
interface Props {
outlineLink?: string;
org?: string;
number?: string;
title?: string;
}
const CourseLockUp: FunctionComponent<Props> = ({
outlineLink = '',
org = '',
number = '',
title = '',
}) => {
const intl = useIntl();
return (
@@ -41,18 +45,4 @@ const CourseLockUp = (
);
};
CourseLockUp.propTypes = {
number: PropTypes.string,
org: PropTypes.string,
title: PropTypes.string,
outlineLink: PropTypes.string,
};
CourseLockUp.defaultProps = {
number: null,
org: null,
title: null,
outlineLink: null,
};
export default CourseLockUp;

View File

@@ -1,23 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import React, { type ReactNode, type ComponentProps } from 'react';
import classNames from 'classnames';
import {
ActionRow,
Button,
Container,
Icon,
IconButton,
Nav,
Row,
} from '@openedx/paragon';
import { Close, MenuIcon, Search } from '@openedx/paragon/icons';
import { Close, MenuIcon } from '@openedx/paragon/icons';
import CourseLockUp from './CourseLockUp';
import UserMenu from './UserMenu';
import BrandNav from './BrandNav';
import NavDropdownMenu from './NavDropdownMenu';
import messages from './messages';
import StudioHeaderSearchButtonSlot from '../plugin-slots/StudioHeaderSearchButtonSlot';
export interface HeaderBodyProps {
studioBaseUrl: string;
logoutUrl: string;
setModalPopupTarget?: ((instance: HTMLButtonElement | null) => void) | null;
toggleModalPopup?: React.MouseEventHandler<HTMLButtonElement>;
isModalPopupOpen?: boolean;
number?: string;
org?: string;
title: string;
logo: string;
logoAltText: string;
authenticatedUserAvatar?: string;
username?: string;
isAdmin?: boolean;
isMobile?: boolean;
isHiddenMainMenu?: boolean;
mainMenuDropdowns?: {
id: string;
buttonTitle: ReactNode;
items: { title: ReactNode; href: string; }[];
}[];
outlineLink?: string;
searchButtonAction?: React.MouseEventHandler<HTMLButtonElement>;
containerProps?: Omit<ComponentProps<typeof Container>, 'children'>;
}
const HeaderBody = ({
logo,
@@ -31,16 +53,15 @@ const HeaderBody = ({
logoutUrl,
authenticatedUserAvatar,
isMobile,
setModalPopupTarget,
setModalPopupTarget = null,
toggleModalPopup,
isModalPopupOpen,
isHiddenMainMenu,
mainMenuDropdowns,
isModalPopupOpen = false,
isHiddenMainMenu = false,
mainMenuDropdowns = [],
outlineLink,
searchButtonAction,
containerProps,
}) => {
const intl = useIntl();
containerProps = {},
}: HeaderBodyProps) => {
const renderBrandNav = (
<BrandNav
@@ -52,7 +73,7 @@ const HeaderBody = ({
/>
);
const { className: containerClassName, ...restContainerProps } = containerProps || {};
const { className: containerClassName, ...restContainerProps } = containerProps;
return (
<Container
@@ -116,17 +137,9 @@ const HeaderBody = ({
</>
)}
<ActionRow.Spacer />
{searchButtonAction && (
<Nav>
<IconButton
src={Search}
iconAs={Icon}
onClick={searchButtonAction}
aria-label={intl.formatMessage(messages['header.label.search.nav'])}
alt={intl.formatMessage(messages['header.label.search.nav'])}
/>
</Nav>
)}
<StudioHeaderSearchButtonSlot
searchButtonAction={searchButtonAction}
/>
<Nav>
<UserMenu
{...{
@@ -144,53 +157,4 @@ const HeaderBody = ({
);
};
HeaderBody.propTypes = {
studioBaseUrl: PropTypes.string.isRequired,
logoutUrl: PropTypes.string.isRequired,
setModalPopupTarget: PropTypes.func,
toggleModalPopup: PropTypes.func,
isModalPopupOpen: PropTypes.bool,
number: PropTypes.string,
org: PropTypes.string,
title: PropTypes.string,
logo: PropTypes.string,
logoAltText: PropTypes.string,
authenticatedUserAvatar: PropTypes.string,
username: PropTypes.string,
isAdmin: PropTypes.bool,
isMobile: PropTypes.bool,
isHiddenMainMenu: PropTypes.bool,
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
buttonTitle: PropTypes.node,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string,
title: PropTypes.node,
})),
})),
outlineLink: PropTypes.string,
searchButtonAction: PropTypes.func,
containerProps: PropTypes.shape(Container.propTypes),
};
HeaderBody.defaultProps = {
setModalPopupTarget: null,
toggleModalPopup: null,
isModalPopupOpen: false,
logo: null,
logoAltText: null,
number: '',
org: '',
title: '',
authenticatedUserAvatar: null,
username: null,
isAdmin: false,
isMobile: false,
isHiddenMainMenu: false,
mainMenuDropdowns: [],
outlineLink: null,
searchButtonAction: null,
containerProps: {},
};
export default HeaderBody;

View File

@@ -1,19 +1,32 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import React, { type FunctionComponent, useState } from 'react';
import { useToggle, ModalPopup } from '@openedx/paragon';
import HeaderBody from './HeaderBody';
import HeaderBody, { type HeaderBodyProps } from './HeaderBody';
import MobileMenu from './MobileMenu';
const MobileHeader = ({
type Props = Pick<HeaderBodyProps,
| 'studioBaseUrl'
| 'logoutUrl'
| 'number'
| 'org'
| 'title'
| 'logo'
| 'logoAltText'
| 'authenticatedUserAvatar'
| 'username'
| 'isAdmin'
| 'mainMenuDropdowns'
| 'outlineLink'
>;
const MobileHeader: FunctionComponent<Props> = ({
mainMenuDropdowns,
...props
}) => {
const [isOpen, , close, toggle] = useToggle(false);
const [target, setTarget] = useState(null);
const [target, setTarget] = useState<HTMLButtonElement | null>(null);
return (
<>
{/* @ts-expect-error The type of 'props' is any until we convert from propTypes to TypeScript interface/types */}
<HeaderBody
{...props}
isMobile
@@ -36,39 +49,4 @@ const MobileHeader = ({
);
};
MobileHeader.propTypes = {
studioBaseUrl: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
logoutUrl: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
number: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
org: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
title: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
logo: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
logoAltText: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
authenticatedUserAvatar: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
username: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
isAdmin: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
buttonTitle: PropTypes.node,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string,
title: PropTypes.node,
})),
})),
outlineLink: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
};
MobileHeader.defaultProps = {
logo: null,
logoAltText: null,
number: null,
org: null,
title: null,
authenticatedUserAvatar: null,
username: null,
isAdmin: false,
mainMenuDropdowns: [],
outlineLink: null,
};
export default MobileHeader;

View File

@@ -1,16 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import React, { type ReactNode } from 'react';
import {
Dropdown,
DropdownButton,
} from '@openedx/paragon';
import { Link } from 'react-router-dom';
interface Props {
id: string;
buttonTitle: ReactNode;
items: { title: ReactNode; href: string; }[];
}
const NavDropdownMenu = ({
id,
buttonTitle,
items,
}) => (
}: Props) => (
<DropdownButton
id={id}
title={buttonTitle}
@@ -30,13 +35,4 @@ const NavDropdownMenu = ({
</DropdownButton>
);
NavDropdownMenu.propTypes = {
id: PropTypes.string.isRequired,
buttonTitle: PropTypes.node.isRequired,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string.isRequired,
title: PropTypes.node.isRequired,
})).isRequired,
};
export default NavDropdownMenu;

View File

@@ -12,7 +12,6 @@ import { Context as ResponsiveContext } from 'react-responsive';
import { MemoryRouter } from 'react-router-dom';
import StudioHeader from './StudioHeader';
import messages from './messages';
const authenticatedUser = {
userId: 3,
@@ -72,12 +71,8 @@ const props: React.ComponentProps<typeof StudioHeader> = {
},
],
outlineLink: 'tEsTLInK',
searchButtonAction: null,
searchButtonAction: undefined,
isNewHomePage: true,
// These default values shouldn't be needed but typescript is confused by propTypes; can remove after converting
// from propTypes to TypeScript:
containerProps: {},
isHiddenMainMenu: false,
};
describe('Header', () => {
@@ -119,16 +114,6 @@ describe('Header', () => {
expect(dropdownOption).toBeVisible();
});
it('maintenance should not be in user menu', async () => {
currentUser = { ...authenticatedUser, administrator: false };
const { getAllByRole, queryByText } = render(<RootWrapper {...props} />);
const userMenu = getAllByRole('button')[1];
await waitFor(() => fireEvent.click(userMenu));
const maintenanceButton = queryByText(messages['header.user.menu.maintenance'].defaultMessage);
expect(maintenanceButton).toBeNull();
});
it('user menu should use avatar icon', async () => {
currentUser = { ...authenticatedUser, avatar: null };
const { getByTestId } = render(<RootWrapper {...props} />);
@@ -157,7 +142,7 @@ describe('Header', () => {
});
it('should not show search button', async () => {
const testProps = { ...props, searchButtonAction: null };
const testProps = { ...props, searchButtonAction: undefined };
const { queryByRole } = render(<RootWrapper {...testProps} />);
expect(queryByRole('button', { name: 'Search content' })).not.toBeInTheDocument();
});
@@ -190,15 +175,6 @@ describe('Header', () => {
expect(desktopMenu).toBeNull();
});
it('maintenance should be in user menu', async () => {
const { getAllByRole, getByText } = render(<RootWrapper {...props} />);
const userMenu = getAllByRole('button')[1];
await waitFor(() => fireEvent.click(userMenu));
const maintenanceButton = getByText(messages['header.user.menu.maintenance'].defaultMessage);
expect(maintenanceButton).toBeVisible();
});
it('user menu should use avatar image', async () => {
const { getByTestId } = render(<RootWrapper {...props} />);
const avatarImage = getByTestId('avatar-image');

View File

@@ -1,11 +1,10 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import React, { type FunctionComponent, useContext } from 'react';
import Responsive from 'react-responsive';
import { AppContext } from '@edx/frontend-platform/react';
import { ensureConfig } from '@edx/frontend-platform';
import MobileHeader from './MobileHeader';
import HeaderBody from './HeaderBody';
import HeaderBody, { HeaderBodyProps } from './HeaderBody';
ensureConfig([
'STUDIO_BASE_URL',
@@ -15,9 +14,29 @@ ensureConfig([
'LOGO_URL',
], 'Studio Header component');
const StudioHeader = ({
number, org, title, containerProps, isHiddenMainMenu, mainMenuDropdowns,
outlineLink, searchButtonAction, isNewHomePage,
type Props = Pick<HeaderBodyProps,
| 'number'
| 'org'
| 'title'
| 'containerProps'
| 'isHiddenMainMenu'
| 'mainMenuDropdowns'
| 'outlineLink'
| 'searchButtonAction'
> & {
isNewHomePage: boolean;
};
const StudioHeader: FunctionComponent<Props> = ({
number,
org,
title,
containerProps,
isHiddenMainMenu,
mainMenuDropdowns,
outlineLink,
searchButtonAction,
isNewHomePage,
}) => {
// @ts-expect-error - frontend-platform doesn't yet have type information :/
const { authenticatedUser, config } = useContext(AppContext);
@@ -52,33 +71,4 @@ const StudioHeader = ({
);
};
StudioHeader.propTypes = {
number: PropTypes.string,
org: PropTypes.string,
title: PropTypes.string.isRequired,
containerProps: HeaderBody.propTypes.containerProps,
isHiddenMainMenu: PropTypes.bool,
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
buttonTitle: PropTypes.node,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string,
title: PropTypes.node,
})),
})),
outlineLink: PropTypes.string,
searchButtonAction: PropTypes.func,
isNewHomePage: PropTypes.bool.isRequired,
};
StudioHeader.defaultProps = {
number: '',
org: '',
containerProps: {},
isHiddenMainMenu: false,
mainMenuDropdowns: [],
outlineLink: null,
searchButtonAction: null,
};
export default StudioHeader;

View File

@@ -6,11 +6,6 @@ const messages = defineMessages({
defaultMessage: 'Studio Home',
description: 'Link to Studio Home',
},
'header.user.menu.maintenance': {
id: 'header.user.menu.maintenance',
defaultMessage: 'Maintenance',
description: 'Link to the Studio maintenance page',
},
'header.user.menu.logout': {
id: 'header.user.menu.logout',
defaultMessage: 'Logout',

View File

@@ -1,4 +1,3 @@
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
const getUserMenuItems = ({
@@ -21,9 +20,6 @@ const getUserMenuItems = ({
{
href: `${studioBaseUrl}`,
title: intl.formatMessage(messages['header.user.menu.studio']),
}, {
href: `${getConfig().STUDIO_BASE_URL}/maintenance`,
title: intl.formatMessage(messages['header.user.menu.maintenance']),
}, {
href: `${logoutUrl}`,
title: intl.formatMessage(messages['header.user.menu.logout']),