@openmrs/esm-extensions 10.0.1-pre.5263 → 10.0.1-pre.5299

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.
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 11 files with swc (123.7ms)
1
+ [0] Successfully compiled: 11 files with swc (154.25ms)
2
2
  [0] swc --strip-leading-paths src -d dist exited with code 0
3
3
  [1] tsc --project tsconfig.build.json exited with code 0
@@ -61,29 +61,38 @@ export declare function detach(extensionSlotName: string, extensionId: string):
61
61
  */
62
62
  export declare function detachAll(extensionSlotName: string): void;
63
63
  /**
64
- * Gets the list of extensions assigned to a given slot
64
+ * Gets the extensions a given rendering of a slot should display, in order. This is the supported
65
+ * way to ask what belongs in a slot; reading the extension store directly skips display conditions.
65
66
  *
66
- * @param slotName The slot to load the assigned extensions for
67
- * @returns An array of extensions assigned to the named slot
67
+ * Display conditions are evaluated against `state`, so pass whatever the slot is being rendered
68
+ * for. The same slot can be rendered many times with different state — once per row of a table,
69
+ * say — and each rendering can resolve to a different set of extensions. Omitting `state` hides
70
+ * every extension whose condition refers to it, since the condition cannot be evaluated.
71
+ *
72
+ * @param slotName The slot to load the extensions for
73
+ * @param state The state of the rendering of the slot the extensions will be displayed in
74
+ * @returns Those extensions assigned to the slot whose display conditions hold
68
75
  */
69
- export declare function getAssignedExtensions(slotName: string): Array<AssignedExtension>;
76
+ export declare function getAssignedExtensions(slotName: string, state?: ExtensionSlotCustomState): Array<AssignedExtension>;
70
77
  /**
71
- * Used by by extension slots at mount time.
78
+ * Gets everything assigned to a slot without evaluating any display condition, which
79
+ * {@link getAssignedExtensions} cannot do without knowing the state of a particular rendering.
80
+ *
81
+ * This exists for tools that present a slot's configuration rather than render it — the UI editor
82
+ * has to list an extension in order to let an implementer reorder or remove it, even where no
83
+ * rendering would display it. Anything deciding what to render wants `getAssignedExtensions()`.
72
84
  *
73
- * @param moduleName The name of the module that contains the extension slot
74
- * @param slotName The extension slot name that is actually used
75
- * @param state Optional custom state for the slot, which will be stored in the extension store.
76
85
  * @internal
77
86
  */
78
- export declare const registerExtensionSlot: (moduleName: string, slotName: string, state?: ExtensionSlotCustomState) => void;
87
+ export declare function getCandidateExtensions(slotName: string): Array<AssignedExtension>;
79
88
  /**
80
- * Used by extension slots to update the copy of the state for the extension slot
89
+ * Used by by extension slots at mount time.
81
90
  *
82
- * @param slotName The name of the slot with state to update
83
- * @param state A copy of the new state
84
- * @param partial Whether this should be applied as a partial
91
+ * @param moduleName The name of the module that contains the extension slot
92
+ * @param slotName The extension slot name that is actually used
93
+ * @internal
85
94
  */
