Skip to content

Commit c0966a5

Browse files
authored
feat: make widget registry backward compatible (#1899)
1 parent 59113b0 commit c0966a5

9 files changed

Lines changed: 237 additions & 8 deletions

File tree

docs/decisions/0010-upgrade-widget-extraction.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,151 @@ import upgradeWidget from './src/widgets/upgrade/src/index';
3636

3737
export const SIDEBAR_WIDGETS = [upgradeWidget];
3838
```
39+
40+
---
41+
42+
## Migration & Backward Compatibility
43+
44+
The following section documents every renamed identifier, the backward-compatibility shims that were added, and what operators and plugin authors need to know. This inventory is structured so that a future OEP-21 DEPR filer can lift it directly.
45+
46+
### Deprecated Identifiers
47+
48+
| Deprecated | Replacement | Deprecated since | Planned removal |
49+
|-----------|-------------|-----------------|-----------------|
50+
| Plugin slot `org.openedx.frontend.learning.notification_tray.v1` (and `notification_tray_slot`) | `org.openedx.frontend.learning.upgrade_panel.v1` | PR #1899 | TBD — one full deprecation cycle |
51+
| Plugin slot `org.openedx.frontend.learning.notifications_discussions_sidebar.v1` (and `notifications_discussions_sidebar_slot`) | `org.openedx.frontend.learning.right_sidebar.v1` / `right_sidebar_slot` | PR #1899 | TBD |
52+
| Plugin slot `org.openedx.frontend.learning.notifications_discussions_sidebar_trigger.v1` (and `notifications_discussions_sidebar_trigger_slot`) | `org.openedx.frontend.learning.right_sidebar_trigger.v1` / `right_sidebar_trigger_slot` | PR #1899 | TBD |
53+
| `pluginProps` keys `notificationCurrentState` / `setNotificationCurrentState` on the upgrade panel slot | `upgradeCurrentState` / `setUpgradeCurrentState` | PR #1899 | TBD |
54+
| `WIDGETS.NOTIFICATIONS` (value changed from `'NOTIFICATIONS'` to `'UPGRADE'`) | `ID` exported from `@src/widgets/upgrade/src` | PR #1899 | TBD |
55+
56+
### Default-Behavior Change
57+
58+
`upgradeWidgetConfig` is currently included in `DEFAULT_WIDGETS`. A future release will remove it, after which operators who want the Upgrade panel visible must add it to `SIDEBAR_WIDGETS` in `env.config.jsx`. Target release: TBD.
59+
60+
---
61+
62+
### 1. Plugin Slot IDs
63+
64+
#### Sidebar panel slot
65+
66+
| Old ID (deprecated) | New ID |
67+
|---------------------|--------|
68+
| `org.openedx.frontend.learning.notification_tray.v1` | `org.openedx.frontend.learning.upgrade_panel.v1` |
69+
| `notification_tray_slot` _(short alias)_ |_(use the full ID)_ |
70+
71+
Both old IDs are preserved as `idAliases` on the new slot — **existing plugin configs
72+
continue to work without changes**. They are deprecated and will be removed in a future
73+
major version.
74+
75+
#### Right sidebar slot
76+
77+
| Old ID (deprecated) | New ID |
78+
|---------------------|--------|
79+
| `org.openedx.frontend.learning.notifications_discussions_sidebar.v1` | `org.openedx.frontend.learning.right_sidebar.v1` |
80+
| `notifications_discussions_sidebar_slot` _(short alias)_ | `right_sidebar_slot` _(short alias)_ |
81+
82+
#### Right sidebar trigger slot
83+
84+
| Old ID (deprecated) | New ID |
85+
|---------------------|--------|
86+
| `org.openedx.frontend.learning.notifications_discussions_sidebar_trigger.v1` | `org.openedx.frontend.learning.right_sidebar_trigger.v1` |
87+
| `notifications_discussions_sidebar_trigger_slot` _(short alias)_ | `right_sidebar_trigger_slot` _(short alias)_ |
88+
89+
---
90+
91+
### 2. Plugin Props — Upgrade Panel Slot
92+
93+
Plugins injected into the upgrade panel slot received props renamed in ADR 0010.
94+
Both old and new names are available as `pluginProps` for backward compatibility:
95+
96+
| Old prop _(deprecated)_ | New prop |
97+
|--------------------------|----------|
98+
| `notificationCurrentState` | `upgradeCurrentState` |
99+
| `setNotificationCurrentState` | `setUpgradeCurrentState` |
100+
101+
Update your plugin to use the new prop names. The old aliases will be removed in a future
102+
major version.
103+
104+
---
105+
106+
### 3. JavaScript Constants
107+
108+
`WIDGETS.NOTIFICATIONS` previously held the value `'NOTIFICATIONS'` to match the old widget ID.
109+
After ADR 0010 it holds `'UPGRADE'`, so existing comparisons like
110+
`currentSidebar === WIDGETS.NOTIFICATIONS` continue to resolve correctly against the
111+
renamed widget. The constant itself is **deprecated** — update new code to use the widget's
112+
own `ID` constant:
113+
114+
```js
115+
// Before ADR 0010
116+
import { WIDGETS } from '@src/constants';
117+
WIDGETS.NOTIFICATIONS // === 'NOTIFICATIONS'
118+
119+
// After ADR 0010 (current)
120+
WIDGETS.NOTIFICATIONS // === 'UPGRADE' — deprecated alias, value changed
121+
122+
// Recommended for new code
123+
import { ID as UPGRADE_WIDGET_ID } from '@src/widgets/upgrade/src/UpgradeTrigger';
124+
// or
125+
import { ID } from '@src/widgets/upgrade/src';
126+
127+
if (currentSidebar === ID) { /* ... */ }
128+
```
129+
130+
---
131+
132+
### 4. Context API Renames
133+
134+
If your code consumed the old React context properties directly (e.g. via a custom hook
135+
wrapping `SidebarContext` or `UpgradeWidgetContext`), update the following names:
136+
137+
| Old name _(deprecated)_ | New name |
138+
|--------------------------|----------|
139+
| `notificationStatus` | `upgradeWidgetStatus` |
140+
| `setNotificationStatus` | `setUpgradeWidgetStatus` |
141+
| `onNotificationSeen` | `onUpgradeWidgetSeen` |
142+
| `notificationCurrentState` | `upgradeCurrentState` |
143+
| `setNotificationCurrentState` | `setUpgradeCurrentState` |
144+
145+
Access upgrade widget context via:
146+
147+
```js
148+
import { useUpgradeWidgetContext } from '@src/widgets/upgrade/src';
149+
150+
const { upgradeWidgetStatus, onUpgradeWidgetSeen, upgradeCurrentState } = useUpgradeWidgetContext();
151+
```
152+
153+
---
154+
155+
### 5. localStorage Keys
156+
157+
The persistent status keys for the upgrade widget were renamed. Old keys stored in a
158+
learner's browser are **not** migrated automatically — the first page load after upgrading
159+
will reset the relevant state until the learner interacts with the panel again.
160+
161+
| Old key _(deprecated)_ | New key |
162+
|------------------------|---------|
163+
| `notificationStatus.${courseId}` | `upgradeWidget.${courseId}` |
164+
| `upgradeNotificationLastSeen.${courseId}` | `upgradeWidgetLastSeen.${courseId}` |
165+
| `upgradeNotificationCurrentState.${courseId}` | `upgradeWidgetState.${courseId}` |
166+
167+
---
168+
169+
### 6. `env.config.jsx` Configuration
170+
171+
- **Today:** You do not need to configure the Upgrade widget — it is included by default. If you add it to `SIDEBAR_WIDGETS`, the platform will ignore the duplicate and only show the default.
172+
- **In the future:** You will need to add it to `SIDEBAR_WIDGETS` to keep the Upgrade panel visible.
173+
- You can safely add `upgradeWidgetConfig` to `SIDEBAR_WIDGETS` now to future-proof your config. It will only take effect once the default is removed.
174+
175+
**To enable the Upgrade panel (future-proof):**
176+
177+
```js
178+
import { upgradeWidgetConfig } from './src/widgets/upgrade/src';
179+
export default {
180+
SIDEBAR_WIDGETS: [upgradeWidgetConfig],
181+
};
182+
```
183+
184+
**To disable the Upgrade panel (once it is no longer a default):**
185+
186+
Simply do not include `upgradeWidgetConfig` in your `SIDEBAR_WIDGETS`.

src/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ export const ALLOW_UPSELL_MODES = [
6565
export const WIDGETS = {
6666
DISCUSSIONS: 'DISCUSSIONS',
6767
COURSE_OUTLINE: 'COURSE_OUTLINE',
68+
/**
69+
* @deprecated since ADR 0010. The widget was renamed from 'Notifications' to 'Upgrade'.
70+
* This alias maps to the current widget ID 'UPGRADE' so that legacy checks
71+
* `currentSidebar === WIDGETS.NOTIFICATIONS` continue to resolve correctly.
72+
* Use the upgrade widget's `ID` constant directly for new code.
73+
*/
74+
NOTIFICATIONS: 'UPGRADE',
6875
} as const satisfies Readonly<{ [k: string]: string }>;
6976

7077
export const LOADING = 'loading';

src/courseware/course/sidebar/SidebarTriggers.test.jsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ jest.mock('@src/widgets/discussions/widgetConfig', () => ({
2525
},
2626
}));
2727

28+
jest.mock('@src/widgets/upgrade/src/widgetConfig', () => ({
29+
upgradeWidgetConfig: {
30+
id: 'UPGRADE',
31+
priority: 20,
32+
Sidebar: () => null,
33+
Trigger: () => <button type="button" data-testid="trigger-UPGRADE">Upgrade</button>,
34+
isAvailable: jest.fn(),
35+
enabled: true,
36+
},
37+
}));
38+
2839
const mockToggleSidebar = jest.fn();
2940
// eslint-disable-next-line react/prop-types
3041
const MockTriggerA = ({ onClick }) => (

src/courseware/course/sidebar/defaultWidgets.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
11
import { getConfig } from '@edx/frontend-platform';
22
import { discussionsWidgetConfig } from '@src/widgets/discussions/widgetConfig';
3+
import { upgradeWidgetConfig } from '@src/widgets/upgrade/src/widgetConfig';
34
import { WIDGET_PRIORITIES, WIDGET_CONFIG } from './constants';
45

56
/**
67
* Default built-in widgets for the RIGHT sidebar.
78
*/
89
export const DEFAULT_WIDGETS = [
910
discussionsWidgetConfig,
11+
upgradeWidgetConfig,
1012
];
1113

1214
/**
1315
* Get all enabled widgets (built-in + configured external widgets)
1416
* @returns {Array} Array of widget configurations
1517
*/
1618
export function getEnabledWidgets() {
17-
const widgets = [...DEFAULT_WIDGETS].filter(widget => widget.enabled);
19+
const externalWidgets = getConfig()[WIDGET_CONFIG.EXTERNAL_WIDGETS_KEY] || [];
20+
const disabledIds = new Set(
21+
externalWidgets.filter(w => w.enabled === false).map(w => w.id),
22+
);
23+
const defaultIds = new Set(DEFAULT_WIDGETS.map(w => w.id));
24+
25+
const widgets = [...DEFAULT_WIDGETS].filter(
26+
widget => widget.enabled && !disabledIds.has(widget.id),
27+
);
1828

19-
// Add external widgets from config if any; respect their enabled flag same as built-ins
20-
const externalWidgets = (getConfig()[WIDGET_CONFIG.EXTERNAL_WIDGETS_KEY] || []).filter(w => w.enabled !== false);
21-
widgets.push(...externalWidgets);
29+
widgets.push(
30+
...externalWidgets.filter(
31+
w => w.enabled !== false && !defaultIds.has(w.id),
32+
),
33+
);
2234

23-
// Sort by priority (lower number = higher priority)
2435
return widgets.sort(
2536
(a, b) => (a.priority || WIDGET_PRIORITIES.DEFAULT)
2637
- (b.priority || WIDGET_PRIORITIES.DEFAULT),

src/courseware/course/sidebar/defaultWidgets.test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,36 @@ describe('defaultWidgets', () => {
114114
getConfig.mockReturnValue({});
115115
expect(() => getEnabledWidgets()).not.toThrow();
116116
});
117+
118+
it('gives priority to platform default widgets over external widgets with the same id', () => {
119+
// Simulate a platform default and an external widget with the same id
120+
getConfig.mockReturnValue({
121+
SIDEBAR_WIDGETS: [{
122+
id: 'DISCUSSIONS',
123+
priority: 99,
124+
Sidebar: () => 'External',
125+
Trigger: () => null,
126+
enabled: true,
127+
}],
128+
});
129+
const widgets = getEnabledWidgets();
130+
// Only the platform default should be present
131+
expect(widgets.filter(w => w.id === 'DISCUSSIONS')).toHaveLength(1);
132+
// It should be the platform default, not the external one
133+
expect(widgets.find(w => w.id === 'DISCUSSIONS').priority).toBe(10);
134+
});
135+
136+
it('removes a platform default if an external widget with the same id sets enabled: false', () => {
137+
getConfig.mockReturnValue({
138+
SIDEBAR_WIDGETS: [{
139+
id: 'DISCUSSIONS',
140+
enabled: false,
141+
}],
142+
});
143+
const widgets = getEnabledWidgets();
144+
// The default should be removed
145+
expect(widgets.some(w => w.id === 'DISCUSSIONS')).toBe(false);
146+
});
117147
});
118148

119149
describe('buildSidebarsRegistry', () => {

src/plugin-slots/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
* [`org.openedx.frontend.learning.gated_unit_content_message.v1`](./GatedUnitContentMessageSlot/)
1414
* [`org.openedx.frontend.learning.learner_tools.v1`](./LearnerToolsSlot/)
1515
* [`org.openedx.frontend.learning.next_unit_top_nav_trigger.v1`](./NextUnitTopNavTriggerSlot/)
16-
* [`org.openedx.frontend.learning.notification_tray.v1`](./NotificationTraySlot/)
16+
* ~~`org.openedx.frontend.learning.notification_tray.v1`~~ _(deprecated / aliased to `upgrade_panel.v1` — see [ADR 0010](../../docs/decisions/0010-upgrade-widget-extraction.md))_
1717
* [`org.openedx.frontend.learning.right_sidebar_trigger.v1`](./RightSidebarTriggerSlot/)
1818
* [`org.openedx.frontend.learning.right_sidebar.v1`](./RightSidebarSlot/)
19+
* [`org.openedx.frontend.learning.upgrade_panel.v1`](../widgets/upgrade/) _(upgrade / upsell panel — see [ADR 0010](../../docs/decisions/0010-upgrade-widget-extraction.md))_
1920
* [`org.openedx.frontend.learning.progress_certificate_status.v1`](./ProgressCertificateStatusSlot/)
2021
* [`org.openedx.frontend.learning.progress_tab_certificate_status_main_body.v1`](./ProgressTabCertificateStatusMainBodySlot/)
2122
* [`org.openedx.frontend.learning.progress_tab_certificate_status_side_panel.v1`](./ProgressTabCertificateStatusSidePanelSlot/)

src/plugin-slots/RightSidebarSlot/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import Sidebar from '../../courseware/course/sidebar/Sidebar';
77
export const RightSidebarSlot : React.FC = () => (
88
<PluginSlot
99
id="org.openedx.frontend.learning.right_sidebar.v1"
10-
idAliases={['right_sidebar_slot', 'notifications_discussions_sidebar_slot']}
10+
idAliases={[
11+
'right_sidebar_slot',
12+
// @deprecated — aliased for backward compat; remove after one deprecation cycle (ADR 0010)
13+
'notifications_discussions_sidebar_slot',
14+
// @deprecated — aliased for backward compat; remove after one deprecation cycle (ADR 0010)
15+
'org.openedx.frontend.learning.notifications_discussions_sidebar.v1',
16+
]}
1117
slotOptions={{
1218
mergeProps: true,
1319
}}

src/plugin-slots/RightSidebarTriggerSlot/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import SidebarTriggers from '../../courseware/course/sidebar/SidebarTriggers';
77
export const RightSidebarTriggerSlot : React.FC = () => (
88
<PluginSlot
99
id="org.openedx.frontend.learning.right_sidebar_trigger.v1"
10-
idAliases={['right_sidebar_trigger_slot', 'notifications_discussions_sidebar_trigger_slot']}
10+
idAliases={[
11+
'right_sidebar_trigger_slot',
12+
// @deprecated — aliased for backward compat; remove after one deprecation cycle (ADR 0010)
13+
'notifications_discussions_sidebar_trigger_slot',
14+
// @deprecated — aliased for backward compat; remove after one deprecation cycle (ADR 0010)
15+
'org.openedx.frontend.learning.notifications_discussions_sidebar_trigger.v1',
16+
]}
1117
slotOptions={{
1218
mergeProps: true,
1319
}}

src/widgets/upgrade/src/UpgradePanel.jsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,20 @@ const UpgradePanel = () => {
8888
{verifiedMode ? (
8989
<PluginSlot
9090
id="org.openedx.frontend.learning.upgrade_panel.v1"
91+
idAliases={[
92+
// @deprecated — aliased for backward compat; remove after one deprecation cycle (ADR 0010)
93+
'org.openedx.frontend.learning.notification_tray.v1',
94+
// @deprecated — aliased for backward compat; remove after one deprecation cycle (ADR 0010)
95+
'notification_tray_slot',
96+
]}
9197
pluginProps={{
9298
courseId,
9399
model: 'coursewareMeta',
94100
upgradeCurrentState,
95101
setUpgradeCurrentState,
102+
// @deprecated — prop aliases for backward compat; remove after one deprecation cycle (ADR 0010)
103+
notificationCurrentState: upgradeCurrentState,
104+
setNotificationCurrentState: setUpgradeCurrentState,
96105
}}
97106
/>
98107
) : (

0 commit comments

Comments
 (0)