@openmrs/esm-implementer-tools-app 6.3.1-pre.3234 → 6.3.1-pre.3236

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.
@@ -391,30 +391,6 @@
391
391
  "hash": "ad8873304325ee50",
392
392
  "childrenByOrder": {}
393
393
  },
394
- {
395
- "rendered": true,
396
- "initial": false,
397
- "entry": false,
398
- "recorded": false,
399
- "size": 828969,
400
- "sizes": {
401
- "javascript": 828969
402
- },
403
- "names": [],
404
- "idHints": [],
405
- "runtime": [
406
- "@openmrs/esm-implementer-tools-app",
407
- "main"
408
- ],
409
- "files": [
410
- "3150.js"
411
- ],
412
- "auxiliaryFiles": [
413
- "3150.js.map"
414
- ],
415
- "hash": "837abee3ee3cab10",
416
- "childrenByOrder": {}
417
- },
418
394
  {
419
395
  "rendered": true,
420
396
  "initial": false,
@@ -536,9 +512,9 @@
536
512
  "initial": false,
537
513
  "entry": false,
538
514
  "recorded": false,
539
- "size": 1119,
515
+ "size": 1500,
540
516
  "sizes": {
541
- "javascript": 1119
517
+ "javascript": 1500
542
518
  },
543
519
  "names": [],
544
520
  "idHints": [],
@@ -550,7 +526,7 @@
550
526
  "4300.js"
551
527
  ],
552
528
  "auxiliaryFiles": [],
553
- "hash": "e4d6b54b001185fc",
529
+ "hash": "e5ec50abd2006d8c",
554
530
  "childrenByOrder": {}
555
531
  },
556
532
  {
@@ -776,7 +752,7 @@
776
752
  "auxiliaryFiles": [
777
753
  "5563.js.map"
778
754
  ],
779
- "hash": "50c736da17271a3b",
755
+ "hash": "d3d48bbe9d564559",
780
756
  "childrenByOrder": {}
781
757
  },
782
758
  {
@@ -901,6 +877,30 @@
901
877
  "hash": "7e9ab64623e07a93",
902
878
  "childrenByOrder": {}
903
879
  },
880
+ {
881
+ "rendered": true,
882
+ "initial": false,
883
+ "entry": false,
884
+ "recorded": false,
885
+ "size": 847661,
886
+ "sizes": {
887
+ "javascript": 847661
888
+ },
889
+ "names": [],
890
+ "idHints": [],
891
+ "runtime": [
892
+ "@openmrs/esm-implementer-tools-app",
893
+ "main"
894
+ ],
895
+ "files": [
896
+ "6132.js"
897
+ ],
898
+ "auxiliaryFiles": [
899
+ "6132.js.map"
900
+ ],
901
+ "hash": "fa1951d76a17d7ff",
902
+ "childrenByOrder": {}
903
+ },
904
904
  {
905
905
  "rendered": true,
906
906
  "initial": false,
@@ -1325,7 +1325,7 @@
1325
1325
  "auxiliaryFiles": [
1326
1326
  "main.js.map"
1327
1327
  ],
1328
- "hash": "638341df0713847a",
1328
+ "hash": "8e276b02ed1ca12e",
1329
1329
  "childrenByOrder": {}
1330
1330
  },
1331
1331
  {
@@ -1578,7 +1578,7 @@
1578
1578
  "auxiliaryFiles": [
1579
1579
  "openmrs-esm-implementer-tools-app.js.map"
1580
1580
  ],
1581
- "hash": "9586c029e3c34b3e",
1581
+ "hash": "bf3a56f61f240a22",
1582
1582
  "childrenByOrder": {}
1583
1583
  }
1584
1584
  ]
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","pages":[{"component":"implementerTools","route":true},{"component":"globalImplementerTools","route":true}],"extensions":[{"name":"implementer-tools-button","slot":"top-nav-actions-slot","component":"implementerToolsButton","order":20}],"version":"6.3.1-pre.3234"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","pages":[{"component":"implementerTools","route":true},{"component":"globalImplementerTools","route":true}],"extensions":[{"name":"implementer-tools-button","slot":"top-nav-actions-slot","component":"implementerToolsButton","order":20}],"version":"6.3.1-pre.3236"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-implementer-tools-app",
3
- "version": "6.3.1-pre.3234",
3
+ "version": "6.3.1-pre.3236",
4
4
  "license": "MPL-2.0",
