init commit

This commit is contained in:
Valdior 2025-05-14 21:42:26 +02:00
commit ed437d2e31
1144 changed files with 102663 additions and 0 deletions

30
.editorconfig Normal file
View 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
View File

@ -0,0 +1,6 @@
dist/**
src/index.html
flow-typed/**
node_modules/**
seed/node_modules/**
admin/node_modules/**

13
.eslintrc.js Normal file
View 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
View File

@ -0,0 +1,5 @@
.idea
node_modules
package-lock.json
dist
.DS_store

7
.prettierrc.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
singleQuote: true,
trailingComma: 'all',
printWidth: 100,
tabWidth: 2,
useTabs: false,
};

121
README.md Normal file
View 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
View File

@ -0,0 +1,5 @@
.idea
node_modules
package-lock.json
dist
.DS_Store

287
admin/README-config.md Normal file
View 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
View 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
View 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
View 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,
});

View File

@ -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 />
));

View File

@ -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 />);
});
});

View File

@ -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;

View File

@ -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);
}
}
}
}

View File

@ -0,0 +1,3 @@
import CustomFixedTable from './CustomFixedTable';
export default CustomFixedTable;

View File

@ -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 />);

View File

@ -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 />);
});
});

View File

@ -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;

View File

@ -0,0 +1,3 @@
import ModuleDetailStructure from './ModuleDetailStructure';
export default ModuleDetailStructure;

View File

@ -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;
}
}

View File

@ -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]} />;
});

View File

@ -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 />);
});
});

View File

@ -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;

View File

@ -0,0 +1,11 @@
@import '~styles/mixins';
@import '~styles/adminMixins';
.GenericFieldArray {
@include genericFields;
.buttons {
margin-top: 7px;
text-align: right;
}
}

View File

@ -0,0 +1,3 @@
import GenericFieldArray from './GenericFieldArray';
export default GenericFieldArray;

View File

@ -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') },
}}
/>
),
);

View File

@ -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 />);
});
});

View 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>
);
}
}

View File

@ -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;
}
}

View 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;

View File

@ -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',
})}
/>
));

View File

@ -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 />);
});
});

View 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>
);
}
}

View 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;

View File

@ -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;
}
}
}
}

View File

@ -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" />);

View File

@ -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 />);
});
});

View 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>
);
}
}

View File

@ -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();
}
}

View 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;
}
}
}
}
}

View 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;

View File

@ -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 />);

View File

@ -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 />);
});
});

View 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;

View 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;
}
}

View File

@ -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 />);

View File

@ -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 />);
});
});

View 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'}
/>
);
}
}

View File

@ -0,0 +1,3 @@
import OnBlurInput from './OnBlurInput';
export default OnBlurInput;

View File

@ -0,0 +1,5 @@
@import "~styles/mixins";
.OnBlurInput {
display: inline-block;
}

View File

@ -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 />);

View File

@ -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 />);
});
});

View 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;

View 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;

View 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;
}
}
}

View 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}
/>
));

View 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 />);
});
});

View 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>
);
}
}

View File

@ -0,0 +1,3 @@
import Wysiwyg from './Wysiwyg';
export default Wysiwyg;

View 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;
}
}

View File

@ -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 />);

View File

@ -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 />);
});
});

View 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;

View 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;
}
}

View 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;

View File

@ -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 />);

View File

@ -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 />);
});
});

View File

@ -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;

View File

@ -0,0 +1,5 @@
@import "~styles/mixins";
.ChangePasswordForm {
}

View 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;

View File

@ -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 />);

View File

@ -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 />);
});
});

View File

@ -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;
}
}

View 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;

View File

@ -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={{}} />
));

View File

@ -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={{}} />);
});
});

View File

@ -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>
);
}
}

View File

@ -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;
}
}

View 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;

View File

@ -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' } }}
/>
));

View 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' } }}
/>,
);
});
});

View 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>
);
}
}

View File

@ -0,0 +1,6 @@
@import '~styles/mixins';
@import '~styles/adminMixins';
.GenericForm {
@include genericPage;
}

View 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);

View File

@ -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 />);

View File

@ -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 />);
});
});

View 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;

View 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;

View File

@ -0,0 +1,3 @@
@import "~styles/mixins";
.MessageForm {}

View File

@ -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 />);

View File

@ -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 />);
});
});

View 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>
);
}
}

View 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);

View 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;
}
}
}
}

View File

@ -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 />);

View File

@ -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 />);
});
});

File diff suppressed because it is too large Load Diff

View 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;

View 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