From 9292ea8fdae3d1fb7183b00e7559d0b9ac7510dc Mon Sep 17 00:00:00 2001 From: "Adolfo R. Brandes" Date: Fri, 10 Apr 2026 08:49:54 -0300 Subject: [PATCH] feat!: use `provides` for chromeless shell mode Apps can now request chromeless mode (no header or footer) by listing their route roles under providesChromelessRolesId in their `provides` config. The shell checks these against active roles at render time. A new getProvidesAsStrings() runtime helper is added beside getProvides(). The shell now exports providesChromelessRolesId and providesCourseNavigationRolesId for use by consuming apps. BREAKING CHANGE: The course navigation bar's provides data shape is simplified from { courseNavigationRoles: string[] } to a plain string[]. getProvidesAsRoles() is replaced by getProvidesAsStrings() and moved from the shell into the runtime. Co-Authored-By: Claude --- ...-provides-for-inter-app-configuration.rst} | 79 +++++++++++-------- runtime/config/index.test.ts | 26 +++--- runtime/config/index.ts | 26 ++++-- runtime/index.ts | 3 +- shell/app.ts | 28 ++++--- shell/constants.ts | 1 + shell/header/app.tsx | 6 +- shell/header/constants.ts | 4 +- .../CourseTabsNavigation.tsx | 19 +++-- .../course-navigation-bar/utils.test.ts | 36 +++------ shell/header/course-navigation-bar/utils.ts | 34 ++++---- shell/header/index.ts | 1 + shell/index.ts | 3 +- types.ts | 2 +- 14 files changed, 144 insertions(+), 124 deletions(-) rename docs/decisions/{0013-app-provides-for-inter-app-data.rst => 0013-app-provides-for-inter-app-configuration.rst} (64%) create mode 100644 shell/constants.ts diff --git a/docs/decisions/0013-app-provides-for-inter-app-data.rst b/docs/decisions/0013-app-provides-for-inter-app-configuration.rst similarity index 64% rename from docs/decisions/0013-app-provides-for-inter-app-data.rst rename to docs/decisions/0013-app-provides-for-inter-app-configuration.rst index 09adeaf1..f9f5c0bf 100644 --- a/docs/decisions/0013-app-provides-for-inter-app-data.rst +++ b/docs/decisions/0013-app-provides-for-inter-app-configuration.rst @@ -1,5 +1,5 @@ -App ``provides`` for Inter-App Data -#################################### +App ``provides`` for Inter-App Configuration +############################################ Status ====== @@ -11,15 +11,15 @@ Context ======= frontend-base applications currently communicate through two structured -mechanisms: ``routes`` and ``slots``. Both are defined in the ``App`` interface -and consumed directly by frontend-base's runtime. +mechanisms: ``routes``, ``slots``, and ``providers``. All are defined in the +``App`` interface and consumed directly by frontend-base's runtime. -As the platform evolves, however, situations arise where apps need to share data -with each other that frontend-base itself has no reason to understand. A -concrete example is the course navigation bar introduced in the header app. -The header needs to know two things from other apps: +As the platform evolves, however, situations arise where apps need to share +configuration data with each other that frontend-base itself has no reason to +understand. A concrete example is the course navigation bar introduced in the +header app. The header needs to know two things from other apps: -1. Which apps want the course navigation bar to appear (currently a hardcoded +1. Which apps want the course navigation bar to appear (previously a hardcoded list of roles in ``constants.ts``). 2. Which URL patterns each app handles client-side, so the navigation bar can @@ -36,8 +36,8 @@ runtime needs to interpret them directly. It builds a router from ``routes`` and renders widgets from ``slots``. Any new field that frontend-base itself must consume deserves the same treatment: a dedicated, typed field. -But for data that flows between apps - where frontend-base is just the conduit - -a generic mechanism is more appropriate. +But for generic configuration between apps - where frontend-base is just the +conduit - a generic mechanism is more appropriate. Decision @@ -55,25 +55,23 @@ Add an optional ``provides`` field to the ``App`` interface:: provides?: Record, } -``provides`` is a flat key-value map where each key is an identifier agreed -upon by the providing and consuming apps, and the value is whatever the -consumer expects. frontend-base stores this data and exposes it through a -runtime function, but does not interpret it. Any namespaced identifier can -serve as a key. +``provides`` is a flat key-value map where each key is a namespaced identifier +agreed upon by the providing and consuming apps, and the value takes whatever +shape the consuming app expects. The runtime stores this data and exposes it +through a runtime function, but does not interpret it. A runtime helper would look something like:: - // Returns all `provides` entries matching the given key. - function getProvidedData(key: string): unknown[] + // Returns all `provides` entries matching the given identifier. + function getProvides(id: string): unknown[] Guidelines ========== -1. ``provides`` is for inter-app data that frontend-base does not need to - interpret. If frontend-base's runtime must consume the data to function - (as it does with routes and slots), a dedicated typed field on ``App`` is - the right choice. +1. ``provides`` is for inter-app configuration that the runtime does not need + to interpret. If it must consume the data to function (as it does with + routes and slots), a dedicated typed field on ``App`` is the right choice. 2. Keys in ``provides`` should be their own namespaced identifiers, not duplicates of existing app, slot, or widget IDs. This allows different @@ -84,8 +82,8 @@ Guidelines consuming apps. It is not enforced by frontend-base. Consuming apps should validate or type-guard the data they receive. -4. ``provides`` should not be used as a back door to modify frontend-base's - behavior. It is not a configuration mechanism for the runtime. +4. ``provides`` should not be used as a back door to modify the runtime's + behavior. It is not a configuration mechanism for the runtime itself. Consequences @@ -107,28 +105,29 @@ Course navigation bar example As a concrete illustration, the Instructor Dashboard app could declare:: const config: App = { - appId: 'org.openedx.frontend.app.instructor', + appId: 'org.openedx.frontend.app.instructorDashboard', provides: { - 'org.openedx.frontend.provides.courseNavigationRoles.v1': { - courseNavigationRoles: ['org.openedx.frontend.role.instructor'], - }, + 'org.openedx.frontend.provides.courseNavigationRoles.v1': [ + 'org.openedx.frontend.role.instructorDashboard', + ], }, routes: [...], slots: [...], }; The header's course navigation bar widget collects ``provides`` entries keyed -to its provides identifier from all registered apps. From the provided roles -it determines both when to render the navigation bar (by checking -``getActiveRoles()``) and which tab URLs can be navigated client-side (by -resolving roles to route paths via ``getUrlByRouteRole()``). +to the course navigation roles identifier from all registered apps. It expects +the provided values to be role identifiers, from which it determines both when +to render the navigation bar (by checking ``getActiveRoles()``) and which tab +URLs can be navigated client-side (by resolving roles to route paths via +``getUrlByRouteRole()``). Rejected alternatives ===================== -Slot operations ---------------- +Widget operations +----------------- Each app could register its own widget into the course navigation bar slot with an ``active`` condition on its role. The ``OPTIONS`` operation can even @@ -157,3 +156,15 @@ with no standard way to discover them. Providers are the right tool when data changes over time and consumers need to re-render. The course navigation roles are fixed at registration time and never change, making ``provides`` a more natural fit. + +Reusing ``App.config`` +---------------------- + +The existing ``App.config`` field has the same type (``Record``) +and could theoretically hold provided data. However, ``config`` is per-app: it +is retrieved by ``appId`` via ``getAppConfig()`` and is meant to hold settings +*for* that app. ``provides`` has a cross-app access pattern: +``getProvides()`` collects entries from all apps that declared data under a +given identifier. Merging the two would require scanning every app's config +for a specific key, blurring the distinction between settings an app consumes +and data it exposes for others. diff --git a/runtime/config/index.test.ts b/runtime/config/index.test.ts index 49547bb0..6d064843 100644 --- a/runtime/config/index.test.ts +++ b/runtime/config/index.test.ts @@ -3,7 +3,7 @@ import * as subscriptions from '../subscriptions'; import { addAppConfigs, getAppConfig, - getProvidedData, + getProvides, getSiteConfig, mergeSiteConfig, setSiteConfig, @@ -352,10 +352,10 @@ describe('mergeSiteConfig', () => { }); }); - describe('getProvidedData', () => { + describe('getProvides', () => { it('should return empty array when no apps exist', () => { setSiteConfig({ ...defaultSiteConfig, apps: [] }); - expect(getProvidedData('org.openedx.frontend.provides.testKey.v1')).toEqual([]); + expect(getProvides('org.openedx.frontend.provides.testProvidesId.v1')).toEqual([]); }); it('should return empty array when no apps provide data for the consumer', () => { @@ -366,7 +366,7 @@ describe('mergeSiteConfig', () => { { appId: 'app-two' }, ], }); - expect(getProvidedData('org.openedx.frontend.provides.testKey.v1')).toEqual([]); + expect(getProvides('org.openedx.frontend.provides.testProvidesId.v1')).toEqual([]); }); it('should collect provided data from apps that declare it', () => { @@ -376,19 +376,19 @@ describe('mergeSiteConfig', () => { { appId: 'app-one', provides: { - 'org.openedx.frontend.provides.testKey.v1': { urlPattern: '/one/' }, + 'org.openedx.frontend.provides.testProvidesId.v1': { urlPattern: '/one/' }, }, }, { appId: 'app-two', provides: { - 'org.openedx.frontend.provides.testKey.v1': { urlPattern: '/two/' }, + 'org.openedx.frontend.provides.testProvidesId.v1': { urlPattern: '/two/' }, }, }, ], }); - const result = getProvidedData('org.openedx.frontend.provides.testKey.v1'); + const result = getProvides('org.openedx.frontend.provides.testProvidesId.v1'); expect(result).toEqual([ { urlPattern: '/one/' }, { urlPattern: '/two/' }, @@ -402,17 +402,17 @@ describe('mergeSiteConfig', () => { { appId: 'app-one', provides: { - 'org.openedx.frontend.provides.testKey.v1': { urlPattern: '/one/' }, - 'org.openedx.frontend.provides.otherKey.v1': { showBranding: true }, + 'org.openedx.frontend.provides.testProvidesId.v1': { urlPattern: '/one/' }, + 'org.openedx.frontend.provides.otherProvidesId.v1': { showBranding: true }, }, }, ], }); - const headerData = getProvidedData('org.openedx.frontend.provides.testKey.v1'); + const headerData = getProvides('org.openedx.frontend.provides.testProvidesId.v1'); expect(headerData).toEqual([{ urlPattern: '/one/' }]); - const footerData = getProvidedData('org.openedx.frontend.provides.otherKey.v1'); + const footerData = getProvides('org.openedx.frontend.provides.otherProvidesId.v1'); expect(footerData).toEqual([{ showBranding: true }]); }); @@ -424,14 +424,14 @@ describe('mergeSiteConfig', () => { { appId: 'app-two', provides: { - 'org.openedx.frontend.provides.testKey.v1': { urlPattern: '/two/' }, + 'org.openedx.frontend.provides.testProvidesId.v1': { urlPattern: '/two/' }, }, }, { appId: 'app-three', config: { VALUE: 'test' } }, ], }); - const result = getProvidedData('org.openedx.frontend.provides.testKey.v1'); + const result = getProvides('org.openedx.frontend.provides.testProvidesId.v1'); expect(result).toEqual([{ urlPattern: '/two/' }]); }); }); diff --git a/runtime/config/index.ts b/runtime/config/index.ts index 9c33f6b2..7b880f71 100644 --- a/runtime/config/index.ts +++ b/runtime/config/index.ts @@ -325,25 +325,39 @@ export function getActiveRoles() { } /** - * Collects all `provides` entries from registered apps that match the given key. + * Collects all `provides` entries from registered apps that match the given identifier. * This enables inter-app data sharing without frontend-base needing to understand the data shape. * - * @param key - The namespaced identifier for the provided data. - * @returns An array of provided data objects from all apps that declared data for this key. + * @param id - The namespaced provides identifier. + * @returns An array of provided data from all apps that declared data for this identifier. */ -export function getProvidedData(key: string): unknown[] { +export function getProvides(id: string): unknown[] { const { apps } = getSiteConfig(); if (!apps) return []; const results: unknown[] = []; for (const app of apps) { - if (app.provides && app.provides[key] !== undefined) { - results.push(app.provides[key]); + if (app.provides && app.provides[id] !== undefined) { + results.push(app.provides[id]); } } return results; } +/** + * Collects and flattens all `provides` entries for the given identifier + * as strings. Each entry can be a single string or a string array; entries + * of other types are silently skipped. + * + * @param id - The namespaced provides identifier. + * @returns A flat array of strings from all apps that declared data for this identifier. + */ +export function getProvidesAsStrings(id: string): string[] { + return getProvides(id) + .filter((data): data is string | string[] => typeof data === 'string' || Array.isArray(data)) + .flat(); +} + /** * Get an external link URL based on the URL provided. If the passed in URL is overridden in the * `externalLinkUrlOverrides` object, it will return the overridden URL. Otherwise, it will return diff --git a/runtime/index.ts b/runtime/index.ts index 9023bbbd..1235d756 100644 --- a/runtime/index.ts +++ b/runtime/index.ts @@ -45,7 +45,8 @@ export { getActiveWidgetRoles, getActiveRoles, getExternalLinkUrl, - getProvidedData + getProvides, + getProvidesAsStrings } from './config'; export * from './constants'; diff --git a/shell/app.ts b/shell/app.ts index ca370358..d28c94c2 100644 --- a/shell/app.ts +++ b/shell/app.ts @@ -1,15 +1,23 @@ -import { WidgetOperationTypes } from '../runtime'; +import { getActiveRoles, getProvidesAsStrings, WidgetOperationTypes } from '../runtime'; import { App } from '../types'; import { Footer } from './footer'; import { Header } from './header'; +import { providesChromelessRolesId } from './constants'; -const inactive = [ - 'org.openedx.frontend.role.login', - 'org.openedx.frontend.role.register', - 'org.openedx.frontend.role.resetPassword', - 'org.openedx.frontend.role.confirmPassword', - 'org.openedx.frontend.role.welcome' -]; +/* + * Returns false when the current route should be chromeless (no header or + * footer). Apps request chromeless mode by listing their route roles under + * providesChromelessRolesId in their `provides` entry, e.g.: + * + * provides: { [providesChromelessRolesId]: ['org.openedx.frontend.role.authn'] } + * + * The widget is disabled when any of those roles is currently active. + */ +function isChromeVisible(): boolean { + const activeRoles = getActiveRoles(); + const chromelessRoles = getProvidesAsStrings(providesChromelessRolesId); + return !chromelessRoles.some(role => activeRoles.includes(role)); +} const app: App = { appId: 'org.openedx.frontend.app.shell', @@ -20,7 +28,7 @@ const app: App = { op: WidgetOperationTypes.APPEND, component: Header, condition: { - inactive, + callback: isChromeVisible, } }, { @@ -29,7 +37,7 @@ const app: App = { op: WidgetOperationTypes.APPEND, component: Footer, condition: { - inactive, + callback: isChromeVisible, } }, ] diff --git a/shell/constants.ts b/shell/constants.ts new file mode 100644 index 00000000..d13370b0 --- /dev/null +++ b/shell/constants.ts @@ -0,0 +1 @@ +export const providesChromelessRolesId = 'org.openedx.frontend.provides.chromelessRoles.v1'; diff --git a/shell/header/app.tsx b/shell/header/app.tsx index 1e413fb3..0f0fb35a 100644 --- a/shell/header/app.tsx +++ b/shell/header/app.tsx @@ -14,7 +14,7 @@ import MobileNavLinks from './mobile/MobileNavLinks'; import messages from '../Shell.messages'; import CourseTabsNavigation from './course-navigation-bar/CourseTabsNavigation'; import { isCourseNavigationRoute } from './course-navigation-bar/utils'; -import { appId, courseNavigationBarSlotId, courseTabsNavigationWidgetId } from './constants'; +import { appId } from './constants'; const config: App = { appId, @@ -139,8 +139,8 @@ const config: App = { } }, { - slotId: courseNavigationBarSlotId, - id: courseTabsNavigationWidgetId, + slotId: 'org.openedx.frontend.slot.header.courseNavigationBar.v1', + id: 'org.openedx.frontend.widget.header.courseNavigationBar.v1', op: WidgetOperationTypes.APPEND, component: CourseTabsNavigation, condition: { diff --git a/shell/header/constants.ts b/shell/header/constants.ts index 78a75d95..0f246ecc 100644 --- a/shell/header/constants.ts +++ b/shell/header/constants.ts @@ -1,4 +1,2 @@ export const appId = 'org.openedx.frontend.app.header'; -export const courseNavigationBarSlotId = 'org.openedx.frontend.slot.header.courseNavigationBar.v1'; -export const courseTabsNavigationWidgetId = 'org.openedx.frontend.widget.header.courseTabsNavigation.v1'; -export const courseNavigationRolesProvidesKey = 'org.openedx.frontend.provides.courseNavigationRoles.v1'; +export const providesCourseNavigationRolesId = 'org.openedx.frontend.provides.courseNavigationRoles.v1'; diff --git a/shell/header/course-navigation-bar/CourseTabsNavigation.tsx b/shell/header/course-navigation-bar/CourseTabsNavigation.tsx index 9640f577..62d13c88 100644 --- a/shell/header/course-navigation-bar/CourseTabsNavigation.tsx +++ b/shell/header/course-navigation-bar/CourseTabsNavigation.tsx @@ -13,13 +13,18 @@ interface ResolvedTab extends CourseTab { clientPath: string | null, } -// Returns the tabId of the tab whose pathname is the longest prefix match -// against the current path. Uses react-router's matchPath for segment-aware -// matching. For example, given tabs with paths /course/ (tabId: "outline") -// and /course/dates/ (tabId: "dates"): -// /course/dates/foo -> "dates" (longest prefix match) -// /course/outline -> "outline" -// /courseware -> null (not a segment boundary) +/* + * Returns the tabId of the tab whose pathname is the longest prefix match + * against the current path. Uses react-router's matchPath for segment-aware + * matching. + * + * For example, given tabs with paths /course/ (tabId: "outline") + * and /course/dates/ (tabId: "dates"): + * + * /course/dates/foo -> "dates" (longest prefix match) + * /course/outline -> "outline" + * /courseware -> null (not a segment boundary) + */ const getActiveTabId = (currentPath: string, tabs: ResolvedTab[]): string | null => { let best: ResolvedTab | null = null; for (const tab of tabs) { diff --git a/shell/header/course-navigation-bar/utils.test.ts b/shell/header/course-navigation-bar/utils.test.ts index c6c1fe0d..87fb98cc 100644 --- a/shell/header/course-navigation-bar/utils.test.ts +++ b/shell/header/course-navigation-bar/utils.test.ts @@ -4,31 +4,27 @@ import * as runtime from '../../../runtime'; jest.mock('../../../runtime'); const mockGetActiveRoles = runtime.getActiveRoles as jest.MockedFunction; -const mockGetProvidedData = runtime.getProvidedData as jest.MockedFunction; +const mockGetProvidesAsStrings = runtime.getProvidesAsStrings as jest.MockedFunction; const mockGetUrlByRouteRole = runtime.getUrlByRouteRole as jest.MockedFunction; describe('isCourseNavigationRoute', () => { it('returns true when a provided role is active', () => { - mockGetProvidedData.mockReturnValue([ - { courseNavigationRoles: ['org.openedx.frontend.role.instructor'] }, - ]); - mockGetActiveRoles.mockReturnValue(['org.openedx.frontend.role.instructor']); + mockGetProvidesAsStrings.mockReturnValue(['org.openedx.frontend.role.instructorDashboard']); + mockGetActiveRoles.mockReturnValue(['org.openedx.frontend.role.instructorDashboard']); expect(isCourseNavigationRoute()).toBe(true); }); it('returns false when no provided roles are active', () => { - mockGetProvidedData.mockReturnValue([ - { courseNavigationRoles: ['org.openedx.frontend.role.instructor'] }, - ]); + mockGetProvidesAsStrings.mockReturnValue(['org.openedx.frontend.role.instructorDashboard']); mockGetActiveRoles.mockReturnValue(['org.openedx.frontend.role.learning']); expect(isCourseNavigationRoute()).toBe(false); }); it('returns false when no providers exist', () => { - mockGetProvidedData.mockReturnValue([]); - mockGetActiveRoles.mockReturnValue(['org.openedx.frontend.role.instructor']); + mockGetProvidesAsStrings.mockReturnValue([]); + mockGetActiveRoles.mockReturnValue(['org.openedx.frontend.role.instructorDashboard']); expect(isCourseNavigationRoute()).toBe(false); }); @@ -36,45 +32,35 @@ describe('isCourseNavigationRoute', () => { describe('isClientRoute', () => { it('matches a pathname under a static route path', () => { - mockGetProvidedData.mockReturnValue([ - { courseNavigationRoles: ['org.openedx.frontend.role.learning'] }, - ]); + mockGetProvidesAsStrings.mockReturnValue(['org.openedx.frontend.role.learning']); mockGetUrlByRouteRole.mockReturnValue('/course'); expect(isClientRoute('/course/outline')).toBe(true); }); it('matches a pathname under a parameterized route path', () => { - mockGetProvidedData.mockReturnValue([ - { courseNavigationRoles: ['org.openedx.frontend.role.instructor'] }, - ]); + mockGetProvidesAsStrings.mockReturnValue(['org.openedx.frontend.role.instructorDashboard']); mockGetUrlByRouteRole.mockReturnValue('/instructor-dashboard/:courseId'); expect(isClientRoute('/instructor-dashboard/course-v1:edX+DemoX+Demo')).toBe(true); }); it('does not match a pathname outside the route prefix', () => { - mockGetProvidedData.mockReturnValue([ - { courseNavigationRoles: ['org.openedx.frontend.role.instructor'] }, - ]); + mockGetProvidesAsStrings.mockReturnValue(['org.openedx.frontend.role.instructorDashboard']); mockGetUrlByRouteRole.mockReturnValue('/instructor-dashboard/:courseId'); expect(isClientRoute('/courses/some-course/instructor')).toBe(false); }); it('returns false for external routes', () => { - mockGetProvidedData.mockReturnValue([ - { courseNavigationRoles: ['org.openedx.frontend.role.learning'] }, - ]); + mockGetProvidesAsStrings.mockReturnValue(['org.openedx.frontend.role.learning']); mockGetUrlByRouteRole.mockReturnValue('https://external.example.com/course'); expect(isClientRoute('/course/outline')).toBe(false); }); it('returns false when role has no matching route', () => { - mockGetProvidedData.mockReturnValue([ - { courseNavigationRoles: ['org.openedx.frontend.role.learning'] }, - ]); + mockGetProvidesAsStrings.mockReturnValue(['org.openedx.frontend.role.learning']); mockGetUrlByRouteRole.mockReturnValue(null); expect(isClientRoute('/course/outline')).toBe(false); diff --git a/shell/header/course-navigation-bar/utils.ts b/shell/header/course-navigation-bar/utils.ts index a7f84027..b3ef99c5 100644 --- a/shell/header/course-navigation-bar/utils.ts +++ b/shell/header/course-navigation-bar/utils.ts @@ -1,32 +1,26 @@ import { matchPath } from 'react-router-dom'; -import { getActiveRoles, getProvidedData, getUrlByRouteRole } from '../../../runtime'; -import { courseNavigationRolesProvidesKey } from '../constants'; +import { getActiveRoles, getProvidesAsStrings, getUrlByRouteRole } from '../../../runtime'; +import { providesCourseNavigationRolesId } from '../constants'; -interface CourseNavigationProviderData { - courseNavigationRoles: string[], -} - -function getProviders(): CourseNavigationProviderData[] { - return getProvidedData(courseNavigationRolesProvidesKey).filter( - (data): data is CourseNavigationProviderData => - data !== null - && typeof data === 'object' - && 'courseNavigationRoles' in data - && Array.isArray((data as CourseNavigationProviderData).courseNavigationRoles) - ); -} - -function getProvidedRoles(): string[] { - return getProviders().flatMap(data => data.courseNavigationRoles); +/* + * Collects route role strings from all apps that opted into the course + * navigation bar feature. Each app declares its roles as a string array: + * + * provides: { + * [providesCourseNavigationRolesId]: ['org.openedx.frontend.role.learning'], + * } + */ +function getCourseNavigationBarRoles(): string[] { + return getProvidesAsStrings(providesCourseNavigationRolesId); } export function isCourseNavigationRoute(): boolean { const activeRoles = getActiveRoles(); - return getProvidedRoles().some(role => activeRoles.includes(role)); + return getCourseNavigationBarRoles().some(role => activeRoles.includes(role)); } export function isClientRoute(pathname: string): boolean { - return getProvidedRoles().some(role => { + return getCourseNavigationBarRoles().some(role => { const routePath = getUrlByRouteRole(role); return routePath !== null && routePath.startsWith('/') diff --git a/shell/header/index.ts b/shell/header/index.ts index 31abdcef..7b293368 100644 --- a/shell/header/index.ts +++ b/shell/header/index.ts @@ -1,2 +1,3 @@ export { default as headerApp } from './app'; +export { providesCourseNavigationRolesId } from './constants'; export { default as Header } from './Header'; diff --git a/shell/index.ts b/shell/index.ts index 7a0d4411..8f2d17dc 100644 --- a/shell/index.ts +++ b/shell/index.ts @@ -2,6 +2,7 @@ export { default as DefaultLayout } from './DefaultLayout'; export { default as DefaultMain } from './DefaultMain'; export { default as shellApp } from './app'; export { Footer, footerApp } from './footer'; -export { Header, headerApp } from './header'; +export { providesCourseNavigationRolesId, Header, headerApp } from './header'; +export { providesChromelessRolesId } from './constants'; export { default as LinkMenuItem } from './menus/LinkMenuItem'; export { default as NavDropdownMenuSlot } from './menus/NavDropdownMenuSlot'; diff --git a/types.ts b/types.ts index d9f98d32..32f7186f 100644 --- a/types.ts +++ b/types.ts @@ -27,9 +27,9 @@ export interface App { appId: string, routes?: RoleRouteObject[], providers?: AppProvider[], - provides?: Record, slots?: SlotOperation[], config?: AppConfig, + provides?: Record, } // Site Config