@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.
@@ -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
- /value.*{\"a\":{\"b\":5}\}.*foo-module.bar.*c must equal a\.b \+ diff/
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: { a: { b: 0 }, c: 2, diff: 2 },
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
- gohan: { _type: Type.String },
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({ furi: { kake: { gohan: "ok" } } });
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]: schema },
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]: schema },
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
- resolve(state.config);
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
- validateConfig(schema, providedConfig, keyPathContext);
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
- validateConfig(schema, combinedConfig, nameOfSchemaSource);
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
- // Schema is bad; error will have been logged during schema validation
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
- validateConfig(schema, inputConfig, moduleName);
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
- // Recursively check the provided config tree to make sure that all
597
- // of the provided properties exist in the schema. Run validators
598
- // where present in the schema.
599
- const validateConfig = (
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 !== "extensionSlots") {
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
- checkType(thisKeyPath, schemaPart._type, value);
620
- runValidators(thisKeyPath, schemaPart._validators, value);
621
-
622
- if (isOrdinaryObject(value)) {
623
- // structurally validate only if there's elements specified
624
- // or there's a `_default` value, which indicates a freeform object
625
- if (schemaPart._type === Type.Object) {
626
- validateDictionary(schemaPart, value, thisKeyPath);
627
- } else if (!schemaPart.hasOwnProperty("_default")) {
628
- // recurse to validate nested object structure
629
- validateConfig(schemaPart, value, thisKeyPath);
630
- }
631
- } else {
632
- if (schemaPart._type === Type.Array) {
633
- validateArray(schemaPart, value, thisKeyPath);
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 validateDictionary(
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
- validateConfig(dictionarySchema._elements, value, `${keyPath}.${key}`);
678
+ validateStructure(dictionarySchema._elements, value, `${keyPath}.${key}`);
648
679
  }
649
680
  }
650
681
  }
651
682
 
652
- function validateArray(
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
- validateConfig(arraySchema._elements, value[i], `${keyPath}[${i}]`);
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
- runValidators(
667
- `${keyPath}[${i}]`,
668
- arraySchema._elements?._validators,
669
- value[i]
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
- const valueString =
703
- typeof value === "object" ? JSON.stringify(value) : value;
704
- console.error(
705
- `Invalid configuration value ${valueString} for ${keyPath}: ${validatorResult}`
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(`Skipping invalid validator at "${keyPath}".`);
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
- return getExtensionConfigFromStore(
203
- getExtensionsConfigStore().getState(),
204
- slotName,
205
- extensionId
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(template: string): string {
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
- * navigate({
31
- * to: interpolateString(
32
- * config.links.patientChart,
33
- * { patientUuid: patient.uuid }
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(template: string, params: object): string {
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]),