86
- export declare function updateExtensionSlotState(slotName: string, state: ExtensionSlotCustomState, partial?: boolean): void;
95
+ export declare const registerExtensionSlot: (moduleName: string, slotName: string) => void;
87
96
  /**
88
97
  * @internal
89
98
  * Just for testing.
@@ -1 +1 @@
1
- {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"AAAA,kCAAkC;AA4BlC,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAM9B,MAAM,SAAS,CAAC;AAsHjB;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,UAGzD;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,sBAAsB,EAC7B,WAAW,EAAE,MAAM,GAClB,qBAAqB,GAAG,SAAS,CAGnC;AAED,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAG/F;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,CAAC,qBAAqB,EAAE,qBAAqB,KAAK,IAO9E,CAAC;AAEL;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,QA4B3D;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,iBAAiB,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,QAmBpE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,iBAAiB,EAAE,MAAM,QAmBlD;AAqHD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAC,iBAAiB,CAAC,CAoBhF;AAwBD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,wBAAwB,KAAK,IA2C5G,CAAC;AAEL;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,wBAAwB,EAAE,OAAO,GAAE,OAAe,QAcnH;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,MAAM,IAMtB,CAAC"}
1
+ {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"AAAA,kCAAkC;AA2BlC,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAQ9B,MAAM,SAAS,CAAC;AA4MjB;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,UAGzD;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,sBAAsB,EAC7B,WAAW,EAAE,MAAM,GAClB,qBAAqB,GAAG,SAAS,CAGnC;AAED,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAG/F;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,CAAC,qBAAqB,EAAE,qBAAqB,KAAK,IAO7E,CAAC;AAEN;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,QA4B3D;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,iBAAiB,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,QAmBpE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,iBAAiB,EAAE,MAAM,QAmBlD;AAmJD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,wBAAwB,GAAG,KAAK,CAAC,iBAAiB,CAAC,CAOlH;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAC,iBAAiB,CAAC,CAEjF;AAqBD;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAsC1E,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,MAAM,IAOzB,CAAC"}
@@ -4,66 +4,157 @@
4
4
  * - attached (set via code in form of: attach, detach, ...)
5
5
  * - configured (set via configuration in form of: added, removed, ...)
6
6
  * - assigned (computed from attached and configured)
7
- * - connected (computed from assigned using connectivity and online / offline)
7
+ * - displayed (computed from assigned)
8
8
  */ import { sessionStore, userHasAccess } from "@openmrs/esm-api";
9
- import { getExtensionConfigFromExtensionSlotStore, getExtensionConfigFromStore, getExtensionSlotConfig, getExtensionSlotConfigFromStore, getExtensionSlotsConfigStore, getExtensionsConfigStore } from "@openmrs/esm-config";
9
+ import { getExtensionConfigFromExtensionSlotStore, getExtensionConfigFromStore, getExtensionSlotConfigFromStore, getExtensionSlotsConfigStore, getExtensionsConfigStore } from "@openmrs/esm-config";
10
10
  import { evaluateAsBoolean } from "@openmrs/esm-expression-evaluator";
11
11
  import { featureFlagsStore } from "@openmrs/esm-feature-flags";
12
12
  import { subscribeConnectivityChanged } from "@openmrs/esm-globals";
13
13
  import { isOnline as isOnlineFn } from "@openmrs/esm-utils";
14
14
  import { isEqual, merge } from "lodash-es";
15
15
  import { checkStatusFor } from "./helpers.js";
16
- import { getExtensionInternalStore, getExtensionStore, updateInternalExtensionStore } from "./store.js";
16
+ import { getExtensionRenderingsStore, getExtensionInternalStore, getExtensionStore, scheduleRecomputation, updateInternalExtensionStore } from "./store.js";
17
17
  const extensionInternalStore = getExtensionInternalStore();
18
18
  const extensionStore = getExtensionStore();
19
19
  const slotsConfigStore = getExtensionSlotsConfigStore();
20
20
  const extensionsConfigStore = getExtensionsConfigStore();
