@openmrs/esm-config 4.0.0-pre.0 → 4.0.1-pre.206
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 +10 -10
- package/dist/openmrs-esm-module-config.js +1 -1
- package/dist/openmrs-esm-module-config.js.map +1 -1
- package/package.json +4 -4
- package/src/module-config/module-config.test.ts +117 -13
- package/src/module-config/module-config.ts +136 -48
- package/src/module-config/state.ts +10 -4
- package/src/navigation/interpolate-string.test.ts +12 -3
- package/src/navigation/interpolate-string.ts +36 -11
- package/src/navigation/navigate.ts +17 -4
- package/src/public.ts +1 -1
- package/src/types.ts +5 -0
- package/src/validators/validator.ts +27 -2
- package/src/validators/validators.test.ts +18 -1
- package/src/validators/validators.ts +13 -1
- package/.turbo/turbo-lint.log +0 -2
- package/.turbo/turbo-test.log +0 -13
- package/.turbo/turbo-typescript.log +0 -2
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
ConfigInternalStore,
|
|
13
13
|
configInternalStore,
|
|
14
14
|
getExtensionConfig,
|
|
15
|
-
getExtensionsConfigStore,
|
|
16
15
|
ImplementerToolsConfigStore,
|
|
17
16
|
implementerToolsConfigStore,
|
|
18
17
|
temporaryConfigStore,
|
|
@@ -101,6 +100,33 @@ describe("defineConfigSchema", () => {
|
|
|
101
100
|
expect(console.error).not.toHaveBeenCalled();
|
|
102
101
|
});
|
|
103
102
|
|
|
103
|
+
it("runs top-level validators on filled-in config", () => {
|
|
104
|
+
const schema = {
|
|
105
|
+
foo: {
|
|
106
|
+
_type: Type.String,
|
|
107
|
+
_default: "bar-value",
|
|
108
|
+
},
|
|
109
|
+
fooStart: {
|
|
110
|
+
_type: Type.String,
|
|
111
|
+
_default: "bar",
|
|
112
|
+
},
|
|
113
|
+
_validators: [
|
|
114
|
+
validator(
|
|
115
|
+
(v) => v.foo.startsWith(v.fooStart),
|
|
116
|
+
(v) =>
|
|
117
|
+
`The value of \`foo\` must start with the value of \`fooStart\` (which is ${v.fooStart}).`
|
|
118
|
+
),
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
Config.defineConfigSchema("foo-module", schema);
|
|
122
|
+
Config.provide({ "foo-module": { foo: "different" } });
|
|
123
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
124
|
+
expect.stringMatching(
|
|
125
|
+
/The value of `foo` must start with the value of `fooStart`.*bar/
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
104
130
|
it("logs an error if a non-function validator is provided", () => {
|
|
105
131
|
const schema = {
|
|
106
132
|
bar: { _default: [], _validators: [false] },
|
|
@@ -175,6 +201,20 @@ describe("defineConfigSchema", () => {
|
|
|
175
201
|
Config.defineConfigSchema("mod-mod", schema);
|
|
176
202
|
expect(console.error).not.toHaveBeenCalled();
|
|
177
203
|
});
|
|
204
|
+
|
|
205
|
+
it("logs an error if the schema attempts to include a key named 'Display conditions'", () => {
|
|
206
|
+
const schema = {
|
|
207
|
+
"Display conditions": {
|
|
208
|
+
_type: Type.Array,
|
|
209
|
+
_default: [],
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
Config.defineConfigSchema("mod-mod", schema);
|
|
214
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
215
|
+
expect.stringMatching(/mod-mod.*\bDisplay conditions\b/)
|
|
216
|
+
);
|
|
217
|
+
});
|
|
178
218
|
});
|
|
179
219
|
|
|
180
220
|
describe("getConfig", () => {
|
|
@@ -264,7 +304,7 @@ describe("getConfig", () => {
|
|
|
264
304
|
await Config.getConfig("foo-module");
|
|
265
305
|
expect(console.error).toHaveBeenCalledWith(
|
|
266
306
|
expect.stringMatching(
|
|
267
|
-
/
|
|
307
|
+
/Invalid configuration for foo-module.bar.*c must equal a\.b \+ diff/
|
|
268
308
|
)
|
|
269
309
|
);
|
|
270
310
|
|
|
@@ -275,7 +315,11 @@ describe("getConfig", () => {
|
|
|
275
315
|
Config.provide(goodConfig);
|
|
276
316
|
const result = await Config.getConfig("foo-module");
|
|
277
317
|
expect(result).toStrictEqual({
|
|
278
|
-
bar: {
|
|
318
|
+
bar: {
|
|
319
|
+
a: { b: 0 },
|
|
320
|
+
c: 2,
|
|
321
|
+
diff: 2,
|
|
322
|
+
},
|
|
279
323
|
});
|
|
280
324
|
});
|
|
281
325
|
|
|
@@ -360,6 +404,19 @@ describe("getConfig", () => {
|
|
|
360
404
|
);
|
|
361
405
|
});
|
|
362
406
|
|
|
407
|
+
it("tolerates defaults that don't pass validation", async () => {
|
|
408
|
+
Config.defineConfigSchema("foo-module", {
|
|
409
|
+
foo: {
|
|
410
|
+
_default: null,
|
|
411
|
+
_validators: [
|
|
412
|
+
validator((val) => val.startsWith("thi"), "must start with 'thi'"),
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
await Config.getConfig("foo-module");
|
|
417
|
+
expect(console.error).not.toHaveBeenCalled();
|
|
418
|
+
});
|
|
419
|
+
|
|
363
420
|
it("validators pass", async () => {
|
|
364
421
|
Config.defineConfigSchema("foo-module", {
|
|
365
422
|
foo: {
|
|
@@ -446,16 +503,14 @@ describe("getConfig", () => {
|
|
|
446
503
|
Config.defineConfigSchema("object-def", {
|
|
447
504
|
furi: {
|
|
448
505
|
_type: Type.Object,
|
|
449
|
-
_elements: {
|
|
450
|
-
|
|
451
|
-
},
|
|
452
|
-
_default: {
|
|
453
|
-
kake: { gohan: "ok" },
|
|
454
|
-
},
|
|
506
|
+
_elements: { gohan: { _type: Type.String } },
|
|
507
|
+
_default: { kake: { gohan: "ok" } },
|
|
455
508
|
},
|
|
456
509
|
});
|
|
457
510
|
const config = await Config.getConfig("object-def");
|
|
458
|
-
expect(config).toStrictEqual({
|
|
511
|
+
expect(config).toStrictEqual({
|
|
512
|
+
furi: { kake: { gohan: "ok" } },
|
|
513
|
+
});
|
|
459
514
|
});
|
|
460
515
|
|
|
461
516
|
it("interpolates freeform object element defaults", async () => {
|
|
@@ -505,6 +560,27 @@ describe("getConfig", () => {
|
|
|
505
560
|
expect(console.error).not.toHaveBeenCalled();
|
|
506
561
|
});
|
|
507
562
|
|
|
563
|
+
it("supports array of string elements", async () => {
|
|
564
|
+
Config.defineConfigSchema("foo-module", {
|
|
565
|
+
foo: {
|
|
566
|
+
_type: Type.Array,
|
|
567
|
+
_default: ["bar"],
|
|
568
|
+
_elements: {
|
|
569
|
+
_type: Type.String,
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
const testConfig = {
|
|
574
|
+
"foo-module": {
|
|
575
|
+
foo: ["bar", "baz", "qux"],
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
Config.provide(testConfig);
|
|
579
|
+
const config = await Config.getConfig("foo-module");
|
|
580
|
+
expect(config.foo).toStrictEqual(["bar", "baz", "qux"]);
|
|
581
|
+
expect(console.error).not.toHaveBeenCalled();
|
|
582
|
+
});
|
|
583
|
+
|
|
508
584
|
it("supports validation of array elements", async () => {
|
|
509
585
|
Config.defineConfigSchema("foo-module", {
|
|
510
586
|
foo: {
|
|
@@ -638,9 +714,7 @@ describe("getConfig", () => {
|
|
|
638
714
|
Config.provide(badConfig);
|
|
639
715
|
await Config.getConfig("foo-module");
|
|
640
716
|
expect(console.error).toHaveBeenCalledWith(
|
|
641
|
-
expect.stringMatching(
|
|
642
|
-
/value.*{\"a\":{\"b\":1},\"c\":3}.*foo-module.bar\[1\].*c must equal a\.b \+ 1/
|
|
643
|
-
)
|
|
717
|
+
expect.stringMatching(/foo-module.bar\[1\].*c must equal a\.b \+ 1/i)
|
|
644
718
|
);
|
|
645
719
|
|
|
646
720
|
await resetAll();
|
|
@@ -765,9 +839,29 @@ describe("implementer tools config", () => {
|
|
|
765
839
|
_description: "All the foo",
|
|
766
840
|
_validators: [],
|
|
767
841
|
},
|
|
842
|
+
"Display conditions": {
|
|
843
|
+
privileges: {
|
|
844
|
+
_default: [],
|
|
845
|
+
_description:
|
|
846
|
+
"The privilege(s) the user must have to use this extension",
|
|
847
|
+
_source: "default",
|
|
848
|
+
_type: Type.Array,
|
|
849
|
+
_value: [],
|
|
850
|
+
},
|
|
851
|
+
},
|
|
768
852
|
},
|
|
769
853
|
"bar-module": {
|
|
770
854
|
bar: { _value: "baz", _source: "my config source", _default: "quinn" },
|
|
855
|
+
"Display conditions": {
|
|
856
|
+
privileges: {
|
|
857
|
+
_default: [],
|
|
858
|
+
_description:
|
|
859
|
+
"The privilege(s) the user must have to use this extension",
|
|
860
|
+
_source: "default",
|
|
861
|
+
_type: Type.Array,
|
|
862
|
+
_value: [],
|
|
863
|
+
},
|
|
864
|
+
},
|
|
771
865
|
},
|
|
772
866
|
});
|
|
773
867
|
});
|
|
@@ -917,6 +1011,16 @@ describe("extension slot config", () => {
|
|
|
917
1011
|
remove: { _value: ["bar"], _source: "provided" },
|
|
918
1012
|
},
|
|
919
1013
|
},
|
|
1014
|
+
"Display conditions": {
|
|
1015
|
+
privileges: {
|
|
1016
|
+
_default: [],
|
|
1017
|
+
_description:
|
|
1018
|
+
"The privilege(s) the user must have to use this extension",
|
|
1019
|
+
_source: "default",
|
|
1020
|
+
_type: Type.Array,
|
|
1021
|
+
_value: [],
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
920
1024
|
},
|
|
921
1025
|
});
|
|
922
1026
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @module @category Config */
|
|
2
|
-
import { clone, reduce, mergeDeepRight, equals } from "ramda";
|
|
2
|
+
import { clone, reduce, mergeDeepRight, equals, omit } from "ramda";
|
|
3
3
|
import {
|
|
4
4
|
Config,
|
|
5
5
|
ConfigObject,
|
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
implementerToolsConfigStore,
|
|
28
28
|
temporaryConfigStore,
|
|
29
29
|
getExtensionSlotsConfigStore,
|
|
30
|
-
ExtensionsConfigStore,
|
|
31
30
|
} from "./state";
|
|
32
31
|
import type {} from "@openmrs/esm-globals";
|
|
33
32
|
import { TemporaryConfigStore } from "..";
|
|
@@ -192,9 +191,14 @@ function computeExtensionConfigs(
|
|
|
192
191
|
*/
|
|
193
192
|
export function defineConfigSchema(moduleName: string, schema: ConfigSchema) {
|
|
194
193
|
validateConfigSchema(moduleName, schema);
|
|
194
|
+
const enhancedSchema = mergeDeepRight(
|
|
195
|
+
schema,
|
|
196
|
+
displayConditionsSchema
|
|
197
|
+
) as ConfigSchema;
|
|
198
|
+
|
|
195
199
|
const state = configInternalStore.getState();
|
|
196
200
|
configInternalStore.setState({
|
|
197
|
-
schemas: { ...state.schemas, [moduleName]:
|
|
201
|
+
schemas: { ...state.schemas, [moduleName]: enhancedSchema },
|
|
198
202
|
});
|
|
199
203
|
}
|
|
200
204
|
|
|
@@ -220,14 +224,20 @@ export function defineExtensionConfigSchema(
|
|
|
220
224
|
schema: ConfigSchema
|
|
221
225
|
) {
|
|
222
226
|
validateConfigSchema(extensionName, schema);
|
|
227
|
+
const enhancedSchema = mergeDeepRight(
|
|
228
|
+
schema,
|
|
229
|
+
displayConditionsSchema
|
|
230
|
+
) as ConfigSchema;
|
|
231
|
+
|
|
223
232
|
const state = configInternalStore.getState();
|
|
224
233
|
if (state.schemas[extensionName]) {
|
|
225
234
|
console.warn(
|
|
226
235
|
`Config schema for extension ${extensionName} already exists. If there are multiple extensions with this same name, one will probably crash.`
|
|
227
236
|
);
|
|
228
237
|
}
|
|
238
|
+
|
|
229
239
|
configInternalStore.setState({
|
|
230
|
-
schemas: { ...state.schemas, [extensionName]:
|
|
240
|
+
schemas: { ...state.schemas, [extensionName]: enhancedSchema },
|
|
231
241
|
});
|
|
232
242
|
}
|
|
233
243
|
|
|
@@ -256,7 +266,8 @@ export function getConfig(moduleName: string): Promise<Config> {
|
|
|
256
266
|
const store = getConfigStore(moduleName);
|
|
257
267
|
function update(state: ConfigStore) {
|
|
258
268
|
if (state.loaded && state.config) {
|
|
259
|
-
|
|
269
|
+
const config = omit(["Display conditions"], state.config);
|
|
270
|
+
resolve(config);
|
|
260
271
|
unsubscribe && unsubscribe();
|
|
261
272
|
}
|
|
262
273
|
}
|
|
@@ -280,8 +291,9 @@ export function processConfig(
|
|
|
280
291
|
keyPathContext: string,
|
|
281
292
|
devDefaultsAreOn: boolean = false
|
|
282
293
|
) {
|
|
283
|
-
|
|
294
|
+
validateStructure(schema, providedConfig, keyPathContext);
|
|
284
295
|
const config = setDefaults(schema, providedConfig, devDefaultsAreOn);
|
|
296
|
+
runAllValidatorsInConfigTree(schema, config, keyPathContext);
|
|
285
297
|
return config;
|
|
286
298
|
}
|
|
287
299
|
|
|
@@ -326,12 +338,13 @@ function computeExtensionConfig(
|
|
|
326
338
|
// TODO: validate that a schema exists for the module
|
|
327
339
|
const schema =
|
|
328
340
|
extensionConfigSchema ?? configState.schemas[extensionModuleName];
|
|
329
|
-
|
|
341
|
+
validateStructure(schema, combinedConfig, nameOfSchemaSource);
|
|
330
342
|
const config = setDefaults(
|
|
331
343
|
schema,
|
|
332
344
|
combinedConfig,
|
|
333
345
|
configState.devDefaultsAreOn
|
|
334
346
|
);
|
|
347
|
+
runAllValidatorsInConfigTree(schema, config, nameOfSchemaSource);
|
|
335
348
|
delete config.extensionSlots;
|
|
336
349
|
return config;
|
|
337
350
|
}
|
|
@@ -360,7 +373,7 @@ function getSchemaWithValuesAndSources(schema) {
|
|
|
360
373
|
return obj;
|
|
361
374
|
}, {});
|
|
362
375
|
} else {
|
|
363
|
-
//
|
|
376
|
+
// at this point, the schema is bad and an error will have been logged during schema validation
|
|
364
377
|
return {};
|
|
365
378
|
}
|
|
366
379
|
}
|
|
@@ -490,6 +503,12 @@ function validateConfigSchema(
|
|
|
490
503
|
const thisKeyPath = keyPath + (keyPath && ".") + key;
|
|
491
504
|
const schemaPart = schema[key] as ConfigSchema;
|
|
492
505
|
|
|
506
|
+
if (thisKeyPath === "Display conditions") {
|
|
507
|
+
console.error(
|
|
508
|
+
`${moduleName} declares a configuration option called "Display conditions"; the "Display conditions" option is a reserved name. ${updateMessage}`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
493
512
|
if (!isOrdinaryObject(schemaPart)) {
|
|
494
513
|
console.error(
|
|
495
514
|
`${moduleName} has bad config schema definition for key '${thisKeyPath}'. ${updateMessage}`
|
|
@@ -571,8 +590,9 @@ function getConfigForModule(
|
|
|
571
590
|
moduleName,
|
|
572
591
|
getProvidedConfigs(configState, tempConfigState)
|
|
573
592
|
);
|
|
574
|
-
|
|
593
|
+
validateStructure(schema, inputConfig, moduleName);
|
|
575
594
|
const config = setDefaults(schema, inputConfig, configState.devDefaultsAreOn);
|
|
595
|
+
runAllValidatorsInConfigTree(schema, config, moduleName);
|
|
576
596
|
delete config.extensionSlots;
|
|
577
597
|
return config;
|
|
578
598
|
}
|
|
@@ -593,21 +613,25 @@ function mergeConfigs(configs: Array<Config>) {
|
|
|
593
613
|
return mergeDeepAll({}, configs) as Config;
|
|
594
614
|
}
|
|
595
615
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
616
|
+
/**
|
|
617
|
+
* Recursively check the provided config tree to make sure that all
|
|
618
|
+
* of the provided properties exist in the schema, and that types are
|
|
619
|
+
* correct. Does not run validators yet, since those will be run on
|
|
620
|
+
* the config with the defaults filled in.
|
|
621
|
+
*/
|
|
622
|
+
function validateStructure(
|
|
600
623
|
schema: ConfigSchema,
|
|
601
624
|
config: ConfigObject,
|
|
602
625
|
keyPath = ""
|
|
603
|
-
)
|
|
626
|
+
) {
|
|
627
|
+
// validate each constituent element
|
|
604
628
|
for (const key of Object.keys(config)) {
|
|
605
629
|
const value = config[key];
|
|
606
630
|
const thisKeyPath = keyPath + "." + key;
|
|
607
631
|
const schemaPart = schema[key] as ConfigSchema;
|
|
608
632
|
|
|
609
633
|
if (!schema.hasOwnProperty(key)) {
|
|
610
|
-
if (key
|
|
634
|
+
if (!(key === "extensionSlots" && keyPath !== "")) {
|
|
611
635
|
console.error(
|
|
612
636
|
`Unknown config key '${thisKeyPath}' provided. Ignoring.`
|
|
613
637
|
);
|
|
@@ -616,27 +640,34 @@ const validateConfig = (
|
|
|
616
640
|
continue;
|
|
617
641
|
}
|
|
618
642
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
643
|
+
validateBranchStructure(schemaPart, value, thisKeyPath);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function validateBranchStructure(
|
|
648
|
+
schemaPart: ConfigSchema,
|
|
649
|
+
value: any,
|
|
650
|
+
keyPath: string
|
|
651
|
+
) {
|
|
652
|
+
checkType(keyPath, schemaPart._type, value);
|
|
653
|
+
|
|
654
|
+
if (isOrdinaryObject(value)) {
|
|
655
|
+
// structurally validate only if there's elements specified
|
|
656
|
+
// or there's no `_default` value (which would indicate a freeform object)
|
|
657
|
+
if (schemaPart._type === Type.Object) {
|
|
658
|
+
validateDictionaryStructure(schemaPart, value, keyPath);
|
|
659
|
+
} else if (!schemaPart.hasOwnProperty("_default")) {
|
|
660
|
+
// recurse to validate nested object structure
|
|
661
|
+
validateStructure(schemaPart, value, keyPath);
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
if (schemaPart._type === Type.Array) {
|
|
665
|
+
validateArrayStructure(schemaPart, value, keyPath);
|
|
635
666
|
}
|
|
636
667
|
}
|
|
637
|
-
}
|
|
668
|
+
}
|
|
638
669
|
|
|
639
|
-
function
|
|
670
|
+
function validateDictionaryStructure(
|
|
640
671
|
dictionarySchema: ConfigSchema,
|
|
641
672
|
config: ConfigObject,
|
|
642
673
|
keyPath: string
|
|
@@ -644,12 +675,12 @@ function validateDictionary(
|
|
|
644
675
|
if (dictionarySchema._elements) {
|
|
645
676
|
for (const key of Object.keys(config)) {
|
|
646
677
|
const value = config[key];
|
|
647
|
-
|
|
678
|
+
validateStructure(dictionarySchema._elements, value, `${keyPath}.${key}`);
|
|
648
679
|
}
|
|
649
680
|
}
|
|
650
681
|
}
|
|
651
682
|
|
|
652
|
-
function
|
|
683
|
+
function validateArrayStructure(
|
|
653
684
|
arraySchema: ConfigSchema,
|
|
654
685
|
value: ConfigObject,
|
|
655
686
|
keyPath: string
|
|
@@ -657,17 +688,55 @@ function validateArray(
|
|
|
657
688
|
// if there is an array element object schema, verify that elements match it
|
|
658
689
|
if (hasObjectSchema(arraySchema._elements)) {
|
|
659
690
|
for (let i = 0; i < value.length; i++) {
|
|
660
|
-
|
|
691
|
+
validateBranchStructure(
|
|
692
|
+
arraySchema._elements,
|
|
693
|
+
value[i],
|
|
694
|
+
`${keyPath}[${i}]`
|
|
695
|
+
);
|
|
661
696
|
}
|
|
662
697
|
}
|
|
663
698
|
|
|
664
699
|
for (let i = 0; i < value.length; i++) {
|
|
665
700
|
checkType(`${keyPath}[${i}]`, arraySchema._elements?._type, value[i]);
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Run all the validators in the config tree. This should be run
|
|
706
|
+
* on the config object after it has been filled in with all the defaults, since
|
|
707
|
+
* higher-level validators may refer to default values.
|
|
708
|
+
*/
|
|
709
|
+
function runAllValidatorsInConfigTree(
|
|
710
|
+
schema: ConfigSchema,
|
|
711
|
+
config: ConfigObject,
|
|
712
|
+
keyPath = ""
|
|
713
|
+
) {
|
|
714
|
+
// If `!schema`, there should have been a structural validation error printed already.
|
|
715
|
+
if (schema) {
|
|
716
|
+
if (config !== schema._default) {
|
|
717
|
+
runValidators(keyPath, schema._validators, config);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (isOrdinaryObject(config)) {
|
|
721
|
+
for (const key of Object.keys(config)) {
|
|
722
|
+
const value = config[key];
|
|
723
|
+
const thisKeyPath = keyPath + "." + key;
|
|
724
|
+
const schemaPart = schema[key] as ConfigSchema;
|
|
725
|
+
if (schema._type === Type.Object && schema._elements) {
|
|
726
|
+
runAllValidatorsInConfigTree(schema._elements, value, thisKeyPath);
|
|
727
|
+
} else {
|
|
728
|
+
runAllValidatorsInConfigTree(schemaPart, value, thisKeyPath);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
} else if (Array.isArray(config) && schema._elements) {
|
|
732
|
+
for (let i = 0; i < config.length; i++) {
|
|
733
|
+
runAllValidatorsInConfigTree(
|
|
734
|
+
schema._elements,
|
|
735
|
+
config[i],
|
|
736
|
+
`${keyPath}[${i}]`
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
671
740
|
}
|
|
672
741
|
}
|
|
673
742
|
|
|
@@ -699,15 +768,21 @@ function runValidators(
|
|
|
699
768
|
const validatorResult = validator(value);
|
|
700
769
|
|
|
701
770
|
if (typeof validatorResult === "string") {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
771
|
+
if (typeof value === "object") {
|
|
772
|
+
console.error(
|
|
773
|
+
`Invalid configuration for ${keyPath}: ${validatorResult}`
|
|
774
|
+
);
|
|
775
|
+
} else {
|
|
776
|
+
console.error(
|
|
777
|
+
`Invalid configuration value ${value} for ${keyPath}: ${validatorResult}`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
707
780
|
}
|
|
708
781
|
}
|
|
709
782
|
} catch (e) {
|
|
710
|
-
console.error(
|
|
783
|
+
console.error(
|
|
784
|
+
`Skipping invalid validator at "${keyPath}". Encountered error\n\t${e}`
|
|
785
|
+
);
|
|
711
786
|
}
|
|
712
787
|
}
|
|
713
788
|
}
|
|
@@ -748,7 +823,7 @@ const setDefaults = (
|
|
|
748
823
|
const elements = schemaPart._elements;
|
|
749
824
|
|
|
750
825
|
if (configPart && hasObjectSchema(elements)) {
|
|
751
|
-
if (schemaPart._type === Type.Array) {
|
|
826
|
+
if (schemaPart._type === Type.Array && Array.isArray(configPart)) {
|
|
752
827
|
const configWithDefaults = configPart.map((conf: Config) =>
|
|
753
828
|
setDefaults(elements, conf, devDefaultsAreOn)
|
|
754
829
|
);
|
|
@@ -805,3 +880,16 @@ function getExtensionNameFromId(extensionId: string) {
|
|
|
805
880
|
const [extensionName] = extensionId.split("#");
|
|
806
881
|
return extensionName;
|
|
807
882
|
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* The displayConditionsSchema is implicitly included in every configuration schema
|
|
886
|
+
*/
|
|
887
|
+
const displayConditionsSchema: ConfigSchema = {
|
|
888
|
+
"Display conditions": {
|
|
889
|
+
privileges: {
|
|
890
|
+
_description: "The privilege(s) the user must have to use this extension",
|
|
891
|
+
_type: Type.Array,
|
|
892
|
+
_default: [],
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** @module @category Config */
|
|
2
2
|
import { createGlobalStore, getGlobalStore } from "@openmrs/esm-state";
|
|
3
|
+
import { omit } from "ramda";
|
|
3
4
|
import {
|
|
4
5
|
Config,
|
|
5
6
|
ConfigObject,
|
|
@@ -199,11 +200,16 @@ export function getExtensionsConfigStore() {
|
|
|
199
200
|
|
|
200
201
|
/** @internal */
|
|
201
202
|
export function getExtensionConfig(slotName: string, extensionId: string) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
const extensionConfig = Object.assign(
|
|
204
|
+
{},
|
|
205
|
+
getExtensionConfigFromStore(
|
|
206
|
+
getExtensionsConfigStore().getState(),
|
|
207
|
+
slotName,
|
|
208
|
+
extensionId
|
|
209
|
+
)
|
|
206
210
|
);
|
|
211
|
+
extensionConfig.config = omit(["Display conditions"], extensionConfig.config);
|
|
212
|
+
return extensionConfig;
|
|
207
213
|
}
|
|
208
214
|
|
|
209
215
|
/** @internal */
|
|
@@ -6,6 +6,15 @@ describe("interpolateUrl", () => {
|
|
|
6
6
|
expect(result).toBe("test /openmrs /openmrs/spa ok");
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
+
it("interpolates other URL template parameters", () => {
|
|
10
|
+
const result = interpolateUrl("${openmrsSpaBase}/patient/${patientUuid}", {
|
|
11
|
+
patientUuid: "4fcb7185-c6c9-450f-8828-ccae9436bd82",
|
|
12
|
+
});
|
|
13
|
+
expect(result).toBe(
|
|
14
|
+
"/openmrs/spa/patient/4fcb7185-c6c9-450f-8828-ccae9436bd82"
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
9
18
|
it("works when no interpolation needed", () => {
|
|
10
19
|
const result = interpolateUrl("test ok");
|
|
11
20
|
expect(result).toBe("test ok");
|
|
@@ -15,14 +24,14 @@ describe("interpolateUrl", () => {
|
|
|
15
24
|
describe("interpolateString", () => {
|
|
16
25
|
it("interpolates template elements", () => {
|
|
17
26
|
const result = interpolateString("test ${one} ${two} 3", {
|
|
18
|
-
one: 1,
|
|
19
|
-
two: 2,
|
|
27
|
+
one: "1",
|
|
28
|
+
two: "2",
|
|
20
29
|
});
|
|
21
30
|
expect(result).toBe("test 1 2 3");
|
|
22
31
|
});
|
|
23
32
|
|
|
24
33
|
it("tolerates extra parameters", () => {
|
|
25
|
-
const result = interpolateString("test ok", { one: 1, two: 2 });
|
|
34
|
+
const result = interpolateString("test ok", { one: "1", two: "2" });
|
|
26
35
|
expect(result).toBe("test ok");
|
|
27
36
|
});
|
|
28
37
|
});
|
|
@@ -7,38 +7,63 @@ function trimTrailingSlash(str: string) {
|
|
|
7
7
|
/**
|
|
8
8
|
* Interpolates a string with openmrsBase and openmrsSpaBase.
|
|
9
9
|
*
|
|
10
|
-
* Useful for accepting `${openmrsBase}` or `${openmrsSpaBase}` template
|
|
10
|
+
* Useful for accepting `${openmrsBase}` or `${openmrsSpaBase}`plus additional template
|
|
11
11
|
* parameters in configurable URLs.
|
|
12
12
|
*
|
|
13
|
+
* Example usage:
|
|
14
|
+
* ```js
|
|
15
|
+
* interpolateUrl("test ${openmrsBase} ${openmrsSpaBase} ok");
|
|
16
|
+
* // will return "test /openmrs /openmrs/spa ok"
|
|
17
|
+
*
|
|
18
|
+
* interpolateUrl("${openmrsSpaBase}/patient/${patientUuid}", {
|
|
19
|
+
* patientUuid: "4fcb7185-c6c9-450f-8828-ccae9436bd82",
|
|
20
|
+
* }); // will return "/openmrs/spa/patient/4fcb7185-c6c9-450f-8828-ccae9436bd82"
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* This can be used in conjunction with the `navigate` function like so
|
|
24
|
+
* ```js
|
|
25
|
+
* navigate({
|
|
26
|
+
* to: interpolateUrl(
|
|
27
|
+
* "${openmrsSpaBase}/patient/${patientUuid}",
|
|
28
|
+
* { patientUuid: patient.uuid }
|
|
29
|
+
* )
|
|
30
|
+
* }); // will navigate to "/openmrs/spa/patient/4fcb7185-c6c9-450f-8828-ccae9436bd82"
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
13
33
|
* @param template A string to interpolate
|
|
34
|
+
* @param additionalParams Additional values to interpolate into the string template
|
|
14
35
|
*/
|
|
15
|
-
export function interpolateUrl(
|
|
36
|
+
export function interpolateUrl(
|
|
37
|
+
template: string,
|
|
38
|
+
additionalParams?: { [key: string]: string }
|
|
39
|
+
): string {
|
|
16
40
|
const openmrsSpaBase = trimTrailingSlash(window.getOpenmrsSpaBase());
|
|
17
41
|
return interpolateString(template, {
|
|
18
42
|
openmrsBase: window.openmrsBase,
|
|
19
43
|
openmrsSpaBase: openmrsSpaBase,
|
|
44
|
+
...additionalParams,
|
|
20
45
|
}).replace(/^\/\//, "/"); // remove extra initial slash if present
|
|
21
46
|
}
|
|
22
47
|
|
|
23
48
|
/**
|
|
24
49
|
* Interpolates values of `params` into the `template` string.
|
|
25
50
|
*
|
|
26
|
-
* Useful for additional template parameters in URLs.
|
|
27
|
-
*
|
|
28
51
|
* Example usage:
|
|
29
52
|
* ```js
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* });
|
|
53
|
+
* interpolateString("test ${one} ${two} 3", {
|
|
54
|
+
* one: "1",
|
|
55
|
+
* two: "2",
|
|
56
|
+
* }); // will return "test 1 2 3"
|
|
57
|
+
* interpolateString("test ok", { one: "1", two: "2" }) // will return "test ok"
|
|
36
58
|
* ```
|
|
37
59
|
*
|
|
38
60
|
* @param template With optional params wrapped in `${ }`
|
|
39
61
|
* @param params Values to interpolate into the string template
|
|
40
62
|
*/
|
|
41
|
-
export function interpolateString(
|
|
63
|
+
export function interpolateString(
|
|
64
|
+
template: string,
|
|
65
|
+
params: { [key: string]: string }
|
|
66
|
+
): string {
|
|
42
67
|
const names = Object.keys(params);
|
|
43
68
|
return names.reduce(
|
|
44
69
|
(prev, curr) => prev.split("${" + curr + "}").join(params[curr]),
|