Skip to content

feat!: migrate to frontend-base#107

Draft
arbrandes wants to merge 23 commits intomainfrom
frontend-base
Draft

feat!: migrate to frontend-base#107
arbrandes wants to merge 23 commits intomainfrom
frontend-base

Conversation

@arbrandes
Copy link
Copy Markdown
Contributor

@arbrandes arbrandes commented Apr 20, 2026

Description

This branch renames the package from @edx/frontend-plugin-notifications to @openedx/frontend-app-notifications and migrates it end-to-end to @openedx/frontend-base. The three legacy plugin slots collapse into the unified header's desktopRight / mobileRight slots, so Studio and learning pages pick up the notifications bell without extra wiring.

Alongside the rename the source is fully converted to TypeScript, the data layer is ported to @tanstack/react-query v5 (no more bespoke context-based fetching), the account-settings link is resolved via the new getUrlByRouteRole mechanism, and a self-contained dev app under dev/ ships stateful mocks so the bell can be exercised without a live LMS.

Semantic-release is configured to publish alphas from this branch as @openedx/frontend-app-notifications@3.0.0-alpha.x, keeping main free to continue shipping @edx/frontend-plugin-notifications@2.x for consumers that haven't migrated off frontend-platform yet. The PR is kept in draft while the new package lands in downstream sites and we gather feedback.

BREAKING CHANGE: This won't work with legacy MFEs after this.

Testing

In dev mode, this mocks most API calls so that notifications can be seen without actually having any real ones. Start the LMS, run npm run dev and go to:

http://apps.local.openedx.io:1992/notifications

Click around the notifications in the top right:

image

You should also see the example "tour" on the left:

image

LLM usage notice

Built with assistance from Claude.

Migration plan (historical artifact)

Migrate frontend-plugin-notifications -> @openedx/frontend-app-notifications

Context

frontend-plugin-notifications (published as @edx/frontend-plugin-notifications) is currently a peer-dependency library consumed by Open edX MFEs via @openedx/frontend-plugin-framework. It exports a NotificationsTray component, which tutor-contrib-platform-notifications injects into three legacy slots (header_desktop_secondary_menu.v1, header_learning_help.v1, studio_header_search_button_slot.v1) through PLUGIN_SLOTS.

frontend-base replaces frontend-plugin-framework with a new runtime. Plugins are now App configs that register widgets against frontend-base slot IDs, and the three legacy slot IDs no longer exist - the new unified header uses org.openedx.frontend.slot.header.desktopRight.v1 and .mobileRight.v1. This plan converts the repo end-to-end following frontend-base/docs/how_tos/migrate-frontend-app.md, renames the package to @openedx/frontend-app-notifications, ports the data layer to @tanstack/react-query v5, and wires the notification bell into the new header via an exported App config. The matching tutor-contrib-platform-notifications update is tracked separately and is out of scope for this plan.

Decisions locked in (from pre-plan review)

  • Package name: rename from @edx/frontend-plugin-notifications to @openedx/frontend-app-notifications (both the org prefix and plugin -> app). The repo directory name stays as-is.
  • Header placement: desktop/mobile right slots only. No course-navigation extraContent wiring.
  • Data layer: port src/Notification/data/hook.js to @tanstack/react-query v5. Remove any <QueryClientProvider /> - the shell already provides one.
  • Public API: keep named exports (NotificationsTray, Notifications, useAppNotifications, useNotification) alongside the default App config export.

Each phase below is runnable standalone: it lists its inputs, actions, and acceptance checks so that a fresh agent (or the user) can pick it up without prior conversation context.


Phase 0 - Preconditions and peer repos

We install @openedx/frontend-base from npm - no local build required. The sibling checkouts are reference material only:

  • ~/src/openedx/master/frontend-base - read-only reference (slot IDs, App shape, migration doc at docs/how_tos/migrate-frontend-app.md, example config at test-site/src/example-page/). Do not build.
  • ~/src/openedx/master/tutor-contrib-platform-notifications - reference for the legacy slot wiring. The matching Tutor-plugin update is tracked separately and out of scope here.

Run this repo's work from ~/src/openedx/master/frontend-plugin-notifications (it should be the current working directory).

