@openmrs/esm-extensions 3.1.15-pre.892 → 3.2.1-pre.1012

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-extensions",
3
- "version": "3.1.15-pre.892",
3
+ "version": "3.2.1-pre.1012",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Coordinates extensions and extension points in the OpenMRS Frontend",
6
6
  "browser": "dist/openmrs-esm-extensions.js",
@@ -42,13 +42,13 @@
42
42
  "single-spa": "5.x"
43
43
  },
44
44
  "devDependencies": {
45
- "@openmrs/esm-api": "^3.1.15-pre.892",
46
- "@openmrs/esm-config": "^3.1.15-pre.892",
47
- "@openmrs/esm-state": "^3.1.15-pre.892",
45
+ "@openmrs/esm-api": "^3.2.1-pre.1012",
46
+ "@openmrs/esm-config": "^3.2.1-pre.1012",
47
+ "@openmrs/esm-state": "^3.2.1-pre.1012",
48
48
  "single-spa": "^5.9.2"
49
49
  },
50
50
  "dependencies": {
51
51
  "lodash-es": "^4.17.21"
52
52
  },
53
- "gitHead": "bb0cbb2387f66d9b5b8470e87284b7c7e3d612ac"
53
+ "gitHead": "96acf2eb0cf6d8d83d7ce12280bbf445c34c5672"
54
54
  }
package/src/extensions.ts CHANGED
@@ -1,42 +1,79 @@
1
+ /**
2
+ * We have the following extension modes:
3
+ *
4
+ * - attached (set via code in form of: attach, detach, ...)
5
+ * - configured (set via configuration in form of: added, removed, ...)
6
+ * - assigned (computed from attached and configured)
7
+ * - connected (computed from assigned using connectivity and online / offline)
8
+ */
9
+
1
10
  import {
2
11
  ExtensionSlotConfigObject,
3
- getExtensionSlotsConfigStore,
12
+ getExtensionSlotConfigStore,
4
13
  } from "@openmrs/esm-config";
5
- import { ExtensionInfo } from ".";
14
+ import {
15
+ getExtensionInternalStore,
16
+ ExtensionSlotState,
17
+ AssignedExtension,
18
+ checkStatusFor,
19
+ ConnectedExtension,
20
+ } from ".";
6
21
  import {
7
22
  ExtensionRegistration,
8
23
  ExtensionSlotInfo,
9
- ExtensionSlotInstance,
10
- ExtensionStore,
11
- extensionStore,
12
- updateExtensionStore,
24
+ ExtensionInternalStore,
25
+ getExtensionStore,
26
+ updateInternalExtensionStore,
13
27
  } from "./store";
14
28
 
15
- function createNewExtensionSlotInstance(): ExtensionSlotInstance {
16
- return {
17
- addedIds: [],
18
- idOrder: [],
19
- removedIds: [],
20
- };
21
- }
29
+ const extensionInternalStore = getExtensionInternalStore();
30
+ const extensionStore = getExtensionStore();
31
+
32
+ // Keep the output store updated
33
+ extensionInternalStore.subscribe((internalStore) => {
34
+ const slots: Record<string, ExtensionSlotState> = {};
35
+ for (let [slotName, slot] of Object.entries(internalStore.slots)) {
36
+ if (slot.moduleName) {
37
+ // Only include registered slots
38
+ const assignedExtensions = getAssignedExtensions(slotName);
39
+ slots[slotName] = { moduleName: slot.moduleName, assignedExtensions };
40
+ }
41
+ }
42
+ extensionStore.setState({ slots: slots });
43
+ });
22
44
 
23
45
  function createNewExtensionSlotInfo(
24
- extensionSlotName: string
46
+ slotName: string,
47
+ moduleName?: string
25
48
  ): ExtensionSlotInfo {
26
49
  return {
27
- name: extensionSlotName,
50
+ moduleName,
51
+ name: slotName,
28
52
  attachedIds: [],
29
- instances: {},
53
+ config: null,
30
54
  };
31
55
  }
32
56
 
