Skip to content

Commit

Permalink
(feat) O3-3857: Support extension overrides in per-slot extension con…
Browse files Browse the repository at this point in the history
…fig (#1119)
  • Loading branch information
ibacher authored Aug 23, 2024
1 parent 237bda7 commit 362d430
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 37 deletions.
2 changes: 2 additions & 0 deletions packages/framework/esm-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@
"peerDependencies": {
"@openmrs/esm-globals": "5.x",
"@openmrs/esm-state": "5.x",
"@openmrs/esm-utils": "5.x",
"single-spa": "5.x"
},
"devDependencies": {
"@openmrs/esm-globals": "workspace:*",
"@openmrs/esm-state": "workspace:*",
"@openmrs/esm-utils": "workspace:*",
"@types/ramda": "^0.26.44",
"single-spa": "^6.0.1"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1108,8 +1108,13 @@ describe('extension config', () => {
const moduleLevelConfig = { 'ext-mod': { bar: 'qux' } };
updateConfigExtensionStore();
Config.provide(moduleLevelConfig);
const result = getExtensionConfig('barSlot', 'fooExt').config;
expect(result).toStrictEqual({ bar: 'qux', baz: 'bazzy' });
const result = getExtensionConfig('barSlot', 'fooExt').getState().config;
expect(result).toStrictEqual({
bar: 'qux',
baz: 'bazzy',
'Display conditions': { privileges: [] },
'Translation overrides': {},
});
expect(console.error).not.toHaveBeenCalled();
});

Expand All @@ -1126,8 +1131,13 @@ describe('extension config', () => {
},
};
Config.provide(configureConfig);
const result = getExtensionConfig('barSlot', 'fooExt#id0').config;
expect(result).toStrictEqual({ bar: 'qux', baz: 'quiz' });
const result = getExtensionConfig('barSlot', 'fooExt#id0').getState().config;
expect(result).toStrictEqual({
bar: 'qux',
baz: 'quiz',
'Display conditions': { privileges: [] },
'Translation overrides': {},
});
expect(console.error).not.toHaveBeenCalled();
});

Expand All @@ -1154,8 +1164,12 @@ describe('extension config', () => {
});
const extensionAtBaseConfig = { fooExt: { qux: 'quxolotl' } };
Config.provide(extensionAtBaseConfig);
const result = getExtensionConfig('barSlot', 'fooExt').config;
expect(result).toStrictEqual({ qux: 'quxolotl' });
const result = getExtensionConfig('barSlot', 'fooExt').getState().config;
expect(result).toStrictEqual({
qux: 'quxolotl',
'Display conditions': { privileges: [] },
'Translation overrides': {},
});
expect(console.error).not.toHaveBeenCalled();
});

Expand All @@ -1175,8 +1189,12 @@ describe('extension config', () => {
},
};
Config.provide(configureConfig);
const result = getExtensionConfig('barSlot', 'fooExt#id2').config;
expect(result).toStrictEqual({ qux: 'quxotic' });
const result = getExtensionConfig('barSlot', 'fooExt#id2').getState().config;
expect(result).toStrictEqual({
qux: 'quxotic',
'Display conditions': { privileges: [] },
'Translation overrides': {},
});
});

