@openmrs/esm-extensions 3.1.15-pre.757 → 3.1.15-pre.769

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.757",
3
+ "version": "3.1.15-pre.769",
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.757",
46
- "@openmrs/esm-config": "^3.1.15-pre.757",
47
- "@openmrs/esm-state": "^3.1.15-pre.757",
45
+ "@openmrs/esm-api": "^3.1.15-pre.769",
46
+ "@openmrs/esm-config": "^3.1.15-pre.769",
47
+ "@openmrs/esm-state": "^3.1.15-pre.769",
48
48
  "single-spa": "^5.9.2"
49
49
  },
50
50
  "dependencies": {
51
51
  "lodash-es": "^4.17.21"
52
52
  },
53
- "gitHead": "9b005ddcbb5f1f0412081c7c85b5afbe4477b17e"
53
+ "gitHead": "a32d1815905118ea85a2ad139da3d45c9187d4ad"
54
54
  }
package/src/extensions.ts CHANGED
@@ -1,79 +1,42 @@
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
-
10
1
  import {
11
2
  ExtensionSlotConfigObject,
12
- getExtensionSlotConfigStore,
3
+ getExtensionSlotsConfigStore,
13
4
  } from "@openmrs/esm-config";
14
- import {
15
- getExtensionInternalStore,
16
- ExtensionSlotState,
17
- AssignedExtension,
18
- checkStatusFor,
19
- ConnectedExtension,
20
- } from ".";
5
+ import { ExtensionInfo } from ".";
21
6
  import {
22
7
  ExtensionRegistration,
23
8
  ExtensionSlotInfo,
24
- ExtensionInternalStore,
25
- getExtensionStore,
26
- updateInternalExtensionStore,
9
+ ExtensionSlotInstance,
10
+ ExtensionStore,
11
+ extensionStore,
12
+ updateExtensionStore,
27
13
  } from "./store";
28
14
 
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
- });
15
+ function createNewExtensionSlotInstance(): ExtensionSlotInstance {
16
+ return {
17
+ addedIds: [],
18
+ idOrder: [],
19
+ removedIds: [],
20
+ };
21
+ }
44
22
 
45
23
  function createNewExtensionSlotInfo(
46
- slotName: string,
47
- moduleName?: string
24
+ extensionSlotName: string
48
25
  ): ExtensionSlotInfo {
49
26
  return {
50
- moduleName,
51
- name: slotName,
27
+ name: extensionSlotName,
52
28
  attachedIds: [],
53
- config: null,
29
+ instances: {},
54
30
  };
55
31
  }
56
32
 
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
- */
70
33
  export function getExtensionNameFromId(extensionId: string) {
71
34
  const [extensionName] = extensionId.split("#");
72
35
  return extensionName;
73
36
  }
74
37
 
