feat: Add extensibility for tmtv and segmentation modes (#6128)
* feat: Add extensibility for tmtv and segmentation modes * Fixes for ordering issues on laod * Remove unnecessary reference lookup * Chane side panel timing to fix tests * PR comments - change how mode definitions get created * Improvements to mode customizations * Start organizing customizations * Misc fixes for a customization demo page * Security fixes * PR requested changes to naming
This commit is contained in:
1 parent
f79055f98e
commit
b266c0a86a
49 files changed
+2653
-1215
No files matched your search
+23
-11
@@ -93,13 +93,31 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
||||
const loadedExtensions = await loadModules([...defaultExtensions, ...appConfig.extensions]);
|
||||
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
if (!appConfig.modes) {
|
||||
throw new Error('No modes are defined! Check your app-config.js');
|
||||
}
|
||||
|
||||
// Load the mode modules and register the customizations they carry (plain
|
||||
// `customizationId -> value` maps on the mode definition) at Default scope
|
||||
// BEFORE the bootstrap phase applies, so bootstrap / `?customization=`
|
||||
// modules can modify a mode's registered values before anything reads them.
|
||||
// The mode *instances* are only created after the global phase (below), so
|
||||
// they too see any modifications.
|
||||
const loadedModes = await loadModules([...(appConfig.modes || []), ...defaultModes]);
|
||||
for (const mode of loadedModes) {
|
||||
if (mode?.customizations) {
|
||||
customizationService.addReferences(mode.customizations, customizationService.Scope.Default);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve every customization module up front — from
|
||||
// `appConfig.customizationService.requires` and the `?customization=` URL
|
||||
// parameter — long before any mode loads, then apply the `bootstrap` phase
|
||||
// BEFORE extensions register so it is in place while they initialize. Modules
|
||||
// are only loaded when `appConfig.customizationUrlPrefixes` allows their
|
||||
// prefix; the feature is off by default, and a value with an unconfigured
|
||||
// prefix throws here (aborting startup) rather than being silently ignored.
|
||||
// parameter — then apply the `bootstrap` phase BEFORE extensions register so
|
||||
// it is in place while they initialize. Modules are only loaded when
|
||||
// `appConfig.customizationUrlPrefixes` allows their prefix; the feature is
|
||||
// off by default, and a value with an unconfigured prefix throws here
|
||||
// (aborting startup) rather than being silently ignored.
|
||||
await customizationService.loadAndApplyBootstrapCustomizations(extensionManager);
|
||||
|
||||
await extensionManager.registerExtensions(loadedExtensions, appConfig.dataSources);
|
||||
@@ -114,12 +132,6 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
||||
// TODO: We no longer init webWorkers at app level
|
||||
// TODO: We no longer init the user Manager
|
||||
|
||||
if (!appConfig.modes) {
|
||||
throw new Error('No modes are defined! Check your app-config.js');
|
||||
}
|
||||
|
||||
const loadedModes = await loadModules([...(appConfig.modes || []), ...defaultModes]);
|
||||
|
||||
// This is the name for the loaded instance object
|
||||
appConfig.loadedModes = [];
|
||||
const modesById = new Set();
|
||||
|
||||
@@ -97,6 +97,15 @@ export default function ModeRoute({
|
||||
}
|
||||
}
|
||||
|
||||
// Mode dependency extensions register their customization modules here,
|
||||
// but `registerExtension` does not merge them into the customization
|
||||
// service — that otherwise only happens later in
|
||||
// `extensionManager.onModeEnter`. Merge them now so anything that runs
|
||||
// before setupRouteInit (e.g. a mode's layoutTemplate) can already read
|
||||
// the defaults these extensions provide.
|
||||
// `init` is idempotent — each extension module is merged at most once.
|
||||
customizationService.init(extensionManager);
|
||||
|
||||
if (isMounted.current) {
|
||||
setExtensionDependenciesLoaded(true);
|
||||
}
|
||||
@@ -180,10 +189,19 @@ export default function ModeRoute({
|
||||
if (isMounted.current) {
|
||||
const { leftPanels = [], rightPanels = [], ...layoutProps } = layoutData.props;
|
||||
|
||||
// Register panels immediately so ViewerLayout's first render sees them.
|
||||
// ResizablePanelsHook only auto-expands side panels on the initial mount;
|
||||
// if panels are added later, the viewport grid keeps the wrong width.
|
||||
// setupRouteInit (below) resets and re-applies them after customizations
|
||||
// are layered on, so URL/config modules can still modify the lists.
|
||||
panelService.reset();
|
||||
panelService.addPanels(panelService.PanelPosition.Left, leftPanels);
|
||||
panelService.addPanels(panelService.PanelPosition.Right, rightPanels);
|
||||
|
||||
// Stash the layout lists for setupRouteInit to seed into the
|
||||
// `leftPanels` / `rightPanels` customizations.
|
||||
layoutData.panels = { leftPanels, rightPanels };
|
||||
|
||||
// layoutProps contains all props but leftPanels and rightPanels
|
||||
layoutData.props = layoutProps;
|
||||
|
||||
@@ -222,11 +240,55 @@ export default function ModeRoute({
|
||||
});
|
||||
|
||||
// `extensionManager.onModeEnter` resets the customization mode scope via
|
||||
// `customizationService.onModeEnter`; now layer on the `mode` phase blocks
|
||||
// for this mode — the general (`*`) block first, then any block keyed by
|
||||
// this mode's id / routeName so a single mode can override the general one.
|
||||
// `customizationService.onModeEnter`; the mode scope is then layered
|
||||
// bottom-up so the final value of every key is decided by scope
|
||||
// precedence (global > mode > default) and application order alone:
|
||||
// 1. the mode's own values — its layout panel lists, seeded as the
|
||||
// standard `leftPanels` / `rightPanels` customizations;
|
||||
// its toolbar/tool-group composition, seeded as the plain
|
||||
// `toolbarButtons` / `toolbarSections` / `toolGroupAdditions`
|
||||
// customizations (resolved to concrete definitions later, when the
|
||||
// mode's `onModeEnter` registers the toolbar); and its
|
||||
// `modeCustomizations` block (declared as data on the mode
|
||||
// instance, usually as a customization name registered at default
|
||||
// scope when the mode loaded, so bootstrap/global customizations
|
||||
// can modify the block itself before it is applied);
|
||||
// 2. the app config / URL `mode` phase blocks — the general (`*`)
|
||||
// block first, then any block keyed by this mode's id / routeName.
|
||||
const { leftPanels = [], rightPanels = [] } = layoutTemplateData.current.panels ?? {};
|
||||
customizationService.setCustomizations({
|
||||
leftPanels,
|
||||
rightPanels,
|
||||
toolbarButtons: mode.toolbarButtons ?? [],
|
||||
toolbarSections: mode.toolbarSections ?? [],
|
||||
toolGroupAdditions: mode.toolGroupAdditions ?? {},
|
||||
});
|
||||
|
||||
const modeCustomizations =
|
||||
typeof mode.modeCustomizations === 'string'
|
||||
? customizationService.getCustomization(mode.modeCustomizations)
|
||||
: mode.modeCustomizations;
|
||||
if (modeCustomizations) {
|
||||
customizationService.setCustomizations(modeCustomizations);
|
||||
}
|
||||
customizationService.applyModeCustomizations([mode.id, mode.routeName]);
|
||||
|
||||
// Re-apply panels only when customizations changed the lists. When they
|
||||
// match the layout, the panels registered in retrieveLayoutData are left
|
||||
// in place so ViewerLayout's ResizablePanelsHook keeps the correct sizes
|
||||
// from its one-time initial expand.
|
||||
const resolvedLeftPanels = customizationService.getValue('leftPanels') ?? [];
|
||||
const resolvedRightPanels = customizationService.getValue('rightPanels') ?? [];
|
||||
const panelsChanged =
|
||||
JSON.stringify(resolvedLeftPanels) !== JSON.stringify(leftPanels) ||
|
||||
JSON.stringify(resolvedRightPanels) !== JSON.stringify(rightPanels);
|
||||
|
||||
if (panelsChanged) {
|
||||
panelService.reset();
|
||||
panelService.addPanels(panelService.PanelPosition.Left, resolvedLeftPanels);
|
||||
panelService.addPanels(panelService.PanelPosition.Right, resolvedRightPanels);
|
||||
}
|
||||
|
||||
// use the URL hangingProtocolId if it exists, otherwise use the one
|
||||
// defined in the mode configuration
|
||||
const hangingProtocolIdToUse = hangingProtocolService.getProtocolById(
|
||||
|
||||
@@ -13,6 +13,14 @@ describe('preserveQueryParameters', () => {
|
||||
expect(out.getAll('configUrl')).toEqual(['foo.js']);
|
||||
});
|
||||
|
||||
it('preserves the theme key across mode navigation', () => {
|
||||
const current = new URLSearchParams();
|
||||
current.append('theme', 'arctic');
|
||||
const out = new URLSearchParams();
|
||||
preserveQueryParameters(out, undefined, current);
|
||||
expect(out.get('theme')).toBe('arctic');
|
||||
});
|
||||
|
||||
it('preserves all repeated values for the customization key', () => {
|
||||
const current = new URLSearchParams();
|
||||
current.append('customization', 'a');
|
||||
|
||||
@@ -12,6 +12,7 @@ export const preserveKeys = [
|
||||
'screenNumber',
|
||||
'hangingProtocolId',
|
||||
'customization',
|
||||
'theme',
|
||||
];
|
||||
|
||||
function preserveKey(query: URLSearchParams, current: URLSearchParams, key: string) {
|
||||
|
||||
Reference in new issue
Block a user