diff --git a/packages/ui-concerto/src/index.js b/packages/ui-concerto/src/index.js
index 3565d0a..f2082e3 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 0000000..7e405ad
--- /dev/null
+++ b/packages/ui-concerto/src/modelBuilderVisitor.js
@@ -0,0 +1,185 @@
+/* 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 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.
+ * @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) {
+ const fqn = classDeclaration.getFullyQualifiedName();
+ if (fqn === 'concerto.metamodel.ConceptDeclaration') {
+ return this.visitMetaConceptDeclaration(classDeclaration, parameters);
+ }
+
+ if (fqn === 'concerto.metamodel.TypeIdentifier') {
+ parameters.skipLabel = true;
+ const component = super.visitClassDeclaration(classDeclaration, parameters);
+ parameters.skipLabel = false;
+ return component;
+ }
+
+ const declarationTypeNames = declarationTypes.map(({ value }) => value);
+ if (declarationTypeNames.includes(fqn)) {
+ return this.visitMetaFieldDeclaration(classDeclaration, parameters);
+ }
+
+ return super.visitClassDeclaration(classDeclaration, parameters);
+ }
+
+ visitMetaConceptDeclaration(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)}
+
+
;
+ }
+
+ 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');
+ let type;
+
+ const key = pathToString(parameters.stack);
+ const value = get(parameters.json, key);
+
+ const hasTypeProperty = name => [
+ 'concerto.metamodel.ObjectFieldDeclaration',
+ '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 => {
+ const { value: newClassName } = data;
+
+ // 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;
+ }
+
+ // Remove any old type properties
+ value.type = undefined;
+ value.$class = newClassName;
+ parameters.onFieldValueChange({
+ ...data,
+ value,
+ }, key);
+ };
+
+ return (
+
+
{name.accept(this, parameters)}
+
+
+ ({
+ key: `option-${value}`,
+ value,
+ text,
+ }))}
+ />
+
+
{type && type.accept(this, parameters)}
+
{isArray.accept(this, parameters)}
+
{isOptional.accept(this, parameters)}
+
+ );
+ }
+}
+export default ModelBuilderVisitor;
diff --git a/packages/ui-concerto/src/reactformvisitor.js b/packages/ui-concerto/src/reactformvisitor.js
index dbf9d95..f2c74a2 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,24 +285,62 @@ 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(
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)}
);
@@ -348,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 = {
@@ -384,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 = (
@@ -397,7 +418,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 91162d8..3414beb 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];
};
@@ -153,6 +171,7 @@ export const hideProperty = (property, parameters) => {
* @private
*/
export const getDefaultValue = (field, parameters) => {
+ const { includeOptionalFields, includeSampleData } = parameters;
if (field.isPrimitive()) {
return convertToJSON(field);
}
@@ -161,8 +180,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;
+};