Reference app for "what a converted plugin looks like": ~/src/openedx/master/frontend-app-learner-dashboard, checked out on branch frontend-base.


Phase 1 - package.json rewrite [DONE - commit a196b02]

Files: package.json.

  1. Uninstall: @edx/frontend-platform, @openedx/frontend-build, @edx/reactifex, husky, glob (keep @openedx/frontend-plugin-framework removal in mind if present; this repo only documents it in README - verify and remove any usage).
  2. Delete package-lock.json and node_modules/.
  3. Move runtime deps shared with the shell to peerDependencies (broadened semver):
    • @openedx/paragon: ^23
    • @tanstack/react-query: ^5 (introduced by the Phase 4.5 data-layer port)
    • react: ^18, react-dom: ^18
    • react-router: ^6, react-router-dom: ^6
  4. Add @openedx/frontend-base to peer deps with the dev sentinel: "@openedx/frontend-base": "^1.0.0-alpha || 0.0.0-dev" (only alpha tags are published on npm as of 2026-04; match the converted reference app).
  5. Replace scripts with the standard block from the migration doc (build/clean/dev/i18n_extract/lint/lint:fix/prepack/snapshot/test). Use PORT=1992 PUBLIC_PATH=/notifications for dev (the legacy 2002 collides with another local MFE).
  6. Dev deps to add: tsc-alias, turbo, nodemon, plus any TS types the source ends up needing.
  7. Fields to set/update:
    • name: rename to @openedx/frontend-app-notifications. Set version to 0.0.0-dev (the convention for frontend-base apps - semantic-release replaces it at publish time).
    • author: "Open edX", license: AGPL-3.0.
    • exports: { ".": "./dist/index.js", "./app.scss": "./dist/app.scss" } (add the scss export only if an app.scss ends up being shipped - likely not for a plugin-only library).
    • files: ["/dist"].
    • sideEffects: ["*.css", "*.scss"].
    • workspaces: ["packages/*"].
    • atlasTranslations: { path, dependencies: ["@openedx/frontend-base"] } (see Phase 7).
  8. Run npm install fresh.

Acceptance: npm install succeeds with no peer-dep errors; package.json contains no references to frontend-build, frontend-platform, or fedx-scripts.


Phase 2 - Build/tool config files [DONE - commit e812b0d]

