Skip to content

Commit

Permalink
Support additionalProperties for 3 simple types: number, `integer…
Browse files Browse the repository at this point in the history
…`, `string`

eclipsesource#1765
  • Loading branch information
mirismaili committed Dec 23, 2021
1 parent 7e3a737 commit 984c369
Show file tree
Hide file tree
Showing 18 changed files with 888 additions and 57 deletions.
34 changes: 34 additions & 0 deletions packages/core/src/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const INIT: 'jsonforms/INIT' = 'jsonforms/INIT';
export const UPDATE_CORE: 'jsonforms/UPDATE_CORE' = `jsonforms/UPDATE_CORE`;
export const SET_AJV: 'jsonforms/SET_AJV' = 'jsonforms/SET_AJV';
export const UPDATE_DATA: 'jsonforms/UPDATE' = 'jsonforms/UPDATE';
export const ADD_NEW_PROPERTY: 'jsonforms/ADD_PROPERTY' = 'jsonforms/ADD_PROPERTY';
export const REMOVE_A_PROPERTY: 'jsonforms/REMOVE_PROPERTY' = 'jsonforms/REMOVE_PROPERTY';
export const UPDATE_ERRORS: 'jsonforms/UPDATE_ERRORS' =
'jsonforms/UPDATE_ERRORS';
export const VALIDATE: 'jsonforms/VALIDATE' = 'jsonforms/VALIDATE';
Expand Down Expand Up @@ -64,6 +66,8 @@ export type CoreActions =
| InitAction
| UpdateCoreAction
| UpdateAction
| AddPropertyAction
| RemovePropertyAction
| UpdateErrorsAction
| SetAjvAction
| SetSchemaAction
Expand All @@ -76,6 +80,18 @@ export interface UpdateAction {
updater(existingData?: any): any;
}

export interface AddPropertyAction {
type: 'jsonforms/ADD_PROPERTY';
path: string[];
key: string;
value: any;
}

export interface RemovePropertyAction {
type: 'jsonforms/REMOVE_PROPERTY';
path: string[];
}

export interface UpdateErrorsAction {
type: 'jsonforms/UPDATE_ERRORS';
errors: ErrorObject[];
Expand Down Expand Up @@ -175,6 +191,24 @@ export const update = (
updater
});

export const addNewProperty = (
path: string[],
key: string,
value: any,
): AddPropertyAction => ({
type: ADD_NEW_PROPERTY,
path,
key,
value,
});

export const removeThisProperty = (
path: string[],
): RemovePropertyAction => ({
type: REMOVE_A_PROPERTY,
path,
});