57
+ /**
58
+ * Given an extension ID, which is a string uniquely identifying
59
+ * an instance of an extension within an extension slot, this
60
+ * returns the extension name.
61
+ *
62
+ * @example
63
+ * ```js
64
+ * getExtensionNameFromId("foo#bar")
65
+ * --> "foo"
66
+ * getExtensionNameFromId("baz")
67
+ * --> "baz"
68
+ * ```
69
+ */
33
70
  export function getExtensionNameFromId(extensionId: string) {
34
71
  const [extensionName] = extensionId.split("#");
35
72
  return extensionName;
36
73
  }
37
74
 
38
75
  export function getExtensionRegistrationFrom(
39
- state: ExtensionStore,
76
+ state: ExtensionInternalStore,
40
77
  extensionId: string
41
78
  ): ExtensionRegistration | undefined {
42
79
  const name = getExtensionNameFromId(extensionId);
@@ -46,43 +83,57 @@ export function getExtensionRegistrationFrom(
46
83
  export function getExtensionRegistration(
47
84
  extensionId: string
48
85
  ): ExtensionRegistration | undefined {
49
- const state = extensionStore.getState();
86
+ const state = extensionInternalStore.getState();
50
87
  return getExtensionRegistrationFrom(state, extensionId);
51
88
  }
52
89
 
53
- export interface ExtensionDetails {
54
- moduleName: string;
55
- load: () => Promise<any>;
56
- meta: Record<string, any>;
57
- online?: boolean | object;
58
- offline?: boolean | object;
59
- order?: number;
60
- }
61
-
90
+ /**
91
+ * Extensions must be registered in order to be rendered.
92
+ * This is handled by the app shell, when extensions are provided
93
+ * via the `setupOpenMRS` return object.
94
+ * @internal
95
+ */
62
96
  export const registerExtension: (
63
- name: string,
64
- details: ExtensionDetails
65
- ) => void = extensionStore.action(
66
- (state, name: string, details: ExtensionDetails) => {
67
- state.extensions[name] = {
68
- ...details,
69
- name,
97
+ extensionRegistration: ExtensionRegistration
98
+ ) => void = extensionInternalStore.action(
99
+ (state, extensionRegistration: ExtensionRegistration) => {
100
+ state.extensions[extensionRegistration.name] = {
101
+ ...extensionRegistration,
70
102
  instances: {},
71
103
  };
72
104
  }
73
105
  );
74
106
 
75
- export function attach(extensionSlotName: string, extensionId: string) {
76
- updateExtensionStore((state) => {
77
- const existingSlot = state.slots[extensionSlotName];
107
+ /**
108
+ * Attach an extension to an extension slot.
109
+ *
110
+ * This will cause the extension to be rendered into the specified
111
+ * extension slot, unless it is removed by configuration. Using
112
+ * `attach` is an alternative to specifying the `slot` or `slots`
113
+ * in the extension declaration.
114
+ *
115
+ * It is particularly useful when creating a slot into which
116
+ * you want to render an existing extension. This enables you
117
+ * to do so without modifying the extension's declaration, which
118
+ * may be impractical or inappropriate, for example if you are
119
+ * writing a module for a specific implementation.
120
+ *
121
+ * @param slotName a name uniquely identifying the slot
122
+ * @param extensionId an extension name, with an optional #-suffix
123
+ * to distinguish it from other instances of the same extension
124
+ * attached to the same slot.
125
+ */
126
+ export function attach(slotName: string, extensionId: string) {
127
+ updateInternalExtensionStore((state) => {
128
+ const existingSlot = state.slots[slotName];
78
129
 
79
130
  if (!existingSlot) {
80
131
  return {
81
132
  ...state,
82
133
  slots: {
83
134
  ...state.slots,
84
- [extensionSlotName]: {
85
- ...createNewExtensionSlotInfo(extensionSlotName),
135
+ [slotName]: {
136
+ ...createNewExtensionSlotInfo(slotName),
86
137
  attachedIds: [extensionId],
87
138
  },
88
139
  },
@@ -92,7 +143,7 @@ export function attach(extensionSlotName: string, extensionId: string) {
92
143
  ...state,
93
144
  slots: {
94
145
  ...state.slots,
95
- [extensionSlotName]: {
146
+ [slotName]: {
96
147
  ...existingSlot,
97
148
  attachedIds: [...existingSlot.attachedIds, extensionId],
98
149
  },
@@ -102,8 +153,9 @@ export function attach(extensionSlotName: string, extensionId: string) {
102
153
  });
103
154
  }
104
155
 
156
+ /** Avoid using this. Extension attachments should be considered declarative. */
105
157
  export function detach(extensionSlotName: string, extensionId: string) {
106
- updateExtensionStore((state) => {
158
+ updateInternalExtensionStore((state) => {
107
159
  const existingSlot = state.slots[extensionSlotName];
108
160
 
109
161
  if (existingSlot && existingSlot.attachedIds.includes(extensionId)) {
@@ -125,8 +177,9 @@ export function detach(extensionSlotName: string, extensionId: string) {
125
177
  });
126
178
  }
127
179
 
180
+ /** Avoid using this. Extension attachments should be considered declarative. */
128
181
  export function detachAll(extensionSlotName: string) {
129
- updateExtensionStore((state) => {
182
+ updateInternalExtensionStore((state) => {
130
183
  const existingSlot = state.slots[extensionSlotName];
131
184
 
132
185
  if (existingSlot) {
@@ -175,15 +228,55 @@ function getOrder(
175
228
  }
176
229
  }
177
230
 
178
- export function getAssignedIds(
179
- slotName: string,
231
+ /**
232
+ * Filters a list of extensions according to whether they support the
233
+ * current connectivity status.
234
+ *
235
+ * @param assignedExtensions The list of extensions to filter.
236
+ * @param online Whether the app is currently online. If `null`, uses `navigator.onLine`.
237
+ * @returns A list of extensions that should be rendered
238
+ */
239
+ export function getConnectedExtensions(
240
+ assignedExtensions: Array<AssignedExtension>,
241
+ online: boolean | null = null
242
+ ): Array<ConnectedExtension> {
243
+ const isOnline = online ?? navigator.onLine;
244
+ return assignedExtensions.filter((e) =>
245
+ checkStatusFor(isOnline, e.online, e.offline)
246
+ );
247
+ }
248
+
249
+ export function getAssignedExtensions(
250
+ slotName: string
251
+ ): Array<AssignedExtension> {
252
+ const internalState = extensionInternalStore.getState();
253
+ const config = getExtensionSlotConfigStore(slotName).getState().config;
254
+ const attachedIds = internalState.slots[slotName].attachedIds;
255
+ const assignedIds = calculateAssignedIds(config, attachedIds);
256
+ const extensions: Array<AssignedExtension> = [];
257
+ for (let id of assignedIds) {
258
+ const name = getExtensionNameFromId(id);
259
+ const extension = internalState.extensions[name];
260
+ extensions.push({
261
+ id,
262
+ name,
263
+ moduleName: extension.moduleName,
264
+ meta: extension.meta,
265
+ online: extension.online,
266
+ offline: extension.offline,
267
+ });
268
+ }
269
+ return extensions;
270
+ }
271
+
272
+ function calculateAssignedIds(
180
273
  config: ExtensionSlotConfigObject,
181
274
  attachedIds: Array<string>
182
275
  ) {
183
276
  const addedIds = config.add || [];
184
277
  const removedIds = config.remove || [];
185
278
  const idOrder = config.order || [];
186
- const { extensions } = extensionStore.getState();
279
+ const { extensions } = extensionInternalStore.getState();
187
280
 
188
281
  return [...attachedIds, ...addedIds]
189
282
  .filter((id) => !removedIds.includes(id))
@@ -211,101 +304,49 @@ export function getAssignedIds(
211
304
  });
212
305
  }
213
306
 
214
- function getUpdatedExtensionSlotInfoForRegistration(
215
- existingSlot: ExtensionSlotInfo,
216
- slotName: string,
217
- moduleName: string
218
- ) {
219
- if (!existingSlot) {
220
- return getUpdatedExtensionSlotInfo(slotName, moduleName, {
221
- ...createNewExtensionSlotInfo(slotName),
222
- instances: {
223
- [moduleName]: createNewExtensionSlotInstance(),
224
- },
225
- });
226
- } else if (moduleName in existingSlot.instances) {
227
- return getUpdatedExtensionSlotInfo(slotName, moduleName, existingSlot);
228
- } else {
229
- return getUpdatedExtensionSlotInfo(slotName, moduleName, {
230
- ...existingSlot,
231
- instances: {
232
- ...existingSlot.instances,
233
- [moduleName]: createNewExtensionSlotInstance(),
234
- },
235
- });
236
- }
237
- }
238
-
239
- function getUpdatedExtensionSlotInfoForUnregistration(
240
- existingSlot: ExtensionSlotInfo,
241
- extensionSlotName: string,
242
- moduleName: string
243
- ) {
244
- const { [moduleName]: existing, ...instances } = existingSlot.instances;
245
-
246
- return getUpdatedExtensionSlotInfo(extensionSlotName, moduleName, {
247
- ...existingSlot,
248
- instances,
249
- });
250
- }
251
-
252
307
  /**
308
+ * Used by by extension slots at mount time.
309
+ *
253
310
  * @param moduleName The name of the module that contains the extension slot
254
311
  * @param slotName The extension slot name that is actually used
312
+ * @internal
255
313
  */
256
- export function registerExtensionSlot(moduleName: string, slotName: string) {
257
- updateExtensionStore((state) => {
258
- const existingSlot = state.slots[slotName];
259
- const updatedSlot = getUpdatedExtensionSlotInfoForRegistration(
260
- existingSlot,
261
- slotName,
262
- moduleName
314
+ export const registerExtensionSlot: (
315
+ moduleName: string,
316
+ slotName: string
317
+ ) => void = extensionInternalStore.action((state, moduleName, slotName) => {
318
+ const existingModuleName = state.slots[slotName]?.moduleName;
319
+ if (existingModuleName && existingModuleName != moduleName) {
320
+ console.warn(
321
+ `An extension slot with the name '${slotName}' already exists. Refusing to register the same slot name twice (in "registerExtensionSlot"). The existing one is from module ${existingModuleName}.`
263
322
  );
264
-
265
- if (existingSlot !== updatedSlot) {
266
- return {
267
- ...state,
268
- slots: {
269
- ...state.slots,
270
- [slotName]: updatedSlot,
271
- },
272
- };
273
- }
274
-
275
323
  return state;
276
- });
277
- }
278
-
279
- export function unregisterExtensionSlot(moduleName: string, slotName: string) {
280
- updateExtensionStore((state) => {
281
- const existingSlot = state.slots[slotName];
282
-
283
- if (existingSlot && moduleName in existingSlot.instances) {
284
- const updatedSlot = getUpdatedExtensionSlotInfoForUnregistration(
285
- existingSlot,
286
- slotName,
287
- moduleName
288
- );
289
-
290
- return {
291
- ...state,
292
- slots: {
293
- ...state.slots,
294
- [slotName]: updatedSlot,
295
- },
296
- };
297
- }
298
-
324
+ }
325
+ if (existingModuleName && existingModuleName == moduleName) {
326
+ // Re-rendering an existing slot
299
327
  return state;
300
- });
301
- }
302
-
303
- export function getExtensionSlotsForModule(moduleName: string) {
304
- const state = extensionStore.getState();
305
- return Object.keys(state.slots).filter(
306
- (name) => moduleName in state.slots[name].instances
307
- );
308
- }
328
+ }
329
+ if (state.slots[slotName]) {
330
+ return {
331
+ ...state,
332
+ slots: {
333
+ ...state.slots,
334
+ [slotName]: {
335
+ ...state.slots[slotName],
336
+ moduleName,
337
+ },
338
+ },
339
+ };
340
+ }
341
+ const slot = createNewExtensionSlotInfo(slotName, moduleName);
342
+ return {
343
+ ...state,
344
+ slots: {
345
+ ...state.slots,
346
+ [slotName]: slot,
347
+ },
348
+ };
349
+ });
309
350
 
310
351
  /**
311
352
  * @internal
@@ -317,79 +358,3 @@ export const reset: () => void = extensionStore.action(() => {
317
358
  extensions: {},
318
359
  };
319
360
  });
320
-
321
- /**
322
- * Returns information describing all extensions which can be rendered into an extension slot with
323
- * the specified name.
324
- * The returned information describe the extension itself, as well as the extension slot name(s)
325
- * with which it has been attached.
326
- * @param slotName The extension slot name for which matching extension info should be returned.
327
- * @param moduleName The module name. Used for applying extension-specific config values to the result.
328
- * @param extensionSlot The extension slot information object.
329
- */
330
- export function getUpdatedExtensionSlotInfo(
331
- slotName: string,
332
- moduleName: string,
333
- extensionSlot: ExtensionSlotInfo
334
- ): ExtensionSlotInfo {
335
- let instance = extensionSlot.instances[moduleName];
336
-
337
- if (instance) {
338
- const originalInstance = instance;
339
- const config =
340
- getExtensionSlotsConfigStore(moduleName).getState().extensionSlotConfigs[
341
- slotName
342
- ] ?? {};
343
-
344
- if (Array.isArray(config.add)) {
345
- config.add.forEach((extensionId) => {
346
- if (!instance.addedIds.includes(extensionId)) {
347
- instance = {
348
- ...instance,
349
- addedIds: [...instance.addedIds, extensionId],
350
- };
351
- }
352
- });
353
- }
354
-
355
- if (Array.isArray(config.remove)) {
356
- config.remove.forEach((extensionId) => {
357
- if (!instance.removedIds.includes(extensionId)) {
358
- instance = {
359
- ...instance,
360
- removedIds: [...instance.removedIds, extensionId],
361
- };
362
- }
363
- });
364
- }
365
-
366
- if (Array.isArray(config.order)) {
367
- const testOrder = config.order.join(",");
368
- const fullOrder = instance.idOrder.join(",");
369
-
370
- if (!fullOrder.endsWith(testOrder)) {
371
- config.order.forEach((extensionId) => {
372
- instance = {
373
- ...instance,
374
- idOrder: [
375
- ...instance.idOrder.filter((m) => m !== extensionId),
376
- extensionId,
377
- ],
378
- };
379
- });
380
- }
381
- }
382
-
383
- if (instance !== originalInstance) {
384
- return {
385
- ...extensionSlot,
386
- instances: {
387
- ...extensionSlot.instances,
388
- [moduleName]: instance,
389
- },
390
- };
391
- }
392
- }
393
-
394
- return extensionSlot;
395
- }
package/src/index.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  export * from "./contexts";
2
+ export * from "./store";
2
3
  export * from "./extensions";
3
4
  export * from "./helpers";
4
5
  export * from "./render";
5
- export * from "./store";
6
+
7
+ // Temporary compatibility hack
8
+ // What is now `extensionInternalStore` used to be exposed
9
+ // and used as `extensionStore`.
10
+ import { getExtensionInternalStore } from "./store";
11
+ /** @deprecated Use `getExtensionStore`. The structure of this store has also changed. */
12
+ const internalStore = getExtensionInternalStore();
13
+ export { internalStore as extensionStore };
package/src/render.ts CHANGED
@@ -3,7 +3,7 @@ import { update } from "@openmrs/esm-state";
3
3
  import { mountRootParcel, Parcel } from "single-spa";
4
4
  import { getExtensionNameFromId, getExtensionRegistration } from "./extensions";
5
5
  import { checkStatus, getCustomProps } from "./helpers";
6
- import { updateExtensionStore } from "./store";
6
+ import { updateInternalExtensionStore } from "./store";
7
7
 
8
8
  export interface CancelLoading {
9
9
  (): void;
@@ -54,7 +54,7 @@ export function renderExtension(
54
54
  }
55
55
  });
56
56
 
57
- updateExtensionStore((state) =>
57
+ updateInternalExtensionStore((state) =>
58
58
  update(
59
59
  state,
60
60
  [
@@ -68,6 +68,10 @@ export function renderExtension(
68
68
  )
69
69
  );
70
70
  }
71
+ } else {
72
+ console.warn(
73
+ `Tried to render ${extensionId} into ${extensionSlotName} but no DOM element was available.`
74
+ );
71
75
  }
72
76
 
73
77
  return () => {