{extension.meta.code}
@@ -226,33 +214,6 @@ describe('ExtensionSlot, Extension, and useExtensionSlotMeta', () => {
expect(within(screen.getByTestId('Hindi')).getByRole('heading')).toHaveTextContent('hi');
});
- test('Extension renders with child function', async () => {
- registerSimpleExtension('Hindi', 'esm-languages-app', undefined, {
- code: 'hi',
- });
- attach('Box', 'Hindi');
- const App = openmrsComponentDecorator({
- moduleName: 'esm-languages-app',
- featureName: 'Languages',
- disableTranslations: true,
- })(() => {
- return (
-
-
- {() => {(slot) => {slot}
}}
-
-
- );
- });
-
- render(
);
-
- await waitFor(() => expect(screen.getByTestId('custom-wrapper')).toBeInTheDocument());
-
- // essentially: is the first child of custom-wrapper the extension?
- expect(screen.getByTestId('custom-wrapper').children[0]).toHaveAttribute('data-extension-id', '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');
diff --git a/packages/framework/esm-react-utils/src/index.ts b/packages/framework/esm-react-utils/src/index.ts
index 3d5cdfc6a..6d0148090 100644
--- a/packages/framework/esm-react-utils/src/index.ts
+++ b/packages/framework/esm-react-utils/src/index.ts
@@ -20,6 +20,7 @@ export * from './useDefineAppContext';
export * from './useExtensionInternalStore';
export * from './useExtensionSlot';
export * from './useExtensionSlotMeta';
+export * from './useExtensionSlotStore';
export * from './useExtensionStore';
export * from './useFeatureFlag';
export * from './useForceUpdate';
diff --git a/packages/framework/esm-react-utils/src/public.ts b/packages/framework/esm-react-utils/src/public.ts
index d9e2da022..f5e4b4ca1 100644
--- a/packages/framework/esm-react-utils/src/public.ts
+++ b/packages/framework/esm-react-utils/src/public.ts
@@ -18,6 +18,7 @@ export * from './useDebounce';
export * from './useDefineAppContext';
export * from './useExtensionSlotMeta';
export * from './useExtensionStore';
+export * from './useExtensionSlotStore';
export * from './useFeatureFlag';
export * from './useLayoutType';
export * from './useLocations';
diff --git a/packages/framework/esm-react-utils/src/useAssignedExtensionIds.ts b/packages/framework/esm-react-utils/src/useAssignedExtensionIds.ts
index 8ebbd38b1..8b1647c27 100644
--- a/packages/framework/esm-react-utils/src/useAssignedExtensionIds.ts
+++ b/packages/framework/esm-react-utils/src/useAssignedExtensionIds.ts
@@ -1,7 +1,7 @@
/** @module @category Extension */
import { useEffect, useState } from 'react';
import { getExtensionStore } from '@openmrs/esm-extensions';
-import isEqual from 'lodash-es/isEqual';
+import { isEqual } from 'lodash-es';
/**
* Gets the assigned extension ids for a given extension slot name.
diff --git a/packages/framework/esm-react-utils/src/useAssignedExtensions.ts b/packages/framework/esm-react-utils/src/useAssignedExtensions.ts
index 1918e859f..ebdbdf802 100644
--- a/packages/framework/esm-react-utils/src/useAssignedExtensions.ts
+++ b/packages/framework/esm-react-utils/src/useAssignedExtensions.ts
@@ -1,18 +1,11 @@
/** @module @category Extension */
-import { useMemo } from 'react';
-import { useExtensionStore } from './useExtensionStore';
+import { useExtensionSlotStore } from './useExtensionSlotStore';
/**
* Gets the assigned extensions for a given extension slot name.
- * Does not consider if offline or online.
* @param slotName The name of the slot to get the assigned extensions for.
*/
export function useAssignedExtensions(slotName: string) {
- const { slots } = useExtensionStore();
-
- const extensions = useMemo(() => {
- return slots[slotName]?.assignedExtensions ?? [];
- }, [slots, slotName]);
-
- return extensions;
+ const slotStore = useExtensionSlotStore(slotName);
+ return slotStore?.assignedExtensions;
}
diff --git a/packages/framework/esm-react-utils/src/useConnectedExtensions.ts b/packages/framework/esm-react-utils/src/useConnectedExtensions.ts
index 20b808df6..e6db27d84 100644
--- a/packages/framework/esm-react-utils/src/useConnectedExtensions.ts
+++ b/packages/framework/esm-react-utils/src/useConnectedExtensions.ts
@@ -1,31 +1,10 @@
/** @module @category Extension */
-import { useMemo } from 'react';
-import type { ConnectedExtension } from '@openmrs/esm-extensions';
-import { getConnectedExtensions } from '@openmrs/esm-extensions';
-import { useConnectivity } from './useConnectivity';
+import { type ConnectedExtension } from '@openmrs/esm-extensions';
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, and what feature flags are enabled.
* @param slotName The name of the slot to get the assigned extensions for.
+ * @deprecated Use useAssignedExtensions instead
*/
-export function useConnectedExtensions(slotName: string): 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, enabledFeatureFlags);
- }, [assignedExtensions, online, enabledFeatureFlags]);
-
- return connectedExtensions;
-}
+export const useConnectedExtensions = useAssignedExtensions as (slotName: string) => Array;
diff --git a/packages/framework/esm-react-utils/src/useExtensionSlot.ts b/packages/framework/esm-react-utils/src/useExtensionSlot.ts
index ab2e67104..fe0786d57 100644
--- a/packages/framework/esm-react-utils/src/useExtensionSlot.ts
+++ b/packages/framework/esm-react-utils/src/useExtensionSlot.ts
@@ -1,7 +1,7 @@
import { useContext, useEffect } from 'react';
import { registerExtensionSlot } from '@openmrs/esm-extensions';
import { ComponentContext } from './ComponentContext';
-import { useConnectedExtensions } from './useConnectedExtensions';
+import { useAssignedExtensions } from './useAssignedExtensions';
/** @internal */
export function useExtensionSlot(slotName: string) {
@@ -15,7 +15,7 @@ export function useExtensionSlot(slotName: string) {
registerExtensionSlot(moduleName, slotName);
}, []);
- const extensions = useConnectedExtensions(slotName);
+ const extensions = useAssignedExtensions(slotName);
return {
extensions,
diff --git a/packages/framework/esm-react-utils/src/useExtensionSlotMeta.ts b/packages/framework/esm-react-utils/src/useExtensionSlotMeta.ts
index 59fbbf76b..5f5fc525b 100644
--- a/packages/framework/esm-react-utils/src/useExtensionSlotMeta.ts
+++ b/packages/framework/esm-react-utils/src/useExtensionSlotMeta.ts
@@ -1,14 +1,14 @@
/** @module @category Extension */
import type { ExtensionMeta } from '@openmrs/esm-extensions';
import { useMemo } from 'react';
-import { useConnectedExtensions } from './useConnectedExtensions';
+import { useAssignedExtensions } from './useAssignedExtensions';
/**
* Extract meta data from all extension for a given extension slot.
* @param extensionSlotName
*/
export function useExtensionSlotMeta(extensionSlotName: string) {
- const extensions = useConnectedExtensions(extensionSlotName);
+ const extensions = useAssignedExtensions(extensionSlotName);
return useMemo(() => Object.fromEntries(extensions.map((ext) => [ext.name, ext.meta as T])), [extensions]);
}
diff --git a/packages/framework/esm-react-utils/src/useExtensionSlotStore.ts b/packages/framework/esm-react-utils/src/useExtensionSlotStore.ts
new file mode 100644
index 000000000..cadc97462
--- /dev/null
+++ b/packages/framework/esm-react-utils/src/useExtensionSlotStore.ts
@@ -0,0 +1,6 @@
+/** @module @category Extension */
+import { type ExtensionSlotState, type ExtensionStore, getExtensionStore } from '@openmrs/esm-extensions';
+import { useStore } from './useStore';
+
+export const useExtensionSlotStore = (slot: string) =>
+ useStore(getExtensionStore(), (state) => state.slots?.[slot]);
diff --git a/packages/framework/esm-react-utils/src/useExtensionStore.ts b/packages/framework/esm-react-utils/src/useExtensionStore.ts
index c5f6781ba..8f7749168 100644
--- a/packages/framework/esm-react-utils/src/useExtensionStore.ts
+++ b/packages/framework/esm-react-utils/src/useExtensionStore.ts
@@ -1,6 +1,5 @@
/** @module @category Extension */
-import type { ExtensionStore } from '@openmrs/esm-extensions';
-import { getExtensionStore } from '@openmrs/esm-extensions';
+import { type ExtensionStore, getExtensionStore } from '@openmrs/esm-extensions';
import { createUseStore } from './useStore';
export const useExtensionStore = createUseStore(getExtensionStore());
diff --git a/packages/framework/esm-react-utils/src/useStore.ts b/packages/framework/esm-react-utils/src/useStore.ts
index c4d7e6f92..48688c3cc 100644
--- a/packages/framework/esm-react-utils/src/useStore.ts
+++ b/packages/framework/esm-react-utils/src/useStore.ts
@@ -17,7 +17,7 @@ function bindActions(store: StoreApi, actions: Actions): BoundActions {
const bound = {};
for (let i in actions) {
- bound[i] = function (...args) {
+ bound[i] = function (...args: Array) {
store.setState((state) => {
let _args = [state, ...args];
return actions[i](..._args);
@@ -28,13 +28,16 @@ function bindActions(store: StoreApi, actions: Actions): BoundActions {
return bound;
}
-const defaultSelectFunction = (x) => x;
+const defaultSelectFunction =
+ () =>
+ (x: T) =>
+ x as unknown as U;
function useStore(store: StoreApi): T;
function useStore(store: StoreApi, select: (state: T) => U): U;
function useStore(store: StoreApi, select: undefined, actions: Actions): T & BoundActions;
function useStore(store: StoreApi, select: (state: T) => U, actions: Actions): U & BoundActions;
-function useStore(store: StoreApi, select: (state: T) => U = defaultSelectFunction, actions?: Actions) {
+function useStore(store: StoreApi, select: (state: T) => U = defaultSelectFunction(), actions?: Actions) {
const [state, setState] = useState(() => select(store.getState()));
useEffect(() => subscribeTo(store, select, setState), [store, select]);
@@ -50,7 +53,7 @@ function useStore(store: StoreApi, select: (state: T) => U = defaultSel
* @returns
*/
function useStoreWithActions(store: StoreApi, actions: Actions): T & BoundActions {
- return useStore(store, defaultSelectFunction, actions);
+ return useStore(store, defaultSelectFunction(), actions);
}
/**
diff --git a/packages/framework/esm-routes/src/loaders/components.ts b/packages/framework/esm-routes/src/loaders/components.ts
index 060d524f3..791fb7541 100644
--- a/packages/framework/esm-routes/src/loaders/components.ts
+++ b/packages/framework/esm-routes/src/loaders/components.ts
@@ -12,7 +12,7 @@ import {
type WorkspaceDefinition,
} from '@openmrs/esm-globals';
import { getLoader } from './app';
-import { FeatureFlag, registerFeatureFlag } from '@openmrs/esm-feature-flags';
+import { registerFeatureFlag } from '@openmrs/esm-feature-flags';
/**
* This function registers an extension definition with the framework and will
@@ -204,7 +204,7 @@ supported, so the workspace will not be loaded.`,
* This function registers a workspace definition with the framework so that it can be launched.
*
* @param appName The name of the app defining this workspace
- * @param workspace An object that describes the workspace, derived from `routes.json`
+ * @param featureFlag An object that describes the workspace, derived from `routes.json`
*/
export function tryRegisterFeatureFlag(appName: string, featureFlag: FeatureFlagDefinition) {
const name = featureFlag.flagName;
diff --git a/packages/framework/esm-state/package.json b/packages/framework/esm-state/package.json
index 406e0fcff..4f78b7b8a 100644
--- a/packages/framework/esm-state/package.json
+++ b/packages/framework/esm-state/package.json
@@ -41,9 +41,11 @@
"zustand": "^4.5.5"
},
"peerDependencies": {
- "@openmrs/esm-globals": "5.x"
+ "@openmrs/esm-globals": "5.x",
+ "@openmrs/esm-utils": "5.x"
},
"devDependencies": {
- "@openmrs/esm-globals": "workspace:*"
+ "@openmrs/esm-globals": "workspace:*",
+ "@openmrs/esm-utils": "workspace:*"
}
}
diff --git a/packages/framework/esm-state/src/state.ts b/packages/framework/esm-state/src/state.ts
index ce0d4701f..94c8d527c 100644
--- a/packages/framework/esm-state/src/state.ts
+++ b/packages/framework/esm-state/src/state.ts
@@ -1,7 +1,8 @@
/** @module @category Store */
+import type {} from '@openmrs/esm-globals';
+import { shallowEqual } from '@openmrs/esm-utils';
import type { StoreApi } from 'zustand/vanilla';
import { createStore } from 'zustand/vanilla';
-import type {} from '@openmrs/esm-globals';
interface StoreEntity {
value: StoreApi;
@@ -31,7 +32,7 @@ export function createGlobalStore(name: string, initialState: T): StoreApi
if (available) {
if (available.active) {
- console.error('Cannot override an existing store. Make sure that stores are only created once.');
+ console.error(`Attempted to override the existing store ${name}. Make sure that stores are only created once.`);
} else {
available.value.setState(initialState, true);
}
@@ -63,7 +64,7 @@ export function registerGlobalStore(name: string, store: StoreApi): StoreA
if (available) {
if (available.active) {
- console.error('Cannot override an existing store. Make sure that stores are only created once.');
+ console.error(`Attempted to override the existing store ${name}. Make sure that stores are only created once.`);
} else {
available.value = store;
}
@@ -103,15 +104,25 @@ export function getGlobalStore(name: string, fallbackState?: T): StoreApi
return available.value as StoreApi;
}
-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);
+type SubscribeToArgs = [StoreApi, (state: T) => void] | [StoreApi, (state: T) => U, (state: U) => void];
+
+export function subscribeTo(store: StoreApi, handle: (state: T) => void): () => void;
+export function subscribeTo(
+ store: StoreApi,
+ select: (state: T) => U,
+ handle: (subState: U) => void,
+): () => void;
+export function subscribeTo(...args: SubscribeToArgs): () => void {
+ const [store, select, handle] = args;
+ const handler = typeof handle === 'undefined' ? (select as unknown as (state: U) => void) : handle;
+ const selector = typeof handle === 'undefined' ? (state: T) => state as unknown as U : (select as (state: T) => U);
+
+ handler(selector(store.getState()));
+ return store.subscribe((state, previous) => {
+ const current = selector(state);
+
+ if (!shallowEqual(previous, current)) {
+ handler(current);
}
});
}
diff --git a/packages/framework/esm-styleguide/src/patient-banner/actions-menu/patient-banner-actions-menu.component.tsx b/packages/framework/esm-styleguide/src/patient-banner/actions-menu/patient-banner-actions-menu.component.tsx
index 59784ad64..8b8be1e85 100644
--- a/packages/framework/esm-styleguide/src/patient-banner/actions-menu/patient-banner-actions-menu.component.tsx
+++ b/packages/framework/esm-styleguide/src/patient-banner/actions-menu/patient-banner-actions-menu.component.tsx
@@ -1,7 +1,7 @@
/** @module @category UI */
import React, { useMemo } from 'react';
import { OverflowMenuVertical } from '@carbon/react/icons';
-import { ExtensionSlot, useConnectedExtensions, usePatient } from '@openmrs/esm-react-utils';
+import { ExtensionSlot, useExtensionSlot } from '@openmrs/esm-react-utils';
import { getCoreTranslation } from '@openmrs/esm-translations';
import { CustomOverflowMenu } from '../../custom-overflow-menu/custom-overflow-menu.component';
import styles from './patient-banner-actions-menu.module.scss';
@@ -25,7 +25,7 @@ export function PatientBannerActionsMenu({
isDeceased,
additionalActionsSlotState,
}: PatientBannerActionsMenuProps) {
- const patientActions = useConnectedExtensions(actionsSlotName);
+ const { extensions: patientActions } = useExtensionSlot(actionsSlotName);
const patientActionsSlotState = useMemo(
() => ({ patientUuid, patient, ...additionalActionsSlotState }),
[patientUuid, additionalActionsSlotState],
diff --git a/packages/framework/esm-styleguide/src/workspaces/container/workspace-container.component.tsx b/packages/framework/esm-styleguide/src/workspaces/container/workspace-container.component.tsx
index ce62af9cf..879bfaeab 100644
--- a/packages/framework/esm-styleguide/src/workspaces/container/workspace-container.component.tsx
+++ b/packages/framework/esm-styleguide/src/workspaces/container/workspace-container.component.tsx
@@ -5,7 +5,6 @@ import { DownToBottom, Maximize, Minimize } from '@carbon/react/icons';
import { ComponentContext, ExtensionSlot, isDesktop, useBodyScrollLock, useLayoutType } from '@openmrs/esm-react-utils';
import { getCoreTranslation } from '@openmrs/esm-translations';
import { I18nextProvider, useTranslation } from 'react-i18next';
-
import { ArrowLeftIcon, ArrowRightIcon, CloseIcon } from '../../icons';
import { WorkspaceNotification } from '../notification/workspace-notification.component';
import ActionMenu from './action-menu.component';
diff --git a/packages/framework/esm-styleguide/src/workspaces/container/workspace-container.test.tsx b/packages/framework/esm-styleguide/src/workspaces/container/workspace-container.test.tsx
index 8e9708f12..8c7dfa8c7 100644
--- a/packages/framework/esm-styleguide/src/workspaces/container/workspace-container.test.tsx
+++ b/packages/framework/esm-styleguide/src/workspaces/container/workspace-container.test.tsx
@@ -1,6 +1,6 @@
///
import React from 'react';
-import { screen, render, within, renderHook, act } from '@testing-library/react';
+import { act, screen, renderHook, render, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { registerWorkspace } from '@openmrs/esm-extensions';
import { ComponentContext, isDesktop, useLayoutType } from '@openmrs/esm-react-utils';
@@ -17,6 +17,11 @@ jest.mock('./workspace-renderer.component.tsx', () => {
};
});
+jest.mock('react-i18next', () => ({
+ ...jest.requireActual('react-i18next'),
+ useTranslation: jest.fn().mockImplementation(() => ({ t: (arg: string) => arg })),
+}));
+
const mockedIsDesktop = isDesktop as unknown as jest.Mock;
const mockedUseLayoutType = useLayoutType as jest.Mock;
@@ -195,8 +200,8 @@ describe('WorkspaceContainer in overlay mode', () => {
it('opens with overridable title and closes', async () => {
mockedUseLayoutType.mockReturnValue('small-desktop');
const user = userEvent.setup();
- act(() => launchWorkspace('patient-search', { workspaceTitle: 'Make an appointment' }));
renderWorkspaceOverlay();
+ act(() => launchWorkspace('patient-search', { workspaceTitle: 'Make an appointment' }));
expect(screen.queryByRole('complementary')).toBeInTheDocument();
expectToBeVisible(screen.getByRole('complementary'));
diff --git a/packages/framework/esm-styleguide/src/workspaces/workspaces.ts b/packages/framework/esm-styleguide/src/workspaces/workspaces.ts
index 613556874..4b56873ef 100644
--- a/packages/framework/esm-styleguide/src/workspaces/workspaces.ts
+++ b/packages/framework/esm-styleguide/src/workspaces/workspaces.ts
@@ -208,7 +208,7 @@ export function launchWorkspace<
function updateStoreWithNewWorkspace(workspaceToBeAdded: OpenWorkspace, restOfTheWorkspaces?: Array) {
store.setState((state) => {
const openWorkspaces = [workspaceToBeAdded, ...(restOfTheWorkspaces ?? state.openWorkspaces)];
- let workspaceWindowState = getUpdatedWorkspaceWindowState(openWorkspaces[0]);
+ let workspaceWindowState = getUpdatedWorkspaceWindowState(workspaceToBeAdded);
return {
...state,
@@ -233,8 +233,8 @@ export function launchWorkspace<
} else if (isWorkspaceAlreadyOpen) {
const openWorkspace = openWorkspaces[workspaceIndexInOpenWorkspaces];
// Only update the title if it hasn't been set by `setTitle`
- if (openWorkspace.title == getWorkspaceTitle(openWorkspace, openWorkspace.additionalProps)) {
- openWorkspace.title = getWorkspaceTitle(openWorkspace, newWorkspace.additionalProps);
+ if (openWorkspace.title === getWorkspaceTitle(openWorkspace, openWorkspace.additionalProps)) {
+ openWorkspace.title = getWorkspaceTitle(newWorkspace, newWorkspace.additionalProps);
}
openWorkspace.additionalProps = newWorkspace.additionalProps;
const restOfTheWorkspaces = openWorkspaces.filter((w) => w.name != name);
@@ -376,7 +376,7 @@ const initialState: WorkspaceStoreState = {
export const workspaceStore = createGlobalStore('workspace', initialState);
export function getWorkspaceStore() {
- return getGlobalStore('workspace', initialState);
+ return workspaceStore;
}
export function updateWorkspaceWindowState(value: WorkspaceWindowState) {
diff --git a/yarn.lock b/yarn.lock
index 37ff6a025..3e0a1da42 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3369,9 +3369,11 @@ __metadata:
resolution: "@openmrs/esm-state@workspace:packages/framework/esm-state"
dependencies:
"@openmrs/esm-globals": "workspace:*"
+ "@openmrs/esm-utils": "workspace:*"
zustand: "npm:^4.5.5"
peerDependencies:
"@openmrs/esm-globals": 5.x
+ "@openmrs/esm-utils": 5.x
languageName: unknown
linkType: soft