Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix "dot-separated" paths by making paths type string[] (instead of string) #1862

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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