Repository: guillotinaweb/guillotina_react (language: python)

Issue:
New components to edit data
Hi! I create some components to edit item's data. For example in properties tab or another custom tab to edit item's data. At now there is a EditableField that it allow to edit data and renderField that it allow view data. But if I can edit a field type attribute or multiple value type attribute this component don't allow it beucause in this cases onChange funcion is diferent or there ara some features diferent too. For example input (html tag) attributes. 

I create EditableFileField, EditableInputField and EditableSelectField to edit data, and FieldValue to render data. 

To use these components we define an array or schema to decide which component use in each case. I use @types endpoint from guillotina to get the schema.  These components get by props if are readonly or the user can edit their data. I kept the possibility to edit by props the widget component. 

I don't do pull request because I don't know if these components will be useful for the library or not. We can speak about it. 

Thanks! 

Code: 

[https://gist.github.com/rboixaderg/f286336a5ece3819b0bd7f1805ada6a2](https://gist.github.com/rboixaderg/f286336a5ece3819b0bd7f1805ada6a2)



Discussion / hints:

Fix patch:
diff --git a/package.json b/package.json
index da5ec323..7670cc2a 100644
--- a/package.json
+++ b/package.json
@@ -31,11 +31,11 @@
     "@testing-library/jest-dom": "5.11.6",
     "@testing-library/react": "11.2.2",
     "@testing-library/user-event": "12.6.0",
+    "husky": "4.3.6",
     "microbundle": "0.13.0",
     "node-sass": "5.0.0",
-    "serialize-javascript": "5.0.1",
-    "husky": "4.3.6",
-    "prettier": "2.2.1"
+    "prettier": "2.2.1",
+    "serialize-javascript": "5.0.1"
   },
   "scripts": {
     "format": "prettier --write \"src/**/*.js\"",
@@ -74,4 +74,4 @@
   "author": "Jordi Collell <jordic@gmail.com> ",
   "license": "MIT",
   "homepage": "https://github.com/guillotinaweb/guillotina_react/"
-}
\ No newline at end of file
+}
diff --git a/src/guillo-gmi/actions/add_item.js b/src/guillo-gmi/actions/add_item.js
index e882ef16..666c7693 100644
--- a/src/guillo-gmi/actions/add_item.js
+++ b/src/guillo-gmi/actions/add_item.js
@@ -41,6 +41,7 @@ export function AddItem(props) {
         actionName={'Add ' + type}
         title={'Add ' + type}
         type={type}
+        dataTest={`formAdd${type}Test`}
       />
     </Modal>
   )
diff --git a/src/guillo-gmi/components/behavior_view.js b/src/guillo-gmi/components/behavior_view.js
index a9b72142..f014a403 100644
--- a/src/guillo-gmi/components/behavior_view.js
+++ b/src/guillo-gmi/components/behavior_view.js
@@ -1,8 +1,9 @@
 import React from 'react'
 import { Table } from './ui/table'
 import { useTraversal } from '../contexts'
+import { get } from '../lib/utils'
 
