init commit
This commit is contained in:
commit
ed437d2e31
30
.editorconfig
Normal file
30
.editorconfig
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
|
||||||
|
# A special property that should be specified at the top of the file outside of
|
||||||
|
# any sections. Set to true to stop .editor config file search on current file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
# Indentation style
|
||||||
|
# Possible values - tab, space
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
# Indentation size in single-spaced characters
|
||||||
|
# Possible values - an integer, tab
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Line ending file format
|
||||||
|
# Possible values - lf, crlf, cr
|
||||||
|
end_of_line = lf
|
||||||
|
|
||||||
|
# File character encoding
|
||||||
|
# Possible values - latin1, utf-8, utf-16be, utf-16le
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
# Denotes whether to trim whitespace at the end of lines
|
||||||
|
# Possible values - true, false
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# Denotes whether file should end with a newline
|
||||||
|
# Possible values - true, false
|
||||||
|
insert_final_newline = true
|
||||||
6
.eslintignore
Normal file
6
.eslintignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
dist/**
|
||||||
|
src/index.html
|
||||||
|
flow-typed/**
|
||||||
|
node_modules/**
|
||||||
|
seed/node_modules/**
|
||||||
|
admin/node_modules/**
|
||||||
13
.eslintrc.js
Normal file
13
.eslintrc.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
"extends": "./seed/.eslintrc.js",
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"webpack":{
|
||||||
|
config: require.resolve('./seed/config/webpack/default.js')
|
||||||
|
},
|
||||||
|
"node": {
|
||||||
|
"paths": ["src", "seed/src", "admin/src"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.idea
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
dist
|
||||||
|
.DS_store
|
||||||
7
.prettierrc.js
Normal file
7
.prettierrc.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'all',
|
||||||
|
printWidth: 100,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
};
|
||||||
121
README.md
Normal file
121
README.md
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
# Project
|
||||||
|
|
||||||
|
Creobis admin
|
||||||
|
|
||||||
|
# Author
|
||||||
|
|
||||||
|
Nicolas Bernier
|
||||||
|
|
||||||
|
# Git modules
|
||||||
|
|
||||||
|
## Seed react
|
||||||
|
|
||||||
|
Add this git as a submodule
|
||||||
|
|
||||||
|
git@gitlab.com:makeit-group/apps/seed-react.git
|
||||||
|
|
||||||
|
(you must have the right access)
|
||||||
|
|
||||||
|
Add it as seed/ (the name/path => very important!)
|
||||||
|
|
||||||
|
## Seed admin
|
||||||
|
|
||||||
|
Add this git as a submodule
|
||||||
|
|
||||||
|
git@gitlab.com:makeit-group/apps/seed-admin.git
|
||||||
|
|
||||||
|
(you must have the right access)
|
||||||
|
|
||||||
|
Add it as admin/ (the name/path => very important!)
|
||||||
|
|
||||||
|
## Strucutre
|
||||||
|
|
||||||
|
the final structure of a project containing this seed shoudl be
|
||||||
|
|
||||||
|
-- project-name/
|
||||||
|
---- admin (seed-admin)
|
||||||
|
---- seed (seed-react)
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
(after gitmodules)
|
||||||
|
|
||||||
|
npm i
|
||||||
|
cd seed
|
||||||
|
npm i
|
||||||
|
cd ../admin
|
||||||
|
npm i
|
||||||
|
|
||||||
|
or directly
|
||||||
|
|
||||||
|
npm i && cd seed && npm i && cd ../admin && npm i && cd ../seed
|
||||||
|
|
||||||
|
# Structure Project
|
||||||
|
|
||||||
|
/components : react part
|
||||||
|
|
||||||
|
- /enhancers : high order components
|
||||||
|
- /formItems : components used by redux-form
|
||||||
|
- /forms : redux-from forms
|
||||||
|
- /global : global components of the app (Navigation, Header, ...)
|
||||||
|
- /items : components used in multiple other components
|
||||||
|
- /layouts : layout components of the app
|
||||||
|
- /listItems : components used in list
|
||||||
|
- /pageItems : specific sub-part of pages (if used several times, shoudl be moved to items)
|
||||||
|
- /routes : page components (endpoint of the react-router)
|
||||||
|
- /structure : structural components taking children
|
||||||
|
|
||||||
|
/store : redux part
|
||||||
|
|
||||||
|
- /actions
|
||||||
|
- /apis
|
||||||
|
- /reducers
|
||||||
|
- /sagas
|
||||||
|
- /utils
|
||||||
|
|
||||||
|
/styles : general scss
|
||||||
|
|
||||||
|
/types : flow types
|
||||||
|
|
||||||
|
# Generation
|
||||||
|
|
||||||
|
Make sure to have plop installed globally
|
||||||
|
|
||||||
|
sudo npm i -g plop
|
||||||
|
|
||||||
|
then simply run the following command and follow the terminal instructions
|
||||||
|
|
||||||
|
plop
|
||||||
|
|
||||||
|
# Seed
|
||||||
|
|
||||||
|
The seed containes all the shared logic accross projects
|
||||||
|
|
||||||
|
# Storybook
|
||||||
|
|
||||||
|
cd ./seed
|
||||||
|
yarn storybook
|
||||||
|
|
||||||
|
it will also look at the parent stories
|
||||||
|
|
||||||
|
# Test
|
||||||
|
|
||||||
|
cd ./seed
|
||||||
|
yarn test
|
||||||
|
|
||||||
|
it will also look at the parent test
|
||||||
|
|
||||||
|
# Start build
|
||||||
|
|
||||||
|
cd ./seed
|
||||||
|
yarn start
|
||||||
|
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# Icons
|
||||||
|
|
||||||
|
Font Awesome is installed in the seed and imported in the admin
|
||||||
|
|
||||||
|
# Zeus
|
||||||
|
|
||||||
|
zeus https://oxaav8d1i7.execute-api.eu-central-1.amazonaws.com/dev/ ./src/config --ts
|
||||||
5
admin/.gitignore
vendored
Normal file
5
admin/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.idea
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
287
admin/README-config.md
Normal file
287
admin/README-config.md
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
# Models
|
||||||
|
|
||||||
|
The models of the admin have to be in the parent src/config folder
|
||||||
|
|
||||||
|
## root
|
||||||
|
|
||||||
|
The root file must import all the
|
||||||
|
|
||||||
|
- Resource transform function
|
||||||
|
- Resource options
|
||||||
|
- Resource model
|
||||||
|
Othe all the resources
|
||||||
|
|
||||||
|
## Resource transform function
|
||||||
|
|
||||||
|
Function used to display additional informations about the model on their listing/detail view
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const item = obj => ({
|
||||||
|
infos: {
|
||||||
|
info1: obj.something,
|
||||||
|
...
|
||||||
|
info4: obj.somethingElse,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resource options
|
||||||
|
|
||||||
|
The options is an object having the following possible attributes
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const options = {
|
||||||
|
titleList: 'string', //List title
|
||||||
|
titleUpdate: 'string', //Edit title
|
||||||
|
titleCreate: 'string', //Add title
|
||||||
|
titleDetail: 'string', //Detail title
|
||||||
|
successCreate: 'string, // Success create message
|
||||||
|
successUpdate: 'string, // Success update message
|
||||||
|
filters: { // filter options
|
||||||
|
...
|
||||||
|
},
|
||||||
|
exportable: boolean, // show export action on listing view
|
||||||
|
confirmOnSave: boolean, // show confirm modal when saving
|
||||||
|
noDelete: boolean, // disabled or not delete
|
||||||
|
noDetail: boolean, // disabled or not view
|
||||||
|
noUpdate: boolean, // disabled or not edit
|
||||||
|
noCreate: boolean, // disabled or not create
|
||||||
|
// Other config
|
||||||
|
standalone: boolean, // when listData (= subresource) that is a standalone API => avoid to add the ?parent type and id in the url (since standalone)
|
||||||
|
separated: boolean, // when subs (= subresource) that has a separatated view (sometime with another submenu)
|
||||||
|
// legend options
|
||||||
|
legend: {
|
||||||
|
titleDetail: 'string', // title of the legend
|
||||||
|
infos: {
|
||||||
|
info1: 'string', // Detail about info
|
||||||
|
info1Detail: 'string', // Detail text for edit/view
|
||||||
|
// up to info4
|
||||||
|
},
|
||||||
|
},
|
||||||
|
callApiOptions: {
|
||||||
|
// Add options to detail call api
|
||||||
|
detail: {
|
||||||
|
noSuccess: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
baseNav: // base url for subnav link
|
||||||
|
nav:[ // subnavigation
|
||||||
|
{
|
||||||
|
title: 'title',
|
||||||
|
link: 'absolute_url',
|
||||||
|
maxWidth: 50,
|
||||||
|
minWidth: 50,
|
||||||
|
},
|
||||||
|
// no link => separator
|
||||||
|
{
|
||||||
|
title: 'Data',
|
||||||
|
icon: 'database',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
headers: [ // array of headers
|
||||||
|
{
|
||||||
|
disableSort: boolean, // disableSort
|
||||||
|
noOrder: boolean, // disableSort alias
|
||||||
|
type: 'status' | 'date' | 'small' | '',
|
||||||
|
key: 'string', // data key in the object,
|
||||||
|
title: 'string', // title of the column,
|
||||||
|
list: 'array', // array of result when list
|
||||||
|
values: 'string', // path into the content for the list,
|
||||||
|
noUpdate: 'boolean', // disable fast editing on column.
|
||||||
|
fieldName: 'string', // name of the field object if !== to the key
|
||||||
|
pathIdKey: 'exemple._id', // path to id key used for update.
|
||||||
|
link: 'string', // reference of transform funciton key dynamic link
|
||||||
|
// For link
|
||||||
|
id: 'string', // data key in the object for the id of the linked resource,
|
||||||
|
resource: 'string', // resource name (for linked resource)
|
||||||
|
resourceType: 'detail' | 'edit' | 'listing', // type of target
|
||||||
|
searchParameters: Object, // additional search parameters for link,
|
||||||
|
avoidContentText: 'boolean' // Avoid rendering header text with <TextItem>
|
||||||
|
otherResource: 'string', // choose model who must be based the field in FastUpdate case ( Example: check Infamy Project )
|
||||||
|
objectToSend: 'string', // Path to the object who must be send in generic FastUpdate ( to use when otherResource is Used )
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// Complementary form
|
||||||
|
complementary: {
|
||||||
|
// Message success
|
||||||
|
title: 'Post comment',
|
||||||
|
success: 'Success Message',
|
||||||
|
api: '',
|
||||||
|
// Other model
|
||||||
|
fields: complementaryModel,
|
||||||
|
},
|
||||||
|
// Other actions forms models
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
title: 'Action1',
|
||||||
|
// api path
|
||||||
|
api: '',
|
||||||
|
success: 'Success Message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action2',
|
||||||
|
// api path
|
||||||
|
api: '',
|
||||||
|
success: 'Success Message',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model object
|
||||||
|
|
||||||
|
Object structure describing the object (object of field)
|
||||||
|
|
||||||
|
### fields
|
||||||
|
|
||||||
|
#### structural (view OR edit)
|
||||||
|
|
||||||
|
- Type column
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const model={
|
||||||
|
...,
|
||||||
|
column:{
|
||||||
|
type: 'column',
|
||||||
|
fields: {
|
||||||
|
// any structure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Type section
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const model={
|
||||||
|
...,
|
||||||
|
section:{
|
||||||
|
type: 'section',
|
||||||
|
fields: {
|
||||||
|
// any structure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Type list
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const model={
|
||||||
|
...,
|
||||||
|
list:{
|
||||||
|
type: 'list',
|
||||||
|
label: 'list',
|
||||||
|
labelAdd: 'add in list',
|
||||||
|
fields: {
|
||||||
|
// any structure
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### form item
|
||||||
|
|
||||||
|
- General structure
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const model={
|
||||||
|
...,
|
||||||
|
elem: {
|
||||||
|
type: 'string' | 'link' | 'textarea' | 'multi' | 'multiarea' | 'text' | 'date' | 'datetime' | 'picture' | 'select' | 'bool' | 'wysiwyg' | 'schedule',
|
||||||
|
first: boolean, // disable margin top (for top elements)
|
||||||
|
required: boolean, // required or not
|
||||||
|
name: string, // path in the data model
|
||||||
|
disabled: boolean, // disabled or not
|
||||||
|
label: string, // label to display,
|
||||||
|
hideDetail: boolean, // Hide field for detail view
|
||||||
|
hideUpdate: boolean, // Hide field for update view,
|
||||||
|
hideCreate: boolean, // Hide field for create view,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- link
|
||||||
|
|
||||||
|
Attach link to value
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const model={
|
||||||
|
...,
|
||||||
|
elem: {
|
||||||
|
type: 'link',
|
||||||
|
resource: 'string', // resource name (for linked resource)
|
||||||
|
resourceType: 'detail' | 'edit' | 'listing', // type of target
|
||||||
|
link: 'string', // path of the value in the data object
|
||||||
|
id: 'string', // same as link
|
||||||
|
searchParameters: Object, // additional search parameters
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Text
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const model={
|
||||||
|
...,
|
||||||
|
elem: {
|
||||||
|
type: 'text',
|
||||||
|
api: Object, // api parameters to give
|
||||||
|
modelValue: { value: '\_id', text: 'title.fr' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Datetime
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const model={
|
||||||
|
...,
|
||||||
|
elem: {
|
||||||
|
type: 'datetime',
|
||||||
|
dateFormat: 'LL',
|
||||||
|
timeFormat: 'HH:mm'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Select
|
||||||
|
|
||||||
|
Additional data
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const model={
|
||||||
|
...,
|
||||||
|
elem: {
|
||||||
|
type: 'select',
|
||||||
|
values: 'string' || Array<any>, // items for the customSelect
|
||||||
|
list: 'Array', // items for the customSelect
|
||||||
|
asObject: boolean, // tel customSelect how to select value
|
||||||
|
apiPayload: Object, // api parameters to give
|
||||||
|
api: Object, // allias of apiPayload
|
||||||
|
modelValues: { value: '\_id', text: 'title.fr' }, // values to use in the model to display and save data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Picture
|
||||||
|
|
||||||
|
Additional data
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const model={
|
||||||
|
...,
|
||||||
|
elem: {
|
||||||
|
...,
|
||||||
|
type: 'picture',
|
||||||
|
s3Upload: 'boolean', // If S3 Upload is used
|
||||||
|
aspectRatio: 1.25, // aspect ratio for the cropper
|
||||||
|
displayValue: 'string', // if returned value is an object eg: { thumb: .... , full: ...} choose path to display eg:('full')
|
||||||
|
saveValue: 'string', // key in the result object to store in the input value,
|
||||||
|
asObject: // similar to saveValue but says to the element to key the entire response (if not provided try to get res.url || res.default || res.picture || res)
|
||||||
|
label: 'picture 4',
|
||||||
|
name: 'pictures.pic4',
|
||||||
|
apiPath: 'string', // path of the auto upload api in the apis object
|
||||||
|
url: string, // api for the auto upload (alternative to apiPath) /!\ if s3Upload === true url must be api to get upload link
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
79
admin/README-project.md
Normal file
79
admin/README-project.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# Project
|
||||||
|
|
||||||
|
Seed admin
|
||||||
|
|
||||||
|
# Author
|
||||||
|
|
||||||
|
Nicolas Bernier
|
||||||
|
|
||||||
|
# Infos
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
This project needs to be the submodule of a project.
|
||||||
|
He is not supposed to be launched on its own.
|
||||||
|
|
||||||
|
## Seed react
|
||||||
|
|
||||||
|
This project is supposed to at the same level than another submodule *seed react*
|
||||||
|
|
||||||
|
git@gitlab.com:makeit-group/apps/seed-react.git
|
||||||
|
|
||||||
|
(you must have the right access)
|
||||||
|
|
||||||
|
## Strucutre
|
||||||
|
|
||||||
|
the final structure of a project containing this seed shoudl be
|
||||||
|
|
||||||
|
-- project-name/
|
||||||
|
---- admin (seed-admin)
|
||||||
|
---- seed (seed-react)
|
||||||
|
|
||||||
|
|
||||||
|
# Src structure
|
||||||
|
|
||||||
|
/components : react part
|
||||||
|
|
||||||
|
- /enhancers : high order components
|
||||||
|
- /formHelpers : components by the forms in
|
||||||
|
- /formItems : components used by redux-form
|
||||||
|
- /forms : redux-from forms
|
||||||
|
- /global : global components of the app (Navigation, Header, ...)
|
||||||
|
- /items : components used in multiple other components
|
||||||
|
- /layouts : layout components of the app
|
||||||
|
- /listItems : components used in list
|
||||||
|
- /pageItems : specific sub-part of pages (if used several times, shoudl be moved to items)
|
||||||
|
- /routes : page components (endpoint of the react-router)
|
||||||
|
- /structure : structural components taking children
|
||||||
|
|
||||||
|
/store : redux part
|
||||||
|
|
||||||
|
- /actions
|
||||||
|
- /apis
|
||||||
|
- /reducers
|
||||||
|
- /sagas
|
||||||
|
- /utils
|
||||||
|
|
||||||
|
/styles : general scss
|
||||||
|
|
||||||
|
/types : flow types
|
||||||
|
|
||||||
|
# Generation
|
||||||
|
|
||||||
|
Make sure to have plop installed globally
|
||||||
|
|
||||||
|
sudo npm i -g plop
|
||||||
|
|
||||||
|
then simply run the following command and follow the terminal instructions
|
||||||
|
|
||||||
|
plop
|
||||||
|
|
||||||
|
# Icons
|
||||||
|
|
||||||
|
We use this librairie (check in dependencies)
|
||||||
|
|
||||||
|
https://react-icons.netlify.com/#/
|
||||||
|
|
||||||
|
# Eslint
|
||||||
|
|
||||||
|
Could be removed as it should be present in the parent
|
||||||
22
admin/package.json
Normal file
22
admin/package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "seed-admin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Nicolas Bernier",
|
||||||
|
"license": "MIT",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {},
|
||||||
|
"dependencies": {
|
||||||
|
"@ckeditor/ckeditor5-build-classic": "git+https://github.com/Nico924/ckeditor5-build-classic.git",
|
||||||
|
"@ckeditor/ckeditor5-react": "1.1.1",
|
||||||
|
"chart.js": "^2.8.0",
|
||||||
|
"core-js": "^2.6.5",
|
||||||
|
"dom-helpers": "^5.1.3",
|
||||||
|
"draft-js": "^0.11.7",
|
||||||
|
"draftjs-to-html": "^0.8.4",
|
||||||
|
"html-to-draftjs": "^1.4.0",
|
||||||
|
"react-countup": "^4.1.3",
|
||||||
|
"react-draft-wysiwyg": "^1.12.13",
|
||||||
|
"react-infinite-scroller": "^1.2.4",
|
||||||
|
"unflatten": "^1.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
admin/plopfile.js
Normal file
18
admin/plopfile.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const templateDir = '../seed/plop-templates';
|
||||||
|
|
||||||
|
const getTemplatePath = filename => path.join(templateDir, filename);
|
||||||
|
|
||||||
|
const componentPath = './src/components';
|
||||||
|
const storePath = './src/store';
|
||||||
|
|
||||||
|
const plopBase = require('../seed/plopbase');
|
||||||
|
|
||||||
|
module.exports = plopBase({
|
||||||
|
getTemplatePath,
|
||||||
|
path,
|
||||||
|
storePath,
|
||||||
|
componentPath,
|
||||||
|
});
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import CustomFixedTable from './CustomFixedTable';
|
||||||
|
|
||||||
|
storiesOf('admin/structure/CustomFixedTable', module).add('CustomFixedTable component', () => (
|
||||||
|
<CustomFixedTable />
|
||||||
|
));
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import CustomFixedTable from './CustomFixedTable';
|
||||||
|
|
||||||
|
describe('CustomFixedTable', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<CustomFixedTable />);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,663 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
import ModuleButton from 'components/items/ModuleButton';
|
||||||
|
import ReactPaginate from 'react-paginate';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import VirtualTable from 'components/pageItems/VirtualTable';
|
||||||
|
import Loading from 'components/items/Loading';
|
||||||
|
import { RiOrderPlayFill } from 'react-icons/ri';
|
||||||
|
|
||||||
|
import '!style-loader!css-loader!react-virtualized/styles.css';
|
||||||
|
|
||||||
|
import qs from 'query-string';
|
||||||
|
|
||||||
|
import filter from 'lodash/filter';
|
||||||
|
import { config } from 'config/general';
|
||||||
|
import { clone } from 'store/utils/helper';
|
||||||
|
import styleIdentifiers from './customFixedTable.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface CustomFixedTableProps {
|
||||||
|
noCheckbox?: boolean;
|
||||||
|
items: any[];
|
||||||
|
headers: Record<string, any>[];
|
||||||
|
loaded?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
customOrder?: Function;
|
||||||
|
itemProps?: Record<string, any>;
|
||||||
|
noShadow?: boolean;
|
||||||
|
location: Record<string, any>;
|
||||||
|
history: Record<string, any>;
|
||||||
|
url?: string;
|
||||||
|
loadFunc?: Function;
|
||||||
|
handleFilters?: Function;
|
||||||
|
subhead?: React.Element<any>;
|
||||||
|
actions?: React.Element<any>;
|
||||||
|
legend?: React.Element<any>;
|
||||||
|
actionDelete?: Function;
|
||||||
|
title?: string;
|
||||||
|
loaded?: boolean;
|
||||||
|
virtual?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
loadMore?: Function;
|
||||||
|
noMore?: boolean;
|
||||||
|
itemProps?: Record<string, any>;
|
||||||
|
// filters
|
||||||
|
displayFilters?: boolean;
|
||||||
|
filtersProps?: Record<string, any>;
|
||||||
|
filtersInitialValues?: Record<string, any>;
|
||||||
|
// reset
|
||||||
|
pagination?: Record<string, any>;
|
||||||
|
currentPage?: number;
|
||||||
|
noResultMessage?: string;
|
||||||
|
// language
|
||||||
|
lg?: string;
|
||||||
|
// More
|
||||||
|
inModule?: boolean;
|
||||||
|
// legend
|
||||||
|
legend?: object;
|
||||||
|
transformFunction?: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomFixedTableState {
|
||||||
|
list: any[];
|
||||||
|
filters: Record<string, any>;
|
||||||
|
masterCheck: boolean;
|
||||||
|
order: string;
|
||||||
|
asc: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomFixedTable(
|
||||||
|
ItemComponent: React.ComponentType<any>,
|
||||||
|
FiltersForm: React.Component<any>,
|
||||||
|
) {
|
||||||
|
class FixedTable extends React.Component<CustomFixedTableProps, CustomFixedTableState> {
|
||||||
|
constructor(props: CustomFixedTableProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const { location } = props;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
// used to check the item internaly (could have used the store as well)
|
||||||
|
list: [],
|
||||||
|
filters: qs.parse(location.search),
|
||||||
|
masterCheck: false,
|
||||||
|
checkedItems: [],
|
||||||
|
order: '',
|
||||||
|
asc: true,
|
||||||
|
selectedLegends: [],
|
||||||
|
// checked: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateList();
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: CustomFixedTableProps, prevState) {
|
||||||
|
this.updateList(prevProps);
|
||||||
|
this.onUpdate(prevProps, prevState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could be done in reducers
|
||||||
|
onItemClick = (id: number, targetValue?: boolean) => {
|
||||||
|
const list = this.items();
|
||||||
|
|
||||||
|
let { checkedItems } = this.state;
|
||||||
|
|
||||||
|
checkedItems = checkedItems || [];
|
||||||
|
|
||||||
|
const index = checkedItems.indexOf(id);
|
||||||
|
|
||||||
|
if (targetValue === true) {
|
||||||
|
// force true
|
||||||
|
if (index < 0) checkedItems.push(id);
|
||||||
|
// else keep
|
||||||
|
} else if (targetValue === false && index >= 0) {
|
||||||
|
if (index >= 0) checkedItems.splice(index, 1);
|
||||||
|
// else don't change anything
|
||||||
|
} else if (index < 0) {
|
||||||
|
checkedItems.push(id);
|
||||||
|
} else {
|
||||||
|
checkedItems.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let masterCheck = false;
|
||||||
|
if (checkedItems.length === list.length) {
|
||||||
|
masterCheck = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
checkedItems,
|
||||||
|
masterCheck,
|
||||||
|
});
|
||||||
|
|
||||||
|
// const index = _.findIndex(list, e => e[config.idKey] === id);
|
||||||
|
// const newElement = list[index];
|
||||||
|
|
||||||
|
// let masterCheck = true;
|
||||||
|
// // toggle part
|
||||||
|
// if (targetValue !== undefined) newElement.checked = targetValue;
|
||||||
|
// else if (newElement.checked === true) {
|
||||||
|
// newElement.checked = false;
|
||||||
|
// } else {
|
||||||
|
// newElement.checked = true;
|
||||||
|
// }
|
||||||
|
// list.splice(index, 1, newElement);
|
||||||
|
|
||||||
|
// if (!targetValue) {
|
||||||
|
// list.forEach(element => {
|
||||||
|
// if (!element.checked) masterCheck = false;
|
||||||
|
// });
|
||||||
|
// this.setState({
|
||||||
|
// list,
|
||||||
|
// masterCheck,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdate = (prevProps?: CustomFixedTableProps, prevState) => {
|
||||||
|
const { location, handleChecked } = this.props;
|
||||||
|
const { checkedItems } = this.state;
|
||||||
|
|
||||||
|
// provided checked items to parent
|
||||||
|
if (checkedItems && handleChecked) {
|
||||||
|
handleChecked(checkedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prevProps || location !== prevProps.location) {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
checkedItems: [],
|
||||||
|
masterCheck: false,
|
||||||
|
order: '',
|
||||||
|
asc: true,
|
||||||
|
filters: qs.parse(location.search),
|
||||||
|
},
|
||||||
|
this.loadData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// getCheckedItems = () => {
|
||||||
|
// const { checked } = this.state;
|
||||||
|
// const list = this.items();
|
||||||
|
|
||||||
|
// const data = [];
|
||||||
|
// if (!list || !Array.isArray(list)) return data;
|
||||||
|
// list.forEach(element => {
|
||||||
|
// if (element.checked) data.push(element);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (checked.length !== data.length) this.setState({ checked: data });
|
||||||
|
|
||||||
|
// return data;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Update location
|
||||||
|
setFilters = filters => {
|
||||||
|
const { history, url } = this.props;
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
pathname: url,
|
||||||
|
search: qs.stringify(filters),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the list displayed internaly
|
||||||
|
updateList = (prevProps?: Record<string, any>) => {
|
||||||
|
const { items, resourceData } = this.props;
|
||||||
|
|
||||||
|
if (!prevProps || prevProps.items !== items) {
|
||||||
|
this.setState({
|
||||||
|
list: items,
|
||||||
|
});
|
||||||
|
this.resetSelectedLegend();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData = (more?: boolean) => {
|
||||||
|
const { loadFunc } = this.props;
|
||||||
|
|
||||||
|
const { filters } = this.state;
|
||||||
|
|
||||||
|
if (more && loadFunc) {
|
||||||
|
loadFunc({
|
||||||
|
...filters,
|
||||||
|
more: true,
|
||||||
|
});
|
||||||
|
} else if (loadFunc) {
|
||||||
|
loadFunc(filters);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
applyFilters = values => {
|
||||||
|
const { filters } = this.state;
|
||||||
|
const { handleFilters } = this.props;
|
||||||
|
|
||||||
|
const newFilters = handleFilters ? handleFilters(values, filters) : values;
|
||||||
|
|
||||||
|
if (newFilters) this.setFilters(newFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPage = page => {
|
||||||
|
const currentPage = parseInt(page.selected, 10) + 1;
|
||||||
|
|
||||||
|
const { filters } = this.state;
|
||||||
|
|
||||||
|
const newFilters = {
|
||||||
|
...filters,
|
||||||
|
page: currentPage,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setFilters(newFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
masterCheck = () => {
|
||||||
|
const { masterCheck } = this.state;
|
||||||
|
|
||||||
|
const list = this.items();
|
||||||
|
|
||||||
|
list.forEach(element => {
|
||||||
|
this.onItemClick(element[config.idKey], !masterCheck);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
masterCheck: !masterCheck,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get internal list
|
||||||
|
items = () => {
|
||||||
|
const { list } = this.state;
|
||||||
|
return list || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get actually displayed data
|
||||||
|
filtered = () => {
|
||||||
|
const { order, asc, selectedLegends } = this.state;
|
||||||
|
const { customOrder, lg, transformFunction } = this.props;
|
||||||
|
|
||||||
|
let filtered = this.items();
|
||||||
|
|
||||||
|
if (selectedLegends.length > 0) {
|
||||||
|
filtered = filter(filtered, item => {
|
||||||
|
const transformed =
|
||||||
|
typeof transformFunction === 'function' ? transformFunction(item) : item;
|
||||||
|
let dismiss = false;
|
||||||
|
for (let i = 0; i < selectedLegends.length; i++) {
|
||||||
|
if (transformed && transformed.infos && !transformed.infos[selectedLegends[i]]) {
|
||||||
|
dismiss = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !dismiss;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = _.orderBy(
|
||||||
|
filtered,
|
||||||
|
(item: Record<string, any>) => {
|
||||||
|
if (customOrder) return customOrder(item, order) || false;
|
||||||
|
const val = _.get(item, order);
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
if (new Date(val) != 'Invalid Date') return new Date(val).getTime();
|
||||||
|
/* eslint-enable */
|
||||||
|
if (!isNaN(val)) return parseFloat(val);
|
||||||
|
if (typeof val === 'string') return val.toUpperCase();
|
||||||
|
// case multilingual
|
||||||
|
if (typeof val === 'object' && typeof val[lg] === 'string') return val[lg].toUpperCase();
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
[asc ? 'asc' : 'desc'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
};
|
||||||
|
|
||||||
|
orderBy = (field?: string) => {
|
||||||
|
const { order, asc } = this.state;
|
||||||
|
|
||||||
|
if (order !== field) {
|
||||||
|
this.setState({
|
||||||
|
order: field,
|
||||||
|
asc: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
asc: !asc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
listHeadItem = (item: Record<string, any>) => {
|
||||||
|
const { order, asc } = this.state;
|
||||||
|
if (item.key) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles('headItem', item.center && 'center', 'sortable-head')}
|
||||||
|
onClick={() => this.orderBy(item.key)}
|
||||||
|
>
|
||||||
|
<TextItem path={item.title} />
|
||||||
|
{order === item.key && (
|
||||||
|
<span className={styles('arrow', !asc && 'inverted')}>
|
||||||
|
<RiOrderPlayFill />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles(
|
||||||
|
'headItem',
|
||||||
|
item.center && 'center',
|
||||||
|
item.type === 'empty' && 'empty',
|
||||||
|
'not-sortable-head',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TextItem path={item.title} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
setSort = options => {
|
||||||
|
const { sortBy } = options;
|
||||||
|
this.orderBy(sortBy);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCheckVirtual = res => {
|
||||||
|
if (res.all) this.masterCheck();
|
||||||
|
|
||||||
|
if (res.item) this.onItemClick(res.item[config.idKey]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legend features
|
||||||
|
*/
|
||||||
|
|
||||||
|
resetSelectedLegend = () => {
|
||||||
|
this.setState({
|
||||||
|
selectedLegends: [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
selectLegend = key => {
|
||||||
|
const { selectedLegends } = this.state;
|
||||||
|
|
||||||
|
const newList = selectedLegends.slice();
|
||||||
|
const index = newList.indexOf(key);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
newList.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
newList.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedLegends: newList,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderStaticTable = () => {
|
||||||
|
const { noCheckbox, itemProps, headers } = this.props;
|
||||||
|
const { masterCheck } = this.state;
|
||||||
|
const list = this.filtered();
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className={styles('table-wrapper')}>
|
||||||
|
<div className={styles('table-header')}>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{!noCheckbox && (
|
||||||
|
<th onClick={this.masterCheck} className={styles('box-wrapper')}>
|
||||||
|
<div className={styles('box', masterCheck && 'checked')} />
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
<th className={styles('index')}>
|
||||||
|
{this.listHeadItem({
|
||||||
|
title: '#',
|
||||||
|
})}
|
||||||
|
</th>
|
||||||
|
{headers &&
|
||||||
|
headers.map((item, key) => (
|
||||||
|
<th
|
||||||
|
className={styles(item.class, item.type)}
|
||||||
|
colSpan={item.colSpan || 1}
|
||||||
|
key={item.title || key}
|
||||||
|
>
|
||||||
|
{this.listHeadItem(item)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className={styles('edit')} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className={styles('table-content')}>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{list.map((item, index) => (
|
||||||
|
<ItemComponent
|
||||||
|
action={() => this.onItemClick(item.id)}
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
key={item.id || index}
|
||||||
|
noCheckbox={noCheckbox}
|
||||||
|
{...itemProps}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render legend
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderLegend = () => {
|
||||||
|
const { legend } = this.props;
|
||||||
|
|
||||||
|
if (!legend || !legend.infos) return <span />;
|
||||||
|
const { selectedLegends } = this.state;
|
||||||
|
const keys = Object.keys(legend.infos);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles('legend')}>
|
||||||
|
{legend.title && (
|
||||||
|
<div className={styles('title')}>
|
||||||
|
<TextItem path={legend.title} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{keys.map((key, index) => {
|
||||||
|
const text = legend.infos[key];
|
||||||
|
if (key.indexOf('Detail') >= 0) return false;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key || index}
|
||||||
|
onClick={() => this.selectLegend(key)}
|
||||||
|
className={styles('item', key, selectedLegends.indexOf(key) >= 0 && 'selected')}
|
||||||
|
>
|
||||||
|
<div className={styles('text')}>{text}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table
|
||||||
|
*/
|
||||||
|
renderVirtualTable = () => {
|
||||||
|
const { itemProps, headers, noCheckbox, inModule, options, noModule, ref } = this.props;
|
||||||
|
const { order, asc, masterCheck, checkedItems } = this.state;
|
||||||
|
const list = this.filtered();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTable
|
||||||
|
order={order}
|
||||||
|
asc={asc}
|
||||||
|
sort={this.setSort}
|
||||||
|
headers={headers}
|
||||||
|
items={list}
|
||||||
|
noCheckbox={noCheckbox}
|
||||||
|
onCheck={val => this.handleCheckVirtual(val)}
|
||||||
|
masterCheck={masterCheck}
|
||||||
|
checkedItems={checkedItems}
|
||||||
|
inModule={inModule}
|
||||||
|
noModule={noModule}
|
||||||
|
moduleOptions={options}
|
||||||
|
{...itemProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
subhead,
|
||||||
|
actions,
|
||||||
|
actionDelete,
|
||||||
|
title,
|
||||||
|
loaded,
|
||||||
|
loading,
|
||||||
|
legend,
|
||||||
|
loadMore,
|
||||||
|
noMore,
|
||||||
|
virtual,
|
||||||
|
// filters
|
||||||
|
displayFilters,
|
||||||
|
filtersProps,
|
||||||
|
filtersInitialValues,
|
||||||
|
// reset
|
||||||
|
pagination,
|
||||||
|
currentPage,
|
||||||
|
noResultMessage,
|
||||||
|
noHeader,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const { filters, checkedItems } = this.state;
|
||||||
|
const list = this.filtered();
|
||||||
|
|
||||||
|
// const checkedList = this.getCheckedItems();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles(
|
||||||
|
'CustomFixedTable',
|
||||||
|
displayFilters && 'filters',
|
||||||
|
noHeader && 'no-header',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!noHeader && (
|
||||||
|
<div className={styles('header')}>
|
||||||
|
<div className={styles('title')}>
|
||||||
|
<h1>
|
||||||
|
<TextItem path={title} />
|
||||||
|
</h1>
|
||||||
|
{subhead && <div className={styles('subhead')}>{subhead}</div>}
|
||||||
|
</div>
|
||||||
|
<div className={styles('actions')}>
|
||||||
|
{!checkedItems || checkedItems.length === 0 ? (
|
||||||
|
actions
|
||||||
|
) : (
|
||||||
|
<ModuleButton
|
||||||
|
noMargin
|
||||||
|
relative
|
||||||
|
shadow
|
||||||
|
className={styles('delete')}
|
||||||
|
color="red"
|
||||||
|
label="general.messages.deleteSelection"
|
||||||
|
action={() => actionDelete && actionDelete(checkedItems)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{displayFilters && (
|
||||||
|
<div className={styles('filters')}>
|
||||||
|
<FiltersForm
|
||||||
|
onSubmit={this.applyFilters}
|
||||||
|
{...filtersProps}
|
||||||
|
initialValues={{
|
||||||
|
...filters,
|
||||||
|
...filtersInitialValues,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles('listing')}>
|
||||||
|
{loaded && list.length === 0 && (
|
||||||
|
<div className={styles('no-result')}>
|
||||||
|
<TextItem path={noResultMessage || 'admin.messages.noResult'} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{list.length === 0 && loading && (
|
||||||
|
<div className={styles('loading')}>
|
||||||
|
<div className={styles('loading-item')}>
|
||||||
|
<Loading loader={config.loader} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{list.length > 0 && !virtual && this.renderStaticTable()}
|
||||||
|
{list.length > 0 && virtual && this.renderVirtualTable()}
|
||||||
|
</div>
|
||||||
|
<div className={styles('footer')}>
|
||||||
|
<div className={styles('left')}>
|
||||||
|
<div className={styles('stats')}>
|
||||||
|
<TextItem path="admin.messages.numDisplayed" />
|
||||||
|
<b>{(list && list.length) || '...'}</b>
|
||||||
|
</div>
|
||||||
|
{this.renderLegend()}
|
||||||
|
</div>
|
||||||
|
<div className={styles('right')}>
|
||||||
|
{list.length > 0 && !noMore && (
|
||||||
|
<div className={styles('buttons')}>
|
||||||
|
<ModuleButton
|
||||||
|
noMargin
|
||||||
|
small
|
||||||
|
relative
|
||||||
|
loading={loading}
|
||||||
|
label="Load more"
|
||||||
|
action={() => !loading && this.loadData(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{pagination && pagination.last_page > 1 && (
|
||||||
|
<ReactPaginate
|
||||||
|
pageCount={pagination.last_page}
|
||||||
|
pageRangeDisplayed={3}
|
||||||
|
marginPagesDisplayed={1}
|
||||||
|
// initialPage={parseInt(currentPage - 1, 10)}
|
||||||
|
forcePage={parseInt((currentPage || 1) - 1, 10)}
|
||||||
|
disableInitialCallback
|
||||||
|
breakLabel="..."
|
||||||
|
onPageChange={this.loadPage}
|
||||||
|
previousLabel="Previous"
|
||||||
|
nextLabel="Next"
|
||||||
|
containerClassName={styles('pages')}
|
||||||
|
pageClassName={styles('page')}
|
||||||
|
activeClassName={styles('active')}
|
||||||
|
disabledClassName={styles('disabled')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return withRouter(FixedTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomFixedTable;
|
||||||
@ -0,0 +1,206 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
@import '~styles/adminMixins';
|
||||||
|
@import '~styles/items/checkedBox';
|
||||||
|
|
||||||
|
.CustomFixedTable {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
padding-top: $height_page_header;
|
||||||
|
padding-bottom: $height_page_footer;
|
||||||
|
|
||||||
|
&.no-header {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
@include fixedHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
@include fixedFooter;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.filters {
|
||||||
|
padding-top: $height_filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
@include fixedStructure;
|
||||||
|
// background-color: $color_dark;
|
||||||
|
height: $height_filters;
|
||||||
|
border-bottom: 1px solid $color_border;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$height_table_header: 42px;
|
||||||
|
|
||||||
|
.listing {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
padding-top: $height_table_header;
|
||||||
|
min-width: $table_min_width;
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: $height_table_header;
|
||||||
|
border-bottom: 1px solid $color_border;
|
||||||
|
box-shadow: 0 2px 20px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
th {
|
||||||
|
height: $height_table_header;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-content {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-wrapper {
|
||||||
|
@include checkedBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headItem {
|
||||||
|
padding: $table_head_spacing;
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-head {
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-right: 30px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
@include v_align;
|
||||||
|
right: 8px;
|
||||||
|
|
||||||
|
&.inverted {
|
||||||
|
transform: translateY(-50%) rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $color-grey-font;
|
||||||
|
text-transform: uppercase;
|
||||||
|
vertical-align: middle;
|
||||||
|
//opacity: 0.6;
|
||||||
|
text-align: left;
|
||||||
|
user-select: none;
|
||||||
|
@include cellStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
border-top: 1px solid $color_border;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-result,
|
||||||
|
.loading {
|
||||||
|
padding: 20px;
|
||||||
|
//background-color:$color-grey-background;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.loading-item {
|
||||||
|
width: 40px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pages {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
li {
|
||||||
|
@include middle;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
a {
|
||||||
|
border: 1px solid $color_border;
|
||||||
|
@include ball(25px);
|
||||||
|
background-color: white;
|
||||||
|
border-radius: $border_radius;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: $color-grey-background;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
a {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
@include legend;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover:before,
|
||||||
|
&.selected:before {
|
||||||
|
content: '';
|
||||||
|
@include absolute-fill;
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
&.selected {
|
||||||
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
admin/src/components/enhancers/CustomFixedTable/index.ts
Normal file
3
admin/src/components/enhancers/CustomFixedTable/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import CustomFixedTable from './CustomFixedTable';
|
||||||
|
|
||||||
|
export default CustomFixedTable;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import ModuleDetailStructure from './ModuleDetailStructure';
|
||||||
|
|
||||||
|
storiesOf('newComponents/ModuleDetailStructure', module).add('Default', () => <ModuleDetailStructure />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import ModuleDetailStructure from './ModuleDetailStructure';
|
||||||
|
|
||||||
|
describe('ModuleDetailStructure', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<ModuleDetailStructure />);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import CustomIcon from 'components/items/CustomIcon';
|
||||||
|
import ModuleButton from 'components/items/ModuleButton';
|
||||||
|
import styleIdentifiers from './moduleDetailStructure.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface StateProps {}
|
||||||
|
|
||||||
|
export interface DispatchProps {}
|
||||||
|
|
||||||
|
export interface OwnProps {}
|
||||||
|
|
||||||
|
export type ModuleDetailStructureProps = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
|
interface ModuleDetailStructureState {}
|
||||||
|
|
||||||
|
function ModuleDetailStructure(ItemComponent: React.ComponentType<any>) {
|
||||||
|
class DetailStructure extends React.Component<
|
||||||
|
ModuleDetailStructureProps,
|
||||||
|
ModuleDetailStructureState
|
||||||
|
> {
|
||||||
|
returnFieldsPart(isOption) {
|
||||||
|
const { items } = this.props;
|
||||||
|
|
||||||
|
const list = items.filter(e => {
|
||||||
|
let isInOptions = false;
|
||||||
|
|
||||||
|
if (typeof e === 'object') {
|
||||||
|
const inArray = Object.values(e);
|
||||||
|
if (inArray.filter(i => !i.side).length === 0) isInOptions = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOption) return isInOptions;
|
||||||
|
return !isInOptions;
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAction() {
|
||||||
|
const { actions } = this.props;
|
||||||
|
if (!actions) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles('action-container')}>
|
||||||
|
{actions.map((item, key) => (
|
||||||
|
<ModuleButton key={key} {...item} noMargin />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeader() {
|
||||||
|
const { title, history } = this.props;
|
||||||
|
if (!title) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles('header')}>
|
||||||
|
<div className={styles('back')} onClick={() => history.goBack()}>
|
||||||
|
<CustomIcon className={styles('icon')} icon="angle-left" />
|
||||||
|
<TextItem path="admin.buttons.back" />
|
||||||
|
</div>
|
||||||
|
<div className={styles('title')}>{title}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { actions } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={styles('ModuleDetailStructure')}>
|
||||||
|
<div className={styles('general')}>
|
||||||
|
{this.renderHeader()}
|
||||||
|
{this.returnFieldsPart().map((item, key) => (
|
||||||
|
<ItemComponent {...this.props} item={item} key={key} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{(this.returnFieldsPart(true).length !== 0 || actions) && (
|
||||||
|
<div className={styles('options')}>
|
||||||
|
{this.renderAction()}
|
||||||
|
{this.returnFieldsPart(true).map((item, key) => (
|
||||||
|
<ItemComponent {...this.props} item={item} key={key} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return withRouter(DetailStructure);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModuleDetailStructure;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import ModuleDetailStructure from './ModuleDetailStructure';
|
||||||
|
|
||||||
|
export default ModuleDetailStructure;
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
@import '~styles/adminMixins';
|
||||||
|
|
||||||
|
.ModuleDetailStructure {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.general {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 30px 30px 30px 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 2px solid $color-border;
|
||||||
|
padding: 0 0 25px 0;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back {
|
||||||
|
color: $color_primary;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 30px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
min-width: 300px;
|
||||||
|
padding: 30px;
|
||||||
|
outline: 2px solid $color-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: $color-border !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-container {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
import GenericFieldArray from './GenericFieldArray';
|
||||||
|
|
||||||
|
storiesOf('admin/formHelpers/GenericFieldArray', module)
|
||||||
|
.addDecorator(story => {
|
||||||
|
const Wrapper = reduxForm({ form: 'test' })(() => story());
|
||||||
|
return <Wrapper />;
|
||||||
|
})
|
||||||
|
.add('GenericFieldArray component', () => {
|
||||||
|
const info = {
|
||||||
|
id: { type: 'string', label: 'id' },
|
||||||
|
master: { type: 'multi', label: 'master' },
|
||||||
|
tester: { type: 'multi', label: 'tester' },
|
||||||
|
};
|
||||||
|
return <GenericFieldArray label="test" labelAdd="Add" infos={info} fields={[info]} />;
|
||||||
|
});
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
import GenericFieldArray from './GenericFieldArray';
|
||||||
|
|
||||||
|
describe('GenericFieldArray', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
const Wrapper = reduxForm({ form: 'test' })(() => (
|
||||||
|
<GenericFieldArray meta={{}} fields={{}} infos={{}} push={() => {}} />
|
||||||
|
));
|
||||||
|
shallow(<Wrapper />);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import ModuleButton from 'components/items/ModuleButton';
|
||||||
|
|
||||||
|
import GenericFields from 'components/formHelpers/GenericFields';
|
||||||
|
import GenericDropDown from 'components/pageItems/GenericDropDown';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import { FormSpy } from 'react-final-form';
|
||||||
|
import styleIdentifiers from './genericFieldArray.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface StateProps {
|
||||||
|
infos: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DispatchProps {}
|
||||||
|
|
||||||
|
export interface OwnProps {}
|
||||||
|
|
||||||
|
export type GenericFieldArrayProps = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
|
const GenericFieldArray = (props: GenericFieldArrayProps) => {
|
||||||
|
const { fields, item, formValues, infos, resourceData, finalForm, lg, activeLgs } = props;
|
||||||
|
|
||||||
|
function deleteItem(index: number) {
|
||||||
|
// could add a are you sure
|
||||||
|
fields.remove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTitle(values, name) {
|
||||||
|
const value = get(values, `${name}.${item?.dropdownLabel || 'title'}`);
|
||||||
|
|
||||||
|
if (!value) return false;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addField() {
|
||||||
|
fields.push();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles('GenericFieldArray')}>
|
||||||
|
{fields &&
|
||||||
|
fields.map(
|
||||||
|
(name, index): JSX => (
|
||||||
|
<FormSpy key={index}>
|
||||||
|
{({ values }) => (
|
||||||
|
<GenericDropDown
|
||||||
|
title={renderTitle(values, name) || `Item ${index + 1}`}
|
||||||
|
titleClassName={styles('title-dropdown')}
|
||||||
|
className={styles('list-value')}
|
||||||
|
>
|
||||||
|
<GenericFields
|
||||||
|
formValues={formValues}
|
||||||
|
push={fields}
|
||||||
|
fields={infos}
|
||||||
|
base={name}
|
||||||
|
activeLgs={activeLgs}
|
||||||
|
resourceData={resourceData}
|
||||||
|
finalForm={finalForm}
|
||||||
|
/>
|
||||||
|
<div className={styles('buttons')}>
|
||||||
|
<ModuleButton
|
||||||
|
small
|
||||||
|
className={styles('delete')}
|
||||||
|
relative
|
||||||
|
color="red"
|
||||||
|
noMargin
|
||||||
|
label={item.labelRemove || 'Remove item'}
|
||||||
|
action={() => deleteItem(index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</GenericDropDown>
|
||||||
|
)}
|
||||||
|
</FormSpy>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<ModuleButton
|
||||||
|
action={addField}
|
||||||
|
label={item.labelAdd || 'Add'}
|
||||||
|
color="primary"
|
||||||
|
small
|
||||||
|
noMargin
|
||||||
|
relative
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenericFieldArray;
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
@import '~styles/adminMixins';
|
||||||
|
|
||||||
|
.GenericFieldArray {
|
||||||
|
@include genericFields;
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
margin-top: 7px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import GenericFieldArray from './GenericFieldArray';
|
||||||
|
|
||||||
|
export default GenericFieldArray;
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, select } from '@storybook/addon-knobs';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
import GenericFields from './GenericFields';
|
||||||
|
|
||||||
|
const field = [
|
||||||
|
'string',
|
||||||
|
'separator',
|
||||||
|
'select',
|
||||||
|
'schedule',
|
||||||
|
'textarea',
|
||||||
|
'multiarea',
|
||||||
|
'multi',
|
||||||
|
'bool',
|
||||||
|
'date',
|
||||||
|
'picture',
|
||||||
|
'video',
|
||||||
|
'wysiwyg',
|
||||||
|
'multiwysiwyg',
|
||||||
|
];
|
||||||
|
|
||||||
|
storiesOf('admin/formHelpers/GenericFields', module)
|
||||||
|
.addDecorator(
|
||||||
|
(story): JSX => {
|
||||||
|
const Wrapper = reduxForm({ form: 'test' })(() => story());
|
||||||
|
return <Wrapper />;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
'GenericFields component',
|
||||||
|
(): JSX => (
|
||||||
|
<GenericFields
|
||||||
|
loadData={(): void => {}}
|
||||||
|
loadOne={(): void => {}}
|
||||||
|
push={(): void => {}}
|
||||||
|
upload={(): void => {}}
|
||||||
|
fields={{
|
||||||
|
id: { type: select('type', field, 'string'), label: text('label', 'label') },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
|
||||||
|
import GenericFields from './index';
|
||||||
|
|
||||||
|
describe('GenericFields', (): void => {
|
||||||
|
it('should render without crashing', (): void => {
|
||||||
|
const Wrapper = reduxForm({ form: 'test' })(
|
||||||
|
(): JSX => (
|
||||||
|
<GenericFields push={(): void => {}} fields={{ id: { type: 'string', label: 'label' } }} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
shallow(<Wrapper />);
|
||||||
|
});
|
||||||
|
});
|
||||||
869
admin/src/components/formHelpers/GenericFields/GenericFields.tsx
Normal file
869
admin/src/components/formHelpers/GenericFields/GenericFields.tsx
Normal file
@ -0,0 +1,869 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
|
||||||
|
import { Field as ReduxField, FieldArray as ReduxFieldArray } from 'redux-form';
|
||||||
|
|
||||||
|
import { Field as FinalFormField, FormSpy, Form } from 'react-final-form';
|
||||||
|
import { FieldArray as FinalFormFieldArray } from 'react-final-form-arrays';
|
||||||
|
|
||||||
|
import { composeValidators, geoLocValidation, required } from 'store/utils/validation';
|
||||||
|
import InputLanguage from 'components/formHelpers/InputLanguage';
|
||||||
|
import GenericFieldArray from 'components/formHelpers/GenericFieldArray';
|
||||||
|
import { viewConfig, config } from 'config/general';
|
||||||
|
import ModuleButton from 'components/items/ModuleButton';
|
||||||
|
|
||||||
|
import Input from 'components/formItems/Input';
|
||||||
|
import NumberInput from 'components/formItems/NumberInput';
|
||||||
|
import Textarea from 'components/formItems/Textarea';
|
||||||
|
import UploadZone from 'components/formItems/UploadZone';
|
||||||
|
import Password from 'components/formItems/Password';
|
||||||
|
import Wysiwyg from 'components/formItems/Wysiwyg';
|
||||||
|
import CkEditorField from 'components/formItems/CkEditorField';
|
||||||
|
import FileStackInput from 'components/formItems/FileStackInput';
|
||||||
|
|
||||||
|
import CustomSelect from 'components/formItems/CustomSelect';
|
||||||
|
import Checkbox from 'components/formItems/Checkbox';
|
||||||
|
import DateTimePicker from 'components/formItems/DateTimePicker';
|
||||||
|
|
||||||
|
import SwitchInput from 'components/formItems/SwitchInput';
|
||||||
|
import ScheduleInput from 'components/formItems/ScheduleInput';
|
||||||
|
import { handleVarInString } from 'store/utils/adminHelpers';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { clone } from 'store/utils/helper';
|
||||||
|
import ColorPicker from 'components/formItems/ColorPicker';
|
||||||
|
import TagsInput from 'components/formItems/TagsInput';
|
||||||
|
import GeoLocInput from 'components/formItems/GeoLocInput';
|
||||||
|
import JsonField from 'components/formItems/JsonField';
|
||||||
|
import PriceForm from 'components/forms/PriceForm';
|
||||||
|
import FormItemLabel from 'components/items/FormItemLabel';
|
||||||
|
import CodeEditorInput from 'components/formItems/CodeEditorInput';
|
||||||
|
import GenericDropDown from 'components/pageItems/GenericDropDown';
|
||||||
|
import LanguageSelector from 'components/pageItems/LanguageSelector';
|
||||||
|
import MultipleUploadZone from 'components/formItems/MultipleUploadZone';
|
||||||
|
import styleIdentifiers from './genericFields.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface GenericFieldsProps {
|
||||||
|
fields: Record<string, any>;
|
||||||
|
base?: any;
|
||||||
|
push: Function;
|
||||||
|
upload: Function;
|
||||||
|
loadData: Function;
|
||||||
|
loadOne: Function;
|
||||||
|
edit?: boolean;
|
||||||
|
values?: Record<string, any>;
|
||||||
|
sectionWrapperClassName: string;
|
||||||
|
sectionClassName: string;
|
||||||
|
sectionTitleClassName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GenericFieldsState {}
|
||||||
|
|
||||||
|
export default class GenericFields extends React.Component<GenericFieldsProps, GenericFieldsState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
activeLgs: [config.defaultLanguage],
|
||||||
|
errors: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getName = (item: Record<string, any>, key: string): string => {
|
||||||
|
const { base } = this.props;
|
||||||
|
|
||||||
|
const name = item.name || item.dataField || key;
|
||||||
|
return base ? `${base}.${name}` : name;
|
||||||
|
};
|
||||||
|
|
||||||
|
checkIfLanguageSelector = array => {
|
||||||
|
for (let index = 0; index < array.length; index++) {
|
||||||
|
const element = array[index];
|
||||||
|
if (element.type?.includes('multi')) return true;
|
||||||
|
if (element.fields) {
|
||||||
|
const sublevel = Object.values(element.fields);
|
||||||
|
if (this.checkIfLanguageSelector(sublevel)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
checkIfErrorIsInSection = section => {
|
||||||
|
const { errors } = this.state;
|
||||||
|
if (!errors) return false;
|
||||||
|
|
||||||
|
for (let index = 0; index < Object.keys(errors).length; index++) {
|
||||||
|
const element = Object.keys(errors)[index];
|
||||||
|
return section.includes(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
renderItem = (item: any, key: string, index): JSX => {
|
||||||
|
const {
|
||||||
|
edit,
|
||||||
|
oneSection,
|
||||||
|
sectionWrapperClassName,
|
||||||
|
sectionClassName,
|
||||||
|
sectionTitleClassName,
|
||||||
|
allOpened,
|
||||||
|
fieldClassName,
|
||||||
|
} = this.props;
|
||||||
|
const { activeLgs } = this.state;
|
||||||
|
|
||||||
|
if (!item) return false;
|
||||||
|
|
||||||
|
if (edit && (item.hideUpdate || item.noUpdate)) return false;
|
||||||
|
if (!edit && (item.hideCreate || item.noCreate)) return false;
|
||||||
|
|
||||||
|
if (item.type === 'group' || item.type === 'col' || item.type === 'column') {
|
||||||
|
return (
|
||||||
|
<div key={key} className={styles('group')}>
|
||||||
|
<GenericFields {...this.props} fields={item.fields} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === 'section') {
|
||||||
|
let hasError = false;
|
||||||
|
hasError = this.checkIfErrorIsInSection(Object.keys(item.fields));
|
||||||
|
let arrayFields = item.fields;
|
||||||
|
if (arrayFields && !Array.isArray(arrayFields)) {
|
||||||
|
arrayFields = Object.values(arrayFields);
|
||||||
|
}
|
||||||
|
if (Object.keys(item.fields)[0] === '0') {
|
||||||
|
const allName = [];
|
||||||
|
for (let i = 0; i < Object.values(item.fields).length; i++) {
|
||||||
|
const element = Object.values(item.fields)[i];
|
||||||
|
allName.push(element?.name || element?.label);
|
||||||
|
}
|
||||||
|
hasError = this.checkIfErrorIsInSection(allName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key} className={styles('section-wrapper', sectionWrapperClassName)}>
|
||||||
|
<GenericDropDown
|
||||||
|
title={item.label || 'Default'}
|
||||||
|
titleClassName={styles(
|
||||||
|
'section-title',
|
||||||
|
sectionTitleClassName,
|
||||||
|
hasError && 'section-title-error',
|
||||||
|
)}
|
||||||
|
defaultOpen={item.opened || oneSection || allOpened}
|
||||||
|
className={styles('section', item.class, sectionClassName, hasError && 'section-error')}
|
||||||
|
>
|
||||||
|
{this.checkIfLanguageSelector(arrayFields) && (
|
||||||
|
<LanguageSelector
|
||||||
|
active={activeLgs}
|
||||||
|
onSelect={e => this.setState({ activeLgs: e })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<GenericFields
|
||||||
|
{...this.props}
|
||||||
|
oneSection={false}
|
||||||
|
fields={item.fields}
|
||||||
|
activeLgs={activeLgs}
|
||||||
|
/>
|
||||||
|
</GenericDropDown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.displayIf || item.hideIf) {
|
||||||
|
return (
|
||||||
|
<div key={key} className={styles('field', fieldClassName)}>
|
||||||
|
<FormSpy>{({ values }) => this.renderField(item, key, values)}</FormSpy>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key} className={styles('field', fieldClassName)}>
|
||||||
|
{this.renderField(item, key)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkNoMargin = (item: Record<string, any>): boolean => {
|
||||||
|
const { edit } = this.props;
|
||||||
|
|
||||||
|
return item.first || item.noMargin || (!edit && item.firstCreate) || (edit && item.firstUpdate);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderField = (item: any, key: string, formValues: object): JSX => {
|
||||||
|
const {
|
||||||
|
base,
|
||||||
|
resourceData,
|
||||||
|
upload,
|
||||||
|
loadData,
|
||||||
|
edit,
|
||||||
|
uploadS3,
|
||||||
|
finalForm,
|
||||||
|
propsInput,
|
||||||
|
uploadApollo,
|
||||||
|
loadApolloData,
|
||||||
|
fieldsProps,
|
||||||
|
dialogShow,
|
||||||
|
dialogHide,
|
||||||
|
activeLgs,
|
||||||
|
lg,
|
||||||
|
form,
|
||||||
|
value,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// Default inputs props
|
||||||
|
const inputProps =
|
||||||
|
(propsInput && viewConfig && { ...propsInput, ...viewConfig.inputProps }) ||
|
||||||
|
(viewConfig && viewConfig.inputProps) ||
|
||||||
|
propsInput ||
|
||||||
|
{};
|
||||||
|
|
||||||
|
if (!edit && item.type === 'link') return false;
|
||||||
|
|
||||||
|
const type = item.type;
|
||||||
|
|
||||||
|
const name = this.getName(item, key);
|
||||||
|
|
||||||
|
// Override field props
|
||||||
|
let overwriteProps = {};
|
||||||
|
|
||||||
|
// Override by type
|
||||||
|
if (fieldsProps && fieldsProps[type]) overwriteProps = fieldsProps[type];
|
||||||
|
|
||||||
|
// Override by field name
|
||||||
|
if (fieldsProps && fieldsProps[name]) {
|
||||||
|
// extend type fields (if exists) with field type
|
||||||
|
overwriteProps = {
|
||||||
|
...overwriteProps,
|
||||||
|
...fieldsProps[name],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayIf = item.displayIf;
|
||||||
|
const hideIf = item.hideIf;
|
||||||
|
|
||||||
|
if (item.hide) return false;
|
||||||
|
|
||||||
|
// Conditional display / Hide
|
||||||
|
if (displayIf && Array.isArray(displayIf)) {
|
||||||
|
// 1. check current level
|
||||||
|
let check = _.get(formValues, `${base}.${displayIf[0]}`);
|
||||||
|
// 2. check root level
|
||||||
|
if (!check) check = _.get(formValues, displayIf[0]);
|
||||||
|
|
||||||
|
if (!check || check !== displayIf[1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (displayIf && typeof displayIf === 'object') {
|
||||||
|
const keys = Object.keys(displayIf);
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
// 1. check current level
|
||||||
|
let check = _.get(formValues, `${base}.${keys[i]}`);
|
||||||
|
// 2. check root level
|
||||||
|
if (!check) check = _.get(formValues, keys[i]);
|
||||||
|
|
||||||
|
if (!check || check !== displayIf[keys[i]]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hideIf && Array.isArray(hideIf)) {
|
||||||
|
// 1. check current level
|
||||||
|
let check = _.get(formValues, `${base}.${hideIf[0]}`);
|
||||||
|
// 2. check root level
|
||||||
|
if (!check) check = _.get(formValues, hideIf[0]);
|
||||||
|
|
||||||
|
if (check && check === hideIf[1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (hideIf && typeof hideIf === 'object') {
|
||||||
|
const keys = Object.keys(hideIf);
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
// 1. check current level
|
||||||
|
let check = _.get(formValues, `${base}.${keys[i]}`);
|
||||||
|
// 2. check root level
|
||||||
|
if (!check) check = _.get(formValues, keys[i]);
|
||||||
|
if (check && check === hideIf[keys[i]]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Field = finalForm ? FinalFormField : ReduxField;
|
||||||
|
const FieldArray = finalForm ? FinalFormFieldArray : ReduxFieldArray;
|
||||||
|
|
||||||
|
const commonProps = {
|
||||||
|
...inputProps,
|
||||||
|
defaultValue: item.defaultValue,
|
||||||
|
label: item.label || key,
|
||||||
|
warning: item.warning,
|
||||||
|
name,
|
||||||
|
// override button class (need to be implemented in components)
|
||||||
|
button: ModuleButton,
|
||||||
|
validate: item.required ? required : null,
|
||||||
|
noMargin: this.checkNoMargin(item),
|
||||||
|
info: item.info,
|
||||||
|
labelClassName: styles('generic-label', type === 'bool' && 'radio-generic-label'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === 'string' || type === 'input' || type === 'text') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
component={Input}
|
||||||
|
{...commonProps}
|
||||||
|
min={item.min}
|
||||||
|
max={item.max}
|
||||||
|
disabled={item.disabled || type === 'text'}
|
||||||
|
onClick={() => (item.disabled || type === 'text') && item.onClick && item.onClick(value)}
|
||||||
|
placeholder={item.placeholder}
|
||||||
|
initialValue={item.initialValue}
|
||||||
|
inputType={item.inputType || 'text'}
|
||||||
|
label={item.label || key}
|
||||||
|
validate={item.required ? required : null}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'textarea') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={Textarea}
|
||||||
|
border
|
||||||
|
rows={item.rows || 4}
|
||||||
|
type={item.inputType || 'text'}
|
||||||
|
label={item.label || key}
|
||||||
|
validate={item.required ? required : null}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'tags') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={TagsInput}
|
||||||
|
name={name}
|
||||||
|
label={item.label || key}
|
||||||
|
validate={item.required ? required : null}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'password') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
noMargin={this.checkNoMargin(item)}
|
||||||
|
component={Password}
|
||||||
|
name={name}
|
||||||
|
label={item.label || key}
|
||||||
|
validate={item.required ? required : null}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'switch') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={SwitchInput}
|
||||||
|
wide={item.wide}
|
||||||
|
left={item.left}
|
||||||
|
right={item.right}
|
||||||
|
position={item.position || 'right'}
|
||||||
|
labelClassName={styles('label-switch')}
|
||||||
|
name={name}
|
||||||
|
type={item.inputType || 'text'}
|
||||||
|
label={item.label || key}
|
||||||
|
validate={item.required ? required : null}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'number') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={NumberInput}
|
||||||
|
suffix={item.suffix || ''}
|
||||||
|
decimalSeparator={item.decimalSeparator || '.'}
|
||||||
|
decimalScale={item.decimalScale || 2}
|
||||||
|
thousandSeparator={item.thousandSeparator || ' '}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'link') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={Input}
|
||||||
|
disabled
|
||||||
|
inputProps={inputProps}
|
||||||
|
type={item.inputType || 'text'}
|
||||||
|
label={item.label || key}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'multi') {
|
||||||
|
return (
|
||||||
|
<InputLanguage
|
||||||
|
{...commonProps}
|
||||||
|
redux={!finalForm}
|
||||||
|
item={item}
|
||||||
|
disabled={item.disabled}
|
||||||
|
inputProps={inputProps}
|
||||||
|
warning={item.warning}
|
||||||
|
activeLgs={activeLgs}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'multiarea') {
|
||||||
|
return (
|
||||||
|
<InputLanguage
|
||||||
|
{...commonProps}
|
||||||
|
autoResize={item.autoResize || true}
|
||||||
|
redux={!finalForm}
|
||||||
|
type="textarea"
|
||||||
|
item={item}
|
||||||
|
disabled={item.disabled}
|
||||||
|
inputProps={inputProps}
|
||||||
|
warning={item.warning}
|
||||||
|
activeLgs={activeLgs}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'multiwysiwyg') {
|
||||||
|
let uploadFunc;
|
||||||
|
if (item.s3Upload) uploadFunc = uploadS3;
|
||||||
|
if (item.apollo) uploadFunc = uploadApollo;
|
||||||
|
else uploadFunc = upload;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputLanguage
|
||||||
|
{...commonProps}
|
||||||
|
redux={!finalForm}
|
||||||
|
type="wysiwyg"
|
||||||
|
item={item}
|
||||||
|
disabled={item.disabled}
|
||||||
|
inputProps={inputProps}
|
||||||
|
upload={uploadFunc}
|
||||||
|
apollo={item.apollo}
|
||||||
|
uploadUrl={item.url || 'pictures'}
|
||||||
|
warning={item.warning}
|
||||||
|
activeLgs={activeLgs}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'multicode') {
|
||||||
|
return (
|
||||||
|
<InputLanguage
|
||||||
|
{...commonProps}
|
||||||
|
autoResize={item.autoResize || true}
|
||||||
|
redux={!finalForm}
|
||||||
|
type="code"
|
||||||
|
language={item.language}
|
||||||
|
item={item}
|
||||||
|
disabled={item.disabled}
|
||||||
|
activeLgs={activeLgs}
|
||||||
|
inputProps={inputProps}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'separator') {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles(
|
||||||
|
'separator',
|
||||||
|
item.noMargin && 'no-margin',
|
||||||
|
item.small && 'intermediary',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<h3>{item.label}</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'price') {
|
||||||
|
return (
|
||||||
|
<ModuleButton
|
||||||
|
label="Manage price"
|
||||||
|
color="primary"
|
||||||
|
action={() =>
|
||||||
|
dialogShow({
|
||||||
|
id: 'custom',
|
||||||
|
large: true,
|
||||||
|
children: (
|
||||||
|
<Form
|
||||||
|
component={PriceForm}
|
||||||
|
initialValues={{
|
||||||
|
price: formValues?.price || value?.price,
|
||||||
|
vatClassId: formValues?.vatClassId || value?.vatClassId,
|
||||||
|
currency: formValues?.currency || value?.currency,
|
||||||
|
}}
|
||||||
|
onSubmit={val => {
|
||||||
|
dialogHide('custom');
|
||||||
|
if (form) {
|
||||||
|
form.change('price', val.price);
|
||||||
|
form.change('currency', val.currency);
|
||||||
|
form.change('vatClassId', val.vatClassId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'datetime' || type === 'date') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
{...item}
|
||||||
|
component={DateTimePicker}
|
||||||
|
name={name}
|
||||||
|
label={item.label || key}
|
||||||
|
validate={item.required ? required : null}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'list') {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{item.label && (
|
||||||
|
<div className={styles('separator', this.checkNoMargin(item) && 'no-margin')}>
|
||||||
|
<h3>{item.label}</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles('list-values', this.checkNoMargin(item) && 'no-margin')}>
|
||||||
|
<FieldArray
|
||||||
|
component={GenericFieldArray}
|
||||||
|
lg={lg}
|
||||||
|
name={name}
|
||||||
|
finalForm={finalForm}
|
||||||
|
formValues={formValues}
|
||||||
|
infos={item.fields}
|
||||||
|
item={item}
|
||||||
|
resourceData={resourceData}
|
||||||
|
activeLgs={activeLgs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'select') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
component={CustomSelect}
|
||||||
|
{...commonProps}
|
||||||
|
orderBy={item.order}
|
||||||
|
onlyItemsValues={item.onlyItemsValues}
|
||||||
|
items={
|
||||||
|
(item.itemsDataKey && resourceData && resourceData[item.itemsDataKey]) ||
|
||||||
|
item.values ||
|
||||||
|
item.list
|
||||||
|
}
|
||||||
|
apollo={item.apollo}
|
||||||
|
asObject={item.asObject}
|
||||||
|
multipleSelection={item.multiple || item.multipleSelection}
|
||||||
|
filter={!item.noFilter}
|
||||||
|
inputs={item.inputs || {}}
|
||||||
|
apiPayload={item.apiPayload || item.api || null}
|
||||||
|
noMountApi={item.noMountApi}
|
||||||
|
noFilterApi={item.noFilterApi}
|
||||||
|
copyObject={item.copyObject}
|
||||||
|
loadApiData={item.apollo ? loadApolloData : loadData}
|
||||||
|
modelValues={item.modelValues}
|
||||||
|
noHttpHeader={item.noHttpHeader || false}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'geoloc') {
|
||||||
|
let validation;
|
||||||
|
if (finalForm) {
|
||||||
|
if (item.required) {
|
||||||
|
validation = composeValidators(item.required && required, geoLocValidation);
|
||||||
|
} else validation = geoLocValidation;
|
||||||
|
} else {
|
||||||
|
validation = [geoLocValidation];
|
||||||
|
if (item.required) validation.push(required);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
component={GeoLocInput}
|
||||||
|
multiple={item.multiple}
|
||||||
|
{...inputProps}
|
||||||
|
{...commonProps}
|
||||||
|
{...overwriteProps}
|
||||||
|
validate={validation}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'bool') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={Checkbox}
|
||||||
|
type="checkbox"
|
||||||
|
useBox
|
||||||
|
activeClassName={styles('active')}
|
||||||
|
items={item.values}
|
||||||
|
disabled={item.disabled}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'color') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={ColorPicker}
|
||||||
|
saveValue={item.saveValue}
|
||||||
|
disabled={item.disabled}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'filestack') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={FileStackInput}
|
||||||
|
info={item.label || key}
|
||||||
|
validate={item.required ? required : null}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'schedule') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={ScheduleInput}
|
||||||
|
noMargin={this.checkNoMargin(item)}
|
||||||
|
name={name}
|
||||||
|
initialValue={item.initialValue}
|
||||||
|
label={item.label || key}
|
||||||
|
noValidation={item.noValidation}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'wysiwyg') {
|
||||||
|
let uploadFunc;
|
||||||
|
if (item.s3Upload) uploadFunc = uploadS3;
|
||||||
|
if (item.apollo) uploadFunc = uploadApollo;
|
||||||
|
else uploadFunc = upload;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={CkEditorField}
|
||||||
|
upload={uploadFunc}
|
||||||
|
apollo={item.apollo}
|
||||||
|
uploadUrl={item.url || 'pictures'}
|
||||||
|
noResize={item.noResize}
|
||||||
|
name={name}
|
||||||
|
label={item.label || key}
|
||||||
|
validate={item.required ? required : null}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'wysiwyg2') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={Wysiwyg}
|
||||||
|
customOptions={item.customOptions}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'code') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={CodeEditorInput}
|
||||||
|
language={item.language}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File part
|
||||||
|
let filePrefix = item.s3UploadPrefix || item.uploadPrefix || item.prefix;
|
||||||
|
filePrefix = filePrefix && handleVarInString(resourceData || formValues, filePrefix);
|
||||||
|
if (filePrefix) {
|
||||||
|
filePrefix = filePrefix
|
||||||
|
.toLowerCase()
|
||||||
|
.split(' ')
|
||||||
|
.join('_');
|
||||||
|
}
|
||||||
|
|
||||||
|
let uploadFunc;
|
||||||
|
if (item.s3Upload) uploadFunc = uploadS3;
|
||||||
|
if (item.apollo) uploadFunc = uploadApollo;
|
||||||
|
else uploadFunc = upload;
|
||||||
|
|
||||||
|
const uploadProps = {
|
||||||
|
component: UploadZone,
|
||||||
|
s3Upload: item.s3Upload,
|
||||||
|
uploadPrefix: filePrefix,
|
||||||
|
noFileName: item.noFileName || item.noRename,
|
||||||
|
upload: uploadFunc,
|
||||||
|
noUnique: item.noUnique,
|
||||||
|
apollo: item.apollo,
|
||||||
|
uploadPath: item.apiPath,
|
||||||
|
asObject: item.asObject,
|
||||||
|
showProgress: item.showProgress,
|
||||||
|
treatmentFile: item.treatmentFile,
|
||||||
|
saveValue: item.saveValue,
|
||||||
|
subInfo: item.subInfo,
|
||||||
|
inputs: item.inputs,
|
||||||
|
noUpload: item.noUpload,
|
||||||
|
displayValue: item.displayValue || false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === 'file') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
{...uploadProps}
|
||||||
|
type={type}
|
||||||
|
uploadUrl={item.url || 'files'}
|
||||||
|
accept={item.allowFile || item.accept || 'application/*'}
|
||||||
|
noPreview={!item.apollo}
|
||||||
|
filePreview
|
||||||
|
noResize
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'video') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
{...uploadProps}
|
||||||
|
type={type}
|
||||||
|
uploadUrl={item.url || 'videos'}
|
||||||
|
accept="video/*"
|
||||||
|
noResize
|
||||||
|
videoPreview
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'picture') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
{...uploadProps}
|
||||||
|
type={type}
|
||||||
|
uploadUrl={item.url || 'pictures'}
|
||||||
|
cropperOptions={item.cropperOptions}
|
||||||
|
withLibrary
|
||||||
|
aspectRatio={item.aspectRatio || item.ratio}
|
||||||
|
accept="image/*"
|
||||||
|
noResize={item.noResize}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'multiFiles') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
{...uploadProps}
|
||||||
|
component={MultipleUploadZone}
|
||||||
|
type={type}
|
||||||
|
oneFile={item.oneFile}
|
||||||
|
renderPreviewTop={item.renderPreviewTop}
|
||||||
|
previewType="file"
|
||||||
|
accept="application/*"
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'multiPictures') {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
{...uploadProps}
|
||||||
|
component={MultipleUploadZone}
|
||||||
|
type={type}
|
||||||
|
oneFile={item.oneFile}
|
||||||
|
renderPreviewTop={item.renderPreviewTop}
|
||||||
|
previewType="picture"
|
||||||
|
// cropperOptions={item.cropperOptions}
|
||||||
|
// withLibrary
|
||||||
|
displayValue={item.displayValue || false}
|
||||||
|
aspectRatio={item.aspectRatio || item.ratio}
|
||||||
|
accept="image/*"
|
||||||
|
noResize={item.noResize}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'json') {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{item.label && (
|
||||||
|
<div className={styles('label', this.checkNoMargin(item) && 'no-margin')}>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Field
|
||||||
|
{...commonProps}
|
||||||
|
component={JsonField}
|
||||||
|
name={name}
|
||||||
|
infos={item.fields}
|
||||||
|
item={item}
|
||||||
|
resourceData={resourceData}
|
||||||
|
{...overwriteProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): JSX {
|
||||||
|
const { fields, item, className } = this.props;
|
||||||
|
const data = fields || item;
|
||||||
|
const keys = Object.keys(data);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles('GenericFields', className)}>
|
||||||
|
<FormSpy
|
||||||
|
subscription={{ submitFailed: true, errors: true }}
|
||||||
|
onChange={e => {
|
||||||
|
if (e?.submitFailed) this.setState({ errors: e?.errors });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{keys.map((key, index) => this.renderItem(data[key], key, index))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
@import '~styles/adminMixins';
|
||||||
|
|
||||||
|
.GenericFields {
|
||||||
|
@include genericFields;
|
||||||
|
|
||||||
|
.section {
|
||||||
|
&.section-error {
|
||||||
|
box-shadow: 4px 4px 10px 0 rgba($color: $color_error, $alpha: 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
&.section-title-error {
|
||||||
|
color: $color_error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.generic-label {
|
||||||
|
color: $color_primary;
|
||||||
|
letter-spacing: 0;
|
||||||
|
&.radio-generic-label {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-values {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
&.no-margin {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background-color: $color_primary;
|
||||||
|
border-color: $color_primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
@include separator;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-switch {
|
||||||
|
font-weight: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
color: #8c96a3;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
pointer-events: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
admin/src/components/formHelpers/GenericFields/index.ts
Normal file
27
admin/src/components/formHelpers/GenericFields/index.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import generic from 'store/generic';
|
||||||
|
import genericApollo from 'store/genericApollo';
|
||||||
|
import file from 'store/file';
|
||||||
|
import app from 'store/app';
|
||||||
|
import GenericFields, { GenericFieldsProps } from './GenericFields';
|
||||||
|
|
||||||
|
const mapStateToProps = (state): Record<string, any> => ({
|
||||||
|
content: state.content.raw,
|
||||||
|
lg: state.content.lg,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
loadData: generic.actions.search.request.action,
|
||||||
|
loadApolloData: genericApollo.actions.search.request.action,
|
||||||
|
loadOne: generic.actions.detail.request.action,
|
||||||
|
upload: generic.actions.upload.request.action,
|
||||||
|
uploadS3: generic.actions.uploadS3Url.request.action,
|
||||||
|
uploadApollo: file.actions.upload.request.action,
|
||||||
|
dialogShow: app.actions.dialog.show.action,
|
||||||
|
dialogHide: app.actions.dialog.hide.action,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapped = connect<GenericFieldsProps>(mapStateToProps, mapDispatchToProps)(GenericFields);
|
||||||
|
|
||||||
|
export default Wrapped;
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
import { object } from '@storybook/addon-knobs';
|
||||||
|
import InputLanguage from './InputLanguage';
|
||||||
|
|
||||||
|
|
||||||
|
storiesOf('admin/formHelpers/InputLanguage', module)
|
||||||
|
.addDecorator(story => {
|
||||||
|
|
||||||
|
const Wrapper = reduxForm({ form: 'test' })(() => story());
|
||||||
|
return <Wrapper />;
|
||||||
|
})
|
||||||
|
.add('default', () => <InputLanguage name="test" item={{ label: 'test' }} />)
|
||||||
|
.add('custom', () => (
|
||||||
|
<InputLanguage
|
||||||
|
name="test"
|
||||||
|
item={{ label: 'test' }}
|
||||||
|
inputProps={object('inputProps', {
|
||||||
|
withBorder: true,
|
||||||
|
small: true,
|
||||||
|
// placeholder: 'type here...',
|
||||||
|
// label: 'Text',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
|
||||||
|
import InputLanguage from './InputLanguage';
|
||||||
|
|
||||||
|
describe('InputLanguage', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
const Wrapper = reduxForm({ form: 'test' })(() => (
|
||||||
|
<InputLanguage name="test" item={{ label: 'test' }} />
|
||||||
|
));
|
||||||
|
shallow(<Wrapper />);
|
||||||
|
});
|
||||||
|
});
|
||||||
233
admin/src/components/formHelpers/InputLanguage/InputLanguage.tsx
Normal file
233
admin/src/components/formHelpers/InputLanguage/InputLanguage.tsx
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
// import TextItem from 'components/items/TextItem';
|
||||||
|
import { Field as FinalField } from 'react-final-form';
|
||||||
|
import { Field as ReduxField } from 'redux-form';
|
||||||
|
|
||||||
|
import Input from 'components/formItems/Input';
|
||||||
|
import Textarea from 'components/formItems/Textarea';
|
||||||
|
import Wysiwyg from 'components/formItems/Wysiwyg';
|
||||||
|
import CkEditorField from 'components/formItems/CkEditorField';
|
||||||
|
import CodeEditorInput from 'components/formItems/CodeEditorInput';
|
||||||
|
|
||||||
|
import { config } from 'config/general';
|
||||||
|
import { required } from 'store/utils/validation';
|
||||||
|
import FormItemLabel from 'components/items/FormItemLabel';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { FaLock, FaUnlock } from 'react-icons/fa';
|
||||||
|
import ModuleButton from 'components/items/ModuleButton';
|
||||||
|
import styleIdentifiers from './inputLanguage.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface InputLanguageProps {
|
||||||
|
item?: Record<string, any>;
|
||||||
|
name: string;
|
||||||
|
type?: string;
|
||||||
|
inputProps?: Record<string, any>;
|
||||||
|
noMargin?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InputLanguageState {}
|
||||||
|
|
||||||
|
export default class InputLanguage extends React.Component<InputLanguageProps, InputLanguageState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const { lg } = this.props;
|
||||||
|
|
||||||
|
const firstDiff = _.find(config.languages, it => {
|
||||||
|
const l = it.short || it.value;
|
||||||
|
return l !== lg;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
displayAllLanguages: false,
|
||||||
|
mainLanguage: lg,
|
||||||
|
currentLg: firstDiff ? firstDiff.short || firstDiff.value : lg,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponent = () => {
|
||||||
|
const { type } = this.props;
|
||||||
|
if (type === 'textarea') return Textarea;
|
||||||
|
if (type === 'wysiwyg') return CkEditorField;
|
||||||
|
if (type === 'code') return CodeEditorInput;
|
||||||
|
|
||||||
|
return Input;
|
||||||
|
};
|
||||||
|
|
||||||
|
checkMatch = lgCode => {
|
||||||
|
const { matches, matchClassName } = this.props;
|
||||||
|
|
||||||
|
if (!matches) return '';
|
||||||
|
|
||||||
|
if (matches.indexOf(lgCode) >= 0) {
|
||||||
|
return matchClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
switchLanguage = target => {
|
||||||
|
const { currentLg, mainLanguage } = this.state;
|
||||||
|
|
||||||
|
// switch main language
|
||||||
|
if (currentLg === target) {
|
||||||
|
this.setState({
|
||||||
|
mainLanguage: target,
|
||||||
|
currentLg: mainLanguage,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
currentLg: target,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleDisplayLanguages = () => {
|
||||||
|
const { displayAllLanguages } = this.state;
|
||||||
|
this.setState({
|
||||||
|
displayAllLanguages: !displayAllLanguages,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
redux,
|
||||||
|
item,
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
inputProps,
|
||||||
|
noMargin,
|
||||||
|
lock,
|
||||||
|
locked,
|
||||||
|
activeLgs,
|
||||||
|
...rest
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const { currentLg, displayAllLanguages, mainLanguage } = this.state;
|
||||||
|
|
||||||
|
const Field = redux ? ReduxField : FinalField;
|
||||||
|
|
||||||
|
const numLg = config.languages.length;
|
||||||
|
|
||||||
|
let languages =
|
||||||
|
numLg <= config.maxNumLanguagesDisplayed
|
||||||
|
? config.languages
|
||||||
|
: [_.find(config.languages, lg => lg.short === mainLanguage || lg.value === mainLanguage)];
|
||||||
|
if (displayAllLanguages) languages = config.languages;
|
||||||
|
|
||||||
|
if (activeLgs) languages = languages.filter(e => activeLgs?.includes(e.value || e.short));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles('InputLanguage', noMargin && 'no-margin')}>
|
||||||
|
{label && <FormItemLabel {...inputProps} {...this.props} label={label} />}
|
||||||
|
{languages.map((lang, index) => {
|
||||||
|
const lgCode = lang.value || lang.short;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={lgCode || index}
|
||||||
|
className={styles('language-wrapper', lgCode.length > 2 && 'large')}
|
||||||
|
>
|
||||||
|
<div className={styles('language-label', type)}>{lgCode}</div>
|
||||||
|
<div className={styles('field-wrapper-container')}>
|
||||||
|
<Field
|
||||||
|
{...rest}
|
||||||
|
{...inputProps}
|
||||||
|
// adding min rows
|
||||||
|
rows={2}
|
||||||
|
component={this.getComponent()}
|
||||||
|
fieldWrapperClassName={styles(
|
||||||
|
inputProps.fieldWrapperClassName,
|
||||||
|
this.checkMatch(lgCode),
|
||||||
|
)}
|
||||||
|
name={`${name}.${lgCode}`}
|
||||||
|
type="text"
|
||||||
|
noMargin
|
||||||
|
validate={item && item.required ? required : null}
|
||||||
|
/>
|
||||||
|
{lock && (
|
||||||
|
<div
|
||||||
|
className={styles(
|
||||||
|
'icon-locker',
|
||||||
|
locked && 'active',
|
||||||
|
type === 'wysiwyg' && 'with-wysiwyg',
|
||||||
|
)}
|
||||||
|
onClick={lock}
|
||||||
|
>
|
||||||
|
{locked ? <FaLock /> : <FaUnlock />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{numLg > config.maxNumLanguagesDisplayed && (
|
||||||
|
<>
|
||||||
|
{!displayAllLanguages && (
|
||||||
|
<div className={styles('language-wrapper', currentLg.length > 2 && 'large')}>
|
||||||
|
<div className={styles('language-label', type)}>{currentLg}</div>
|
||||||
|
<div className={styles('field-wrapper-container')}>
|
||||||
|
<Field
|
||||||
|
{...rest}
|
||||||
|
{...inputProps}
|
||||||
|
rows={2}
|
||||||
|
component={this.getComponent()}
|
||||||
|
fieldWrapperClassName={styles(
|
||||||
|
inputProps.fieldWrapperClassName,
|
||||||
|
this.checkMatch(currentLg),
|
||||||
|
)}
|
||||||
|
name={`${name}.${currentLg}`}
|
||||||
|
type="text"
|
||||||
|
noMargin
|
||||||
|
validate={item && item.required ? required : null}
|
||||||
|
/>
|
||||||
|
{lock && (
|
||||||
|
<div
|
||||||
|
className={styles(
|
||||||
|
'icon-locker',
|
||||||
|
locked && 'active',
|
||||||
|
type === 'wysiwyg' && 'with-wysiwyg',
|
||||||
|
)}
|
||||||
|
onClick={lock}
|
||||||
|
>
|
||||||
|
{locked ? <FaLock /> : <FaUnlock />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles('language-switch')}>
|
||||||
|
{!displayAllLanguages &&
|
||||||
|
config.languages.map((l, key) => {
|
||||||
|
const lgCode = l.short || l.value;
|
||||||
|
// if (label === currentLg) return false;
|
||||||
|
if (lgCode === mainLanguage) return false;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
onClick={() => this.switchLanguage(lgCode)}
|
||||||
|
className={styles('language-label', currentLg === lgCode && 'active')}
|
||||||
|
>
|
||||||
|
{lgCode}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ModuleButton
|
||||||
|
className={styles('button')}
|
||||||
|
noMargin
|
||||||
|
color="grey"
|
||||||
|
small
|
||||||
|
relative
|
||||||
|
label={displayAllLanguages ? 'admin.buttons.hideAll' : 'admin.buttons.showAll'}
|
||||||
|
action={this.toggleDisplayLanguages}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
admin/src/components/formHelpers/InputLanguage/index.ts
Normal file
13
admin/src/components/formHelpers/InputLanguage/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import InputLanguage from './InputLanguage';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StoreProps) => ({
|
||||||
|
lg: state.content.lg,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {};
|
||||||
|
|
||||||
|
const Wrapped = connect(mapStateToProps, mapDispatchToProps)(InputLanguage);
|
||||||
|
|
||||||
|
export default Wrapped;
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
|
||||||
|
.InputLanguage {
|
||||||
|
.language-wrapper {
|
||||||
|
margin-top: 8px;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 17px;
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
padding-left: 43px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-label {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
top: 11px;
|
||||||
|
opacity: 0.4;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
margin-top: $form_spacing;
|
||||||
|
|
||||||
|
&.no-margin {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-wrapper-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.icon-locker {
|
||||||
|
@include v_align;
|
||||||
|
display: flex;
|
||||||
|
right: 5px;
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.2;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.with-wysiwyg {
|
||||||
|
bottom: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
top: auto;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.language-switch {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.button {
|
||||||
|
margin: 4px 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.language-label {
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 4px;
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: pointer;
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { reduxForm, Field } from 'redux-form';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
import CkEditorField from './CkEditorField';
|
||||||
|
|
||||||
|
storiesOf('seed/formItems/CkEditorField', module)
|
||||||
|
.addDecorator(story => {
|
||||||
|
const Wrapper = reduxForm({ form: 'test' })(() => story());
|
||||||
|
return <Wrapper />;
|
||||||
|
})
|
||||||
|
.add('Normal', () => <Field name="test" component={CkEditorField} label="test" />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import CkEditorField from './CkEditorField';
|
||||||
|
|
||||||
|
describe('CkEditorField', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<CkEditorField />);
|
||||||
|
});
|
||||||
|
});
|
||||||
253
admin/src/components/formItems/CkEditorField/CkEditorField.tsx
Normal file
253
admin/src/components/formItems/CkEditorField/CkEditorField.tsx
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
|
||||||
|
import CKEditor from '@ckeditor/ckeditor5-react';
|
||||||
|
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
|
||||||
|
import { getFileName } from 'store/utils/api';
|
||||||
|
|
||||||
|
import FormElement from 'components/structure/FormElement';
|
||||||
|
import FieldWrapper from 'components/structure/FieldWrapper';
|
||||||
|
|
||||||
|
import Cropper from 'components/global/Cropper';
|
||||||
|
|
||||||
|
import CustomIcon from 'components/items/CustomIcon';
|
||||||
|
import { FaExpandAlt } from 'react-icons/fa';
|
||||||
|
import CodeEditorInput from 'components/formItems/CodeEditorInput';
|
||||||
|
import styleIdentifiers from './ckEditorField.scss';
|
||||||
|
import UploadAdapter from './UploadAdapter';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
type CkEditorFieldProps = FormElementProps &
|
||||||
|
FieldWrapperProps &
|
||||||
|
FormItemLabelProps & {
|
||||||
|
name: string;
|
||||||
|
placeholder?: string;
|
||||||
|
dialogShow: Function;
|
||||||
|
dialogHide: Function;
|
||||||
|
aspectRatio?: number;
|
||||||
|
upload: Function;
|
||||||
|
uploadUrl: string;
|
||||||
|
noResize?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CkEditorFieldState {
|
||||||
|
code: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CkEditorField extends React.Component<CkEditorFieldProps, CkEditorFieldState> {
|
||||||
|
constructor(props: CkEditorFieldProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
code: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a custom adapter
|
||||||
|
myAdapter = (editor: any) => {
|
||||||
|
const { dialogHide } = this.props;
|
||||||
|
|
||||||
|
editor.plugins.get('FileRepository').createUploadAdapter = loader =>
|
||||||
|
// Attach a custom upload adapter and provide the loader
|
||||||
|
new UploadAdapter({
|
||||||
|
loader,
|
||||||
|
onFileSelected: this.onFileSelected,
|
||||||
|
cancel: () => dialogHide('custom'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onFileSelected = (file: File, callback: Function, progressFunc?: Function) => {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
const { noResize } = this.props;
|
||||||
|
|
||||||
|
if (noResize) {
|
||||||
|
this.uploadImage(file, callback, progressFunc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// read file
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = () => {
|
||||||
|
const fileAsBinaryString = reader.result;
|
||||||
|
|
||||||
|
self.resizeImage(fileAsBinaryString, callback, progressFunc);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onabort = () => {
|
||||||
|
callback({
|
||||||
|
error: true,
|
||||||
|
message: 'file reading was aborted',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
callback({
|
||||||
|
error: true,
|
||||||
|
message: 'file reading has failed',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use cropper to resize
|
||||||
|
resizeImage = (binary: string, callback: Function, progressFunc?: Function) => {
|
||||||
|
const { dialogShow, aspectRatio, apollo } = this.props;
|
||||||
|
// return;
|
||||||
|
dialogShow({
|
||||||
|
id: 'custom',
|
||||||
|
medium: true,
|
||||||
|
noBackgroundClose: true,
|
||||||
|
closeButton: 'light',
|
||||||
|
closePosition: 'right',
|
||||||
|
children: (
|
||||||
|
<Cropper
|
||||||
|
aspectRatio={aspectRatio}
|
||||||
|
apollo={apollo}
|
||||||
|
src={binary}
|
||||||
|
action={file => this.uploadImage(file, callback, progressFunc)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
uploadImage = (file: File, callback: Function, progressFunc?: Function) => {
|
||||||
|
const { dialogHide, upload, uploadUrl, apollo, uploadPrefix } = this.props;
|
||||||
|
|
||||||
|
if (apollo && upload) {
|
||||||
|
upload({
|
||||||
|
data: {
|
||||||
|
title: getFileName(uploadPrefix, file, false),
|
||||||
|
base64: file,
|
||||||
|
},
|
||||||
|
callback,
|
||||||
|
});
|
||||||
|
} else if (!apollo) {
|
||||||
|
if (uploadUrl && upload) {
|
||||||
|
upload({
|
||||||
|
url: uploadUrl,
|
||||||
|
file,
|
||||||
|
fileName: getFileName(uploadPrefix, file, false),
|
||||||
|
callback,
|
||||||
|
noLoading: true,
|
||||||
|
callbackError: errorData => {
|
||||||
|
callback({
|
||||||
|
...errorData,
|
||||||
|
error: true,
|
||||||
|
message: 'upload failed, server issue',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
progress: data => {
|
||||||
|
if (progressFunc) progressFunc(data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback({
|
||||||
|
error: true,
|
||||||
|
message: 'no upload data provided',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogHide('custom');
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChange = (event: KeyboardEvent, editor: Record<string, any>) => {
|
||||||
|
const { input } = this.props;
|
||||||
|
|
||||||
|
const data = editor.getData();
|
||||||
|
|
||||||
|
if (input) input.onChange(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
switchType = () => {
|
||||||
|
const { code } = this.state;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
code: !code,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderInput = () => {
|
||||||
|
const { code } = this.state;
|
||||||
|
const { input, disabled } = this.props;
|
||||||
|
return !code ? (
|
||||||
|
<CKEditor
|
||||||
|
editor={ClassicEditor}
|
||||||
|
config={{
|
||||||
|
extraPlugins: [this.myAdapter],
|
||||||
|
}}
|
||||||
|
data={input && input.value}
|
||||||
|
onInit={editor => {
|
||||||
|
this.editor = editor;
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles('code-input-wrapper')}>
|
||||||
|
<CodeEditorInput
|
||||||
|
disabled={disabled}
|
||||||
|
name={name}
|
||||||
|
{...this.props}
|
||||||
|
label=""
|
||||||
|
className={styles('code-input')}
|
||||||
|
noMargin
|
||||||
|
language="jsx"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
openFullScreen = () => {
|
||||||
|
const { dialogShow } = this.props;
|
||||||
|
|
||||||
|
dialogShow({
|
||||||
|
id: 'wysiwyg',
|
||||||
|
style: { zIndex: 10 },
|
||||||
|
large: true,
|
||||||
|
children: <div className={styles('cke-modal')}>{this.renderInput()}</div>,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* insertImage = () => {
|
||||||
|
if (!this.editor) return;
|
||||||
|
console.log(this.editor);
|
||||||
|
this.editor.execute('insertImage', {
|
||||||
|
src:
|
||||||
|
'https://s3.eu-central-1.amazonaws.com/tipaw-pictures/f851775c-314b-449f-ac26-ce4dd6d93b89.jpg',
|
||||||
|
});
|
||||||
|
const { input } = this.props;
|
||||||
|
|
||||||
|
let data = this.editor.getData();
|
||||||
|
|
||||||
|
data +=
|
||||||
|
"<img src='https://s3.eu-central-1.amazonaws.com/tipaw-pictures/f851775c-314b-449f-ac26-ce4dd6d93b89.jpg'/>";
|
||||||
|
input.onChange(data);
|
||||||
|
}; */
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { className, input, valueClassName, disabled, name, effect, placeholder } = this.props;
|
||||||
|
const { code } = this.state;
|
||||||
|
return (
|
||||||
|
<FormElement {...this.props} className={styles('CkEditorField', className)}>
|
||||||
|
<FieldWrapper
|
||||||
|
{...this.props}
|
||||||
|
valueClassName={styles('editor-wrapper', code && 'code-wrapper', valueClassName)}
|
||||||
|
>
|
||||||
|
<div className={styles('top-actions', code && 'code')}>
|
||||||
|
<div className={styles('switch')} onClick={this.switchType}>
|
||||||
|
<TextItem path={!code ? 'Switch to code' : 'Go back to editor'} />
|
||||||
|
</div>
|
||||||
|
{!code && (
|
||||||
|
<div className={styles('open-full')} onClick={this.openFullScreen}>
|
||||||
|
<FaExpandAlt />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{this.renderInput()}
|
||||||
|
</FieldWrapper>
|
||||||
|
</FormElement>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
export default class UploadAdapter {
|
||||||
|
constructor(props) {
|
||||||
|
// The file loader instance to use during the upload.
|
||||||
|
this.loader = props.loader;
|
||||||
|
this.onFileSelected = props.onFileSelected;
|
||||||
|
this.cancel = props.cancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
progressFunc = data => {
|
||||||
|
this.loader.uploadTotal = data.total;
|
||||||
|
this.loader.uploaded = data.loaded;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Starts the upload process. (must return a Promise)
|
||||||
|
// see https://ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/upload-adapter.html
|
||||||
|
upload() {
|
||||||
|
return this.loader.file.then(
|
||||||
|
file =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
this.onFileSelected(
|
||||||
|
file,
|
||||||
|
response => {
|
||||||
|
if (response.error) {
|
||||||
|
reject(response.message);
|
||||||
|
console.log('error upload', response);
|
||||||
|
} else {
|
||||||
|
// could be formated in a function given to the constructor
|
||||||
|
resolve({
|
||||||
|
// other sizes can be provided
|
||||||
|
default: response.url || response.large,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this.progressFunc,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aborts the upload process.
|
||||||
|
abort() {
|
||||||
|
if (this.cancel) this.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
126
admin/src/components/formItems/CkEditorField/ckEditorField.scss
Normal file
126
admin/src/components/formItems/CkEditorField/ckEditorField.scss
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
@import '~styles/adminMixins';
|
||||||
|
|
||||||
|
$margin_top: 8px;
|
||||||
|
$height_small: 350px;
|
||||||
|
$height_bar: 75px;
|
||||||
|
|
||||||
|
.CkEditorField {
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
|
@include baseTableStyle;
|
||||||
|
|
||||||
|
.editor-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-actions {
|
||||||
|
padding-top: $margin_top;
|
||||||
|
text-align: right;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
@include middle;
|
||||||
|
margin-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
font-size: 10px;
|
||||||
|
z-index: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
|
||||||
|
.insert-image {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-full {
|
||||||
|
border-left: 1px solid $color-border;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.code {
|
||||||
|
border-bottom: 1px solid #f1f1f1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.code-input-wrapper {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .ck {
|
||||||
|
font-family: $font;
|
||||||
|
color: $color_dark;
|
||||||
|
|
||||||
|
.ck-toolbar {
|
||||||
|
border: 1px solid $color_border !important;
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-editor__main > .ck-editor__editable {
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-sticky-panel .ck-sticky-panel__content_sticky {
|
||||||
|
top: $height_global_header - 15px;
|
||||||
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: revert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper.small {
|
||||||
|
:global .ck {
|
||||||
|
.ck-editor__main > .ck-editor__editable {
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cke-modal {
|
||||||
|
text-align: left;
|
||||||
|
height: 80vh;
|
||||||
|
.code-input-wrapper {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ck-editor {
|
||||||
|
height: 100% !important;
|
||||||
|
|
||||||
|
.ck-editor__main {
|
||||||
|
height: calc(100% - 38px) !important;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color: rgba($color: #888, $alpha: 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: rgba($color: #888, $alpha: 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba($color: #888, $alpha: 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck-content {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
admin/src/components/formItems/CkEditorField/index.ts
Normal file
14
admin/src/components/formItems/CkEditorField/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import app from 'store/app';
|
||||||
|
import CkEditorField from './CkEditorField';
|
||||||
|
|
||||||
|
const mapStateToProps = () => ({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dialogShow: app.actions.dialog.show.action,
|
||||||
|
dialogHide: app.actions.dialog.hide.action,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapped = connect(mapStateToProps, mapDispatchToProps)(CkEditorField);
|
||||||
|
|
||||||
|
export default Wrapped;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import JsonField from './index';
|
||||||
|
|
||||||
|
storiesOf('newComponents/JsonField', module).add('Default', () => <JsonField />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import JsonField from './index';
|
||||||
|
|
||||||
|
describe('JsonField', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<JsonField />);
|
||||||
|
});
|
||||||
|
});
|
||||||
99
admin/src/components/formItems/JsonField/index.tsx
Normal file
99
admin/src/components/formItems/JsonField/index.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
// Redux part
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
// Import actions here
|
||||||
|
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
import GenericFields from 'components/formHelpers/GenericFields';
|
||||||
|
import ModuleButton from 'components/items/ModuleButton';
|
||||||
|
import styleIdentifiers from './jsonField.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface StateProps {}
|
||||||
|
|
||||||
|
export interface DispatchProps {}
|
||||||
|
|
||||||
|
export interface OwnProps {}
|
||||||
|
|
||||||
|
export type JsonFieldProps = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
|
const JsonField = (props: JsonFieldProps) => {
|
||||||
|
const { input } = props;
|
||||||
|
|
||||||
|
const [jsonString, setJsonString] = useState('');
|
||||||
|
|
||||||
|
const refInput = useRef();
|
||||||
|
|
||||||
|
function safelyParseJSON(json) {
|
||||||
|
// This function cannot be optimised, it's best to
|
||||||
|
// keep it small!
|
||||||
|
let parsed;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(json);
|
||||||
|
} catch (e) {
|
||||||
|
return `Error: ${e.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed; // Could be undefined!
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUserKeyPress() {
|
||||||
|
let value = input.value;
|
||||||
|
let position;
|
||||||
|
|
||||||
|
if (value.length !== 0) {
|
||||||
|
if (value[0] !== '{') value = `{${value}`;
|
||||||
|
if (value[value.length - 1] !== '}') {
|
||||||
|
value = `${value}}`;
|
||||||
|
position = value.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.replace(/'/g, '"');
|
||||||
|
|
||||||
|
input.onChange(value);
|
||||||
|
|
||||||
|
if (position) refInput.current.setSelectionRange(position, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('keyup', handleUserKeyPress);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keyup', handleUserKeyPress);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
input.onChange(JSON.stringify(input.value));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles('json-field')}>
|
||||||
|
<div className={styles('content-wrapper')}>
|
||||||
|
<textarea rows={10} ref={refInput} {...input} placeholder="{}" />
|
||||||
|
</div>
|
||||||
|
<div className={styles('json-value')}>
|
||||||
|
{input.value && (
|
||||||
|
<>
|
||||||
|
{typeof safelyParseJSON(input.value) === 'string' ? (
|
||||||
|
<div className={styles('json-error')}>{safelyParseJSON(input.value)}</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles('json-res')}>
|
||||||
|
{JSON.stringify(JSON.parse(input.value), null, '\t')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JsonField;
|
||||||
34
admin/src/components/formItems/JsonField/jsonField.scss
Normal file
34
admin/src/components/formItems/JsonField/jsonField.scss
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
@import "~styles/mixins";
|
||||||
|
|
||||||
|
.json-field {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.json-value {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #343844;
|
||||||
|
padding: 10px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.json-error {
|
||||||
|
color: $color-red;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-res {
|
||||||
|
color: white;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
flex: 1;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import OnBlurInput from './OnBlurInput';
|
||||||
|
|
||||||
|
storiesOf('newComponents/OnBlurInput', module).add('Default', () => <OnBlurInput />);
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import OnBlurInput from './OnBlurInput';
|
||||||
|
|
||||||
|
describe('OnBlurInput', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<OnBlurInput />);
|
||||||
|
});
|
||||||
|
});
|
||||||
56
admin/src/components/formItems/OnBlurInput/OnBlurInput.tsx
Normal file
56
admin/src/components/formItems/OnBlurInput/OnBlurInput.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import styleIdentifiers from './onBlurInput.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface OnBlurInputProps {
|
||||||
|
className?: string;
|
||||||
|
input: Record<string, any>;
|
||||||
|
handleBlur?: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnBlurInputState {
|
||||||
|
inputValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class OnBlurInput extends React.Component<OnBlurInputProps, OnBlurInputState> {
|
||||||
|
state = {
|
||||||
|
inputValue: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { input } = this.props;
|
||||||
|
this.setState({ inputValue: input && input.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = (e: EventHandler) => {
|
||||||
|
this.setState({ inputValue: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleBlur = (e: EventHandler) => {
|
||||||
|
const { input, handleBlur } = this.props;
|
||||||
|
|
||||||
|
const { inputValue } = this.state;
|
||||||
|
|
||||||
|
if (handleBlur) {
|
||||||
|
handleBlur(inputValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.onBlur(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { inputValue } = this.state;
|
||||||
|
const { className, inputType } = this.props;
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className={styles('OnBlurInput', className)}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
type={inputType || 'text'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
admin/src/components/formItems/OnBlurInput/index.ts
Normal file
3
admin/src/components/formItems/OnBlurInput/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import OnBlurInput from './OnBlurInput';
|
||||||
|
|
||||||
|
export default OnBlurInput;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
@import "~styles/mixins";
|
||||||
|
|
||||||
|
.OnBlurInput {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import Schedule from './Schedule';
|
||||||
|
|
||||||
|
storiesOf('newComponents/Schedule', module).add('Default', () => <Schedule />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import Schedule from './Schedule';
|
||||||
|
|
||||||
|
describe('Schedule', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<Schedule />);
|
||||||
|
});
|
||||||
|
});
|
||||||
159
admin/src/components/formItems/Schedule/Schedule.tsx
Normal file
159
admin/src/components/formItems/Schedule/Schedule.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import ModuleButton from 'components/items/ModuleButton';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { clone } from 'store/utils/helper';
|
||||||
|
import styleIdentifiers from './schedule.scss';
|
||||||
|
|
||||||
|
const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface StateProps {
|
||||||
|
num: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DispatchProps {}
|
||||||
|
|
||||||
|
export interface OwnProps {
|
||||||
|
initialValue?: {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScheduleProps = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
|
const Schedule = (props: ScheduleProps): JSX => {
|
||||||
|
// Initial value
|
||||||
|
const { noMargin, label, name, input, initialValue, keyFrom, keyTo } = props;
|
||||||
|
|
||||||
|
const init = input.value || initialValue || [[], [], [], [], [], [], []];
|
||||||
|
|
||||||
|
const isArray = Array.isArray(init);
|
||||||
|
|
||||||
|
const [days, setDays] = useState(init);
|
||||||
|
const daysKeys = Object.keys(days);
|
||||||
|
|
||||||
|
const fromInput = useRef(null);
|
||||||
|
const toInput = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// save it to redux form when component update
|
||||||
|
if (input) input.onChange(days);
|
||||||
|
}, [days]); // give value on which the effect depend
|
||||||
|
|
||||||
|
const saveDays = () => {
|
||||||
|
// Use a copy (or same object)
|
||||||
|
const newDays = clone(days);
|
||||||
|
setDays(newDays);
|
||||||
|
input.onChange(newDays);
|
||||||
|
};
|
||||||
|
|
||||||
|
const multipleAdd = (first, last): void => {
|
||||||
|
const from = fromInput && fromInput.current && fromInput.current.value;
|
||||||
|
const to = toInput && toInput.current && toInput.current.value;
|
||||||
|
if (!from || !to) return;
|
||||||
|
|
||||||
|
for (let i = first; i < last; i++) {
|
||||||
|
days[daysKeys[i]].push([from, to]);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDays();
|
||||||
|
};
|
||||||
|
|
||||||
|
const addWeek = (): void => {
|
||||||
|
multipleAdd(0, 5);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addWeekend = (): void => {
|
||||||
|
multipleAdd(5, 7);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAll = (): void => {
|
||||||
|
multipleAdd(0, 7);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTimeSlot = day => {
|
||||||
|
const last = day[day.length - 1];
|
||||||
|
|
||||||
|
if (last && last[1]) {
|
||||||
|
const to = last[1];
|
||||||
|
const toTime = moment(to, 'HH:mm');
|
||||||
|
const newFrom = moment(toTime)
|
||||||
|
.hours(toTime.hours() + 1)
|
||||||
|
.format('HH:mm');
|
||||||
|
const newTo = moment(toTime)
|
||||||
|
.hours(toTime.hours() + 2)
|
||||||
|
.format('HH:mm');
|
||||||
|
day.push([newFrom, newTo]);
|
||||||
|
} else {
|
||||||
|
day.push(['08:00', '18:00']);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDays();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTimeSlot = (day, index: string | number): void => {
|
||||||
|
// item is a element of days
|
||||||
|
day.splice(index, 1);
|
||||||
|
saveDays();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event, timeSlot, index): void => {
|
||||||
|
timeSlot[index] = event.target.value;
|
||||||
|
saveDays();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTimeSlot = (timeSlot, day, index) => (
|
||||||
|
<div className={styles('hours')} key={index}>
|
||||||
|
<input
|
||||||
|
placeholder="from"
|
||||||
|
type="time"
|
||||||
|
value={timeSlot[keyFrom || 0]}
|
||||||
|
onChange={(event): void => handleChange(event, timeSlot, 0)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
placeholder="to"
|
||||||
|
type="time"
|
||||||
|
value={timeSlot[keyTo || 1]}
|
||||||
|
onChange={(event): void => handleChange(event, timeSlot, 1)}
|
||||||
|
/>
|
||||||
|
<div className={styles('remove')} onClick={(): void => removeTimeSlot(day, index)}>
|
||||||
|
x
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles('Schedule', noMargin && 'no-margin')}>
|
||||||
|
<div className={styles('label')}>{label || name}</div>
|
||||||
|
<div className={styles('quick-add')}>
|
||||||
|
<div className={styles('title')}>Quick add</div>
|
||||||
|
<div className={styles('hours')}>
|
||||||
|
<input ref={fromInput} placeholder="from" type="time" />
|
||||||
|
<input ref={toInput} placeholder="to" type="time" />
|
||||||
|
<ModuleButton noMargin small relative label="+ week" action={addWeek} />
|
||||||
|
<ModuleButton noMargin small relative label="+ weekend" action={addWeekend} />
|
||||||
|
<ModuleButton noMargin small relative label="+ all" action={addAll} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles('days-wrapper')}>
|
||||||
|
{daysKeys.map((key, dayIndex) => (
|
||||||
|
<div className={styles('day-wrapper')} key={dayIndex}>
|
||||||
|
<div className={styles('left')}>
|
||||||
|
<div className={styles('day')}>{DAYS[dayIndex]}</div>
|
||||||
|
{days[key].map((timeSlot, index) => renderTimeSlot(timeSlot, days[key], index))}
|
||||||
|
</div>
|
||||||
|
<ModuleButton
|
||||||
|
noMargin
|
||||||
|
small
|
||||||
|
relative
|
||||||
|
label="+ slot"
|
||||||
|
action={(): void => addTimeSlot(days[key])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Schedule;
|
||||||
15
admin/src/components/formItems/Schedule/index.tsx
Normal file
15
admin/src/components/formItems/Schedule/index.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
import Schedule, { StateProps, DispatchProps, OwnProps } from './Schedule';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StoreState): Record<string, any> => ({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {};
|
||||||
|
|
||||||
|
const Wrapped = connect<StateProps, DispatchProps, OwnProps>(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(Schedule);
|
||||||
|
|
||||||
|
export default Wrapped;
|
||||||
92
admin/src/components/formItems/Schedule/schedule.scss
Normal file
92
admin/src/components/formItems/Schedule/schedule.scss
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
@import "~styles/mixins";
|
||||||
|
|
||||||
|
.Schedule {
|
||||||
|
color: $color_font;
|
||||||
|
font-size: 14px;
|
||||||
|
// padding: 5px 0px;
|
||||||
|
margin-top: $form_spacing;
|
||||||
|
|
||||||
|
&.no-margin {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
color: #8c96a3;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 600;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day {
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 85px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.days-wrapper {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-wrapper {
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid $color_border;
|
||||||
|
display: flex;
|
||||||
|
// justify-content: space-between;
|
||||||
|
|
||||||
|
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-add {
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hours {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding-bottom:10px;
|
||||||
|
border-bottom:1px solid $color_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hours {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
>* {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0px 4px;
|
||||||
|
margin: 0;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
text-align: center;
|
||||||
|
width: 90px;
|
||||||
|
height: 30px;
|
||||||
|
background-color: rgb(240, 240, 240);
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
admin/src/components/formItems/Wysiwyg/Wysiwyg.stories.tsx
Normal file
33
admin/src/components/formItems/Wysiwyg/Wysiwyg.stories.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { reduxForm, Field } from 'redux-form';
|
||||||
|
import { text, object, boolean } from '@storybook/addon-knobs';
|
||||||
|
import { required } from 'store/utils/validation';
|
||||||
|
import Wysiwyg from './Wysiwyg';
|
||||||
|
|
||||||
|
|
||||||
|
storiesOf('admin/formItems/Wysiwyg', module)
|
||||||
|
.addDecorator(story => {
|
||||||
|
|
||||||
|
const Wrapper = reduxForm({ form: 'test' })(() => story());
|
||||||
|
return <Wrapper />;
|
||||||
|
})
|
||||||
|
.add('default', () => <Field name="test" component={Wysiwyg} label="Wysiwyg" />)
|
||||||
|
.add('custom', () => (
|
||||||
|
<Field
|
||||||
|
name="test"
|
||||||
|
component={Wysiwyg}
|
||||||
|
label={text('label', 'Wysiwyg')}
|
||||||
|
withBorder={boolean('withBorder', false)}
|
||||||
|
withValueBorder={boolean('withValueBorder', false)}
|
||||||
|
labelIcon={text('labelIcon', '')}
|
||||||
|
// styling
|
||||||
|
fieldWrapperStyle={object('fieldWrapperStyle', {})}
|
||||||
|
labelStyle={object('labelStyle', {})}
|
||||||
|
labelIconStyle={object('labelIconStyle', {})}
|
||||||
|
valueStyle={object('valueStyle', {})}
|
||||||
|
validate={required}
|
||||||
|
/>
|
||||||
|
));
|
||||||
15
admin/src/components/formItems/Wysiwyg/Wysiwyg.test.tsx
Normal file
15
admin/src/components/formItems/Wysiwyg/Wysiwyg.test.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { reduxForm, Field } from 'redux-form';
|
||||||
|
|
||||||
|
import Wysiwyg from './Wysiwyg';
|
||||||
|
|
||||||
|
describe('Wysiwyg', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
const Wrapper = reduxForm({ form: 'test' })(() => (
|
||||||
|
<Field name="test" component={Wysiwyg} label="test" />
|
||||||
|
));
|
||||||
|
shallow(<Wrapper />);
|
||||||
|
});
|
||||||
|
});
|
||||||
174
admin/src/components/formItems/Wysiwyg/Wysiwyg.tsx
Normal file
174
admin/src/components/formItems/Wysiwyg/Wysiwyg.tsx
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
|
||||||
|
import { Editor } from 'react-draft-wysiwyg';
|
||||||
|
import { EditorState, convertToRaw, ContentState, Modifier } from 'draft-js';
|
||||||
|
|
||||||
|
import FormElement from 'components/structure/FormElement';
|
||||||
|
import FieldWrapper from 'components/structure/FieldWrapper';
|
||||||
|
|
||||||
|
import draftToHtml from 'draftjs-to-html';
|
||||||
|
import htmlToDraft from 'html-to-draftjs';
|
||||||
|
import styleIdentifiers from './wysiwyg.scss';
|
||||||
|
|
||||||
|
/* eslint import/no-unresolved : "off" */
|
||||||
|
|
||||||
|
import '!style-loader!css-loader!react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export type WysiwygProps = FormElementProps &
|
||||||
|
FieldWrapperProps &
|
||||||
|
FormItemLabelProps & { name: string; placeholder?: string };
|
||||||
|
|
||||||
|
interface WysiwygState {
|
||||||
|
editorState?: any;
|
||||||
|
code: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomOption extends React.Component {
|
||||||
|
addCustomOption: Function = (): void => {
|
||||||
|
const { editorState, onChange, custom } = this.props;
|
||||||
|
const contentState = Modifier.replaceText(
|
||||||
|
editorState.getCurrentContent(),
|
||||||
|
editorState.getSelection(),
|
||||||
|
custom,
|
||||||
|
editorState.getCurrentInlineStyle(),
|
||||||
|
);
|
||||||
|
onChange(EditorState.push(editorState, contentState, 'insert-characters'));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { text } = this.props;
|
||||||
|
return <div onClick={this.addCustomOption}>{text}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Wysiwyg extends React.Component<WysiwygProps, WysiwygState> {
|
||||||
|
constructor(props: WysiwygProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
code: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.handleProps();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: WysiwygProps) {
|
||||||
|
const { input } = this.props;
|
||||||
|
if ((!prevProps.input || !prevProps.input.value) && input && input.value) {
|
||||||
|
this.handleProps();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditorStateChange = (editorState: any) => {
|
||||||
|
const { input } = this.props;
|
||||||
|
const rawContentState = convertToRaw(editorState.getCurrentContent());
|
||||||
|
const markup = draftToHtml(
|
||||||
|
rawContentState,
|
||||||
|
// hashtagConfig,
|
||||||
|
// directional,
|
||||||
|
// customEntityTransform,
|
||||||
|
);
|
||||||
|
|
||||||
|
// item.value = markup;
|
||||||
|
this.setState({
|
||||||
|
editorState,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (input) input.onChange(markup);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleProps = () => {
|
||||||
|
const { input } = this.props;
|
||||||
|
|
||||||
|
const html = (input && input.value) || '';
|
||||||
|
const contentBlock = htmlToDraft(html);
|
||||||
|
if (contentBlock) {
|
||||||
|
const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
|
||||||
|
const editorState = EditorState.createWithContent(contentState);
|
||||||
|
this.setState({
|
||||||
|
editorState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switchType = () => {
|
||||||
|
const { code } = this.state;
|
||||||
|
if (code) {
|
||||||
|
this.handleProps();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ code: !code });
|
||||||
|
};
|
||||||
|
|
||||||
|
renderCustomOptions = () => {
|
||||||
|
const { customOptions } = this.props;
|
||||||
|
const options = [];
|
||||||
|
if (customOptions && customOptions.length !== 0) {
|
||||||
|
for (let index = 0; index < customOptions.length; index++) {
|
||||||
|
const element = customOptions[index];
|
||||||
|
options.push(<CustomOption text={element.label} custom={element.insert} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
// <CustomOption text="download" custom="<DownloadFile />" />
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { editorState, code } = this.state;
|
||||||
|
const { className, valueClassName, input, disabled, name, effect, placeholder } = this.props;
|
||||||
|
if (!editorState) return false;
|
||||||
|
return (
|
||||||
|
<FormElement {...this.props} className={styles('Wysiwyg', className)}>
|
||||||
|
<FieldWrapper
|
||||||
|
{...this.props}
|
||||||
|
valueClassName={styles('editor-wrapper', code && 'code-wrapper', valueClassName)}
|
||||||
|
>
|
||||||
|
<div className={styles('switch', code && 'code')}>
|
||||||
|
<div className={styles('text')} onClick={this.switchType}>
|
||||||
|
{!code ? 'Switch to code' : 'Go back to editor'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!code ? (
|
||||||
|
<Editor
|
||||||
|
editorState={editorState}
|
||||||
|
// toolbarClassName="toolbarClassName"
|
||||||
|
// wrapperClassName="wrapperClassName"
|
||||||
|
editorClassName={styles('editor')}
|
||||||
|
onEditorStateChange={this.onEditorStateChange}
|
||||||
|
toolbarCustomButtons={this.renderCustomOptions()}
|
||||||
|
toolbar={{
|
||||||
|
options: [
|
||||||
|
'inline',
|
||||||
|
'blockType',
|
||||||
|
'fontSize',
|
||||||
|
// 'fontFamily',
|
||||||
|
'list',
|
||||||
|
'textAlign',
|
||||||
|
'colorPicker',
|
||||||
|
'link',
|
||||||
|
'embedded',
|
||||||
|
'image',
|
||||||
|
'remove',
|
||||||
|
'history',
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<textarea
|
||||||
|
disabled={disabled}
|
||||||
|
name={name}
|
||||||
|
rows={15}
|
||||||
|
placeholder={(!effect && placeholder) || ''}
|
||||||
|
{...input}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FieldWrapper>
|
||||||
|
</FormElement>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
admin/src/components/formItems/Wysiwyg/index.ts
Normal file
3
admin/src/components/formItems/Wysiwyg/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Wysiwyg from './Wysiwyg';
|
||||||
|
|
||||||
|
export default Wysiwyg;
|
||||||
62
admin/src/components/formItems/Wysiwyg/wysiwyg.scss
Normal file
62
admin/src/components/formItems/Wysiwyg/wysiwyg.scss
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
|
||||||
|
$margin_top: 8px;
|
||||||
|
$height_small: 350px;
|
||||||
|
$height_bar: 75px;
|
||||||
|
|
||||||
|
.Wysiwyg {
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
|
.editor-wrapper {
|
||||||
|
padding-top: $margin_top;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.code-wrapper {
|
||||||
|
> * {
|
||||||
|
> * {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
min-height: 500px;
|
||||||
|
padding: 0 10px;
|
||||||
|
max-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 10px;
|
||||||
|
z-index: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.code {
|
||||||
|
border-bottom: 1px solid #f1f1f1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper.small {
|
||||||
|
.editor {
|
||||||
|
max-height: $height_small;
|
||||||
|
min-height: $height_small;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import CellRowForm from './CellRowForm';
|
||||||
|
|
||||||
|
storiesOf('newComponents/CellRowForm', module).add('Default', () => <CellRowForm />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import CellRowForm from './CellRowForm';
|
||||||
|
|
||||||
|
describe('CellRowForm', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<CellRowForm />);
|
||||||
|
});
|
||||||
|
});
|
||||||
88
admin/src/components/forms/CellRowForm/CellRowForm.tsx
Normal file
88
admin/src/components/forms/CellRowForm/CellRowForm.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import { Field } from 'react-final-form';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import { required, composeValidators } from 'store/utils/validation';
|
||||||
|
import GenericFields from 'components/formHelpers/GenericFields';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import styleIdentifiers from './cellRowForm.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface StateProps {}
|
||||||
|
|
||||||
|
export interface DispatchProps {}
|
||||||
|
|
||||||
|
export interface OwnProps {
|
||||||
|
onSubmit: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CellRowFormProps = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
|
const CellRowForm = (props: CellRowFormProps) => {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
smallMargin,
|
||||||
|
inputName,
|
||||||
|
field,
|
||||||
|
onClickOutside,
|
||||||
|
values,
|
||||||
|
initialValues,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const formRef = useRef(null);
|
||||||
|
|
||||||
|
function checkIfValuesHaveChange(val, initVal) {
|
||||||
|
if (field.inputType === 'number') parseInt(initVal, 10);
|
||||||
|
return val !== initVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmitOnChange() {
|
||||||
|
if (field.type === 'select' && !field.multiple) handleSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(event) {
|
||||||
|
if (formRef.current && !formRef.current.contains(event.target)) {
|
||||||
|
const haveChange = checkIfValuesHaveChange(
|
||||||
|
_.get(values, inputName),
|
||||||
|
_.get(initialValues, inputName),
|
||||||
|
);
|
||||||
|
if (haveChange) handleSubmit();
|
||||||
|
else onClickOutside();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles('CellRowForm')} ref={formRef} onSubmit={handleSubmit}>
|
||||||
|
<GenericFields
|
||||||
|
propsInput={{
|
||||||
|
small: true,
|
||||||
|
labelClassName: styles('no-label'),
|
||||||
|
className: styles('input-small', smallMargin && 'small-margin'),
|
||||||
|
contentWrapperClassName: styles('input-content-wrapper'),
|
||||||
|
autoFocus: true,
|
||||||
|
onChange: handleSubmitOnChange,
|
||||||
|
}}
|
||||||
|
edit
|
||||||
|
finalForm
|
||||||
|
fields={[
|
||||||
|
{
|
||||||
|
name: inputName,
|
||||||
|
// default type
|
||||||
|
type: 'string',
|
||||||
|
...field,
|
||||||
|
noMargin: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CellRowForm;
|
||||||
21
admin/src/components/forms/CellRowForm/cellRowForm.scss
Normal file
21
admin/src/components/forms/CellRowForm/cellRowForm.scss
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
|
||||||
|
.CellRowForm {
|
||||||
|
width: 100%;
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
.no-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-small {
|
||||||
|
margin: 0;
|
||||||
|
&.small-margin {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-content-wrapper {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
admin/src/components/forms/CellRowForm/index.tsx
Normal file
15
admin/src/components/forms/CellRowForm/index.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
import CellRowForm, { StateProps, DispatchProps, OwnProps } from './CellRowForm';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StoreState): {} => ({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {};
|
||||||
|
|
||||||
|
const Wrapped = connect<StateProps, DispatchProps, OwnProps>(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(CellRowForm);
|
||||||
|
|
||||||
|
export default Wrapped;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import ChangePasswordForm from './ChangePasswordForm';
|
||||||
|
|
||||||
|
storiesOf('newComponents/ChangePasswordForm', module).add('Default', () => <ChangePasswordForm />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import ChangePasswordForm from './ChangePasswordForm';
|
||||||
|
|
||||||
|
describe('ChangePasswordForm', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<ChangePasswordForm />);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Field } from 'react-final-form';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import ModuleButton from 'components/items/ModuleButton';
|
||||||
|
import { required, compareTo, composeValidators } from 'store/utils/validation';
|
||||||
|
import Password from 'components/formItems/Password';
|
||||||
|
import config, { viewConfig } from 'config/general';
|
||||||
|
// import styleIdentifiers from './changePasswordForm.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind();
|
||||||
|
|
||||||
|
export interface StateProps {}
|
||||||
|
|
||||||
|
export interface DispatchProps {}
|
||||||
|
|
||||||
|
export interface OwnProps {
|
||||||
|
onSubmit: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChangePasswordFormProps = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
|
const ChangePasswordForm = (props: ChangePasswordFormProps) => {
|
||||||
|
const { handleSubmit, submitting, valid, values, form } = props;
|
||||||
|
|
||||||
|
const checkConfirm = compareTo('new password')(values.password);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles('ChangePasswordForm')} onSubmit={handleSubmit}>
|
||||||
|
<Field
|
||||||
|
{...viewConfig.inputProps}
|
||||||
|
component={Password}
|
||||||
|
name={config.accountOptions.changePassword.currentPassword}
|
||||||
|
label="Current password"
|
||||||
|
validate={composeValidators(required)}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
{...viewConfig.inputProps}
|
||||||
|
component={Password}
|
||||||
|
name={config.accountOptions.changePassword.password}
|
||||||
|
type="email"
|
||||||
|
label="New password"
|
||||||
|
validate={composeValidators(required)}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
{...viewConfig.inputProps}
|
||||||
|
component={Password}
|
||||||
|
name={config.accountOptions.changePassword.confirmPassword}
|
||||||
|
type="email"
|
||||||
|
label="Confirm password"
|
||||||
|
validate={composeValidators(required, checkConfirm)}
|
||||||
|
/>
|
||||||
|
<ModuleButton
|
||||||
|
disabled={submitting || !valid}
|
||||||
|
color="primary"
|
||||||
|
label="admin.buttons.submit"
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChangePasswordForm;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
@import "~styles/mixins";
|
||||||
|
|
||||||
|
.ChangePasswordForm {
|
||||||
|
|
||||||
|
}
|
||||||
15
admin/src/components/forms/ChangePasswordForm/index.tsx
Normal file
15
admin/src/components/forms/ChangePasswordForm/index.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
import ChangePasswordForm, { StateProps, DispatchProps, OwnProps } from './ChangePasswordForm';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StoreState): {} => ({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {};
|
||||||
|
|
||||||
|
const Wrapped = connect<StateProps, DispatchProps, OwnProps>(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(ChangePasswordForm);
|
||||||
|
|
||||||
|
export default Wrapped;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import CreateRoomForm from './index';
|
||||||
|
|
||||||
|
storiesOf('newComponents/CreateRoomForm', module).add('Default', () => <CreateRoomForm />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import CreateRoomForm from './index';
|
||||||
|
|
||||||
|
describe('CreateRoomForm', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<CreateRoomForm />);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
@import "~styles/mixins";
|
||||||
|
|
||||||
|
.create-room-form {
|
||||||
|
min-width: 400px;
|
||||||
|
padding: 25px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
admin/src/components/forms/CreateRoomForm/index.tsx
Normal file
67
admin/src/components/forms/CreateRoomForm/index.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Field } from 'react-final-form';
|
||||||
|
|
||||||
|
// Redux part
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
// Import actions here
|
||||||
|
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import Input from 'components/formItems/Input';
|
||||||
|
import ModuleButton from 'components/items/ModuleButton';
|
||||||
|
import { required, email, composeValidators } from 'store/utils/validation';
|
||||||
|
import GenericFields from 'components/formHelpers/GenericFields';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
import styleIdentifiers from './createRoomForm.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface StateProps {}
|
||||||
|
|
||||||
|
export interface DispatchProps {}
|
||||||
|
|
||||||
|
export interface OwnProps {
|
||||||
|
onSubmit: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateRoomFormProps = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
|
const CreateRoomForm = (props: CreateRoomFormProps) => {
|
||||||
|
const { handleSubmit, submitting, valid } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles('create-room-form')} onSubmit={handleSubmit}>
|
||||||
|
<div className={styles('title')}>
|
||||||
|
<TextItem path="Create Room" />
|
||||||
|
</div>
|
||||||
|
<GenericFields
|
||||||
|
propsInput={{
|
||||||
|
small: true,
|
||||||
|
apollo: true,
|
||||||
|
}}
|
||||||
|
finalForm
|
||||||
|
fields={[
|
||||||
|
{
|
||||||
|
name: 'accountIds',
|
||||||
|
label: 'Users',
|
||||||
|
type: 'select',
|
||||||
|
noMargin: true,
|
||||||
|
multiple: true,
|
||||||
|
apollo: true,
|
||||||
|
api: {
|
||||||
|
resource: 'accountsGetMany',
|
||||||
|
},
|
||||||
|
modelValues: {
|
||||||
|
value: '_id',
|
||||||
|
text: 'email',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<ModuleButton disabled={submitting || !valid} label="Create" type="submit" />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateRoomForm;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import GenericFiltersForm from './index';
|
||||||
|
|
||||||
|
storiesOf('admin/forms/GenericFiltersForm', module).add('GenericFiltersForm component', () => (
|
||||||
|
<GenericFiltersForm handleSubmit={() => {}} forcedOption={{}} />
|
||||||
|
));
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import GenericFiltersForm from './GenericFiltersForm';
|
||||||
|
|
||||||
|
describe('GenericFiltersForm', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<GenericFiltersForm type="" handleSubmit={() => {}} forcedOption={{}} />);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Field } from 'redux-form';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import Input from 'components/formItems/Input';
|
||||||
|
import ModuleButton from 'components/items/ModuleButton';
|
||||||
|
import { options } from 'config/root';
|
||||||
|
import { config, viewConfig } from 'config/general';
|
||||||
|
import { getPageInfo } from 'store/utils/adminHelpers';
|
||||||
|
import { getContent } from 'store/utils/helper';
|
||||||
|
import { content as localeContent } from 'config/content';
|
||||||
|
import styleIdentifiers from './genericFiltersForm.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface GenericFiltersFormProps {
|
||||||
|
handleSubmit: Function;
|
||||||
|
submitting?: boolean;
|
||||||
|
forcedOption?: Record<string, any>;
|
||||||
|
valid?: boolean;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GenericFiltersFormState {}
|
||||||
|
|
||||||
|
export default class GenericFiltersForm extends React.Component<
|
||||||
|
GenericFiltersFormProps,
|
||||||
|
GenericFiltersFormState
|
||||||
|
> {
|
||||||
|
render() {
|
||||||
|
const { handleSubmit, submitting, valid, forcedOption } = this.props;
|
||||||
|
|
||||||
|
const pageInfo = getPageInfo(this.props, options);
|
||||||
|
|
||||||
|
if (typeof pageInfo === 'string') return false;
|
||||||
|
|
||||||
|
const { modelOptions } = pageInfo;
|
||||||
|
|
||||||
|
const formOption = (modelOptions && modelOptions.filters) || forcedOption;
|
||||||
|
|
||||||
|
if (!formOption) return <span />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles('GenericFiltersForm')} onSubmit={handleSubmit}>
|
||||||
|
<Field
|
||||||
|
className={styles('large')}
|
||||||
|
component={Input}
|
||||||
|
name="search"
|
||||||
|
withBorder
|
||||||
|
small
|
||||||
|
type="text"
|
||||||
|
placeholder="admin.labels.search"
|
||||||
|
{...viewConfig.inputProps}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
className={styles('input')}
|
||||||
|
component={Input}
|
||||||
|
withBorder
|
||||||
|
name="limit"
|
||||||
|
max={config.maxLimit}
|
||||||
|
small
|
||||||
|
type="number"
|
||||||
|
placeholder="admin.labels.limit"
|
||||||
|
{...viewConfig.inputProps}
|
||||||
|
/>
|
||||||
|
<ModuleButton
|
||||||
|
noMargin
|
||||||
|
color={viewConfig.colorFilter || 'primary'}
|
||||||
|
relative
|
||||||
|
className={styles('button')}
|
||||||
|
icon="search"
|
||||||
|
disabled={!valid || submitting}
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
|
||||||
|
$height: 36px;
|
||||||
|
|
||||||
|
.GenericFiltersForm {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.input,
|
||||||
|
.large,
|
||||||
|
.middle,
|
||||||
|
.small {
|
||||||
|
min-width: 100px;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input,
|
||||||
|
.large,
|
||||||
|
.middle,
|
||||||
|
.small,
|
||||||
|
.spacing {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
max-width: 120px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle {
|
||||||
|
min-width: 100px;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
margin-left: 20px;
|
||||||
|
max-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
input {
|
||||||
|
height: $height;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 1px 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
admin/src/components/forms/GenericFiltersForm/index.ts
Normal file
20
admin/src/components/forms/GenericFiltersForm/index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import GenericFiltersForm from './GenericFiltersForm';
|
||||||
|
|
||||||
|
const createReduxForm = reduxForm({
|
||||||
|
// a unique name for the form
|
||||||
|
form: 'genericFiltersForm',
|
||||||
|
enableReinitialize: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = createReduxForm(GenericFiltersForm);
|
||||||
|
|
||||||
|
const mapStateToProps = () => ({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {};
|
||||||
|
|
||||||
|
const Wrapped = withRouter(connect(mapStateToProps, mapDispatchToProps)(form));
|
||||||
|
|
||||||
|
export default Wrapped;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import GenericForm from './index';
|
||||||
|
|
||||||
|
storiesOf('admin/forms/GenericForm', module).add('GenericForm component', () => (
|
||||||
|
<GenericForm
|
||||||
|
handleSubmit={() => {}}
|
||||||
|
deleteItem={() => {}}
|
||||||
|
history={() => {}}
|
||||||
|
push={() => {}}
|
||||||
|
initialValues={{}}
|
||||||
|
legend="Status article"
|
||||||
|
fields={{ id: { type: 'string', label: 'yes' } }}
|
||||||
|
/>
|
||||||
|
));
|
||||||
19
admin/src/components/forms/GenericForm/GenericForm.test.tsx
Normal file
19
admin/src/components/forms/GenericForm/GenericForm.test.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import GenericForm from './GenericForm';
|
||||||
|
|
||||||
|
describe('GenericForm', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(
|
||||||
|
<GenericForm
|
||||||
|
handleSubmit={() => {}}
|
||||||
|
deleteItem={() => {}}
|
||||||
|
loading={{}}
|
||||||
|
match={{ params: {}, isExact: true, url: '', path: '' }}
|
||||||
|
push={() => {}}
|
||||||
|
initialValues={{}}
|
||||||
|
fields={{ id: { type: 'string', label: 'yes' } }}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
129
admin/src/components/forms/GenericForm/GenericForm.tsx
Normal file
129
admin/src/components/forms/GenericForm/GenericForm.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import GenericFields from 'components/formHelpers/GenericFields';
|
||||||
|
import MosaicStructure from 'components/enhancers/MosaicStructure';
|
||||||
|
|
||||||
|
import ModuleButton from 'components/items/ModuleButton';
|
||||||
|
// import { required } from 'store/utils/validation';
|
||||||
|
import { config } from 'config/general';
|
||||||
|
import { fieldsToList } from 'store/utils/adminHelpers';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
import { formMutators } from 'store/utils/helper';
|
||||||
|
import GenericHeader from 'components/pageItems/GenericHeader';
|
||||||
|
import styleIdentifiers from './genericForm.scss';
|
||||||
|
|
||||||
|
const GenericStructure = MosaicStructure(GenericFields);
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface GenericFormProps {
|
||||||
|
handleSubmit: Function;
|
||||||
|
valid?: boolean;
|
||||||
|
fields: Record<string, any>;
|
||||||
|
initialValues: Record<string, any>;
|
||||||
|
title?: string;
|
||||||
|
legend?: Record<string, any>;
|
||||||
|
push: Function;
|
||||||
|
name?: string;
|
||||||
|
loading: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GenericFormState {}
|
||||||
|
|
||||||
|
export default class GenericForm extends React.Component<GenericFormProps, GenericFormState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const { allOpened } = props;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
allOpened,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { form, setForm } = this.props;
|
||||||
|
setForm(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
fields,
|
||||||
|
title,
|
||||||
|
handleSubmit,
|
||||||
|
legend,
|
||||||
|
initialValues,
|
||||||
|
loading,
|
||||||
|
resourceData,
|
||||||
|
dataOptions,
|
||||||
|
withHeader,
|
||||||
|
noHeader,
|
||||||
|
fieldsProps,
|
||||||
|
noBack,
|
||||||
|
noCancel,
|
||||||
|
classNameStructure,
|
||||||
|
children,
|
||||||
|
submitLabel,
|
||||||
|
noSubmit,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const { allOpened } = this.state;
|
||||||
|
// If id => edit (no header submit)
|
||||||
|
const showHeader = !initialValues || !initialValues[config.idKey] || dataOptions || withHeader;
|
||||||
|
|
||||||
|
const listFields = fieldsToList(fields);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles('GenericForm', legend && 'legend')} onSubmit={handleSubmit}>
|
||||||
|
{showHeader && !noHeader && (
|
||||||
|
<GenericHeader
|
||||||
|
title={title}
|
||||||
|
loading={loading}
|
||||||
|
noBack={noBack}
|
||||||
|
noCancel={noCancel}
|
||||||
|
saveButton
|
||||||
|
submitLabel={submitLabel}
|
||||||
|
noSubmit={noSubmit}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</GenericHeader>
|
||||||
|
)}
|
||||||
|
<div className={styles('content', showHeader && !noHeader && 'padding')}>
|
||||||
|
<GenericStructure
|
||||||
|
finalForm
|
||||||
|
scrollableColumns
|
||||||
|
className={classNameStructure}
|
||||||
|
minWidth={config.widthEditColumn}
|
||||||
|
value={initialValues}
|
||||||
|
resourceData={resourceData}
|
||||||
|
edit={initialValues && initialValues[config.idKey]}
|
||||||
|
items={listFields}
|
||||||
|
oneSection={listFields?.length === 1}
|
||||||
|
fieldsProps={fieldsProps}
|
||||||
|
mutators={{ ...formMutators }}
|
||||||
|
allOpened={allOpened}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{(listFields.length >= 3 || legend) && (
|
||||||
|
<div className={styles('footer')}>
|
||||||
|
<div className={styles('left')}>{legend}</div>
|
||||||
|
{listFields.length >= 3 && (
|
||||||
|
<ModuleButton
|
||||||
|
label={allOpened ? 'admin.buttons.closeAll' : 'admin.buttons.openAll'}
|
||||||
|
noMargin
|
||||||
|
small
|
||||||
|
color="primary"
|
||||||
|
relative
|
||||||
|
action={() => {
|
||||||
|
this.setState({
|
||||||
|
allOpened: !allOpened,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
admin/src/components/forms/GenericForm/genericForm.scss
Normal file
6
admin/src/components/forms/GenericForm/genericForm.scss
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
@import '~styles/adminMixins';
|
||||||
|
|
||||||
|
.GenericForm {
|
||||||
|
@include genericPage;
|
||||||
|
}
|
||||||
18
admin/src/components/forms/GenericForm/index.ts
Normal file
18
admin/src/components/forms/GenericForm/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { arrayPush, reduxForm, getFormValues } from 'redux-form';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import app from 'store/app';
|
||||||
|
import GenericForm from './GenericForm';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
content: state.content.raw,
|
||||||
|
loading: state.generic.loading,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setForm: app.actions.setForm.action,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapped = connect(mapStateToProps, mapDispatchToProps)(GenericForm);
|
||||||
|
|
||||||
|
export default withRouter(Wrapped);
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import MessageForm from './MessageForm';
|
||||||
|
|
||||||
|
storiesOf('newComponents/MessageForm', module).add('Default', () => <MessageForm />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import MessageForm from './MessageForm';
|
||||||
|
|
||||||
|
describe('MessageForm', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<MessageForm />);
|
||||||
|
});
|
||||||
|
});
|
||||||
44
admin/src/components/forms/MessageForm/MessageForm.tsx
Normal file
44
admin/src/components/forms/MessageForm/MessageForm.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
|
import { Field } from 'react-final-form';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import { required, email, composeValidators } from 'store/utils/validation';
|
||||||
|
import Textarea from 'components/formItems/Textarea';
|
||||||
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
import ChatInput from 'components/formItems/ChatInput';
|
||||||
|
|
||||||
|
const styles = classNames.bind();
|
||||||
|
|
||||||
|
export interface StateProps {}
|
||||||
|
|
||||||
|
export interface DispatchProps {}
|
||||||
|
|
||||||
|
export interface OwnProps {
|
||||||
|
onSubmit: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageFormProps = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
|
const MessageForm = (props: MessageFormProps) => {
|
||||||
|
const { handleSubmit, values, className, forwardRef, form } = props;
|
||||||
|
|
||||||
|
function onKeyPress(e) {
|
||||||
|
if (e.keyCode === 13 && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit(values, form);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles('MessageForm', className)} ref={forwardRef} onSubmit={handleSubmit}>
|
||||||
|
<Field
|
||||||
|
component={ChatInput}
|
||||||
|
onKeyPress={e => onKeyPress(e)}
|
||||||
|
name="message"
|
||||||
|
placeholder="Write your message here"
|
||||||
|
validate={composeValidators(required)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessageForm;
|
||||||
15
admin/src/components/forms/MessageForm/index.tsx
Normal file
15
admin/src/components/forms/MessageForm/index.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
import MessageForm, { StateProps, DispatchProps, OwnProps } from './MessageForm';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StoreState): {} => ({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {};
|
||||||
|
|
||||||
|
const Wrapped = connect<StateProps, DispatchProps, OwnProps>(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(MessageForm);
|
||||||
|
|
||||||
|
export default Wrapped;
|
||||||
3
admin/src/components/forms/MessageForm/messageForm.scss
Normal file
3
admin/src/components/forms/MessageForm/messageForm.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@import "~styles/mixins";
|
||||||
|
|
||||||
|
.MessageForm {}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import ModuleForm from './ModuleForm';
|
||||||
|
|
||||||
|
storiesOf('newComponents/ModuleForm', module).add('Default', () => <ModuleForm />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import ModuleForm from './ModuleForm';
|
||||||
|
|
||||||
|
describe('ModuleForm', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<ModuleForm />);
|
||||||
|
});
|
||||||
|
});
|
||||||
112
admin/src/components/forms/ModuleForm/ModuleForm.tsx
Normal file
112
admin/src/components/forms/ModuleForm/ModuleForm.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import Input from 'components/formItems/Input';
|
||||||
|
import ModuleButton from 'components/items/ModuleButton';
|
||||||
|
import { required, email, composeValidators } from 'store/utils/validation';
|
||||||
|
import { config, viewConfig } from 'config/general';
|
||||||
|
import { fieldsToList } from 'store/utils/adminHelpers';
|
||||||
|
import ModuleDetailStructure from 'components/enhancers/ModuleDetailStructure';
|
||||||
|
import GenericFields from 'components/formHelpers/GenericFields';
|
||||||
|
import { formMutators } from 'store/utils/helper';
|
||||||
|
import styleIdentifiers from './moduleForm.scss';
|
||||||
|
|
||||||
|
const GenericStructure = ModuleDetailStructure(GenericFields);
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface StateProps {}
|
||||||
|
|
||||||
|
export interface DispatchProps {}
|
||||||
|
|
||||||
|
export interface OwnProps {
|
||||||
|
onSubmit: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ModuleForm extends React.Component<StateProps, DispatchProps, OwnProps> {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
fields,
|
||||||
|
valid,
|
||||||
|
history,
|
||||||
|
push,
|
||||||
|
title,
|
||||||
|
submitting,
|
||||||
|
handleSubmit,
|
||||||
|
legend,
|
||||||
|
initialValues,
|
||||||
|
formValues,
|
||||||
|
loading,
|
||||||
|
resourceData,
|
||||||
|
dataOptions,
|
||||||
|
withHeader,
|
||||||
|
noHeader,
|
||||||
|
fieldsProps,
|
||||||
|
form,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// If id => edit (no header submit)
|
||||||
|
const showHeader = !initialValues || !initialValues[config.idKey] || dataOptions || withHeader;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles('ModuleForm', legend && 'legend')} onSubmit={handleSubmit}>
|
||||||
|
{/* {showHeader && !noHeader && (
|
||||||
|
<div className={styles('header')}>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
<div className={styles('actions')}>
|
||||||
|
<ModuleButton
|
||||||
|
label="admin.buttons.back"
|
||||||
|
noMargin
|
||||||
|
color={viewConfig.colorBack}
|
||||||
|
relative
|
||||||
|
action={() => history.goBack()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModuleButton
|
||||||
|
disabled={!valid || submitting}
|
||||||
|
label="admin.buttons.submit"
|
||||||
|
loading={loading.update || loading.add}
|
||||||
|
noMargin
|
||||||
|
color={viewConfig.colorSubmit}
|
||||||
|
relative
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
<div className={styles('content')}>
|
||||||
|
<GenericStructure
|
||||||
|
finalForm
|
||||||
|
allOpened
|
||||||
|
scrollableColumns
|
||||||
|
minWidth={config.widthEditColumn}
|
||||||
|
value={initialValues}
|
||||||
|
formValues={formValues}
|
||||||
|
form={form}
|
||||||
|
resourceData={resourceData}
|
||||||
|
edit={initialValues && initialValues[config.idKey]}
|
||||||
|
push={push}
|
||||||
|
items={fieldsToList(fields)}
|
||||||
|
formName="moduleForm"
|
||||||
|
fieldsProps={fieldsProps}
|
||||||
|
mutators={{ ...formMutators }}
|
||||||
|
sectionWrapperClassName={styles('wrapper-section')}
|
||||||
|
sectionClassName={styles('section')}
|
||||||
|
sectionTitleClassName={styles('section-title')}
|
||||||
|
title={title}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'admin.buttons.submit',
|
||||||
|
type: 'submit',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{legend && (
|
||||||
|
<div className={styles('footer')}>
|
||||||
|
<div className={styles('left')}>{legend}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
admin/src/components/forms/ModuleForm/index.tsx
Normal file
27
admin/src/components/forms/ModuleForm/index.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { arrayPush, reduxForm, getFormValues, change } from 'redux-form';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import app from 'store/app';
|
||||||
|
import ModuleForm from './ModuleForm';
|
||||||
|
|
||||||
|
// const createReduxForm = reduxForm({
|
||||||
|
// // a unique name for the form
|
||||||
|
// form: 'moduleForm',
|
||||||
|
// enableReinitialize: true,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const form = createReduxForm(ModuleForm);
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
content: state.content.raw,
|
||||||
|
loading: state.generic.loading,
|
||||||
|
formValues: getFormValues('moduleForm')(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
push: arrayPush,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapped = connect(mapStateToProps, mapDispatchToProps)(ModuleForm);
|
||||||
|
|
||||||
|
export default withRouter(Wrapped);
|
||||||
23
admin/src/components/forms/ModuleForm/moduleForm.scss
Normal file
23
admin/src/components/forms/ModuleForm/moduleForm.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
@import "~styles/mixins";
|
||||||
|
|
||||||
|
.ModuleForm {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 0px;
|
||||||
|
border-radius: 0px;
|
||||||
|
background-color: none;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import PageCmsForm from './PageCmsForm';
|
||||||
|
|
||||||
|
storiesOf('newComponents/PageCmsForm', module).add('Default', () => <PageCmsForm />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import PageCmsForm from './PageCmsForm';
|
||||||
|
|
||||||
|
describe('PageCmsForm', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
shallow(<PageCmsForm />);
|
||||||
|
});
|
||||||
|
});
|
||||||
1111
admin/src/components/forms/PageCmsForm/PageCmsForm.tsx
Normal file
1111
admin/src/components/forms/PageCmsForm/PageCmsForm.tsx
Normal file
File diff suppressed because it is too large
Load Diff
26
admin/src/components/forms/PageCmsForm/index.ts
Normal file
26
admin/src/components/forms/PageCmsForm/index.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import app from 'store/app';
|
||||||
|
import generic from 'store/generic';
|
||||||
|
import cms from 'store/cms';
|
||||||
|
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
import PageCmsForm from './PageCmsForm';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StoreState): Record<string, any> => ({
|
||||||
|
copy: state.cms.copy,
|
||||||
|
user: state.account.profile && state.account.profile.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dialogSuccess: app.actions.dialog.success.action,
|
||||||
|
dialogError: app.actions.dialog.error.action,
|
||||||
|
dialogShow: app.actions.dialog.show.action,
|
||||||
|
upload: generic.actions.upload.request.action,
|
||||||
|
copyContent: cms.actions.copyContent.action,
|
||||||
|
sideMenuShow: app.actions.sideMenu.show.action,
|
||||||
|
sideMenuHide: app.actions.sideMenu.hide.action,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapped = connect(mapStateToProps, mapDispatchToProps)(PageCmsForm);
|
||||||
|
|
||||||
|
export default Wrapped;
|
||||||
328
admin/src/components/forms/PageCmsForm/pageCmsForm.scss
Normal file
328
admin/src/components/forms/PageCmsForm/pageCmsForm.scss
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
@import '~styles/adminMixins';
|
||||||
|
|
||||||
|
.PageCmsForm {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: $height_page_footer;
|
||||||
|
|
||||||
|
/*$height_filter: 40px;
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
height: $height_filter;
|
||||||
|
padding: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid $color_border;
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: 1px solid $color_border;
|
||||||
|
border-radius: $border_radius;
|
||||||
|
padding: 3px 8px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
padding: 10px;
|
||||||
|
padding-top: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
@include fixedFooter;
|
||||||
|
padding: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
min-width: 120px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.search {
|
||||||
|
color: $color_search;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-container {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.upload-container-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.field,
|
||||||
|
.object,
|
||||||
|
.array {
|
||||||
|
.field-wrapper {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
.active-checkbox {
|
||||||
|
background-color: $color_primary;
|
||||||
|
border-color: $color_primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-wrapper-value-container,
|
||||||
|
.field-wrapper-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.field-link-wrapper {
|
||||||
|
background-color: white;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-locker {
|
||||||
|
@include v_align;
|
||||||
|
display: flex;
|
||||||
|
right: 5px;
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.2;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @include card;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
//padding: 5px 10px;
|
||||||
|
&.to-complete {
|
||||||
|
background: $color_to_complete;
|
||||||
|
|
||||||
|
> .head input {
|
||||||
|
font-weight: bold;
|
||||||
|
color: $color_dark_orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-matched {
|
||||||
|
> .head .matched {
|
||||||
|
background-color: $color_search_item;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .content .matched {
|
||||||
|
background-color: $color_search_item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.object,
|
||||||
|
.array {
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: $border_radius;
|
||||||
|
border: 1px solid $color_border;
|
||||||
|
background-color: rgba(245, 245, 245, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
width: 49%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
font-size: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&.TextArea {
|
||||||
|
padding-left: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
@include transition();
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.position {
|
||||||
|
padding: 0 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 40px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
.label {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-options {
|
||||||
|
font-size: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
//position: absolute;
|
||||||
|
//right: 5px;
|
||||||
|
// top: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
span {
|
||||||
|
&.active {
|
||||||
|
color: #0e5cad;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #0e5cad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.head {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.head-info {
|
||||||
|
@include v_align;
|
||||||
|
right: 5px;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indic {
|
||||||
|
// @include v_align;
|
||||||
|
// right: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $color-blue;
|
||||||
|
border-radius: $border_radius;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 4px;
|
||||||
|
background-color: rgb(250, 250, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
.locker {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
opacity: 0.2;
|
||||||
|
@include transition();
|
||||||
|
position: relative;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
&.locked {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
background-color: rgba(255, 255, 255, 1);
|
||||||
|
border-radius: $border_radius;
|
||||||
|
padding: 4px;
|
||||||
|
color: $color-red;
|
||||||
|
@include v_align;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
@include transition();
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-right {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
.url {
|
||||||
|
margin-top: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.array,
|
||||||
|
.object {
|
||||||
|
@include transition(all);
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
box-shadow: 0 0 12px 0 rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 120px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
height: 37px;
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user