From fd9342886618a926f54a2cb2f940ec6c2abff93e Mon Sep 17 00:00:00 2001 From: Lucas Koehler Date: Tue, 12 Jul 2022 07:50:45 +0000 Subject: [PATCH] Provide additional tester context core: * Change the third parameter of Tester and RankedTester from just the root schema to a new TesterContext object. It contains the root schema and the global config. * Remove UI Schema generation from mapStateToJsonFormsRendererProps. Instead, it is taken from the state. react: * Rename ctxToJsonFormsDispatchProps to ctxToJsonFormsRendererProps * Add HOC withJsonFormsRendererProps that injects state props vue: * Upgrade @vue/test-utils to the latest stable version * Add config as an optional renderer prop Fixes #1970 --- MIGRATION.md | 18 ++++-- .../src/controls/number.renderer.ts | 6 +- .../test/date-control.spec.ts | 3 +- .../test/table-control.spec.ts | 9 +-- packages/angular-material/test/util.ts | 28 +++++++++ packages/angular/src/jsonforms.component.ts | 10 ++- packages/core/src/testers/testers.ts | 56 ++++++++++------- packages/core/src/util/renderer.ts | 29 +++------ packages/core/test/testers.test.ts | 63 ++++++++++++------- packages/core/test/util/renderer.test.ts | 9 ++- .../renderers/MaterialArrayLayout.test.tsx | 8 +-- packages/material/test/renderers/util.ts | 5 ++ packages/react/src/DispatchCell.tsx | 16 ++++- packages/react/src/JsonForms.tsx | 51 ++++++--------- packages/react/src/JsonFormsContext.tsx | 14 ++++- .../react/test/renderers/JsonForms.test.tsx | 2 + .../vanilla/src/controls/InputControl.tsx | 8 ++- packages/vue/vue-vanilla/package-lock.json | 6 +- packages/vue/vue-vanilla/package.json | 2 +- packages/vue/vue/package-lock.json | 6 +- packages/vue/vue/package.json | 2 +- .../vue/vue/src/components/DispatchCell.vue | 5 +- .../vue/src/components/DispatchRenderer.vue | 5 +- packages/vue/vue/src/jsonFormsCompositions.ts | 6 ++ 24 files changed, 227 insertions(+), 140 deletions(-) create mode 100644 packages/angular-material/test/util.ts diff --git a/MIGRATION.md b/MIGRATION.md index bfad9d875..042aed95c 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -16,15 +16,21 @@ Therefore JSON Forms was not able to properly run the testers on schemas contain The workaround for this was to resolve the JSON Schema by hand before handing it over to JSON Forms. Only the React renderers did this automatically but we removed this functionality, see the next section for more information. -We now added an additional parameter to the testers, the `rootSchema`. +We now added an additional parameter to the testers, the new `TesterContext`. ```ts -type Tester = (uischema: UISchemaElement, schema: JsonSchema, rootSchema: JsonSchema) => boolean; -type RankedTester = (uischema: UISchemaElement, schema: JsonSchema, rootSchema: JsonSchema) => number; +interface TesterContext { + rootSchema: JsonSchema; + config: any; +} + +type Tester = (uischema: UISchemaElement, schema: JsonSchema, context: TesterContext) => boolean; +type RankedTester = (uischema: UISchemaElement, schema: JsonSchema, context: TesterContext) => number; ``` -This allows the testers to resolve any `$ref` they might encounter in their handed over `schema`. +This allows the testers to resolve any `$ref` they might encounter in their handed over `schema` by using the context's `rootSchema`. Therefore the manual resolving of JSON Schemas before handing them over to JSON Forms does not need to be performed in those cases. +In addition, testers can now access the global `config` to consider default UI Schema options. ### Removal of JSON Schema $Ref Parser @@ -114,6 +120,10 @@ The utility function `fromScopable` was renamed to `fromScoped` accordingly. Date Picker in Angular Material will use the global configuration of your Angular Material application. +### React prop mapping functions + +Renamed `ctxToJsonFormsDispatchProps` to `ctxToJsonFormsRendererProps` in order to better reflect the function's purpose. + ## Migrating to JSON Forms 2.5 ### JsonForms Component for Angular diff --git a/packages/angular-material/src/controls/number.renderer.ts b/packages/angular-material/src/controls/number.renderer.ts index f5c88a05f..471493c88 100644 --- a/packages/angular-material/src/controls/number.renderer.ts +++ b/packages/angular-material/src/controls/number.renderer.ts @@ -127,7 +127,11 @@ export class NumberControlRenderer extends JsonFormsControl { mapAdditionalProps(props:StatePropsOfControl) { if (this.scopedSchema) { - const defaultStep = isNumberControl(this.uischema, this.rootSchema, this.rootSchema) + const testerContext = { + rootSchema: this.rootSchema, + config: props.config + } + const defaultStep = isNumberControl(this.uischema, this.rootSchema, testerContext) ? 0.1 : 1; this.min = this.scopedSchema.minimum; diff --git a/packages/angular-material/test/date-control.spec.ts b/packages/angular-material/test/date-control.spec.ts index ed274b2aa..006b4ba55 100644 --- a/packages/angular-material/test/date-control.spec.ts +++ b/packages/angular-material/test/date-control.spec.ts @@ -41,6 +41,7 @@ import { Actions, ControlElement, JsonSchema } from '@jsonforms/core'; import { DateControlRenderer, DateControlRendererTester } from '../src'; import { FlexLayoutModule } from '@angular/flex-layout'; import { JsonFormsAngularService } from '@jsonforms/angular'; +import { createTesterContext } from './util'; const data = { foo: '2018-01-01' }; const schema: JsonSchema = { @@ -59,7 +60,7 @@ const uischema: ControlElement = { describe('Material boolean field tester', () => { it('should succeed', () => { - expect(DateControlRendererTester(uischema, schema, schema)).toBe(2); + expect(DateControlRendererTester(uischema, schema, createTesterContext(schema))).toBe(2); }); }); const imports = [ diff --git a/packages/angular-material/test/table-control.spec.ts b/packages/angular-material/test/table-control.spec.ts index 5bf7563e2..a6e2ceda2 100644 --- a/packages/angular-material/test/table-control.spec.ts +++ b/packages/angular-material/test/table-control.spec.ts @@ -40,6 +40,7 @@ import { } from '../src/other/table.renderer'; import { FlexLayoutModule } from '@angular/flex-layout'; import { setupMockStore } from '@jsonforms/angular-test'; +import { createTesterContext } from './util'; const uischema1: ControlElement = { type: 'Control', scope: '#' }; const uischema2: ControlElement = { @@ -95,10 +96,10 @@ const renderers = [ describe('Table tester', () => { it('should succeed', () => { - expect(TableRendererTester(uischema1, schema_object1, schema_object1)).toBe(3); - expect(TableRendererTester(uischema1, schema_simple1, schema_simple1)).toBe(3); - expect(TableRendererTester(uischema2, schema_object2, schema_object2)).toBe(3); - expect(TableRendererTester(uischema2, schema_simple2, schema_simple2)).toBe(3); + expect(TableRendererTester(uischema1, schema_object1, createTesterContext(schema_object1))).toBe(3); + expect(TableRendererTester(uischema1, schema_simple1, createTesterContext(schema_simple1))).toBe(3); + expect(TableRendererTester(uischema2, schema_object2, createTesterContext(schema_object2))).toBe(3); + expect(TableRendererTester(uischema2, schema_simple2, createTesterContext(schema_simple2))).toBe(3); }); }); describe('Table', () => { diff --git a/packages/angular-material/test/util.ts b/packages/angular-material/test/util.ts new file mode 100644 index 000000000..f8aae2282 --- /dev/null +++ b/packages/angular-material/test/util.ts @@ -0,0 +1,28 @@ +/* + The MIT License + + Copyright (c) 2022 EclipseSource + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +import { JsonSchema, TesterContext } from '@jsonforms/core'; + +export const createTesterContext = + (rootSchema: JsonSchema, config?: any): TesterContext => ({ rootSchema, config }); diff --git a/packages/angular/src/jsonforms.component.ts b/packages/angular/src/jsonforms.component.ts index 1848c97d4..1f3cf0ac0 100644 --- a/packages/angular/src/jsonforms.component.ts +++ b/packages/angular/src/jsonforms.component.ts @@ -35,6 +35,7 @@ import { import { createId, isControl, + getConfig, JsonFormsProps, JsonFormsState, JsonSchema, @@ -104,11 +105,14 @@ export class JsonFormsOutlet extends JsonFormsBaseRenderer const { renderers } = props as JsonFormsProps; const schema: JsonSchema = this.schema || props.schema; const uischema = this.uischema || props.uischema; - const rootSchema = props.rootSchema; + const testerContext = { + rootSchema: props.rootSchema, + config: getConfig(state) + }; - const renderer = maxBy(renderers, r => r.tester(uischema, schema, rootSchema)); + const renderer = maxBy(renderers, r => r.tester(uischema, schema, testerContext)); let bestComponent: Type = UnknownRenderer; - if (renderer !== undefined && renderer.tester(uischema, schema, rootSchema) !== -1) { + if (renderer !== undefined && renderer.tester(uischema, schema, testerContext) !== -1) { bestComponent = renderer.renderer; } diff --git a/packages/core/src/testers/testers.ts b/packages/core/src/testers/testers.ts index 70a89feea..49bd86e79 100644 --- a/packages/core/src/testers/testers.ts +++ b/packages/core/src/testers/testers.ts @@ -49,7 +49,7 @@ export const NOT_APPLICABLE = -1; * A tester is a function that receives an UI schema and a JSON schema and returns a boolean. * The rootSchema is handed over as context. Can be used to resolve references. */ -export type Tester = (uischema: UISchemaElement, schema: JsonSchema, rootSchema: JsonSchema) => boolean; +export type Tester = (uischema: UISchemaElement, schema: JsonSchema, context: TesterContext) => boolean; /** * A ranked tester associates a tester with a number. @@ -57,9 +57,19 @@ export type Tester = (uischema: UISchemaElement, schema: JsonSchema, rootSchema: export type RankedTester = ( uischema: UISchemaElement, schema: JsonSchema, - rootSchema: JsonSchema + context: TesterContext ) => number; +/** + * Additional context given to a tester in addition to UISchema and JsonSchema. + */ +export interface TesterContext { + /** The root JsonSchema of the form. Can be used to resolve references. */ + rootSchema: JsonSchema; + /** The global configuration object given to JsonForms. Can be used to derive default UISchema options. */ + config: any; +} + export const isControl = (uischema: any): uischema is ControlElement => !isEmpty(uischema) && uischema.scope !== undefined; @@ -75,7 +85,7 @@ export const isControl = (uischema: any): uischema is ControlElement => */ export const schemaMatches = ( predicate: (schema: JsonSchema, rootSchema: JsonSchema) => boolean -): Tester => (uischema: UISchemaElement, schema: JsonSchema, rootSchema: JsonSchema): boolean => { +): Tester => (uischema: UISchemaElement, schema: JsonSchema, context: TesterContext): boolean => { if (isEmpty(uischema) || !isControl(uischema)) { return false; } @@ -88,26 +98,26 @@ export const schemaMatches = ( } let currentDataSchema = schema; if (hasType(schema, 'object')) { - currentDataSchema = resolveSchema(schema, schemaPath, rootSchema); + currentDataSchema = resolveSchema(schema, schemaPath, context?.rootSchema); } if (currentDataSchema === undefined) { return false; } - return predicate(currentDataSchema, rootSchema); + return predicate(currentDataSchema, context?.rootSchema); }; export const schemaSubPathMatches = ( subPath: string, predicate: (schema: JsonSchema, rootSchema: JsonSchema) => boolean -): Tester => (uischema: UISchemaElement, schema: JsonSchema, rootSchema: JsonSchema): boolean => { +): Tester => (uischema: UISchemaElement, schema: JsonSchema, context: TesterContext): boolean => { if (isEmpty(uischema) || !isControl(uischema)) { return false; } const schemaPath = uischema.scope; let currentDataSchema: JsonSchema = schema; if (hasType(schema, 'object')) { - currentDataSchema = resolveSchema(schema, schemaPath, rootSchema); + currentDataSchema = resolveSchema(schema, schemaPath, context?.rootSchema); } currentDataSchema = get(currentDataSchema, subPath); @@ -115,7 +125,7 @@ export const schemaSubPathMatches = ( return false; } - return predicate(currentDataSchema, rootSchema); + return predicate(currentDataSchema, context?.rootSchema); }; /** @@ -218,8 +228,8 @@ export const scopeEndIs = (expected: string): Tester => ( export const and = (...testers: Tester[]): Tester => ( uischema: UISchemaElement, schema: JsonSchema, - rootSchema: JsonSchema -) => testers.reduce((acc, tester) => acc && tester(uischema, schema, rootSchema), true); + context: TesterContext +) => testers.reduce((acc, tester) => acc && tester(uischema, schema, context), true); /** * A tester that allow composing other testers by || them. @@ -229,8 +239,8 @@ export const and = (...testers: Tester[]): Tester => ( export const or = (...testers: Tester[]): Tester => ( uischema: UISchemaElement, schema: JsonSchema, - rootSchema: JsonSchema -) => testers.reduce((acc, tester) => acc || tester(uischema, schema, rootSchema), false); + context: TesterContext +) => testers.reduce((acc, tester) => acc || tester(uischema, schema, context), false); /** * Create a ranked tester that will associate a number with a given tester, if the * latter returns true. @@ -241,9 +251,9 @@ export const or = (...testers: Tester[]): Tester => ( export const rankWith = (rank: number, tester: Tester) => ( uischema: UISchemaElement, schema: JsonSchema, - rootSchema: JsonSchema + context: TesterContext ): number => { - if (tester(uischema, schema, rootSchema)) { + if (tester(uischema, schema, context)) { return rank; } @@ -253,9 +263,9 @@ export const rankWith = (rank: number, tester: Tester) => ( export const withIncreasedRank = (by: number, rankedTester: RankedTester) => ( uischema: UISchemaElement, schema: JsonSchema, - rootSchema: JsonSchema + context: TesterContext ): number => { - const rank = rankedTester(uischema, schema, rootSchema); + const rank = rankedTester(uischema, schema, context); if (rank === NOT_APPLICABLE) { return NOT_APPLICABLE; } @@ -438,13 +448,13 @@ const traverse = ( export const isObjectArrayWithNesting = ( uischema: UISchemaElement, schema: JsonSchema, - rootSchema: JsonSchema + context: TesterContext ): boolean => { - if (!uiTypeIs('Control')(uischema, schema, rootSchema)) { + if (!uiTypeIs('Control')(uischema, schema, context)) { return false; } const schemaPath = (uischema as ControlElement).scope; - const resolvedSchema = resolveSchema(schema, schemaPath, rootSchema ?? schema); + const resolvedSchema = resolveSchema(schema, schemaPath, context?.rootSchema ?? schema); let objectDepth = 0; if (resolvedSchema !== undefined && resolvedSchema.items !== undefined) { // check if nested arrays @@ -466,7 +476,7 @@ export const isObjectArrayWithNesting = ( return true; } return false; - }, rootSchema) + }, context?.rootSchema) ) { return true; } @@ -532,7 +542,7 @@ export const isRangeControl = and( /** * Tests whether the given UI schema is of type Control, if the schema - * is of type string and has option format + * is of type integer and has option format * @type {Tester} */ export const isNumberFormatControl = and( @@ -566,6 +576,6 @@ export const categorizationHasCategory = (uischema: UISchemaElement) => export const not = (tester: Tester): Tester => ( uischema: UISchemaElement, schema: JsonSchema, - rootSchema: JsonSchema + context: TesterContext -) => !tester(uischema, schema, rootSchema); +) => !tester(uischema, schema, context); diff --git a/packages/core/src/util/renderer.ts b/packages/core/src/util/renderer.ts index 001f83c69..2b8afaa6c 100644 --- a/packages/core/src/util/renderer.ts +++ b/packages/core/src/util/renderer.ts @@ -32,7 +32,6 @@ import { JsonFormsRendererRegistryEntry, } from '../reducers'; import { - findUISchema, getAjv, getCells, getConfig, @@ -889,6 +888,7 @@ export interface OwnPropsOfJsonFormsRenderer extends OwnPropsOfRenderer {} export interface StatePropsOfJsonFormsRenderer extends OwnPropsOfJsonFormsRenderer { rootSchema: JsonSchema; + config: any; } export interface JsonFormsProps extends StatePropsOfJsonFormsRenderer {} @@ -897,30 +897,15 @@ export const mapStateToJsonFormsRendererProps = ( state: JsonFormsState, ownProps: OwnPropsOfJsonFormsRenderer ): StatePropsOfJsonFormsRenderer => { - let uischema = ownProps.uischema; - if (uischema === undefined) { - if (ownProps.schema) { - uischema = findUISchema( - state.jsonforms.uischemas, - ownProps.schema, - undefined, - ownProps.path, - undefined, - undefined, - state.jsonforms.core.schema - ); - } else { - uischema = getUiSchema(state); - } - } - return { - renderers: ownProps.renderers || get(state.jsonforms, 'renderers') || [], - cells: ownProps.cells || get(state.jsonforms, 'cells') || [], + renderers: ownProps.renderers || get(state.jsonforms, 'renderers'), + cells: ownProps.cells || get(state.jsonforms, 'cells'), schema: ownProps.schema || getSchema(state), rootSchema: getSchema(state), - uischema: uischema, - path: ownProps.path + uischema: ownProps.uischema || getUiSchema(state), + path: ownProps.path, + enabled: ownProps.enabled, + config: getConfig(state) }; }; diff --git a/packages/core/test/testers.test.ts b/packages/core/test/testers.test.ts index 07b2dcae0..b73c31f15 100644 --- a/packages/core/test/testers.test.ts +++ b/packages/core/test/testers.test.ts @@ -46,7 +46,8 @@ import { scopeEndIs, scopeEndsWith, uiTypeIs, - isOneOfEnumControl + isOneOfEnumControl, + TesterContext } from '../src/testers'; import { ControlElement, @@ -57,6 +58,9 @@ import { const test = anyTest as TestInterface<{ uischema: ControlElement }>; +const createTesterContext = + (rootSchema: JsonSchema, config?: any): TesterContext => ({ rootSchema, config }); + test.beforeEach(t => { t.context.uischema = { type: 'Control', @@ -75,8 +79,9 @@ test('schemaTypeIs should check type sub-schema of control', t => { type: 'Control', scope: '#/properties/foo' }; - t.true(schemaTypeIs('string')(uischema, schema, schema)); - t.false(schemaTypeIs('integer')(uischema, schema, schema)); + const testerContext = createTesterContext(schema); + t.true(schemaTypeIs('string')(uischema, schema, testerContext)); + t.false(schemaTypeIs('integer')(uischema, schema, testerContext)); }); test('schemaTypeIs should return false for non-control UI schema elements', t => { @@ -90,7 +95,8 @@ test('schemaTypeIs should return false for non-control UI schema elements', t => type: 'Label', text: 'some text' }; - t.false(schemaTypeIs('integer')(label, schema, schema)); + const testerContext = createTesterContext(schema); + t.false(schemaTypeIs('integer')(label, schema, testerContext)); }); test('schemaTypeIs should return false for control pointing to invalid sub-schema', t => { @@ -104,7 +110,8 @@ test('schemaTypeIs should return false for control pointing to invalid sub-schem foo: { type: 'string' } } }; - t.false(schemaTypeIs('string')(uischema, schema, schema)); + const testerContext = createTesterContext(schema); + t.false(schemaTypeIs('string')(uischema, schema, testerContext)); }); test('schemaTypeIs should return true for array type', t => { @@ -118,8 +125,9 @@ test('schemaTypeIs should return true for array type', t => { type: 'Control', scope: '#/properties/foo' }; - t.true(schemaTypeIs('string')(uischema, schema, schema)); - t.true(schemaTypeIs('integer')(uischema, schema, schema)); + const testerContext = createTesterContext(schema); + t.true(schemaTypeIs('string')(uischema, schema, testerContext)); + t.true(schemaTypeIs('integer')(uischema, schema, testerContext)); }); test('formatIs should check the format of a resolved sub-schema', t => { @@ -136,7 +144,8 @@ test('formatIs should check the format of a resolved sub-schema', t => { } } }; - t.true(formatIs('date-time')(uischema, schema, schema)); + const testerContext = createTesterContext(schema); + t.true(formatIs('date-time')(uischema, schema, testerContext)); }); test('uiTypeIs', t => { @@ -183,8 +192,9 @@ test('schemaMatches should check type sub-schema of control via predicate', t => type: 'Control', scope: '#/properties/foo' }; + const testerContext = createTesterContext(schema); t.true( - schemaMatches(subSchema => subSchema.type === 'string')(uischema, schema, schema) + schemaMatches(subSchema => subSchema.type === 'string')(uischema, schema, testerContext) ); }); @@ -198,8 +208,9 @@ test('schemaMatches should check type sub-schema of control via predicate also w type: 'Control', scope: '#/properties/foo' }; + const testerContext = createTesterContext(schema); t.true( - schemaMatches(subSchema => subSchema.type === 'string')(uischema, schema, schema) + schemaMatches(subSchema => subSchema.type === 'string')(uischema, schema, testerContext) ); }); @@ -214,7 +225,8 @@ test('schemaMatches should return false for non-control UI schema elements', t = type: 'Label', text: 'some text' }; - t.false(schemaMatches(() => false)(label, schema, schema)); + const testerContext = createTesterContext(schema); + t.false(schemaMatches(() => false)(label, schema, testerContext)); }); test('schemaMatches should return false for control pointing to invalid subschema', t => { @@ -228,7 +240,8 @@ test('schemaMatches should return false for control pointing to invalid subschem type: 'Control', scope: '#/properties/bar' }; - t.false(schemaMatches(() => false)(uischema, schema, schema)); + const testerContext = createTesterContext(schema); + t.false(schemaMatches(() => false)(uischema, schema, testerContext)); }); test('scopeEndsWith checks whether the ref of a control ends with a certain string', t => { @@ -274,7 +287,8 @@ test('and should allow to compose multiple testers', t => { type: 'Control', scope: '#/properties/foo' }; - t.true(and(schemaTypeIs('string'), scopeEndIs('foo'))(uischema, schema, schema)); + const testerContext = createTesterContext(schema); + t.true(and(schemaTypeIs('string'), scopeEndIs('foo'))(uischema, schema, testerContext)); }); test('or should allow to compose multiple testers', t => { @@ -288,8 +302,9 @@ test('or should allow to compose multiple testers', t => { type: 'Control', scope: '#/properties/foo' }; + const testerContext = createTesterContext(schema); t.true( - or(schemaTypeIs('integer'), optionIs('slider', true))(uischema, schema, schema) + or(schemaTypeIs('integer'), optionIs('slider', true))(uischema, schema, testerContext) ); }); @@ -308,7 +323,7 @@ test('tester isPrimitiveArrayControl', t => { } }; t.true( - isPrimitiveArrayControl(control, schema, schema), + isPrimitiveArrayControl(control, schema, createTesterContext(schema)), `Primitive array tester was not triggered for 'integer' schema type` ); const schemaWithRefs = { @@ -322,7 +337,7 @@ test('tester isPrimitiveArrayControl', t => { } }; t.true( - isPrimitiveArrayControl(control, schemaWithRefs, schemaWithRefs), + isPrimitiveArrayControl(control, schemaWithRefs, createTesterContext(schemaWithRefs)), `Primitive array tester was not triggered for 'integer' schema type with refs` ); const objectSchema = { @@ -335,7 +350,7 @@ test('tester isPrimitiveArrayControl', t => { } }; t.false( - isPrimitiveArrayControl(control, objectSchema, objectSchema), + isPrimitiveArrayControl(control, objectSchema, createTesterContext(objectSchema)), `Primitive array tester was not triggered for 'object' schema type` ); }); @@ -401,7 +416,7 @@ test('tester isObjectArrayControl', t => { } } }; - t.true(isObjectArrayControl(control, schema, schema)); + t.true(isObjectArrayControl(control, schema, createTesterContext(schema))); const schema_noType: JsonSchema = { type: 'object', properties: { @@ -416,7 +431,7 @@ test('tester isObjectArrayControl', t => { } } }; - t.true(isObjectArrayControl(control, schema_noType, schema_noType)); + t.true(isObjectArrayControl(control, schema_noType, createTesterContext(schema_noType))); const schema_innerAllOf: JsonSchema = { type: 'object', properties: { @@ -439,7 +454,7 @@ test('tester isObjectArrayControl', t => { } } }; - t.true(isObjectArrayControl(control, schema_innerAllOf, schema_innerAllOf)); + t.true(isObjectArrayControl(control, schema_innerAllOf, createTesterContext(schema_innerAllOf))); const schemaWithRefs = { definitions: { @@ -461,7 +476,8 @@ test('tester isObjectArrayControl', t => { } } } - t.true(isObjectArrayControl(control, schemaWithRefs, schemaWithRefs)); + const testerContext = createTesterContext(schemaWithRefs); + t.true(isObjectArrayControl(control, schemaWithRefs, testerContext)); }); test('isBooleanControl', t => { @@ -801,7 +817,7 @@ test('tester isObjectArrayWithNesting', t => { t.true(isObjectArrayWithNesting(uischema, nestedSchema, undefined)); t.true(isObjectArrayWithNesting(uischema, nestedSchema2, undefined)); t.true(isObjectArrayWithNesting(uischema, nestedSchema3, undefined)); - t.true(isObjectArrayWithNesting(uischema, nestedSchemaWithRef, nestedSchemaWithRef)); + t.true(isObjectArrayWithNesting(uischema, nestedSchemaWithRef, createTesterContext(nestedSchemaWithRef))); t.false(isObjectArrayWithNesting(uischemaOptions.default, schema, undefined)); t.true(isObjectArrayWithNesting(uischemaOptions.generate, schema, undefined)); @@ -872,5 +888,6 @@ test('tester isOneOfEnumControl', t => { } } } - t.true(isOneOfEnumControl(control, schemaWithRefs, schemaWithRefs)); + const testerContext = createTesterContext(schemaWithRefs); + t.true(isOneOfEnumControl(control, schemaWithRefs, testerContext)); }); diff --git a/packages/core/test/util/renderer.test.ts b/packages/core/test/util/renderer.test.ts index df43fcacd..53043f5db 100644 --- a/packages/core/test/util/renderer.test.ts +++ b/packages/core/test/util/renderer.test.ts @@ -35,7 +35,6 @@ import { CoreActions, init, setValidationMode, update, UpdateAction, UPDATE_DATA import { ControlElement, LabelElement, RuleEffect, UISchemaElement } from '../../src/models/uischema'; import { computeLabel, createDefaultValue, mapDispatchToArrayControlProps, mapDispatchToControlProps, mapDispatchToMultiEnumProps, mapStateToAnyOfProps, mapStateToArrayLayoutProps, mapStateToControlProps, mapStateToEnumControlProps, mapStateToJsonFormsRendererProps, mapStateToLabelProps, mapStateToLayoutProps, mapStateToMultiEnumControlProps, mapStateToOneOfEnumControlProps, mapStateToOneOfProps, OwnPropsOfControl } from '../../src/util/renderer'; import { clearAllIds } from '../../src/util/ids'; -import { generateDefaultUISchema } from '../../src/generators/uischema'; import { JsonSchema } from '../../src/models/jsonSchema'; import { rankWith } from '../../src/testers/testers'; import { createAjv } from '../../src/util/validator'; @@ -495,7 +494,7 @@ test('createDefaultValue', t => { t.deepEqual(createDefaultValue({ type: 'something' }), {}); }); -test(`mapStateToDispatchRendererProps should generate UI schema given ownProps schema`, t => { +test(`mapStateToJsonFormsRendererProps should use registered UI schema given ownProps schema`, t => { const store = mockStore(createState(coreUISchema)); const schema = { type: 'object', @@ -507,16 +506,16 @@ test(`mapStateToDispatchRendererProps should generate UI schema given ownProps s }; const props = mapStateToJsonFormsRendererProps(store.getState(), { schema }); - t.deepEqual(props.uischema, generateDefaultUISchema(schema)); + t.deepEqual(props.uischema, coreUISchema); }); -test(`mapStateToDispatchRendererProps should use registered UI schema given no ownProps`, t => { +test(`mapStateToJsonFormsRendererProps should use registered UI schema given no ownProps`, t => { const store = mockStore(createState(coreUISchema)); const props = mapStateToJsonFormsRendererProps(store.getState(), {}); t.deepEqual(props.uischema, coreUISchema); }); -test(`mapStateToDispatchRendererProps should use UI schema if given via ownProps`, t => { +test(`mapStateToJsonFormsRendererProps should use UI schema if given via ownProps`, t => { const store = mockStore(createState(coreUISchema)); const schema = { type: 'object', diff --git a/packages/material/test/renderers/MaterialArrayLayout.test.tsx b/packages/material/test/renderers/MaterialArrayLayout.test.tsx index 362e1f2b7..8e9849b84 100644 --- a/packages/material/test/renderers/MaterialArrayLayout.test.tsx +++ b/packages/material/test/renderers/MaterialArrayLayout.test.tsx @@ -37,7 +37,7 @@ import Enzyme, { mount, ReactWrapper } from 'enzyme'; import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import { JsonForms, JsonFormsStateProvider } from '@jsonforms/react'; import { Accordion } from '@mui/material'; -import { initCore } from './util'; +import { createTesterContext, initCore } from './util'; Enzyme.configure({ adapter: new Adapter() }); @@ -181,9 +181,9 @@ describe('Material array layout tester', () => { expect(materialArrayLayoutTester(uischema, schema, undefined)).toBe(-1); expect(materialArrayLayoutTester(uischema, nestedSchema, undefined)).toBe(4); expect(materialArrayLayoutTester(uischema, nestedSchema2, undefined)).toBe(4); - expect(materialArrayLayoutTester(uischema, nestedSchemaWithRef, nestedSchemaWithRef)).toBe(4); - expect(materialArrayLayoutTester(uischema, nestedSchemaWithRef, nestedSchemaWithRef)).toBe(4); - expect(materialArrayLayoutTester(uischema, nestedSchema2WithRef, nestedSchema2WithRef)).toBe(4); + expect(materialArrayLayoutTester(uischema, nestedSchemaWithRef, createTesterContext(nestedSchemaWithRef))).toBe(4); + expect(materialArrayLayoutTester(uischema, nestedSchemaWithRef, createTesterContext(nestedSchemaWithRef))).toBe(4); + expect(materialArrayLayoutTester(uischema, nestedSchema2WithRef, createTesterContext(nestedSchema2WithRef))).toBe(4); expect(materialArrayLayoutTester(uischemaOptions.default, schema, undefined)).toBe(-1); expect(materialArrayLayoutTester(uischemaOptions.generate, schema, undefined)).toBe(4); diff --git a/packages/material/test/renderers/util.ts b/packages/material/test/renderers/util.ts index 18673c58d..75b6e29a0 100644 --- a/packages/material/test/renderers/util.ts +++ b/packages/material/test/renderers/util.ts @@ -26,6 +26,7 @@ import { createAjv, JsonSchema, + TesterContext, UISchemaElement } from '@jsonforms/core'; import { JsonFormsReactProps, useJsonForms } from '@jsonforms/react'; @@ -43,3 +44,7 @@ export const TestEmitter : React.FC = ({onChange}) => { }, [data, errors]); return null; }; + +export const createTesterContext = (rootSchema: JsonSchema, config?: any): TesterContext => { + return { rootSchema, config }; +}; \ No newline at end of file diff --git a/packages/react/src/DispatchCell.tsx b/packages/react/src/DispatchCell.tsx index a61a3c767..08352af9b 100644 --- a/packages/react/src/DispatchCell.tsx +++ b/packages/react/src/DispatchCell.tsx @@ -31,9 +31,19 @@ import { withJsonFormsDispatchCellProps } from './JsonFormsContext'; /** * Dispatch renderer component for cells. */ -export const Dispatch = ({uischema, schema, rootSchema, path, cells, id, enabled, renderers}:DispatchCellProps) => { - const cell = useMemo(() => maxBy(cells, r => r.tester(uischema, schema, rootSchema)), [cells, uischema, schema]); - if (cell === undefined || cell.tester(uischema, schema, rootSchema) === -1) { +export const Dispatch = ({uischema, schema, rootSchema, path, cells, id, enabled, renderers, config }: DispatchCellProps) => { + const testerContext = useMemo( + () => ({ + rootSchema: rootSchema, + config: config + }), + [rootSchema, config] + ); + const cell = useMemo( + () => maxBy(cells, r => r.tester(uischema, schema, testerContext)), + [cells, uischema, schema, testerContext] + ); + if (cell === undefined || cell.tester(uischema, schema, testerContext) === -1) { return ; } else { const Cell = cell.cell; diff --git a/packages/react/src/JsonForms.tsx b/packages/react/src/JsonForms.tsx index e9541af1f..095ca82c9 100644 --- a/packages/react/src/JsonForms.tsx +++ b/packages/react/src/JsonForms.tsx @@ -23,7 +23,7 @@ THE SOFTWARE. */ import maxBy from 'lodash/maxBy'; -import React, { useMemo } from 'react'; +import React, { ComponentType, useMemo } from 'react'; import Ajv, { ErrorObject } from 'ajv'; import { UnknownRenderer } from './UnknownRenderer'; import { @@ -44,7 +44,7 @@ import { } from '@jsonforms/core'; import { JsonFormsStateProvider, - useJsonForms + withJsonFormsRendererProps } from './JsonFormsContext'; interface JsonFormsRendererState { @@ -75,7 +75,7 @@ export class JsonFormsDispatchRenderer extends React.Component< } render() { - const { schema, rootSchema, uischema, path, enabled, renderers, cells } = this.props as JsonFormsProps; + const { schema, rootSchema, uischema, path, enabled, renderers, cells, config } = this.props as JsonFormsProps; return ( ); } @@ -102,14 +103,22 @@ const TestAndRender = React.memo( renderers: JsonFormsRendererRegistryEntry[]; cells: JsonFormsCellRendererRegistryEntry[]; id: string; + config: any; }) => { + const testerContext = useMemo( + () => ({ + rootSchema: props.rootSchema, + config: props.config + }), + [props.rootSchema, props.config] + ); const renderer = useMemo( - () => maxBy(props.renderers, r => r.tester(props.uischema, props.schema, props.rootSchema)), - [props.renderers, props.uischema, props.schema] + () => maxBy(props.renderers, r => r.tester(props.uischema, props.schema, testerContext)), + [props.renderers, props.uischema, props.schema, testerContext] ); if ( renderer === undefined || - renderer.tester(props.uischema, props.schema, props.rootSchema) === -1 + renderer.tester(props.uischema, props.schema, testerContext) === -1 ) { return ; } else { @@ -140,38 +149,16 @@ export class ResolvedJsonFormsDispatchRenderer extends JsonFormsDispatchRenderer } } -const useJsonFormsDispatchRendererProps = (props: OwnPropsOfJsonFormsRenderer & JsonFormsReactProps) => { - const ctx = useJsonForms(); - - return { - schema: props.schema || ctx.core.schema, - uischema: props.uischema || ctx.core.uischema, - path: props.path || '', - enabled: props.enabled, - rootSchema: ctx.core.schema, - renderers: props.renderers || ctx.renderers, - cells: props.cells || ctx.cells, - }; -} - -export const JsonFormsDispatch = React.memo( - (props: OwnPropsOfJsonFormsRenderer & JsonFormsReactProps) => { - const renderProps = useJsonFormsDispatchRendererProps(props); - return - } -); +export const JsonFormsDispatch: ComponentType = + withJsonFormsRendererProps(JsonFormsDispatchRenderer); /** * @deprecated Since Version 3.0 this optimization component is no longer necessary. * Use `JsonFormsDispatch` instead. * We still export it for backward compatibility */ -export const ResolvedJsonFormsDispatch = React.memo( - (props: OwnPropsOfJsonFormsRenderer & JsonFormsReactProps) => { - const renderProps = useJsonFormsDispatchRendererProps(props); - return - } -); +export const ResolvedJsonFormsDispatch: ComponentType = + withJsonFormsRendererProps(ResolvedJsonFormsDispatchRenderer); export interface JsonFormsInitStateProps { data: any; diff --git a/packages/react/src/JsonFormsContext.tsx b/packages/react/src/JsonFormsContext.tsx index 407040cff..6567b71a1 100644 --- a/packages/react/src/JsonFormsContext.tsx +++ b/packages/react/src/JsonFormsContext.tsx @@ -35,6 +35,7 @@ import { DispatchPropsOfControl, EnumCellProps, JsonFormsCore, + JsonFormsProps, JsonFormsSubStates, LayoutProps, OwnPropsOfCell, @@ -300,7 +301,7 @@ export const ctxToOneOfProps = ( }; }; -export const ctxToJsonFormsDispatchProps = ( +export const ctxToJsonFormsRendererProps = ( ctx: JsonFormsStateContext, ownProps: OwnPropsOfJsonFormsRenderer ) => mapStateToJsonFormsRendererProps({ jsonforms: { ...ctx } }, ownProps); @@ -381,6 +382,13 @@ export const withJsonFormsContext = return ; }; +export const withContextToJsonFormsRendererProps = + (Component: ComponentType): ComponentType => + ({ ctx, props }: JsonFormsStateContext & JsonFormsProps) => { + const contextProps = ctxToJsonFormsRendererProps(ctx, props) + return () + }; + const withContextToControlProps = (Component: ComponentType): ComponentType => ({ ctx, props }: JsonFormsStateContext & ControlProps) => { @@ -524,6 +532,10 @@ const withContextToLabelProps = // top level HOCs -- +export const withJsonFormsRendererProps = + (Component: ComponentType, memoize = true): ComponentType => + withJsonFormsContext(withContextToJsonFormsRendererProps(memoize ? React.memo(Component): Component)); + export const withJsonFormsControlProps = (Component: ComponentType, memoize = true): ComponentType => withJsonFormsContext(withContextToControlProps(memoize ? React.memo(Component) : Component)); diff --git a/packages/react/test/renderers/JsonForms.test.tsx b/packages/react/test/renderers/JsonForms.test.tsx index fb1335777..02117db30 100644 --- a/packages/react/test/renderers/JsonForms.test.tsx +++ b/packages/react/test/renderers/JsonForms.test.tsx @@ -304,6 +304,7 @@ test('render schema with $ref', () => { schema={schemaWithRef} renderers={renderers} rootSchema={schemaWithRef} + config={undefined} /> ); @@ -360,6 +361,7 @@ test('updates schema with ref', () => { schema={fixture.schema} renderers={renderers} rootSchema={resolvedSchema} + config={undefined} /> ); expect(wrapper.find(CustomRenderer1).length).toBe(1); diff --git a/packages/vanilla/src/controls/InputControl.tsx b/packages/vanilla/src/controls/InputControl.tsx index abf819c91..2cd4b8df0 100644 --- a/packages/vanilla/src/controls/InputControl.tsx +++ b/packages/vanilla/src/controls/InputControl.tsx @@ -78,10 +78,14 @@ export class InputControl extends Control< this.state.isFocused, appliedUiSchemaOptions.showUnfocusedDescription ); - const cell = maxBy(cells, r => r.tester(uischema, schema, rootSchema)); + const testerContext = { + rootSchema: rootSchema, + config: config + }; + const cell = maxBy(cells, r => r.tester(uischema, schema, testerContext)); if ( cell === undefined || - cell.tester(uischema, schema, rootSchema) === NOT_APPLICABLE + cell.tester(uischema, schema, testerContext) === NOT_APPLICABLE ) { console.warn('No applicable cell found.', uischema, schema); return null; diff --git a/packages/vue/vue-vanilla/package-lock.json b/packages/vue/vue-vanilla/package-lock.json index 4dcea4ba1..cfd0f86b9 100644 --- a/packages/vue/vue-vanilla/package-lock.json +++ b/packages/vue/vue-vanilla/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@vue/test-utils": { - "version": "2.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.0.0-rc.18.tgz", - "integrity": "sha512-aifolXjVdsogjaLmDoZ0FU8vN+R67aWmg9OuVeED4w5Ij5GFQLrlhM19uhWe/r5xXUL4fXMk3pX5wW6FJP1NcQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.0.2.tgz", + "integrity": "sha512-E2P4oXSaWDqTZNbmKZFVLrNN/siVN78YkEqs7pHryWerrlZR9bBFLWdJwRoguX45Ru6HxIflzKl4vQvwRMwm5g==", "dev": true }, "rollup-plugin-vue": { diff --git a/packages/vue/vue-vanilla/package.json b/packages/vue/vue-vanilla/package.json index 2295acad4..1bec9b635 100644 --- a/packages/vue/vue-vanilla/package.json +++ b/packages/vue/vue-vanilla/package.json @@ -65,7 +65,7 @@ "@vue/cli-plugin-unit-mocha": "~4.5.0", "@vue/cli-service": "~4.5.0", "@vue/compiler-sfc": "^3.2.26", - "@vue/test-utils": "^2.0.0-0", + "@vue/test-utils": "^2.0.2", "chai": "^4.1.2", "cross-env": "^7.0.2", "npm-run-all": "^4.1.5", diff --git a/packages/vue/vue/package-lock.json b/packages/vue/vue/package-lock.json index d6c07dc5f..3156f42eb 100644 --- a/packages/vue/vue/package-lock.json +++ b/packages/vue/vue/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@vue/test-utils": { - "version": "2.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.0.0-rc.18.tgz", - "integrity": "sha512-aifolXjVdsogjaLmDoZ0FU8vN+R67aWmg9OuVeED4w5Ij5GFQLrlhM19uhWe/r5xXUL4fXMk3pX5wW6FJP1NcQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.0.2.tgz", + "integrity": "sha512-E2P4oXSaWDqTZNbmKZFVLrNN/siVN78YkEqs7pHryWerrlZR9bBFLWdJwRoguX45Ru6HxIflzKl4vQvwRMwm5g==", "dev": true }, "core-js": { diff --git a/packages/vue/vue/package.json b/packages/vue/vue/package.json index c9035e02d..5be9dc164 100644 --- a/packages/vue/vue/package.json +++ b/packages/vue/vue/package.json @@ -56,7 +56,7 @@ "@vue/cli-plugin-unit-jest": "~4.5.0", "@vue/cli-service": "~4.5.0", "@vue/compiler-sfc": "^3.2.26", - "@vue/test-utils": "^2.0.0-0", + "@vue/test-utils": "^2.0.2", "core-js": "^3.9.1", "cross-env": "^7.0.2", "rimraf": "^3.0.2", diff --git a/packages/vue/vue/src/components/DispatchCell.vue b/packages/vue/vue/src/components/DispatchCell.vue index 1b562ab73..58c9b5b86 100644 --- a/packages/vue/vue/src/components/DispatchCell.vue +++ b/packages/vue/vue/src/components/DispatchCell.vue @@ -22,12 +22,13 @@ export default defineComponent({ }, computed: { determinedCell(): any { + const testerContext = { rootSchema: this.cell.rootSchema, config: this.config }; const cell = maxBy(this.cell.cells, r => - r.tester(this.cell.uischema, this.cell.schema, this.cell.rootSchema) + r.tester(this.cell.uischema, this.cell.schema, testerContext) ); if ( cell === undefined || - cell.tester(this.cell.uischema, this.cell.schema, this.cell.rootSchema) === -1 + cell.tester(this.cell.uischema, this.cell.schema, testerContext) === -1 ) { return UnknownRenderer; } else { diff --git a/packages/vue/vue/src/components/DispatchRenderer.vue b/packages/vue/vue/src/components/DispatchRenderer.vue index d3af2130d..511d22f45 100644 --- a/packages/vue/vue/src/components/DispatchRenderer.vue +++ b/packages/vue/vue/src/components/DispatchRenderer.vue @@ -18,12 +18,13 @@ export default defineComponent({ }, computed: { determinedRenderer(): any { + const testerContext = { rootSchema: this.rootSchema, config: this.config }; const renderer = maxBy(this.renderer.renderers, r => - r.tester(this.renderer.uischema, this.renderer.schema, this.rootSchema) + r.tester(this.renderer.uischema, this.renderer.schema, testerContext) ); if ( renderer === undefined || - renderer.tester(this.renderer.uischema, this.renderer.schema, this.rootSchema) === -1 + renderer.tester(this.renderer.uischema, this.renderer.schema, testerContext) === -1 ) { return UnknownRenderer; } else { diff --git a/packages/vue/vue/src/jsonFormsCompositions.ts b/packages/vue/vue/src/jsonFormsCompositions.ts index 0e58b8257..2bdb9d3a4 100644 --- a/packages/vue/vue/src/jsonFormsCompositions.ts +++ b/packages/vue/vue/src/jsonFormsCompositions.ts @@ -87,6 +87,11 @@ export const rendererProps = () => ({ ArrayConstructor >, default: undefined + }, + config: { + required: false, + type: Object, + default: undefined } }); @@ -136,6 +141,7 @@ export interface RendererProps { enabled?: boolean; renderers?: JsonFormsRendererRegistryEntry[]; cells?: JsonFormsCellRendererRegistryEntry[]; + config?: any; } export interface ControlProps extends RendererProps {