From b1c6a83e07a41cb41ee405ca8cf85a859d583d0d Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Mon, 12 Jul 2021 21:23:00 +0100 Subject: [PATCH 01/10] feat(concerto): Add Model Builder story Signed-off-by: Matt Roberts --- .../src/stories/3-ConcertoForm.stories.js | 223 +++++++++++++++++- .../src/components/concertoForm.css | 45 ++-- packages/ui-concerto/src/components/fields.js | 150 ++++++------ packages/ui-concerto/src/formgenerator.js | 2 +- packages/ui-concerto/src/index.js | 3 +- .../ui-concerto/src/modelBuilderVisitor.js | 80 +++++++ packages/ui-concerto/src/reactformvisitor.js | 14 +- packages/ui-concerto/src/utilities.js | 2 +- 8 files changed, 419 insertions(+), 100 deletions(-) create mode 100644 packages/ui-concerto/src/modelBuilderVisitor.js diff --git a/packages/storybook/src/stories/3-ConcertoForm.stories.js b/packages/storybook/src/stories/3-ConcertoForm.stories.js index 36431b32..3433c25e 100644 --- a/packages/storybook/src/stories/3-ConcertoForm.stories.js +++ b/packages/storybook/src/stories/3-ConcertoForm.stories.js @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { action } from '@storybook/addon-actions'; import { text, boolean, object } from '@storybook/addon-knobs'; -import { ConcertoForm } from '@accordproject/ui-concerto'; +import { ConcertoForm, ModelBuilderVisitor } from '@accordproject/ui-concerto'; export default { title: 'Concerto Form', @@ -57,8 +57,8 @@ export const Demo = () => { }; options.relationshipProvider = { - getOptions : (field) => { - if(field.getFullyQualifiedTypeName() === 'test.Person') { + getOptions: (field) => { + if (field.getFullyQualifiedTypeName() === 'test.Person') { return [{ key: '001', value: 'test.Person#Marissa', @@ -85,19 +85,228 @@ export const Demo = () => { value: 'test.Person#Rosalind', text: 'Rosalind Picard' } - ] + ] } else { return null; - }} + } + } + }; + + return ( +
+ +
+ ) +}; + + +export const ModelBuilder = () => { + const readOnly = boolean('Read-only', false); + const type = text('Type', 'concerto.metamodel.ModelFile'); + const options = object('Options', { + includeOptionalFields: false, + includeSampleData: 'sample', + updateExternalModels: false, + hiddenFields: [ + 'concerto.metamodel.Decorator' + ], + visitor: new ModelBuilderVisitor() + }); + const model = text('Model', ` /* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + namespace concerto.metamodel + + /** + * The metadmodel for Concerto files + */ + + abstract concept DecoratorLiteral { + } + + concept DecoratorString extends DecoratorLiteral { + o String value + } + + concept DecoratorNumber extends DecoratorLiteral { + o Double value + } + + concept DecoratorBoolean extends DecoratorLiteral { + o Boolean value + } + + concept TypeIdentifier { + o String fullyQualifiedName + } + + concept DecoratorIdentifier extends DecoratorLiteral { + o TypeIdentifier identifier + o Boolean isArray default=false + } + + concept Decorator { + o String name + o DecoratorLiteral[] arguments optional + } + + abstract concept ClassDeclaration { + @FormEditor("hide", true) + o Decorator[] decorators optional + o Boolean isAbstract default=false + @FormEditor("title", "name") + o String identifier + o String identifiedByField optional + @FormEditor("title", "parentType") + o TypeIdentifier superType optional + o BooleanFieldDeclaration[] fields + } + + concept AssetDeclaration extends ClassDeclaration { + } + + concept ParticipantDeclaration extends ClassDeclaration { + } + + concept TransactionDeclaration extends ClassDeclaration { + } + + concept EventDeclaration extends ClassDeclaration { + } + + concept ConceptDeclaration extends ClassDeclaration { + } + + // TODO - enums do not support abstract or super types + concept EnumDeclaration extends ClassDeclaration { + } + + concept StringDefault { + o String value + } + + concept BooleanDefault { + o Boolean value + } + + concept IntegerDefault { + o Integer value + } + + concept RealDefault { + o Double value + } + + abstract concept FieldDeclaration { + o String name + o Boolean isArray optional + o Boolean isOptional optional + @FormEditor("hide", true) + o Decorator[] decorators optional + } + + concept ObjectFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o StringDefault defaultValue optional + o TypeIdentifier type + } + + concept BooleanFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o BooleanDefault defaultValue optional + } + + concept DateTimeFieldDeclaration extends FieldDeclaration { + } + + concept StringFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o StringDefault defaultValue optional + @FormEditor("hide", true) + o StringRegexValidator validator optional + } + + concept StringRegexValidator { + o String regex + } + + concept RealDomainValidator { + o Double lower optional + o Double upper optional + } + + concept IntegerDomainValidator { + o Integer lower optional + o Integer upper optional + } + + concept RealFieldDeclaration extends FieldDeclaration { + o RealDefault defaultValue optional + o RealDomainValidator validator optional + } + + concept IntegerFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o IntegerDefault defaultValue optional + @FormEditor("hide", true) + o IntegerDomainValidator validator optional + } + + concept RelationshipDeclaration extends FieldDeclaration { + o TypeIdentifier type + } + + abstract concept Import { + o String uri optional + } + + concept NamespaceImport extends Import { + o String namespace + } + + concept TypeImport extends Import { + o TypeIdentifier identifier + } + + concept ModelFile { + @FormEditor("hide", true) + o String namespace + @FormEditor("hide", true) + o Import[] imports optional + @FormEditor("title", "class") + o ClassDeclaration[] declarations optional + } + `); + + const handleValueChange = (json) => { + return action("value changed")(json); }; - + return (
.field { + border-left: 1px solid rgba(34, 36, 38, 0.15); + padding: 5px 0px 5px 10px; } .grid { - display: grid; - grid-template-columns: auto 36px; - grid-column-gap: 5px; + display: grid; + grid-template-columns: auto 42px; + column-gap: 5px; } .fullHeight { - height:100% + height: 100%; } .monetaryAmount { - display: grid; - grid-template-columns: auto 80px; - grid-column-gap: 5px; + display: grid; + grid-template-columns: auto 80px; + column-gap: 5px; } .duration { - display: grid; - grid-template-columns: auto 100px; - grid-column-gap: 5px; + display: grid; + grid-template-columns: auto 100px; + column-gap: 5px; +} + +.fieldDeclaration { + display: grid; + grid-template-columns: auto 80px 80px; + column-gap: 5px; } .arrayButton { - box-shadow: none !important; + box-shadow: none !important; + background-color: rgb(240, 240, 240) !important; +} + +.ui.form .field > label { + margin-top: 2px; } diff --git a/packages/ui-concerto/src/components/fields.js b/packages/ui-concerto/src/components/fields.js index 506c9057..b4b660ea 100644 --- a/packages/ui-concerto/src/components/fields.js +++ b/packages/ui-concerto/src/components/fields.js @@ -18,6 +18,16 @@ import { Checkbox, Input, Form, Button, Select, Popup, Label, Icon } from 'seman import { DateTimeInput } from 'semantic-ui-calendar-react'; import { parseValue, normalizeLabel } from '../utilities'; +export const applyDecoratorTitle = field => { + const decorator = field.getDecorator('FormEditor'); + let name = field.getName(); + if (decorator) { + const args = decorator.getArguments(); + name = args[1]; + } + return name; +}; + export const ConcertoLabel = ({ skip, name, htmlFor }) => !skip ? : null; @@ -31,7 +41,7 @@ export const ConcertoCheckbox = ({ skipLabel, }) => ( - + ( - + + id={id} + value={value} + field={field} + readOnly={readOnly} + onFieldValueChange={onFieldValueChange} + options={relationshipOptions} + key={id} + /> : {normalizeLabel(relationship.getType())}} - labelPosition='right' - readOnly={readOnly} - value={relationship.getIdentifier()} - onChange={(e, data) => { - relationship.setIdentifier(data.value || 'resource1'); - return onFieldValueChange( - { ...data, value: relationship.toURI() }, - id - ); - }} - key={id} -/>; + type={type} + label={} + labelPosition='right' + readOnly={readOnly} + value={relationship.getIdentifier()} + onChange={(e, data) => { + relationship.setIdentifier(data.value || 'resource1'); + return onFieldValueChange( + { ...data, value: relationship.toURI() }, + id + ); + }} + key={id} + />; return - + {relationshipEditor} ; }; @@ -145,7 +155,7 @@ export const ConcertoDateTime = ({ skipLabel, }) => ( - + ( - + {children}
-
-
+
); @@ -203,24 +209,22 @@ export const ConcertoArrayElement = ({ }) => (
{children}
-
); @@ -231,16 +235,16 @@ export const ConcertoDropdown = ({ onFieldValueChange, options, }) => !readOnly ? ( - onFieldValueChange(data, id)} + key={`select-${id}`} + options={options} + /> ) : ( - -); + + ); const BinaryField = ({ className, children }) => (
diff --git a/packages/ui-concerto/src/formgenerator.js b/packages/ui-concerto/src/formgenerator.js index c681d689..1311e53c 100644 --- a/packages/ui-concerto/src/formgenerator.js +++ b/packages/ui-concerto/src/formgenerator.js @@ -133,7 +133,7 @@ class FormGenerator { }; if (classDeclaration.isConcept()) { - const concept = this.factory.newConcept(ns, name, factoryOptions); + const concept = this.factory.newConcept(ns, name, undefined, factoryOptions); return this.serializer.toJSON(concept); } const resource = this.factory.newResource( diff --git a/packages/ui-concerto/src/index.js b/packages/ui-concerto/src/index.js index 3565d0a0..f2082e3b 100644 --- a/packages/ui-concerto/src/index.js +++ b/packages/ui-concerto/src/index.js @@ -13,6 +13,7 @@ */ import ConcertoFormWrapper from './components/concertoFormWrapper'; import ReactFormVisitor from './reactformvisitor'; +import ModelBuilderVisitor from './modelBuilderVisitor'; import * as Utilities from './utilities'; -export { ConcertoFormWrapper as ConcertoForm, ReactFormVisitor, Utilities }; +export { ConcertoFormWrapper as ConcertoForm, ReactFormVisitor, ModelBuilderVisitor, Utilities }; diff --git a/packages/ui-concerto/src/modelBuilderVisitor.js b/packages/ui-concerto/src/modelBuilderVisitor.js new file mode 100644 index 00000000..01067d8a --- /dev/null +++ b/packages/ui-concerto/src/modelBuilderVisitor.js @@ -0,0 +1,80 @@ +/* eslint-disable class-methods-use-this */ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import ReactFormVisitor from './reactformvisitor'; + +/** + * Convert the contents of a ModelManager to React compnents. + * @class + */ +class ModelBuilderVisitor extends ReactFormVisitor { + /** + * Visitor design pattern + * @param {ClassDeclaration} classDeclaration - the object being visited + * @param {Object} parameters - the parameter + * @return {Object} the result of visiting or null + * @private + */ + visitClassDeclaration(classDeclaration, parameters) { + if (classDeclaration.getFullyQualifiedName() === 'concerto.metamodel.AssetDeclaration') { + return this.visitAssetDeclaration(classDeclaration, parameters); + } + + if (classDeclaration.getFullyQualifiedName() === 'concerto.metamodel.TypeIdentifier') { + parameters.skipLabel = true; + const component = super.visitClassDeclaration(classDeclaration, parameters); + parameters.skipLabel = false; + return component; + } + + if (classDeclaration.getFullyQualifiedName() === 'concerto.metamodel.BooleanFieldDeclaration') { + return this.visitFieldDeclaration(classDeclaration, parameters); + } + + return super.visitClassDeclaration(classDeclaration, parameters); + } + + visitAssetDeclaration(declaration, parameters) { + const props = declaration.getProperties(); + const identifier = props.find(({ name }) => name === 'identifier'); + const superType = props.find(({ name }) => name === 'superType'); + const fields = props.find(({ name }) => name === 'fields'); + + return
+
{identifier.accept(this, parameters)}
+
{superType.accept(this, parameters)}
+
{fields.accept(this, parameters)}
+
; + } + + visitFieldDeclaration(declaration, parameters) { + const props = declaration.getProperties(); + + const name = props.find(({ name }) => name === 'name'); + const isOptional = props.find(({ name }) => name === 'isOptional'); + const isArray = props.find(({ name }) => name === 'isArray'); + + const component = ( +
+
{name.accept(this, parameters)}
+
{isArray.accept(this, parameters)}
+
{isOptional.accept(this, parameters)}
+
+ ); + return component; + } +} +export default ModelBuilderVisitor; diff --git a/packages/ui-concerto/src/reactformvisitor.js b/packages/ui-concerto/src/reactformvisitor.js index dbf9d953..3347ba1a 100644 --- a/packages/ui-concerto/src/reactformvisitor.js +++ b/packages/ui-concerto/src/reactformvisitor.js @@ -317,9 +317,17 @@ class ReactFormVisitor { field.getFullyQualifiedTypeName() ); type = findConcreteSubclass(type); + + const decorator = field.getDecorator('FormEditor'); + let name = field.getName(); + if (decorator) { + const args = decorator.getArguments(); + name = args[1]; + } + return ( - - + + {type.accept(this, parameters)} ); @@ -397,7 +405,7 @@ class ReactFormVisitor { ); } else { - component = ; + component = ; } stack.pop(); diff --git a/packages/ui-concerto/src/utilities.js b/packages/ui-concerto/src/utilities.js index a3b4c007..66553e35 100644 --- a/packages/ui-concerto/src/utilities.js +++ b/packages/ui-concerto/src/utilities.js @@ -161,7 +161,7 @@ export const getDefaultValue = (field, parameters) => { if (type.isConcept()) { const concept = parameters.modelManager .getFactory() - .newConcept(type.getNamespace(), type.getName(), { + .newConcept(type.getNamespace(), type.getName(), undefined, { includeOptionalFields: true, generate: 'sample', }); From 9fd0dbdc90c377aae5fc211e031913c60bd06071 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Tue, 13 Jul 2021 12:27:43 +0100 Subject: [PATCH 02/10] feat(modelBuilderVisitor): wip, improve UX Signed-off-by: Matt Roberts --- .../src/stories/3-ConcertoForm.stories.js | 21 +++- .../src/components/concertoForm.css | 36 ++++-- .../src/components/concertoForm.js | 57 ++++----- packages/ui-concerto/src/components/fields.js | 21 +--- .../ui-concerto/src/modelBuilderVisitor.js | 115 ++++++++++++++++-- packages/ui-concerto/src/reactformvisitor.js | 65 ++++++---- packages/ui-concerto/src/utilities.js | 72 ++++++++++- 7 files changed, 290 insertions(+), 97 deletions(-) diff --git a/packages/storybook/src/stories/3-ConcertoForm.stories.js b/packages/storybook/src/stories/3-ConcertoForm.stories.js index 3433c25e..56a5fff1 100644 --- a/packages/storybook/src/stories/3-ConcertoForm.stories.js +++ b/packages/storybook/src/stories/3-ConcertoForm.stories.js @@ -14,13 +14,14 @@ export default { } }; -export const Demo = () => { +export const SimpleExample = () => { const readOnly = boolean('Read-only', false); const type = text('Type', 'test.Person'); const options = object('Options', { includeOptionalFields: true, includeSampleData: 'sample', updateExternalModels: true, + checkboxStyle: 'toggle', hiddenFields: [ 'org.accordproject.base.Transaction.transactionId', 'org.accordproject.cicero.contract.AccordContract.contractId', @@ -115,10 +116,13 @@ export const ModelBuilder = () => { includeOptionalFields: false, includeSampleData: 'sample', updateExternalModels: false, - hiddenFields: [ - 'concerto.metamodel.Decorator' - ], - visitor: new ModelBuilderVisitor() + visitor: new ModelBuilderVisitor(), + customSelectors: { + types: [ + { text: 'Contract', value: 'org.accordproject.cicero.contract.AccordContract' }, + { text: 'Party', value: 'org.accordproject.cicero.contract.AccordParty' } + ] + } }); const model = text('Model', ` /* * Licensed under the Apache License, Version 2.0 (the "License"); @@ -156,6 +160,7 @@ export const ModelBuilder = () => { } concept TypeIdentifier { + @FormEditor("selectOptions", "types") o String fullyQualifiedName } @@ -218,7 +223,9 @@ export const ModelBuilder = () => { abstract concept FieldDeclaration { o String name + @FormEditor("title", "isArray?") o Boolean isArray optional + @FormEditor("title", "isOptional?") o Boolean isOptional optional @FormEditor("hide", true) o Decorator[] decorators optional @@ -227,6 +234,7 @@ export const ModelBuilder = () => { concept ObjectFieldDeclaration extends FieldDeclaration { @FormEditor("hide", true) o StringDefault defaultValue optional + @FormEditor("selectOptions", "types") o TypeIdentifier type } @@ -272,6 +280,7 @@ export const ModelBuilder = () => { } concept RelationshipDeclaration extends FieldDeclaration { + @FormEditor("selectOptions", "types") o TypeIdentifier type } @@ -292,7 +301,7 @@ export const ModelBuilder = () => { o String namespace @FormEditor("hide", true) o Import[] imports optional - @FormEditor("title", "class") + @FormEditor("title", "classes") o ClassDeclaration[] declarations optional } `); diff --git a/packages/ui-concerto/src/components/concertoForm.css b/packages/ui-concerto/src/components/concertoForm.css index aed2d86d..af863b0b 100644 --- a/packages/ui-concerto/src/components/concertoForm.css +++ b/packages/ui-concerto/src/components/concertoForm.css @@ -14,8 +14,9 @@ .arrayElement, .classElement { - border-left: 1px solid rgba(34, 36, 38, 0.15); + border: 1px dashed rgba(34, 36, 38, 0.15); padding: 5px 0px 5px 10px; + margin-bottom: 5px; } .field > .field { @@ -45,12 +46,6 @@ column-gap: 5px; } -.fieldDeclaration { - display: grid; - grid-template-columns: auto 80px 80px; - column-gap: 5px; -} - .arrayButton { box-shadow: none !important; background-color: rgb(240, 240, 240) !important; @@ -59,3 +54,30 @@ .ui.form .field > label { margin-top: 2px; } + +.ui.form .field { + margin-bottom: 0; +} + +/** CSS Classes for use by the ModelBuilderVisitor */ +.mbFieldDeclaration { + display: grid; + grid-template-columns: auto 150px 80px 80px; + column-gap: 5px; +} + +.mbObjectDeclaration { + display: grid; + grid-template-columns: auto 150px 150px 80px 80px; + column-gap: 5px; +} + +.mbIdentifierDeclaration { + display: grid; + grid-template-columns: auto 150px; + column-gap: 5px; +} + +.mbFieldDeclarations { + padding-top: 5px; +} diff --git a/packages/ui-concerto/src/components/concertoForm.js b/packages/ui-concerto/src/components/concertoForm.js index db95fc3d..37be6bb3 100644 --- a/packages/ui-concerto/src/components/concertoForm.js +++ b/packages/ui-concerto/src/components/concertoForm.js @@ -17,6 +17,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import get from 'lodash.get'; import set from 'lodash.set'; +import toPath from 'lodash.topath'; import { Form, Dimmer, Loader, Message } from 'semantic-ui-react'; import { ModelManager } from '@accordproject/concerto-core'; @@ -53,11 +54,11 @@ const ConcertoForm = (props) => { }, [onValueChange, value]); const addElement = useCallback((e, key, elementValue) => { - const array = get(value, key) || []; - const path = typeof key === 'string' ? [key] : key; + const pathKey = toPath(key); + const array = get(value, pathKey) || []; const valueClone = set( { ...value }, - [...path, array.length], + [...pathKey, array.length], elementValue ); setValue(valueClone); @@ -132,9 +133,9 @@ const ConcertoForm = (props) => { if (loading) { return ( - - Loading - + + Loading + ); } @@ -142,42 +143,42 @@ const ConcertoForm = (props) => { try { const form = generator.generateHTML(props.type, value); return ( -
- {form} -
+
+ {form} +
); } catch (err) { console.error(err); return ( - - - An error occured while generating this form - -
{err.message}
-
+ + + An error occured while generating this form + +
{err.message}
+
); } } if (!props.type) { return ( - - No model type specified -

- Please specify a model type to display the form. -

-
- ); - } - - return ( - Invalid JSON instance provided + No model type specified

- The JSON value does not match the model type associated with this - form. + Please specify a model type to display the form.

+ ); + } + + return ( + + Invalid JSON instance provided +

+ The JSON value does not match the model type associated with this + form. +

+
); }; diff --git a/packages/ui-concerto/src/components/fields.js b/packages/ui-concerto/src/components/fields.js index b4b660ea..d06d9231 100644 --- a/packages/ui-concerto/src/components/fields.js +++ b/packages/ui-concerto/src/components/fields.js @@ -16,17 +16,7 @@ import React from 'react'; import { Relationship } from '@accordproject/concerto-core'; import { Checkbox, Input, Form, Button, Select, Popup, Label, Icon } from 'semantic-ui-react'; import { DateTimeInput } from 'semantic-ui-calendar-react'; -import { parseValue, normalizeLabel } from '../utilities'; - -export const applyDecoratorTitle = field => { - const decorator = field.getDecorator('FormEditor'); - let name = field.getName(); - if (decorator) { - const args = decorator.getArguments(); - name = args[1]; - } - return name; -}; +import { parseValue, normalizeLabel, applyDecoratorTitle } from '../utilities'; export const ConcertoLabel = ({ skip, name, htmlFor }) => !skip ? : null; @@ -39,11 +29,13 @@ export const ConcertoCheckbox = ({ value, onFieldValueChange, skipLabel, + toggle }) => ( } + trigger={} />} disabled={readOnly} className='arrayButton' @@ -214,7 +205,7 @@ export const ConcertoArrayElement = ({ content='Remove this element' position='left center' key={`array-popup-${id}`} - trigger={} + trigger={} />} aria-label={`Remove element ${index} from ${normalizeLabel(`${id}`)}`} className='arrayButton' diff --git a/packages/ui-concerto/src/modelBuilderVisitor.js b/packages/ui-concerto/src/modelBuilderVisitor.js index 01067d8a..5e42727d 100644 --- a/packages/ui-concerto/src/modelBuilderVisitor.js +++ b/packages/ui-concerto/src/modelBuilderVisitor.js @@ -14,7 +14,26 @@ */ import React from 'react'; +import get from 'lodash.get'; +import { Form } from 'semantic-ui-react'; import ReactFormVisitor from './reactformvisitor'; +import { + ConcertoDropdown, + ConcertoLabel, +} from './components/fields'; +import { + pathToString +} from './utilities'; + +const declarationTypes = [ + { value: 'concerto.metamodel.StringFieldDeclaration', text: 'Text' }, + { value: 'concerto.metamodel.IntegerFieldDeclaration', text: 'Whole Number' }, + { value: 'concerto.metamodel.BooleanFieldDeclaration', text: 'Boolean' }, + { value: 'concerto.metamodel.DateTimeFieldDeclaration', text: 'Date' }, + { value: 'concerto.metamodel.RealFieldDeclaration', text: 'Decimal' }, + { value: 'concerto.metamodel.ObjectFieldDeclaration', text: 'Object' }, + { value: 'concerto.metamodel.RelationshipDeclaration', text: 'Relationship' }, +]; /** * Convert the contents of a ModelManager to React compnents. @@ -29,34 +48,40 @@ class ModelBuilderVisitor extends ReactFormVisitor { * @private */ visitClassDeclaration(classDeclaration, parameters) { - if (classDeclaration.getFullyQualifiedName() === 'concerto.metamodel.AssetDeclaration') { - return this.visitAssetDeclaration(classDeclaration, parameters); + const fqn = classDeclaration.getFullyQualifiedName(); + if (fqn === 'concerto.metamodel.AssetDeclaration') { + return this.visitConceptDeclaration(classDeclaration, parameters); } - if (classDeclaration.getFullyQualifiedName() === 'concerto.metamodel.TypeIdentifier') { + if (fqn === 'concerto.metamodel.TypeIdentifier') { parameters.skipLabel = true; const component = super.visitClassDeclaration(classDeclaration, parameters); parameters.skipLabel = false; return component; } - if (classDeclaration.getFullyQualifiedName() === 'concerto.metamodel.BooleanFieldDeclaration') { + const declarationTypeNames = declarationTypes.map(({ value }) => value); + if (declarationTypeNames.includes(fqn)) { return this.visitFieldDeclaration(classDeclaration, parameters); } return super.visitClassDeclaration(classDeclaration, parameters); } - visitAssetDeclaration(declaration, parameters) { + visitConceptDeclaration(declaration, parameters) { const props = declaration.getProperties(); const identifier = props.find(({ name }) => name === 'identifier'); const superType = props.find(({ name }) => name === 'superType'); const fields = props.find(({ name }) => name === 'fields'); return
-
{identifier.accept(this, parameters)}
-
{superType.accept(this, parameters)}
-
{fields.accept(this, parameters)}
+
+
{identifier.accept(this, parameters)}
+
{superType.accept(this, parameters)}
+
+
+ {fields.accept(this, parameters)} +
; } @@ -66,15 +91,83 @@ class ModelBuilderVisitor extends ReactFormVisitor { const name = props.find(({ name }) => name === 'name'); const isOptional = props.find(({ name }) => name === 'isOptional'); const isArray = props.find(({ name }) => name === 'isArray'); + const type = props.find(({ name }) => name === 'type'); + + const key = pathToString(parameters.stack); + const value = get(parameters.json, key); + + const hasTypeProperty = name => [ + 'concerto.metamodel.ObjectFieldDeclaration', + 'concerto.metamodel.RelationshipDeclaration' + ].includes(name); + + // We need a special version of `onFieldValueChange` because changing the $class value + // requires us to regenerate the instance + const onFieldValueChange = (data, key) => { + const { value: newClassName } = data; + if (hasTypeProperty(newClassName) && !hasTypeProperty(value.$class)) { + console.log('adding type property'); + value.type = null; + } + + if (!hasTypeProperty(newClassName) && hasTypeProperty(value.$class)) { + console.log('removing type property'); + + value.type = undefined; + } + + return parameters.onFieldValueChange(data, key); + }; + + if (hasTypeProperty(declaration.getFullyQualifiedName())) { + // const typeField = new Field(declaration, ) + return ( +
+
{name.accept(this, parameters)}
+
{type.accept(this, parameters)}
+ + + ({ + key: `option-${value}`, + value, + text, + }))} + /> + +
{isArray.accept(this, parameters)}
+
{isOptional.accept(this, parameters)}
+
+ ); + } - const component = ( -
+ return ( +
{name.accept(this, parameters)}
+ + + ({ + key: `option-${value}`, + value, + text, + }))} + /> +
{isArray.accept(this, parameters)}
{isOptional.accept(this, parameters)}
); - return component; } } export default ModelBuilderVisitor; diff --git a/packages/ui-concerto/src/reactformvisitor.js b/packages/ui-concerto/src/reactformvisitor.js index 3347ba1a..f2c74a2d 100644 --- a/packages/ui-concerto/src/reactformvisitor.js +++ b/packages/ui-concerto/src/reactformvisitor.js @@ -42,28 +42,11 @@ import { findConcreteSubclass, getDefaultValue, toFieldType, + pathToString, + getCustomSelectDecoratorKey, + isHidden } from './utilities'; -const toPath = (paramsArr) => paramsArr.join('.'); - -/** - * Returns true if the field has the @FormEditor( "hide", true ) - * decorator - * @param {object} field the Concerto field - */ -function isHidden(field) { - const decorator = field.getDecorator('FormEditor'); - if (decorator) { - const args = decorator.getArguments(); - if (args.find((d, index) => d === 'hide' - && index < args[args.length - 1] && args[index + 1] === true)) { - return true; - } - } - - return false; -} - /** * Convert the contents of a ModelManager to React compnents. * @class @@ -205,7 +188,7 @@ class ReactFormVisitor { * @private */ visitEnumDeclaration(enumDeclaration, parameters) { - const key = toPath(parameters.stack); + const key = pathToString(parameters.stack); const value = get(parameters.json, key); return ( @@ -247,7 +230,7 @@ class ReactFormVisitor { } stack.push(field.getName()); - const key = toPath(stack); + const key = pathToString(stack); const value = get(parameters.json, key); let component = null; @@ -302,15 +285,45 @@ class ReactFormVisitor { * @private */ visitSingletonField(field, parameters, props) { - const key = toPath(parameters.stack); + const key = pathToString(parameters.stack); const value = get(parameters.json, key); if (field.isPrimitive()) { if (field.getType() === 'Boolean') { - return ; + const { checkboxStyle } = parameters; + return ; } if (toFieldType(field.getType()) === 'datetime-local') { return ; } + + // Allow the client application to override the definition of a basic input + // field with a Dropdown using their values + const customSelectorKey = getCustomSelectDecoratorKey(field); + if (customSelectorKey) { + const { customSelectors = {} } = parameters; + const options = customSelectors[customSelectorKey]; + if (!options) { + throw new Error(`Custom selector key '${customSelectorKey}' not found`); + } + return ({ + key: `option-${value}`, + value, + text, + }))} + />; + } return ; } let type = parameters.modelManager.getType( @@ -356,7 +369,7 @@ class ReactFormVisitor { } stack.push(relationship.getName()); - const key = toPath(stack); + const key = pathToString(stack); const value = get(parameters.json, key); const commonProps = { @@ -392,7 +405,7 @@ class ReactFormVisitor { {value && value.map((_element, index) => { stack.push(index); - const key = toPath(stack); + const key = pathToString(stack); const value = get(parameters.json, key); const arrayComponent = ( diff --git a/packages/ui-concerto/src/utilities.js b/packages/ui-concerto/src/utilities.js index 66553e35..85aab463 100644 --- a/packages/ui-concerto/src/utilities.js +++ b/packages/ui-concerto/src/utilities.js @@ -153,6 +153,7 @@ export const hideProperty = (property, parameters) => { * @private */ export const getDefaultValue = (field, parameters) => { + const { includeOptionalFields, includeSampleData } = parameters; if (field.isPrimitive()) { return convertToJSON(field); } @@ -162,16 +163,79 @@ export const getDefaultValue = (field, parameters) => { const concept = parameters.modelManager .getFactory() .newConcept(type.getNamespace(), type.getName(), undefined, { - includeOptionalFields: true, - generate: 'sample', + includeOptionalFields, + generate: includeSampleData, }); return parameters.modelManager.getSerializer().toJSON(concept); } const resource = parameters.modelManager .getFactory() .newResource(type.getNamespace(), type.getName(), 'resource1', { - includeOptionalFields: true, - generate: 'sample', + includeOptionalFields, + generate: includeSampleData, }); return parameters.modelManager.getSerializer().toJSON(resource); }; + +/** + * Convert an array path, e.g. ['a', 1, 'b'] to the path string e.g. 'a[1].b' + * @param {Array} array - the source array path + * @return {String} - A string representation of the path. + * @private + */ +export const pathToString = (array) => array.reduce((string, item) => { + const prefix = string === '' ? '' : '.'; + return string + (Number.isNaN(Number(item)) ? prefix + item : `[${item}]`); +}, ''); + +/** + * Substitutes the field name for a value in a decorator, @FormEditor( "title", "My Name" ) + * @param {object} field the Concerto field + * @private + */ +export const applyDecoratorTitle = field => { + const decorator = field.getDecorator('FormEditor'); + let name = field.getName(); + if (decorator) { + const args = decorator.getArguments(); + const index = args.findIndex(d => d === 'title'); + if (index >= 0 && index < args.length - 1) { + name = args[index + 1]; + } + } + return name; +}; + +/** + * Returns true if the field has the decorator @FormEditor( "hide", true ) + * @param {object} field the Concerto field + * @private + */ +export const isHidden = field => { + const decorator = field.getDecorator('FormEditor'); + if (decorator) { + const args = decorator.getArguments(); + if (args.find((d, index) => d === 'hide' + && index < args[args.length - 1] && args[index + 1] === true)) { + return true; + } + } + return false; +}; + +/** + * Returns the value of the decorator @FormEditor( "selectOptions", "key" ) + * @param {object} field the Concerto field + * @private + */ +export const getCustomSelectDecoratorKey = field => { + const decorator = field.getDecorator('FormEditor'); + if (decorator) { + const args = decorator.getArguments(); + const index = args.findIndex(d => d === 'selectOptions'); + if (index >= 0 && index < args.length - 1) { + return args[index + 1]; + } + } + return undefined; +}; From 8dce51c17fce22de3c1295821cc6945baddd4055 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Tue, 13 Jul 2021 16:22:39 +0100 Subject: [PATCH 03/10] feat(ModelBuilderVisitor): Add support for Object and Relationship TypeIdentifiers Signed-off-by: Matt Roberts --- .../src/stories/3-ConcertoForm.stories.js | 4 +- .../ui-concerto/src/modelBuilderVisitor.js | 100 ++++++++++-------- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/packages/storybook/src/stories/3-ConcertoForm.stories.js b/packages/storybook/src/stories/3-ConcertoForm.stories.js index 56a5fff1..d8cc5511 100644 --- a/packages/storybook/src/stories/3-ConcertoForm.stories.js +++ b/packages/storybook/src/stories/3-ConcertoForm.stories.js @@ -234,7 +234,7 @@ export const ModelBuilder = () => { concept ObjectFieldDeclaration extends FieldDeclaration { @FormEditor("hide", true) o StringDefault defaultValue optional - @FormEditor("selectOptions", "types") + @FormEditor("title", "typeIdentifier", "selectOptions", "types") o TypeIdentifier type } @@ -280,7 +280,7 @@ export const ModelBuilder = () => { } concept RelationshipDeclaration extends FieldDeclaration { - @FormEditor("selectOptions", "types") + @FormEditor("title", "typeIdentifier", "selectOptions", "types") o TypeIdentifier type } diff --git a/packages/ui-concerto/src/modelBuilderVisitor.js b/packages/ui-concerto/src/modelBuilderVisitor.js index 5e42727d..b3d7b697 100644 --- a/packages/ui-concerto/src/modelBuilderVisitor.js +++ b/packages/ui-concerto/src/modelBuilderVisitor.js @@ -50,7 +50,7 @@ class ModelBuilderVisitor extends ReactFormVisitor { visitClassDeclaration(classDeclaration, parameters) { const fqn = classDeclaration.getFullyQualifiedName(); if (fqn === 'concerto.metamodel.AssetDeclaration') { - return this.visitConceptDeclaration(classDeclaration, parameters); + return this.visitMetaConceptDeclaration(classDeclaration, parameters); } if (fqn === 'concerto.metamodel.TypeIdentifier') { @@ -62,13 +62,13 @@ class ModelBuilderVisitor extends ReactFormVisitor { const declarationTypeNames = declarationTypes.map(({ value }) => value); if (declarationTypeNames.includes(fqn)) { - return this.visitFieldDeclaration(classDeclaration, parameters); + return this.visitMetaFieldDeclaration(classDeclaration, parameters); } return super.visitClassDeclaration(classDeclaration, parameters); } - visitConceptDeclaration(declaration, parameters) { + visitMetaConceptDeclaration(declaration, parameters) { const props = declaration.getProperties(); const identifier = props.find(({ name }) => name === 'identifier'); const superType = props.find(({ name }) => name === 'superType'); @@ -85,13 +85,13 @@ class ModelBuilderVisitor extends ReactFormVisitor {
; } - visitFieldDeclaration(declaration, parameters) { + visitMetaFieldDeclaration(declaration, parameters) { const props = declaration.getProperties(); const name = props.find(({ name }) => name === 'name'); const isOptional = props.find(({ name }) => name === 'isOptional'); const isArray = props.find(({ name }) => name === 'isArray'); - const type = props.find(({ name }) => name === 'type'); + let type; const key = pathToString(parameters.stack); const value = get(parameters.json, key); @@ -101,59 +101,70 @@ class ModelBuilderVisitor extends ReactFormVisitor { 'concerto.metamodel.RelationshipDeclaration' ].includes(name); + // Create a new concept + if (value.$class) { + const tempParts = value.$class.split('.'); + const tempName = tempParts.pop(); + const concept = parameters.modelManager + .getFactory() + .newConcept(tempParts.join('.'), tempName, undefined, { + parameters: parameters.includeOptionalFields, + generate: parameters.includeSampleData, + }); + type = concept.getClassDeclaration().getProperties().find(({ name }) => name === 'type'); + } + // We need a special version of `onFieldValueChange` because changing the $class value // requires us to regenerate the instance - const onFieldValueChange = (data, key) => { + const onFieldValueChange = data => { const { value: newClassName } = data; - if (hasTypeProperty(newClassName) && !hasTypeProperty(value.$class)) { - console.log('adding type property'); - value.type = null; - } - if (!hasTypeProperty(newClassName) && hasTypeProperty(value.$class)) { - console.log('removing type property'); - - value.type = undefined; + // Add a new type property to the data + if (hasTypeProperty(newClassName) && !hasTypeProperty(value.$class)) { + const parts = newClassName.split('.'); + const name = parts.pop(); + + // Create a new concept + const concept = parameters.modelManager + .getFactory() + .newConcept(parts.join('.'), name, undefined, { + parameters: parameters.includeOptionalFields, + generate: parameters.includeSampleData, + }); + + // Keep any existing values + const json = { + ...parameters.modelManager.getSerializer().toJSON(concept), + name: value.name, + isOptional: value.isOptional, + isArray: value.isArray, + }; + value.type = json; + + parameters.onFieldValueChange({ + ...data, + value: json + }, key); + return; } - return parameters.onFieldValueChange(data, key); + // Remove any old type properties + value.type = undefined; + value.$class = newClassName; + parameters.onFieldValueChange({ + ...data, + value, + }, key); }; - if (hasTypeProperty(declaration.getFullyQualifiedName())) { - // const typeField = new Field(declaration, ) - return ( -
-
{name.accept(this, parameters)}
-
{type.accept(this, parameters)}
- - - ({ - key: `option-${value}`, - value, - text, - }))} - /> - -
{isArray.accept(this, parameters)}
-
{isOptional.accept(this, parameters)}
-
- ); - } - return ( -
+
{name.accept(this, parameters)}
+
{type && type.accept(this, parameters)}
{isArray.accept(this, parameters)}
{isOptional.accept(this, parameters)}
From 8ff9cb4f7a4b533ebde97d3bc190ce0a2fbf2968 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Wed, 14 Jul 2021 10:02:28 +0100 Subject: [PATCH 04/10] feat(concerto): Add regex validation and explicit subclass selection --- .../src/stories/3-ConcertoForm.stories.js | 208 +---------------- .../storybook/src/stories/concerto.models.js | 212 ++++++++++++++++++ packages/ui-concerto/src/components/fields.js | 18 +- .../ui-concerto/src/modelBuilderVisitor.js | 2 +- packages/ui-concerto/src/utilities.js | 18 ++ 5 files changed, 248 insertions(+), 210 deletions(-) create mode 100644 packages/storybook/src/stories/concerto.models.js diff --git a/packages/storybook/src/stories/3-ConcertoForm.stories.js b/packages/storybook/src/stories/3-ConcertoForm.stories.js index d8cc5511..1044b659 100644 --- a/packages/storybook/src/stories/3-ConcertoForm.stories.js +++ b/packages/storybook/src/stories/3-ConcertoForm.stories.js @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { action } from '@storybook/addon-actions'; import { text, boolean, object } from '@storybook/addon-knobs'; import { ConcertoForm, ModelBuilderVisitor } from '@accordproject/ui-concerto'; +import { ConcertoMetamodel, TestModel } from './concerto.models'; export default { title: 'Concerto Form', @@ -29,29 +30,7 @@ export const SimpleExample = () => { 'org.accordproject.cicero.contract.AccordContractState.stateId', ], }); - const model = text('Model', `namespace test - - enum Country { - o USA - o UK - o France - o Sweden - } - - participant Person identified by name { - o String name - o Address address - --> Person[] children optional - } - - concept Address { - o String street - o String city - @FormEditor( "hide", true) - o String zipCode - o Country country - } - `); + const model = text('Model', TestModel); const handleValueChange = (json) => { return action("value changed")(json); @@ -114,7 +93,6 @@ export const ModelBuilder = () => { const type = text('Type', 'concerto.metamodel.ModelFile'); const options = object('Options', { includeOptionalFields: false, - includeSampleData: 'sample', updateExternalModels: false, visitor: new ModelBuilderVisitor(), customSelectors: { @@ -124,187 +102,7 @@ export const ModelBuilder = () => { ] } }); - const model = text('Model', ` /* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - namespace concerto.metamodel - - /** - * The metadmodel for Concerto files - */ - - abstract concept DecoratorLiteral { - } - - concept DecoratorString extends DecoratorLiteral { - o String value - } - - concept DecoratorNumber extends DecoratorLiteral { - o Double value - } - - concept DecoratorBoolean extends DecoratorLiteral { - o Boolean value - } - - concept TypeIdentifier { - @FormEditor("selectOptions", "types") - o String fullyQualifiedName - } - - concept DecoratorIdentifier extends DecoratorLiteral { - o TypeIdentifier identifier - o Boolean isArray default=false - } - - concept Decorator { - o String name - o DecoratorLiteral[] arguments optional - } - - abstract concept ClassDeclaration { - @FormEditor("hide", true) - o Decorator[] decorators optional - o Boolean isAbstract default=false - @FormEditor("title", "name") - o String identifier - o String identifiedByField optional - @FormEditor("title", "parentType") - o TypeIdentifier superType optional - o BooleanFieldDeclaration[] fields - } - - concept AssetDeclaration extends ClassDeclaration { - } - - concept ParticipantDeclaration extends ClassDeclaration { - } - - concept TransactionDeclaration extends ClassDeclaration { - } - - concept EventDeclaration extends ClassDeclaration { - } - - concept ConceptDeclaration extends ClassDeclaration { - } - - // TODO - enums do not support abstract or super types - concept EnumDeclaration extends ClassDeclaration { - } - - concept StringDefault { - o String value - } - - concept BooleanDefault { - o Boolean value - } - - concept IntegerDefault { - o Integer value - } - - concept RealDefault { - o Double value - } - - abstract concept FieldDeclaration { - o String name - @FormEditor("title", "isArray?") - o Boolean isArray optional - @FormEditor("title", "isOptional?") - o Boolean isOptional optional - @FormEditor("hide", true) - o Decorator[] decorators optional - } - - concept ObjectFieldDeclaration extends FieldDeclaration { - @FormEditor("hide", true) - o StringDefault defaultValue optional - @FormEditor("title", "typeIdentifier", "selectOptions", "types") - o TypeIdentifier type - } - - concept BooleanFieldDeclaration extends FieldDeclaration { - @FormEditor("hide", true) - o BooleanDefault defaultValue optional - } - - concept DateTimeFieldDeclaration extends FieldDeclaration { - } - - concept StringFieldDeclaration extends FieldDeclaration { - @FormEditor("hide", true) - o StringDefault defaultValue optional - @FormEditor("hide", true) - o StringRegexValidator validator optional - } - - concept StringRegexValidator { - o String regex - } - - concept RealDomainValidator { - o Double lower optional - o Double upper optional - } - - concept IntegerDomainValidator { - o Integer lower optional - o Integer upper optional - } - - concept RealFieldDeclaration extends FieldDeclaration { - o RealDefault defaultValue optional - o RealDomainValidator validator optional - } - - concept IntegerFieldDeclaration extends FieldDeclaration { - @FormEditor("hide", true) - o IntegerDefault defaultValue optional - @FormEditor("hide", true) - o IntegerDomainValidator validator optional - } - - concept RelationshipDeclaration extends FieldDeclaration { - @FormEditor("title", "typeIdentifier", "selectOptions", "types") - o TypeIdentifier type - } - - abstract concept Import { - o String uri optional - } - - concept NamespaceImport extends Import { - o String namespace - } - - concept TypeImport extends Import { - o TypeIdentifier identifier - } - - concept ModelFile { - @FormEditor("hide", true) - o String namespace - @FormEditor("hide", true) - o Import[] imports optional - @FormEditor("title", "classes") - o ClassDeclaration[] declarations optional - } - `); + const model = text('Model', ConcertoMetamodel); const handleValueChange = (json) => { return action("value changed")(json); diff --git a/packages/storybook/src/stories/concerto.models.js b/packages/storybook/src/stories/concerto.models.js new file mode 100644 index 00000000..e128aa7c --- /dev/null +++ b/packages/storybook/src/stories/concerto.models.js @@ -0,0 +1,212 @@ +export const TestModel = `namespace test + +enum Country { + o USA + o UK + o France + o Sweden +} + +participant Person identified by name { + o String name + o Address address + --> Person[] children optional +} + +concept Address { + o String street + o String city + @FormEditor( "hide", true) + o String zipCode + o Country country +} +`; + +export const ConcertoMetamodel = `/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + namespace concerto.metamodel + + /** + * The metadmodel for Concerto files + */ + + abstract concept DecoratorLiteral { + } + + concept DecoratorString extends DecoratorLiteral { + o String value + } + + concept DecoratorNumber extends DecoratorLiteral { + o Double value + } + + concept DecoratorBoolean extends DecoratorLiteral { + o Boolean value + } + + concept TypeIdentifier { + @FormEditor("selectOptions", "types") + o String fullyQualifiedName + } + + concept DecoratorIdentifier extends DecoratorLiteral { + o TypeIdentifier identifier + o Boolean isArray default=false + } + + concept Decorator { + o String name + o DecoratorLiteral[] arguments optional + } + + @FormEditor("defaultSubclass","concerto.metamodel.ConceptDeclaration") + abstract concept ClassDeclaration { + @FormEditor("hide", true) + o Decorator[] decorators optional + o Boolean isAbstract default=false + // TODO use regex /^(?!null|true|false)(\\p{Lu}|\\p{Ll}|\\p{Lt}|\\p{Lm}|\\p{Lo}|\\p{Nl}|\\$|_|\\\\u[0-9A-Fa-f]{4})(?:\\p{Lu}|\\p{Ll}|\\p{Lt}|\\p{Lm}|\\p{Lo}|\\p{Nl}|\\$|_|\\\\u[0-9A-Fa-f]{4}|\\p{Mn}|\\p{Mc}|\\p{Nd}|\\p{Pc}|\\u200C|\\u200D)*/u + @FormEditor("title", "name") + o String identifier default="className" regex=/^(?!null|true|false)(\\w|\\d|\\$|_|\\\\u[0-9A-Fa-f]{4})(?:\\w|\\d|\\$|_|\\\\u[0-9A-Fa-f]{4}|\\S|\\u200C|\\u200D)*$/ + o String identifiedByField optional + @FormEditor("title", "parentType") + o TypeIdentifier superType optional + o BooleanFieldDeclaration[] fields + } + + concept AssetDeclaration extends ClassDeclaration { + } + + concept ParticipantDeclaration extends ClassDeclaration { + } + + concept TransactionDeclaration extends ClassDeclaration { + } + + concept EventDeclaration extends ClassDeclaration { + } + + concept ConceptDeclaration extends ClassDeclaration { + } + + // TODO - enums do not support abstract or super types + concept EnumDeclaration extends ClassDeclaration { + } + + concept StringDefault { + o String value + } + + concept BooleanDefault { + o Boolean value + } + + concept IntegerDefault { + o Integer value + } + + concept RealDefault { + o Double value + } + + // TODO this decorator doesn't work because Concerto's version of 'findConcreteSubclass' doesn't support it yet. + @FormEditor("defaultSubclass","concerto.metamodel.StringFieldDeclaration") + abstract concept FieldDeclaration { + // TODO Allow regex modifiers e.g. //ui + // regex /^(?!null|true|false)(\\p{Lu}|\\p{Ll}|\\p{Lt}|\\pLm}|\\p{Lo}|\\p{Nl}|\\$|_|\\\\u[0-9A-Fa-f]{4})(?:\\p{Lu}|\\p{Ll}|\\p{Lt}|\\p{Lm}|\\p{Lo}|\\p{Nl}|\\$|_|\\\\u[0-9A-Fa-f]{4}|\\p{Mn}|\\p{Mc}|\\p{Nd}|\\p{Pc}|\\u200C|\\u200D)*/u + // This regex is an approximation of what the parser accepts without using unicode character classes + o String name default="fieldName" regex=/^(?!null|true|false)(\\w|\\d|\\$|_|\\\\u[0-9A-Fa-f]{4})(?:\\w|\\d|\\$|_|\\\\u[0-9A-Fa-f]{4}|\\S|\\u200C|\\u200D)*$/ + @FormEditor("title", "isArray?") + o Boolean isArray optional + @FormEditor("title", "isOptional?") + o Boolean isOptional optional + @FormEditor("hide", true) + o Decorator[] decorators optional + } + + concept ObjectFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o StringDefault defaultValue optional + @FormEditor("title", "typeIdentifier", "selectOptions", "types") + o TypeIdentifier type + } + + concept BooleanFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o BooleanDefault defaultValue optional + } + + concept DateTimeFieldDeclaration extends FieldDeclaration { + } + + concept StringFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o StringDefault defaultValue optional + @FormEditor("hide", true) + o StringRegexValidator validator optional + } + + concept StringRegexValidator { + o String regex + } + + concept RealDomainValidator { + o Double lower optional + o Double upper optional + } + + concept IntegerDomainValidator { + o Integer lower optional + o Integer upper optional + } + + concept RealFieldDeclaration extends FieldDeclaration { + o RealDefault defaultValue optional + o RealDomainValidator validator optional + } + + concept IntegerFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o IntegerDefault defaultValue optional + @FormEditor("hide", true) + o IntegerDomainValidator validator optional + } + + concept RelationshipDeclaration extends FieldDeclaration { + @FormEditor("title", "typeIdentifier", "selectOptions", "types") + o TypeIdentifier type + } + + abstract concept Import { + o String uri optional + } + + concept NamespaceImport extends Import { + o String namespace + } + + concept TypeImport extends Import { + o TypeIdentifier identifier + } + + concept ModelFile { + @FormEditor("hide", true) + o String namespace + @FormEditor("hide", true) + o Import[] imports optional + @FormEditor("title", "classes") + o ClassDeclaration[] declarations optional + } +`; \ No newline at end of file diff --git a/packages/ui-concerto/src/components/fields.js b/packages/ui-concerto/src/components/fields.js index d06d9231..c2990706 100644 --- a/packages/ui-concerto/src/components/fields.js +++ b/packages/ui-concerto/src/components/fields.js @@ -54,8 +54,18 @@ export const ConcertoInput = ({ onFieldValueChange, skipLabel, type, -}) => ( - +}) => { + let error; + const validator = field.getValidator(); + if (validator) { + try { + validator.validate(id, value); + } catch (validationError) { + error = true; + console.warn(validationError.message); + } + } + return - -); + ; +}; export const ConcertoRelationship = ({ id, diff --git a/packages/ui-concerto/src/modelBuilderVisitor.js b/packages/ui-concerto/src/modelBuilderVisitor.js index b3d7b697..7e405ad9 100644 --- a/packages/ui-concerto/src/modelBuilderVisitor.js +++ b/packages/ui-concerto/src/modelBuilderVisitor.js @@ -49,7 +49,7 @@ class ModelBuilderVisitor extends ReactFormVisitor { */ visitClassDeclaration(classDeclaration, parameters) { const fqn = classDeclaration.getFullyQualifiedName(); - if (fqn === 'concerto.metamodel.AssetDeclaration') { + if (fqn === 'concerto.metamodel.ConceptDeclaration') { return this.visitMetaConceptDeclaration(classDeclaration, parameters); } diff --git a/packages/ui-concerto/src/utilities.js b/packages/ui-concerto/src/utilities.js index 85aab463..c686b65d 100644 --- a/packages/ui-concerto/src/utilities.js +++ b/packages/ui-concerto/src/utilities.js @@ -116,6 +116,24 @@ export const findConcreteSubclass = declaration => { throw new Error('No concrete subclasses found'); } + // Allow the model to specify an explicit default sub-class, + // e.g. @FormEditor("defaultSubclass", "concerto.metadata.ConceptDeclaration") + const decorator = declaration.getDecorator('FormEditor'); + let explicitSubclassName; + if (decorator) { + const args = decorator.getArguments(); + const index = args.findIndex(d => d === 'defaultSubclass'); + if (index >= 0 && index < args.length - 1) { + explicitSubclassName = args[index + 1]; + } + } + + const explicitSubclass = concreteSubclasses + .find(c => c.getFullyQualifiedName() === explicitSubclassName); + if (explicitSubclass) { + return explicitSubclass; + } + return concreteSubclasses[0]; }; From df81370a09a266fb9188422620e5f0c2a99f03f5 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Mon, 12 Jul 2021 21:23:00 +0100 Subject: [PATCH 05/10] feat(concerto): Add Model Builder story Signed-off-by: Matt Roberts --- .../src/stories/3-ConcertoForm.stories.js | 223 +++++++++++++++++- .../src/components/concertoForm.css | 45 ++-- packages/ui-concerto/src/components/fields.js | 150 ++++++------ packages/ui-concerto/src/index.js | 3 +- .../ui-concerto/src/modelBuilderVisitor.js | 80 +++++++ packages/ui-concerto/src/reactformvisitor.js | 14 +- 6 files changed, 417 insertions(+), 98 deletions(-) create mode 100644 packages/ui-concerto/src/modelBuilderVisitor.js diff --git a/packages/storybook/src/stories/3-ConcertoForm.stories.js b/packages/storybook/src/stories/3-ConcertoForm.stories.js index 36431b32..3433c25e 100644 --- a/packages/storybook/src/stories/3-ConcertoForm.stories.js +++ b/packages/storybook/src/stories/3-ConcertoForm.stories.js @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { action } from '@storybook/addon-actions'; import { text, boolean, object } from '@storybook/addon-knobs'; -import { ConcertoForm } from '@accordproject/ui-concerto'; +import { ConcertoForm, ModelBuilderVisitor } from '@accordproject/ui-concerto'; export default { title: 'Concerto Form', @@ -57,8 +57,8 @@ export const Demo = () => { }; options.relationshipProvider = { - getOptions : (field) => { - if(field.getFullyQualifiedTypeName() === 'test.Person') { + getOptions: (field) => { + if (field.getFullyQualifiedTypeName() === 'test.Person') { return [{ key: '001', value: 'test.Person#Marissa', @@ -85,19 +85,228 @@ export const Demo = () => { value: 'test.Person#Rosalind', text: 'Rosalind Picard' } - ] + ] } else { return null; - }} + } + } + }; + + return ( +
+ +
+ ) +}; + + +export const ModelBuilder = () => { + const readOnly = boolean('Read-only', false); + const type = text('Type', 'concerto.metamodel.ModelFile'); + const options = object('Options', { + includeOptionalFields: false, + includeSampleData: 'sample', + updateExternalModels: false, + hiddenFields: [ + 'concerto.metamodel.Decorator' + ], + visitor: new ModelBuilderVisitor() + }); + const model = text('Model', ` /* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + namespace concerto.metamodel + + /** + * The metadmodel for Concerto files + */ + + abstract concept DecoratorLiteral { + } + + concept DecoratorString extends DecoratorLiteral { + o String value + } + + concept DecoratorNumber extends DecoratorLiteral { + o Double value + } + + concept DecoratorBoolean extends DecoratorLiteral { + o Boolean value + } + + concept TypeIdentifier { + o String fullyQualifiedName + } + + concept DecoratorIdentifier extends DecoratorLiteral { + o TypeIdentifier identifier + o Boolean isArray default=false + } + + concept Decorator { + o String name + o DecoratorLiteral[] arguments optional + } + + abstract concept ClassDeclaration { + @FormEditor("hide", true) + o Decorator[] decorators optional + o Boolean isAbstract default=false + @FormEditor("title", "name") + o String identifier + o String identifiedByField optional + @FormEditor("title", "parentType") + o TypeIdentifier superType optional + o BooleanFieldDeclaration[] fields + } + + concept AssetDeclaration extends ClassDeclaration { + } + + concept ParticipantDeclaration extends ClassDeclaration { + } + + concept TransactionDeclaration extends ClassDeclaration { + } + + concept EventDeclaration extends ClassDeclaration { + } + + concept ConceptDeclaration extends ClassDeclaration { + } + + // TODO - enums do not support abstract or super types + concept EnumDeclaration extends ClassDeclaration { + } + + concept StringDefault { + o String value + } + + concept BooleanDefault { + o Boolean value + } + + concept IntegerDefault { + o Integer value + } + + concept RealDefault { + o Double value + } + + abstract concept FieldDeclaration { + o String name + o Boolean isArray optional + o Boolean isOptional optional + @FormEditor("hide", true) + o Decorator[] decorators optional + } + + concept ObjectFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o StringDefault defaultValue optional + o TypeIdentifier type + } + + concept BooleanFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o BooleanDefault defaultValue optional + } + + concept DateTimeFieldDeclaration extends FieldDeclaration { + } + + concept StringFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o StringDefault defaultValue optional + @FormEditor("hide", true) + o StringRegexValidator validator optional + } + + concept StringRegexValidator { + o String regex + } + + concept RealDomainValidator { + o Double lower optional + o Double upper optional + } + + concept IntegerDomainValidator { + o Integer lower optional + o Integer upper optional + } + + concept RealFieldDeclaration extends FieldDeclaration { + o RealDefault defaultValue optional + o RealDomainValidator validator optional + } + + concept IntegerFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o IntegerDefault defaultValue optional + @FormEditor("hide", true) + o IntegerDomainValidator validator optional + } + + concept RelationshipDeclaration extends FieldDeclaration { + o TypeIdentifier type + } + + abstract concept Import { + o String uri optional + } + + concept NamespaceImport extends Import { + o String namespace + } + + concept TypeImport extends Import { + o TypeIdentifier identifier + } + + concept ModelFile { + @FormEditor("hide", true) + o String namespace + @FormEditor("hide", true) + o Import[] imports optional + @FormEditor("title", "class") + o ClassDeclaration[] declarations optional + } + `); + + const handleValueChange = (json) => { + return action("value changed")(json); }; - + return (
.field { + border-left: 1px solid rgba(34, 36, 38, 0.15); + padding: 5px 0px 5px 10px; } .grid { - display: grid; - grid-template-columns: auto 36px; - grid-column-gap: 5px; + display: grid; + grid-template-columns: auto 42px; + column-gap: 5px; } .fullHeight { - height:100% + height: 100%; } .monetaryAmount { - display: grid; - grid-template-columns: auto 80px; - grid-column-gap: 5px; + display: grid; + grid-template-columns: auto 80px; + column-gap: 5px; } .duration { - display: grid; - grid-template-columns: auto 100px; - grid-column-gap: 5px; + display: grid; + grid-template-columns: auto 100px; + column-gap: 5px; +} + +.fieldDeclaration { + display: grid; + grid-template-columns: auto 80px 80px; + column-gap: 5px; } .arrayButton { - box-shadow: none !important; + box-shadow: none !important; + background-color: rgb(240, 240, 240) !important; +} + +.ui.form .field > label { + margin-top: 2px; } diff --git a/packages/ui-concerto/src/components/fields.js b/packages/ui-concerto/src/components/fields.js index 506c9057..b4b660ea 100644 --- a/packages/ui-concerto/src/components/fields.js +++ b/packages/ui-concerto/src/components/fields.js @@ -18,6 +18,16 @@ import { Checkbox, Input, Form, Button, Select, Popup, Label, Icon } from 'seman import { DateTimeInput } from 'semantic-ui-calendar-react'; import { parseValue, normalizeLabel } from '../utilities'; +export const applyDecoratorTitle = field => { + const decorator = field.getDecorator('FormEditor'); + let name = field.getName(); + if (decorator) { + const args = decorator.getArguments(); + name = args[1]; + } + return name; +}; + export const ConcertoLabel = ({ skip, name, htmlFor }) => !skip ? : null; @@ -31,7 +41,7 @@ export const ConcertoCheckbox = ({ skipLabel, }) => ( - + ( - + + id={id} + value={value} + field={field} + readOnly={readOnly} + onFieldValueChange={onFieldValueChange} + options={relationshipOptions} + key={id} + /> : {normalizeLabel(relationship.getType())}} - labelPosition='right' - readOnly={readOnly} - value={relationship.getIdentifier()} - onChange={(e, data) => { - relationship.setIdentifier(data.value || 'resource1'); - return onFieldValueChange( - { ...data, value: relationship.toURI() }, - id - ); - }} - key={id} -/>; + type={type} + label={} + labelPosition='right' + readOnly={readOnly} + value={relationship.getIdentifier()} + onChange={(e, data) => { + relationship.setIdentifier(data.value || 'resource1'); + return onFieldValueChange( + { ...data, value: relationship.toURI() }, + id + ); + }} + key={id} + />; return - + {relationshipEditor} ; }; @@ -145,7 +155,7 @@ export const ConcertoDateTime = ({ skipLabel, }) => ( - + ( - + {children}
-
-
+
); @@ -203,24 +209,22 @@ export const ConcertoArrayElement = ({ }) => (
{children}
-
); @@ -231,16 +235,16 @@ export const ConcertoDropdown = ({ onFieldValueChange, options, }) => !readOnly ? ( - onFieldValueChange(data, id)} + key={`select-${id}`} + options={options} + /> ) : ( - -); + + ); const BinaryField = ({ className, children }) => (
diff --git a/packages/ui-concerto/src/index.js b/packages/ui-concerto/src/index.js index 3565d0a0..f2082e3b 100644 --- a/packages/ui-concerto/src/index.js +++ b/packages/ui-concerto/src/index.js @@ -13,6 +13,7 @@ */ import ConcertoFormWrapper from './components/concertoFormWrapper'; import ReactFormVisitor from './reactformvisitor'; +import ModelBuilderVisitor from './modelBuilderVisitor'; import * as Utilities from './utilities'; -export { ConcertoFormWrapper as ConcertoForm, ReactFormVisitor, Utilities }; +export { ConcertoFormWrapper as ConcertoForm, ReactFormVisitor, ModelBuilderVisitor, Utilities }; diff --git a/packages/ui-concerto/src/modelBuilderVisitor.js b/packages/ui-concerto/src/modelBuilderVisitor.js new file mode 100644 index 00000000..01067d8a --- /dev/null +++ b/packages/ui-concerto/src/modelBuilderVisitor.js @@ -0,0 +1,80 @@ +/* eslint-disable class-methods-use-this */ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import ReactFormVisitor from './reactformvisitor'; + +/** + * Convert the contents of a ModelManager to React compnents. + * @class + */ +class ModelBuilderVisitor extends ReactFormVisitor { + /** + * Visitor design pattern + * @param {ClassDeclaration} classDeclaration - the object being visited + * @param {Object} parameters - the parameter + * @return {Object} the result of visiting or null + * @private + */ + visitClassDeclaration(classDeclaration, parameters) { + if (classDeclaration.getFullyQualifiedName() === 'concerto.metamodel.AssetDeclaration') { + return this.visitAssetDeclaration(classDeclaration, parameters); + } + + if (classDeclaration.getFullyQualifiedName() === 'concerto.metamodel.TypeIdentifier') { + parameters.skipLabel = true; + const component = super.visitClassDeclaration(classDeclaration, parameters); + parameters.skipLabel = false; + return component; + } + + if (classDeclaration.getFullyQualifiedName() === 'concerto.metamodel.BooleanFieldDeclaration') { + return this.visitFieldDeclaration(classDeclaration, parameters); + } + + return super.visitClassDeclaration(classDeclaration, parameters); + } + + visitAssetDeclaration(declaration, parameters) { + const props = declaration.getProperties(); + const identifier = props.find(({ name }) => name === 'identifier'); + const superType = props.find(({ name }) => name === 'superType'); + const fields = props.find(({ name }) => name === 'fields'); + + return
+
{identifier.accept(this, parameters)}
+
{superType.accept(this, parameters)}
+
{fields.accept(this, parameters)}
+
; + } + + visitFieldDeclaration(declaration, parameters) { + const props = declaration.getProperties(); + + const name = props.find(({ name }) => name === 'name'); + const isOptional = props.find(({ name }) => name === 'isOptional'); + const isArray = props.find(({ name }) => name === 'isArray'); + + const component = ( +
+
{name.accept(this, parameters)}
+
{isArray.accept(this, parameters)}
+
{isOptional.accept(this, parameters)}
+
+ ); + return component; + } +} +export default ModelBuilderVisitor; diff --git a/packages/ui-concerto/src/reactformvisitor.js b/packages/ui-concerto/src/reactformvisitor.js index dbf9d953..3347ba1a 100644 --- a/packages/ui-concerto/src/reactformvisitor.js +++ b/packages/ui-concerto/src/reactformvisitor.js @@ -317,9 +317,17 @@ class ReactFormVisitor { field.getFullyQualifiedTypeName() ); type = findConcreteSubclass(type); + + const decorator = field.getDecorator('FormEditor'); + let name = field.getName(); + if (decorator) { + const args = decorator.getArguments(); + name = args[1]; + } + return ( - - + + {type.accept(this, parameters)} ); @@ -397,7 +405,7 @@ class ReactFormVisitor { ); } else { - component = ; + component = ; } stack.pop(); From 93a6f524d7599888b4d3fe3e0c5d087a16c1b12d Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Tue, 13 Jul 2021 12:27:43 +0100 Subject: [PATCH 06/10] feat(modelBuilderVisitor): wip, improve UX Signed-off-by: Matt Roberts --- .../src/stories/3-ConcertoForm.stories.js | 21 +++- .../src/components/concertoForm.css | 36 ++++-- .../src/components/concertoForm.js | 57 ++++----- packages/ui-concerto/src/components/fields.js | 21 +--- .../ui-concerto/src/modelBuilderVisitor.js | 115 ++++++++++++++++-- packages/ui-concerto/src/reactformvisitor.js | 65 ++++++---- packages/ui-concerto/src/utilities.js | 68 ++++++++++- 7 files changed, 288 insertions(+), 95 deletions(-) diff --git a/packages/storybook/src/stories/3-ConcertoForm.stories.js b/packages/storybook/src/stories/3-ConcertoForm.stories.js index 3433c25e..56a5fff1 100644 --- a/packages/storybook/src/stories/3-ConcertoForm.stories.js +++ b/packages/storybook/src/stories/3-ConcertoForm.stories.js @@ -14,13 +14,14 @@ export default { } }; -export const Demo = () => { +export const SimpleExample = () => { const readOnly = boolean('Read-only', false); const type = text('Type', 'test.Person'); const options = object('Options', { includeOptionalFields: true, includeSampleData: 'sample', updateExternalModels: true, + checkboxStyle: 'toggle', hiddenFields: [ 'org.accordproject.base.Transaction.transactionId', 'org.accordproject.cicero.contract.AccordContract.contractId', @@ -115,10 +116,13 @@ export const ModelBuilder = () => { includeOptionalFields: false, includeSampleData: 'sample', updateExternalModels: false, - hiddenFields: [ - 'concerto.metamodel.Decorator' - ], - visitor: new ModelBuilderVisitor() + visitor: new ModelBuilderVisitor(), + customSelectors: { + types: [ + { text: 'Contract', value: 'org.accordproject.cicero.contract.AccordContract' }, + { text: 'Party', value: 'org.accordproject.cicero.contract.AccordParty' } + ] + } }); const model = text('Model', ` /* * Licensed under the Apache License, Version 2.0 (the "License"); @@ -156,6 +160,7 @@ export const ModelBuilder = () => { } concept TypeIdentifier { + @FormEditor("selectOptions", "types") o String fullyQualifiedName } @@ -218,7 +223,9 @@ export const ModelBuilder = () => { abstract concept FieldDeclaration { o String name + @FormEditor("title", "isArray?") o Boolean isArray optional + @FormEditor("title", "isOptional?") o Boolean isOptional optional @FormEditor("hide", true) o Decorator[] decorators optional @@ -227,6 +234,7 @@ export const ModelBuilder = () => { concept ObjectFieldDeclaration extends FieldDeclaration { @FormEditor("hide", true) o StringDefault defaultValue optional + @FormEditor("selectOptions", "types") o TypeIdentifier type } @@ -272,6 +280,7 @@ export const ModelBuilder = () => { } concept RelationshipDeclaration extends FieldDeclaration { + @FormEditor("selectOptions", "types") o TypeIdentifier type } @@ -292,7 +301,7 @@ export const ModelBuilder = () => { o String namespace @FormEditor("hide", true) o Import[] imports optional - @FormEditor("title", "class") + @FormEditor("title", "classes") o ClassDeclaration[] declarations optional } `); diff --git a/packages/ui-concerto/src/components/concertoForm.css b/packages/ui-concerto/src/components/concertoForm.css index aed2d86d..af863b0b 100644 --- a/packages/ui-concerto/src/components/concertoForm.css +++ b/packages/ui-concerto/src/components/concertoForm.css @@ -14,8 +14,9 @@ .arrayElement, .classElement { - border-left: 1px solid rgba(34, 36, 38, 0.15); + border: 1px dashed rgba(34, 36, 38, 0.15); padding: 5px 0px 5px 10px; + margin-bottom: 5px; } .field > .field { @@ -45,12 +46,6 @@ column-gap: 5px; } -.fieldDeclaration { - display: grid; - grid-template-columns: auto 80px 80px; - column-gap: 5px; -} - .arrayButton { box-shadow: none !important; background-color: rgb(240, 240, 240) !important; @@ -59,3 +54,30 @@ .ui.form .field > label { margin-top: 2px; } + +.ui.form .field { + margin-bottom: 0; +} + +/** CSS Classes for use by the ModelBuilderVisitor */ +.mbFieldDeclaration { + display: grid; + grid-template-columns: auto 150px 80px 80px; + column-gap: 5px; +} + +.mbObjectDeclaration { + display: grid; + grid-template-columns: auto 150px 150px 80px 80px; + column-gap: 5px; +} + +.mbIdentifierDeclaration { + display: grid; + grid-template-columns: auto 150px; + column-gap: 5px; +} + +.mbFieldDeclarations { + padding-top: 5px; +} diff --git a/packages/ui-concerto/src/components/concertoForm.js b/packages/ui-concerto/src/components/concertoForm.js index 2201587a..191906aa 100644 --- a/packages/ui-concerto/src/components/concertoForm.js +++ b/packages/ui-concerto/src/components/concertoForm.js @@ -17,6 +17,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import get from 'lodash.get'; import set from 'lodash.set'; +import toPath from 'lodash.topath'; import { Form, Dimmer, Loader, Message } from 'semantic-ui-react'; import { ModelManager } from '@accordproject/concerto-core'; @@ -53,11 +54,11 @@ const ConcertoForm = (props) => { }, [onValueChange, value]); const addElement = useCallback((e, key, elementValue) => { - const array = get(value, key) || []; - const path = typeof key === 'string' ? [key] : key; + const pathKey = toPath(key); + const array = get(value, pathKey) || []; const valueClone = set( { ...value }, - [...path, array.length], + [...pathKey, array.length], elementValue ); setValue(valueClone); @@ -144,9 +145,9 @@ const ConcertoForm = (props) => { if (loading) { return ( - - Loading - + + Loading + ); } @@ -154,42 +155,42 @@ const ConcertoForm = (props) => { try { const form = generator.generateHTML(props.type, value); return ( -
- {form} -
+
+ {form} +
); } catch (err) { console.error(err); return ( - - - An error occured while generating this form - -
{err.message}
-
+ + + An error occured while generating this form + +
{err.message}
+
); } } if (!props.type) { return ( - - No model type specified -

- Please specify a model type to display the form. -

-
- ); - } - - return ( - Invalid JSON instance provided + No model type specified

- The JSON value does not match the model type associated with this - form. + Please specify a model type to display the form.

+ ); + } + + return ( + + Invalid JSON instance provided +

+ The JSON value does not match the model type associated with this + form. +

+
); }; diff --git a/packages/ui-concerto/src/components/fields.js b/packages/ui-concerto/src/components/fields.js index b4b660ea..d06d9231 100644 --- a/packages/ui-concerto/src/components/fields.js +++ b/packages/ui-concerto/src/components/fields.js @@ -16,17 +16,7 @@ import React from 'react'; import { Relationship } from '@accordproject/concerto-core'; import { Checkbox, Input, Form, Button, Select, Popup, Label, Icon } from 'semantic-ui-react'; import { DateTimeInput } from 'semantic-ui-calendar-react'; -import { parseValue, normalizeLabel } from '../utilities'; - -export const applyDecoratorTitle = field => { - const decorator = field.getDecorator('FormEditor'); - let name = field.getName(); - if (decorator) { - const args = decorator.getArguments(); - name = args[1]; - } - return name; -}; +import { parseValue, normalizeLabel, applyDecoratorTitle } from '../utilities'; export const ConcertoLabel = ({ skip, name, htmlFor }) => !skip ? : null; @@ -39,11 +29,13 @@ export const ConcertoCheckbox = ({ value, onFieldValueChange, skipLabel, + toggle }) => ( } + trigger={} />} disabled={readOnly} className='arrayButton' @@ -214,7 +205,7 @@ export const ConcertoArrayElement = ({ content='Remove this element' position='left center' key={`array-popup-${id}`} - trigger={} + trigger={} />} aria-label={`Remove element ${index} from ${normalizeLabel(`${id}`)}`} className='arrayButton' diff --git a/packages/ui-concerto/src/modelBuilderVisitor.js b/packages/ui-concerto/src/modelBuilderVisitor.js index 01067d8a..5e42727d 100644 --- a/packages/ui-concerto/src/modelBuilderVisitor.js +++ b/packages/ui-concerto/src/modelBuilderVisitor.js @@ -14,7 +14,26 @@ */ import React from 'react'; +import get from 'lodash.get'; +import { Form } from 'semantic-ui-react'; import ReactFormVisitor from './reactformvisitor'; +import { + ConcertoDropdown, + ConcertoLabel, +} from './components/fields'; +import { + pathToString +} from './utilities'; + +const declarationTypes = [ + { value: 'concerto.metamodel.StringFieldDeclaration', text: 'Text' }, + { value: 'concerto.metamodel.IntegerFieldDeclaration', text: 'Whole Number' }, + { value: 'concerto.metamodel.BooleanFieldDeclaration', text: 'Boolean' }, + { value: 'concerto.metamodel.DateTimeFieldDeclaration', text: 'Date' }, + { value: 'concerto.metamodel.RealFieldDeclaration', text: 'Decimal' }, + { value: 'concerto.metamodel.ObjectFieldDeclaration', text: 'Object' }, + { value: 'concerto.metamodel.RelationshipDeclaration', text: 'Relationship' }, +]; /** * Convert the contents of a ModelManager to React compnents. @@ -29,34 +48,40 @@ class ModelBuilderVisitor extends ReactFormVisitor { * @private */ visitClassDeclaration(classDeclaration, parameters) { - if (classDeclaration.getFullyQualifiedName() === 'concerto.metamodel.AssetDeclaration') { - return this.visitAssetDeclaration(classDeclaration, parameters); + const fqn = classDeclaration.getFullyQualifiedName(); + if (fqn === 'concerto.metamodel.AssetDeclaration') { + return this.visitConceptDeclaration(classDeclaration, parameters); } - if (classDeclaration.getFullyQualifiedName() === 'concerto.metamodel.TypeIdentifier') { + if (fqn === 'concerto.metamodel.TypeIdentifier') { parameters.skipLabel = true; const component = super.visitClassDeclaration(classDeclaration, parameters); parameters.skipLabel = false; return component; } - if (classDeclaration.getFullyQualifiedName() === 'concerto.metamodel.BooleanFieldDeclaration') { + const declarationTypeNames = declarationTypes.map(({ value }) => value); + if (declarationTypeNames.includes(fqn)) { return this.visitFieldDeclaration(classDeclaration, parameters); } return super.visitClassDeclaration(classDeclaration, parameters); } - visitAssetDeclaration(declaration, parameters) { + visitConceptDeclaration(declaration, parameters) { const props = declaration.getProperties(); const identifier = props.find(({ name }) => name === 'identifier'); const superType = props.find(({ name }) => name === 'superType'); const fields = props.find(({ name }) => name === 'fields'); return
-
{identifier.accept(this, parameters)}
-
{superType.accept(this, parameters)}
-
{fields.accept(this, parameters)}
+
+
{identifier.accept(this, parameters)}
+
{superType.accept(this, parameters)}
+
+
+ {fields.accept(this, parameters)} +
; } @@ -66,15 +91,83 @@ class ModelBuilderVisitor extends ReactFormVisitor { const name = props.find(({ name }) => name === 'name'); const isOptional = props.find(({ name }) => name === 'isOptional'); const isArray = props.find(({ name }) => name === 'isArray'); + const type = props.find(({ name }) => name === 'type'); + + const key = pathToString(parameters.stack); + const value = get(parameters.json, key); + + const hasTypeProperty = name => [ + 'concerto.metamodel.ObjectFieldDeclaration', + 'concerto.metamodel.RelationshipDeclaration' + ].includes(name); + + // We need a special version of `onFieldValueChange` because changing the $class value + // requires us to regenerate the instance + const onFieldValueChange = (data, key) => { + const { value: newClassName } = data; + if (hasTypeProperty(newClassName) && !hasTypeProperty(value.$class)) { + console.log('adding type property'); + value.type = null; + } + + if (!hasTypeProperty(newClassName) && hasTypeProperty(value.$class)) { + console.log('removing type property'); + + value.type = undefined; + } + + return parameters.onFieldValueChange(data, key); + }; + + if (hasTypeProperty(declaration.getFullyQualifiedName())) { + // const typeField = new Field(declaration, ) + return ( +
+
{name.accept(this, parameters)}
+
{type.accept(this, parameters)}
+ + + ({ + key: `option-${value}`, + value, + text, + }))} + /> + +
{isArray.accept(this, parameters)}
+
{isOptional.accept(this, parameters)}
+
+ ); + } - const component = ( -
+ return ( +
{name.accept(this, parameters)}
+ + + ({ + key: `option-${value}`, + value, + text, + }))} + /> +
{isArray.accept(this, parameters)}
{isOptional.accept(this, parameters)}
); - return component; } } export default ModelBuilderVisitor; diff --git a/packages/ui-concerto/src/reactformvisitor.js b/packages/ui-concerto/src/reactformvisitor.js index 3347ba1a..f2c74a2d 100644 --- a/packages/ui-concerto/src/reactformvisitor.js +++ b/packages/ui-concerto/src/reactformvisitor.js @@ -42,28 +42,11 @@ import { findConcreteSubclass, getDefaultValue, toFieldType, + pathToString, + getCustomSelectDecoratorKey, + isHidden } from './utilities'; -const toPath = (paramsArr) => paramsArr.join('.'); - -/** - * Returns true if the field has the @FormEditor( "hide", true ) - * decorator - * @param {object} field the Concerto field - */ -function isHidden(field) { - const decorator = field.getDecorator('FormEditor'); - if (decorator) { - const args = decorator.getArguments(); - if (args.find((d, index) => d === 'hide' - && index < args[args.length - 1] && args[index + 1] === true)) { - return true; - } - } - - return false; -} - /** * Convert the contents of a ModelManager to React compnents. * @class @@ -205,7 +188,7 @@ class ReactFormVisitor { * @private */ visitEnumDeclaration(enumDeclaration, parameters) { - const key = toPath(parameters.stack); + const key = pathToString(parameters.stack); const value = get(parameters.json, key); return ( @@ -247,7 +230,7 @@ class ReactFormVisitor { } stack.push(field.getName()); - const key = toPath(stack); + const key = pathToString(stack); const value = get(parameters.json, key); let component = null; @@ -302,15 +285,45 @@ class ReactFormVisitor { * @private */ visitSingletonField(field, parameters, props) { - const key = toPath(parameters.stack); + const key = pathToString(parameters.stack); const value = get(parameters.json, key); if (field.isPrimitive()) { if (field.getType() === 'Boolean') { - return ; + const { checkboxStyle } = parameters; + return ; } if (toFieldType(field.getType()) === 'datetime-local') { return ; } + + // Allow the client application to override the definition of a basic input + // field with a Dropdown using their values + const customSelectorKey = getCustomSelectDecoratorKey(field); + if (customSelectorKey) { + const { customSelectors = {} } = parameters; + const options = customSelectors[customSelectorKey]; + if (!options) { + throw new Error(`Custom selector key '${customSelectorKey}' not found`); + } + return ({ + key: `option-${value}`, + value, + text, + }))} + />; + } return ; } let type = parameters.modelManager.getType( @@ -356,7 +369,7 @@ class ReactFormVisitor { } stack.push(relationship.getName()); - const key = toPath(stack); + const key = pathToString(stack); const value = get(parameters.json, key); const commonProps = { @@ -392,7 +405,7 @@ class ReactFormVisitor { {value && value.map((_element, index) => { stack.push(index); - const key = toPath(stack); + const key = pathToString(stack); const value = get(parameters.json, key); const arrayComponent = ( diff --git a/packages/ui-concerto/src/utilities.js b/packages/ui-concerto/src/utilities.js index 91162d86..5127a466 100644 --- a/packages/ui-concerto/src/utilities.js +++ b/packages/ui-concerto/src/utilities.js @@ -153,6 +153,7 @@ export const hideProperty = (property, parameters) => { * @private */ export const getDefaultValue = (field, parameters) => { + const { includeOptionalFields, includeSampleData } = parameters; if (field.isPrimitive()) { return convertToJSON(field); } @@ -161,8 +162,71 @@ export const getDefaultValue = (field, parameters) => { const resource = parameters.modelManager .getFactory() .newResource(type.getNamespace(), type.getName(), type.isIdentified() ? 'resource1' : null, { - includeOptionalFields: true, - generate: 'sample', + includeOptionalFields, + generate: includeSampleData, }); return parameters.modelManager.getSerializer().toJSON(resource); }; + +/** + * Convert an array path, e.g. ['a', 1, 'b'] to the path string e.g. 'a[1].b' + * @param {Array} array - the source array path + * @return {String} - A string representation of the path. + * @private + */ +export const pathToString = (array) => array.reduce((string, item) => { + const prefix = string === '' ? '' : '.'; + return string + (Number.isNaN(Number(item)) ? prefix + item : `[${item}]`); +}, ''); + +/** + * Substitutes the field name for a value in a decorator, @FormEditor( "title", "My Name" ) + * @param {object} field the Concerto field + * @private + */ +export const applyDecoratorTitle = field => { + const decorator = field.getDecorator('FormEditor'); + let name = field.getName(); + if (decorator) { + const args = decorator.getArguments(); + const index = args.findIndex(d => d === 'title'); + if (index >= 0 && index < args.length - 1) { + name = args[index + 1]; + } + } + return name; +}; + +/** + * Returns true if the field has the decorator @FormEditor( "hide", true ) + * @param {object} field the Concerto field + * @private + */ +export const isHidden = field => { + const decorator = field.getDecorator('FormEditor'); + if (decorator) { + const args = decorator.getArguments(); + if (args.find((d, index) => d === 'hide' + && index < args[args.length - 1] && args[index + 1] === true)) { + return true; + } + } + return false; +}; + +/** + * Returns the value of the decorator @FormEditor( "selectOptions", "key" ) + * @param {object} field the Concerto field + * @private + */ +export const getCustomSelectDecoratorKey = field => { + const decorator = field.getDecorator('FormEditor'); + if (decorator) { + const args = decorator.getArguments(); + const index = args.findIndex(d => d === 'selectOptions'); + if (index >= 0 && index < args.length - 1) { + return args[index + 1]; + } + } + return undefined; +}; From dd333e2e542d05dbdd4852432f7a2b4c9d09cba2 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Tue, 13 Jul 2021 16:22:39 +0100 Subject: [PATCH 07/10] feat(ModelBuilderVisitor): Add support for Object and Relationship TypeIdentifiers Signed-off-by: Matt Roberts --- .../src/stories/3-ConcertoForm.stories.js | 4 +- .../ui-concerto/src/modelBuilderVisitor.js | 100 ++++++++++-------- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/packages/storybook/src/stories/3-ConcertoForm.stories.js b/packages/storybook/src/stories/3-ConcertoForm.stories.js index 56a5fff1..d8cc5511 100644 --- a/packages/storybook/src/stories/3-ConcertoForm.stories.js +++ b/packages/storybook/src/stories/3-ConcertoForm.stories.js @@ -234,7 +234,7 @@ export const ModelBuilder = () => { concept ObjectFieldDeclaration extends FieldDeclaration { @FormEditor("hide", true) o StringDefault defaultValue optional - @FormEditor("selectOptions", "types") + @FormEditor("title", "typeIdentifier", "selectOptions", "types") o TypeIdentifier type } @@ -280,7 +280,7 @@ export const ModelBuilder = () => { } concept RelationshipDeclaration extends FieldDeclaration { - @FormEditor("selectOptions", "types") + @FormEditor("title", "typeIdentifier", "selectOptions", "types") o TypeIdentifier type } diff --git a/packages/ui-concerto/src/modelBuilderVisitor.js b/packages/ui-concerto/src/modelBuilderVisitor.js index 5e42727d..b3d7b697 100644 --- a/packages/ui-concerto/src/modelBuilderVisitor.js +++ b/packages/ui-concerto/src/modelBuilderVisitor.js @@ -50,7 +50,7 @@ class ModelBuilderVisitor extends ReactFormVisitor { visitClassDeclaration(classDeclaration, parameters) { const fqn = classDeclaration.getFullyQualifiedName(); if (fqn === 'concerto.metamodel.AssetDeclaration') { - return this.visitConceptDeclaration(classDeclaration, parameters); + return this.visitMetaConceptDeclaration(classDeclaration, parameters); } if (fqn === 'concerto.metamodel.TypeIdentifier') { @@ -62,13 +62,13 @@ class ModelBuilderVisitor extends ReactFormVisitor { const declarationTypeNames = declarationTypes.map(({ value }) => value); if (declarationTypeNames.includes(fqn)) { - return this.visitFieldDeclaration(classDeclaration, parameters); + return this.visitMetaFieldDeclaration(classDeclaration, parameters); } return super.visitClassDeclaration(classDeclaration, parameters); } - visitConceptDeclaration(declaration, parameters) { + visitMetaConceptDeclaration(declaration, parameters) { const props = declaration.getProperties(); const identifier = props.find(({ name }) => name === 'identifier'); const superType = props.find(({ name }) => name === 'superType'); @@ -85,13 +85,13 @@ class ModelBuilderVisitor extends ReactFormVisitor {
; } - visitFieldDeclaration(declaration, parameters) { + visitMetaFieldDeclaration(declaration, parameters) { const props = declaration.getProperties(); const name = props.find(({ name }) => name === 'name'); const isOptional = props.find(({ name }) => name === 'isOptional'); const isArray = props.find(({ name }) => name === 'isArray'); - const type = props.find(({ name }) => name === 'type'); + let type; const key = pathToString(parameters.stack); const value = get(parameters.json, key); @@ -101,59 +101,70 @@ class ModelBuilderVisitor extends ReactFormVisitor { 'concerto.metamodel.RelationshipDeclaration' ].includes(name); + // Create a new concept + if (value.$class) { + const tempParts = value.$class.split('.'); + const tempName = tempParts.pop(); + const concept = parameters.modelManager + .getFactory() + .newConcept(tempParts.join('.'), tempName, undefined, { + parameters: parameters.includeOptionalFields, + generate: parameters.includeSampleData, + }); + type = concept.getClassDeclaration().getProperties().find(({ name }) => name === 'type'); + } + // We need a special version of `onFieldValueChange` because changing the $class value // requires us to regenerate the instance - const onFieldValueChange = (data, key) => { + const onFieldValueChange = data => { const { value: newClassName } = data; - if (hasTypeProperty(newClassName) && !hasTypeProperty(value.$class)) { - console.log('adding type property'); - value.type = null; - } - if (!hasTypeProperty(newClassName) && hasTypeProperty(value.$class)) { - console.log('removing type property'); - - value.type = undefined; + // Add a new type property to the data + if (hasTypeProperty(newClassName) && !hasTypeProperty(value.$class)) { + const parts = newClassName.split('.'); + const name = parts.pop(); + + // Create a new concept + const concept = parameters.modelManager + .getFactory() + .newConcept(parts.join('.'), name, undefined, { + parameters: parameters.includeOptionalFields, + generate: parameters.includeSampleData, + }); + + // Keep any existing values + const json = { + ...parameters.modelManager.getSerializer().toJSON(concept), + name: value.name, + isOptional: value.isOptional, + isArray: value.isArray, + }; + value.type = json; + + parameters.onFieldValueChange({ + ...data, + value: json + }, key); + return; } - return parameters.onFieldValueChange(data, key); + // Remove any old type properties + value.type = undefined; + value.$class = newClassName; + parameters.onFieldValueChange({ + ...data, + value, + }, key); }; - if (hasTypeProperty(declaration.getFullyQualifiedName())) { - // const typeField = new Field(declaration, ) - return ( -
-
{name.accept(this, parameters)}
-
{type.accept(this, parameters)}
- - - ({ - key: `option-${value}`, - value, - text, - }))} - /> - -
{isArray.accept(this, parameters)}
-
{isOptional.accept(this, parameters)}
-
- ); - } - return ( -
+
{name.accept(this, parameters)}
+
{type && type.accept(this, parameters)}
{isArray.accept(this, parameters)}
{isOptional.accept(this, parameters)}
From 20d76b8156a1a877079065515bc6ee4b164ae7c4 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Wed, 14 Jul 2021 10:02:28 +0100 Subject: [PATCH 08/10] feat(concerto): Add regex validation and explicit subclass selection --- .../src/stories/3-ConcertoForm.stories.js | 208 +---------------- .../storybook/src/stories/concerto.models.js | 212 ++++++++++++++++++ packages/ui-concerto/src/components/fields.js | 18 +- .../ui-concerto/src/modelBuilderVisitor.js | 2 +- packages/ui-concerto/src/utilities.js | 18 ++ 5 files changed, 248 insertions(+), 210 deletions(-) create mode 100644 packages/storybook/src/stories/concerto.models.js diff --git a/packages/storybook/src/stories/3-ConcertoForm.stories.js b/packages/storybook/src/stories/3-ConcertoForm.stories.js index d8cc5511..1044b659 100644 --- a/packages/storybook/src/stories/3-ConcertoForm.stories.js +++ b/packages/storybook/src/stories/3-ConcertoForm.stories.js @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { action } from '@storybook/addon-actions'; import { text, boolean, object } from '@storybook/addon-knobs'; import { ConcertoForm, ModelBuilderVisitor } from '@accordproject/ui-concerto'; +import { ConcertoMetamodel, TestModel } from './concerto.models'; export default { title: 'Concerto Form', @@ -29,29 +30,7 @@ export const SimpleExample = () => { 'org.accordproject.cicero.contract.AccordContractState.stateId', ], }); - const model = text('Model', `namespace test - - enum Country { - o USA - o UK - o France - o Sweden - } - - participant Person identified by name { - o String name - o Address address - --> Person[] children optional - } - - concept Address { - o String street - o String city - @FormEditor( "hide", true) - o String zipCode - o Country country - } - `); + const model = text('Model', TestModel); const handleValueChange = (json) => { return action("value changed")(json); @@ -114,7 +93,6 @@ export const ModelBuilder = () => { const type = text('Type', 'concerto.metamodel.ModelFile'); const options = object('Options', { includeOptionalFields: false, - includeSampleData: 'sample', updateExternalModels: false, visitor: new ModelBuilderVisitor(), customSelectors: { @@ -124,187 +102,7 @@ export const ModelBuilder = () => { ] } }); - const model = text('Model', ` /* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - namespace concerto.metamodel - - /** - * The metadmodel for Concerto files - */ - - abstract concept DecoratorLiteral { - } - - concept DecoratorString extends DecoratorLiteral { - o String value - } - - concept DecoratorNumber extends DecoratorLiteral { - o Double value - } - - concept DecoratorBoolean extends DecoratorLiteral { - o Boolean value - } - - concept TypeIdentifier { - @FormEditor("selectOptions", "types") - o String fullyQualifiedName - } - - concept DecoratorIdentifier extends DecoratorLiteral { - o TypeIdentifier identifier - o Boolean isArray default=false - } - - concept Decorator { - o String name - o DecoratorLiteral[] arguments optional - } - - abstract concept ClassDeclaration { - @FormEditor("hide", true) - o Decorator[] decorators optional - o Boolean isAbstract default=false - @FormEditor("title", "name") - o String identifier - o String identifiedByField optional - @FormEditor("title", "parentType") - o TypeIdentifier superType optional - o BooleanFieldDeclaration[] fields - } - - concept AssetDeclaration extends ClassDeclaration { - } - - concept ParticipantDeclaration extends ClassDeclaration { - } - - concept TransactionDeclaration extends ClassDeclaration { - } - - concept EventDeclaration extends ClassDeclaration { - } - - concept ConceptDeclaration extends ClassDeclaration { - } - - // TODO - enums do not support abstract or super types - concept EnumDeclaration extends ClassDeclaration { - } - - concept StringDefault { - o String value - } - - concept BooleanDefault { - o Boolean value - } - - concept IntegerDefault { - o Integer value - } - - concept RealDefault { - o Double value - } - - abstract concept FieldDeclaration { - o String name - @FormEditor("title", "isArray?") - o Boolean isArray optional - @FormEditor("title", "isOptional?") - o Boolean isOptional optional - @FormEditor("hide", true) - o Decorator[] decorators optional - } - - concept ObjectFieldDeclaration extends FieldDeclaration { - @FormEditor("hide", true) - o StringDefault defaultValue optional - @FormEditor("title", "typeIdentifier", "selectOptions", "types") - o TypeIdentifier type - } - - concept BooleanFieldDeclaration extends FieldDeclaration { - @FormEditor("hide", true) - o BooleanDefault defaultValue optional - } - - concept DateTimeFieldDeclaration extends FieldDeclaration { - } - - concept StringFieldDeclaration extends FieldDeclaration { - @FormEditor("hide", true) - o StringDefault defaultValue optional - @FormEditor("hide", true) - o StringRegexValidator validator optional - } - - concept StringRegexValidator { - o String regex - } - - concept RealDomainValidator { - o Double lower optional - o Double upper optional - } - - concept IntegerDomainValidator { - o Integer lower optional - o Integer upper optional - } - - concept RealFieldDeclaration extends FieldDeclaration { - o RealDefault defaultValue optional - o RealDomainValidator validator optional - } - - concept IntegerFieldDeclaration extends FieldDeclaration { - @FormEditor("hide", true) - o IntegerDefault defaultValue optional - @FormEditor("hide", true) - o IntegerDomainValidator validator optional - } - - concept RelationshipDeclaration extends FieldDeclaration { - @FormEditor("title", "typeIdentifier", "selectOptions", "types") - o TypeIdentifier type - } - - abstract concept Import { - o String uri optional - } - - concept NamespaceImport extends Import { - o String namespace - } - - concept TypeImport extends Import { - o TypeIdentifier identifier - } - - concept ModelFile { - @FormEditor("hide", true) - o String namespace - @FormEditor("hide", true) - o Import[] imports optional - @FormEditor("title", "classes") - o ClassDeclaration[] declarations optional - } - `); + const model = text('Model', ConcertoMetamodel); const handleValueChange = (json) => { return action("value changed")(json); diff --git a/packages/storybook/src/stories/concerto.models.js b/packages/storybook/src/stories/concerto.models.js new file mode 100644 index 00000000..e128aa7c --- /dev/null +++ b/packages/storybook/src/stories/concerto.models.js @@ -0,0 +1,212 @@ +export const TestModel = `namespace test + +enum Country { + o USA + o UK + o France + o Sweden +} + +participant Person identified by name { + o String name + o Address address + --> Person[] children optional +} + +concept Address { + o String street + o String city + @FormEditor( "hide", true) + o String zipCode + o Country country +} +`; + +export const ConcertoMetamodel = `/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + namespace concerto.metamodel + + /** + * The metadmodel for Concerto files + */ + + abstract concept DecoratorLiteral { + } + + concept DecoratorString extends DecoratorLiteral { + o String value + } + + concept DecoratorNumber extends DecoratorLiteral { + o Double value + } + + concept DecoratorBoolean extends DecoratorLiteral { + o Boolean value + } + + concept TypeIdentifier { + @FormEditor("selectOptions", "types") + o String fullyQualifiedName + } + + concept DecoratorIdentifier extends DecoratorLiteral { + o TypeIdentifier identifier + o Boolean isArray default=false + } + + concept Decorator { + o String name + o DecoratorLiteral[] arguments optional + } + + @FormEditor("defaultSubclass","concerto.metamodel.ConceptDeclaration") + abstract concept ClassDeclaration { + @FormEditor("hide", true) + o Decorator[] decorators optional + o Boolean isAbstract default=false + // TODO use regex /^(?!null|true|false)(\\p{Lu}|\\p{Ll}|\\p{Lt}|\\p{Lm}|\\p{Lo}|\\p{Nl}|\\$|_|\\\\u[0-9A-Fa-f]{4})(?:\\p{Lu}|\\p{Ll}|\\p{Lt}|\\p{Lm}|\\p{Lo}|\\p{Nl}|\\$|_|\\\\u[0-9A-Fa-f]{4}|\\p{Mn}|\\p{Mc}|\\p{Nd}|\\p{Pc}|\\u200C|\\u200D)*/u + @FormEditor("title", "name") + o String identifier default="className" regex=/^(?!null|true|false)(\\w|\\d|\\$|_|\\\\u[0-9A-Fa-f]{4})(?:\\w|\\d|\\$|_|\\\\u[0-9A-Fa-f]{4}|\\S|\\u200C|\\u200D)*$/ + o String identifiedByField optional + @FormEditor("title", "parentType") + o TypeIdentifier superType optional + o BooleanFieldDeclaration[] fields + } + + concept AssetDeclaration extends ClassDeclaration { + } + + concept ParticipantDeclaration extends ClassDeclaration { + } + + concept TransactionDeclaration extends ClassDeclaration { + } + + concept EventDeclaration extends ClassDeclaration { + } + + concept ConceptDeclaration extends ClassDeclaration { + } + + // TODO - enums do not support abstract or super types + concept EnumDeclaration extends ClassDeclaration { + } + + concept StringDefault { + o String value + } + + concept BooleanDefault { + o Boolean value + } + + concept IntegerDefault { + o Integer value + } + + concept RealDefault { + o Double value + } + + // TODO this decorator doesn't work because Concerto's version of 'findConcreteSubclass' doesn't support it yet. + @FormEditor("defaultSubclass","concerto.metamodel.StringFieldDeclaration") + abstract concept FieldDeclaration { + // TODO Allow regex modifiers e.g. //ui + // regex /^(?!null|true|false)(\\p{Lu}|\\p{Ll}|\\p{Lt}|\\pLm}|\\p{Lo}|\\p{Nl}|\\$|_|\\\\u[0-9A-Fa-f]{4})(?:\\p{Lu}|\\p{Ll}|\\p{Lt}|\\p{Lm}|\\p{Lo}|\\p{Nl}|\\$|_|\\\\u[0-9A-Fa-f]{4}|\\p{Mn}|\\p{Mc}|\\p{Nd}|\\p{Pc}|\\u200C|\\u200D)*/u + // This regex is an approximation of what the parser accepts without using unicode character classes + o String name default="fieldName" regex=/^(?!null|true|false)(\\w|\\d|\\$|_|\\\\u[0-9A-Fa-f]{4})(?:\\w|\\d|\\$|_|\\\\u[0-9A-Fa-f]{4}|\\S|\\u200C|\\u200D)*$/ + @FormEditor("title", "isArray?") + o Boolean isArray optional + @FormEditor("title", "isOptional?") + o Boolean isOptional optional + @FormEditor("hide", true) + o Decorator[] decorators optional + } + + concept ObjectFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o StringDefault defaultValue optional + @FormEditor("title", "typeIdentifier", "selectOptions", "types") + o TypeIdentifier type + } + + concept BooleanFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o BooleanDefault defaultValue optional + } + + concept DateTimeFieldDeclaration extends FieldDeclaration { + } + + concept StringFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o StringDefault defaultValue optional + @FormEditor("hide", true) + o StringRegexValidator validator optional + } + + concept StringRegexValidator { + o String regex + } + + concept RealDomainValidator { + o Double lower optional + o Double upper optional + } + + concept IntegerDomainValidator { + o Integer lower optional + o Integer upper optional + } + + concept RealFieldDeclaration extends FieldDeclaration { + o RealDefault defaultValue optional + o RealDomainValidator validator optional + } + + concept IntegerFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o IntegerDefault defaultValue optional + @FormEditor("hide", true) + o IntegerDomainValidator validator optional + } + + concept RelationshipDeclaration extends FieldDeclaration { + @FormEditor("title", "typeIdentifier", "selectOptions", "types") + o TypeIdentifier type + } + + abstract concept Import { + o String uri optional + } + + concept NamespaceImport extends Import { + o String namespace + } + + concept TypeImport extends Import { + o TypeIdentifier identifier + } + + concept ModelFile { + @FormEditor("hide", true) + o String namespace + @FormEditor("hide", true) + o Import[] imports optional + @FormEditor("title", "classes") + o ClassDeclaration[] declarations optional + } +`; \ No newline at end of file diff --git a/packages/ui-concerto/src/components/fields.js b/packages/ui-concerto/src/components/fields.js index d06d9231..c2990706 100644 --- a/packages/ui-concerto/src/components/fields.js +++ b/packages/ui-concerto/src/components/fields.js @@ -54,8 +54,18 @@ export const ConcertoInput = ({ onFieldValueChange, skipLabel, type, -}) => ( - +}) => { + let error; + const validator = field.getValidator(); + if (validator) { + try { + validator.validate(id, value); + } catch (validationError) { + error = true; + console.warn(validationError.message); + } + } + return - -); + ; +}; export const ConcertoRelationship = ({ id, diff --git a/packages/ui-concerto/src/modelBuilderVisitor.js b/packages/ui-concerto/src/modelBuilderVisitor.js index b3d7b697..7e405ad9 100644 --- a/packages/ui-concerto/src/modelBuilderVisitor.js +++ b/packages/ui-concerto/src/modelBuilderVisitor.js @@ -49,7 +49,7 @@ class ModelBuilderVisitor extends ReactFormVisitor { */ visitClassDeclaration(classDeclaration, parameters) { const fqn = classDeclaration.getFullyQualifiedName(); - if (fqn === 'concerto.metamodel.AssetDeclaration') { + if (fqn === 'concerto.metamodel.ConceptDeclaration') { return this.visitMetaConceptDeclaration(classDeclaration, parameters); } diff --git a/packages/ui-concerto/src/utilities.js b/packages/ui-concerto/src/utilities.js index 5127a466..3414beb3 100644 --- a/packages/ui-concerto/src/utilities.js +++ b/packages/ui-concerto/src/utilities.js @@ -116,6 +116,24 @@ export const findConcreteSubclass = declaration => { throw new Error('No concrete subclasses found'); } + // Allow the model to specify an explicit default sub-class, + // e.g. @FormEditor("defaultSubclass", "concerto.metadata.ConceptDeclaration") + const decorator = declaration.getDecorator('FormEditor'); + let explicitSubclassName; + if (decorator) { + const args = decorator.getArguments(); + const index = args.findIndex(d => d === 'defaultSubclass'); + if (index >= 0 && index < args.length - 1) { + explicitSubclassName = args[index + 1]; + } + } + + const explicitSubclass = concreteSubclasses + .find(c => c.getFullyQualifiedName() === explicitSubclassName); + if (explicitSubclass) { + return explicitSubclass; + } + return concreteSubclasses[0]; }; From b7c53bf973398dec72c5ca74d21b6bbc147eca05 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Wed, 14 Jul 2021 12:03:07 +0100 Subject: [PATCH 09/10] fix(concerto): Update ClassDeclaration fields property Signed-off-by: Matt Roberts --- packages/storybook/src/stories/concerto.models.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/storybook/src/stories/concerto.models.js b/packages/storybook/src/stories/concerto.models.js index e128aa7c..125faceb 100644 --- a/packages/storybook/src/stories/concerto.models.js +++ b/packages/storybook/src/stories/concerto.models.js @@ -83,7 +83,7 @@ export const ConcertoMetamodel = `/* o String identifiedByField optional @FormEditor("title", "parentType") o TypeIdentifier superType optional - o BooleanFieldDeclaration[] fields + o FieldDeclaration[] fields } concept AssetDeclaration extends ClassDeclaration { @@ -121,7 +121,6 @@ export const ConcertoMetamodel = `/* o Double value } - // TODO this decorator doesn't work because Concerto's version of 'findConcreteSubclass' doesn't support it yet. @FormEditor("defaultSubclass","concerto.metamodel.StringFieldDeclaration") abstract concept FieldDeclaration { // TODO Allow regex modifiers e.g. //ui From 451d20b55f6f3fe4083bf1941d9d33e24d0a3290 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Wed, 14 Jul 2021 14:20:09 +0100 Subject: [PATCH 10/10] feat(concerto): Make dropdown clearable, unhide namespace Signed-off-by: Matt Roberts --- packages/storybook/src/stories/concerto.models.js | 1 - packages/ui-concerto/src/components/fields.js | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/storybook/src/stories/concerto.models.js b/packages/storybook/src/stories/concerto.models.js index 125faceb..3cb78875 100644 --- a/packages/storybook/src/stories/concerto.models.js +++ b/packages/storybook/src/stories/concerto.models.js @@ -201,7 +201,6 @@ export const ConcertoMetamodel = `/* } concept ModelFile { - @FormEditor("hide", true) o String namespace @FormEditor("hide", true) o Import[] imports optional diff --git a/packages/ui-concerto/src/components/fields.js b/packages/ui-concerto/src/components/fields.js index c2990706..0b7541e0 100644 --- a/packages/ui-concerto/src/components/fields.js +++ b/packages/ui-concerto/src/components/fields.js @@ -237,6 +237,7 @@ export const ConcertoDropdown = ({ options, }) => !readOnly ? (