export const updateErrors = (errors: ErrorObject[]): UpdateErrorsAction => ({
type: UPDATE_ERRORS,
errors
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/generators/Generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export const Generate: {
jsonSchema: JsonSchema,
layoutType?: string,
prefix?: string,
rootSchema?: JsonSchema
rootSchema?: JsonSchema,
path?: string[],
): UISchemaElement;
controlElement(ref: string): ControlElement;
} = {
Expand Down
58 changes: 54 additions & 4 deletions packages/core/src/generators/uischema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,33 @@ import {
JsonSchema,
LabelElement,
Layout,
DynamicLayout,
UISchemaElement
} from '../models';
import { deriveTypes, resolveSchema } from '../util';

/**
* Creates a new ILayout.
* @param layoutType The type of the laoyut
* @param layoutType The type of the layout
* @returns the new ILayout
*/
const createLayout = (layoutType: string): Layout => ({
type: layoutType,
elements: []
});

/**
* Creates a new ILayout.
* @param layoutType The type of the layout
* @param ref
* @returns the new ILayout
*/
const createScopableLayout = (layoutType: string, ref: string): DynamicLayout => {
const layout = createLayout(layoutType) as DynamicLayout;
layout.scope = ref;
return layout;
};

/**
* Creates a IControlObject with the given label referencing the given ref
*/
Expand All @@ -55,6 +68,18 @@ export const createControlElement = (ref: string): ControlElement => ({
scope: ref
});

/**
* Creates a Dynamic-IControlObject with the given label referencing the given ref
*/
export const createDynamicControlElement = (
ref: string,
dataPath: string,
): ControlElement => {
const controlElement = createControlElement(ref);
controlElement.dataPath = dataPath;
return controlElement;
};

/**
* Wraps the given {@code uiSchema} in a Layout if there is none already.
* @param uischema The ui schema to wrap in a layout.
Expand Down Expand Up @@ -177,6 +202,29 @@ const generateUISchema = (
});
}

if (jsonSchema.additionalProperties) {
let value = jsonSchema.additionalProperties as JsonSchema;
const ref = `${currentRef}/additionalProperties`;
if (value.$ref) {
value = resolveSchema(rootSchema, value.$ref);
}

generateUISchema(
value,
layout.elements,
ref,
schemaName,
layoutType,
rootSchema
);
}
return layout;
}

if (currentRef.endsWith('additionalProperties')) {
const layout = createScopableLayout('DynamicGroup', currentRef);
addLabel(layout, schemaName);
schemaElements.push(layout);
return layout;
}

Expand Down Expand Up @@ -211,9 +259,11 @@ export const generateDefaultUISchema = (
jsonSchema: JsonSchema,
layoutType = 'VerticalLayout',
prefix = '#',
rootSchema = jsonSchema
rootSchema = jsonSchema,
path: string[] = [],
schemaName = path?.at(-1)
): UISchemaElement =>
wrapInLayoutIfNecessary(
generateUISchema(jsonSchema, [], prefix, '', layoutType, rootSchema),
layoutType
generateUISchema(jsonSchema, [], prefix, schemaName, layoutType, rootSchema),
layoutType,
);
2 changes: 1 addition & 1 deletion packages/core/src/i18n/i18nUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const getI18nKeyPrefixBySchema = (
export const transformPathToI18nPrefix = (path: string[]) => {
return (
path
.filter(segment => !/^\d+$/.test(segment))
?.filter(segment => !/^\d+$/.test(segment))
.join('.') || 'root'
);
};
Expand Down
35 changes: 30 additions & 5 deletions packages/core/src/models/uischema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ export interface UISchemaElement {
*/
rule?: Rule;

/**
* An optional dataPath (for dynamic-controls)
*/
dataPath?: string;

/**
* Any additional options.
*/
Expand All @@ -152,6 +157,12 @@ export interface Layout extends UISchemaElement {
elements: UISchemaElement[];
}

/**
* Represents a scopable-layout element which can order its
* children in a specific way.
*/
export interface DynamicLayout extends Layout, Scopable {}

/**
* A layout which orders its child elements vertically (i.e. from top to bottom).
*/
Expand All @@ -167,17 +178,31 @@ export interface HorizontalLayout extends Layout {
}

/**
* A group resembles a vertical layout, but additionally might have a label.
* This layout is useful when grouping different elements by a certain criteria.
* A group layout without specified type
*/
export interface GroupLayout extends Layout {
type: 'Group';
export interface AbstractGroupLayout {
/**
* The label of this group layout.
*/
label?: string;
}

/**
* A group resembles a vertical layout, but additionally might have a label.
* This layout is useful when grouping different elements by a certain criteria.
*/
export interface GroupLayout extends AbstractGroupLayout, Layout {
type: 'Group';
}

/**
* A group resembles a vertical layout, but additionally might have a label.
* This layout is useful when grouping different elements by a certain criteria.
*/
export interface DynamicGroupLayout extends AbstractGroupLayout, DynamicLayout {
type: 'DynamicGroup';
}

/**
* Represents an object that can be used to configure a label.
*/
Expand Down Expand Up @@ -245,7 +270,7 @@ export interface Categorization extends UISchemaElement {
}

export const isGroup = (layout: Layout): layout is GroupLayout =>
layout.type === 'Group';
layout.type === 'Group' || layout.type === 'DynamicGroup';

export const isLayout = (uischema: UISchemaElement): uischema is Layout =>
(uischema as Layout).elements !== undefined;
38 changes: 36 additions & 2 deletions packages/core/src/reducers/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,29 @@

import cloneDeep from 'lodash/cloneDeep';
import setFp from 'lodash/fp/set';
import unsetFp from 'lodash/fp/unset';
import get from 'lodash/get';
import filter from 'lodash/filter';
import isEqual from 'lodash/isEqual';
import isFunction from 'lodash/isFunction';
import Ajv, { ErrorObject, ValidateFunction } from 'ajv';
import {
ADD_NEW_PROPERTY,
CoreActions,
INIT,
InitAction,
InitActionOptions,
REMOVE_A_PROPERTY,
SET_AJV,
SET_SCHEMA,
SET_UISCHEMA,
SET_VALIDATION_MODE,
UPDATE_CORE,
UPDATE_DATA,
UPDATE_ERRORS,
UpdateCoreAction
UpdateCoreAction,
} from '../actions';
import { pathsAreEqual, createAjv, pathStartsWith, Reducer } from '../util';
import { composePaths, pathsAreEqual, createAjv, pathStartsWith, Reducer } from '../util';
import { JsonSchema, UISchemaElement } from '../models';

export const validate = (validator: ValidateFunction | undefined, data: any): ErrorObject[] => {
Expand Down Expand Up @@ -256,6 +259,37 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
};
}
}
case ADD_NEW_PROPERTY: {
if ([undefined, null].includes(action.path)) {
return state;
}
const newData: any = setFp(
composePaths(action.path, action.key),
action.value,
state.data === undefined ? {} : state.data,
);
const errors = validate(state.validator, newData);
return {
...state,
data: newData,
errors,
};
}
case REMOVE_A_PROPERTY: {
if ([undefined, null].includes(action.path)) {
return state;
}
const newData: any = unsetFp(
action.path,
state.data === undefined ? {} : state.data,
);
const errors = validate(state.validator, newData);
return {
...state,
data: newData,
errors,
};
}
case UPDATE_ERRORS: {
return {
...state,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/reducers/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const findUISchema = (
// default
const uiSchema = findMatchingUISchema(uischemas)(schema, schemaPath, path);
if (uiSchema === undefined) {
return Generate.uiSchema(schema, fallbackLayoutType, '#', rootSchema);
return Generate.uiSchema(schema, fallbackLayoutType, '#', rootSchema, path);
}
return uiSchema;
};
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/util/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const deriveLabel = (
if (schemaElement && typeof schemaElement.title === 'string') {
return schemaElement.title;
}
if (typeof controlElement.dataPath === 'string') {
return startCase(controlElement.dataPath);
}
if (typeof controlElement.scope === 'string') {
const ref = controlElement.scope;
const label = ref.substr(ref.lastIndexOf('/') + 1);
Expand Down
Loading

0 comments on commit 984c369

Please sign in to comment.