diff --git a/lib/js/liform-react/.gitignore b/lib/js/liform-react/.gitignore new file mode 100644 index 000000000..62b82089f --- /dev/null +++ b/lib/js/liform-react/.gitignore @@ -0,0 +1,12 @@ +node_modules +npm-debug.log +dist +lib +es +.DS_Store +yarn.lock +.nyc_output/ +coverage/ +examples/bundle* +package-lock.json +built_docs/ diff --git a/lib/js/liform-react/package.json b/lib/js/liform-react/package.json new file mode 100644 index 000000000..2fe35d4bd --- /dev/null +++ b/lib/js/liform-react/package.json @@ -0,0 +1,37 @@ +{ + "name": "@alchemy/liform-react", + "version": "1.0.0", + "description": "Generate forms from json-schema to use with React (and redux-form)", + "main": "./src/index.jsx", + "scripts": {}, + "keywords": [ + "react", + "json-schema", + "form", + "redux-form" + ], + "repository": { + "type": "git", + "url": "https://github.com/limenius/liform-react.git" + }, + "author": "Nacho Martin", + "license": "MIT", + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "dependencies": { + "ajv": "^8.12.0", + "classnames": "^2.2.5", + "deepmerge": "^2.0.1", + "lodash": "^4.17.21", + "prop-types": "^15.5.10", + "react-redux": "^9.0.4", + "redux": "^4.2.1", + "redux-form": "^8.3.10" + }, + "devDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/lib/js/liform-react/src/Form.jsx b/lib/js/liform-react/src/Form.jsx new file mode 100644 index 000000000..e69de29bb diff --git a/lib/js/liform-react/src/buildSyncValidation.js b/lib/js/liform-react/src/buildSyncValidation.js new file mode 100644 index 000000000..187d3f6d0 --- /dev/null +++ b/lib/js/liform-react/src/buildSyncValidation.js @@ -0,0 +1,84 @@ +import Ajv from "ajv"; +import merge from "deepmerge"; +import { set as _set } from "lodash"; + +const setError = (error, schema) => { + // convert property accessor (.xxx[].xxx) notation to jsonPointers notation + if (error.instancePath.charAt(0) === ".") { + error.instancePath = error.instancePath.replace(/[.[]/gi, "/"); + error.instancePath = error.instancePath.replace(/[\]]/gi, ""); + } + const instancePathParts = error.instancePath.split("/").slice(1); + let instancePath = error.instancePath.slice(1).replace(/\//g, "."); + const type = findTypeInSchema(schema, instancePathParts); + + let errorToSet; + if (type === "array" || type === "allOf" || type === "oneOf") { + errorToSet = { _error: error.message }; + } else { + errorToSet = error.message; + } + + let errors = {}; + _set(errors, instancePath, errorToSet); + return errors; +}; + +const findTypeInSchema = (schema, instancePath) => { + if (!schema) { + return; + } else if (instancePath.length === 0 && schema.hasOwnProperty("type")) { + return schema.type; + } else { + if (schema.type === "array") { + return findTypeInSchema(schema.items, instancePath.slice(1)); + } else if (schema.hasOwnProperty("allOf")) { + if (instancePath.length === 0) return "allOf"; + schema = { ...schema, ...merge.all(schema.allOf) }; + delete schema.allOf; + return findTypeInSchema(schema, instancePath); + } else if (schema.hasOwnProperty("oneOf")) { + if (instancePath.length === 0) return "oneOf"; + schema.oneOf.forEach(item => { + let type = findTypeInSchema(item, instancePath); + if (type) { + return type; + } + }); + } else { + return findTypeInSchema( + schema.properties[instancePath[0]], + instancePath.slice(1) + ); + } + } +}; + +const buildSyncValidation = (schema, ajvParam = null) => { + let ajv = ajvParam; + if (ajv === null) { + ajv = new Ajv({ + allErrors: true, + strict: false + }); + } + return values => { + const valid = ajv.validate(schema, values); + if (valid) { + return {}; + } + const ajvErrors = ajv.errors; + + let errors = ajvErrors.map(error => { + return setError(error, schema); + }); + // We need at least two elements + errors.push({}); + errors.push({}); + return merge.all(errors); + }; +}; + +export default buildSyncValidation; + +export { setError }; diff --git a/lib/js/liform-react/src/compileSchema.js b/lib/js/liform-react/src/compileSchema.js new file mode 100644 index 000000000..89becd4d1 --- /dev/null +++ b/lib/js/liform-react/src/compileSchema.js @@ -0,0 +1,44 @@ +function isObject(thing) { + return typeof thing === "object" && thing !== null && !Array.isArray(thing); +} + +function compileSchema(schema, root) { + if (!root) { + root = schema; + } + let newSchema; + + if (isObject(schema)) { + newSchema = {}; + for (let i in schema) { + if (schema.hasOwnProperty(i)) { + if (i === "$ref") { + newSchema = compileSchema(resolveRef(schema[i], root), root); + } else { + newSchema[i] = compileSchema(schema[i], root); + } + } + } + return newSchema; + } + + if (Array.isArray(schema)) { + newSchema = []; + for (let i = 0; i < schema.length; i += 1) { + newSchema[i] = compileSchema(schema[i], root); + } + return newSchema; + } + + return schema; +} + +function resolveRef(uri, schema) { + uri = uri.replace("#/", ""); + const tokens = uri.split("/"); + const tip = tokens.reduce((obj, token) => obj[token], schema); + + return tip; +} + +export default compileSchema; diff --git a/lib/js/liform-react/src/index.jsx b/lib/js/liform-react/src/index.jsx new file mode 100644 index 000000000..c05d4a7bd --- /dev/null +++ b/lib/js/liform-react/src/index.jsx @@ -0,0 +1,68 @@ +import React from "react"; +import PropTypes from "prop-types"; +import DefaultTheme from "./themes/bootstrap3"; +import { reduxForm } from "redux-form"; +import renderFields from "./renderFields"; +import renderField from "./renderField"; +import processSubmitErrors from "./processSubmitErrors"; +import buildSyncValidation from "./buildSyncValidation"; +import { setError } from "./buildSyncValidation"; +import compileSchema from "./compileSchema"; + +const BaseForm = props => { + const { schema, handleSubmit, theme, error, submitting, context } = props; + return ( +
+ {renderField(schema, null, theme || DefaultTheme, "", context)} +
{error && {error}}
+ +
+ ); +}; + +const Liform = props => { + const schema = compileSchema(props.schema); + props.schema.showLabel = false; + const schemaWithOptions = compileSchema(props.schema); + const formName = props.formKey || props.schema.title || "form"; + + const FinalForm = reduxForm({ + form: props.formKey || props.schema.title || "form", + validate: props.syncValidation || buildSyncValidation(schema, props.ajv), + initialValues: props.initialValues, + context: { ...props.context, formName } + })(props.baseForm || BaseForm); + + return ( + + ); +}; + +Liform.propTypes = { + schema: PropTypes.object, + onSubmit: PropTypes.func, + initialValues: PropTypes.object, + syncValidation: PropTypes.func, + formKey: PropTypes.string, + baseForm: PropTypes.func, + context: PropTypes.object, + ajv: PropTypes.object +}; + +export default Liform; + +export { + renderFields, + renderField, + processSubmitErrors, + DefaultTheme, + setError, + buildSyncValidation, + compileSchema, +}; diff --git a/lib/js/liform-react/src/processSubmitErrors.js b/lib/js/liform-react/src/processSubmitErrors.js new file mode 100644 index 000000000..74f0ab95d --- /dev/null +++ b/lib/js/liform-react/src/processSubmitErrors.js @@ -0,0 +1,40 @@ +import { SubmissionError } from "redux-form"; +import { isEmpty as _isEmpty } from "lodash"; // added for empty check + +const convertToReduxFormErrors = obj => { + let objectWithoutChildrenAndFalseErrors = {}; + Object.keys(obj).map(name => { + if (name === "children") { + objectWithoutChildrenAndFalseErrors = { + ...objectWithoutChildrenAndFalseErrors, + ...convertToReduxFormErrors(obj[name]) + }; + } else { + if (obj[name].hasOwnProperty("children")) { + // if children, take field from it and set them directly as own field + objectWithoutChildrenAndFalseErrors[name] = convertToReduxFormErrors( + obj[name] + ); + } else { + if ( + obj[name].hasOwnProperty("errors") && + !_isEmpty(obj[name]["errors"]) + ) { + // using lodash for empty error check, dont add them if empty + objectWithoutChildrenAndFalseErrors[name] = obj[name]["errors"]; + } + } + } + return null; + }); + return objectWithoutChildrenAndFalseErrors; +}; + +const processSubmitErrors = errors => { + if (errors.hasOwnProperty("errors")) { + errors = convertToReduxFormErrors(errors.errors); + throw new SubmissionError(errors); + } +}; + +export default processSubmitErrors; diff --git a/lib/js/liform-react/src/renderField.js b/lib/js/liform-react/src/renderField.js new file mode 100644 index 000000000..2e531301d --- /dev/null +++ b/lib/js/liform-react/src/renderField.js @@ -0,0 +1,51 @@ +import React from "react"; +import deepmerge from "deepmerge"; + +const guessWidget = (fieldSchema, theme) => { + if (fieldSchema.widget) { + return fieldSchema.widget; + } else if (fieldSchema.hasOwnProperty("enum")) { + return "choice"; + } else if (fieldSchema.hasOwnProperty("oneOf")) { + return "oneOf"; + } else if (theme[fieldSchema.format]) { + return fieldSchema.format; + } + return fieldSchema.type || "object"; +}; + +const renderField = ( + fieldSchema, + fieldName, + theme, + prefix = "", + context = {}, + required = false +) => { + if (fieldSchema.hasOwnProperty("allOf")) { + fieldSchema = { ...fieldSchema, ...deepmerge.all(fieldSchema.allOf) }; + delete fieldSchema.allOf; + } + + const widget = guessWidget(fieldSchema, theme); + + if (!theme[widget]) { + throw new Error("liform: " + widget + " is not defined in the theme"); + } + + const newFieldName = prefix ? prefix + fieldName : fieldName; + + return React.createElement(theme[widget], { + key: fieldName, + fieldName: widget === "oneOf" ? fieldName : newFieldName, + label: + fieldSchema.showLabel === false ? "" : fieldSchema.title || fieldName, + required: required, + schema: fieldSchema, + theme, + context, + prefix + }); +}; + +export default renderField; diff --git a/lib/js/liform-react/src/renderFields.js b/lib/js/liform-react/src/renderFields.js new file mode 100644 index 000000000..ee25342fb --- /dev/null +++ b/lib/js/liform-react/src/renderFields.js @@ -0,0 +1,38 @@ +import renderField from "./renderField"; + +export const isRequired = (schema, fieldName) => { + if (!schema.required) { + return false; + } + return schema.required.indexOf(fieldName) !== -1; +}; + +const renderFields = (schema, theme, prefix = null, context = {}) => { + let props = []; + for (let i in schema.properties) { + props.push({ prop: i, propertyOrder: schema.properties[i].propertyOrder }); + } + props = props.sort((a, b) => { + if (a.propertyOrder > b.propertyOrder) { + return 1; + } else if (a.propertyOrder < b.propertyOrder) { + return -1; + } else { + return 0; + } + }); + return props.map(item => { + const name = item.prop; + const field = schema.properties[name]; + return renderField( + field, + name, + theme, + prefix, + context, + isRequired(schema, name) + ); + }); +}; + +export default renderFields; diff --git a/lib/js/liform-react/src/themes/bootstrap3/ArrayWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ArrayWidget.jsx new file mode 100644 index 000000000..6e2afd87a --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/ArrayWidget.jsx @@ -0,0 +1,152 @@ +import React from "react"; +import PropTypes from "prop-types"; +import renderField from "../../renderField"; +import { FieldArray } from "redux-form"; +import { times as _times} from "lodash"; +import ChoiceWidget from "./ChoiceWidget"; +import classNames from "classnames"; + +const renderArrayFields = ( + count, + schema, + theme, + fieldName, + remove, + context, + swap +) => { + const prefix = fieldName + "."; + if (count) { + return _times(count, idx => { + return ( +
+
+ {idx !== count - 1 && count > 1 ? ( + + ) : ( + "" + )} + {idx !== 0 && count > 1 ? ( + + ) : ( + "" + )} + + +
+ {renderField( + { ...schema, showLabel: false }, + idx.toString(), + theme, + prefix, + context + )} +
+ ); + }); + } else { + return null; + } +}; + +const renderInput = field => { + const className = classNames([ + "arrayType", + { "has-error": field.meta.submitFailed && field.meta.error } + ]); + + return ( +
+ {field.label} + {field.meta.submitFailed && + field.meta.error && ( + {field.meta.error} + )} + {renderArrayFields( + field.fields.length, + field.schema.items, + field.theme, + field.fieldName, + idx => field.fields.remove(idx), + field.context, + (a, b) => { + field.fields.swap(a, b); + } + )} + +
+
+ ); +}; + +const CollectionWidget = props => { + return ( + + ); +}; + +const ArrayWidget = props => { + // Arrays are tricky because they can be multiselects or collections + if ( + props.schema.items.hasOwnProperty("enum") && + props.schema.hasOwnProperty("uniqueItems") && + props.schema.uniqueItems + ) { + return ChoiceWidget({ + ...props, + schema: props.schema.items, + multiple: true + }); + } else { + return CollectionWidget(props); + } +}; + +ArrayWidget.propTypes = { + schema: PropTypes.object.isRequired, + fieldName: PropTypes.string, + label: PropTypes.string, + theme: PropTypes.object, + context: PropTypes.object +}; + +export default ArrayWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/BaseInputWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/BaseInputWidget.jsx new file mode 100644 index 000000000..fd7861409 --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/BaseInputWidget.jsx @@ -0,0 +1,59 @@ +import React from "react"; +import PropTypes from "prop-types"; +import classNames from "classnames"; +import { Field } from "redux-form"; + +const renderInput = field => { + const className = classNames([ + "form-group", + { "has-error": field.meta.touched && field.meta.error } + ]); + return ( +
+ + + {field.meta.touched && + field.meta.error && ( + {field.meta.error} + )} + {field.description && ( + {field.description} + )} +
+ ); +}; + +const BaseInputWidget = props => { + return ( + + ); +}; + +BaseInputWidget.propTypes = { + schema: PropTypes.object.isRequired, + type: PropTypes.string.isRequired, + required: PropTypes.bool, + fieldName: PropTypes.string, + label: PropTypes.string, + normalizer: PropTypes.func +}; + +export default BaseInputWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/CheckboxWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/CheckboxWidget.jsx new file mode 100644 index 000000000..aadcf10cc --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/CheckboxWidget.jsx @@ -0,0 +1,56 @@ +import React from "react"; +import PropTypes from "prop-types"; +import classNames from "classnames"; +import { Field } from "redux-form"; + +const renderInput = field => { + const className = classNames([ + "form-group", + { "has-error": field.meta.touched && field.meta.error } + ]); + return ( +
+
+ +
+ {field.meta.touched && + field.meta.error && ( + {field.meta.error} + )} + {field.description && ( + {field.description} + )} +
+ ); +}; + +const CheckboxWidget = props => { + return ( + + ); +}; + +CheckboxWidget.propTypes = { + schema: PropTypes.object.isRequired, + fieldName: PropTypes.string, + label: PropTypes.string, + theme: PropTypes.object +}; + +export default CheckboxWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/ChoiceExpandedWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ChoiceExpandedWidget.jsx new file mode 100644 index 000000000..71b776036 --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/ChoiceExpandedWidget.jsx @@ -0,0 +1,67 @@ +import React from "react"; +import classNames from "classnames"; +import { Field } from "redux-form"; + +const zipObject = (props, values) => + props.reduce( + (prev, prop, i) => Object.assign(prev, { [prop]: values[i] }), + {} + ); + +const renderChoice = field => { + const className = classNames([ + "form-group", + { "has-error": field.meta.touched && field.meta.error } + ]); + const options = field.schema.enum; + const optionNames = field.schema.enum_titles || options; + + const selectOptions = zipObject(options, optionNames); + return ( +
+ + {Object.entries(selectOptions).map(([value, name]) => ( +
+ +
+ ))} + + {field.meta.touched && + field.meta.error && ( + {field.meta.error} + )} + {field.description && ( + {field.description} + )} +
+ ); +}; + +const ChoiceExpandedWidget = props => { + return ( + + ); +}; + +export default ChoiceExpandedWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/ChoiceMultipleExpandedWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ChoiceMultipleExpandedWidget.jsx new file mode 100644 index 000000000..f4099150e --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/ChoiceMultipleExpandedWidget.jsx @@ -0,0 +1,84 @@ +import React from "react"; +import classNames from "classnames"; +import { Field } from "redux-form"; + +const zipObject = (props, values) => + props.reduce( + (prev, prop, i) => Object.assign(prev, { [prop]: values[i] }), + {} + ); + +const changeValue = (checked, item, onChange, currentValue = []) => { + if (checked) { + if (currentValue.indexOf(checked) === -1) { + return onChange([...currentValue, item]); + } + } else { + return onChange(currentValue.filter(items => it === item)); + } + return onChange(currentValue); +}; + +const renderChoice = field => { + const className = classNames([ + "form-group", + { "has-error": field.meta.touched && field.meta.error } + ]); + const options = field.schema.items.enum; + const optionNames = field.schema.items.enum_titles || options; + + const selectOptions = zipObject(options, optionNames); + return ( +
+ + {Object.entries(selectOptions).map(([value, name]) => ( +
+ +
+ ))} + + {field.meta.touched && + field.meta.error && ( + {field.meta.error} + )} + {field.description && ( + {field.description} + )} +
+ ); +}; + +const ChoiceMultipleExpandedWidget = props => { + return ( + + ); +}; + +export default ChoiceMultipleExpandedWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/ChoiceWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ChoiceWidget.jsx new file mode 100644 index 000000000..783c94f08 --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/ChoiceWidget.jsx @@ -0,0 +1,78 @@ +import React from "react"; +import PropTypes from "prop-types"; +import classNames from "classnames"; +import { Field } from "redux-form"; +import { zipObject as _zipObject, map as _map } from "lodash"; + +const renderSelect = field => { + const className = classNames([ + "form-group", + { "has-error": field.meta.touched && field.meta.error } + ]); + const options = field.schema.enum; + const optionNames = field.schema.enum_titles || options; + + const selectOptions = _zipObject(options, optionNames); + return ( +
+ + + + {field.meta.touched && + field.meta.error && ( + {field.meta.error} + )} + {field.description && ( + {field.description} + )} +
+ ); +}; + +const ChoiceWidget = props => { + return ( + + ); +}; + +ChoiceWidget.propTypes = { + schema: PropTypes.object.isRequired, + fieldName: PropTypes.string, + label: PropTypes.string, + theme: PropTypes.object, + multiple: PropTypes.bool, + required: PropTypes.bool +}; + +export default ChoiceWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/ColorWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ColorWidget.jsx new file mode 100644 index 000000000..8a3b5ea71 --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/ColorWidget.jsx @@ -0,0 +1,17 @@ +import React from "react"; +import PropTypes from "prop-types"; +import BaseInputWidget from "./BaseInputWidget"; + +const ColorWidget = props => { + return ; +}; + +BaseInputWidget.propTypes = { + schema: PropTypes.object.isRequired, + type: PropTypes.string.isRequired, + required: PropTypes.bool, + fieldName: PropTypes.string, + label: PropTypes.string +}; + +export default ColorWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateTimeWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateTimeWidget.jsx new file mode 100644 index 000000000..8fb85cf33 --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateTimeWidget.jsx @@ -0,0 +1,192 @@ +import React from "react"; +import classNames from "classnames"; +import { Field } from "redux-form"; +import DateSelector from "./DateSelector"; + +// produces an array [start..end-1] +const range = (start, end) => + Array.from({ length: end - start }, (v, k) => k + start); + +// produces an array [start..end-1] padded with zeros, (two digits) +const rangeZeroPad = (start, end) => + Array.from({ length: end - start }, (v, k) => ("0" + (k + start)).slice(-2)); + +const extractYear = value => { + return extractDateTimeToken(value, 0); +}; +const extractMonth = value => { + return extractDateTimeToken(value, 1); +}; +const extractDay = value => { + return extractDateTimeToken(value, 2); +}; +const extractHour = value => { + return extractDateTimeToken(value, 3); +}; +const extractMinute = value => { + return extractDateTimeToken(value, 4); +}; +const extractSecond = value => { + return extractDateTimeToken(value, 5); +}; + +const extractDateTimeToken = (value, index) => { + if (!value) { + return ""; + } + // Remove timezone Z + value = value.substring(0, value.length - 1); + const tokens = value.split(/[-T:]/); + if (tokens.length !== 6) { + return ""; + } + return tokens[index]; +}; + +class CompatibleDateTime extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + year: null, + month: null, + day: null, + hour: null, + minute: null, + second: null + }; + this.onBlur = this.onBlur.bind(this); + } + + // Produces a RFC 3339 full-date from the state + buildRfc3339Date() { + const year = this.state.year || ""; + const month = this.state.month || ""; + const day = this.state.day || ""; + return year + "-" + month + "-" + day; + } + + // Produces a RFC 3339 datetime from the state + buildRfc3339DateTime() { + const date = this.buildRfc3339Date(); + const hour = this.state.hour || ""; + const minute = this.state.minute || ""; + const second = this.state.second || ""; + return date + "T" + hour + ":" + minute + ":" + second + "Z"; + } + + onChangeField(field, e) { + const value = e.target.value; + let changeset = {}; + changeset[field] = value; + this.setState(changeset, () => { + this.props.input.onChange(this.buildRfc3339DateTime()); + }); + } + onBlur() { + this.props.input.onBlur(this.buildRfc3339DateTime()); + } + render() { + const field = this.props; + const className = classNames([ + "form-group", + { "has-error": field.meta.touched && field.meta.error } + ]); + return ( +
+ +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ {field.meta.touched && + field.meta.error && ( + {field.meta.error} + )} + {field.description && ( + {field.description} + )} +
+ ); + } +} +const CompatibleDateTimeWidget = props => { + return ( + + ); +}; + +export default CompatibleDateTimeWidget; + +// Only for testing purposes +export { extractDateTimeToken }; diff --git a/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateWidget.jsx new file mode 100644 index 000000000..ef329c0ac --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateWidget.jsx @@ -0,0 +1,144 @@ +import React from "react"; +import classNames from "classnames"; +import { Field } from "redux-form"; +import DateSelector from "./DateSelector"; + +// produces an array [start..end-1] +const range = (start, end) => + Array.from({ length: end - start }, (v, k) => k + start); + +// produces an array [start..end-1] padded with zeros, (two digits) +const rangeZeroPad = (start, end) => + Array.from({ length: end - start }, (v, k) => ("0" + (k + start)).slice(-2)); + +const extractYear = value => { + return extractDateToken(value, 0); +}; +const extractMonth = value => { + return extractDateToken(value, 1); +}; +const extractDay = value => { + return extractDateToken(value, 2); +}; + +const extractDateToken = (value, index) => { + if (!value) { + return ""; + } + const tokens = value.split(/-/); + if (tokens.length !== 3) { + return ""; + } + return tokens[index]; +}; + +class CompatibleDate extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + year: null, + month: null, + day: null, + hour: null, + minute: null, + second: null + }; + this.onBlur = this.onBlur.bind(this); + } + + // Produces a RFC 3339 full-date from the state + buildRfc3339Date() { + const year = this.state.year || ""; + const month = this.state.month || ""; + const day = this.state.day || ""; + return year + "-" + month + "-" + day; + } + + onChangeField(field, e) { + const value = e.target.value; + let changeset = {}; + changeset[field] = value; + this.setState(changeset, () => { + this.props.input.onChange(this.buildRfc3339Date()); + }); + } + + onBlur() { + this.props.input.onBlur(this.buildRfc3339Date()); + } + + render() { + const field = this.props; + const className = classNames([ + "form-group", + { "has-error": field.meta.touched && field.meta.error } + ]); + return ( +
+ +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ {field.meta.touched && + field.meta.error && ( + {field.meta.error} + )} + {field.description && ( + {field.description} + )} +
+ ); + } +} +const CompatibleDateWidget = props => { + return ( + + ); +}; + +export default CompatibleDateWidget; + +// Only for testing purposes +export { extractDateToken }; diff --git a/lib/js/liform-react/src/themes/bootstrap3/DateSelector.jsx b/lib/js/liform-react/src/themes/bootstrap3/DateSelector.jsx new file mode 100644 index 000000000..4b5227b6b --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/DateSelector.jsx @@ -0,0 +1,29 @@ +import React from "react"; + +const DateSelector = props => { + return ( + + ); +}; + +export default DateSelector; diff --git a/lib/js/liform-react/src/themes/bootstrap3/DateTimeWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/DateTimeWidget.jsx new file mode 100644 index 000000000..1833f8809 --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/DateTimeWidget.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import BaseInputWidget from "./BaseInputWidget"; + +const DateTimeWidget = props => { + return ; +}; + +export default DateTimeWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/DateWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/DateWidget.jsx new file mode 100644 index 000000000..bbc8bc69a --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/DateWidget.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import BaseInputWidget from "./BaseInputWidget"; + +const DateWidget = props => { + return ; +}; + +export default DateWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/EmailWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/EmailWidget.jsx new file mode 100644 index 000000000..5c3aac95a --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/EmailWidget.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import PropTypes from "prop-types"; +import BaseInputWidget from "./BaseInputWidget"; + +const EmailWidget = props => { + return ; +}; + +EmailWidget.propTypes = { + schema: PropTypes.object.isRequired, + fieldName: PropTypes.string, + label: PropTypes.string, + theme: PropTypes.object, + multiple: PropTypes.bool, + required: PropTypes.bool +}; + +export default EmailWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/FileWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/FileWidget.jsx new file mode 100644 index 000000000..28a4dd702 --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/FileWidget.jsx @@ -0,0 +1,62 @@ +import React from "react"; +import { Field } from "redux-form"; +import classNames from "classnames"; + +const processFile = (onChange, e) => { + const files = e.target.files; + return new Promise(() => { + let reader = new FileReader(); + reader.addEventListener( + "load", + () => { + onChange(reader.result); + }, + false + ); + reader.readAsDataURL(files[0]); + }); +}; + +const File = field => { + const className = classNames([ + "form-group", + { "has-error": field.meta.touched && field.meta.error } + ]); + return ( +
+ + + {field.meta.touched && + field.meta.error && ( + {field.meta.error} + )} + {field.description && {field.description}} +
+ ); +}; + +const FileWidget = props => { + return ( + + ); +}; + +export default FileWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/MoneyWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/MoneyWidget.jsx new file mode 100644 index 000000000..722c866f5 --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/MoneyWidget.jsx @@ -0,0 +1,61 @@ +import React from "react"; +import PropTypes from "prop-types"; +import classNames from "classnames"; +import { Field } from "redux-form"; + +const renderInput = field => { + const className = classNames([ + "form-group", + { "has-error": field.meta.touched && field.meta.error } + ]); + return ( +
+ +
+ + +
+ {field.meta.touched && + field.meta.error && ( + {field.meta.error} + )} + {field.description && ( + {field.description} + )} +
+ ); +}; + +const MoneyWidget = props => { + return ( + + ); +}; + +MoneyWidget.propTypes = { + schema: PropTypes.object.isRequired, + fieldName: PropTypes.string, + label: PropTypes.string, + theme: PropTypes.object, + multiple: PropTypes.bool, + required: PropTypes.bool +}; + +export default MoneyWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/NumberWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/NumberWidget.jsx new file mode 100644 index 000000000..00e02be95 --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/NumberWidget.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import BaseInputWidget from "./BaseInputWidget"; + +const NumberWidget = props => { + return ; +}; + +export default NumberWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/ObjectWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ObjectWidget.jsx new file mode 100644 index 000000000..deacdbac4 --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/ObjectWidget.jsx @@ -0,0 +1,27 @@ +import React from "react"; +import PropTypes from "prop-types"; +import renderFields from "../../renderFields"; + +const Widget = props => { + return ( +
+ {props.label && {props.label}} + {renderFields( + props.schema, + props.theme, + props.fieldName && props.fieldName + ".", + props.context + )} +
+ ); +}; + +Widget.propTypes = { + schema: PropTypes.object.isRequired, + fieldName: PropTypes.string, + label: PropTypes.string, + theme: PropTypes.object, + context: PropTypes.object +}; + +export default Widget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/PasswordWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/PasswordWidget.jsx new file mode 100644 index 000000000..456f2793e --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/PasswordWidget.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import BaseInputWidget from "./BaseInputWidget"; + +const PasswordWidget = props => { + return ; +}; + +export default PasswordWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/PercentWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/PercentWidget.jsx new file mode 100644 index 000000000..89707dd2a --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/PercentWidget.jsx @@ -0,0 +1,61 @@ +import React from "react"; +import PropTypes from "prop-types"; +import classNames from "classnames"; +import { Field } from "redux-form"; + +const renderInput = field => { + const className = classNames([ + "form-group", + { "has-error": field.meta.touched && field.meta.error } + ]); + return ( +
+ +
+ + % +
+ {field.meta.touched && + field.meta.error && ( + {field.meta.error} + )} + {field.description && ( + {field.description} + )} +
+ ); +}; + +const Widget = props => { + return ( + + ); +}; + +Widget.propTypes = { + schema: PropTypes.object.isRequired, + fieldName: PropTypes.string, + label: PropTypes.string, + theme: PropTypes.object, + multiple: PropTypes.bool, + required: PropTypes.bool +}; + +export default Widget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/SearchWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/SearchWidget.jsx new file mode 100644 index 000000000..8a93e3efa --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/SearchWidget.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import BaseInputWidget from "./BaseInputWidget"; + +const SearchWidget = props => { + return ; +}; + +export default SearchWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/StringWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/StringWidget.jsx new file mode 100644 index 000000000..ad84d401c --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/StringWidget.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import BaseInputWidget from "./BaseInputWidget"; + +const StringWidget = props => { + return ; +}; + +export default StringWidget; diff --git a/lib/js/liform-react/src/themes/bootstrap3/TextareaWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/TextareaWidget.jsx new file mode 100644 index 000000000..765b35243 --- /dev/null +++ b/lib/js/liform-react/src/themes/bootstrap3/TextareaWidget.jsx @@ -0,0 +1,57 @@ +import React from "react"; +import PropTypes from "prop-types"; +import classNames from "classnames"; +import { Field } from "redux-form"; + +const renderInput = field => { + const className = classNames([ + "form-group", + { "has-error": field.meta.touched && field.meta.error } + ]); + return ( +
+ +