Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/opuscapita/react-crudeditor

OpusCapita React CRUD Editor
https://github.com/opuscapita/react-crudeditor

crud crud-editor javascript js react

Last synced: about 1 month ago
JSON representation

OpusCapita React CRUD Editor

Awesome Lists containing this project

README

        

# CRUD Editor

[![CircleCI](https://circleci.com/gh/OpusCapita/react-crudeditor.svg?style=shield&circle-token=d9a917e9d6b76fc2d83928b2ec06e2297b3e05a4)](https://circleci.com/gh/OpusCapita/react-crudeditor)
[![npm version](https://img.shields.io/npm/v/@opuscapita/react-crudeditor.svg)](https://npmjs.org/package/@opuscapita/react-crudeditor)
[![Dependency Status](https://img.shields.io/david/OpusCapita/react-crudeditor.svg)](https://david-dm.org/OpusCapita/react-crudeditor)
[![NPM Downloads](https://img.shields.io/npm/dm/@opuscapita/react-crudeditor.svg)](https://npmjs.org/package/@opuscapita/react-crudeditor)
![badge-license](https://img.shields.io/github/license/OpusCapita/react-crudeditor.svg)

### [Demo](https://opuscapita.github.io/react-crudeditor/branches/master/?currentComponentName=ContractEditor&maxContainerWidth=100%25&showSidebar=false)

**Table of Content**

- [Terminology](#terminology)
- [Usage](#usage)
- [*EditorComponent*](#editorcomponent)
* [props.view.name](#editorcomponent-propsviewname)
* [props.view.state](#editorcomponent-propsviewstate)
* [props.onTransition](#editorcomponent-propsontransition)
* [props.externalOperations](#editorcomponent-propsexternaloperations)
* [props.customBulkOperations](#editorcomponent-propscustombulkoperations)
* [props.uiConfig](#editorcomponent-propsuiconfig)
- [Model Definition](#model-definition)
* [Definition Object Structure](#definition-object-structure)
* [FieldInputComponent](#fieldinputcomponent)
* [Embedded FieldInputComponents](#embedded-fieldinputcomponents)
* [Default FieldInputComponents](#default-fieldinputcomponents)
* [FieldRenderComponent](#fieldrendercomponent)
* [TabFormComponent](#tabformcomponent)
* [ViewComponent](#viewcomponent)
* [doTransition](#dotransition)
* [i18n Translations](#i18n-translations)
- [Redux Store](#redux-store)
* [State Structure](#state-structure)
* [Parsing Error and Field/Instance Validation Error](#parsing-error-and-fieldinstance-validation-error)
* [Internal Error](#internal-error)
- [*model* Property](#model-property)
* [Search View *model* Property](#search-view-model-property)
* [Create View *model* Property](#create-view-model-property)
* [Edit View *model* Property](#edit-view-model-property)
* [Show View *model* Property](#show-view-model-property)
* [Error View *model* Property](#error-view-model-property)
- [Diagrams](#diagrams)
* [Transitions of views and their states](#transitions-of-views-and-their-states)
* [Data Flow](#data-flow)
- [Code Conventions](#code-conventions)
* [Redux Actions](#redux-actions)
* [Code Structure](#code-structure)
- [TODO](#todo)

## Terminology


Logical Key

Field(s) and their value(s) constituting visible unique identifier of an entity instance. It may or may not be DB Primary ID.

Operation

An actions to be perfomed on a button click (or menu item in Split button dropdown). There are three kinds of operations:


  • Standard - predefined operation performed on an instance and (optionally) changing view name/state. Its handler is defined inside CRUD Editor. Standard operations IDs:

    • "delete"

    • "edit"

    • "save"

    • "saveAndNext"

    • "saveAndNew"

    • "show"






  • Custom - an operation for navigation inside CRUD Editor. Its handler must be a pure function returning either nothing or new view name/state. Custom operations are defined in Model Definition's ui.operations property.




    Important: Before moving into a new view/state a user is warned about unsaved changes (if any) with confirmation dialog - so the transition may be cancelled.




  • External - an operation for navigating out of CRUD Editor. Its handler is not a pure function because it has side effects and returns nothing. The handler is defined by an application as a callback function passed to EditorComponent props.externalOperations.




    Important: All unsaved Editor data gets lost if the handler changes window.location or view name/state.



Persistent Field

Entity attribute stored on server and returned as instance property by api.get() and api.search() calls. CRUD Editor does not necessarily knows about and works with all persistent fields, but only those listed in Model Definition's model.fields.

Composite Field

In contrast to a Persistent field, composite field is not stored on server and represents some combination of Persistent fields. It is only used for displaying an entity instance in Search Result listing.

Store State


Redux store state of CRUD Editor. It must be serializable.

Editor State

CRUD Editor state which may be saved and later restored by e.g. an application. It is a subset of Store State and contains information about active View Name/State. See EditorComponent props.onTransition for Editor State structure.

Field Type


Field classification, "string" by default. There are standard types as well as custom. A custom type can be any string, ex. "collection", "com.jcatalog.core.DateRange", etc.



There are default React Components for displaying fields of standard types. Rendering of custom types fields requires specifying custom React Components (see FieldInputComponent and FieldRenderComponent) in Model Definition's ui.search, ui.create, ui.edit and ui.show.



Field Type has nothing to do with JavaScript types and defines a structure of any serializable data. By convention, null is considered to be empty value for any Field Type.




Field Types are defined in Model Definition's model.fields.

UI Type


Value conversion is necessary for communication with a React Component rendering the field. Every field value is formated from its Field Type to appropriate UI Type before sending to a React Component, and parsed from the UI Type back to its Field Type after the React Component modifies the value and returns it in onChange event handler.




UI Type has nothing to do with JavaScript types and defines a structure of any serializable data. By convention, null is considered to be empty value for any UI Type. Thus any React Components displaying a field must have embedded empty value concept and be able to deal with null.




UI Types are defined in render.value.type of searchableFields and formLayout (see Model Definition's ui.search, ui.create, ui.edit and ui.show)

Instance

An object CRUD operations are performed upon. Each instance has three different representations in CRUD Editor:


  • Persistent Instance - an instance as stored on server.


  • Form Instance - an instance as displayed in Search/Create/Show/Edit Form. It is distint from Persistent Instance when a user modified the instance but has not saved changes yet.


  • Formated Instance - Form Instance with field values formated to UI Type.



## Usage

```javascript
// 'contract-crudeditor' package.
import React from 'react';

import createEditor, {
VIEW_SEARCH,
VIEW_CREATE,
VIEW_EDIT,
VIEW_SHOW
} from '@opuscapita/react-crudeditor';

const ContractEditor = createEditor();
export default ContractEditor;
```

```javascript
// application.
import React from 'react';
import ContractEditor from 'contract-crudeditor';

export default class extends React.Component {
render() {
return (
...
, ?state: }
?onTransition={}
?externalOperations={}
?uiConfig={{
?headerLevel:
}}
/>;
...
)
```

`createEditor` is a function which the only argument is [Model Definition](#model-definition) object. It returns [*EditorComponent*](#editorcomponent).

## *EditorComponent*

React component with the following props:

Name | Default | Description
---|---|---
view | {
  name: "search",
  state: {}
}| [View Name](#editorcomponent-propsviewname) and full/sliced [View State](#editorcomponent-propsviewstate)
[onTransition](#editorcomponent-propsontransition) | - | [Editor State](#editor-state) transition handler
[externalOperations](#editorcomponent-propsexternaloperations) | - | Function returning a set of [External Operations](#external-operation) handlers

### *EditorComponent* props.view.name

Name of a custom/standard View. *Custom Views* are defined in [Model Definition](#model-definition)'s **ui.customViews**. *Standard View* is one of:

View Name | Description
---|---
search | Search criteria and result
create | New entity instance creation
edit | Existing entity instance editing
show | The same as *edit* but read-only
error | Error page

### *EditorComponent* props.view.state

Full/sliced State describing [props.view.name](#editorcomponent-propsviewname). Its structure is determined by View it describes.

If View State is sliced, not given or `{}`, all not-mentioned properties have their default values.

View State *must* be serializable.

#### *EditorComponent* props.state for *"search"* View:

```javascript
{
?filter: {
: ,
...
},
?sort: ,
?order: <"asc"|"desc", sort order>,
?max: ,
?offset: ,
?hideSearchForm:
}
```

Name | Default
---|---
filter | `{}`
sort | Result field marked with `sortByDefault` (first `sortable` result field if no `sortByDefault` marker is set, or first result field if there are neither `sortByDefault` no `sortable` fields)
order | `"asc"`
max | `30`
offset | `0`
hideSearchForm | false

#### *EditorComponent* props.state for *"create"* View:

```javascript
{
?predefinedFields:
}
```

Name | Default
---|---
predefinedFields | {}

#### *EditorComponent* props.state for *"edit"* and *"show"* Views:

```javascript
{
instance: ,
?tab:
}
```

Name | Default
---|---
instance | -
tab | First tab name

#### *EditorComponent* props.state for *"error"* View:

```javascript
{
code: ,
?payload:
}
```

or

```javascript
[{
code: ,
?payload:
}, ...]
```

Name | Default
---|---
code | -
payload | -

### *EditorComponent* props.onTransition

A transition handler to be called after [Editor State](#editor-state) changes to the one with "ready" status. Its only argument is [Editor State](#editor-state) object. Usually the function reflects [Editor State](#editor-state) to URL. It may also change [Editor State](#editor-state) by rendering [*EditorComponent*](#editorcomponent) with new *props*.

```javascript
function ({
name: , // See EditorComponent props.view.name
state: // See EditorComponent props.view.state
}) {
...
return; // Return value is ignored.
}
```

### *EditorComponent* props.externalOperations

A function returning an array of [External Operations](#external-operation). Each has a handler which is called when a corresponding [External Operation](#external-operation) is triggered by CRUD Editor.

No arguments are passed to the function in Create View since it does not have persistent instance.

In case of unsaved changes, Confirmation Dialog is called after dedicated button press and before **handler()** call => each external operation *must* have side effects, or set **disabled** to true, or set **show** to false - othersise calling Confirmation Dialog is in vain.

```javascript
function( ) {
...
return [{
handler() {
...
return; // Return value is ignored.
},
ui({
name: , // See EditorComponent props.view.name
state: // See EditorComponent props.view.state
}) {
return {
title() {
...
return ,
},

?show: ,
?disabled: ,

/*
* whether the operation has own dedicated button (false)
* or it is to be placed in a dropdown of a previous button (true).
* A previous button is either previous external operation with "dropdown" set to false
* OR previous custom operation with "dropdown" set to false if there is no such external operation
* OR (for Search View) "Edit" button if there is no such external/custom operation.
*/
?dropdown: ,

/*
* React Element or string name of an icon to be displayed inside a button, ex. "trash", "edit";
* see full list at
* http://getbootstrap.com/components/#glyphicons
*/
?icon:
};
}
}, ...]
}
```

### *EditorComponent* props.customBulkOperations

An array of objects defines bulk operations that could be done with selected instances.

An object consist of two parts: handler function, that accepts an array of selected instances, and UI configuration for dropdown element(title).

```javascript
...
customBulkOperations={[{
handler(instances) {
...
return ...; // Could return a Promise. Return nothing in case of synchronous function.
},
ui({ instances }) {
return {
title: ,
}
}
}]}
...
```

### *EditorComponent* props.uiConfig

An object with optional configurations for UI.

Name | Type | Default | Description
---|---|---|---
headerLevel | integer from 1 to 6 | 1 | Header text size in all Views. Specially designed for sub-editors.

## Model Definition

### Definition Object Structure

Complete example of the model configuration file: [contracts model](https://github.com/OpusCapita/react-crudeditor/blob/master/src/demo/models/contracts/index.js).

Model Definition is an object describing an entity. It has the following structure:

```javascript
{
model: {
name: ,
translationsKeyPrefix: ,

/*
* Persistent fields CRUD Editor is interested in.
*/
fields: {
: {

/*
* At least one field must have "unique" property set to true.
*/
?unique: ,

?type: ,

/*
* Constraints for field validation.
* Their allowed set and tuning parameters of each constraint depend on field type.
* Constraints are usually called after field input's "onBlur" event
* and before saving instance modifications.
*/
?constraints: {
?max: ,
?min: ,
?required: ,
?email: ,
?matches: ,
?url: ,

/*
* Custom field-validator returning boolean true in case of successful validation,
* or throwing an array of errors (or single error object) if validation failed.
*/
?validate(, ) {
...
throw [, ...];
...
return true;
}
}
},
...
},

/*
* Custom instance-validator, usually called after "Submit" button press
* but before sending the instance to the server for save/modify.
* Field-validation is done upon all fields just before calling the instance-validator.
* The function returns boolean true in case of successful validation,
* or throws an array of error (or single error object) if validation failed.
* The function may also be asyncronous and return a promise.
*/
?validate({
persistentInstnace: ,
formInstnace: ,
viewName: , // See EditorComponent props.view.name
}) {
...
throw [, ...];
...
return true;
},

translations: // See "i18n tranlations" section.
},

permissions: {
crudOperations: {
/*
* At least one field must be set to 'true' or defined as a function.
*
* Each permission can be defined as either a boolean or a function.
*
* If defined as a boolean, a permission sets editor-wise user permission
* for a specific operation.
*
* An example for booleans:
* {
* create: true,
* delete: false,
* ...
* }
*
* If defined as a function, a permission operates in two modes,
* depending on a number of function arguments:
* - "global" mode - (no arguments) function's return value
* sets editor-wise user permission for a specific operation.
* - "per-instance" mode - ( as the only argument)
* function's return value sets instance-wise user permission for
* a specific operation.
*
* Editor-wise permission is checked before instance-wise one, therefore if
* "global" premission is 'false' then "per-instance" permission is ignored.
*
* If specified, 'edit' and 'delete' permission defined as a function
* _must_ operate in both "global" and "per-instance" mode.
*
* If specified, 'create' and 'view' permission defined as a function
* _must_ operate in "global" only mode.
*
* An example for functions:
* {
* create: () => {
* ...
* return ; // editor-wise permission.
* },
* delete: ({ instance } = {}) => {
* if (instance) {
* // The function is called in "per-instance" mode.
* ...
* return ; // instance-wise permission.
* } else {
* // The function is called in "global" mode.
* ...
* return ; // editor-wise permission.
* }
* },
* ...
* }
*/
?create: , // false by default
?edit: , // false by default
?delete: , // false by default
?view: , // false by default
}
},

/*
* Methods for async operations.
* Each method returns a promise. In case of failure it rejects to
* {
* code: ,
* ?payload:
* }
*/
api: {
/*
* get single entity instance by its Logical Key.
*/
async get: function({
instance:
}) {
...
return {
: ,
...
};
},

/*
* search for entity instances by a criteria.
*/
async search: function({
?filter: {
: ,
...
},
?sort: ,
?order: <"asc"|"desc", sort order>,
?max: ,
?offset:
}) {
...
return {
instances: [{
: ,
...
}, ...],
totalCount:
};
},

/*
* Delete entity instances by their Logical Keys.
* In case of a failure deleting one or more instances,
* an optional "errors" property may be specified with an array of error objects.
* If error object format corresponds to Instance Validation Error, appropriately
* translated messages are displayed as Notifications.
* Errors array length may be different from the number of instances failed to be deleted.
*/
async delete: function({
instances:
}) {
...
return {
count: ,
?errors: [, ...]
};
},

/*
* create new entity instance and return its actial server copy.
*/
async create: function({
instance: {
: ,
...
}
}) {
...
return {
: ,
...
};
},

/*
* update existing entity instance and return its actial server copy.
*/
async update: function({
instance: {
: ,
...
}
}) {
...
return {
: ,
...
};
}
},

?ui: {
?Spinner: ,

?search: function() {
...
return {
/*
* Only Persistent fields from model.fields are allowed.
* By default, all Persistent fields from model.fields
* are used for building search criteria.
*/
?searchableFields: [{
name: ,

/*
* There is no default "render" property for a field of custom Field Type
* => "render" property must be explicitly defined in such a case.
*
* Default "render" property for a field of standard Field Type:
* {
* component: ,
*
* value: {
* propName: "value",
* type:
* }
* }
*/
?render: {

/*
* Either custom FieldInputComponent (see corresponding subheading)
* or id of embedded FieldInputComponent.
*/
component: ,

?props: ,
?value: {
?propName: ,

/*
* Redundant for an embedded FieldInputComponent,
* because UI Type it works with is already known to CRUD Editor.
*
* When omitted for custom FieldInputComponent, UI Type is considered to be unknown.
* In such a case:
* 1. either define converter,
* 2. or unconverted (i.e. of Field Type) field value is sent to FieldInputComponent and
* FieldInputComponent is presupposed to return a value of Field Type.
*
* Ignored when custom "converter" is defined.
*/
?type: ,

/*
* Custom converter which overwrites default converter, if any.
*
* There is a default converter when Field Type is known to CRUID Editor and
* 1. component is embedded FieldInputComponent, or
* 2. component is custom FieldInputComponent and "type" with UI Type is specified.
*/
?converter: {

/*
* Field Type to UI Typer converter.
*/
format(value) {
...
return ;
}

/*
* UI Type to Field Type converter.
* An error must be thrown if a value is invalid, i.e. cannot be converted to the Field Type.
*/
parse(value) {
...
return ;
}
}
}
}
}, ...],

/*
* Both persistent and composite fields are allowed.
* By default, all Persistent fields from model.fields are used in result listing.
* Only one field may have "sortByDefault" set to true.
*/
?resultFields: [{
name: ,
?sortable: ,
?sortByDefault: ,
?textAlignment: <"left"|"center"|"right">,
?component: // see "FieldRenderComponent" subheading.
}, ...]
};
},

/*
* Generate label for entity instance description.
* Default is instance._objectLabel
*/
?instanceLabel() {
...
return ;
},

?create: {

/*
* Generate and return an entity instance with predefined field values.
* The instance is not persistent.
*/
?defaultNewInstance: function() {
...
return ;
},

/*
* tab(), section() and field() may be replaced with false/undefined/null which are ignored.
*
* See "TabFormComponent" and "FieldInputComponent" subheading for React components props.
*
* If formLayout is not specified, create/edit/show View does not have any tabs/sections
* and displays all fields from the model. The following fields are read-only in such case:
* -- all fields in Show view,
* -- Logical Key fields in Edit view.
*/
?formLayout: ({ tab, section, field }) => instance => {
...
return [
?tab(
{
name: ,
?disabled: ,
?component: ,
?columns:
},
?section(
{ name: , ?columns: },
?field({
name: ,
?readOnly: ,
?render: { // see "searchableFields" above for detailed explanation.
component: ,
?props: ,
?value: {
?propName: ,
?type: ,
?converter: { format, parse }
}
},
?validate(, ) { // Field-validator.
...
throw [, ...];
...
return true;
}
}),
?field({
name: ,
?readOnly: ,
?render: { // see "searchableFields" above for detailed explanation.
component: ,
?props: ,
?value: {
?propName: ,
?type: ,
?converter: { format, parse }
},
?validate(, ) { // Field-validator.
...
throw [, ...];
...
return true;
}
}
}),
...
),
?field({
name: ,
?readOnly: ,
?render: { // see "searchableFields" above for detailed explanation.
component: ,
?props: ,
?value: {
?propName: ,
?type: ,
?converter: { format, parse }
}
},
?validate(, ) { // Field-validator.
...
throw [, ...];
...
return true;
}
}),
...
)
?section({ name: },
?field({
name: ,
?readOnly: ,
?render: { // see "searchableFields" above for detailed explanation.
component: ,
?props: ,
?value: {
?propName: ,
?type: ,
?converter: { format, parse }
}
}
?validate(, ) { // Field-validator.
...
throw [, ...];
...
return true;
}
}),
...
),
?field({
name: ,
?readOnly: ,
?render: { // see "searchableFields" above for detailed explanation.
component: ,
?props: ,
?value: {
?propName: ,
?type: ,
?converter: { format, parse }
}
}
?validate(, ) { // Field-validator.
...
throw [, ...];
...
return true;
}
}),
...
]
}
},

?edit: {
?formLayout: // see ui.create.formLayout for details
},

?show: {
?formLayout: // see ui.create.formLayout for details
},

/*
* Views in addition to standard ones.
* TODO
*/
?customViews: {
: , // see "ViewComponent" subheading.
...
},

/*
* Custom operations available in CRUD Editor.
* No arguments are passed to the method in Create View
* since it does not have persistent instance.
*/
?customOperations: function( ) {
...
return [{

/*
* handler() is called at operation button render, not after button press
* => handler() must be a pure function.
* If handler() returns undefined, the button is displayed as disabled;
* otherwise view's name/state are saved and get redirected to only after the button press.
* When the button gets pressed and there are unsaved changes, Confirmation Dialog is called.
*
* Disabling the button by appropriate ui() return value
* prevents handler from been called at operation button render.
*/
handler() {
...
// return value is either undefined or view name/state.
return {
name: ,
?state:
};
},
ui({
name: , // See EditorComponent props.view.name
state: // See EditorComponent props.view.state
}) {
return {
title() {
...
return ,
},

?show: ,
?disabled: ,

/*
* whether the operation has own dedicated button (false)
* or it is to be placed in a dropdown of a previous button (true).
* A previous button is either previous custom operation with "dropdown" set to false
* OR (for Search View) "Edit" button if there is no such custom operation.
*/
?dropdown: ,

/*
* React Element or string name of an icon to be displayed inside a button, ex. "trash", "edit";
* see full list at
* http://getbootstrap.com/components/#glyphicons
*/
?icon:
};
}
}, ...]
}
}
}
```

## FieldInputComponent

Custom React Component for rendering [Formatted Instance](#formated-instance)'s field in Search Form or Create/Edit/Show Form.

Props:

Name | Type | Necessity | Default | Description
---|---|---|---|---
readOnly | boolean | optional | false | Whether field value can be changed
value | serializable | mandatory | - | [Persistent field](#persistent-field) value formated to appropriate [UI Type](#ui-type)
onChange | function | mandatory | - | Handler called when component's value changes.

function(<serializable, new field value>) {
  ...
  return; // return value is ignored
}

onBlur | function | optional | - | Handler called when component loses focus.
function() {
  ...
  return; // return value is ignored
}

### Embedded FieldInputComponents

In CRUD Editor here are two embedded FieldInputComponents:

FieldInputComponent | id
---|---
[BUILTIN_INPUT](#builtin_input) | "input"
[BUILTIN_RANGE_INPUT](#builtin_range_input) | "rangeInput"

For being treated as embedded, string id must be used. Additionally, the embedded FieldInputComponents can be imported from CRUD Editor package:

```javascript
import { BUILTIN_INPUT, BUILTIN_RANGE_INPUT } from '@opuscapita/react-crudeditor';
```

Embedded FieldInputComponents also accept all props defined for [FieldInputComponent](#fieldinputcomponent).

#### BUILTIN_INPUT

Singular input field.

props.type | Description | UI Type | Auto-convertable field types
---|---|---|---
`string` | Regular input field which works with strings | UI_TYPE_STRING | FIELD_TYPE_STRING, FIELD_TYPE_BOOLEAN, FIELD_TYPE_DECIMAL, FIELD_TYPE_INTEGER, FIELD_TYPE_STRING_DATE, FIELD_TYPE_STRING_DECIMAL, FIELD_TYPE_STRING_INTEGER
`checkbox` | Checkbox | UI_TYPE_BOOLEAN | FIELD_TYPE_BOOLEAN
`date` | [DateInput](https://opuscapita.github.io/react-dates//branches/master/index.html?currentComponentName=DateInput) | UI_TYPE_DATE | FIELD_TYPE_STRING_DATE
`integer` | Input which accepts only numbers and `-` sign and formats using [i18n](https://github.com/OpusCapita/i18n).formatNumber | UI_TYPE_INTEGER | FIELD_TYPE_STRING_INTEGER, FIELD_TYPE_INTEGER, FIELD_TYPE_BOOLEAN, FIELD_TYPE_STRING
`decimal` | Input which accepts only numbers and `-` sign and formats using [i18n](https://github.com/OpusCapita/i18n).formatDecimalNumber | UI_TYPE_DECIMAL | FIELD_TYPE_STRING_DECIMAL, FIELD_TYPE_DECIMAL, FIELD_TYPE_BOOLEAN, FIELD_TYPE_STRING

#### BUILTIN_RANGE_INPUT

Range input field.

props.type | Description | UI Type | Auto-convertable field types
---|---|---|---
`string` | Range input which works with strings | UI_TYPE_STRING_RANGE_OBJECT | FIELD_TYPE_DECIMAL_RANGE, FIELD_TYPE_INTEGER_RANGE, FIELD_TYPE_STRING_DATE_RANGE, FIELD_TYPE_STRING_DECIMAL_RANGE, FIELD_TYPE_STRING_INTEGER_RANGE
`date` | [DateRangeInput](https://opuscapita.github.io/react-dates//branches/master/index.html?currentComponentName=DateRangeInput) | UI_TYPE_DATE_RANGE_OBJECT | FIELD_TYPE_STRING_DATE_RANGE
`integer` | Range input which accepts only numbers and `-` sign and formats using [i18n](https://github.com/OpusCapita/i18n).formatNumber | UI_TYPE_INTEGER_RANGE_OBJECT | FIELD_TYPE_STRING_INTEGER_RANGE, FIELD_TYPE_INTEGER_RANGE
`decimal` | Range input which accepts only numbers and `-` sign and formats using [i18n](https://github.com/OpusCapita/i18n).formatDecimalNumber | UI_TYPE_DECIMAL_RANGE_OBJECT | FIELD_TYPE_STRING_DECIMAL_RANGE, FIELD_TYPE_DECIMAL_RANGE

### Default FieldInputComponents

If you define just a [Field Type](#field-type) in [Model Definition](#model-definition)'s **model.fields.\.type** (and omit any custom render in **searchableFields** and **formLayout**), the following components will be default for the fields:

#### Common mappings for all Views

Field Type | Component | props.type
---|---|---
FIELD_TYPE_BOOLEAN | [BUILTIN_INPUT](#builtin_input) | 'checkbox'
FIELD_TYPE_STRING | [BUILTIN_INPUT](#builtin_input) | 'string'
FIELD_TYPE_DECIMAL_RANGE | [BUILTIN_RANGE_INPUT](#builtin_range_input) | 'decimal'
FIELD_TYPE_INTEGER_RANGE | [BUILTIN_RANGE_INPUT](#builtin_range_input) | 'integer'
FIELD_TYPE_STRING_DATE_RANGE | [BUILTIN_RANGE_INPUT](#builtin_range_input) | 'date'
FIELD_TYPE_STRING_DECIMAL_RANGE | [BUILTIN_RANGE_INPUT](#builtin_range_input) | 'string'
FIELD_TYPE_STRING_INTEGER_RANGE | [BUILTIN_RANGE_INPUT](#builtin_range_input) | 'string'

#### Mappings specific to Create/Edit/Show View

Field Type | Component | props.type
---|---|---
FIELD_TYPE_DECIMAL | [BUILTIN_INPUT](#builtin_input) | 'decimal'
FIELD_TYPE_INTEGER | [BUILTIN_INPUT](#builtin_input) | 'integer'
FIELD_TYPE_STRING_DATE | [BUILTIN_INPUT](#builtin_input) | 'date'
FIELD_TYPE_STRING_DECIMAL | [BUILTIN_INPUT](#builtin_input) | 'string'
FIELD_TYPE_STRING_INTEGER | [BUILTIN_INPUT](#builtin_input) | 'string'

#### Mappings specific to Search View (searchable fields)

Field Type | Component | props.type
---|---|---
FIELD_TYPE_DECIMAL | [BUILTIN_RANGE_INPUT](#builtin_range_input) | 'decimal'
FIELD_TYPE_INTEGER | [BUILTIN_RANGE_INPUT](#builtin_range_input) | 'integer'
FIELD_TYPE_STRING_DATE | [BUILTIN_RANGE_INPUT](#builtin_range_input) | 'date'
FIELD_TYPE_STRING_DECIMAL | [BUILTIN_RANGE_INPUT](#builtin_range_input) | 'string'
FIELD_TYPE_STRING_INTEGER | [BUILTIN_RANGE_INPUT](#builtin_range_input) | 'string'

### FieldRenderComponent

Custom React component for rendering [Formated Instance](#formated-instance)'s [persistent](#persistent-field)/[composite](#composite-field) field value in Search Result listing.

Props:

Name | Type | Necessity | Default | Description
---|---|---|---|---
name | string | mandatory | - | Field name from [Model Definition](#model-definition)'s **ui.search().resultFields**
instance | object | mandatory | - | Entity instance

### TabFormComponent

React component for a custom rendering of Tab form in create/edit/show Views.

Props:

Name | Type | Necessity | Default | Description
---|---|---|---|---
viewName | string | mandatory | - | [View Name](#editorcomponent-propsviewname)
instance | object | mandatory | - | persistent instance
[doTransition](#dotransition) | function | optional | - | [Editor State](#editor-state) change handler

### ViewComponent

React component for a custom View.

Props:

Name | Type | Necessity | Default | Description
---|---|---|---|---
viewState | object | mandatory | - | Custom [View State](#editorcomponent-propsviewstate)
[doTransition](#dotransition) | function | optional | - | [Editor State](#editor-state) change handler

### doTransition

This handler is called when

- active View changes its [State](#editorcomponent-propsviewstate), *name* argument is optional in such case;
- another [View](#editorcomponent-propsviewname) must be displayed, *state* argument is optional in such case.

```javascript
function ({
?name: ,
?state:
}) {
...
return; // return value is ignored.
}
```

Arguments:

Name | Default | Description
---|---|---
name | active View Name | To-be-displayed [View Name](#editorcomponent-propsviewname)
state | `{}` | Full/sliced to-be-displayed [View State](#editorcomponent-propsviewstate)

If View State is sliced, not given or `{}`, all not-mentioned properties retain their current values (or default values in case of initial View rendering).

### i18n Translations

[Model Definition](#model-definition)'s **model.translations** object has translations for labels/messages defined in the model. Its shape should correspond to preferred format for [@opuscapita/i18n](https://github.com/OpusCapita/i18n) library.

Translation keys convention:

Translation Target | Translation Key | Default translation
---|---|---
Model name (shown in the header) | `"model.name"` | `"model.name"`
Model tab label | `"model.tab..label"` | `titleCase("")`
Model section label | `"model.section..label"` | `titleCase("")`
Model field label | `"model.field..label"` | `titleCase("")`
Model field hint | `"model.field..hint"` | -
Model field tooltip | `"model.field..tooltip"` | -
Custom [Field Validation Error](#parsing-error-and-fieldinstance-validation-error) | `"model.field..error."` | `error.message \|\| error.id`
[Instance Validation Error](#parsing-error-and-fieldinstance-validation-error) | `"model.error."` | `error.message \|\| `

**titleCase()** converts its arugment from camelcase to titlecase, ex. `titleCase("maxOrderValue") === "Max Order Value"`.

[React context](https://reactjs.org/docs/context.html) *must* have `i18n` property with [I18nManager](https://github.com/OpusCapita/i18n) as its value.

## Redux Store

### State Structure

Every view *must* have "ready" status defined in its *constants.js* file for [onTransition](#editorcomponent-propsontransition) call to work properly.

```javascript
{
common: {
activeViewName: <"search"|"create"|"edit"|"show"|"error">,
},
views: {
search: {

/*
* filter used in Search Result
*/
resultFilter: {
: ,
...
},

/*
* raw filter as displayed in Search Form
* (may be equal to or different from "resultFilter")
*/
formFilter: {
: ,
...
},

/*
* raw filter as communicated to React Components rendering Search fields
*/
formatedFilter: {
: ,
...
},

sortParams: {
field: ,
order: <"asc"|"desc", sort order>,
},
pageParams: {
max: ,
offset: ,
}
resultInstances: [{
: ,
...
}, ...],
selectedInstances: [
,
...
],
totalCount: ,
status: <"uninitialized"|"initializing"|"ready"|"searching"|"deleting"|"redirecting", search view status>,

/*
* Parsing and Internal Errors -- see relevant subheadings
* (all other errors are displayed on "error" view)
*/
errors: {
fields: {
: [, ...],
...
},
general: [, ...]
}
},
create: {
formInstance: {
: ,
...
},
formatedInstance: {
: ,
...
},
status: <"ready"|"saving", create view status>

/*
* Parsing, Field/Instance Validation and Internal Errors -- see relevant subheadings
* (all other errors are displayed on "error" view)
*/
errors: {
fields: {
: [, ...],
...
},
general: [, ...]
}
},
edit: {

/*
* instance in its "canonical state", i.e. as present on the server
*/
persistentInstance: {
: ,
...
},

/*
* row instance as displayed in Edit Form
*/
formInstance: {
: ,
...
},

/*
* raw instance as communicated to React Components rendering Edit Form fields
*/
formatedInstance: {
: ,
...
},

/*
* Either an array of arrays (representing tabs) -- for tabbed layout,
* or an array of arrays (representing sections) and objects (representing fields) -- otherwise.
*/
formLayout: [

/*
* array representing a tab. Its elements are sections/fields. The array also has props:
* -- "tab", string with tab name,
* -- "disabled", boolean.
* -- "component", optional custom React Component, see TabFormComponent subheading.
*/
[

/*
* array representing a section. Its elements are fields. The array also has props:
* -- "section", string with section name.
*/
[

/*
* object representing a field.
*/
{
field: ,
readOnly: ,
component:
},
...
],
...
]
...
],

/*
* a ref to array representing active tab - for tabbed form layout,
* undefined - otherwise.
*/
activeTab: ,

instanceLabel: ,
status: <"uninitialized"|"initializing"|"ready"|"extracting"|"updating"|"deleting"|"redirecting", edit view status>,

/*
* Parsing, Field/Instance Validation and Internal Errors -- see relevant subheadings
* (all other errors are displayed on "error" view)
*/
errors: {
fields: {
: [, ...],
...
},
general: [, ...]
}
},
show: {
instance: {
: ,
...
},
tab: ,
status: <"uninitialized"|"initializing"|"ready"|"redirecting", show view status>,
},
error: {
errors: [{
code: ,
?payload:
}],
status: <"uninitialized"|"ready"|"redirecting", error view status>
}
}
}
```

### Parsing Error and Field/Instance Validation Error

```javascript
{
code: 400,
id: ,
?message: ,
?args:
}
```

Both plain objects and instances of **Error** may be used. The error *must not* be an instance of system error constructor, like RangeError, SyntaxError, TypeError, etc.

### Internal Error

```javascript
{
code: 500,
id: ,
message: ,
?args:
}
```

All internal errors are plain objects, not instances of **Error**, **InternalError**, **TypeError**, etc.

## *model* Property

Every view passes *model* property to external React Components it uses. The property is designed for communication with CRUD Editor and is distinct for different views. It's important to explicitly forward the property to children if they are designed to communicate with the editor. *model* property must never be modified by React Components.

*model* property general structure:

```javascript
{
/*
* Dynamic collection of data from Redux store
* linked with selectors
* and updated every time the store state changes.
*/
data: {...},

/*
* Static collection of event handlers
* triggering async actions and Redux store state changes.
*/
actions: {...}
}
```

### Search View *model* Property

*model* property structure set by Search View:

```javascript
{
data: {
entityName: .model.name,
formFilter: state.formFilter,
formatedFilter: state.formatedFilter,
isLoading: ,
pageParams: {
max: state.pageParams.max,
offset: state.pageParams.offset
},
resultFields: .ui.search().resultFields || ,
resultFilter: state.resultFilter,
resultInstances: state.resultInstances,
searchableFields: [{
name: ,
component: ,
valuePropName: ,

/*
* Boolean, whether two react components for from-to ranging must be rendered.
* if true, filter field value consists of two keys, "from" and "to",
* and two distinct components are rendered for each of them.
* NOTE: always false for custom component.
*/
isRange:

}]
selectedInstances: state.selectedInstances,
sortParams: {
field: state.sortParams.field,
order: state.sortParams.order
},
status: state.status,
totalCount: state.totalCount
},
actions: {

/*
* Switch to Create View
* and populate its form fields with .ui.create.defaultNewInstance if exists.
*/
createInstance(),

/*
* Delete instances by asynchronously calling the server with
* .api.delete()
* and .api.search() to refresh Result Listing.
* Only Logical Key fields are required, all others are ignored.
deleteInstances([{
: ,
...
}, ...]),

/*
* Load an instance in Edit View.
* Only Logical Key fields of the instance are required, all others are ignored.
*/
editInstance({
instance: {
: ,
...
},
?tab:
}),

/*
* Clear all filter fields without Result Listing change.
*/
resetFormFilter(),

/*
* Make .api.search() call to the server and display response in Result Listing.
*/
searchInstances({
?filter: {
: ,
...
},
?sort: ,
?order: <"asc"|"desc", sort order>,
?max: ,
?offset:
}),

toggleSelected({
instance:
selected: ,
}),
toggleSelectedAll(),

/*
* Usually called with form field's onChange event. Result Listing is not automatically changed.
*/
updateFormFilter({
name: ,
value:
})
}
}
```

, where `state` is `