Skip to content

feat: decouple notifications panel using widget registry mechanism#1885

Open
awais-ansari wants to merge 1 commit intoopenedx:masterfrom
awais-ansari:aansari/hide-upsell-1874
Open

feat: decouple notifications panel using widget registry mechanism#1885
awais-ansari wants to merge 1 commit intoopenedx:masterfrom
awais-ansari:aansari/hide-upsell-1874

Conversation

@awais-ansari
Copy link
Copy Markdown
Contributor

@awais-ansari awais-ansari commented Mar 26, 2026

Summary

Removes the tightly coupled upgrade/upsell ("notifications") panel from the Learning MFE core and replaces it with a pluggable widget registry system. The right sidebar now supports dynamically-registered external widgets, making it easy to add, remove, or replace sidebar panels without forking the MFE. Proposal

Motivation

The built-in upgrade panel (NotificationTray) caused several issues:

  1. Misleading naming: "Notifications" implied platform-level notifications, conflicting with the separate notification tray in the header.
  2. Non-Core: The upgrade panel is a non-core feature. It should not be bundled into the Open edX core.
  3. Empty panel UX problem: In courses without a paid track, the panel rendered empty but remained visible.

Architecture: Pluggable Widget Registry

The right sidebar now operates on a widget registry (SIDEBARS) built at runtime from:

Built-in widgets: only DISCUSSIONS are included by default.
External widgets: installed via npm and registered via SIDEBAR_WIDGETS in env.config.jsx
Each widget provides:

{
  id: string,
  priority: number, 
  Sidebar: ReactComponent,   
  Trigger: ReactComponent, 
  isAvailable: (context) => boolean,
  enabled: boolean,
  Provider?: ReactComponent, 
}

Rename: notifications → upgrade

All internal references to the old "Notifications" panel are renamed for clarity:

  1. WIDGETS.NOTIFICATIONS → WIDGETS.UPGRADE
  2. NotificationTray → UpgradePanel
  3. NotificationTrigger → UpgradeTrigger
  4. NotificationIcon → UpgradeIcon
  5. notificationStatus context → upgradeWidgetStatus
  6. LocalStorage keys updated accordingly

Extracted Upgrade Widget Package

The upgrade panel is extracted into a standalone package skeleton at learning-upgrade-widget. Currently, the package skeleton lives in src/external-widgets/learning-upsell-widget/ until promoted to its own repository.

Configuration

// Production: import { upgradeWidgetConfig } from '@edx/learning-upgrade-widget';
import { upgradeWidgetConfig } from './src/external-widgets/learning-upgrade-widget/src/index';
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';

// Load environment variables from .env file
const config = {
  ...process.env,
  SIDEBAR_WIDGETS: [upgradeWidgetConfig],
};

export default config;

New Widget SDK

sidebar-widget-sdk.js is the sole contract between the host MFE and external widget packages :

import {
  SidebarContext, SidebarBase, SidebarTriggerBase,
  useModel, getLocalStorage, setLocalStorage,
} from '@edx/learning-mfe-widget';

Two-Sidebar Collaboration

The architecture docs in ARCHITECTURE.md fully describe the shared currentSidebar state between the LEFT (Course Outline) and RIGHT (widget panels) sidebars, including:

  • Priority cascade: DISCUSSIONS (10) → UPGRADE (20) → external widgets → COURSE_OUTLINE (fallback)
  • Auto-open logic on initial load
  • Unit shift behavior for each scenario
  • State persistence via localStorage / sessionStorage

Testing

All use cases verified and documented in USE_CASE_VERIFICATION.md, covering:

  • Initial load (desktop / mobile) with all panel availability combinations
  • Unit navigation across 4 scenarios (Discussions open, Upgrade open, Course Outline open, nothing open)
  • Manual toggle behavior
  • Window resize transitions
  • State persistence (localStorage / sessionStorage)

@awais-ansari awais-ansari force-pushed the aansari/hide-upsell-1874 branch from 005f525 to e50a017 Compare March 26, 2026 19:18
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 26, 2026

Codecov Report

❌ Patch coverage is 56.85619% with 129 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.74%. Comparing base (a086614) to head (1b6e9df).

Files with missing lines Patch % Lines
...eware/course/sidebar/hooks/useUnitShiftBehavior.js 48.93% 19 Missing and 5 partials ⚠️
...arning-upgrade-widget/src/UpgradeWidgetContext.jsx 0.00% 23 Missing ⚠️
.../courseware/course/sidebar/hooks/useSidebarSync.js 35.29% 17 Missing and 5 partials ⚠️
...ets/learning-upgrade-widget/src/UpgradeTrigger.jsx 0.00% 13 Missing ⚠️
...dgets/learning-upgrade-widget/src/UpgradePanel.jsx 0.00% 12 Missing ⚠️
...c/courseware/course/sidebar/common/SidebarBase.jsx 0.00% 5 Missing ⚠️
...ware/course/sidebar/hooks/useResponsiveBehavior.js 64.28% 4 Missing and 1 partial ⚠️
src/courseware/course/sidebar/utils/storage.js 82.75% 5 Missing ⚠️
...urseware/course/sidebar/SidebarContextProvider.jsx 88.57% 4 Missing ⚠️
...urseware/course/sidebar/hooks/useInitialSidebar.js 81.81% 2 Missing and 2 partials ⚠️
... and 8 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1885      +/-   ##
==========================================
- Coverage   91.13%   88.74%   -2.39%     
==========================================
  Files         349      343       -6     
  Lines        5807     5747      -60     
  Branches     1376     1374       -2     
