Skip to content

Commit fdd2d37

Browse files
committed
Fix "dot-separated" paths by making paths type string[] (instead of string) for react (in these packages: "core", "react" and "material-renderers")
eclipsesource#1849 eclipsesource#1831
1 parent 075bbc4 commit fdd2d37

27 files changed

+204
-189
lines changed

packages/core/src/actions/actions.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export type CoreActions =
7272

7373
export interface UpdateAction {
7474
type: 'jsonforms/UPDATE';
75-
path: string;
75+
path: string[];
7676
updater(existingData?: any): any;
7777
}
7878

@@ -167,7 +167,7 @@ export const setAjv = (ajv: AJV) => ({
167167
});
168168

169169
export const update = (
170-
path: string,
170+
path: string[],
171171
updater: (existingData: any) => any
172172
): UpdateAction => ({
173173
type: UPDATE_DATA,

packages/core/src/i18n/i18nUtil.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ export const getI18nKeyPrefixBySchema = (
1515
* Transforms a given path to a prefix which can be used for i18n keys.
1616
* Returns 'root' for empty paths and removes array indices
1717
*/
18-
export const transformPathToI18nPrefix = (path: string) => {
18+
export const transformPathToI18nPrefix = (path: string[]) => {
1919
return (
2020
path
21-
?.split('.')
2221
.filter(segment => !/^\d+$/.test(segment))
2322
.join('.') || 'root'
2423
);
@@ -27,7 +26,7 @@ export const transformPathToI18nPrefix = (path: string) => {
2726
export const getI18nKeyPrefix = (
2827
schema: i18nJsonSchema | undefined,
2928
uischema: UISchemaElement | undefined,
30-
path: string | undefined
29+
path: string[] | undefined
3130
): string | undefined => {
3231
return (
3332
getI18nKeyPrefixBySchema(schema, uischema) ??
@@ -38,7 +37,7 @@ export const getI18nKeyPrefix = (
3837
export const getI18nKey = (
3938
schema: i18nJsonSchema | undefined,
4039
uischema: UISchemaElement | undefined,
41-
path: string | undefined,
40+
path: string[] | undefined,
4241
key: string
4342
): string | undefined => {
4443
return `${getI18nKeyPrefix(schema, uischema, path)}.${key}`;
@@ -89,7 +88,7 @@ export const getCombinedErrorMessage = (
8988
t: Translator,
9089
schema?: i18nJsonSchema,
9190
uischema?: UISchemaElement,
92-
path?: string
91+
path?: string[],
9392
) => {
9493
if (errors.length > 0 && t) {
9594
// check whether there is a special message which overwrites all others

packages/core/src/reducers/core.ts

+28-27
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ import {
3939
SET_SCHEMA,
4040
SET_UISCHEMA,
4141
SET_VALIDATION_MODE,
42+
UPDATE_CORE,
4243
UPDATE_DATA,
4344
UPDATE_ERRORS,
44-
UPDATE_CORE,
4545
UpdateCoreAction
4646
} from '../actions';
47-
import { createAjv, Reducer } from '../util';
47+
import { pathsAreEqual, createAjv, pathStartsWith, Reducer } from '../util';
4848
import { JsonSchema, UISchemaElement } from '../models';
4949

5050
export const validate = (validator: ValidateFunction | undefined, data: any): ErrorObject[] => {
@@ -184,7 +184,7 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
184184
state.ajv !== thisAjv ||
185185
state.errors !== errors ||
186186
state.validator !== validator ||
187-
state.validationMode !== validationMode
187+
state.validationMode !== validationMode;
188188
return stateChanged
189189
? {
190190
...state,
@@ -230,7 +230,8 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
230230
case UPDATE_DATA: {
231231
if (action.path === undefined || action.path === null) {
232232
return state;
233-
} else if (action.path === '') {
233+
}
234+
if (action.path.length === 0) {
234235
// empty path is ok
235236
const result = action.updater(cloneDeep(state.data));
236237
const errors = validate(state.validator, result);
@@ -317,32 +318,29 @@ export const getControlPath = (error: ErrorObject) => {
317318
return dataPath.replace(/\//g, '.').substr(1);
318319
}
319320
// dataPath was renamed to instancePath in AJV v8
320-
var controlPath: string = error.instancePath;
321+
const controlPath = error.instancePath
322+
.replace(/^\//, '') // remove leading slash
323+
.split('/'); // convert to string[]
321324

322-
// change '/' chars to '.'
323-
controlPath = controlPath.replace(/\//g, '.');
324-
325325
const invalidProperty = getInvalidProperty(error);
326-
if (invalidProperty !== undefined && !controlPath.endsWith(invalidProperty)) {
327-
controlPath = `${controlPath}.${invalidProperty}`;
326+
if (invalidProperty !== undefined && controlPath.at(-1) !== invalidProperty) {
327+
controlPath.push(invalidProperty);
328328
}
329-
330-
// remove '.' chars at the beginning of paths
331-
controlPath = controlPath.replace(/^./, '');
329+
332330
return controlPath;
333-
}
331+
};
334332

335333
export const errorsAt = (
336-
instancePath: string,
334+
instancePath: string[],
337335
schema: JsonSchema,
338-
matchPath: (path: string) => boolean
336+
matchPath: (path: string[]) => boolean
339337
) => (errors: ErrorObject[]): ErrorObject[] => {
340338
// Get data paths of oneOf and anyOf errors to later determine whether an error occurred inside a subschema of oneOf or anyOf.
341339
const combinatorPaths = filter(
342340
errors,
343341
error => error.keyword === 'oneOf' || error.keyword === 'anyOf'
344342
).map(error => getControlPath(error));
345-
343+
346344
return filter(errors, error => {
347345
// Filter errors that match any keyword that we don't want to show in the UI
348346
if (filteredErrorKeywords.indexOf(error.keyword) !== -1) {
@@ -360,8 +358,8 @@ export const errorsAt = (
360358
// because the parent schema can never match the property schema (e.g. for 'required' checks).
361359
const parentSchema: JsonSchema | undefined = error.parentSchema;
362360
if (result && !isObjectSchema(parentSchema)
363-
&& combinatorPaths.findIndex(p => instancePath.startsWith(p)) !== -1) {
364-
result = result && isEqual(parentSchema, schema);
361+
&& combinatorPaths.some(combinatorPath => pathStartsWith(instancePath, combinatorPath))) {
362+
result = isEqual(parentSchema, schema);
365363
}
366364
return result;
367365
});
@@ -372,7 +370,7 @@ export const errorsAt = (
372370
*/
373371
const isObjectSchema = (schema?: JsonSchema): boolean => {
374372
return schema?.type === 'object' || !!schema?.properties;
375-
}
373+
};
376374

377375
/**
378376
* The error-type of an AJV error is defined by its `keyword` property.
@@ -388,13 +386,16 @@ const isObjectSchema = (schema?: JsonSchema): boolean => {
388386
const filteredErrorKeywords = ['additionalProperties', 'allOf', 'anyOf', 'oneOf'];
389387

390388
const getErrorsAt = (
391-
instancePath: string,
389+
instancePath: string[],
392390
schema: JsonSchema,
393-
matchPath: (path: string) => boolean
391+
matchPath: (path: string[]) => boolean
394392
) => (state: JsonFormsCore): ErrorObject[] =>
395-
errorsAt(instancePath, schema, matchPath)(state.validationMode === 'ValidateAndHide' ? [] : state.errors);
393+
errorsAt(instancePath, schema, matchPath)(
394+
state.validationMode === 'ValidateAndHide' ? [] : state.errors
395+
);
396+
397+
export const errorAt = (instancePath: string[], schema: JsonSchema) =>
398+
getErrorsAt(instancePath, schema, path => pathsAreEqual(path, instancePath));
396399

397-
export const errorAt = (instancePath: string, schema: JsonSchema) =>
398-
getErrorsAt(instancePath, schema, path => path === instancePath);
399-
export const subErrorsAt = (instancePath: string, schema: JsonSchema) =>
400-
getErrorsAt(instancePath, schema, path => path.startsWith(instancePath));
400+
export const subErrorsAt = (instancePath: string[], schema: JsonSchema) =>
401+
getErrorsAt(instancePath, schema, path => pathStartsWith(path, instancePath));

packages/core/src/reducers/reducers.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const findUISchema = (
7474
uischemas: JsonFormsUISchemaRegistryEntry[],
7575
schema: JsonSchema,
7676
schemaPath: string,
77-
path: string,
77+
path: string[],
7878
fallbackLayoutType = 'VerticalLayout',
7979
control?: ControlElement,
8080
rootSchema?: JsonSchema
@@ -104,13 +104,13 @@ export const findUISchema = (
104104
return uiSchema;
105105
};
106106

107-
export const getErrorAt = (instancePath: string, schema: JsonSchema) => (
107+
export const getErrorAt = (instancePath: string[], schema: JsonSchema) => (
108108
state: JsonFormsState
109109
) => {
110110
return errorAt(instancePath, schema)(state.jsonforms.core);
111111
};
112112

113-
export const getSubErrorsAt = (instancePath: string, schema: JsonSchema) => (
113+
export const getSubErrorsAt = (instancePath: string[], schema: JsonSchema) => (
114114
state: JsonFormsState
115115
) => subErrorsAt(instancePath, schema)(state.jsonforms.core);
116116

packages/core/src/reducers/uischemas.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { Reducer } from '../util';
3333
export type UISchemaTester = (
3434
schema: JsonSchema,
3535
schemaPath: string,
36-
path: string
36+
path: string[],
3737
) => number;
3838

3939
export interface JsonFormsUISchemaRegistryEntry {
@@ -64,7 +64,7 @@ export const findMatchingUISchema = (
6464
) => (
6565
jsonSchema: JsonSchema,
6666
schemaPath: string,
67-
path: string
67+
path: string[],
6868
): UISchemaElement => {
6969
const match = maxBy(state, entry =>
7070
entry.tester(jsonSchema, schemaPath, path)

packages/core/src/util/combinators.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const createCombinatorRenderInfos = (
7070
rootSchema: JsonSchema,
7171
keyword: CombinatorKeyword,
7272
control: ControlElement,
73-
path: string,
73+
path: string[],
7474
uischemas: JsonFormsUISchemaRegistryEntry[]
7575
): CombinatorSubSchemaRenderInfo[] =>
7676
combinatorSubSchemas.map((subSchema, subSchemaIndex) => ({

packages/core/src/util/path.ts

+27-22
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,8 @@ import isEmpty from 'lodash/isEmpty';
2727
import range from 'lodash/range';
2828
import { Scopable } from '../models';
2929

30-
export const compose = (path1: string, path2: string) => {
31-
let p1 = path1;
32-
if (!isEmpty(path1) && !isEmpty(path2) && !path2.startsWith('[')) {
33-
p1 = path1 + '.';
34-
}
35-
36-
if (isEmpty(p1)) {
37-
return path2;
38-
} else if (isEmpty(path2)) {
39-
return p1;
40-
} else {
41-
return `${p1}${path2}`;
42-
}
30+
export const compose = (path1: string[], path2: string[] | string) => {
31+
return path1?.concat(path2 ?? []);
4332
};
4433

4534
export { compose as composePaths };
@@ -66,25 +55,41 @@ export const toDataPathSegments = (schemaPath: string): string[] => {
6655
const startIndex = startFromRoot ? 2 : 1;
6756
return range(startIndex, segments.length, 2).map(idx => segments[idx]);
6857
};
69-
58+
// TODO: `toDataPathSegments` and `toDataPath` are the same!
7059
/**
7160
* Remove all schema-specific keywords (e.g. 'properties') from a given path.
7261
* @example
73-
* toDataPath('#/properties/foo/properties/bar') === '#/foo/bar')
62+
* toDataPath('#/properties/foo/properties/bar') === ['foo', 'bar'])
7463
*
7564
* @param {string} schemaPath the schema path to be converted
76-
* @returns {string} the path without schema-specific keywords
65+
* @returns {string[]} the path without schema-specific keywords
7766
*/
78-
export const toDataPath = (schemaPath: string): string => {
79-
return toDataPathSegments(schemaPath).join('.');
80-
};
67+
export const toDataPath = (schemaPath: string): string[] => toDataPathSegments(schemaPath);
8168

82-
export const composeWithUi = (scopableUi: Scopable, path: string): string => {
69+
export const composeWithUi = (scopableUi: Scopable, path: string[]): string[] => {
8370
const segments = toDataPathSegments(scopableUi.scope);
8471

8572
if (isEmpty(segments) && path === undefined) {
86-
return '';
73+
return [];
8774
}
8875

89-
return isEmpty(segments) ? path : compose(path, segments.join('.'));
76+
return isEmpty(segments) ? path : compose(path, segments);
9077
};
78+
79+
/**
80+
* Check if two paths are equal (section by section)
81+
*/
82+
export const pathsAreEqual = (path1: string[], path2: string[]) =>
83+
path2.length === path1.length && path2.every((section, i) => section === path1[i]);
84+
85+
/**
86+
* Check if a path starts with another path (`subPath`)
87+
*/
88+
export const pathStartsWith = (path: string[], subPath: string[]) =>
89+
subPath.length <= path.length && subPath.every((section, i) => section === path[i]);
90+
91+
/**
92+
* Convert path `array` to a `string`, injectively (in a reversible way)
93+
*/
94+
export const stringifyPath = (path: string[]) =>
95+
path.map(segment => encodeURIComponent(segment)).join('/');

0 commit comments

Comments
 (0)