-export function BehaviorsView({ context }) {
+export function BehaviorsView({ context, schema }) {
   const Ctx = useTraversal()
   const { getBehavior } = Ctx.registry
 
@@ -12,7 +13,12 @@ export function BehaviorsView({ context }) {
   )
   const GetBehavior = (b) => {
     const Cls = getBehavior(b, BehaviorNotImplemented)
-    return <Cls {...context[b]} />
+    return (
+      <Cls
+        values={context[b]}
+        properties={get(schema, ['definitions', b, 'properties'], {})}
+      />
+    )
   }
 
   return (
@@ -40,11 +46,3 @@ export function BehaviorNotImplemented() {
     </tr>
   )
 }
-
-/*
-<tr>
-  <td>Creators</td>
-  <td>{Ctx.context.creators.map(item =>
-    <Author name={item}  key={'a' + item} />
-    )}</td>
-</tr> */
diff --git a/src/guillo-gmi/components/behaviors/idublincore.js b/src/guillo-gmi/components/behaviors/idublincore.js
index 92370fc5..1ad89ac2 100644
--- a/src/guillo-gmi/components/behaviors/idublincore.js
+++ b/src/guillo-gmi/components/behaviors/idublincore.js
@@ -1,52 +1,34 @@
 import React from 'react'
-
-import { Icon } from '../ui/icon'
-import { RenderField } from '../fields/renderField'
 import { useTraversal } from '../../contexts'
 import { EditableField } from '../fields/editableField'
-import { Textarea } from '../input/textarea'
-import { Input } from '../input/input'
 
-const Schema = {
-  editable: ['title', 'description', 'effective_date', 'expiration_date'],
-  widgets: {
-    description: Textarea,
-  },
-}
+const editableFiels = [
+  'title',
+  'description',
+  'effective_date',
+  'expiration_date',
+]
 
-export function IDublinCore(props) {
+export function IDublinCore({ properties, values }) {
   const Ctx = useTraversal()
   const modifyContent = Ctx.hasPerm('guillotina.ModifyContent')
 
   return (
     <React.Fragment>
-      {Object.keys(props).map((key) => (
+      {Object.keys(properties).map((key) => (
         <tr key={'dublin_' + key}>
           <td key={1}>{key}</td>
           <td key={2}>
-            {Schema.editable.includes(key) && modifyContent && (
-              <EditableField
-                field={key}
-                value={props[key]}
-                Type={Schema.widgets[key] || Input}
-                ns="guillotina.behaviors.dublincore.IDublinCore"
-              />
-            )}
-            {!Schema.editable.includes(key) && (
-              <RenderField value={props[key]} />
-            )}
+            <EditableField
+              field={key}
+              value={values[key]}
+              ns="guillotina.behaviors.dublincore.IDublinCore"
+              schema={properties[key]}
+              modifyContent={modifyContent && editableFiels.includes(key)}
+            />
           </td>
         </tr>
       ))}
     </React.Fragment>
   )
 }
-
-export function Author({ name }) {
-  return (
-    <div className="container">
-      <Icon icon="fas fa-user" />
-      <span>{name}</span>
-    </div>
-  )
-}
diff --git a/src/guillo-gmi/components/context_toolbar.js b/src/guillo-gmi/components/context_toolbar.js
index d1d68202..d658621f 100644
--- a/src/guillo-gmi/components/context_toolbar.js
+++ b/src/guillo-gmi/components/context_toolbar.js
@@ -53,7 +53,7 @@ export function CreateButton() {
       onChange={doAction}
       options={(state.types || []).map((item) => ({ text: item, value: item }))}
     >
-      <span className="icon">
+      <span className="icon" data-test="itemAddTypeTest">
         <i className="fas fa-plus"></i>
       </span>
     </Dropdown>
diff --git a/src/guillo-gmi/components/fields/downloadField.js b/src/guillo-gmi/components/fields/downloadField.js
new file mode 100644
index 00000000..88977698
--- /dev/null
+++ b/src/guillo-gmi/components/fields/downloadField.js
@@ -0,0 +1,63 @@
+import * as React from 'react'
+import { useTraversal } from '../../contexts'
+
+export const DownloadField = ({ value }) => {
+  const Ctx = useTraversal()
+  const { data, field } = value
+
+  const getField = async (downloadFile) => {
+    const endpoint = `${Ctx.path}@download/${field}`
+    const res = await Ctx.client.download(endpoint)
+    const text = await res.blob()
+    const blob = new Blob([text], {
+      type: data.content_type,
+    })
+    const url = window.URL.createObjectURL(blob)
+
+    // Create blob link to download
+    const link = document.createElement('a')
+    link.href = url
+    if (downloadFile) {
+      link.setAttribute('download', `${data.filename}`)
+    } else {
+      link.setAttribute('target', `_blank`)
+    }
+
+    document.body.appendChild(link)
+    link.click()
+    window.URL.revokeObjectURL(url)
+    link.parentNode?.removeChild(link)
+  }
+
+  return (
+    <div className="field">
+      <div className="label">{data.filename}</div>
+      <div className="columns">
+        <div className="column">
+          <button
+            className="button is-small is-primary level-left"
+            onClick={async (event) => {
+              event.preventDefault()
+              event.stopPropagation()
+              getField(false)
+            }}
+          >
+            Open
+          </button>
+        </div>
+        <div className="column">
+          <button
+            className="button is-small is-primary level-right"
+            onClick={async (event) => {
+              event.preventDefault()
+              event.stopPropagation()
+              getField(true)
+            }}
+          >
+            Download
+          </button>
+        </div>
+      </div>
+    </div>
+  )
+}
diff --git a/src/guillo-gmi/components/fields/editComponent.js b/src/guillo-gmi/components/fields/editComponent.js
new file mode 100644
index 00000000..df306f7c
--- /dev/null
+++ b/src/guillo-gmi/components/fields/editComponent.js
@@ -0,0 +1,101 @@
+import React from 'react'
+import { Textarea } from '../input/textarea'
+import { Checkbox } from '../input/checkbox'
+import { FileUpload } from '../input/upload'
+import { Select } from '../input/select'
+import { Input } from '../input/input'
+import { InputList } from '../input/input_list'
+import { get } from '../../lib/utils'
+
+export const EditComponent = React.forwardRef(
+  ({ schema, val, setValue, dataTest, className, ...rest }, ref) => {
+    if (schema?.widget === 'textarea' || schema?.widget === 'richtext') {
+      return (
+        <Textarea
+          value={val || ''}
+          className={className}
+          onChange={(ev) => setValue(ev)}
+          ref={ref}
+          dataTest={dataTest}
+          {...rest}
+        />
+      )
+    } else if (schema?.type === 'boolean') {
+      return (
+        <Checkbox
+          value={!!val}
+          className={className}
+          onChange={(ev) => setValue(ev)}
+          ref={ref}
+          dataTest={dataTest}
+          {...rest}
+        />
+      )
+    } else if (schema?.type === 'array') {
+      return (
+        <InputList
+          value={val || []}
+          className={className}
+          onChange={(ev) => setValue(ev)}
+          ref={ref}
+          dataTest={dataTest}
+          {...rest}
+        />
+      )
+    } else if (schema?.widget === 'file') {
+      return (
+        <FileUpload
+          onChange={(ev) => setValue(ev)}
+          label={get(val, 'filename', null)}
+          dataTest={dataTest}
+          {...rest}
+        />
+      )
+    } else if (schema?.widget === 'select') {
+      return (
+        <Select
+          value={val || ''}
+          className={className}
+          appendDefault
+          classWrap="is-fullwidth"
+          dataTest={dataTest}
+          {...rest}
+          options={schema?.vocabulary.map((item) => {
+            return {
+              text: item,
+              value: item,
+            }
+          })}
+          onChange={(ev) => {
+            const selectValue = get(ev, 'target.value', '')
+            return setValue(selectValue)
+          }}
+        />
+      )
+    }
+    const getInputType = () => {
+      switch (schema?.type) {
+        case 'integer':
+          return 'number'
+        case 'datetime':
+          return 'datetime-local'
+        default:
+          return 'text'
+      }
+    }
+    return (
+      <Input
+        value={val || ''}
+        className={className}
+        dataTest={dataTest}
+        onChange={(ev) => setValue(ev)}
+        ref={ref}
+        type={getInputType()}
+        {...rest}
+      />
+    )
+  }
+)
+
+EditComponent.displayName = 'EditComponent'
+export default EditComponent
diff --git a/src/guillo-gmi/components/fields/editableField.js b/src/guillo-gmi/components/fields/editableField.js
index b7993846..d27079ca 100644
--- a/src/guillo-gmi/components/fields/editableField.js
+++ b/src/guillo-gmi/components/fields/editableField.js
@@ -1,75 +1,187 @@
 import React from 'react'
-import { Input } from '../input/input'
 import { Button } from '../input/button'
 import { useCrudContext } from '../../hooks/useCrudContext'
 import { useState } from 'react'
 import { useRef } from 'react'
 import { useEffect } from 'react'
 import { Icon } from '../ui'
+import { RenderField } from './renderField'
+import { get } from '../../lib/utils'
+import { DownloadField } from './downloadField'
 
-export function EditableField({ field, value, Type = Input, ns }) {
+export const DEFAULT_VALUE_EDITABLE_FIELD = 'Click to edit'
+export const DEFAULT_VALUE_NO_EDITABLE_FIELD = ' -- '
+
+export function EditableField({
+  field,
+  value,
+  ns,
+  schema = undefined,
+  modifyContent,
+  required,
+}) {
   const ref = useRef()
   const [isEdit, setEdit] = useState(false)
-  const { patch, loading, Ctx } = useCrudContext()
   const [val, setValue] = useState(value)
+  const { patch, loading, Ctx } = useCrudContext()
+
+  const EditComponent = Ctx.registry.get('components', 'EditComponent')
+
+  useEffect(() => {
+    if (isEdit && ref.current) {
+      ref.current.focus()
+    }
+  })
+
+  const canModified = modifyContent && !get(schema, 'readonly', false)
+  const haveDeleteBtn =
+    schema?.widget === 'file' ||
+    schema?.widget === 'select' ||
+    schema?.type === 'array'
+
+  const getRenderProps = () => {
+    const renderProps = {
+      value:
+        val ??
+        (modifyContent
+          ? DEFAULT_VALUE_EDITABLE_FIELD
+          : DEFAULT_VALUE_NO_EDITABLE_FIELD),
+    }
+    if (val && schema?.widget === 'file') {
+      renderProps['value'] = {
+        data: val,
+        field: field,
+      }
+      renderProps['Widget'] = DownloadField
+    } else if (schema?.type === 'boolean') {
+      renderProps['value'] = val?.toString() ?? renderProps['value']
+    } else if (val && schema?.type === 'datetime') {
+      renderProps['value'] = new Date(val).toLocaleString()
+    }
+    return renderProps
+  }
 
   const saveField = async (ev) => {
     if (ev) ev.preventDefault()
-    const data = ns ? { [ns]: { [field]: val } } : { [field]: val }
-    const { isError, errorMessage } = await patch(data)
-    if (!isError) {
-      Ctx.flash(`Field ${field}, updated!`, 'success')
+
+    if (!field) {
+      Ctx.flash(`Provide a key name!`, 'danger')
+      return
+    }
+
+    if (!val && required) {
+      Ctx.flash(`${field} is mandatory!`, 'danger')
+      return
+    }
+
+    if (schema?.widget === 'file') {
+      const value = val
+      if (value) {
+        value['filename'] = unescape(encodeURIComponent(value['filename']))
+      }
+      const endpoint = `${Ctx.path}@upload/${field}`
+      const req = await Ctx.client.upload(endpoint, value)
+      if (req.status !== 200) {
+        Ctx.flash(`Failed to upload file ${field}!`, 'danger')
+      } else {
+        Ctx.flash(`${field} uploaded!`, 'success')
+      }
     } else {
-      Ctx.flash(`Failed to update file ${field}!: ${errorMessage}`, 'danger')
+      const data = ns ? { [ns]: { [field]: val } } : { [field]: val }
+      const dataPatch = await patch(data)
+      if (dataPatch.isError) {
+        Ctx.flash(`Error in field ${field}!`, 'danger')
+      } else {
+        Ctx.flash(`Field ${field}, updated!`, 'success')
+      }
     }
 
-    Ctx.refresh()
     setEdit(false)
+    Ctx.refresh()
   }
 
-  useEffect(() => {
-    if (isEdit && ref.current) {
-      ref.current.focus()
-    }
-  })
+  const deleteField = async (ev) => {
+    if (ev) ev.preventDefault()
+    if (schema?.widget === 'file') {
+      if (!field || (!val && required)) {
+        Ctx.flash(`You can't delete ${field}!`, 'danger')
+        return
+      }
 
+      const data = ns ? { [ns]: { [field]: null } } : { [field]: null }
+      const dataPatch = await patch(data)
+      if (dataPatch.isError) {
+        Ctx.flash(`Error in field ${field}!`, 'danger')
+      } else {
+        Ctx.flash(`Field ${field}, deleted!`, 'success')
+      }
+
+      setEdit(false)
+      Ctx.refresh()
+    } else if (schema?.type === 'string' && schema?.enum) {
+      setValue(null)
+    } else if (schema?.type === 'array' && schema?.items.type === 'string') {
+      setValue([])
+    }
+  }
   return (
     <React.Fragment>
       {!isEdit && (
-        <div className="editable" onClick={() => setEdit(true)}>
-          {value || 'Click to edit'}
-          <Icon icon="fas fa-edit" />
+        <div
+          className={canModified ? 'editable' : ''}
+          onClick={() => {
+            setEdit(!!canModified)
+          }}
+          data-test={`editableFieldTest-${field}`}
+        >
+          <RenderField {...getRenderProps()} />
+          {canModified && <Icon icon="fas fa-edit" />}
         </div>
       )}
 
       {isEdit && (
-        <div className="field">
-          <form onSubmit={saveField}>
+        <div className="field" data-test={`editableFieldTest-${field}`}>
+          <div className="control">
+            <EditComponent
+              ref={ref}
+              schema={schema}
+              val={val}
+              setValue={setValue}
+              dataTest={`editableFieldEditTest`}
+            />
+          </div>
+          <div className="field is-grouped">
+            <div className="control">
+              <Button
+                className="is-small is-primary"
+                loading={loading}
+                onClick={saveField}
+                dataTest="editableFieldBtnSaveTest"
+              >
+                Save
+              </Button>
+            </div>
             <div className="control">
-              <Type
-                value={val || ''}
+              <Button
                 className="is-small"
-                onChange={(ev) => setValue(ev)}
-                ref={ref}
-              />
+                onClick={() => setEdit(false)}
+                dataTest="editableFieldBtnCancelTest"
+              >
+                Cancel
+              </Button>
             </div>
-            <div className="field is-grouped">
+            {!required && haveDeleteBtn && (
               <div className="control">
                 <Button
-                  className="is-small is-primary"
-                  loading={loading}
-                  onClick={saveField}
+                  className="is-small is-danger"
+                  onClick={deleteField}
+                  dataTest="editableFieldBtnDeleteTest"
                 >
-                  Save
+                  Delete
                 </Button>
               </div>
-              <div className="control">
-                <Button className="is-small" onClick={() => setEdit(false)}>
-                  Cancel
-                </Button>
-              </div>
-            </div>
-          </form>
+            )}
+          </div>
         </div>
       )}
     </React.Fragment>
diff --git a/src/guillo-gmi/components/index.js b/src/guillo-gmi/components/index.js
index ed75eb10..35cb8e62 100644
--- a/src/guillo-gmi/components/index.js
+++ b/src/guillo-gmi/components/index.js
@@ -11,9 +11,11 @@ export * from './input/select'
 export * from './input/upload'
 export * from './input/textarea'
 export * from './input/search_input'
+export * from './input/input_list'
 
 export * from './fields/editableField'
 export * from './fields/renderField'
+export * from './fields/downloadField'
 
 export * from './behaviors/iattachment'
 export * from './behaviors/imultiattachment'
diff --git a/src/guillo-gmi/components/input/button.js b/src/guillo-gmi/components/input/button.js
index 18d564a8..bc26423f 100644
--- a/src/guillo-gmi/components/input/button.js
+++ b/src/guillo-gmi/components/input/button.js
@@ -10,6 +10,7 @@ export const Button = ({
   type = 'submit',
   loading = false,
   disabled = false,
+  dataTest,
   ...rest
 }) => {
   let css = [].concat('button', ...className.split(' '))
@@ -24,6 +25,7 @@ export const Button = ({
         onClick={onClick}
         disabled={disabled}
         {...rest}
+        data-test={dataTest}
       >
         {children}
       </button>
diff --git a/src/guillo-gmi/components/input/checkbox.js b/src/guillo-gmi/components/input/checkbox.js
index f1a85d7c..1dea1857 100644
--- a/src/guillo-gmi/components/input/checkbox.js
+++ b/src/guillo-gmi/components/input/checkbox.js
@@ -14,6 +14,7 @@ export const Checkbox = ({
   children,
   placeholder,
   onChange,
+  dataTest,
   ...rest
 }) => {
   const inputRef = useRef(null)
@@ -41,6 +42,7 @@ export const Checkbox = ({
           className="checkbox"
           checked={state}
           onChange={updateState}
+          data-test={dataTest}
           {...rest}
         />
         {children || placeholder}
diff --git a/src/guillo-gmi/components/input/dropdown.js b/src/guillo-gmi/components/input/dropdown.js
index 2c2b9859..65edeaa9 100644
--- a/src/guillo-gmi/components/input/dropdown.js
+++ b/src/guillo-gmi/components/input/dropdown.js
@@ -49,6 +49,7 @@ export default function Dropdown({
                 className={
                   disabled ? 'dropdown-item is-active' : 'dropdown-item'
                 }
+                data-test={`dropdownItemTest-${option.text.toLowerCase()}`}
                 key={option.text}
                 onClick={disabled ? undefined : () => onChange(option.value)}
                 style={
diff --git a/src/guillo-gmi/components/input/email.js b/src/guillo-gmi/components/input/email.js
index 328ab168..62e0e021 100644
--- a/src/guillo-gmi/components/input/email.js
+++ b/src/guillo-gmi/components/input/email.js
@@ -3,13 +3,14 @@ import { Input } from './input'
 import { isEmail } from '../../lib/validators'
 import { Icon } from '../ui/icon'
 
-export const EmailInput = ({ value = '', ...rest }) => {
+export const EmailInput = ({ value = '', dataTest, ...rest }) => {
   return (
     <Input
       type="email"
       validator={isEmail}
       errorMessage="Email address invalid"
       value={value}
+      data-test={dataTest}
       icon={<Icon icon="fas fa-envelope" />}
       {...rest}
     />
diff --git a/src/guillo-gmi/components/input/form.js b/src/guillo-gmi/components/input/form.js
index 9f617287..738527e9 100644
--- a/src/guillo-gmi/components/input/form.js
+++ b/src/guillo-gmi/components/input/form.js
@@ -11,6 +11,7 @@ export const Form = ({
   autoComplete = 'off',
   title,
   error,
+  dataTest,
   ...rest
 }) => {
   const handleSubmit = (event) => {
@@ -18,7 +19,7 @@ export const Form = ({
     onSubmit(event)
   }
   return (
-    <React.Fragment>
+    <div data-test={dataTest}>
       {title && (
         <div className="level">
           <h1 className="title is-size-4">{title}</h1>
@@ -34,7 +35,7 @@ export const Form = ({
       >
         {children}
       </form>
-    </React.Fragment>
+    </div>
   )
 }
 
diff --git a/src/guillo-gmi/components/input/form_builder.js b/src/guillo-gmi/components/input/form_builder.js
index 6290dee7..f036d5a6 100644
--- a/src/guillo-gmi/components/input/form_builder.js
+++ b/src/guillo-gmi/components/input/form_builder.js
@@ -66,7 +66,7 @@ export function FormBuilder({
       onChange: onUpdate(field),
       placeholder: properties[field].title || '',
       id: generateUID(),
-      testid: `${field}TestInput`,
+      dataTest: `${field}TestInput`,
     }
 
     if (required.includes(field)) {
diff --git a/src/guillo-gmi/components/input/index.js b/src/guillo-gmi/components/input/index.js
index 52112c80..452d7f53 100644
--- a/src/guillo-gmi/components/input/index.js
+++ b/src/guillo-gmi/components/input/index.js
@@ -8,4 +8,5 @@ export { default as form_builder } from './form_builder.js'
 export { default as input } from './input.js'
 export { default as password } from './password.js'
 export { default as select } from './select.js'
-export { default as select } from './search_input.js'
+export { default as search_input } from './search_input.js'
+export { default as input_list } from './input_list.js'
diff --git a/src/guillo-gmi/components/input/input.js b/src/guillo-gmi/components/input/input.js
index dfdc2b08..4df47063 100644
--- a/src/guillo-gmi/components/input/input.js
+++ b/src/guillo-gmi/components/input/input.js
@@ -31,7 +31,7 @@ export const Input = React.forwardRef(
       onChange,
       validator = noop,
       errorMessage,
-      testid = 'input-test',
+      dataTest = 'testInput',
       ...rest
     },
     ref
@@ -42,7 +42,7 @@ export const Input = React.forwardRef(
         : [validator, notEmpty]
     }
 
-    const { state, ...handlers } = useInput(onChange, value, validator)
+    const { state, ...handlers } = useInput(onChange, value ?? '', validator)
     const [uid] = useState(generateUID('input'))
     const [mounted, setMounted] = useState(false)
     ref = ref || React.useRef()
@@ -82,7 +82,7 @@ export const Input = React.forwardRef(
             autoComplete={autoComplete}
             disabled={loading || rest.disabled}
             required={required}
-            data-test={testid}
+            data-test={dataTest}
             {...handlers}
             {...rest}
           />
diff --git a/src/guillo-gmi/components/input/input_list.js b/src/guillo-gmi/components/input/input_list.js
new file mode 100644
index 00000000..725bbbdf
--- /dev/null
+++ b/src/guillo-gmi/components/input/input_list.js
@@ -0,0 +1,50 @@
+import * as React from 'react'
+import { Input } from './input'
+
+export const InputList = React.forwardRef(
+  ({ value, onChange, dataTest }, ref) => {
+    const [inputValue, setInputValue] = React.useState('')
+    const addTags = (event) => {
+      if (event.key === 'Enter' && event.target.value !== '') {
+        onChange([...value, event.target.value])
+        setInputValue('')
+      }
+    }
+
+    return (
+      <div className="control">
+        <div className="tags">
+          {value.map((tag, index) => (
+            <div
+              key={`input_list_${tag}_${index}`}
+              className="tag is-info is-medium"
+            >
+              {tag}
+              <button
+                className="delete is-small"
+                onClick={() =>
+                  onChange([
+                    ...value.filter((tag) => value.indexOf(tag) !== index),
+                  ])
+                }
+              ></button>
+            </div>
+          ))}
+        </div>
+        <Input
+          type="text"
+          placeholder="Press enter to add tags"
+          onKeyUp={(event) => addTags(event)}
+          value={inputValue}
+          ref={ref}
+          dataTest={dataTest}
+          onChange={(value) => {
+            if (value !== '') {
+              setInputValue(value)
+            }
+          }}
+        />
+      </div>
+    )
+  }
+)
diff --git a/src/guillo-gmi/components/input/select.js b/src/guillo-gmi/components/input/select.js
index ef5cd369..5967ce9f 100644
--- a/src/guillo-gmi/components/input/select.js
+++ b/src/guillo-gmi/components/input/select.js
@@ -1,7 +1,7 @@
-import React from 'react'
+import React, { useState } from 'react'
 import PropTypes from 'prop-types'
-
-import { classnames } from '../../lib/helpers'
+import ErrorZone from '../error_zone'
+import { classnames, generateUID } from '../../lib/helpers'
 
 // @ TODO implement hasErrors
 
@@ -11,7 +11,10 @@ export const Select = React.forwardRef(
     {
       options,
       error,
+      errorZoneClassName,
       size = 1,
+      placeholder,
+      id,
       className = '',
       classWrap = '',
       multiple = false,
@@ -20,10 +23,13 @@ export const Select = React.forwardRef(
       onChange,
       appendDefault = false,
       style = {},
+      dataTest,
       ...rest
     },
     ref
   ) => {
+    const [uid] = useState(generateUID('select'))
+
     const onUpdate = (ev) => {
       if (ev.target.value === '') {
         onChange({ target: { value: undefined } })
@@ -35,27 +41,44 @@ export const Select = React.forwardRef(
     if (appendDefault) {
       options = [{ text: 'Choose..', value: '' }].concat(options)
     }
+    const statusClasses = error ? 'is-danger' : ''
 
-    const cssWrap = ['select', multiple ? 'is-multiple' : '', classWrap]
+    const cssWrap = [
+      'select',
+      statusClasses,
+      multiple ? 'is-multiple' : '',
+      classWrap,
+    ]
 
     return (
-      <div className={classnames(cssWrap)}>
-        <select
-          className={classnames(['', className])}
-          size={size}
-          multiple={multiple}
-          disabled={loading || rest.disabled}
-          onChange={onUpdate}
-          {...rest}
-          ref={ref}
-          style={style}
-        >
-          {options.map(({ text, ...rest }, index) => (
-            <option key={index.toString()} {...rest}>
-              {text}
-            </option>
-          ))}
-        </select>
+      <div className="field">
+        {id && placeholder ? (
+          <label className="label" htmlFor={id}>
+            {placeholder}
+          </label>
+        ) : null}
+        <div className={classnames(cssWrap)}>
+          <select
+            className={classnames(['', className])}
+            size={size}
+            multiple={multiple}
+            disabled={loading || rest.disabled}
+            onChange={onUpdate}
+            {...rest}
+            ref={ref}
+            style={style}
+            data-test={dataTest}
+          >
+            {options.map(({ text, ...rest }, index) => (
+              <option key={index.toString()} {...rest}>
+                {text}
+              </option>
+            ))}
+          </select>
+        </div>
+        <ErrorZone className={errorZoneClassName} id={uid}>
+          {error ? error : ''}
+        </ErrorZone>
       </div>
     )
   }
diff --git a/src/guillo-gmi/components/input/textarea.js b/src/guillo-gmi/components/input/textarea.js
index 6e3cc601..d632aa62 100644
--- a/src/guillo-gmi/components/input/textarea.js
+++ b/src/guillo-gmi/components/input/textarea.js
@@ -1,25 +1,54 @@
-import React from 'react'
+import React, { useState } from 'react'
+import ErrorZone from '../error_zone'
+import { classnames, generateUID } from '../../lib/helpers'
 
 export const Textarea = React.forwardRef(
-  ({ value = '', rows = 5, className, onChange, ...rest }, ref) => {
-    const css = 'textarea ' + className
+  (
+    {
+      value = '',
+      classWrap = '',
+      rows = 5,
+      className = '',
+      onChange,
+      dataTest,
+      error,
+      errorZoneClassName,
+      placeholder,
+      id,
+      ...rest
+    },
+    ref
+  ) => {
+    const [uid] = useState(generateUID('select'))
 
     const onUpdate = (ev) => {
       if (onChange) {
         onChange(ev.target.value)
       }
     }
+    const statusClasses = error ? 'is-danger' : ''
 
     return (
       <div className="field">
-        <textarea
-          className={css}
-          rows={rows}
-          onChange={onUpdate}
-          value={value}
-          {...rest}
-          ref={ref}
-        />
+        {id && placeholder ? (
+          <label className="label" htmlFor={id}>
+            {placeholder}
+          </label>
+        ) : null}
+        <div className={classWrap}>
+          <textarea
+            className={classnames(['textarea', className, statusClasses])}
+            rows={rows}
+            onChange={onUpdate}
+            value={value}
+            data-test={dataTest}
+            {...rest}
+            ref={ref}
+          />
+        </div>
+        <ErrorZone className={errorZoneClassName} id={uid}>
+          {error ? error : ''}
+        </ErrorZone>
       </div>
     )
   }
diff --git a/src/guillo-gmi/components/item.js b/src/guillo-gmi/components/item.js
index ded8a0fb..685f37e8 100644
--- a/src/guillo-gmi/components/item.js
+++ b/src/guillo-gmi/components/item.js
@@ -31,7 +31,7 @@ export function RItem({ item, search, columns }) {
   const link = () => navigate({ path: model.path }, true)
 
   return (
-    <tr key={item}>
+    <tr key={item} data-test={`itemTest-${item['@name']}`}>
       <td style={smallcss}>
         <ItemCheckbox item={item} />
       </td>
diff --git a/src/guillo-gmi/components/login.js b/src/guillo-gmi/components/login.js
index 9edabb91..09d2e63a 100644
--- a/src/guillo-gmi/components/login.js
+++ b/src/guillo-gmi/components/login.js
@@ -54,7 +54,12 @@ export class Login extends Component {
     const { schemas } = this.props
     return (
       <React.Fragment>
-        <form className="login__form" action="" onSubmit={this.doLogin}>
+        <form
+          className="login__form"
+          action=""
+          onSubmit={this.doLogin}
+          data-test="formLoginTest"
+        >
           <div className="field">
             <label className="label">Username:</label>
             <input
@@ -64,6 +69,7 @@ export class Login extends Component {
               onChange={(e) => this.setState({ username: e.target.value })}
               value={username}
               ref={this.ref}
+              data-test="inputUsernameLoginTest"
             />
           </div>
           <div className="field">
@@ -73,6 +79,7 @@ export class Login extends Component {
               className="input"
               onChange={(e) => this.setState({ password: e.target.value })}
               value={password}
+              data-test="inputPasswordLoginTest"
             />
           </div>
           {schemas && (
@@ -92,7 +99,11 @@ export class Login extends Component {
             </div>
           )}
           <div className="field">
-            <button className="button is-warning" type="submit">
+            <button
+              className="button is-warning"
+              type="submit"
+              data-test="btnLoginTest"
+            >
               Login
             </button>
           </div>
diff --git a/src/guillo-gmi/components/modal.js b/src/guillo-gmi/components/modal.js
index 3bd7be29..ee56342a 100644
--- a/src/guillo-gmi/components/modal.js
+++ b/src/guillo-gmi/components/modal.js
@@ -41,6 +41,7 @@ export function Confirm({ message, onCancel, onConfirm, loading }) {
               loading={loading}
               className="is-success"
               onClick={() => onConfirm()}
+              dataTest="btnConfirmModalTest"
             >
               Confirm
             </Button>
diff --git a/src/guillo-gmi/components/panel/items.js b/src/guillo-gmi/components/panel/items.js
index 17afb388..3b079914 100644
--- a/src/guillo-gmi/components/panel/items.js
+++ b/src/guillo-gmi/components/panel/items.js
@@ -97,7 +97,10 @@ export function PanelItems() {
       </div>
       {loading && <div className="progress-line"></div>}
       {!loading && (
-        <table className="table is-fullwidth is-hoverable">
+        <table
+          className="table is-fullwidth is-hoverable"
+          data-test="itemPanelTableTest"
+        >
           <thead className="is-size-7">
             <tr>
               <th>
diff --git a/src/guillo-gmi/components/panel/properties.js b/src/guillo-gmi/components/panel/properties.js
index 52a5aa27..7343088a 100644
--- a/src/guillo-gmi/components/panel/properties.js
+++ b/src/guillo-gmi/components/panel/properties.js
@@ -1,26 +1,55 @@
-import React from 'react'
+import React, { useEffect } from 'react'
 import { ItemModel } from '../../models'
 import { BehaviorsView } from '../behavior_view'
 import { Icon } from '../ui/icon'
 import { EditableField } from '../fields/editableField'
-import { RenderField } from '../fields/renderField'
 import { PropertiesButtonView } from '../properties_view'
 import { PropertiesView } from '../properties_view'
 import { useConfig } from '../../hooks/useConfig'
+import useSetState from '../../hooks/useSetState'
 import { useTraversal } from '../../contexts'
+import { get } from '../../lib/utils'
 
-const _showProperties = ['@id', '@name', '@uid', 'title']
-const _editable = ['title']
+const _showProperties = ['@id', '@name', '@uid']
+const _ignoreFields = [
+  'guillotina.behaviors.dublincore.IDublinCore',
+  '__behaviors__',
+  'type_name',
+  'creation_date',
+  'modification_date',
+  'uuid',
+  'title',
+]
 
 export function PanelProperties() {
   const Ctx = useTraversal()
   const modifyContent = Ctx.hasPerm('guillotina.ModifyContent')
   const cfg = useConfig()
 
+  const [schema, setSchema] = useSetState({
+    data: undefined,
+    loading: false,
+    error: undefined,
+  })
+
   const model = new ItemModel(Ctx.context)
 
   const showProperties = cfg.properties_default || _showProperties
-  const editable = cfg.properties_editable || _editable
+  const ignoreFields = cfg.properties_ignore_fields || _ignoreFields
+
+  useEffect(() => {
+    ;(async () => {
+      if (!schema.loading && !schema.data && !schema.error) {
+        try {
+          setSchema({ loading: true })
+          const dataJson = await Ctx.client.getTypeSchema(Ctx.path, model.type)
+          setSchema({ loading: false, data: dataJson })
+        } catch (err) {
+          setSchema({ loading: false, error: err })
+        }
+      }
+    })()
+  }, [schema])
 
   return (
     <div className="container">
@@ -38,34 +67,68 @@ export function PanelProperties() {
       </div>
 
       <hr />
-      <div className="columns">
-        <div className="column">
-          <table className="table is-striped is-fullwidth is-size-7">
-            <thead>
-              <tr>
-                <th className="is-2">Prop</th>
-                <th className="is-8">Value</th>
-              </tr>
-            </thead>
-            <tbody>
-              {showProperties.map((prop) => (
-                <tr key={'prop' + prop}>
-                  <td>{prop}</td>
-                  <td>
-                    {editable.includes(prop) && modifyContent ? (
-                      <EditableField field={prop} value={Ctx.context[prop]} />
-                    ) : (
-                      <RenderField value={Ctx.context[prop]} />
-                    )}
-                  </td>
+      {schema && schema.data && !schema.loading && (
+        <div className="columns">
+          <div className="column">
+            <table className="table is-striped is-fullwidth is-size-7">
+              <thead>
+                <tr>
+                  <th className="is-2">Prop</th>
+                  <th className="is-8">Value</th>
                 </tr>
-              ))}
-            </tbody>
-          </table>
-          <PropertiesView />
-          <BehaviorsView context={Ctx.context} />
+              </thead>
+              <tbody>
+                {showProperties.map((prop) => (
+                  <tr key={'prop' + prop}>
+                    <td>{prop}</td>
+                    <td>
+                      <EditableField
+                        field={prop}
+                        value={Ctx.context[prop]}
+                        modifyContent={false}
+                      />
+                    </td>
+                  </tr>
+                ))}
+              </tbody>
+            </table>
+            <table className="table is-striped is-fullwidth is-size-7">
+              <thead>
+                <tr>
+                  <th className="is-2">Prop</th>
+                  <th className="is-8">Value</th>
+                </tr>
+              </thead>
+              <tbody>
+                {Object.keys(schema.data.properties).map((key) => {
+                  const value = schema.data.properties[key]
+                  if (!ignoreFields.includes(key)) {
+                    return (
+                      <tr key={'prop' + key}>
+                        <td>{value.title}</td>
+                        <td>
+                          <EditableField
+                            field={key}
+                            value={Ctx.context[key]}
+                            schema={value}
+                            modifyContent={modifyContent}
+                            required={get(schema.data, 'required', []).includes(
+                              key
+                            )}
+                          />
+                        </td>
+                      </tr>
+                    )
+                  }
+                  return null
+                })}
+              </tbody>
+            </table>
+            <PropertiesView />
+            <BehaviorsView context={Ctx.context} schema={schema.data} />
+          </div>
         </div>
-      </div>
+      )}
     </div>
   )
 }
diff --git a/src/guillo-gmi/components/path.js b/src/guillo-gmi/components/path.js
index 127280d9..f92278f7 100644
--- a/src/guillo-gmi/components/path.js
+++ b/src/guillo-gmi/components/path.js
@@ -36,7 +36,11 @@ export function Path() {
             </li>
           ) : (
             <li key={indx}>
-              <a href={path} onClick={onClick}>
+              <a
+                href={path}
+                onClick={onClick}
+                data-test={`breadcrumbItemTest-${item.toLowerCase()}`}
+              >
                 {item}
               </a>
             </li>
diff --git a/src/guillo-gmi/components/tabs.js b/src/guillo-gmi/components/tabs.js
index c6492a57..7317d6d7 100644
--- a/src/guillo-gmi/components/tabs.js
+++ b/src/guillo-gmi/components/tabs.js
@@ -53,7 +53,12 @@ export function TabsPanel({
                   }
                   key={tab}
                 >
-                  <a onClick={() => changeTab(tab)}>{tab}</a>
+                  <a
+                    data-test={`tabTest-${tab.toLowerCase()}`}
+                    onClick={() => changeTab(tab)}
+                  >
+                    {tab}
+                  </a>
                 </li>
               ))}
             </ul>
diff --git a/src/guillo-gmi/components/ui/delete.js b/src/guillo-gmi/components/ui/delete.js
index cc908262..7ce148d0 100644
--- a/src/guillo-gmi/components/ui/delete.js
+++ b/src/guillo-gmi/components/ui/delete.js
@@ -2,7 +2,12 @@ import React from 'react'
 
 export function Delete({ onClick, children }) {
   return (
-    <button type="button" onClick={onClick} className="delete">
+    <button
+      type="button"
+      onClick={onClick}
+      className="delete"
+      data-test="btnDeleteTest"
+    >
       {children}
     </button>
   )
diff --git a/src/guillo-gmi/components/ui/notification.js b/src/guillo-gmi/components/ui/notification.js
index 4410fab2..c4d93ce3 100644
--- a/src/guillo-gmi/components/ui/notification.js
+++ b/src/guillo-gmi/components/ui/notification.js
@@ -1,5 +1,9 @@
 import React from 'react'
 
 export function Notification({ isColor = '', children }) {
-  return <div className={'notification is-' + isColor}>{children}</div>
+  return (
+    <div className={'notification is-' + isColor} data-test="notificationTest">
+      {children}
+    </div>
+  )
 }
diff --git a/src/guillo-gmi/forms/base.js b/src/guillo-gmi/forms/base.js
index 159812d7..84b189be 100644
--- a/src/guillo-gmi/forms/base.js
+++ b/src/guillo-gmi/forms/base.js
@@ -4,7 +4,7 @@ import { Input } from '../components/input/input'
 import { Form } from '../components/input/form'
 import { stringToSlug } from '../lib/helpers'
 
-export function BaseForm({ onSubmit, actionName, title }) {
+export function BaseForm({ onSubmit, actionName, title, dataTest }) {
   const [name, setName] = useState('')
   const [id, setId] = useState('')
   const [error, setError] = useState(undefined)
@@ -23,7 +23,7 @@ export function BaseForm({ onSubmit, actionName, title }) {
   }
 
   return (
-    <Form title={title} onSubmit={submit}>
+    <Form title={title} onSubmit={submit} dataTest={dataTest}>
       <Input
         id="title"
         placeholder="Title"
@@ -32,12 +32,23 @@ export function BaseForm({ onSubmit, actionName, title }) {
         onChange={setTitle}
         autofocus
         error={error}
+        dataTest="titleTestInput"
       />
 
-      <Input id="id" placeholder="Id" value={id} onChange={setId} />
+      <Input
+        id="id"
+        placeholder="Id"
+        value={id}
+        onChange={setId}
+        dataTest="idTestInput"
+      />
 
       <div className="level level-right">
-        <button type="submit" className="button is-success">
+        <button
+          type="submit"
+          className="button is-success"
+          data-test="formBaseBtnTestSubmit"
+        >
           {actionName || 'Add'}
         </button>
       </div>
diff --git a/src/guillo-gmi/forms/required_fields.js b/src/guillo-gmi/forms/required_fields.js
new file mode 100644
index 00000000..8e4f1952
--- /dev/null
+++ b/src/guillo-gmi/forms/required_fields.js
@@ -0,0 +1,119 @@
+import React from 'react'
+import { useState, useEffect } from 'react'
+import { Form } from '../components/input/form'
+import { stringToSlug } from '../lib/helpers'
+import { useTraversal } from '../contexts'
+import useSetState from '../hooks/useSetState'
+
+const ignoreFiels = []
+const extraFields = ['title']
+
+export function RequiredFieldsForm({
+  onSubmit,
+  actionName,
+  title,
+  dataTest,
+  type,
+}) {
+  const Ctx = useTraversal()
+
+  const EditComponent = Ctx.registry.get('components', 'EditComponent')
+
+  const [formData, setFormData] = useState({})
+  const [errors, setErrors] = useState({})
+
+  const [schema, setSchema] = useSetState({
+    data: undefined,
+    loading: false,
+    error: undefined,
+    formFields: [],
+  })
+
+  useEffect(() => {
+    ;(async () => {
+      if (!schema.loading && !schema.data && !schema.error) {
+        try {
+          setSchema({ loading: true })
+          const dataJson = await Ctx.client.getTypeSchema(Ctx.path, type)
+          setSchema({
+            loading: false,
+            data: dataJson,
+            formFields: [...dataJson.required, ...extraFields],
+          })
+        } catch (err) {
+          setSchema({ loading: false, error: err })
+        }
+      }
+    })()
+  }, [schema])
+
+  const submit = () => {
+    const currentErrors = {}
+
+    schema.formFields.forEach((key) => {
+      if (!formData[key]) {
+        currentErrors[key] = 'This field is required'
+      }
+    })
+
+    if (Object.keys(currentErrors).length > 0) {
+      setErrors(currentErrors)
+      return
+    }
+
+    onSubmit({ id: formData.uuid, ...formData })
+  }
+
+  return (
+    <Form title={title} onSubmit={submit} dataTest={dataTest}>
+      {schema &&
+        schema.data &&
+        !schema.loading &&
+        schema.formFields.map((key) => {
+          if (!ignoreFiels.includes(key)) {
+            const value = schema.data.properties[key]
+            return (
+              <EditComponent
+                key={key}
+                id={key}
+                placeholder={value?.title ?? ''}
+                className=""
+                required
+                schema={schema.data.properties[key]}
+                setValue={(ev) => {
+                  if (key === 'title') {
+                    setFormData({
+                      ...formData,
+                      uuid: stringToSlug(ev),
+                      [key]: ev,
+                    })
+                  } else if (key === 'uuid') {
+                    setFormData({
+                      ...formData,
+                      uuid: stringToSlug(ev),
+                    })
+                  } else {
+                    setFormData({ ...formData, [key]: ev })
+                  }
+                }}
+                error={errors[key]}
+                dataTest={`${key}TestInput`}
+                val={key in formData ? formData[key] : ''}
+              />
+            )
+          }
+          return null
+        })}
+
+      <div className="level level-right">
+        <button
+          type="submit"
+          className="button is-success"
+          data-test="formBaseBtnTestSubmit"
+        >
+          {actionName || 'Add'}
+        </button>
+      </div>
+    </Form>
+  )
+}
diff --git a/src/guillo-gmi/hooks/useRegistry.js b/src/guillo-gmi/hooks/useRegistry.js
index 3f4fed85..1b177ca1 100644
--- a/src/guillo-gmi/hooks/useRegistry.js
+++ b/src/guillo-gmi/hooks/useRegistry.js
@@ -24,6 +24,7 @@ import React from 'react'
 import { NotAllowed } from '../components/notallowed'
 import { NotFound } from '../components/notfound'
 import { Path } from '../components/path'
+import { EditComponent } from '../components/fields/editComponent'
 
 let registry = {
   paths: {},
@@ -67,6 +68,7 @@ let registry = {
   properties: {},
   components: {
     Path: Path,
+    EditComponent: EditComponent,
   },
 }
 
diff --git a/src/guillo-gmi/index.js b/src/guillo-gmi/index.js
index 257355bb..17a89b48 100644
--- a/src/guillo-gmi/index.js
+++ b/src/guillo-gmi/index.js
@@ -5,6 +5,7 @@ export * from './components'
 export * from './contexts'
 export * from './forms/base'
 export * from './forms/users'
+export * from './forms/required_fields'
 export * from './hooks/useConfig'
 export * from './hooks/useCrudContext'
 export * from './hooks/useInput'
diff --git a/src/guillo-gmi/lib/utils.js b/src/guillo-gmi/lib/utils.js
index ae3679c5..3b062fb7 100644
--- a/src/guillo-gmi/lib/utils.js
+++ b/src/guillo-gmi/lib/utils.js
@@ -6,6 +6,18 @@ export const formatDate = (str) => {
   }/${d.getFullYear()} ${d.getHours()}:${minutes}`
 }
 
+export const get = (obj, path, defValue) => {
+  // If path is not defined or it has false value
+  if (!path) return undefined
+  // Check if path is string or array. Regex : ensure that we do not have '.' and brackets.
+  // Regex explained: https://regexr.com/58j0k
+  const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g)
+  // Find value if exist return otherwise return undefined value;
+  return (
+    pathArray.reduce((prevObj, key) => prevObj && prevObj[key], obj) || defValue
+  )
+}
+
 export function getNewId(id = '') {
   const suffix = '-copy-'
   const rgx = new RegExp(`($|${suffix}\\d*)`)
diff --git a/src/guillo-gmi/scss/styles.sass b/src/guillo-gmi/scss/styles.sass
index 1b390dca..932bd157 100644
--- a/src/guillo-gmi/scss/styles.sass
+++ b/src/guillo-gmi/scss/styles.sass
@@ -1,3 +1,3 @@
 @import "../../../node_modules/bulma/bulma.sass"
 @import "./progressbar.sass"
-@import "./gmi.sass"
+@import "./gmi.sass"
\ No newline at end of file
diff --git a/src/guillo-gmi/views/groups.js b/src/guillo-gmi/views/groups.js
index 7daf504f..cf76c06d 100644
--- a/src/guillo-gmi/views/groups.js
+++ b/src/guillo-gmi/views/groups.js
@@ -126,7 +126,11 @@ export function GroupCtx() {
       <div className="columns">
         <div className="column is-4">
           <label className="label">Title: </label>
-          <EditableField field="title" value={Ctx.context.title} />
+          <EditableField
+            field="title"
+            modifyContent
+            value={Ctx.context.title}
+          />
         </div>
         <div className="column is-4 is-size-7">
           <h3 className="title is-size-6">Roles</h3>
diff --git a/src/guillo-gmi/views/users.js b/src/guillo-gmi/views/users.js
index d4c2f2a8..7c9384a8 100644
--- a/src/guillo-gmi/views/users.js
+++ b/src/guillo-gmi/views/users.js
@@ -121,7 +121,9 @@ export function UserCtx() {
             remotes={remotes}
             submitButton={false}
           >
-            <Button loading={loading}>Update</Button>
+            <Button loading={loading} dataTest="formUserTestBtnSubmit">
+              Update
+            </Button>
           </UserForm>
         </div>
         <div className="column">


Test patch:
diff --git a/e2e/cypress.json b/e2e/cypress.json
index c72e8663..9ceaa65d 100644
--- a/e2e/cypress.json
+++ b/e2e/cypress.json
@@ -1,3 +1,4 @@
 {
-  "baseUrl": "http://localhost:3000?path="
+  "baseUrl": "http://localhost:3000?path=",
+  "experimentalFetchPolyfill": true
 }
diff --git a/e2e/cypress/elements/actions-selectors.js b/e2e/cypress/elements/actions-selectors.js
new file mode 100644
index 00000000..9d87b0cc
--- /dev/null
+++ b/e2e/cypress/elements/actions-selectors.js
@@ -0,0 +1,4 @@
+export const ACTION_SELECTORS = {
+  delete: "[data-test='btnDeleteTest']",
+  confirmModal: "[data-test='btnConfirmModalTest']",
+}
\ No newline at end of file
diff --git a/e2e/cypress/elements/breadcrumb-selectors.js b/e2e/cypress/elements/breadcrumb-selectors.js
new file mode 100644
index 00000000..64eae01d
--- /dev/null
+++ b/e2e/cypress/elements/breadcrumb-selectors.js
@@ -0,0 +1,3 @@
+export const BREADCRUMB_SELECTORS = {
+  prefixItem: "breadcrumbItemTest"
+}
\ No newline at end of file
diff --git a/e2e/cypress/elements/form-types-selectors.js b/e2e/cypress/elements/form-types-selectors.js
new file mode 100644
index 00000000..03e8c531
--- /dev/null
+++ b/e2e/cypress/elements/form-types-selectors.js
@@ -0,0 +1,28 @@
+export const FORM_SELECTORS = {
+  containerFolder: "[data-test='formAddFolderTest']",
+  containerItem: "[data-test='formAddItemTest']",
+  containerUser: "[data-test='formAddUserTest']",
+  containerGMI: "[data-test='formAddGMITest']",
+}
+
+export const FORM_BASE_SELECTORS = {
+  prefixField: "TestInput",
+  btn: "[data-test='formBaseBtnTestSubmit']",
+}
+
+export const EDITABLE_FORM_SELECTORS = {
+  prefixEditableField: "editableFieldTest",
+  iconEdit: "[data-test='iconEditTest']",
+  field: "[data-test='editableFieldEditTest']",
+  btnSave: "[data-test='editableFieldBtnSaveTest']",
+  btnCancel: "[data-test='editableFieldBtnCancelTest']",
+  btnDelete:"[data-test='editableFieldBtnDeleteTest']", 
+}
+
+export const USER_FORM_SELECTORS = {
+  username: "[data-test='usernameTestInput']",
+  password: "[data-test='passwordTestInput']",
+  email: "[data-test='emailTestInput']",
+  name: "[data-test='nameTestInput']",
+  btnUpate: "[data-test='formUserTestBtnSubmit']"
+}
diff --git a/e2e/cypress/elements/login-selectors.js b/e2e/cypress/elements/login-selectors.js
new file mode 100644
index 00000000..3739e4ac
--- /dev/null
+++ b/e2e/cypress/elements/login-selectors.js
@@ -0,0 +1,6 @@
+export const LOGIN_SELECTORS = {
+  form: "[data-test='formLoginTest']",
+  username: "[data-test='inputUsernameLoginTest']",
+  password: "[data-test='inputPasswordLoginTest']",
+  btnSubmit: "[data-test='btnLoginTest']",
+}
\ No newline at end of file
diff --git a/e2e/cypress/elements/notification-selectors.js b/e2e/cypress/elements/notification-selectors.js
new file mode 100644
index 00000000..dbc140e9
--- /dev/null
+++ b/e2e/cypress/elements/notification-selectors.js
@@ -0,0 +1,1 @@
+export const NOTIFICATION_SELECTOR = "[data-test='notificationTest']"
\ No newline at end of file
diff --git a/e2e/cypress/elements/panels-selectors.js b/e2e/cypress/elements/panels-selectors.js
new file mode 100644
index 00000000..8780c23a
--- /dev/null
+++ b/e2e/cypress/elements/panels-selectors.js
@@ -0,0 +1,15 @@
+export const ITEMS_PANELS_SELECTORS = {
+  table: "[data-test='itemPanelTableTest']",
+  prefixItem: "itemTest"
+}
+
+export const CONTEXT_TOOLBAR_SELECTORS = {
+  btnAddType: "[data-test='itemAddTypeTest']",
+  btnAddFolder: "[data-test='dropdownItemTest-folder']",
+  btnAddItem: "[data-test='dropdownItemTest-item']",
+  btnAddGMI: "[data-test='dropdownItemTest-gmi']"
+}
+
+export const TABS_PANEL_SELECTOS = {
+  prefixTabs: "tabTest"
+}
\ No newline at end of file
diff --git a/e2e/cypress/elements/user-form-selectors.js b/e2e/cypress/elements/user-form-selectors.js
deleted file mode 100644
index df630225..00000000
--- a/e2e/cypress/elements/user-form-selectors.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export const USER_FORM_SELECTORS = {
-  username: "[data-test='usernameTestInput']",
-  password: "[data-test='passwordTestInput']",
-  email: "[data-test='emailTestInput']",
-  name: "[data-test='nameTestInput']",
-}
diff --git a/e2e/cypress/integration/content.js b/e2e/cypress/integration/content.js
index 70cb82c5..232f8efa 100644
--- a/e2e/cypress/integration/content.js
+++ b/e2e/cypress/integration/content.js
@@ -1,4 +1,16 @@
-import { USER_FORM_SELECTORS } from '../elements/user-form-selectors'
+import { 
+  ITEMS_PANELS_SELECTORS, 
+  CONTEXT_TOOLBAR_SELECTORS 
+} from '../elements/panels-selectors'
+import { 
+  FORM_BASE_SELECTORS, 
+  EDITABLE_FORM_SELECTORS, 
+  FORM_SELECTORS,
+  USER_FORM_SELECTORS
+} from '../elements/form-types-selectors'
+import { NOTIFICATION_SELECTOR } from '../elements/notification-selectors'
+import { ACTION_SELECTORS } from '../elements/actions-selectors'
+import { BREADCRUMB_SELECTORS } from '../elements/breadcrumb-selectors'
 
 describe('test content', function () {
   beforeEach('clear', function () {
@@ -6,54 +18,67 @@ describe('test content', function () {
     cy.clearCookies()
     cy.autologin()
     cy.visit('/db/container/')
-    cy.get('td:nth-child(4)')
+    cy.get(ITEMS_PANELS_SELECTORS.table)
       .should('contain', 'Groups')
       .should('contain', 'Users')
   })
   it('creates a folder as Admin, then deletes it', function () {
     // Create Folder
-    cy.get('.dropdown-trigger > .button > .icon').click()
-    cy.get('#dropdown-menu > .dropdown-content > :nth-child(2)').click()
-    cy.get('.title').should('contain', 'Add Folder')
-    cy.get('#title').type('Test Folder')
-    cy.get('#id').should('have.value', 'test-folder')
-    cy.get('.level > .button').click()
-    cy.get('.notification').should('contain', 'Content created!')
+    cy.get(CONTEXT_TOOLBAR_SELECTORS.btnAddType).click()
+    cy.get(CONTEXT_TOOLBAR_SELECTORS.btnAddFolder).click()
+    cy.get(FORM_SELECTORS.containerFolder).should('contain', 'Add Folder')
+    cy.get(`[data-test='title${FORM_BASE_SELECTORS.prefixField}']`).type('Test Folder')
+    cy.get(`[data-test='id${FORM_BASE_SELECTORS.prefixField}']`).should('have.value', 'test-folder')
+    cy.get(FORM_BASE_SELECTORS.btn).click()
+    cy.get(NOTIFICATION_SELECTOR).should('contain', 'Content created!')
 
     // Delete Folder
-    cy.get('tr:contains(Test Folder) .delete').click()
-    cy.get('.level-right > .control > .button').click()
-    cy.get('.notification').should('contain', 'Items removed!')
+    cy.get(`[data-test='${ITEMS_PANELS_SELECTORS.prefixItem}-test-folder']`).within(() => {
+      cy.get(ACTION_SELECTORS.delete).click()
+    })
+    cy.get(ACTION_SELECTORS.confirmModal).click()
+    cy.get(NOTIFICATION_SELECTOR).should('contain', 'Items removed!')
   })
 
   it('creates an item as Admin, modifies it and delete it', function () {
+
     // Create Item
-    cy.get('.dropdown-trigger > .button > .icon').click()
-    cy.get('#dropdown-menu > .dropdown-content > :nth-child(1)').click()
-    cy.get('.title').should('contain', 'Add Item')
-    cy.get('#title').type('Test Item')
-    cy.get('#id').should('have.value', 'test-item')
-    cy.get('.level > .button').click()
-    cy.get('.notification').should('contain', 'Content created!')
+    cy.intercept(`/db/container/test-item/@canido?permissions=guillotina.AddContent,guillotina.ModifyContent,guillotina.ViewContent,guillotina.DeleteContent,guillotina.AccessContent,guillotina.SeePermissions,guillotina.ChangePermissions,guillotina.MoveContent,guillotina.DuplicateContent,guillotina.ReadConfiguration,guillotina.RegisterConfigurations,guillotina.WriteConfiguration,guillotina.ManageAddons,guillotina.swagger.View`).as('canido')
+
+    cy.get(CONTEXT_TOOLBAR_SELECTORS.btnAddType).click()
+    cy.get(CONTEXT_TOOLBAR_SELECTORS.btnAddItem).click()
+    cy.get(FORM_SELECTORS.containerItem).should('contain', 'Add Item')
+    cy.get(`[data-test='title${FORM_BASE_SELECTORS.prefixField}']`).type('Test Item')
+    cy.get(`[data-test='id${FORM_BASE_SELECTORS.prefixField}']`).should('have.value', 'test-item')
+    cy.get(FORM_BASE_SELECTORS.btn).click()
+    cy.get(NOTIFICATION_SELECTOR).should('contain', 'Content created!')
 
     // Modify Item
-    cy.get('td:contains(Test Item)').click()
+    cy.get(`[data-test='${ITEMS_PANELS_SELECTORS.prefixItem}-test-item']`).click()
+    cy.wait('@canido')
     cy.get(
-      ':nth-child(1) > :nth-child(2) > .editable > .icon > .svg-inline--fa'
+      `[data-test='${EDITABLE_FORM_SELECTORS.prefixEditableField}-title']`
     ).click()
-    cy.get('.input').clear().type('Test Modified Item')
-    cy.get(':nth-child(1) > .control > .button').click()
-    cy.get('.notification').should('contain', 'Field title, updated!')
-    cy.get('.breadcrumb a:contains(container)').click()
+    cy.get(
+      `[data-test='${EDITABLE_FORM_SELECTORS.prefixEditableField}-title']`
+    ).within(() => {
+      cy.get(EDITABLE_FORM_SELECTORS.field).clear().type('Test Modified Item')
+      cy.get(EDITABLE_FORM_SELECTORS.btnSave).click()
+    })
+    
+    cy.get(NOTIFICATION_SELECTOR).should('contain', 'Field title, updated!')
+    cy.get(`[data-test='${BREADCRUMB_SELECTORS.prefixItem}-container']`).click()
 
     // Delete Item
-    cy.get('tr:contains(Test Modified Item) .delete').click()
-    cy.get('.level-right > .control > .button').click()
-    cy.get('.notification').should('contain', 'Items removed!')
+    cy.get(`[data-test='${ITEMS_PANELS_SELECTORS.prefixItem}-test-item']`).within(() => {
+      cy.get(ACTION_SELECTORS.delete).click()
+    })
+    cy.get(ACTION_SELECTORS.confirmModal).click()
+    cy.get(NOTIFICATION_SELECTOR).should('contain', 'Items removed!')
   })
 
   it('creates a User as Admin, modifies it and delete it', function () {
-    cy.get('td:contains(UserManager)').click()
+    cy.get(`[data-test='${ITEMS_PANELS_SELECTORS.prefixItem}-users']`).click()
 
     // Create User
     cy.get('.level-right > .button').click()
@@ -61,22 +86,26 @@ describe('test content', function () {
     cy.get(USER_FORM_SELECTORS.email).type('test-user@test.test')
     cy.get(USER_FORM_SELECTORS.name).type('Test Name')
     cy.get(USER_FORM_SELECTORS.password).type('TestPassword')
-    cy.get('p.control > .button').should('contain', 'Add User').click()
-    cy.get('.notification').should('contain', 'Content created!')
+    cy.get(FORM_SELECTORS.containerUser).within(() => {
+      cy.get('form').submit()
+    })
+    cy.get(NOTIFICATION_SELECTOR).should('contain', 'Content created!')
 
     // Modify Item
-    cy.get('td:contains(test-user)').click()
+    cy.get(`[data-test='${ITEMS_PANELS_SELECTORS.prefixItem}-test-user']`).click()
     cy.get(USER_FORM_SELECTORS.username).type('Test Modified User', {
       force: true,
     })
 
-    cy.get('p.control > .button').click()
-    cy.get('.notification').should('contain', 'Data updated')
-    cy.get('.breadcrumb a:contains(users)').click()
+    cy.get(USER_FORM_SELECTORS.btnUpate).click()
+    cy.get(NOTIFICATION_SELECTOR).should('contain', 'Data updated')
+    cy.get(`[data-test='${BREADCRUMB_SELECTORS.prefixItem}-users']`).click()
 
     // Delete Item
-    cy.get('tr:contains(test-user) .delete').click()
-    cy.get('.level-right > .control > .button').click()
-    cy.get('.notification').should('contain', 'Items removed!')
+    cy.get(`[data-test='${ITEMS_PANELS_SELECTORS.prefixItem}-test-user']`).within(() => {
+      cy.get(ACTION_SELECTORS.delete).click()
+    })
+    cy.get(ACTION_SELECTORS.confirmModal).click()
+    cy.get(NOTIFICATION_SELECTOR).should('contain', 'Items removed!')
   })
 })
diff --git a/e2e/cypress/integration/gmi-type-content.js b/e2e/cypress/integration/gmi-type-content.js
new file mode 100644
index 00000000..e6e47e2b
--- /dev/null
+++ b/e2e/cypress/integration/gmi-type-content.js
@@ -0,0 +1,118 @@
+import { 
+  ITEMS_PANELS_SELECTORS, 
+  CONTEXT_TOOLBAR_SELECTORS,
+  TABS_PANEL_SELECTOS
+} from '../elements/panels-selectors'
+import { 
+  FORM_BASE_SELECTORS, 
+  EDITABLE_FORM_SELECTORS, 
+  FORM_SELECTORS
+} from '../elements/form-types-selectors'
+import { NOTIFICATION_SELECTOR } from '../elements/notification-selectors'
+import { ACTION_SELECTORS } from '../elements/actions-selectors'
+import { BREADCRUMB_SELECTORS } from '../elements/breadcrumb-selectors'
+
+describe('test GMI type', function () {
+  beforeEach('clear', function () {
+    cy.clearLocalStorage()
+    cy.clearCookies()
+    cy.autologin()
+    cy.visit('/db/container/')
+    cy.get(ITEMS_PANELS_SELECTORS.table)
+      .should('contain', 'Groups')
+      .should('contain', 'Users')
+  })
+  it('creates a GMI item as Admin, modifies it and delete it', function () {
+    cy.intercept(`db/container/test-gmi-item/@canido?permissions=guillotina.AddContent,guillotina.ModifyContent,guillotina.ViewContent,guillotina.DeleteContent,guillotina.AccessContent,guillotina.SeePermissions,guillotina.ChangePermissions,guillotina.MoveContent,guillotina.DuplicateContent,guillotina.ReadConfiguration,guillotina.RegisterConfigurations,guillotina.WriteConfiguration,guillotina.ManageAddons,guillotina.swagger.View`).as('canido')
+    cy.intercept('PATCH',`/db/container/test-gmi-item/`).as('patch')
+
+    // Create GMI item
+    cy.get(CONTEXT_TOOLBAR_SELECTORS.btnAddType).click()
+    cy.get(CONTEXT_TOOLBAR_SELECTORS.btnAddGMI).click()
+    cy.get(FORM_SELECTORS.containerGMI).should('contain', 'Add GMI')
+    cy.get(`[data-test='title${FORM_BASE_SELECTORS.prefixField}']`).type('Test GMI item')
+    cy.get(`[data-test='uuid${FORM_BASE_SELECTORS.prefixField}']`).should('have.value', 'test-gmi-item')
+    cy.get(`[data-test='number_field${FORM_BASE_SELECTORS.prefixField}']`).type('5')
+    cy.get(`[data-test='choice_field${FORM_BASE_SELECTORS.prefixField}']`).select('plone')
+    cy.get(FORM_BASE_SELECTORS.btn).click()
+    cy.get(NOTIFICATION_SELECTOR).should('contain', 'Content created!')
+
+    // Modify Item
+    cy.get(`[data-test='${ITEMS_PANELS_SELECTORS.prefixItem}-test-gmi-item']`).click()
+    cy.wait('@canido')
+    cy.get(`[data-test='${TABS_PANEL_SELECTOS.prefixTabs}-properties']`).click()
+
+    // Modify title field ( input )
+    cy.testInput({
+      fieldName: 'title', 
+      newValue: 'Title Modified Item',
+    })
+    // Modify title field ( textarea )
+    cy.testInput({
+      fieldName: 'text_field', 
+      newValue: 'Text field modified',
+    })
+    // Modify number field
+    cy.testInput({
+      fieldName: 'number_field', 
+      newValue: '5',
+    })    
+    // Modify datetiem field 
+    cy.testInput({
+      fieldName: 'datetime_field', 
+      newValue: '2021-01-23T14:53',
+      checkValue : false
+    })
+    
+    // Modify checkbox
+    cy.get(
+      `[data-test='${EDITABLE_FORM_SELECTORS.prefixEditableField}-boolean_field']`
+    ).click()
+    cy.get(
+      `[data-test='${EDITABLE_FORM_SELECTORS.prefixEditableField}-boolean_field']`
+    ).within(() => {
+      cy.get(EDITABLE_FORM_SELECTORS.field).check()
+      cy.get(EDITABLE_FORM_SELECTORS.btnSave).click()
+    })
+    cy.wait('@patch')
+    cy.get(NOTIFICATION_SELECTOR).should('contain', `Field boolean_field, updated!`)
+
+
+    // Modify select
+    cy.get(
+      `[data-test='${EDITABLE_FORM_SELECTORS.prefixEditableField}-choice_field']`
+    ).click()
+    cy.get(
+      `[data-test='${EDITABLE_FORM_SELECTORS.prefixEditableField}-choice_field']`
+    ).within(() => {
+      cy.get(EDITABLE_FORM_SELECTORS.field).select('plone')
+      cy.get(EDITABLE_FORM_SELECTORS.btnSave).click()
+    })
+    cy.wait('@patch')
+    cy.get(NOTIFICATION_SELECTOR).should('contain', `Field choice_field, updated!`)
+
+    // Modify input list
+    cy.get(
+      `[data-test='${EDITABLE_FORM_SELECTORS.prefixEditableField}-list_field']`
+    ).click()
+    cy.get(
+      `[data-test='${EDITABLE_FORM_SELECTORS.prefixEditableField}-list_field']`
+    ).within(() => {
+      cy.get(EDITABLE_FORM_SELECTORS.field).clear().type('first item').type('{enter}')
+      cy.get(EDITABLE_FORM_SELECTORS.field).clear().type('second item').type('{enter}')
+      cy.get(EDITABLE_FORM_SELECTORS.btnSave).click()
+    })
+    cy.wait('@patch')
+    cy.get(NOTIFICATION_SELECTOR).should('contain', `Field list_field, updated!`)
+
+
+    cy.get(`[data-test='${BREADCRUMB_SELECTORS.prefixItem}-container']`).click()
+
+    // Delete GMI
+    cy.get(`[data-test='${ITEMS_PANELS_SELECTORS.prefixItem}-test-gmi-item']`).within(() => {
+      cy.get(ACTION_SELECTORS.delete).click()
+    })
+    cy.get(ACTION_SELECTORS.confirmModal).click()
+    cy.get(NOTIFICATION_SELECTOR).should('contain', 'Items removed!')
+  })
+})
\ No newline at end of file
diff --git a/e2e/cypress/integration/login.js b/e2e/cypress/integration/login.js
index adfd38fd..47ee1eb7 100644
--- a/e2e/cypress/integration/login.js
+++ b/e2e/cypress/integration/login.js
@@ -1,21 +1,19 @@
+import {LOGIN_SELECTORS} from '../elements/login-selectors'
+
 describe('test login', function () {
   beforeEach('clear', function () {
     cy.clearLocalStorage()
     cy.clearCookies()
   })
   it('test manual login', function () {
-    const form = '.login__form'
-    const userInput = '.login__form > .field:nth-child(1) > .input'
-    const passwordInput = '.login__form > .field:nth-child(2) > .input'
-    const submit = '.login__form > .field > .button'
-
+    cy.intercept('POST',`/@login`).as('login')
     cy.visit('/')
-    cy.get(form).should('be.visible')
-    cy.get(userInput).type('root').should('have.value', 'root')
-    cy.get(passwordInput).type('root').should('have.value', 'root')
-    cy.get(submit).click()
-    cy.get(form).should('not.exist')
-    cy.get('.box > .container').should('be.visible')
+    cy.get(LOGIN_SELECTORS.form).should('be.visible')
+    cy.get(LOGIN_SELECTORS.username).type('root').should('have.value', 'root')
+    cy.get(LOGIN_SELECTORS.password).type('root').should('have.value', 'root')
+    cy.get(LOGIN_SELECTORS.form).submit()
+    cy.wait('@login')
+    cy.get(LOGIN_SELECTORS.form).should('not.exist')
   })
 
   it('test autologin', function () {
diff --git a/e2e/cypress/support/form.js b/e2e/cypress/support/form.js
new file mode 100644
index 00000000..3016989a
--- /dev/null
+++ b/e2e/cypress/support/form.js
@@ -0,0 +1,29 @@
+
+import { 
+  EDITABLE_FORM_SELECTORS
+} from '../elements/form-types-selectors'
+import { NOTIFICATION_SELECTOR } from '../elements/notification-selectors'
+
+Cypress.Commands.add('testInput', ({
+  fieldName,
+  newValue,
+  checkValue = true
+}) => {
+  cy.get(
+    `[data-test='${EDITABLE_FORM_SELECTORS.prefixEditableField}-${fieldName}']`
+  ).click()
+  cy.get(
+    `[data-test='${EDITABLE_FORM_SELECTORS.prefixEditableField}-${fieldName}']`
+  ).within(() => {
+    cy.get(EDITABLE_FORM_SELECTORS.field).clear().type(newValue)
+    cy.get(EDITABLE_FORM_SELECTORS.btnSave).click()
+  })
+  cy.wait('@patch')
+  cy.get(NOTIFICATION_SELECTOR).should('contain', `Field ${fieldName}, updated!`)
+
+  if(checkValue){
+    cy.get(
+      `[data-test='${EDITABLE_FORM_SELECTORS.prefixEditableField}-${fieldName}']`
+    ).contains(newValue)
+  }
+})
\ No newline at end of file
diff --git a/e2e/cypress/support/index.js b/e2e/cypress/support/index.js
index 3f15a96a..e284d4c7 100644
--- a/e2e/cypress/support/index.js
+++ b/e2e/cypress/support/index.js
@@ -16,6 +16,7 @@
 // Import commands.js using ES2015 syntax:
 import './commands'
 import './login'
+import './form'
 
 // Alternatively you can use CommonJS syntax:
 // require('./commands')
diff --git a/e2e/example/components/guillotina.js b/e2e/example/components/guillotina.js
index 75427a3f..154a909b 100644
--- a/e2e/example/components/guillotina.js
+++ b/e2e/example/components/guillotina.js
@@ -3,7 +3,7 @@ import { Guillotina } from 'react-gmi'
 import { Auth } from 'react-gmi'
 import { Login } from 'react-gmi'
 import { getClient } from 'react-gmi'
-import { ClientProvider } from 'react-gmi'
+import { ClientProvider , RequiredFieldsForm} from 'react-gmi'
 
 import '../node_modules/react-gmi/dist/css/style.css'
 
@@ -11,6 +11,26 @@ const url = 'http://localhost:8080/'
 const auth = new Auth(url)
 const client = getClient(url, auth)
 
+const registry = {
+  // to register views around guillotina objects paths
+  paths: {},
+  // default views for content types
+  views: {
+  },
+  // forms for adding content, they are fired
+  // throught an action
+  forms: {
+    GMI: RequiredFieldsForm,
+  },
+  // when using the default being able to configure properties
+  properties: {
+    // Producto: ProductProps,
+    // Tag: TagProps
+  },
+  behaviors: {},
+  components: {},
+};
+
 export default function App() {
   const [isLogged, setLogged] = React.useState(auth.isLogged)
 
@@ -23,7 +43,7 @@ export default function App() {
 
   return (
     <ClientProvider client={client}>
-      {isLogged && <Guillotina auth={auth} url={url} />}
+      {isLogged && <Guillotina auth={auth} url={url} registry={registry}/>}
       {!isLogged && (
         <div className="columns is-centered">
           <div className="columns is-half">
diff --git a/e2e/g_conf/config.yaml b/e2e/g_conf/config.yaml
index e0da6b34..f4a185db 100644
--- a/e2e/g_conf/config.yaml
+++ b/e2e/g_conf/config.yaml
@@ -2,6 +2,7 @@ applications:
   - guillotina.contrib.dbusers
   - guillotina.contrib.catalog.pg
   - guillotina.contrib.swagger
+  - guillotina.contrib.dyncontent
 databases:
   db:
     storage: postgresql
@@ -24,7 +25,7 @@ auth_token_validators:
   - guillotina.auth.validators.JWTValidator
 cors:
   allow_origin:
-    - "*" # delegate to reverse proxy whichs domains to allow
+    - '*' # delegate to reverse proxy whichs domains to allow
   allow_methods:
     - GET
     - POST
@@ -34,9 +35,9 @@ cors:
     - PUT
     - OPTIONS
   allow_headers:
-    - "*"
+    - '*'
   expose_headers:
-    - "*"
+    - '*'
   allow_credentials: true
   max_age: 3660
 jwt:
@@ -50,4 +51,69 @@ redis:
   memory_cache_size: 104857600 # 100 MB
   pool:
     minsize: 5
-    maxsize: 10
\ No newline at end of file
+    maxsize: 10
+
+
+contents:
+  GMI:
+    title: GMI title
+    inherited_interface: guillotina.interfaces.IFolder
+    inherited_class: guillotina.content.Folder
+    add_permission: guillotina.AddContent
+    allowed_types: 
+      - Image
+      - File
+    behaviors:
+      - guillotina.behaviors.dublincore.IDublinCore
+    properties:
+      json_example:
+        type: guillotina.schema.JSONField
+        schema: 
+          title: My Json Field
+          type: object
+          properties:
+            items:
+              type: array
+      text_richtext_field:
+        type: guillotina.schema.Text
+        widget: richtext
+        title: Text richtext field
+        index:
+          type: searchabletext
+          required: true
+      text_field:
+        type: guillotina.schema.Text
+        title: Text field
+        index:
+          type: searchabletext
+      text_line_field:
+        type: guillotina.schema.TextLine
+        title: Text line field
+      number_field:
+        type: guillotina.schema.Int
+        title: Number field
+        required: true
+      choice_field:
+        type: guillotina.schema.Choice
+        title: Choice field
+        required: true
+        vocabulary: 
+          plone: Plone text
+          guillotina: Guillotina text
+      boolean_field:
+        type: guillotina.schema.Bool
+        title: Boolean field
+      cloud_file_field: 
+        type: guillotina.fields.CloudFileField
+        title: Cloud file field
+      list_field:
+        type: guillotina.schema.List
+        missing_value: []
+        value_type: guillotina.schema.TextLine
+        title: List field
+      datetime_field:
+        type: guillotina.schema.Datetime
+        title: Datetime field
+
+
+