it('validates the extension configure config, with extension config schema', () => {
Expand Down
53 changes: 40 additions & 13 deletions packages/framework/esm-config/src/module-config/module-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import type { Config, ConfigObject, ConfigSchema, ExtensionSlotConfig, Extension
import { Type } from '../types';
import { isArray, isBoolean, isUuid, isNumber, isObject, isString } from '../validators/type-validators';
import { validator } from '../validators/validator';
import type { ConfigExtensionStore, ConfigInternalStore, ConfigStore } from './state';
import { type ConfigExtensionStore, type ConfigInternalStore, type ConfigStore } from './state';
import {
configInternalStore,
configExtensionStore,
getConfigStore,
getExtensionConfig,
getExtensionsConfigStore,
implementerToolsConfigStore,
temporaryConfigStore,
Expand Down Expand Up @@ -136,6 +137,7 @@ function computeExtensionConfigs(
configState,
tempConfigState,
);

configs[extension.slotName] = {
...configs[extension.slotName],
[extension.extensionId]: { config, loaded: true },
Expand Down Expand Up @@ -270,19 +272,44 @@ export function getConfig<T = Record<string, any>>(moduleName: string): Promise<
}

/** @internal */
export function getTranslationOverrides(moduleName: string): Promise<Object> {
return new Promise<Object>((resolve) => {
const store = getConfigStore(moduleName);
function update(state: ConfigStore) {
if (state.translationOverridesLoaded && state.config) {
const translationOverrides = state.config['Translation overrides'] ?? {};
resolve(translationOverrides);
unsubscribe && unsubscribe();
export function getTranslationOverrides(
moduleName: string,
slotName?: string,
extensionId?: string,
): Promise<Record<string, Record<string, string>>> {
const promises = [
new Promise<Record<string, Record<string, string>>>((resolve) => {
const configStore = getConfigStore(moduleName);
function update(state: ReturnType<(typeof configStore)['getState']>) {
if (state.translationOverridesLoaded && state.config) {
const translationOverrides = state.config['Translation overrides'] ?? {};
resolve(translationOverrides);
unsubscribe && unsubscribe();
}
}
}
update(store.getState());
const unsubscribe = store.subscribe(update);
});
update(configStore.getState());
const unsubscribe = configStore.subscribe(update);
}),
];

if (slotName && extensionId) {
promises.push(
new Promise<Record<string, Record<string, string>>>((resolve) => {
const configStore = getExtensionConfig(slotName, extensionId);
function update(state: ReturnType<(typeof configStore)['getState']>) {
if (state.loaded && state.config) {
const translationOverrides = state.config['Translation overrides'] ?? {};
resolve(translationOverrides);
unsubscribe && unsubscribe();
}
}
update(configStore.getState());
const unsubscribe = configStore.subscribe(update);
}),
);
}

return Promise.all(promises).then((results) => results.reduce((prev, current) => mergeDeepRight(prev, current), {}));
}

/**
Expand Down
67 changes: 59 additions & 8 deletions packages/framework/esm-config/src/module-config/state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @module @category Config */
import { createGlobalStore, getGlobalStore } from '@openmrs/esm-state';
import { omit } from 'ramda';
import { shallowEqual } from '@openmrs/esm-utils';
import { type StoreApi } from 'zustand';
import type { Config, ConfigObject, ConfigSchema, ExtensionSlotConfigObject, ProvidedConfig } from '../types';

/**
Expand Down Expand Up @@ -166,13 +167,63 @@ export function getExtensionsConfigStore() {
}

/** @internal */
export function getExtensionConfig(slotName: string, extensionId: string) {
const extensionConfig = Object.assign(
{},
getExtensionConfigFromStore(getExtensionsConfigStore().getState(), slotName, extensionId),
);
extensionConfig.config = omit(['Display conditions', 'Translation overrides'], extensionConfig.config);
return extensionConfig;
export function getExtensionConfig(
slotName: string,
extensionId: string,
): StoreApi<Omit<ConfigStore, 'translationOverridesLoaded'>> {
if (
typeof slotName !== 'string' ||
typeof extensionId !== 'string' ||
slotName === '__proto__' ||
extensionId === '__proto__' ||
slotName === 'constructor' ||
extensionId === 'constructor' ||
slotName === 'prototype' ||
extensionId === 'prototype'
) {
throw new Error('Attempted to call `getExtensionConfig()` with invalid argument');
}

const extensionConfigStore = getExtensionsConfigStore();
const selector = (configStore: ExtensionsConfigStore) => configStore.configs[slotName]?.[extensionId];

return {
getState() {
return selector(extensionConfigStore.getState()) ?? { loaded: false, config: null };
},
setState(
partial: ConfigStore | Partial<ConfigStore> | ((state: ConfigStore) => ConfigStore | Partial<ConfigStore>),
replace: boolean = false,
) {
extensionConfigStore.setState((state) => {
if (!state.configs[slotName]) {
state.configs[slotName] = {};
}

const newState = typeof partial === 'function' ? partial(state.configs.slotName[extensionId]) : partial;
if (replace) {
state.configs[slotName][extensionId] = Object.assign({}, newState) as ConfigStore;
} else {
state.configs[slotName][extensionId] = Object.assign({}, state.configs[slotName][extensionId], newState);
}

return state;
});
},
subscribe(listener) {
return extensionConfigStore.subscribe((state, prevState) => {
const newState = selector(state);
const oldState = selector(prevState);

if (!shallowEqual(newState, oldState)) {
listener(newState, oldState);
}
});
},
destroy() {
/* this is a no-op */
},
};
}

/** @internal */
Expand Down
8 changes: 4 additions & 4 deletions packages/framework/esm-framework/docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -3051,7 +3051,7 @@ for more information about defining a config schema.

#### Defined in

[packages/framework/esm-config/src/module-config/module-config.ts:164](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-config/src/module-config/module-config.ts#L164)
[packages/framework/esm-config/src/module-config/module-config.ts:166](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-config/src/module-config/module-config.ts#L166)

___

Expand Down Expand Up @@ -3083,7 +3083,7 @@ for more information about defining a config schema.

#### Defined in

[packages/framework/esm-config/src/module-config/module-config.ts:225](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-config/src/module-config/module-config.ts#L225)
[packages/framework/esm-config/src/module-config/module-config.ts:227](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-config/src/module-config/module-config.ts#L227)

___

Expand Down Expand Up @@ -3115,7 +3115,7 @@ of the execution of a function.

#### Defined in

[packages/framework/esm-config/src/module-config/module-config.ts:257](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-config/src/module-config/module-config.ts#L257)
[packages/framework/esm-config/src/module-config/module-config.ts:259](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-config/src/module-config/module-config.ts#L259)

___

Expand All @@ -3136,7 +3136,7 @@ ___

#### Defined in

[packages/framework/esm-config/src/module-config/module-config.ts:241](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-config/src/module-config/module-config.ts#L241)
[packages/framework/esm-config/src/module-config/module-config.ts:243](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-config/src/module-config/module-config.ts#L243)

___

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,14 @@ export function openmrsComponentDecorator<T>(userOpts: ComponentDecoratorOptions
{opts.disableTranslations ? (
<Comp {...this.props} />
) : (
<I18nextProvider i18n={window.i18next} defaultNS={opts.moduleName}>
<I18nextProvider
i18n={window.i18next}
defaultNS={
this.props._extensionContext
? `${opts.moduleName}___${this.props._extensionContext.extensionSlotName}___${this.props._extensionContext.extensionId}`
: opts.moduleName
}
>
<Comp {...this.props} />
</I18nextProvider>
)}
Expand Down
7 changes: 4 additions & 3 deletions packages/shell/esm-app-shell/src/locale.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import merge from 'lodash-es/merge';
import { merge } from 'lodash-es';
import {
getTranslationOverrides,
importDynamic,
Expand Down Expand Up @@ -55,9 +55,10 @@ export function setupI18n() {
callback(err, null);
});
} else {
importDynamic(namespace)
const [ns, slotName, extensionId] = namespace.split('___');
importDynamic(ns)
.then((module) =>
Promise.all([getImportPromise(module, namespace, language), getTranslationOverrides(namespace)]),
Promise.all([getImportPromise(module, ns, language), getTranslationOverrides(ns, slotName, extensionId)]),
)
.then(([json, overrides]) => {
let translations = json ?? {};
Expand Down
2 changes: 2 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2854,12 +2854,14 @@ __metadata:
dependencies:
"@openmrs/esm-globals": "workspace:*"
"@openmrs/esm-state": "workspace:*"
"@openmrs/esm-utils": "workspace:*"
"@types/ramda": "npm:^0.26.44"
ramda: "npm:^0.26.1"
single-spa: "npm:^6.0.1"
peerDependencies:
"@openmrs/esm-globals": 5.x
"@openmrs/esm-state": 5.x
"@openmrs/esm-utils": 5.x
single-spa: 5.x
languageName: unknown
linkType: soft
Expand Down

0 comments on commit 362d430

Please sign in to comment.