From d1c3eabdc614b9b20a9139869a5a6c3f954effc6 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Sun, 23 Sep 2018 23:07:43 +0200 Subject: [PATCH 1/2] Improved React Material Object Renderer * added test cases * fixed rendering issues --- packages/core/src/generators/uischema.ts | 19 +- packages/core/src/models/uischema.ts | 30 +- packages/examples/src/index.ts | 2 + packages/examples/src/object.ts | 79 +++++ .../src/complex/MaterialObjectRenderer.tsx | 38 ++- .../renderers/MaterialObjectControl.test.tsx | 311 ++++++++++++++++++ 6 files changed, 443 insertions(+), 36 deletions(-) create mode 100644 packages/examples/src/object.ts create mode 100644 packages/material/test/renderers/MaterialObjectControl.test.tsx diff --git a/packages/core/src/generators/uischema.ts b/packages/core/src/generators/uischema.ts index 2a04e6b700..fa63a7726d 100644 --- a/packages/core/src/generators/uischema.ts +++ b/packages/core/src/generators/uischema.ts @@ -25,7 +25,7 @@ import * as _ from 'lodash'; import { JsonSchema } from '../models/jsonSchema'; -import { ControlElement, LabelElement, Layout, UISchemaElement } from '../models/uischema'; +import { ControlElement, isGroup, LabelElement, Layout, UISchemaElement } from '../models/uischema'; import { resolveSchema } from '../util/resolvers'; /** @@ -109,12 +109,17 @@ const wrapInLayoutIfNecessary = (uischema: UISchemaElement, layoutType: string): */ const addLabel = (layout: Layout, labelName: string) => { if (!_.isEmpty(labelName) ) { - // add label with name - const label: LabelElement = { - type: 'Label', - text: _.startCase(labelName) - }; - layout.elements.push(label); + const fixedLabel = _.startCase(labelName); + if (isGroup(layout)) { + layout.label = fixedLabel; + } else { + // add label with name + const label: LabelElement = { + type: 'Label', + text: fixedLabel + }; + layout.elements.push(label); + } } }; diff --git a/packages/core/src/models/uischema.ts b/packages/core/src/models/uischema.ts index 5c3b55dd4e..0451b9ca1d 100644 --- a/packages/core/src/models/uischema.ts +++ b/packages/core/src/models/uischema.ts @@ -22,7 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import {JsonSchema} from './jsonSchema'; +import { JsonSchema } from './jsonSchema'; /** * Interface for describing an UI schema element that is referencing @@ -105,20 +105,20 @@ export interface SchemaBasedCondition extends Condition { */ export interface UISchemaElement { - /** - * The type of this UI schema element. - */ - type: string; + /** + * The type of this UI schema element. + */ + type: string; - /** - * An optional rule. - */ - rule?: Rule; + /** + * An optional rule. + */ + rule?: Rule; - /** - * Any additional options. - */ - options?: any; + /** + * Any additional options. + */ + options?: any; } /** @@ -221,5 +221,7 @@ export interface Categorization extends UISchemaElement { * The child elements of this categorization which are either of type * {@link Category} or {@link Categorization}. */ - elements: (Category|Categorization)[]; + elements: (Category | Categorization)[]; } + +export const isGroup = (layout: Layout): layout is GroupLayout => layout.type === 'Group'; diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index 33ddfda100..de3e2e1f7d 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -49,6 +49,7 @@ import * as text from './text'; import * as numbers from './numbers'; import * as listWithDetail from './list-with-detail'; import * as listWithDetailRegistered from './list-with-detail-registered'; +import * as object from './object'; import * as i18n from './i18n'; import * as issue_1169 from './1169'; export * from './register'; @@ -81,6 +82,7 @@ export { numbers, listWithDetail, listWithDetailRegistered, + object, i18n, issue_1169 }; diff --git a/packages/examples/src/object.ts b/packages/examples/src/object.ts new file mode 100644 index 0000000000..d46730d980 --- /dev/null +++ b/packages/examples/src/object.ts @@ -0,0 +1,79 @@ +import { registerExamples } from './register'; + +export const schema = { + '$schema': 'http://json-schema.org/draft-07/schema#', + + 'type': 'object', + + 'properties': { + 'address': { + 'type': 'object', + 'properties': { + 'street_address': { 'type': 'string' }, + 'city': { 'type': 'string' }, + 'state': { 'type': 'string' } + }, + 'required': ['street_address', 'city', 'state'] + }, + 'user': { + 'type': 'object', + 'properties': { + 'name': { 'type': 'string' }, + 'mail': { 'type': 'string' }, + }, + 'required': ['name', 'mail'] + } + } +}; + +export const uischemaRoot = { + type: 'Control', + scope: '#' +}; + +export const uischemaNonRoot = { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/address' + }, + { + type: 'Control', + scope: '#/properties/user', + rule: { + effect: 'SHOW', + condition: { + type: 'LEAF' , + scope: '#/properties/address/properties/state', + expectedValue: 'DC' + } + } + } + ] +}; + +const data = { + 'address': { + 'street_address': '1600 Pennsylvania Avenue NW', + 'city': 'Washington', + 'state': 'DC', + } +}; + +registerExamples([ + { + name: 'rootObject', + label: 'Root Object', + data, + schema, + uischema: uischemaRoot + }, + { + name: 'object', + label: 'Object', + data, + schema, + uischema: uischemaNonRoot + }, +]); diff --git a/packages/material/src/complex/MaterialObjectRenderer.tsx b/packages/material/src/complex/MaterialObjectRenderer.tsx index 741c8b44f7..fcdda0ff14 100644 --- a/packages/material/src/complex/MaterialObjectRenderer.tsx +++ b/packages/material/src/complex/MaterialObjectRenderer.tsx @@ -1,14 +1,20 @@ -import * as React from 'react'; import { ControlProps, findUISchema, - isObjectControl, JsonFormsState, + GroupLayout, + isObjectControl, + JsonFormsState, JsonSchema, - mapStateToControlProps, OwnPropsOfControl, + mapStateToControlProps, + OwnPropsOfControl, RankedTester, - rankWith, UISchemaElement, + rankWith, + UISchemaElement } from '@jsonforms/core'; import { connectToJsonForms, JsonForms } from '@jsonforms/react'; +import { Hidden } from '@material-ui/core'; +import * as _ from 'lodash'; +import * as React from 'react'; interface MaterialObjectRendererProps extends ControlProps { findUiSchema( @@ -28,20 +34,22 @@ class MaterialObjectRenderer extends React.Component + + + ); } } diff --git a/packages/material/test/renderers/MaterialObjectControl.test.tsx b/packages/material/test/renderers/MaterialObjectControl.test.tsx new file mode 100644 index 0000000000..3c50d6ba7f --- /dev/null +++ b/packages/material/test/renderers/MaterialObjectControl.test.tsx @@ -0,0 +1,311 @@ +/* + The MIT License + + Copyright (c) 2018 EclipseSource Munich + 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 { Provider } from 'react-redux'; +import { + Actions, + ControlElement, + getData, + HorizontalLayout, + jsonformsReducer, + JsonFormsState, + JsonSchema, + NOT_APPLICABLE +} from '@jsonforms/core'; +import HorizontalLayoutRenderer from '../../src/layouts/MaterialHorizontalLayout'; +import MaterialObjectRenderer, { + materialObjectControlTester +} from '../../src/complex/MaterialObjectRenderer'; +import * as React from 'react'; +import * as TestUtils from 'react-dom/test-utils'; +import * as ReactDOM from 'react-dom'; +import { combineReducers, createStore, Store } from 'redux'; +import { materialFields, materialRenderers } from '../../src'; + +const initJsonFormsStore = (testData, testSchema, testUiSchema): Store => { + const store: Store = createStore( + combineReducers({ jsonforms: jsonformsReducer() }), + { + jsonforms: { + renderers: materialRenderers, + fields: materialFields, + } + } + ); + + store.dispatch(Actions.init(testData, testSchema, testUiSchema)); + return store; +}; + +const data = { foo: {foo_1: 'foo'}, bar: {bar_1: 'bar'} }; +const schema = { + type: 'object', + properties: { + foo: { + type: 'object', + properties: { + foo_1: {type: 'string'} + } + }, + bar: { + type: 'object', + properties: { + bar_1: {type: 'string'} + } + }, + }, +}; +const uischema1 = { + type: 'Control', + scope: '#', +}; +const uischema2 = { + type: 'Control', + scope: '#/properties/foo', +}; + +describe('Material object renderer tester', () => { + + test('should fail', () => { + expect(materialObjectControlTester(undefined, undefined)).toBe(NOT_APPLICABLE); + expect(materialObjectControlTester(null, undefined)).toBe(NOT_APPLICABLE); + expect(materialObjectControlTester({type: 'Foo'}, undefined)).toBe(NOT_APPLICABLE); + expect(materialObjectControlTester({type: 'Control'}, undefined)).toBe(NOT_APPLICABLE); + expect( + materialObjectControlTester( + uischema2, + { + type: 'object', + properties: { + foo: {type: 'string'}, + }, + }, + ) + ).toBe(NOT_APPLICABLE); + expect( + materialObjectControlTester( + uischema2, + { + type: 'object', + properties: { + foo: {type: 'string'}, + bar: schema.properties.bar, + }, + }, + ) + ).toBe(NOT_APPLICABLE); + }); + + it('should succeed', () => { + expect( + materialObjectControlTester( + uischema2, + { + type: 'object', + properties: { + foo: schema.properties.foo, + }, + }, + ) + ).toBe(2); + }); +}); + +describe('Material object control', () => { + + /** Use this container to render components */ + const container = document.createElement('div'); + + afterEach(() => { + ReactDOM.unmountComponentAtNode(container); + }); + + it('should autofocus first element', () => { + const firstControlElement: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + focus: true + } + }; + const secondControlElement: ControlElement = { + type: 'Control', + scope: '#/properties/bar', + options: { + focus: true + } + }; + const layout: HorizontalLayout = { + type: 'HorizontalLayout', + elements: [ + firstControlElement, + secondControlElement + ] + }; + const store = initJsonFormsStore( + data, + schema, + layout + ); + const tree = ReactDOM.render( + + + , + container + ); + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(tree, 'input'); + expect(document.activeElement).not.toBe(inputs[0]); + expect(document.activeElement).toBe(inputs[1]); + }); + + it('should autofocus via option', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + focus: true + } + }; + const store = initJsonFormsStore(data, schema, control); + const tree = ReactDOM.render( + + + , + container + ); + const input = TestUtils.findRenderedDOMComponentWithTag(tree, 'input') as HTMLInputElement; + expect(document.activeElement).toBe(input); + }); + + it('should not autofocus via option', () => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + options: { + focus: false + } + }; + const store = initJsonFormsStore(data, schema, control); + const tree = ReactDOM.render( + + + , + container + ); + const input = TestUtils.findRenderedDOMComponentWithTag(tree, 'input') as HTMLInputElement; + expect(input.autofocus).toBeFalsy(); + }); + + it('should not autofocus by default', () => { + const store = initJsonFormsStore(data, schema, uischema2); + const tree = ReactDOM.render( + + + , + container + ); + const input = TestUtils.findRenderedDOMComponentWithTag(tree, 'input') as HTMLInputElement; + expect(input.autofocus).toBeFalsy(); + }); + + it('should render all children', () => { + const store = initJsonFormsStore(data, schema, uischema1); + const tree = ReactDOM.render( + + + , + container + ); + + const input = TestUtils.scryRenderedDOMComponentsWithTag(tree, 'input') as HTMLInputElement[]; + expect(input.length).toBe(2); + expect(input[0].type).toBe('text'); + expect(input[0].value).toBe('foo'); + expect(input[1].type).toBe('text'); + expect(input[1].value).toBe('bar'); + }); + + it('should render only itself', () => { + const store = initJsonFormsStore(data, schema, uischema1); + const tree = ReactDOM.render( + + + , + container + ); + + const input = TestUtils.scryRenderedDOMComponentsWithTag(tree, 'input') as HTMLInputElement[]; + expect(input.length).toBe(1); + expect(input[0].type).toBe('text'); + expect(input[0].value).toBe('foo'); + }); + + it('can be disabled', () => { + const store = initJsonFormsStore(data, schema, uischema2); + const tree = ReactDOM.render( + + + , + container + ); + const input = TestUtils.findRenderedDOMComponentWithTag(tree, 'input') as HTMLInputElement; + expect(input.disabled).toBeTruthy(); + }); + + it('should be enabled by default', () => { + const store = initJsonFormsStore(data, schema, uischema2); + const tree = ReactDOM.render( + + + , + container + ); + const input = TestUtils.findRenderedDOMComponentWithTag(tree, 'input') as HTMLInputElement; + expect(input.disabled).toBeFalsy(); + }); + + it('can be invisible', () => { + const store = initJsonFormsStore(data, schema, uischema2); + const tree = ReactDOM.render( + + + , + container + ); + const input = TestUtils.scryRenderedDOMComponentsWithTag(tree, 'input') as HTMLInputElement[]; + expect(input.length).toBe(0); + }); + + it('should be visible by default', () => { + const store = initJsonFormsStore(data, schema, uischema2); + const tree = ReactDOM.render( + + + , + container + ); + const input = TestUtils.findRenderedDOMComponentWithTag(tree, 'input') as HTMLInputElement; + expect(input.hidden).toBeFalsy(); + }); +}); From dfa9a52854e4c8879736a89bb64f63c7ab802990 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Thu, 20 Dec 2018 16:02:54 +0100 Subject: [PATCH 2/2] Removed autofocus and disable tests --- .../renderers/MaterialObjectControl.test.tsx | 137 +++--------------- 1 file changed, 17 insertions(+), 120 deletions(-) diff --git a/packages/material/test/renderers/MaterialObjectControl.test.tsx b/packages/material/test/renderers/MaterialObjectControl.test.tsx index 3c50d6ba7f..0907dff343 100644 --- a/packages/material/test/renderers/MaterialObjectControl.test.tsx +++ b/packages/material/test/renderers/MaterialObjectControl.test.tsx @@ -22,26 +22,23 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { Provider } from 'react-redux'; import { Actions, ControlElement, - getData, HorizontalLayout, jsonformsReducer, JsonFormsState, - JsonSchema, NOT_APPLICABLE } from '@jsonforms/core'; -import HorizontalLayoutRenderer from '../../src/layouts/MaterialHorizontalLayout'; -import MaterialObjectRenderer, { - materialObjectControlTester -} from '../../src/complex/MaterialObjectRenderer'; import * as React from 'react'; -import * as TestUtils from 'react-dom/test-utils'; import * as ReactDOM from 'react-dom'; +import * as TestUtils from 'react-dom/test-utils'; +import { Provider } from 'react-redux'; import { combineReducers, createStore, Store } from 'redux'; import { materialFields, materialRenderers } from '../../src'; +import MaterialObjectRenderer, + { materialObjectControlTester } from '../../src/complex/MaterialObjectRenderer'; +import HorizontalLayoutRenderer from '../../src/layouts/MaterialHorizontalLayout'; const initJsonFormsStore = (testData, testSchema, testUiSchema): Store => { const store: Store = createStore( @@ -58,20 +55,20 @@ const initJsonFormsStore = (testData, testSchema, testUiSchema): Store { test('should fail', () => { expect(materialObjectControlTester(undefined, undefined)).toBe(NOT_APPLICABLE); expect(materialObjectControlTester(null, undefined)).toBe(NOT_APPLICABLE); - expect(materialObjectControlTester({type: 'Foo'}, undefined)).toBe(NOT_APPLICABLE); - expect(materialObjectControlTester({type: 'Control'}, undefined)).toBe(NOT_APPLICABLE); + expect(materialObjectControlTester({ type: 'Foo' }, undefined)).toBe(NOT_APPLICABLE); + expect(materialObjectControlTester({ type: 'Control' }, undefined)).toBe(NOT_APPLICABLE); expect( materialObjectControlTester( uischema2, { type: 'object', properties: { - foo: {type: 'string'}, + foo: { type: 'string' }, }, }, ) @@ -109,7 +106,7 @@ describe('Material object renderer tester', () => { { type: 'object', properties: { - foo: {type: 'string'}, + foo: { type: 'string' }, bar: schema.properties.bar, }, }, @@ -141,99 +138,11 @@ describe('Material object control', () => { ReactDOM.unmountComponentAtNode(container); }); - it('should autofocus first element', () => { - const firstControlElement: ControlElement = { - type: 'Control', - scope: '#/properties/foo', - options: { - focus: true - } - }; - const secondControlElement: ControlElement = { - type: 'Control', - scope: '#/properties/bar', - options: { - focus: true - } - }; - const layout: HorizontalLayout = { - type: 'HorizontalLayout', - elements: [ - firstControlElement, - secondControlElement - ] - }; - const store = initJsonFormsStore( - data, - schema, - layout - ); - const tree = ReactDOM.render( - - - , - container - ); - const inputs = TestUtils.scryRenderedDOMComponentsWithTag(tree, 'input'); - expect(document.activeElement).not.toBe(inputs[0]); - expect(document.activeElement).toBe(inputs[1]); - }); - - it('should autofocus via option', () => { - const control: ControlElement = { - type: 'Control', - scope: '#/properties/foo', - options: { - focus: true - } - }; - const store = initJsonFormsStore(data, schema, control); - const tree = ReactDOM.render( - - - , - container - ); - const input = TestUtils.findRenderedDOMComponentWithTag(tree, 'input') as HTMLInputElement; - expect(document.activeElement).toBe(input); - }); - - it('should not autofocus via option', () => { - const control: ControlElement = { - type: 'Control', - scope: '#/properties/foo', - options: { - focus: false - } - }; - const store = initJsonFormsStore(data, schema, control); - const tree = ReactDOM.render( - - - , - container - ); - const input = TestUtils.findRenderedDOMComponentWithTag(tree, 'input') as HTMLInputElement; - expect(input.autofocus).toBeFalsy(); - }); - - it('should not autofocus by default', () => { - const store = initJsonFormsStore(data, schema, uischema2); - const tree = ReactDOM.render( - - - , - container - ); - const input = TestUtils.findRenderedDOMComponentWithTag(tree, 'input') as HTMLInputElement; - expect(input.autofocus).toBeFalsy(); - }); - it('should render all children', () => { const store = initJsonFormsStore(data, schema, uischema1); const tree = ReactDOM.render( - + , container ); @@ -250,7 +159,7 @@ describe('Material object control', () => { const store = initJsonFormsStore(data, schema, uischema1); const tree = ReactDOM.render( - + , container ); @@ -261,23 +170,11 @@ describe('Material object control', () => { expect(input[0].value).toBe('foo'); }); - it('can be disabled', () => { - const store = initJsonFormsStore(data, schema, uischema2); - const tree = ReactDOM.render( - - - , - container - ); - const input = TestUtils.findRenderedDOMComponentWithTag(tree, 'input') as HTMLInputElement; - expect(input.disabled).toBeTruthy(); - }); - it('should be enabled by default', () => { const store = initJsonFormsStore(data, schema, uischema2); const tree = ReactDOM.render( - + , container ); @@ -289,7 +186,7 @@ describe('Material object control', () => { const store = initJsonFormsStore(data, schema, uischema2); const tree = ReactDOM.render( - + , container ); @@ -301,7 +198,7 @@ describe('Material object control', () => { const store = initJsonFormsStore(data, schema, uischema2); const tree = ReactDOM.render( - + , container );