5
5
  "description": "The admin interface for OpenMRS Frontend",
6
6
  "browser": "dist/openmrs-esm-implementer-tools-app.js",
@@ -17,7 +17,7 @@
17
17
  "analyze": "webpack --mode=production --env analyze=true",
18
18
  "typescript": "tsc",
19
19
  "lint": "eslint src --ext ts,tsx",
20
- "extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.button.tsx' 'src/**/*.extension.tsx' 'src/**/*.modal.tsx' --config='../../../tools/i18next-parser.config.js'"
20
+ "extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.button.tsx' 'src/**/*.extension.tsx' 'src/**/*.modal.tsx' 'src/**/*.resource.ts' --config='../../../tools/i18next-parser.config.js'"
21
21
  },
22
22
  "keywords": [
23
23
  "openmrs",
@@ -51,8 +51,8 @@
51
51
  "swr": "2.x"
52
52
  },
53
53
  "devDependencies": {
54
- "@openmrs/esm-framework": "6.3.1-pre.3234",
55
- "@openmrs/webpack-config": "6.3.1-pre.3234",
54
+ "@openmrs/esm-framework": "6.3.1-pre.3236",
55
+ "@openmrs/webpack-config": "6.3.1-pre.3236",
56
56
  "ace-builds": "^1.4.14",
57
57
  "react": "^18.1.0",
58
58
  "react-ace": "^9.5.0",
@@ -89,7 +89,7 @@ export default function EditableValue({ path, element, customType }: EditableVal
89
89
  customType={customType}
90
90
  path={path}
91
91
  handleClose={closeEditor}
92
- handleSave={(val) => {
92
+ handleSaveToConfiguration={(val) => {
93
93
  try {
94
94
  const result = JSON.parse(val);
95
95
  const tempConfigUpdate = set(cloneDeep(temporaryConfigStore.getState()), ['config', ...path], result);
@@ -0,0 +1,147 @@
1
+ import { type TOptions } from 'i18next';
2
+ import { Type, type ConfigValue, type ConfigSchema, type Validator, translateFrom } from '@openmrs/esm-framework';
3
+ import type { CustomValueType } from './value-editor';
4
+
5
+ const moduleName = '@openmrs/esm-implementer-tools-app';
6
+ const t = (key: string, fallback?: string, options?: Omit<TOptions, 'ns' | 'defaultValue'>) =>
7
+ translateFrom(moduleName, key, fallback, options);
8
+
9
+ const validateString = (value: ConfigValue, validators: Array<Validator>) => {
10
+ if (typeof value !== 'string') {
11
+ return t('stringValidationMessage', 'Value must be a string');
12
+ }
13
+ for (const validator of validators) {
14
+ const error = validator(value);
15
+ if (error) return error;
16
+ }
17
+ return null;
18
+ };
19
+
20
+ const validateNumber = (value: ConfigValue, validators: Array<Validator>) => {
21
+ if (typeof value !== 'number') {
22
+ return t('numberValidationMessage', 'Value must be a number');
23
+ }
24
+ for (const validator of validators) {
25
+ const error = validator(value);
26
+ if (error) return error;
27
+ }
28
+ return null;
29
+ };
30
+
31
+ const validateBoolean = (value: ConfigValue, validators: Array<Validator>) => {
32
+ if (typeof value !== 'boolean') {
33
+ return t('booleanValidationMessage', 'Value must be a boolean');
34
+ }
35
+ for (const validator of validators) {
36
+ const error = validator(value);
37
+ if (error) return error;
38
+ }
39
+ return null;
40
+ };
41
+
42
+ const validateArray = (value: ConfigValue, validators: Array<Validator>, elementSchema?: ConfigSchema) => {
43
+ if (!Array.isArray(value)) {
44
+ return t('arrayValidationMessage', 'Value must be an array');
45
+ }
46
+ for (const validator of validators) {
47
+ const error = validator(value);
48
+ if (error) return error;
49
+ }
50
+ if (elementSchema) {
51
+ for (let i = 0; i < value.length; i++) {
52
+ const element = value[i];
53
+ if (elementSchema._type === Type.Object) {
54
+ const error = validateObject(element, [], elementSchema);
55
+ if (error) return error;
56
+ } else {
57
+ const elementType = elementSchema._type;
58
+ const elementValidators = elementSchema._validators ?? [];
59
+ const error = validateValue(element, elementType, elementValidators, elementSchema._elements);
60
+ if (error) return error;
61
+ }
62
+ }
63
+ }
64
+ return null;
65
+ };
66
+
67
+ const validateObject = (value: ConfigValue, validators: Array<Validator>, elementSchema?: ConfigSchema) => {
68
+ if (!elementSchema || typeof value !== 'object' || value === null || Array.isArray(value)) {
69
+ return t('objectValidationMessage', 'Value must be an object');
70
+ }
71
+ for (const validator of validators) {
72
+ const error = validator(value);
73
+ if (error) return error;
74
+ }
75
+ for (const key of Object.keys(elementSchema)) {
76
+ if (key.startsWith('_')) continue;
77
+ const propSchema = elementSchema[key];
78
+ const propValue = value[key];
79
+ if (typeof propSchema === 'object' && propSchema !== null && '_type' in propSchema) {
80
+ if (propSchema._type === Type.Array) {
81
+ const error = validateArray(propValue, propSchema._validators ?? [], propSchema._elements);
82
+ if (error)
83
+ return t('objectPropertyValidationMessage', 'Property {{key}}: {{error}}', {
84
+ key,
85
+ error,
86
+ });
87
+ } else {
88
+ const propType = propSchema._type;
89
+ const propValidators = propSchema._validators ?? [];
90
+ const error = validateValue(propValue, propType, propValidators, propSchema._elements);
91
+ if (error)
92
+ return t('objectPropertyValidationMessage', 'Property {{key}}: {{error}}', {
93
+ key,
94
+ error,
95
+ });
96
+ }
97
+ } else if (typeof propSchema === 'object' && propSchema !== null) {
98
+ const error = validateObject(propValue, [], propSchema as ConfigSchema);
99
+ if (error)
100
+ return t('objectPropertyValidationMessage', 'Property {{key}}: {{error}}', {
101
+ key,
102
+ error,
103
+ });
104
+ }
105
+ }
106
+ return null;
107
+ };
108
+
109
+ const validateUuid = (value: ConfigValue, validators: Array<Validator>) => {
110
+ if (
111
+ typeof value !== 'string' ||
112
+ !/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|[0-9A-Za-z]{11,36})$/i.test(
113
+ value,
114
+ )
115
+ ) {
116
+ return t('uuidValidationMessage', 'Value must be a valid UUID string');
117
+ }
118
+ for (const validator of validators) {
119
+ const error = validator(value);
120
+ if (error) return error;
121
+ }
122
+ return null;
123
+ };
124
+
125
+ export const validateValue = (
126
+ value: ConfigValue,
127
+ fieldType: Type | CustomValueType | undefined,
128
+ validators: Array<Validator>,
129
+ elementSchema?: ConfigSchema,
130
+ ) => {
131
+ switch (fieldType) {
132
+ case Type.String:
133
+ return validateString(value, validators);
134
+ case Type.Number:
135
+ return validateNumber(value, validators);
136
+ case Type.Boolean:
137
+ return validateBoolean(value, validators);
138
+ case Type.Array:
139
+ return validateArray(value, validators, elementSchema);
140
+ case Type.Object:
141
+ return validateObject(value, validators, elementSchema);
142
+ case Type.UUID || Type.ConceptUuid || Type.PersonAttributeTypeUuid || Type.PatientIdentifierTypeUuid:
143
+ return validateUuid(value, validators);
144
+ default:
145
+ return null;
146
+ }
147
+ };
@@ -1,5 +1,12 @@
1
1
  @use '@carbon/layout';
2
2
  @use '@carbon/styles/scss/components/button';
3
+ @use '@carbon/styles/scss/theme';
4
+
5
+ .errorMessage {
6
+ color: theme.$text-error;
7
+ font-size: layout.$spacing-04;
8
+ margin-top: layout.$spacing-02;
9
+ }
3
10
 
4
11
  .valueEditorButtons {
5
12
  margin-top: layout.$spacing-03;
@@ -1,10 +1,10 @@
1
- import React, { useEffect, useState, useRef } from 'react';
2
- import { useTranslation } from 'react-i18next';
1
+ import React, { useEffect, useState, useRef, useCallback } from 'react';
3
2
  import { Button } from '@carbon/react';
4
- import { CloseIcon, SaveIcon, type Type } from '@openmrs/esm-framework';
3
+ import { CloseIcon, type ConfigSchema, type Config, getCoreTranslation, SaveIcon, Type } from '@openmrs/esm-framework';
5
4
  import type { ConfigValueDescriptor } from './editable-value.component';
6
5
  import { ValueEditorField } from './value-editors/value-editor-field';
7
- import styles from './value-editor.scss';
6
+ import styles from './value-editor.styles.scss';
7
+ import { validateValue } from './validators.resource';
8
8
 
9
9
  export type CustomValueType = 'add' | 'remove' | 'order' | 'configure';
10
10
 
@@ -14,23 +14,41 @@ interface ValueEditorProps {
14
14
  element: ConfigValueDescriptor;
15
15
  customType?: CustomValueType;
16
16
  path: Array<string>;
17
- handleSave: (val: string) => void;
17
+ handleSaveToConfiguration: (val: string) => void;
18
18
  handleClose: () => void;
19
19
  }
20
20
 
21
- export function ValueEditor({ element, customType, path, handleSave, handleClose }: ValueEditorProps) {
21
+ export function ValueEditor({ element, customType, path, handleSaveToConfiguration, handleClose }: ValueEditorProps) {
22
22
  const ref = useRef<HTMLDivElement>(null);
23
23
  const [tmpValue, setTmpValue] = useState(element._value);
24
- const { t } = useTranslation();
24
+ const [error, setError] = useState<string | null>(null);
25
25
 
26
26
  const valueType = customType ?? element._type;
27
+ const validators = element._validators ?? [];
28
+
29
+ let elementSchema: ConfigValueDescriptor | Config | undefined = undefined;
30
+ if (valueType === Type.Object) {
31
+ elementSchema = element;
32
+ } else if (valueType === Type.Array) {
33
+ elementSchema = element._elements;
34
+ }
35
+
36
+ const handleSave = useCallback(() => {
37
+ const errorMessage = validateValue(tmpValue, valueType, validators, elementSchema as ConfigSchema);
38
+ if (errorMessage) {
39
+ setError(errorMessage);
40
+ } else {
41
+ setError(null);
42
+ handleSaveToConfiguration(JSON.stringify(tmpValue));
43
+ }
44
+ }, [tmpValue, valueType, validators, elementSchema, handleSaveToConfiguration]);
27
45
 
28
46
  useEffect(() => {
29
47
  const keyListener = (e: KeyboardEvent) => {
30
48
  if (e.key === 'Escape') {
31
49
  handleClose();
32
50
  } else if (e.key === 'Enter') {
33
- handleSave(JSON.stringify(tmpValue));
51
+ handleSave();
34
52
  }
35
53
  };
36
54
 
@@ -38,21 +56,31 @@ export function ValueEditor({ element, customType, path, handleSave, handleClose
38
56
  return () => {
39
57
  document.removeEventListener('keyup', keyListener);
40
58
  };
41
- }, [handleSave, handleClose, tmpValue]);
59
+ }, [handleSave, handleClose]);
42
60
 
43
61
  return (
44
- <div ref={ref} style={{ display: 'inherit' }}>
45
- <ValueEditorField element={element} path={path} value={tmpValue} onChange={setTmpValue} valueType={valueType} />
62
+ <div ref={ref}>
63
+ <ValueEditorField
64
+ element={element}
65
+ path={path}
66
+ value={tmpValue}
67
+ onChange={setTmpValue}
68
+ valueType={valueType}
69
+ error={error}
70
+ />
71
+ <div className={styles.errorMessage}>
72
+ {valueType !== Type.Number &&
73
+ valueType !== Type.String &&
74
+ valueType !== Type.UUID &&
75
+ valueType !== Type.Boolean &&
76
+ error}
77
+ </div>
46
78
  <div className={styles.valueEditorButtons}>
47
- <Button
48
- renderIcon={(props) => <SaveIcon {...props} size={16} />}
49
- kind="primary"
50
- onClick={() => handleSave(JSON.stringify(tmpValue))}
51
- >
52
- {t('saveValueButtonText', 'Save')}
79
+ <Button renderIcon={(props) => <SaveIcon {...props} size={16} />} kind="primary" onClick={handleSave}>
80
+ {getCoreTranslation('save')}
53
81
  </Button>
54
82
  <Button renderIcon={(props) => <CloseIcon {...props} size={16} />} kind="secondary" onClick={handleClose}>
55
- {t('cancelButtonText', 'Cancel')}
83
+ {getCoreTranslation('cancel')}
56
84
  </Button>
57
85
  </div>
58
86
  </div>
@@ -19,9 +19,10 @@ export interface ValueEditorFieldProps {
19
19
  valueType?: ValueType;
20
20
  value: any;
21
21
  onChange: (value: any) => void;
22
+ error?: string | null;
22
23
  }
23
24
 
24
- export function ValueEditorField({ element, path, valueType, value, onChange }: ValueEditorFieldProps) {
25
+ export function ValueEditorField({ element, path, valueType, value, onChange, error }: ValueEditorFieldProps) {
25
26
  const [id] = useState(uniqueId('value-editor-'));
26
27
 
27
28
  if (valueType === 'remove' && !path) {
@@ -31,7 +32,15 @@ export function ValueEditorField({ element, path, valueType, value, onChange }:
31
32
  return valueType === Type.Array ? (
32
33
  <ArrayEditor element={element} valueArray={value} setValue={onChange} />
33
34
  ) : valueType === Type.Boolean ? (
34
- <Checkbox id={id} checked={value} hideLabel labelText="" onChange={(event, { checked, id }) => onChange(checked)} />
35
+ <Checkbox
36
+ id={id}
37
+ checked={value}
38
+ hideLabel
39
+ labelText=""
40
+ onChange={(event, { checked, id }) => onChange(checked)}
41
+ invalid={Boolean(error)}
42
+ invalidText={error}
43
+ />
35
44
  ) : valueType === Type.ConceptUuid ? (
36
45
  <ConceptSearchBox value={value} setConcept={(concept) => onChange(concept.uuid)} />
37
46
  ) : valueType === Type.PersonAttributeTypeUuid ? (
@@ -47,9 +56,18 @@ export function ValueEditorField({ element, path, valueType, value, onChange }:
47
56
  value={value}
48
57
  onChange={(_, { value }) => onChange(value ? (typeof value === 'string' ? parseInt(value) : value) : 0)}
49
58
  hideSteppers
59
+ invalid={Boolean(error)}
60
+ invalidText={error}
50
61
  />
51
62
  ) : valueType === Type.String || valueType === Type.UUID ? (
52
- <TextInput id={id} value={value} labelText="" onChange={(e) => onChange(e.target.value)} />
63
+ <TextInput
64
+ id={id}
65
+ value={value}
66
+ labelText=""
67
+ onChange={(e) => onChange(e.target.value)}
68
+ invalid={Boolean(error)}
69
+ invalidText={error}
70
+ />
53
71
  ) : valueType === 'add' ? (
54
72
  <ExtensionSlotAdd value={value ?? element._value} setValue={onChange} />
55
73
  ) : valueType === 'remove' && path ? (
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "activeItemSourceText": "The current value comes from ",
3
+ "arrayValidationMessage": "Value must be an array",
3
4
  "backendModules": "Backend modules",
5
+ "booleanValidationMessage": "Value must be a boolean",
4
6
  "checkImplementerToolsMessage": "Check the Backend Modules tab in the Implementer Tools for more details",
5
7
  "clearConfig": "Clear local config",
6
8
  "close": "Close",
@@ -21,12 +23,17 @@
21
23
  "missing": "Missing",
22
24
  "moduleName": "Module name",
23
25
  "modulesWithMissingDependenciesWarning": "Some modules have unresolved backend dependencies",
26
+ "numberValidationMessage": "Value must be a number",
27
+ "objectPropertyValidationMessage": "Property {{key}}: {{error}}",
28
+ "objectValidationMessage": "Value must be an object",
24
29
  "requiredVersion": "Required Version",
25
30
  "resetToDefaultValueButtonText": "Reset to default",
31
+ "stringValidationMessage": "Value must be a string",
26
32
  "toggleImplementerTools": "Toggle Implementer Tools",
27
33
  "uiEditor": "UI editor",
28
34
  "unknownVersion": "unknown",
29
35
  "updateConfig": "Update config",
36
+ "uuidValidationMessage": "Value must be a valid UUID string",
30
37
  "value": "Value",
31
38
  "viewModules": "View modules"
32
39
  }