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