From 2c68cda0e76b3d206a96bdedfd294639e6e18e88 Mon Sep 17 00:00:00 2001 From: Brandon Istenes Date: Fri, 14 Jul 2023 13:19:07 -0400 Subject: [PATCH 1/2] O3-2258: Extension system support for feature flags --- .../framework/esm-extensions/package.json | 2 + .../esm-extensions/src/extensions.ts | 21 +++++-- .../framework/esm-extensions/src/store.ts | 2 + packages/framework/esm-framework/docs/API.md | 25 ++++---- .../docs/interfaces/AssignedExtension.md | 25 +++++--- .../docs/interfaces/ConnectedExtension.md | 8 +-- .../docs/interfaces/ExtensionRegistration.md | 11 ++++ .../docs/interfaces/ExtensionSlotState.md | 4 +- .../docs/interfaces/ExtensionStore.md | 2 +- .../docs/interfaces/OpenmrsAppRoutes.md | 8 +-- .../docs/interfaces/ResourceLoader.md | 2 +- packages/framework/esm-globals/src/types.ts | 6 +- .../__mocks__/openmrs-esm-state.mock.ts | 17 ++++++ .../esm-react-utils/src/extensions.test.tsx | 59 ++++++++++++++++++- .../src/useConnectedExtensions.ts | 19 +++++- packages/shell/esm-app-shell/src/apps.ts | 1 + yarn.lock | 2 + 17 files changed, 173 insertions(+), 41 deletions(-) diff --git a/packages/framework/esm-extensions/package.json b/packages/framework/esm-extensions/package.json index aeddcf0cd..051078404 100644 --- a/packages/framework/esm-extensions/package.json +++ b/packages/framework/esm-extensions/package.json @@ -39,12 +39,14 @@ "peerDependencies": { "@openmrs/esm-api": "5.x", "@openmrs/esm-config": "5.x", + "@openmrs/esm-feature-flags": "5.x", "@openmrs/esm-state": "5.x", "single-spa": "5.x" }, "devDependencies": { "@openmrs/esm-api": "^5.0.2", "@openmrs/esm-config": "^5.0.2", + "@openmrs/esm-feature-flags": "^5.0.2", "@openmrs/esm-state": "^5.0.2", "single-spa": "^5.9.2" }, diff --git a/packages/framework/esm-extensions/src/extensions.ts b/packages/framework/esm-extensions/src/extensions.ts index a6ce435c8..a1717fbf8 100644 --- a/packages/framework/esm-extensions/src/extensions.ts +++ b/packages/framework/esm-extensions/src/extensions.ts @@ -19,6 +19,7 @@ import { getExtensionSlotConfigFromStore, getExtensionSlotsConfigStore, } from "@openmrs/esm-config"; +import { featureFlagsStore } from "@openmrs/esm-feature-flags"; import isEqual from "lodash-es/isEqual"; import isUndefined from "lodash-es/isUndefined"; import { @@ -282,16 +283,27 @@ function getOrder( * * @param assignedExtensions The list of extensions to filter. * @param online Whether the app is currently online. If `null`, uses `navigator.onLine`. + * @param enabledFeatureFlags The names of all enabled feature flags. If `null`, looks + * up the feature flags using the feature flags API. * @returns A list of extensions that should be rendered */ export function getConnectedExtensions( assignedExtensions: Array, - online: boolean | null = null + online: boolean | null = null, + enabledFeatureFlags: Array | null = null ): Array { const isOnline = online ?? navigator.onLine; - return assignedExtensions.filter((e) => - checkStatusFor(isOnline, e.online, e.offline) - ); + const featureFlags = + enabledFeatureFlags ?? + Object.entries(featureFlagsStore.getState().flags) + .filter(([, { enabled }]) => enabled) + .map(([name]) => name); + return assignedExtensions + .filter((e) => checkStatusFor(isOnline, e.online, e.offline)) + .filter( + (e) => + e.featureFlag === undefined || featureFlags?.includes(e.featureFlag) + ); } function getAssignedExtensionsFromSlotData( @@ -343,6 +355,7 @@ function getAssignedExtensionsFromSlotData( name, moduleName: extension.moduleName, config: extensionConfig, + featureFlag: extension.featureFlag, meta: extension.meta, }); } diff --git a/packages/framework/esm-extensions/src/store.ts b/packages/framework/esm-extensions/src/store.ts index a673b7ce8..4e3d7af99 100644 --- a/packages/framework/esm-extensions/src/store.ts +++ b/packages/framework/esm-extensions/src/store.ts @@ -22,6 +22,7 @@ export interface ExtensionRegistration { online?: boolean; offline?: boolean; privileges?: string | Array; + featureFlag?: string; } export interface ExtensionInfo extends ExtensionRegistration { @@ -81,6 +82,7 @@ export interface AssignedExtension { config: ConfigObject | null; online?: boolean | object; offline?: boolean | object; + featureFlag?: string; } export interface ConnectedExtension { diff --git a/packages/framework/esm-framework/docs/API.md b/packages/framework/esm-framework/docs/API.md index 6c16d0543..8d6a173e8 100644 --- a/packages/framework/esm-framework/docs/API.md +++ b/packages/framework/esm-framework/docs/API.md @@ -405,7 +405,7 @@ ___ ### ExtensionDefinition -Ƭ **ExtensionDefinition**: { `meta?`: { `[k: string]`: `unknown`; } ; `name`: `string` ; `offline?`: `boolean` ; `online?`: `boolean` ; `order?`: `number` ; `privileges?`: `string` \| `string`[] ; `slot?`: `string` ; `slots?`: `string`[] } & { `component`: `string` } \| { `component?`: `never` } +Ƭ **ExtensionDefinition**: { `featureFlag?`: `string` ; `meta?`: { `[k: string]`: `unknown`; } ; `name`: `string` ; `offline?`: `boolean` ; `online?`: `boolean` ; `order?`: `number` ; `privileges?`: `string` \| `string`[] ; `slot?`: `string` ; `slots?`: `string`[] } & { `component`: `string` } \| { `component?`: `never` } A definition of an extension as extracted from an app's routes.json @@ -444,7 +444,7 @@ Basically, this is the same as the app routes, with each routes definition keyed #### Defined in -[packages/framework/esm-globals/src/types.ts:251](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-globals/src/types.ts#L251) +[packages/framework/esm-globals/src/types.ts:255](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-globals/src/types.ts#L255) ___ @@ -2257,7 +2257,7 @@ writing a module for a specific implementation. #### Defined in -[packages/framework/esm-extensions/src/extensions.ts:174](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/extensions.ts#L174) +[packages/framework/esm-extensions/src/extensions.ts:175](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/extensions.ts#L175) ___ @@ -2280,7 +2280,7 @@ Avoid using this. Extension attachments should be considered declarative. #### Defined in -[packages/framework/esm-extensions/src/extensions.ts:205](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/extensions.ts#L205) +[packages/framework/esm-extensions/src/extensions.ts:206](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/extensions.ts#L206) ___ @@ -2302,7 +2302,7 @@ Avoid using this. Extension attachments should be considered declarative. #### Defined in -[packages/framework/esm-extensions/src/extensions.ts:229](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/extensions.ts#L229) +[packages/framework/esm-extensions/src/extensions.ts:230](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/extensions.ts#L230) ___ @@ -2326,13 +2326,13 @@ An array of extensions assigned to the named slot #### Defined in -[packages/framework/esm-extensions/src/extensions.ts:360](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/extensions.ts#L360) +[packages/framework/esm-extensions/src/extensions.ts:373](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/extensions.ts#L373) ___ ### getConnectedExtensions -▸ **getConnectedExtensions**(`assignedExtensions`, `online?`): [`ConnectedExtension`](interfaces/ConnectedExtension.md)[] +▸ **getConnectedExtensions**(`assignedExtensions`, `online?`, `enabledFeatureFlags?`): [`ConnectedExtension`](interfaces/ConnectedExtension.md)[] Filters a list of extensions according to whether they support the current connectivity status. @@ -2343,6 +2343,7 @@ current connectivity status. | :------ | :------ | :------ | :------ | | `assignedExtensions` | [`AssignedExtension`](interfaces/AssignedExtension.md)[] | `undefined` | The list of extensions to filter. | | `online` | ``null`` \| `boolean` | `null` | Whether the app is currently online. If `null`, uses `navigator.onLine`. | +| `enabledFeatureFlags` | ``null`` \| `string`[] | `null` | The names of all enabled feature flags. If `null`, looks up the feature flags using the feature flags API. | #### Returns @@ -2352,7 +2353,7 @@ A list of extensions that should be rendered #### Defined in -[packages/framework/esm-extensions/src/extensions.ts:287](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/extensions.ts#L287) +[packages/framework/esm-extensions/src/extensions.ts:290](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/extensions.ts#L290) ___ @@ -2384,7 +2385,7 @@ getExtensionNameFromId("baz") #### Defined in -[packages/framework/esm-extensions/src/extensions.ts:118](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/extensions.ts#L118) +[packages/framework/esm-extensions/src/extensions.ts:119](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/extensions.ts#L119) ___ @@ -2401,7 +2402,7 @@ state of the extension system. #### Defined in -[packages/framework/esm-extensions/src/store.ts:130](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L130) +[packages/framework/esm-extensions/src/store.ts:132](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L132) ___ @@ -2487,7 +2488,7 @@ ___ ▸ **useConnectedExtensions**(`slotName`): [`ConnectedExtension`](interfaces/ConnectedExtension.md)[] Gets the assigned extension for a given extension slot name. -Considers if offline or online. +Considers if offline or online, and what feature flags are enabled. #### Parameters @@ -2501,7 +2502,7 @@ Considers if offline or online. #### Defined in -[packages/framework/esm-react-utils/src/useConnectedExtensions.ts:15](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-react-utils/src/useConnectedExtensions.ts#L15) +[packages/framework/esm-react-utils/src/useConnectedExtensions.ts:17](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-react-utils/src/useConnectedExtensions.ts#L17) ___ diff --git a/packages/framework/esm-framework/docs/interfaces/AssignedExtension.md b/packages/framework/esm-framework/docs/interfaces/AssignedExtension.md index 90c58bf62..b1622679c 100644 --- a/packages/framework/esm-framework/docs/interfaces/AssignedExtension.md +++ b/packages/framework/esm-framework/docs/interfaces/AssignedExtension.md @@ -7,6 +7,7 @@ ### Extension Properties - [config](AssignedExtension.md#config) +- [featureFlag](AssignedExtension.md#featureflag) - [id](AssignedExtension.md#id) - [meta](AssignedExtension.md#meta) - [moduleName](AssignedExtension.md#modulename) @@ -24,7 +25,17 @@ The extension's config. Note that this will be `null` until the slot is mounted. #### Defined in -[packages/framework/esm-extensions/src/store.ts:81](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L81) +[packages/framework/esm-extensions/src/store.ts:82](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L82) + +___ + +### featureFlag + +• `Optional` **featureFlag**: `string` + +#### Defined in + +[packages/framework/esm-extensions/src/store.ts:85](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L85) ___ @@ -34,7 +45,7 @@ ___ #### Defined in -[packages/framework/esm-extensions/src/store.ts:76](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L76) +[packages/framework/esm-extensions/src/store.ts:77](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L77) ___ @@ -44,7 +55,7 @@ ___ #### Defined in -[packages/framework/esm-extensions/src/store.ts:79](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L79) +[packages/framework/esm-extensions/src/store.ts:80](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L80) ___ @@ -54,7 +65,7 @@ ___ #### Defined in -[packages/framework/esm-extensions/src/store.ts:78](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L78) +[packages/framework/esm-extensions/src/store.ts:79](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L79) ___ @@ -64,7 +75,7 @@ ___ #### Defined in -[packages/framework/esm-extensions/src/store.ts:77](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L77) +[packages/framework/esm-extensions/src/store.ts:78](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L78) ___ @@ -74,7 +85,7 @@ ___ #### Defined in -[packages/framework/esm-extensions/src/store.ts:83](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L83) +[packages/framework/esm-extensions/src/store.ts:84](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L84) ___ @@ -84,4 +95,4 @@ ___ #### Defined in -[packages/framework/esm-extensions/src/store.ts:82](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L82) +[packages/framework/esm-extensions/src/store.ts:83](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L83) diff --git a/packages/framework/esm-framework/docs/interfaces/ConnectedExtension.md b/packages/framework/esm-framework/docs/interfaces/ConnectedExtension.md index 502d042c1..21d92ac2f 100644 --- a/packages/framework/esm-framework/docs/interfaces/ConnectedExtension.md +++ b/packages/framework/esm-framework/docs/interfaces/ConnectedExtension.md @@ -21,7 +21,7 @@ The extension's config. Note that this will be `null` until the slot is mounted. #### Defined in -[packages/framework/esm-extensions/src/store.ts:91](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L91) +[packages/framework/esm-extensions/src/store.ts:93](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L93) ___ @@ -31,7 +31,7 @@ ___ #### Defined in -[packages/framework/esm-extensions/src/store.ts:87](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L87) +[packages/framework/esm-extensions/src/store.ts:89](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L89) ___ @@ -41,7 +41,7 @@ ___ #### Defined in -[packages/framework/esm-extensions/src/store.ts:89](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L89) +[packages/framework/esm-extensions/src/store.ts:91](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L91) ___ @@ -51,4 +51,4 @@ ___ #### Defined in -[packages/framework/esm-extensions/src/store.ts:88](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L88) +[packages/framework/esm-extensions/src/store.ts:90](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L90) diff --git a/packages/framework/esm-framework/docs/interfaces/ExtensionRegistration.md b/packages/framework/esm-framework/docs/interfaces/ExtensionRegistration.md index b2fa0b30a..d1b90c3bd 100644 --- a/packages/framework/esm-framework/docs/interfaces/ExtensionRegistration.md +++ b/packages/framework/esm-framework/docs/interfaces/ExtensionRegistration.md @@ -6,6 +6,7 @@ ### Extension Properties +- [featureFlag](ExtensionRegistration.md#featureflag) - [meta](ExtensionRegistration.md#meta) - [moduleName](ExtensionRegistration.md#modulename) - [name](ExtensionRegistration.md#name) @@ -20,6 +21,16 @@ ## Extension Properties +### featureFlag + +• `Optional` **featureFlag**: `string` + +#### Defined in + +[packages/framework/esm-extensions/src/store.ts:25](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L25) + +___ + ### meta • **meta**: [`ExtensionMeta`](ExtensionMeta.md) diff --git a/packages/framework/esm-framework/docs/interfaces/ExtensionSlotState.md b/packages/framework/esm-framework/docs/interfaces/ExtensionSlotState.md index 4eee1caea..7aa12d6a7 100644 --- a/packages/framework/esm-framework/docs/interfaces/ExtensionSlotState.md +++ b/packages/framework/esm-framework/docs/interfaces/ExtensionSlotState.md @@ -17,7 +17,7 @@ #### Defined in -[packages/framework/esm-extensions/src/store.ts:72](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L72) +[packages/framework/esm-extensions/src/store.ts:73](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L73) ___ @@ -27,4 +27,4 @@ ___ #### Defined in -[packages/framework/esm-extensions/src/store.ts:71](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L71) +[packages/framework/esm-extensions/src/store.ts:72](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L72) diff --git a/packages/framework/esm-framework/docs/interfaces/ExtensionStore.md b/packages/framework/esm-framework/docs/interfaces/ExtensionStore.md index 38b564b97..ac468a615 100644 --- a/packages/framework/esm-framework/docs/interfaces/ExtensionStore.md +++ b/packages/framework/esm-framework/docs/interfaces/ExtensionStore.md @@ -16,4 +16,4 @@ #### Defined in -[packages/framework/esm-extensions/src/store.ts:67](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L67) +[packages/framework/esm-extensions/src/store.ts:68](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-extensions/src/store.ts#L68) diff --git a/packages/framework/esm-framework/docs/interfaces/OpenmrsAppRoutes.md b/packages/framework/esm-framework/docs/interfaces/OpenmrsAppRoutes.md index d100314a3..7e5d26f98 100644 --- a/packages/framework/esm-framework/docs/interfaces/OpenmrsAppRoutes.md +++ b/packages/framework/esm-framework/docs/interfaces/OpenmrsAppRoutes.md @@ -23,7 +23,7 @@ A list of backend modules necessary for this frontend module and the correspondi #### Defined in -[packages/framework/esm-globals/src/types.ts:236](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-globals/src/types.ts#L236) +[packages/framework/esm-globals/src/types.ts:240](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-globals/src/types.ts#L240) ___ @@ -35,7 +35,7 @@ An array of all extensions supported by this frontend module. Extensions can be #### Defined in -[packages/framework/esm-globals/src/types.ts:244](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-globals/src/types.ts#L244) +[packages/framework/esm-globals/src/types.ts:248](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-globals/src/types.ts#L248) ___ @@ -47,7 +47,7 @@ An array of all pages supported by this frontend module. Pages are automatically #### Defined in -[packages/framework/esm-globals/src/types.ts:240](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-globals/src/types.ts#L240) +[packages/framework/esm-globals/src/types.ts:244](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-globals/src/types.ts#L244) ___ @@ -59,4 +59,4 @@ The version of this frontend module. #### Defined in -[packages/framework/esm-globals/src/types.ts:232](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-globals/src/types.ts#L232) +[packages/framework/esm-globals/src/types.ts:236](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-globals/src/types.ts#L236) diff --git a/packages/framework/esm-framework/docs/interfaces/ResourceLoader.md b/packages/framework/esm-framework/docs/interfaces/ResourceLoader.md index 1b1fbb1f3..bfecb107e 100644 --- a/packages/framework/esm-framework/docs/interfaces/ResourceLoader.md +++ b/packages/framework/esm-framework/docs/interfaces/ResourceLoader.md @@ -20,4 +20,4 @@ #### Defined in -[packages/framework/esm-globals/src/types.ts:254](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-globals/src/types.ts#L254) +[packages/framework/esm-globals/src/types.ts:258](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-globals/src/types.ts#L258) diff --git a/packages/framework/esm-globals/src/types.ts b/packages/framework/esm-globals/src/types.ts index dfaa18c3b..fba68a5a6 100644 --- a/packages/framework/esm-globals/src/types.ts +++ b/packages/framework/esm-globals/src/types.ts @@ -190,9 +190,13 @@ export type ExtensionDefinition = { */ order?: number; /** - + * The user must have ANY of these privileges to see this extension. */ privileges?: string | Array; + /** + * If supplied, the extension will only be rendered when this feature flag is enabled. + */ + featureFlag?: string; /** * Meta describes any properties that are passed down to the extension when it is loaded */ diff --git a/packages/framework/esm-react-utils/__mocks__/openmrs-esm-state.mock.ts b/packages/framework/esm-react-utils/__mocks__/openmrs-esm-state.mock.ts index a059abdc9..bb34b7d98 100644 --- a/packages/framework/esm-react-utils/__mocks__/openmrs-esm-state.mock.ts +++ b/packages/framework/esm-react-utils/__mocks__/openmrs-esm-state.mock.ts @@ -62,6 +62,23 @@ export function getGlobalStore( return instrumentedStore(name, available.value); } +export function subscribeTo( + store: StoreApi, + select: (state: T) => U, + handle: (subState: U) => void +) { + let previous = select(store.getState()); + + return store.subscribe((state) => { + const current = select(state); + + if (current !== previous) { + previous = current; + handle(current); + } + }); +} + function instrumentedStore(name: string, store: StoreApi) { return { getState: jest.spyOn(store, "getState"), diff --git a/packages/framework/esm-react-utils/src/extensions.test.tsx b/packages/framework/esm-react-utils/src/extensions.test.tsx index 42c62ed10..9e61a8918 100644 --- a/packages/framework/esm-react-utils/src/extensions.test.tsx +++ b/packages/framework/esm-react-utils/src/extensions.test.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useReducer } from "react"; -import { render, screen, waitFor, within } from "@testing-library/react"; +import { act, render, screen, waitFor, within } from "@testing-library/react"; import { attach, ConnectedExtension, @@ -16,6 +16,10 @@ import { ExtensionData, } from "."; import userEvent from "@testing-library/user-event"; +import { + registerFeatureFlag, + setFeatureFlag, +} from "@openmrs/esm-feature-flags"; // For some reason in the text context `isEqual` always returns true // when using the import substitution in jest.config.js. Here's a custom @@ -293,13 +297,63 @@ describe("ExtensionSlot, Extension, and useExtensionSlotMeta", () => { "Hindi" ); }); + + test("Extensions behind feature flags only render when their feature flag is enabled", async () => { + registerSimpleExtension("Arabic", "esm-languages-app"); + registerSimpleExtension( + "Turkish", + "esm-languages-app", + undefined, + undefined, + "turkic" + ); + registerSimpleExtension( + "Turkmeni", + "esm-languages-app", + undefined, + undefined, + "turkic" + ); + registerSimpleExtension( + "Kurmanji", + "esm-languages-app", + undefined, + undefined, + "kurdish" + ); + attach("Box", "Arabic"); + attach("Box", "Turkish"); + attach("Box", "Turkmeni"); + attach("Box", "Kurmanji"); + registerFeatureFlag("turkic", "", ""); + registerFeatureFlag("kurdish", "", ""); + setFeatureFlag("turkic", true); + const App = openmrsComponentDecorator({ + moduleName: "esm-languages-app", + featureName: "Languages", + disableTranslations: true, + })(() => ); + render(); + + await waitFor(() => + expect(screen.getByText(/Turkmeni/)).toBeInTheDocument() + ); + expect(screen.getByText("Arabic")).toBeInTheDocument(); + expect(screen.getByText("Turkish")).toBeInTheDocument(); + expect(screen.queryByText("Kurmanji")).not.toBeInTheDocument(); + act(() => setFeatureFlag("kurdish", true)); + await waitFor(() => + expect(screen.getByText("Kurmanji")).toBeInTheDocument() + ); + }); }); function registerSimpleExtension( name: string, moduleName: string, Component?: React.ComponentType, - meta: object = {} + meta: object = {}, + featureFlag?: string ) { const SimpleComponent = () =>
{name}
; registerExtension({ @@ -311,5 +365,6 @@ function registerSimpleExtension( disableTranslations: true, }), meta, + featureFlag, }); } diff --git a/packages/framework/esm-react-utils/src/useConnectedExtensions.ts b/packages/framework/esm-react-utils/src/useConnectedExtensions.ts index abd260fbe..992c10b52 100644 --- a/packages/framework/esm-react-utils/src/useConnectedExtensions.ts +++ b/packages/framework/esm-react-utils/src/useConnectedExtensions.ts @@ -6,10 +6,12 @@ import { } from "@openmrs/esm-extensions"; import { useConnectivity } from "./useConnectivity"; import { useAssignedExtensions } from "./useAssignedExtensions"; +import { useStore } from "./useStore"; +import { featureFlagsStore } from "@openmrs/esm-feature-flags"; /** * Gets the assigned extension for a given extension slot name. - * Considers if offline or online. + * Considers if offline or online, and what feature flags are enabled. * @param slotName The name of the slot to get the assigned extensions for. */ export function useConnectedExtensions( @@ -17,10 +19,21 @@ export function useConnectedExtensions( ): Array { const online = useConnectivity(); const assignedExtensions = useAssignedExtensions(slotName); + const featureFlagStore = useStore(featureFlagsStore); + + const enabledFeatureFlags = useMemo(() => { + return Object.entries(featureFlagStore.flags) + .filter(([, { enabled }]) => enabled) + .map(([name]) => name); + }, [featureFlagStore.flags]); const connectedExtensions = useMemo(() => { - return getConnectedExtensions(assignedExtensions, online); - }, [assignedExtensions, online]); + return getConnectedExtensions( + assignedExtensions, + online, + enabledFeatureFlags + ); + }, [assignedExtensions, online, enabledFeatureFlags]); return connectedExtensions; } diff --git a/packages/shell/esm-app-shell/src/apps.ts b/packages/shell/esm-app-shell/src/apps.ts index d6c85c00f..253fec3fb 100644 --- a/packages/shell/esm-app-shell/src/apps.ts +++ b/packages/shell/esm-app-shell/src/apps.ts @@ -318,6 +318,7 @@ supported, so the extension will not be loaded.` privileges: extension.privileges, online: extension.online, offline: extension.offline, + featureFlag: extension.featureFlag, }); for (const slot of slots) { diff --git a/yarn.lock b/yarn.lock index 5afa2b8a3..1fb4e8510 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3664,12 +3664,14 @@ __metadata: dependencies: "@openmrs/esm-api": ^5.0.2 "@openmrs/esm-config": ^5.0.2 + "@openmrs/esm-feature-flags": ^5.0.2 "@openmrs/esm-state": ^5.0.2 lodash-es: ^4.17.21 single-spa: ^5.9.2 peerDependencies: "@openmrs/esm-api": 5.x "@openmrs/esm-config": 5.x + "@openmrs/esm-feature-flags": 5.x "@openmrs/esm-state": 5.x single-spa: 5.x languageName: unknown From 38329b38f1c35baab79dc1c89d90dc02ca659af9 Mon Sep 17 00:00:00 2001 From: Brandon Istenes Date: Mon, 17 Jul 2023 13:05:46 -0400 Subject: [PATCH 2/2] Hail Mary --- .../src/integration-tests/extension-config.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framework/esm-framework/src/integration-tests/extension-config.test.tsx b/packages/framework/esm-framework/src/integration-tests/extension-config.test.tsx index 37c232619..a2a14157a 100644 --- a/packages/framework/esm-framework/src/integration-tests/extension-config.test.tsx +++ b/packages/framework/esm-framework/src/integration-tests/extension-config.test.tsx @@ -253,8 +253,8 @@ describe("Interaction between configuration and extension systems", () => { }); await waitFor(() => { - expect(screen.queryByText("green")).not.toBeInTheDocument(); expect(screen.getByTestId("slot")).toHaveTextContent(/black/); + expect(screen.queryByText("green")).not.toBeInTheDocument(); }); });