@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/extensions.d.ts +23 -14
- package/dist/extensions.d.ts.map +1 -1
- package/dist/extensions.js +219 -106
- package/dist/public.d.ts +1 -1
- package/dist/public.d.ts.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +102 -35
- package/dist/store.d.ts +86 -19
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +181 -19
- package/package.json +13 -13
- package/src/extensions.test.ts +520 -72
- package/src/extensions.ts +262 -168
- package/src/public.ts +1 -0
- package/src/render.test.ts +140 -4
- package/src/render.ts +106 -33
- package/src/store.ts +290 -37
package/.turbo/turbo-build.log
CHANGED
package/dist/extensions.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
67
|
-
*
|
|
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
|
-
*
|
|
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
|
|
87
|
+
export declare function getCandidateExtensions(slotName: string): Array<AssignedExtension>;
|
|
79
88
|
/**
|
|
80
|
-
* Used by extension slots
|
|
89
|
+
* Used by by extension slots at mount time.
|
|
81
90
|
*
|
|
82
|
-
* @param
|
|
83
|
-
* @param
|
|
84
|
-
* @
|
|
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
|
|
95
|
+
export declare const registerExtensionSlot: (moduleName: string, slotName: string) => void;
|
|
87
96
|
/**
|
|
88
97
|
* @internal
|
|
89
98
|
* Just for testing.
|
package/dist/extensions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"AAAA,kCAAkC;
|
|
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"}
|
package/dist/extensions.js
CHANGED
|
@@ -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
|
-
* -
|
|
7
|
+
* - displayed (computed from assigned)
|
|
8
8
|
*/ import { sessionStore, userHasAccess } from "@openmrs/esm-api";
|
|
9
|
-
import { getExtensionConfigFromExtensionSlotStore, getExtensionConfigFromStore,
|
|
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
|
-
|
|
22
|
-
|
|
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(
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 (
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
149
|
+
markSlotsDirty(null);
|
|
57
150
|
}
|
|
58
151
|
updateOutputStoreToCurrent();
|
|
59
152
|
subscribeConnectivityChanged(updateOutputStoreToCurrent);
|
|
60
|
-
function createNewExtensionSlotInfo(slotName, moduleName
|
|
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)=>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
293
|
-
*
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
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
|
|
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 = ()=>
|
|
390
|
-
|
|
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
|
package/dist/public.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/render.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|