==========================================
- Hits         5292     5100     -192     
- Misses        496      613     +117     
- Partials       19       34      +15     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@awais-ansari
Copy link
Copy Markdown
Contributor Author

@brian-smith-tcril Please review this PR.

@awais-ansari awais-ansari marked this pull request as ready for review March 26, 2026 20:35
Copy link
Copy Markdown
Contributor

@arbrandes arbrandes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First off: this is amazingly well documented. Congrats, and thank you! I'm probably going to start using this PR as a benchmark for how complicated frontend architectural decisions should be implemented.

Second: this is mostly an architecture review based on the ADR, ARCHITECTURE.md,and a glance at USE_CASE_VERIFICATION.md. I have not looked closely at the code, yet.

Lastly, I'm coming at this "from the future". Prior to the Willow cutoff in October of this year, frontend-app-learning will be converted to frontend-base, and this is one of the situations where the new slot architecture will help (or, at least, affect). Some of the inline comments are just FYIs related to that, and a couple are meant to help with the transition later. Either way, we should keep this in mind.

@@ -0,0 +1,35 @@
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things:

  1. Let's just call this directory widgets, instead of external-widgets. It's what frontend-base apps are doing.
  2. I suggest avoiding having this as a separate package while in the repo, if for no other reason than that our Github repos not being properly tooled to be monorepos. In other words, let's drop the package.json.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: avoiding separate package

This decoupling reminds me of what we did with https://github.com/openedx/frontend-app-learning/tree/master/src/plugin-slots/SequenceNavigationSlot and https://github.com/openedx/frontend-app-learning/tree/master/src/plugin-slots/CourseBreadcrumbsSlot. We kept the old components around, but instead of directly importing/using them we provided example FPF configs showing how to import and use them.

// See https://stackoverflow.com/questions/72382316/jest-encountered-an-unexpected-token-react-markdown
'react-markdown': '<rootDir>/node_modules/react-markdown/react-markdown.min.js',
'@src/(.*)': '<rootDir>/src/$1',
'@edx/learning-mfe-widget': '<rootDir>/src/sidebar-widget-sdk.js',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added boilerplate is another reason not to attempt a monorepo structure.

Comment on lines +2 to +3
UPGRADE: 20,
DISCUSSIONS: 10,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These priorities would ideally be defaults that the widgets themselves set.

Another way to read it is: if the sidebar is going to be truly generic, it should not know anything in advance about the Upgrade widget (or discussions, for that matter).

Comment on lines +14 to +15
* The upgrade widget is no longer built-in — configure it via SIDEBAR_WIDGETS
* in env.config.jsx using the @edx/learning-upgrade-widget package.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: let's not even mention other widgets outside their own scope in the codebase.

* @param {Object} context - Contains courseId, unitId, topic, courseHomeMeta
* @returns {boolean} Whether discussions are available
*/
export const discussionsIsAvailable = ({ topic }) => !!(topic?.id && topic?.enabledInContext);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to have this widget-specific code moved to its own scope (i.e., its own directory) in the codebase?

Comment on lines +95 to +97
| Discussions available | `'DISCUSSIONS'` | `'DISCUSSIONS'` | Discussions opens |
| Only Upgrade | `'UPGRADE'` | `'UPGRADE'` | Upgrade opens |
| Neither available | `null` | `'COURSE_OUTLINE'` | Course Outline opens (fallback) |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this more generic. For example, instead of Discussions or Upgrade, refer to generic widgets and priorities, and how they interact. Alternatively, keep Discussions and Upgrade, but specify their default priorities, instead of assuming the reader knows.

Comment on lines +107 to +110
**Scenario 1: Previous unit had DISCUSSIONS open**
- New unit has discussions → **Stays on Discussions**
- New unit NO discussions, YES upgrade → **Switches to Upgrade**
- New unit NO discussions, NO upgrade → **Closes right sidebar, Course Outline auto-opens**
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this logic hard-coded upstream, or is it emergent behavior based on the widget's self-reported priorities and other properties?

My point, also made in other comments, is that we should never have a hard-coded if (UPGRADE) outside the scope of the Upgrade widget itself.

- **Location**: Right side of screen
- **Component**: `Sidebar` (rendered via `RightSidebarSlot`)
- **Purpose**: Contextual tools and information panels
- **State Management**: `SidebarContextProvider` + widget registry system
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I very much like the registry idea, but my first question when seeing it was: why not offer the same registry for the left sidebar?

Then I remembered that this is exactly one of the things we tried to address with frontend-base. I suspect that during the conversion the registry will be made obsolete - or just much thinner - due to the frontend-base provider hoisting and slot layout systems.

In other words: never mind, for now. Just be aware that this is one of the configuration APIs that will change after the conversion.

- i18n message keys (`notification.*`)


### 3. External package `@edx/learning-upgrade-widget` is created
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned elsewhere, let's just make this a subdirectory of widgets/: no need to have a separate package (with all the monorepo headaches it brings) until it is in a different repository.

2. Set `ENABLE_LEGACY_UPGRADE_WIDGET: false`
3. Add the package widget to `SIDEBAR_WIDGETS` in `env.config.jsx`

The package skeleton lives in `src/external-widgets/learning-upsell-widget/` until promoted to its own repository.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If my suggestion from another comment is accepted, we should add:

### 3. Move the Discussions widget code into its own directory

It would be a sibling to the Upgrade widget.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants