Skip to content

Commit

Permalink
Fix "dot-separated" paths by making paths type string[] (instead of…
Browse files Browse the repository at this point in the history
… `string`) for react (in these packages: "core", "react" and "material-renderers")

#1849
#1831
  • Loading branch information
mirismaili committed Jan 1, 2022
1 parent 075bbc4 commit 58413e1
Show file tree
Hide file tree
Showing 28 changed files with 212 additions and 192 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export type CoreActions =

export interface UpdateAction {
type: 'jsonforms/UPDATE';
path: string;
path: string[];
updater(existingData?: any): any;
}

Expand Down Expand Up @@ -167,7 +167,7 @@ export const setAjv = (ajv: AJV) => ({
});

export const update = (
path: string,
path: string[],
updater: (existingData: any) => any
): UpdateAction => ({
type: UPDATE_DATA,
Expand Down
11 changes: 5 additions & 6 deletions packages/core/src/i18n/i18nUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@ export const getI18nKeyPrefixBySchema = (
* Transforms a given path to a prefix which can be used for i18n keys.
* Returns 'root' for empty paths and removes array indices
*/
export const transformPathToI18nPrefix = (path: string) => {
export const transformPathToI18nPrefix = (path: string[]) => {
return (
path
?.split('.')
.filter(segment => !/^\d+$/.test(segment))
?.filter(segment => !/^\d+$/.test(segment))
.join('.') || 'root'
);
};

export const getI18nKeyPrefix = (
schema: i18nJsonSchema | undefined,
uischema: UISchemaElement | undefined,
path: string | undefined
path: string[] | undefined
): string | undefined => {
return (
getI18nKeyPrefixBySchema(schema, uischema) ??
Expand All @@ -38,7 +37,7 @@ export const getI18nKeyPrefix = (
export const getI18nKey = (
schema: i18nJsonSchema | undefined,
uischema: UISchemaElement | undefined,
path: string | undefined,
path: string[] | undefined,
key: string
): string | undefined => {
return `${getI18nKeyPrefix(schema, uischema, path)}.${key}`;
Expand Down Expand Up @@ -89,7 +88,7 @@ export const getCombinedErrorMessage = (
t: Translator,
schema?: i18nJsonSchema,
uischema?: UISchemaElement,
path?: string
path?: string[],
) => {
if (errors.length > 0 && t) {
// check whether there is a special message which overwrites all others
Expand Down
55 changes: 28 additions & 27 deletions packages/core/src/reducers/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ import {
SET_SCHEMA,
SET_UISCHEMA,
SET_VALIDATION_MODE,
UPDATE_CORE,
UPDATE_DATA,
UPDATE_ERRORS,
UPDATE_CORE,
UpdateCoreAction
} from '../actions';
import { createAjv, Reducer } from '../util';
import { pathsAreEqual, createAjv, pathStartsWith, Reducer } from '../util';
import { JsonSchema, UISchemaElement } from '../models';

export const validate = (validator: ValidateFunction | undefined, data: any): ErrorObject[] => {
Expand Down Expand Up @@ -184,7 +184,7 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
state.ajv !== thisAjv ||
state.errors !== errors ||
state.validator !== validator ||
state.validationMode !== validationMode
state.validationMode !== validationMode;
return stateChanged
? {
...state,
Expand Down Expand Up @@ -230,7 +230,8 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
case UPDATE_DATA: {
if (action.path === undefined || action.path === null) {
return state;
} else if (action.path === '') {
}
if (action.path.length === 0) {
// empty path is ok
const result = action.updater(cloneDeep(state.data));
const errors = validate(state.validator, result);
Expand Down Expand Up @@ -317,32 +318,29 @@ export const getControlPath = (error: ErrorObject) => {
return dataPath.replace(/\//g, '.').substr(1);
}
// dataPath was renamed to instancePath in AJV v8
var controlPath: string = error.instancePath;
const controlPath = error.instancePath
.replace(/^\//, '') // remove leading slash
.split('/'); // convert to string[]

// change '/' chars to '.'
controlPath = controlPath.replace(/\//g, '.');

const invalidProperty = getInvalidProperty(error);
if (invalidProperty !== undefined && !controlPath.endsWith(invalidProperty)) {
controlPath = `${controlPath}.${invalidProperty}`;
if (invalidProperty !== undefined && controlPath.at(-1) !== invalidProperty) {
controlPath.push(invalidProperty);
}

// remove '.' chars at the beginning of paths
controlPath = controlPath.replace(/^./, '');

return controlPath;
}
};

export const errorsAt = (
instancePath: string,
instancePath: string[],
schema: JsonSchema,
matchPath: (path: string) => boolean
matchPath: (path: string[]) => boolean
) => (errors: ErrorObject[]): ErrorObject[] => {
// Get data paths of oneOf and anyOf errors to later determine whether an error occurred inside a subschema of oneOf or anyOf.
const combinatorPaths = filter(
errors,
error => error.keyword === 'oneOf' || error.keyword === 'anyOf'
).map(error => getControlPath(error));

return filter(errors, error => {
// Filter errors that match any keyword that we don't want to show in the UI
if (filteredErrorKeywords.indexOf(error.keyword) !== -1) {
Expand All @@ -360,8 +358,8 @@ export const errorsAt = (
// because the parent schema can never match the property schema (e.g. for 'required' checks).
const parentSchema: JsonSchema | undefined = error.parentSchema;
if (result && !isObjectSchema(parentSchema)
&& combinatorPaths.findIndex(p => instancePath.startsWith(p)) !== -1) {
result = result && isEqual(parentSchema, schema);
&& combinatorPaths.some(combinatorPath => pathStartsWith(instancePath, combinatorPath))) {
result = isEqual(parentSchema, schema);
}
return result;
});
Expand All @@ -372,7 +370,7 @@ export const errorsAt = (
*/
const isObjectSchema = (schema?: JsonSchema): boolean => {
return schema?.type === 'object' || !!schema?.properties;
}
};

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

const getErrorsAt = (
instancePath: string,
instancePath: string[],
schema: JsonSchema,
matchPath: (path: string) => boolean
matchPath: (path: string[]) => boolean
) => (state: JsonFormsCore): ErrorObject[] =>
errorsAt(instancePath, schema, matchPath)(state.validationMode === 'ValidateAndHide' ? [] : state.errors);
errorsAt(instancePath, schema, matchPath)(
state.validationMode === 'ValidateAndHide' ? [] : state.errors
);

export const errorAt = (instancePath: string[], schema: JsonSchema) =>
getErrorsAt(instancePath, schema, path => pathsAreEqual(path, instancePath));

export const errorAt = (instancePath: string, schema: JsonSchema) =>
getErrorsAt(instancePath, schema, path => path === instancePath);
export const subErrorsAt = (instancePath: string, schema: JsonSchema) =>
getErrorsAt(instancePath, schema, path => path.startsWith(instancePath));
export const subErrorsAt = (instancePath: string[], schema: JsonSchema) =>
getErrorsAt(instancePath, schema, path => pathStartsWith(path, instancePath));
6 changes: 3 additions & 3 deletions packages/core/src/reducers/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const findUISchema = (
uischemas: JsonFormsUISchemaRegistryEntry[],
schema: JsonSchema,
schemaPath: string,
path: string,
path: string[],
fallbackLayoutType = 'VerticalLayout',
control?: ControlElement,
rootSchema?: JsonSchema
Expand Down Expand Up @@ -104,13 +104,13 @@ export const findUISchema = (
return uiSchema;
};

export const getErrorAt = (instancePath: string, schema: JsonSchema) => (
export const getErrorAt = (instancePath: string[], schema: JsonSchema) => (
state: JsonFormsState
) => {
return errorAt(instancePath, schema)(state.jsonforms.core);
};

export const getSubErrorsAt = (instancePath: string, schema: JsonSchema) => (
export const getSubErrorsAt = (instancePath: string[], schema: JsonSchema) => (
state: JsonFormsState
) => subErrorsAt(instancePath, schema)(state.jsonforms.core);

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/reducers/uischemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { Reducer } from '../util';
export type UISchemaTester = (
schema: JsonSchema,
schemaPath: string,
path: string
path: string[],
) => number;

export interface JsonFormsUISchemaRegistryEntry {
Expand Down Expand Up @@ -64,7 +64,7 @@ export const findMatchingUISchema = (
) => (
jsonSchema: JsonSchema,
schemaPath: string,
path: string
path: string[],
): UISchemaElement => {
const match = maxBy(state, entry =>
entry.tester(jsonSchema, schemaPath, path)
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/util/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const createCombinatorRenderInfos = (
rootSchema: JsonSchema,
keyword: CombinatorKeyword,
control: ControlElement,
path: string,
path: string[],
uischemas: JsonFormsUISchemaRegistryEntry[]
): CombinatorSubSchemaRenderInfo[] =>
combinatorSubSchemas.map((subSchema, subSchemaIndex) => ({
Expand Down
55 changes: 33 additions & 22 deletions packages/core/src/util/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,8 @@ import isEmpty from 'lodash/isEmpty';
import range from 'lodash/range';
import { Scopable } from '../models';

export const compose = (path1: string, path2: string) => {
let p1 = path1;
if (!isEmpty(path1) && !isEmpty(path2) && !path2.startsWith('[')) {
p1 = path1 + '.';
}

if (isEmpty(p1)) {
return path2;
} else if (isEmpty(path2)) {
return p1;
} else {
return `${p1}${path2}`;
}
export const compose = (path1: string[], path2: string[] | string) => {
return path1?.concat(path2 ?? []);
};

export { compose as composePaths };
Expand All @@ -66,25 +55,47 @@ export const toDataPathSegments = (schemaPath: string): string[] => {
const startIndex = startFromRoot ? 2 : 1;
return range(startIndex, segments.length, 2).map(idx => segments[idx]);
};

// TODO: `toDataPathSegments` and `toDataPath` are the same!
/**
* Remove all schema-specific keywords (e.g. 'properties') from a given path.
* @example
* toDataPath('#/properties/foo/properties/bar') === '#/foo/bar')
* toDataPath('#/properties/foo/properties/bar') === ['foo', 'bar'])
*
* @param {string} schemaPath the schema path to be converted
* @returns {string} the path without schema-specific keywords
* @returns {string[]} the path without schema-specific keywords
*/
export const toDataPath = (schemaPath: string): string => {
return toDataPathSegments(schemaPath).join('.');
};
export const toDataPath = (schemaPath: string): string[] => toDataPathSegments(schemaPath);

export const composeWithUi = (scopableUi: Scopable, path: string): string => {
export const composeWithUi = (scopableUi: Scopable, path: string[]): string[] => {
const segments = toDataPathSegments(scopableUi.scope);

if (isEmpty(segments) && path === undefined) {
return '';
return [];
}

return isEmpty(segments) ? path : compose(path, segments.join('.'));
return isEmpty(segments) ? path : compose(path, segments);
};

/**
* Check if two paths are equal (section by section)
*/
export const pathsAreEqual = (path1: string[], path2: string[]) =>
path2.length === path1.length && path2.every((section, i) => section === path1[i]);

/**
* Check if a path starts with another path (`subPath`)
*/
export const pathStartsWith = (path: string[], subPath: string[]) =>
subPath.length <= path.length && subPath.every((section, i) => section === path[i]);

/**
* Convert path `array` to a `string`, injectively (in a reversible way)
*/
export const stringifyPath = (path: string[]) =>
path.map(segment => ajvInstancePathEncoder(segment)).join('/');

export const ajvInstancePathEncoder = (pathSegment: string) =>
pathSegment.replace(/[~\/]/g, match => match === '~' ? '~0' : '~1');

export const ajvInstancePathDecoder = (pathSegment: string) =>
pathSegment.replace(/~0|~1/g, match => match === '~0' ? '~' : '/');
Loading

0 comments on commit 58413e1

Please sign in to comment.