Files: tsconfig.json, tsconfig.build.json, Makefile, app.d.ts, babel.config.js, jest.config.js, eslint.config.js, .gitignore, .npmignore, delete .eslintrc.js/.eslintignore/module.config.js.example/.env*.

  1. tsconfig.json extending @openedx/frontend-base/tools/tsconfig.json, with @src/* path alias and include list from the doc.
  2. tsconfig.build.json extending ./tsconfig.json, rootDir src, outDir dist, excluding tests/mocks/setupTest.js.
  3. Root app.d.ts with the /// <reference types="@openedx/frontend-base" /> triple-slash directive plus site.config and *.svg module declarations.
  4. babel.config.js: createConfig('babel') from @openedx/frontend-base/tools.
  5. jest.config.js: createConfig('test', {...}) with setupFilesAfterEnv: ['<rootDir>/src/setupTest.js'], coverage ignore for src/setupTest.js and src/i18n, and moduleNameMapper for \\.svg$, \\.png$, ^@src/(.*)$.
  6. eslint.config.js: createLintConfig({ files: ['src/**/*', 'site.config.*'] }) from @openedx/frontend-base/tools.
  7. Makefile: replace with clean and build targets exactly as in the migration doc (tsc -> copy scss/assets -> tsc-alias). Keep the existing pull_translations / extract_translations / i18n.* targets but update them to use openedx translations:pull (Phase 7). Add bin-link, build-packages, clean-packages, dev-packages, dev-site targets from the doc.
  8. .gitignore: adopt the standard block from the doc (adds packages/, /.turbo, /turbo.json, /*.tgz, src/i18n/transifex_input.json, src/i18n/messages.ts, src/i18n/messages/).
  9. .npmignore: reduce to node_modules.
  10. Delete .env, .env.development, .env.test (if present), module.config.js.example, .eslintrc.js, .eslintignore.

Acceptance: npm run lint and make clean && make build succeed and produce dist/.


Phase 3 - Convert source imports and modernize structure [DONE - commit 369aa20]

Files under src/.

  1. Replace every @edx/frontend-platform* import with @openedx/frontend-base. In particular:
    • getConfig -> getSiteConfig (audit each call site for shape differences).
    • getAuthenticatedHttpClient, snakeCaseObject, camelCaseObject, AppContext, useIntl, defineMessages, FormattedMessage, initializeMockApp -> all from @openedx/frontend-base.
  2. If AppProvider is used anywhere, rename to SiteProvider. (Unlikely in this repo - double-check.)
  3. Convert @import to @use in any SCSS file. Audit common/style.scss and Notification/notification.scss for Paragon variable imports and migrate to Paragon 23 CSS variables where any SCSS variable is referenced.
  4. Rename src/plugin-slots (if it ever appears) to src/slots. This repo currently has none; skip unless later work introduces them.
  5. Process.env: search for any process.env.* references and move them to config (or hard-code constants where appropriate).
  6. Do the wholesale .js/.jsx to .ts/.tsx conversion in Phase 3.5; Phase 3 stops at import/SCSS/env rewrites so the TS conversion lands on a stable base.

Acceptance: rg "frontend-platform|frontend-build|fedx-scripts" src returns nothing; jest tests still pass (re-run after Phase 3.5).


Phase 3.5 - Convert source to TypeScript [DONE - commit e32d3ce]

The repo is only ~30 source files (18 .js + 12 .jsx), small enough to convert wholesale and end up with a fully-typed library. Doing it before Phase 4 means the new entry files land in an already-TS tree, and Phase 4.5's hook.ts port is natural rather than an optional rename.

Files: everything under src/.

  1. Rename .js -> .ts and .jsx -> .tsx across src/. Use git mv to preserve blame.
  2. Add a strict-enough tsconfig.json (Phase 2 already stages this) and run npx tsc --noEmit to drive the conversion. Fix errors iteratively rather than silencing with any.
  3. Type the API surfaces that have clear boundaries:
    • Notification/data/api.js -> add response interfaces for getNotificationCounts, getNotificationsList, markNotificationsAsSeen, markNotificationRead.
    • Notification/data/hook.js -> type the hook return shapes (these will be replaced by react-query types in Phase 4.5, but an interim typing catches regressions).
    • Component props: convert PropTypes to TS interfaces. Drop the prop-types import once a component is fully typed; remove the prop-types peer dep from package.json when no usages remain.
    • Context values: NotificationContext and any other contexts get an explicit Context<T> type.
  4. src/Notification/tours/* tour step configs: add a union/enum type for tour step IDs so Phase 4 wiring catches typos.
  5. Test files: convert too. jest.mock(...) blocks typically need jest.requireActual<typeof import('...')>(...) spreads to preserve typing.
  6. For unavoidable anys (e.g., legacy untyped third-party surfaces), prefer unknown + narrowing, or a named alias like type TodoTyped = any with a // TODO so they're greppable.
  7. Leave SCSS/JSON assets alone.

Acceptance: rg -l "\.jsx?$" src returns nothing; npx tsc --noEmit passes; npm test passes; prop-types is removed from package.json if no component still uses it.


Phase 4 - New library entry layout [DONE - commit 7c47255]

Create the standard frontend-base library surface:

  • src/constants.ts: export const appId = 'org.openedx.frontend.notifications'; (finalize ID with user in Phase 11).
  • src/app.ts: the exported App config (populated in Phase 9 with slot wiring). Minimal stub now.
  • src/providers.ts: empty/no-op unless the existing contexts need to be promoted to app-level providers. The current code uses two React Contexts internally - leave them co-located with Notification/.
  • src/routes.tsx: optional; this plugin does not own routes, so this file may be omitted.
  • src/slots.tsx: the list of SlotOperation entries this app contributes (populated in Phase 9).
  • src/index.ts: re-exports only. Export the App config as default plus named exports for NotificationsTray, Notifications, useAppNotifications, useNotification so existing consumers keep working.
  • Rename src/index.tsx -> src/NotificationsTray.tsx (or similar, post-Phase 3.5) and have the new src/index.ts re-export it. Keep the <StrictMode> wrapper behavior.
  • src/setupTest.ts: add mergeSiteConfig(siteConfig) per the doc so getSiteConfig() works inside tests.

Acceptance: npm run build produces a dist/ whose index.js exports the App config plus the public components/hooks; npm test still passes.


Phase 4.5 - Port data layer to @tanstack/react-query v5 [DONE - commit 9b36be7]

Files (post-Phase 3.5, all .ts/.tsx): src/Notification/data/hook.ts, src/Notification/data/api.ts, call sites in src/Notification/index.tsx, NotificationSections.tsx, NotificationTabs.tsx, tours/data/hooks.ts.

  1. Migration guide reference: https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5. Assume v5 API from the start (queryOptions, object-form useQuery({ queryKey, queryFn }), useMutation({ mutationFn }), isPending instead of isLoading for queries, etc.).
  2. Map the existing custom hooks to react-query:
    • useAppNotifications() -> useQuery against getNotificationCounts (and whatever seeds showNotificationsTray); key: ['notifications', 'appData'].
    • useNotification() splits into:
      • useNotificationList(appName, page) -> useQuery with key ['notifications', 'list', appName, page], calling getNotificationsList. Paginated; use placeholderData: keepPreviousData to keep the tray stable while paging.
      • useMarkNotificationSeen() -> useMutation calling markNotificationsAsSeen; on success invalidate ['notifications'].
      • useMarkNotificationRead() -> useMutation calling markNotificationRead (single-notification and per-app variants). On success, optimistically update the list cache for the affected notification id, then invalidate.
  3. Keep notificationsContext for UI-only state (active tab, popover open, scroll refs). Remove data fields (notifications array, pagination cursor) from the context - components should read them from react-query via the new hooks.
  4. Do NOT add a <QueryClientProvider /> here. The frontend-base shell provides one globally. Tests that mount components in isolation need to wrap with a QueryClientProvider in a test helper (add to setupTest.js or a renderWithProviders util).
  5. Update tests: replace axios-mock-adapter assertions where needed with react-query-aware patterns (either mock the API module or mock the HTTP layer and wrap in a fresh QueryClient per test with retry: false, gcTime: 0).
  6. Keep useAppNotifications / useNotification as the exported hook names so src/index.ts re-exports and the public API remain stable. Their internals change; signatures should stay compatible where feasible.
  7. Phase 2 carry-over: fix useTourConfiguration in tours/data/hooks.ts. It is declared async but calls useIntl/useMemo inside (hooks cannot be called from async functions). Phase 2 silenced it with // eslint-disable-next-line react-hooks/rules-of-hooks + a TODO: comment as a stopgap; the real fix is to drop the async (it awaits nothing) and collapse the inner useMemo-returning lambda into a plain hook body. Remove the disable comments and the TODO once fixed.

Acceptance: npm test passes, the tray still opens and paginates in npm run dev, rg "useState.*notifications|useState.*pagination" src shows state management has moved out of components for data fetching, and rg "TODO:.*Phase" src is clean for the 4.5 TODO.


Phase 5 - site.config files for tests and dev [DONE - commit ca48e34]

Files: site.config.test.tsx, site.config.dev.tsx.

  1. site.config.test.tsx: minimal SiteConfig per the doc, registering this plugin's App so the slot wiring is exercised. Use environment: 'test' as SiteConfig['environment'].
  2. site.config.dev.tsx: registers shellApp, headerApp, footerApp from @openedx/frontend-base, plus this plugin's App, plus a simple authenticated test page to see the bell live. Import ./app.scss at the top. Point auth URLs at http://local.openedx.io:8000.
  3. Create a tiny app.scss that @uses @openedx/frontend-base/shell/app.scss and any local styles from the plugin. This is only required to enable npm run dev; it is not shipped via exports.

Acceptance: npm run dev boots the shell and renders the bell in the authenticated header.


Phase 6 - Jest and test mocks [DONE - commit 3d7c6a7]

Files under src/__mocks__/, src/setupTest.ts, existing *.test.tsx files.

  1. Add src/__mocks__/svg.js (module.exports = 'SvgURL';) and src/__mocks__/file.js (module.exports = 'FileMock';). Only needed if tests import SVG/PNG; verify and skip mocks whose types aren't actually imported. Keep these as .js so Jest's module name mapper resolves them without TS transform.
  2. For any jest.mock('@edx/frontend-platform/...') calls in existing tests, replace with jest.mock('@openedx/frontend-base', () => ({ ...jest.requireActual<typeof import('@openedx/frontend-base')>('@openedx/frontend-base'), <mocked exports> })) as per the doc (typed form, since tests are TS after Phase 3.5).
  3. Add initializeMockApp + mergeSiteConfig calls to setupTest.ts if tests rely on getSiteConfig() (the notifications data layer reads config URLs; almost certainly needed).

Acceptance: npm test passes with coverage.


Phase 7 - i18n [DONE - commit 07562b9]

  1. Add description fields to every message in src/Notification/messages.js and src/Notification/tours/messages.js (required by the new ESLint config).
  2. Add "translations:pull": "openedx translations:pull" to package.json scripts.
  3. Add atlasTranslations block to package.json pointing at translations/frontend-app-notifications/src/i18n/messages with dependencies: ["@openedx/frontend-base"].
  4. Replace src/i18n/index.js with:
    • src/i18n/index.ts: export { default } from './messages';
    • src/i18n/messages.d.ts: import type { SiteMessages } from '@openedx/frontend-base'; declare const messages: SiteMessages; export default messages;
  5. Update the Makefile's pull_translations target to invoke npm run translations:pull -- --atlas-options="$(ATLAS_OPTIONS)".

Acceptance: npm run translations:pull generates src/i18n/messages.ts; tests still pass.


Phase 8 - Workspaces (turbo + nodemon) [DONE - commit 5f74319]

Files: turbo.site.json, nodemon.json, Makefile additions (already staged in Phase 2), .gitignore additions.

Copy the exact contents for turbo.site.json and nodemon.json from the migration doc. Confirm Makefile targets build-packages, clean-packages, dev-packages, dev-site, bin-link are present. The bin-link target references packages/frontend-base/package.json; leave as-is.

Acceptance: mkdir -p packages/frontend-base && sudo mount --bind ../frontend-base packages/frontend-base && npm install && npm run dev:packages starts the dev server with a live frontend-base.


Phase 9 - Wire NotificationsTray into the frontend-base header [DONE - commit 69a6eb1]

This is the functional replacement for the Tutor PLUGIN_SLOTS block.

Populate src/slots.tsx with two widget registrations:

import { WidgetOperationTypes } from '@openedx/frontend-base';
import NotificationsTray from './NotificationsTray';
import { appId } from './constants';

export const slots = [
  {
    slotId: 'org.openedx.frontend.slot.header.desktopRight.v1',
    id: `${appId}.widget.notificationsBell.desktop.v1`,
    op: WidgetOperationTypes.INSERT_BEFORE,
    relatedId: 'org.openedx.frontend.widget.header.desktopAuthenticatedMenu.v1',
    element: <NotificationsTray />,
    condition: { authenticated: true },
  },
  {
    slotId: 'org.openedx.frontend.slot.header.mobileRight.v1',
    id: `${appId}.widget.notificationsBell.mobile.v1`,
    op: WidgetOperationTypes.INSERT_BEFORE,
    relatedId: 'org.openedx.frontend.widget.header.mobileAuthenticatedMenu.v1',
    element: <NotificationsTray />,
    condition: { authenticated: true },
  },
];

Notes:

  • The three legacy slot IDs (header_desktop_secondary_menu.v1, header_learning_help.v1, studio_header_search_button_slot.v1) collapse into desktopRight.v1 + mobileRight.v1 because frontend-base has a single unified header with no Studio-specific or learning-specific slots. Studio and course pages inherit the same bell placement for free.
  • Verify the INSERT_BEFORE related IDs against the live frontend-base/shell/header/app.tsx at the time of execution (file:shell/header/app.tsx, lines 57-64 and 126-132 as of this planning). If an INSERT_BEFORE relative op is unsupported or the target ID moved, fall back to APPEND and order with widget priority.
  • NotificationsTray already handles its own popover via Paragon's OverlayTrigger - no additional wrapping required.

Populate src/app.ts:

import { App } from '@openedx/frontend-base';
import { appId } from './constants';
import { slots } from './slots';

const app: App = { appId, slots };
export default app;

And src/index.ts:

export { default } from './app';
export { default as NotificationsTray } from './NotificationsTray';
export { Notifications } from './Notification';
export { useAppNotifications, useNotification } from './Notification/data/hook';

Acceptance: In npm run dev, logging in renders a bell in the header right section on both desktop and mobile layouts; clicking it opens the notifications popover.


Phase 10 - Housekeeping on the rename [DONE - commits ccdedf3, c8a1ab2]

Because the npm package name is changing:

  1. Update README.rst header, install snippets, and any @edx/frontend-plugin-notifications string references to @openedx/frontend-app-notifications.
  2. Update catalog-info.yaml metadata if it references the old name.
  3. Update .github/workflows/release.yml / semantic-release config if the package name is hard-coded. Confirm the release token still has publish rights for the @openedx scope on npm; if not, flag to the user before the first release.
  4. Update atlasTranslations.path and any Transifex references to the new name/slug.
  5. Leave git history untouched; the repo directory and GitHub repo slug are unchanged for this migration.

Phase 11 - Optional follow-ups

Non-blocking improvements that surfaced during the main migration and can be picked up opportunistically. Each entry notes what was done as a stopgap, what the preferred shape is, and a rough acceptance check. Add new entries here when you hit something similar.

ACCOUNT_SETTINGS_URL -> route role [DONE - commit 875f1b1]

Phase 3.5 wired the account-settings link via useAppConfig().ACCOUNT_SETTINGS_URL, a direct port of the legacy env-driven config. The frontend-base convention for cross-app URLs is the route-role lookup: apps and external URLs alike are tagged with a role string, resolved via getUrlByRouteRole(role) which checks both apps[].routes[].handle.roles and externalRoutes. This is the right fit here because the notifications plugin should not care whether the account MFE is deployed as a sibling app or as an external site; only the role matters.

src/Notification/index.tsx now reads getUrlByRouteRole('org.openedx.frontend.role.account') and appends #notifications in-component (the hash is a notifications-UI concern, not an account-app concern). site.config.dev.tsx and site.config.test.tsx register the role under externalRoutes instead of stuffing ACCOUNT_SETTINGS_URL into per-app config. tutor-contrib-platform-notifications needs the matching switch to populate externalRoutes instead of the old env var (tracked in the separate Tutor-plugin update).

Acceptance: rg "ACCOUNT_SETTINGS_URL" src returns nothing; the settings icon still links correctly in dev.


Verification

Run from the repo root with @openedx/frontend-base installed from npm (no bind-mount required unless Phase 8 was done):

  1. npm install
  2. npm run lint
  3. npm test -- --no-coverage --maxWorkers=8
  4. make clean && make build (produces dist/)
  5. npm run dev (or npm run dev:packages if Phase 8 bind-mount is set up) and in a browser:
    • Log in; bell appears in desktop header right; clicking opens the popover with tabs/notifications.
    • Resize to mobile; bell appears in mobile header right.
    • Log out; bell disappears (authenticated condition).
  6. npm pack and install the tgz into a local site checkout to confirm the App default export and named component exports resolve correctly.

Critical files at a glance

  • package.json - Phase 1
  • tsconfig.json, tsconfig.build.json, Makefile, app.d.ts - Phase 2
  • babel.config.js, jest.config.js, eslint.config.js, .gitignore, .npmignore - Phase 2
  • src/**/*.{js,jsx} - import rewrites (Phase 3), then wholesale rename to .ts/.tsx with typing (Phase 3.5)
  • src/index.tsx -> split into src/NotificationsTray.tsx + src/index.ts + src/app.ts + src/slots.tsx + src/constants.ts - Phase 4/9
  • src/Notification/data/hook.ts - react-query port (Phase 4.5)
  • src/Notification/messages.ts, src/Notification/tours/messages.ts - add description (Phase 7)
  • src/i18n/index.ts + src/i18n/messages.d.ts - Phase 7
  • site.config.dev.tsx, site.config.test.tsx, src/app.scss - Phase 5
  • turbo.site.json, nodemon.json - Phase 8 (optional)

arbrandes and others added 18 commits April 20, 2026 10:43
Rename to @openedx/frontend-app-notifications, swap fedx-scripts for
the openedx CLI, move shell-shared runtime deps (paragon, react,
react-router, react-query) to peerDependencies with broadened semver,
add @openedx/frontend-base peer dep, and add tsc-alias, turbo, and
nodemon as dev deps.

Co-Authored-By: Claude <noreply@anthropic.com>
Add tsconfig.json, tsconfig.build.json, app.d.ts, and eslint.config.js;
switch babel, jest, and eslint configs to @openedx/frontend-base/tools;
replace Makefile with the doc-standard build plus workspace targets;
adopt standard .gitignore and minimal .npmignore; drop .env files,
.eslintrc.js, .eslintignore, and module.config.js.example.

Trivial source touch-ups to satisfy the new lint config: remove two
stale import/* disable comments, split two one-line reducers, and add
a TODO + react-hooks/rules-of-hooks stopgap in tours/data/hooks.js
pointing to Phase 4.5 for the real fix.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace all @edx/frontend-platform* imports with @openedx/frontend-base
(flat export surface); rename getConfig -> getSiteConfig; drop the
mergeConfig process.env shim from setupTest.js (Phase 6 replaces it
with mergeSiteConfig + site.config); rename config keys LMS_BASE_URL
-> lmsBaseUrl and ACCOUNT_SETTINGS_URL -> accountSettingsUrl (the
latter still needs wiring; TODO points to Phase 5/9); drop a dead
jest.mock('@src/generic/messages', ...) copy-paste artifact.

SCSS already used Paragon 23 CSS variables and had no @import
statements, so no style work was needed.

Co-Authored-By: Claude <noreply@anthropic.com>
Rename all .js/.jsx under src/ to .ts/.tsx with git mv, type API
boundaries and component props, replace PropTypes with TS interfaces,
and drop prop-types from peerDependencies. Swap the legacy AppContext
read for useAuthenticatedUser, and source ACCOUNT_SETTINGS_URL via
useAppConfig (Phase 11 tracks the ExternalRoute follow-up).

Also lands the minimum scaffolding needed for the test suite to run
under the new stack: site.config.test.tsx, src/__mocks__/{svg,file}.js,
and a mergeSiteConfig(siteConfig) call in setupTest.tsx. Phases 5 and 6
will own the canonical versions of these.

Co-Authored-By: Claude <noreply@anthropic.com>
Rename src/index.tsx to src/NotificationsTray.tsx and add
src/{constants,app,slots,index}.ts so the package now default-exports
a frontend-base App config while keeping the existing component and
hook named exports.

Co-Authored-By: Claude <noreply@anthropic.com>
…Phase 4.5)

Replace the custom-hook + fat-context data layer with react-query queries
and mutations:

- useAppNotifications is a useQuery over getNotificationCounts.
- useNotificationList is a useInfiniteQuery with keepPreviousData; pages
  are flattened for consumers.
- useMarkNotificationSeen / useMarkNotificationRead /
  useMarkAllNotificationsRead are mutations that optimistically patch the
  list cache and invalidate the counts query.
- useNotification is kept as a backwards-compatible facade exposing the
  three mutation mutateAsync functions.
- The tours data layer moves to react-query the same way; the Phase 2
  useTourConfiguration stopgap is removed.

notificationsContext is slimmed to appName + handleActiveTab; all data
state now lives in the react-query cache. Components receive
notificationAppData via props and read the notification list / mutations
directly from the new hooks.

setupTest.tsx exposes createTestQueryClient and each test now wraps its
tree in a QueryClientProvider. The obsolete RequestStatus constants
module is deleted.

Co-Authored-By: Claude <noreply@anthropic.com>
Register the notifications App in site.config.test.tsx and add a
site.config.dev.tsx that mounts the shell/header/footer apps plus a
stub authenticated page so the bell can be exercised in dev mode.
app.scss pulls in the shell stylesheet. Replace the legacy
public/index.html template, which still referenced process.env and
htmlWebpackPlugin.options (both unavailable under frontend-base), with
a minimal static one. Move the dev port from 2002 (taken locally) to
1992.

Co-Authored-By: Claude <noreply@anthropic.com>
No PNG imports exist in src/ (Paragon only pulls in SVGs), so the
file.js mock and its moduleNameMapper entry are dead weight.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Register the bell as INSERT_BEFORE the authenticated menu in the
desktopRight and mobileRight header slots, gated on authenticated: true.

Reorder setupTest.tsx to import site.config before @openedx/frontend-base
to avoid a circular require: frontend-base -> site.config -> app -> slots
-> NotificationsTray -> tours/messages -> defineMessages (still unbound).

Co-Authored-By: Claude <noreply@anthropic.com>
BREAKING CHANGE: package renamed from @edx/frontend-plugin-notifications
to @openedx/frontend-app-notifications and rewritten against
@openedx/frontend-base. Legacy frontend-plugin-framework slot wiring is
replaced by an App config that registers the notifications bell against
the unified header's desktopRight/mobileRight slots. Data layer migrated
to @tanstack/react-query v5; source fully converted to TypeScript.
Rewrite README.rst and catalog-info.yaml for @openedx/frontend-app-notifications,
switch .releaserc to release @edx/frontend-plugin-notifications from main and
@openedx/frontend-app-notifications as alpha prereleases from frontend-base,
and add the frontend-base branch to ci.yml and release.yml triggers.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace useAppConfig().ACCOUNT_SETTINGS_URL with getUrlByRouteRole, so
the account-settings link is sourced from frontend-base's unified
internal/external route lookup. When the resolved URL is relative, use
react-router's Link to keep navigation in-SPA; fall back to Hyperlink
with target=_blank for absolute URLs.

Co-Authored-By: Claude <noreply@anthropic.com>
Move landing page and mocks into dev/ as a self-contained app. Mocks
are stateful (mark-seen/mark-read update an in-memory store), cover
the CSRF endpoint, and shim window.open so clicks show effects in
place. Also set basename so /notifications resolves under PUBLIC_PATH.

Co-Authored-By: Claude <noreply@anthropic.com>
The filter used the raw tourName while the lookup ran it through
camelToConstant, so no value satisfied both. Use the constant-cased
key everywhere and wire the tour into the dev site as a smoke test.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Matches the learner-dashboard pattern. Drops the other re-exports,
since they were originally meant for backward compatibility, but
that turned out not to be possible.

Co-Authored-By: Claude <noreply@anthropic.com>
@openedx-semantic-release-bot
Copy link
Copy Markdown

🎉 This PR is included in version 3.0.0-alpha.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

arbrandes and others added 2 commits April 20, 2026 17:28
Co-Authored-By: Claude <noreply@anthropic.com>
The shell stylesheet moved from a SCSS file to a JS manifest at
@openedx/frontend-base/shell/style. Imports it directly from
site.config.dev.tsx and drops the now-redundant site.scss.

Mirrors openedx/frontend-base#232.

Co-Authored-By: Claude <noreply@anthropic.com>
@openedx-semantic-release-bot
Copy link
Copy Markdown

🎉 This PR is included in version 3.0.0-alpha.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

Adds an npm run build:ci script that exercises the real app graph through webpack, and wires it into the CI workflow after the existing build step.

Co-Authored-By: Claude <noreply@anthropic.com>
@arbrandes arbrandes changed the title Migrate to @openedx/frontend-app-notifications (frontend-base) feat!: migrate to frontend-base Apr 24, 2026
INSERT_BEFORE the desktopSecondaryLinks widget rather than the auth
menu so the bell renders to the left of any secondary nav links
(e.g. the help button from frontend-base#245) instead of between
those links and the user avatar dropdown.

Mobile is left as-is — mobileRight has no secondary-nav cluster to
position relative to (just bell + auth menu), so INSERT_BEFORE the
mobile auth menu remains correct.

Refs openedx/frontend-base#245

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@openedx-semantic-release-bot
Copy link
Copy Markdown

🎉 This PR is included in version 3.0.0-alpha.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

@arbrandes arbrandes linked an issue Apr 29, 2026 that may be closed by this pull request
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Land frontend-base Port frontend-plugin-notifications into an app

3 participants