21
- // Keep the output store updated
22
- function updateExtensionOutputStore(internalState, extensionSlotConfigs, extensionsConfigStore, featureFlagStore, sessionStore) {
21
+ /**
22
+ * Slots whose derived state may have changed since the output store was last updated. `null`
23
+ * stands for "every slot", and is used when an input changes that isn't scoped to one slot —
24
+ * the session, a feature flag, connectivity, or a new extension registration.
25
+ */ let dirtySlots = new Set();
26
+ let isRecomputingOutputStore = false;
27
+ function markSlotsDirty(slots) {
28
+ if (slots === null) {
29
+ dirtySlots = null;
30
+ } else if (dirtySlots !== null) {
31
+ for (const slotName of slots){
32
+ dirtySlots.add(slotName);
33
+ }
34
+ }
35
+ scheduleRecomputation(recomputeExtensionOutputStore);
36
+ }
37
+ /**
38
+ * A subscriber that dirties a slot every time the output store is published would loop here for
39
+ * ever, so the drain gives up after this many passes and leaves the rest for the next write. Any
40
+ * real cascade settles in two or three.
41
+ */ const maxRecomputationPasses = 20;
42
+ function recomputeExtensionOutputStore() {
43
+ // Publishing notifies subscribers synchronously, so one that dirties a slot re-enters here. The
44
+ // loop below picks that work up; recursing instead would nest without bound.
45
+ if (isRecomputingOutputStore) {
46
+ return;
47
+ }
48
+ isRecomputingOutputStore = true;
49
+ try {
50
+ for(let pass = 1;; pass++){
51
+ const slots = dirtySlots;
52
+ if (slots !== null && slots.size === 0) {
53
+ return;
54
+ }
55
+ // Taken before the work so that anything dirtied while it runs accumulates for the next pass
56
+ // rather than being discarded when this one finishes.
57
+ dirtySlots = new Set();
58
+ try {
59
+ updateExtensionOutputStore(extensionInternalStore.getState(), slotsConfigStore.getState(), extensionsConfigStore.getState(), featureFlagsStore.getState(), sessionStore.getState(), slots);
60
+ } catch (e) {
61
+ // Fold the work back in, so a failure leaves these slots marked for the next recomputation
62
+ // instead of stranding them stale.
63
+ dirtySlots = slots === null || dirtySlots === null ? null : new Set([
64
+ ...slots,
65
+ ...dirtySlots
66
+ ]);
67
+ throw e;
68
+ }
69
+ if (pass >= maxRecomputationPasses) {
70
+ console.error(`The extension system stopped recomputing after ${maxRecomputationPasses} passes because ` + `something keeps invalidating slots in response to the extension store being written. ` + `Some slots are left stale until the next write.`);
71
+ return;
72
+ }
73
+ }
74
+ } finally{
75
+ isRecomputingOutputStore = false;
76
+ }
77
+ }
78
+ /**
79
+ * Re-derives the output store from the extension system's inputs.
80
+ *
81
+ * Two things keep this cheap. `dirtySlotNames` limits which slots are recomputed at all; passing
82
+ * `null` recomputes every slot. And slots whose derived value hasn't changed keep their existing
83
+ * object, so that components subscribed to one slot don't re-render because a different slot
84
+ * changed.
85
+ */ function updateExtensionOutputStore(internalState, extensionSlotConfigs, extensionsConfigState, featureFlagState, sessionState, dirtySlotNames = null) {
86
+ const previousSlots = extensionStore.getState().slots;
23
87
  const slots = {};
88
+ let changed = false;
24
89
  const isOnline = isOnlineFn();
25
- const enabledFeatureFlags = Object.entries(featureFlagStore.flags).filter(([, { enabled }])=>enabled).map(([name])=>name);
90
+ const enabledFeatureFlags = Object.entries(featureFlagState.flags).filter(([, { enabled }])=>enabled).map(([name])=>name);
26
91
  for (let [slotName, slot] of Object.entries(internalState.slots)){
92
+ const previous = previousSlots[slotName];
93
+ if (previous && dirtySlotNames && !dirtySlotNames.has(slotName)) {
94
+ slots[slotName] = previous;
95
+ continue;
96
+ }
27
97
  const { config } = getExtensionSlotConfigFromStore(extensionSlotConfigs, slot.name);
28
- const assignedExtensions = getAssignedExtensionsFromSlotData(slotName, internalState, config, extensionsConfigStore, enabledFeatureFlags, isOnline, sessionStore.session);
29
- slots[slotName] = {
30
- moduleName: slot.moduleName,
31
- assignedExtensions
32
- };
98
+ const candidateExtensions = getAssignedExtensionsFromSlotData(slotName, internalState, config, extensionsConfigState, enabledFeatureFlags, isOnline, sessionState.session);
99
+ if (previous && previous.moduleName === slot.moduleName && isEqual(previous.candidateExtensions, candidateExtensions)) {
100
+ slots[slotName] = previous;
101
+ } else {
102
+ slots[slotName] = {
103
+ moduleName: slot.moduleName,
104
+ candidateExtensions
105
+ };
106
+ changed = true;
107
+ }
33
108
  }
34
- if (!isEqual(extensionStore.getState().slots, slots)) {
109
+ if (changed || Object.keys(previousSlots).length !== Object.keys(slots).length) {
35
110
  extensionStore.setState({
36
111
  slots
37
112
  });
38
113
  }
39
114
  }
40
- extensionInternalStore.subscribe((internalStore)=>{
41
- updateExtensionOutputStore(internalStore, slotsConfigStore.getState(), extensionsConfigStore.getState(), featureFlagsStore.getState(), sessionStore.getState());
42
- });
43
- slotsConfigStore.subscribe((slotConfigs)=>{
44
- updateExtensionOutputStore(extensionInternalStore.getState(), slotConfigs, extensionsConfigStore.getState(), featureFlagsStore.getState(), sessionStore.getState());
45
- });
46
- extensionsConfigStore.subscribe((extensionConfigs)=>{
47
- updateExtensionOutputStore(extensionInternalStore.getState(), slotsConfigStore.getState(), extensionConfigs, featureFlagsStore.getState(), sessionStore.getState());
115
+ /**
116
+ * Returns the keys whose values differ between two records, including keys present in only one.
117
+ */ function changedKeys(next, previous) {
118
+ const changed = new Set();
119
+ for (const key of Object.keys(next)){
120
+ if (next[key] !== previous[key]) {
121
+ changed.add(key);
122
+ }
123
+ }
124
+ for (const key of Object.keys(previous)){
125
+ if (!(key in next)) {
126
+ changed.add(key);
127
+ }
128
+ }
129
+ return changed;
130
+ }
131
+ extensionInternalStore.subscribe((state, previousState)=>{
132
+ // A new or changed registration can affect any slot the extension might be assigned to. Every
133
+ // other kind of change here — attaching, registering a slot — is scoped to the slots whose
134
+ // entries actually changed.
135
+ markSlotsDirty(state.extensions !== previousState.extensions ? null : changedKeys(state.slots, previousState.slots));
48
136
  });
49
- featureFlagsStore.subscribe((featureFlagStore)=>{
50
- updateExtensionOutputStore(extensionInternalStore.getState(), slotsConfigStore.getState(), extensionsConfigStore.getState(), featureFlagStore, sessionStore.getState());
137
+ // Slot configs are per-slot, so a config change invalidates only the slots it names.
138
+ slotsConfigStore.subscribe((state, previousState)=>{
139
+ markSlotsDirty(changedKeys(state.slots, previousState.slots));
51
140
  });
52
- sessionStore.subscribe((session)=>{
53
- updateExtensionOutputStore(extensionInternalStore.getState(), slotsConfigStore.getState(), extensionsConfigStore.getState(), featureFlagsStore.getState(), session);
141
+ // Extension configs are keyed by slot too, and the config system hands back the same per-slot
142
+ // object when nothing under it changed, so only the slots whose configs actually moved are dirtied.
143
+ extensionsConfigStore.subscribe((state, previousState)=>{
144
+ markSlotsDirty(changedKeys(state.configs, previousState.configs));
54
145
  });
146
+ featureFlagsStore.subscribe(()=>markSlotsDirty(null));
147
+ sessionStore.subscribe(()=>markSlotsDirty(null));
55
148
  function updateOutputStoreToCurrent() {
56
- updateExtensionOutputStore(extensionInternalStore.getState(), slotsConfigStore.getState(), extensionsConfigStore.getState(), featureFlagsStore.getState(), sessionStore.getState());
149
+ markSlotsDirty(null);
57
150
  }
58
151
  updateOutputStoreToCurrent();
59
152
  subscribeConnectivityChanged(updateOutputStoreToCurrent);
60
- function createNewExtensionSlotInfo(slotName, moduleName, state) {
153
+ function createNewExtensionSlotInfo(slotName, moduleName) {
61
154
  return {
62
155
  moduleName,
63
156
  name: slotName,
64
- attachedIds: [],
65
- config: null,
66
- state
157
+ attachedIds: []
67
158
  };
68
159
  }
69
160
  /**
@@ -95,13 +186,15 @@ export function getExtensionRegistration(extensionId) {
95
186
  * This is handled by the app shell, when extensions are provided
96
187
  * via the `routes.json` file and registered through `registerApp()`.
97
188
  * @internal
98
- */ export const registerExtension = (extensionRegistration)=>extensionInternalStore.setState((state)=>{
99
- state.extensions[extensionRegistration.name] = {
100
- ...extensionRegistration,
101
- instances: []
102
- };
103
- return state;
104
- });
189
+ */ export const registerExtension = (extensionRegistration)=>updateInternalExtensionStore((state)=>({
190
+ ...state,
191
+ extensions: {
192
+ ...state.extensions,
193
+ [extensionRegistration.name]: {
194
+ ...extensionRegistration
195
+ }
196
+ }
197
+ }));
105
198
  /**
106
199
  * Attach an extension to an extension slot.
107
200
  *
@@ -208,6 +301,10 @@ export function getExtensionRegistration(extensionId) {
208
301
  * Get an order index for the extension. This will
209
302
  * come from either its configured order, its registered order
210
303
  * parameter, or the order in which it happened to be attached.
304
+ *
305
+ * The four bands below keep the comparison a total order: every extension lands in exactly one, and
306
+ * those with no ordering information share the last band rather than taking a sentinel that can't be
307
+ * compared against itself.
211
308
  */ function getOrder(extensionId, configuredOrder, registeredOrderIndex, attachedOrder) {
212
309
  const configuredIndex = configuredOrder.indexOf(extensionId);
213
310
  if (configuredIndex !== -1) {
@@ -222,7 +319,9 @@ export function getExtensionRegistration(extensionId) {
222
319
  // after all others
223
320
  return 2000 + assignedIndex;
224
321
  } else {
225
- return -1;
322
+ // Added by configuration with no order of any kind — after everything else, and among
323
+ // themselves in the order the configuration lists them.
324
+ return 3000;
226
325
  }
227
326
  }
228
327
  }
@@ -230,18 +329,12 @@ function getAssignedExtensionsFromSlotData(slotName, internalState, config, exte
230
329
  const attachedIds = internalState.slots[slotName].attachedIds;
231
330
  const assignedIds = calculateAssignedIds(config, attachedIds);
232
331
  const extensions = [];
233
- // Create context once for all extensions in this slot
234
- const slotState = internalState.slots[slotName]?.state;
235
- const expressionContext = slotState && typeof slotState === 'object' ? {
236
- session,
237
- ...slotState
238
- } : {
239
- session
240
- };
241
332
  for (let id of assignedIds){
242
333
  const { config: rawExtensionConfig } = getExtensionConfigFromStore(extensionConfigStoreState, slotName, id);
243
334
  const rawExtensionSlotExtensionConfig = getExtensionConfigFromExtensionSlotStore(config, slotName, id);
244
- const extensionConfig = merge(rawExtensionConfig, rawExtensionSlotExtensionConfig);
335
+ // `merge` mutates its first argument, and `rawExtensionConfig` belongs to the config store,
336
+ // so never merge into it.
337
+ const extensionConfig = rawExtensionSlotExtensionConfig ? merge({}, rawExtensionConfig, rawExtensionSlotExtensionConfig) : rawExtensionConfig;
245
338
  const name = getExtensionNameFromId(id);
246
339
  const extension = internalState.extensions[name];
247
340
  // if the extension has not been registered yet, do not include it
@@ -255,17 +348,6 @@ function getAssignedExtensionsFromSlotData(slotName, internalState, config, exte
255
348
  continue;
256
349
  }
257
350
  }
258
- const displayConditionExpression = extensionConfig?.['Display conditions']?.expression || extension.displayExpression;
259
- if (displayConditionExpression !== undefined && typeof displayConditionExpression === 'string' && displayConditionExpression.trim().length > 0) {
260
- try {
261
- if (!evaluateAsBoolean(displayConditionExpression, expressionContext)) {
262
- continue;
263
- }
264
- } catch (e) {
265
- console.error(`Error while evaluating expression '${displayConditionExpression}' for extension ${name} in slot ${slotName}`, e);
266
- continue;
267
- }
268
- }
269
351
  if (extension.featureFlag && !enabledFeatureFlags.includes(extension.featureFlag)) {
270
352
  continue;
271
353
  }
@@ -280,26 +362,74 @@ function getAssignedExtensionsFromSlotData(slotName, internalState, config, exte
280
362
  featureFlag: extension.featureFlag,
281
363
  meta: extension.meta,
282
364
  online: extensionConfig?.['Display conditions']?.online ?? extension.online ?? true,
283
- offline: extensionConfig?.['Display conditions']?.offline ?? extension.offline ?? false
365
+ offline: extensionConfig?.['Display conditions']?.offline ?? extension.offline ?? false,
366
+ displayConditionExpression: extensionConfig?.['Display conditions']?.expression || extension.displayExpression
284
367
  });
285
368
  }
286
369
  }
287
370
  return extensions;
288
371
  }
289
372
  /**
290
- * Gets the list of extensions assigned to a given slot
373
+ * Narrows `extensions` to those whose display condition holds for one particular rendering of a
374
+ * slot, evaluating each condition against `state`.
291
375
  *
292
- * @param slotName The slot to load the assigned extensions for
293
- * @returns An array of extensions assigned to the named slot
294
- */ export function getAssignedExtensions(slotName) {
295
- const internalState = extensionInternalStore.getState();
296
- const { config: slotConfig } = getExtensionSlotConfig(slotName);
297
- const extensionStoreState = extensionsConfigStore.getState();
298
- const featureFlagState = featureFlagsStore.getState();
299
- const sessionState = sessionStore.getState();
300
- const isOnline = isOnlineFn();
301
- const enabledFeatureFlags = Object.entries(featureFlagState.flags).filter(([, { enabled }])=>enabled).map(([name])=>name);
302
- return getAssignedExtensionsFromSlotData(slotName, internalState, slotConfig, extensionStoreState, enabledFeatureFlags, isOnline, sessionState.session);
376
+ * An extension whose display condition throws is left out, on the grounds that a condition that cannot be
377
+ * evaluated has not been met.
378
+ *
379
+ * @param extensions The extensions assigned to the slot, as the extension store holds them
380
+ * @param state The state of this rendering of the slot
381
+ * @param session The current session, which conditions may refer to as `session`
382
+ * @param slotName The slot these were assigned to, used only to identify them if a condition throws
383
+ * @returns Those of `extensions` that should be displayed, in the same order. Always a new array.
384
+ */ function filterExtensionsByDisplayConditions(extensions, state, session = null, slotName) {
385
+ // Built once for the whole slot rather than per extension: the context is the same for all of
386
+ // them, and most slots have no conditions at all.
387
+ let expressionContext;
388
+ return extensions.filter((extension)=>{
389
+ const expression = extension.displayConditionExpression;
390
+ if (typeof expression !== 'string' || expression.trim().length === 0) {
391
+ return true;
392
+ }
393
+ expressionContext ?? (expressionContext = state && typeof state === 'object' ? {
394
+ session,
395
+ ...state
396
+ } : {
397
+ session
398
+ });
399
+ try {
400
+ return Boolean(evaluateAsBoolean(expression, expressionContext));
401
+ } catch (e) {
402
+ console.error(`Error while evaluating expression '${expression}' for ${extension.id}` + (slotName ? ` in ${slotName}` : ''), e);
403
+ return false;
404
+ }
405
+ });
406
+ }
407
+ /**
408
+ * Gets the extensions a given rendering of a slot should display, in order. This is the supported
409
+ * way to ask what belongs in a slot; reading the extension store directly skips display conditions.
410
+ *
411
+ * Display conditions are evaluated against `state`, so pass whatever the slot is being rendered
412
+ * for. The same slot can be rendered many times with different state — once per row of a table,
413
+ * say — and each rendering can resolve to a different set of extensions. Omitting `state` hides
414
+ * every extension whose condition refers to it, since the condition cannot be evaluated.
415
+ *
416
+ * @param slotName The slot to load the extensions for
417
+ * @param state The state of the rendering of the slot the extensions will be displayed in
418
+ * @returns Those extensions assigned to the slot whose display conditions hold
419
+ */ export function getAssignedExtensions(slotName, state) {
420
+ return filterExtensionsByDisplayConditions(getCandidateExtensions(slotName), state, sessionStore.getState().session, slotName);
421
+ }
422
+ /**
423
+ * Gets everything assigned to a slot without evaluating any display condition, which
424
+ * {@link getAssignedExtensions} cannot do without knowing the state of a particular rendering.
425
+ *
426
+ * This exists for tools that present a slot's configuration rather than render it — the UI editor
427
+ * has to list an extension in order to let an implementer reorder or remove it, even where no
428
+ * rendering would display it. Anything deciding what to render wants `getAssignedExtensions()`.
429
+ *
430
+ * @internal
431
+ */ export function getCandidateExtensions(slotName) {
432
+ return extensionStore.getState().slots[slotName]?.candidateExtensions ?? [];
303
433
  }
304
434
  function calculateAssignedIds(config, attachedIds) {
305
435
  const addedIds = config.add || [];
@@ -312,13 +442,10 @@ function calculateAssignedIds(config, attachedIds) {
312
442
  ].filter((id)=>!removedIds.includes(id)).sort((idA, idB)=>{
313
443
  const ai = getOrder(idA, idOrder, extensions[getExtensionNameFromId(idA)]?.order, attachedIds);
314
444
  const bi = getOrder(idB, idOrder, extensions[getExtensionNameFromId(idB)]?.order, attachedIds);
315
- if (bi === -1) {
316
- return -1;
317
- } else if (ai === -1) {
318
- return 1;
319
- } else {
320
- return ai - bi;
321
- }
445
+ // Ties keep their input order — `Array.prototype.sort` is stable, and the input is
446
+ // `[...attachedIds, ...addedIds]`, so two extensions the ordering rules can't separate
447
+ // come out in the order the code and the configuration declared them.
448
+ return ai - bi;
322
449
  });
323
450
  }
324
451
  /**
@@ -326,9 +453,8 @@ function calculateAssignedIds(config, attachedIds) {
326
453
  *
327
454
  * @param moduleName The name of the module that contains the extension slot
328
455
  * @param slotName The extension slot name that is actually used
329
- * @param state Optional custom state for the slot, which will be stored in the extension store.
330
456
  * @internal
331
- */ export const registerExtensionSlot = (moduleName, slotName, state)=>extensionInternalStore.setState((currentState)=>{
457
+ */ export const registerExtensionSlot = (moduleName, slotName)=>extensionInternalStore.setState((currentState)=>{
332
458
  const existingModuleName = currentState.slots[slotName]?.moduleName;
333
459
  if (existingModuleName && existingModuleName != moduleName) {
334
460
  console.warn(`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}.`);
@@ -345,13 +471,12 @@ function calculateAssignedIds(config, attachedIds) {
345
471
  ...currentState.slots,
346
472
  [slotName]: {
347
473
  ...currentState.slots[slotName],
348
- moduleName,
349
- state
474
+ moduleName
350
475
  }
351
476
  }
352
477
  };
353
478
  }
354
- const slot = createNewExtensionSlotInfo(slotName, moduleName, state);
479
+ const slot = createNewExtensionSlotInfo(slotName, moduleName);
355
480
  return {
356
481
  ...currentState,
357
482
  slots: {
@@ -362,33 +487,21 @@ function calculateAssignedIds(config, attachedIds) {
362
487
  }
363
488
  };
364
489
  });
365
- /**
366
- * Used by extension slots to update the copy of the state for the extension slot
367
- *
368
- * @param slotName The name of the slot with state to update
369
- * @param state A copy of the new state
370
- * @param partial Whether this should be applied as a partial
371
- */ export function updateExtensionSlotState(slotName, state, partial = false) {
372
- extensionInternalStore.setState((currentState)=>{
373
- const newState = partial ? merge(currentState.slots[slotName].state, state) : state;
374
- return {
375
- ...currentState,
376
- slots: {
377
- ...currentState.slots,
378
- [slotName]: {
379
- ...currentState.slots[slotName],
380
- state: newState
381
- }
382
- }
383
- };
384
- });
385
- }
386
490
  /**
387
491
  * @internal
388
492
  * Just for testing.
389
- */ export const reset = ()=>extensionStore.setState(()=>{
390
- return {
493
+ */ export const reset = ()=>{
494
+ // Invalidation is carried in module state, so it has to be reset too or it bleeds into the
495
+ // next test as either a stale dirty set or a missing one.
496
+ dirtySlots = null;
497
+ updateInternalExtensionStore(()=>({
391
498
  slots: {},
392
499
  extensions: {}
393
- };
500
+ }));
501
+ getExtensionRenderingsStore().setState({
502
+ renderings: new Map()
503
+ });
504
+ extensionStore.setState({
505
+ slots: {}
394
506
  });
507
+ };
package/dist/public.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { getExtensionNameFromId, registerExtension, attach, detach, detachAll, getAssignedExtensions, registerExtensionSlot, } from './extensions';
2
2
  export { type LeftNavStore, setLeftNav, unsetLeftNav, type SetLeftNavParams } from './left-nav';
3
3
  export { type CancelLoading, renderExtension } from './render';
4
- export { type ExtensionMeta, type ExtensionRegistration, type ExtensionStore, type AssignedExtension, type ConnectedExtension, type ExtensionSlotState, getExtensionStore, } from './store';
4
+ export { type ExtensionMeta, type ExtensionRegistration, type ExtensionStore, type AssignedExtension, type ConnectedExtension, type ExtensionSlotCustomState, type ExtensionSlotState, getExtensionStore, } from './store';
5
5
  export { type WorkspaceRegistration } from './workspaces';
6
6
  export { type ExtensionData, type ComponentConfig } from './types';
7
7
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../src/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,KAAK,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAChG,OAAO,EAAE,KAAK,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,iBAAiB,GAClB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../src/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,KAAK,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAChG,OAAO,EAAE,KAAK,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,iBAAiB,GAClB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAKpB,MAAM,WAAW,aAAa;IAC5B,IAAI,IAAI,CAAC;CACV;AAED,KAAK,WAAW,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;AAgD3C;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,CAAC,GAAG,WAAW,EAChD,YAAY,EAAE,YAAY,EAC1B,WAAW,EAAE,WAAW,GAAG,CAAC,GAC3B,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAGlC;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,IAAI,WAAW,CAEjD;AAoCD;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,UAAU,EAAE,WAAW,EACvB,iBAAiB,EAAE,MAAM,EACzB,uBAAuB,EAAE,MAAM,EAC/B,WAAW,EAAE,MAAM,EACnB,cAAc,GAAE,CAAC,WAAW,EAAE,YAAY,KAAK,YAAuB,EACtE,eAAe,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GACxC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAyDxB"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,WAAW,EAEhB,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAKpB,MAAM,WAAW,aAAa;IAC5B,IAAI,IAAI,CAAC;CACV;AAED,KAAK,WAAW,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;AA2E3C;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,CAAC,GAAG,WAAW,EAChD,YAAY,EAAE,YAAY,EAC1B,WAAW,EAAE,WAAW,GAAG,CAAC,GAC3B,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAGlC;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,IAAI,WAAW,CAEjD;AAoCD;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,UAAU,EAAE,WAAW,EACvB,iBAAiB,EAAE,MAAM,EACzB,uBAAuB,EAAE,MAAM,EAC/B,WAAW,EAAE,MAAM,EACnB,cAAc,GAAE,CAAC,WAAW,EAAE,YAAY,KAAK,YAAuB,EACtE,eAAe,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GACxC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsGxB"}