diff --git a/packages/framework/esm-config/src/module-config/module-config.ts b/packages/framework/esm-config/src/module-config/module-config.ts index 9235646da..805e37c9d 100644 --- a/packages/framework/esm-config/src/module-config/module-config.ts +++ b/packages/framework/esm-config/src/module-config/module-config.ts @@ -165,11 +165,11 @@ export function defineConfigSchema(moduleName: string, schema: ConfigSchema) { validateConfigSchema(moduleName, schema); const enhancedSchema = mergeDeepRight(schema, implicitConfigSchema) as ConfigSchema; - const state = configInternalStore.getState(); - configInternalStore.setState({ + configInternalStore.setState((state) => ({ + ...state, schemas: { ...state.schemas, [moduleName]: enhancedSchema }, moduleLoaded: { ...state.moduleLoaded, [moduleName]: true }, - }); + })); } /** @@ -239,10 +239,10 @@ export function defineExtensionConfigSchema(extensionName: string, schema: Confi } export function provide(config: Config, sourceName = 'provided') { - const state = configInternalStore.getState(); - configInternalStore.setState({ + configInternalStore.setState((state) => ({ + ...state, providedConfigs: [...state.providedConfigs, { source: sourceName, config }], - }); + })); } /** diff --git a/packages/framework/esm-globals/src/types.ts b/packages/framework/esm-globals/src/types.ts index a3b317cba..33e00da8b 100644 --- a/packages/framework/esm-globals/src/types.ts +++ b/packages/framework/esm-globals/src/types.ts @@ -343,7 +343,7 @@ export interface OpenmrsAppRoutes { /** * The feature flag to enable if this backend dependency is present */ - feature?: string; + feature?: FeatureFlagDefinition; }; }; /** @@ -364,6 +364,12 @@ export interface OpenmrsAppRoutes { workspaces?: Array; } +export interface FeatureFlagDefinition { + flagName: string; + label: string; + description: string; +} + /** * This interfaces describes the format of the overall rotues.json loaded by the app shell. * Basically, this is the same as the app routes, with each routes definition keyed by the app's name diff --git a/packages/shell/esm-app-shell/src/optionaldeps.ts b/packages/shell/esm-app-shell/src/optionaldeps.ts new file mode 100644 index 000000000..a009260f0 --- /dev/null +++ b/packages/shell/esm-app-shell/src/optionaldeps.ts @@ -0,0 +1,73 @@ +import { + type FeatureFlagDefinition, + openmrsFetch, + registerFeatureFlag, + setFeatureFlag, + getCurrentUser, + restBaseUrl, +} from '@openmrs/esm-framework/src/internal'; +import { satisfies } from 'semver'; + +export function registerOptionalDependencyHandler() { + const subscription = getCurrentUser().subscribe((session) => { + if (session.authenticated) { + subscription.unsubscribe(); + setupOptionalDependencies(); + } + }); + + return Promise.resolve(); +} + +function setupOptionalDependencies() { + const optionalDependencyFlags = window.installedModules.reduce< + Map + >((curr, module) => { + if (module[1]?.optionalBackendDependencies) { + Object.entries(module[1].optionalBackendDependencies).forEach((optionalDependency) => { + const optionalDependencyDescriptor = optionalDependency[1]; + if (typeof optionalDependencyDescriptor !== 'string') { + if ( + typeof optionalDependencyDescriptor.feature === 'object' && + optionalDependencyDescriptor.feature && + optionalDependencyDescriptor.feature.flagName?.length > 0 && + optionalDependencyDescriptor.feature.label?.length > 0 && + optionalDependencyDescriptor.feature.description?.length > 0 + ) { + curr.set(optionalDependency[0], { + version: optionalDependencyDescriptor.version, + feature: optionalDependencyDescriptor.feature, + }); + } else { + console.warn( + `Feature flag descriptor for ${module[0]} does not match expected type. Feature flags must define a 'flagName', 'label', and 'description'`, + ); + } + } + }); + } + + return curr; + }, new Map()); + + if (optionalDependencyFlags.size > 0) { + openmrsFetch<{ results: { uuid: string; version: string }[] }>(`${restBaseUrl}/module?v=custom:(uuid,version)`) + .then((response) => { + (response.data.results ?? []).forEach((backendModule) => { + if (optionalDependencyFlags.has(backendModule.uuid)) { + const optionalDependency = optionalDependencyFlags.get(backendModule.uuid); + if (optionalDependency && satisfies(backendModule.version, optionalDependency.version)) { + registerFeatureFlag( + optionalDependency.feature.flagName, + optionalDependency.feature.label, + optionalDependency.feature.description, + ); + + setFeatureFlag(optionalDependency.feature.flagName, true); + } + } + }); + }) + .catch(() => {}); // swallow any issues fetching + } +} diff --git a/packages/shell/esm-app-shell/src/run.ts b/packages/shell/esm-app-shell/src/run.ts index 7e3ab2593..3143a8e7b 100644 --- a/packages/shell/esm-app-shell/src/run.ts +++ b/packages/shell/esm-app-shell/src/run.ts @@ -39,6 +39,7 @@ import { } from '@openmrs/esm-framework/src/internal'; import { finishRegisteringAllApps, registerApp, tryRegisterExtension } from './apps'; import { setupI18n } from './locale'; +import { registerOptionalDependencyHandler } from './optionaldeps'; import { appName, getCoreExtensions } from './ui'; // @internal @@ -414,5 +415,6 @@ export function run(configUrls: Array) { .then(runShell) .catch(handleInitFailure) .then(closeLoading) - .then(offlineEnabled ? setupOffline : undefined); + .then(offlineEnabled ? setupOffline : undefined) + .then(registerOptionalDependencyHandler); }