75
38
  export function getExtensionRegistrationFrom(
76
- state: ExtensionInternalStore,
39
+ state: ExtensionStore,
77
40
  extensionId: string
78
41
  ): ExtensionRegistration | undefined {
79
42
  const name = getExtensionNameFromId(extensionId);
@@ -83,57 +46,43 @@ export function getExtensionRegistrationFrom(
83
46
  export function getExtensionRegistration(
84
47
  extensionId: string
85
48
  ): ExtensionRegistration | undefined {
86
- const state = extensionInternalStore.getState();
49
+ const state = extensionStore.getState();
87
50
  return getExtensionRegistrationFrom(state, extensionId);
88
51
  }
89
52
 
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
- */
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
+
96
62
  export const registerExtension: (
97
- extensionRegistration: ExtensionRegistration
98
- ) => void = extensionInternalStore.action(
99
- (state, extensionRegistration: ExtensionRegistration) => {
100
- state.extensions[extensionRegistration.name] = {
101
- ...extensionRegistration,
63
+ name: string,
64
+ details: ExtensionDetails
65
+ ) => void = extensionStore.action(
66
+ (state, name: string, details: ExtensionDetails) => {
67
+ state.extensions[name] = {
68
+ ...details,
69
+ name,
102
70
  instances: {},
103
71
  };
104
72
  }
105
73
  );
106
74
 
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];
75
+ export function attach(extensionSlotName: string, extensionId: string) {
76
+ updateExtensionStore((state) => {
77
+ const existingSlot = state.slots[extensionSlotName];
129
78
 
130
79
  if (!existingSlot) {
131
80
  return {
132
81
  ...state,
133
82
  slots: {
134
83
  ...state.slots,
135
- [slotName]: {
136
- ...createNewExtensionSlotInfo(slotName),
84
+ [extensionSlotName]: {
85
+ ...createNewExtensionSlotInfo(extensionSlotName),
137
86
  attachedIds: [extensionId],
138
87
  },
139
88
  },
@@ -143,7 +92,7 @@ export function attach(slotName: string, extensionId: string) {
143
92
  ...state,
144
93
  slots: {
145
94
  ...state.slots,
146
- [slotName]: {
95
+ [extensionSlotName]: {
147
96
  ...existingSlot,
148
97
  attachedIds: [...existingSlot.attachedIds, extensionId],
149
98
  },
@@ -153,9 +102,8 @@ export function attach(slotName: string, extensionId: string) {
153
102
  });
154
103
  }
155
104
 
156
- /** Avoid using this. Extension attachments should be considered declarative. */
157
105
  export function detach(extensionSlotName: string, extensionId: string) {
158
- updateInternalExtensionStore((state) => {
106
+ updateExtensionStore((state) => {
159
107
  const existingSlot = state.slots[extensionSlotName];
160
108
 
161
109
  if (existingSlot && existingSlot.attachedIds.includes(extensionId)) {
@@ -177,9 +125,8 @@ export function detach(extensionSlotName: string, extensionId: string) {
177
125
  });
178
126
  }
179
127
 
180
- /** Avoid using this. Extension attachments should be considered declarative. */
181
128
  export function detachAll(extensionSlotName: string) {
182
- updateInternalExtensionStore((state) => {
129
+ updateExtensionStore((state) => {
183
130
  const existingSlot = state.slots[extensionSlotName];
184
131
 
185
132
  if (existingSlot) {
@@ -228,54 +175,15 @@ function getOrder(
228
175
  }
229
176
  }
230
177
 
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
- meta: extension.meta,
264
- online: extension.online,
265
- offline: extension.offline,
266
- });
267
- }
268
- return extensions;
269
- }
270
-
271
- function calculateAssignedIds(
178
+ export function getAssignedIds(
179
+ slotName: string,
272
180
  config: ExtensionSlotConfigObject,
273
181
  attachedIds: Array<string>
274
182
  ) {
275
183
  const addedIds = config.add || [];
276
184
  const removedIds = config.remove || [];
277
185
  const idOrder = config.order || [];
278
- const { extensions } = extensionInternalStore.getState();
186
+ const { extensions } = extensionStore.getState();
279
187
 
280
188
  return [...attachedIds, ...addedIds]
281
189
  .filter((id) => !removedIds.includes(id))
@@ -303,49 +211,101 @@ function calculateAssignedIds(
303
211
  });
304
212
  }
305
213
 
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
+
306
252
  /**
307
- * Used by by extension slots at mount time.
308
- *
309
253
  * @param moduleName The name of the module that contains the extension slot
310
254
  * @param slotName The extension slot name that is actually used
311
- * @internal
312
255
  */
313
- export const registerExtensionSlot: (
314
- moduleName: string,
315
- slotName: string
316
- ) => void = extensionInternalStore.action((state, moduleName, slotName) => {
317
- const existingModuleName = state.slots[slotName]?.moduleName;
318
- if (existingModuleName && existingModuleName != moduleName) {
319
- console.warn(
320
- `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}.`
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
321
263
  );
264
+
265
+ if (existingSlot !== updatedSlot) {
266
+ return {
267
+ ...state,
268
+ slots: {
269
+ ...state.slots,
270
+ [slotName]: updatedSlot,
271
+ },
272
+ };
273
+ }
274
+
322
275
  return state;
323
- }
324
- if (existingModuleName && existingModuleName == moduleName) {
325
- // Re-rendering an existing slot
326
- return state;
327
- }
328
- if (state.slots[slotName]) {
329
- return {
330
- ...state,
331
- slots: {
332
- ...state.slots,
333
- [slotName]: {
334
- ...state.slots[slotName],
335
- moduleName,
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,
336
295
  },
337
- },
338
- };
339
- }
340
- const slot = createNewExtensionSlotInfo(slotName, moduleName);
341
- return {
342
- ...state,
343
- slots: {
344
- ...state.slots,
345
- [slotName]: slot,
346
- },
347
- };
348
- });
296
+ };
297
+ }
298
+
299
+ 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
+ }
349
309
 
350
310
  /**
351
311
  * @internal
@@ -357,3 +317,79 @@ export const reset: () => void = extensionStore.action(() => {
357
317
  extensions: {},
358
318
  };
359
319
  });
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,13 +1,5 @@
1
1
  export * from "./contexts";
2
- export * from "./store";
3
2
  export * from "./extensions";
4
3
  export * from "./helpers";
5
4
  export * from "./render";
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 };
5
+ export * from "./store";
package/src/render.ts CHANGED
@@ -2,7 +2,7 @@ import { update } from "@openmrs/esm-state";
2
2
  import { mountRootParcel, Parcel } from "single-spa";
3
3
  import { getExtensionNameFromId, getExtensionRegistration } from "./extensions";
4
4
  import { checkStatus, getCustomProps } from "./helpers";
5
- import { updateInternalExtensionStore } from "./store";
5
+ import { updateExtensionStore } from "./store";
6
6
 
7
7
  export interface Lifecycle {
8
8
  bootstrap(): void;
@@ -60,7 +60,7 @@ export function renderExtension(
60
60
  }
61
61
  });
62
62
 
63
- updateInternalExtensionStore((state) =>
63
+ updateExtensionStore((state) =>
64
64
  update(
65
65
  state,
66
66
  [
@@ -74,10 +74,6 @@ export function renderExtension(
74
74
  )
75
75
  );
76
76
  }
77
- } else {
78
- console.warn(
79
- `Tried to render ${extensionId} into ${extensionSlotName} but no DOM element was available.`
80
- );
81
77
  }
82
78
 
83
79
  return () => {