├── src ├── app │ ├── field │ │ ├── index.js │ │ └── ToggleField.js │ ├── input │ │ ├── index.js │ │ └── ToggleInput.js │ ├── resources.js │ ├── i18n │ │ ├── index.js │ │ └── en.js │ ├── fields │ │ ├── types.js │ │ ├── FieldReferenceField.js │ │ ├── FieldReferenceManyField.js │ │ ├── appearanceTypes.js │ │ ├── EnumValidationField.js │ │ ├── validate.js │ │ ├── StringInput.js │ │ ├── FieldValidatorHintField.js │ │ ├── FormatValidationField.js │ │ ├── LengthValidationField.js │ │ └── index.js │ ├── itemTypes │ │ ├── ItemTypeReferenceField.js │ │ ├── AddFieldButton.js │ │ └── index.js │ ├── components │ │ └── TextFieldHint.js │ ├── AddUploadFeature.js │ ├── authClient.js │ ├── app.js │ ├── items │ │ ├── ItemField.js │ │ └── index.js │ ├── users.js │ ├── apiClient.js │ └── data.js └── www │ └── index.html ├── .gitignore ├── screenshots ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png └── screenshot4.png ├── .babelrc ├── README.md ├── LICENSE.md ├── webpack-production.config.js ├── package.json └── webpack-dev-server.config.js /src/app/field/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/input/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xzya/headless-cms-dashboard/master/screenshots/screenshot1.png -------------------------------------------------------------------------------- /screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xzya/headless-cms-dashboard/master/screenshots/screenshot2.png -------------------------------------------------------------------------------- /screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xzya/headless-cms-dashboard/master/screenshots/screenshot3.png -------------------------------------------------------------------------------- /screenshots/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xzya/headless-cms-dashboard/master/screenshots/screenshot4.png -------------------------------------------------------------------------------- /src/app/resources.js: -------------------------------------------------------------------------------- 1 | export const ITEM_TYPES = 'item-types'; 2 | export const FIELDS = 'fields'; 3 | export const ITEMS = 'items'; -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-0" 6 | ], 7 | "plugins": [ 8 | "transform-react-jsx" 9 | ] 10 | } -------------------------------------------------------------------------------- /src/app/i18n/index.js: -------------------------------------------------------------------------------- 1 | import { englishMessages } from 'admin-on-rest'; 2 | 3 | import customEnglishMessages from './en'; 4 | 5 | export default { 6 | en: { ...englishMessages, ...customEnglishMessages }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/fields/types.js: -------------------------------------------------------------------------------- 1 | export const STRING = "string"; 2 | export const TEXT = "text"; 3 | 4 | export const TYPES = [ 5 | { id: STRING, name: 'field.types.string' }, 6 | { id: TEXT, name: 'field.types.text' }, 7 | ] -------------------------------------------------------------------------------- /src/app/itemTypes/ItemTypeReferenceField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReferenceField, TextField } from 'admin-on-rest'; 3 | 4 | const ItemTypeReferenceField = (props) => ( 5 | 6 | 7 | 8 | ) 9 | ItemTypeReferenceField.defaultProps = { 10 | source: 'relationships.itemType.data.id', 11 | reference: 'item-types', 12 | addLabel: true, 13 | }; 14 | 15 | export default ItemTypeReferenceField; -------------------------------------------------------------------------------- /src/app/fields/FieldReferenceField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReferenceField, TextField } from 'admin-on-rest'; 3 | 4 | const FieldReferenceField = (props) => ( 5 | 6 | 7 | 8 | ) 9 | FieldReferenceField.defaultProps = { 10 | label: 'resources.fields.fields.attributes.label', 11 | source: 'id', 12 | reference: 'fields', 13 | addLabel: true, 14 | }; 15 | 16 | export default FieldReferenceField; 17 | -------------------------------------------------------------------------------- /src/app/fields/FieldReferenceManyField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ReferenceManyField, 4 | TextField, 5 | Datagrid, 6 | EditButton, 7 | required, 8 | } from 'admin-on-rest'; 9 | 10 | import FieldReferenceField from './FieldReferenceField'; 11 | 12 | const FieldReferenceManyField = (props) => ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | FieldReferenceManyField.defaultProps = { 23 | reference: 'fields', 24 | target: 'itemTypeId', 25 | addLabel: false, 26 | }; 27 | 28 | export default FieldReferenceManyField; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Headless CMS Dashboard 2 | 3 | This is a dashboard for [headless-cms](https://github.com/Xzya/headless-cms). 4 | 5 | ## Screenshots 6 | 7 | ![Screenshot 1](./screenshots/screenshot1.png) 8 | ![Screenshot 2](./screenshots/screenshot2.png) 9 | ![Screenshot 3](./screenshots/screenshot3.png) 10 | ![Screenshot 4](./screenshots/screenshot4.png) 11 | 12 | ## Running the project 13 | 14 | Install the dependencies 15 | 16 | ```bash 17 | npm install 18 | ``` 19 | 20 | Run the project 21 | 22 | ```bash 23 | npm start 24 | ``` 25 | 26 | To build the project for production, run 27 | 28 | ```bash 29 | npm run build 30 | ``` 31 | this will output the result to `build/`. 32 | 33 | ## Built using 34 | 35 | - [React](https://reactjs.org/) 36 | - [admin-on-rest](https://github.com/marmelab/admin-on-rest) 37 | - [material-ui](https://github.com/mui-org/material-ui) 38 | - [redux-form](https://github.com/erikras/redux-form/) 39 | 40 | ## License 41 | 42 | Open sourced under the [MIT license](./LICENSE.md). -------------------------------------------------------------------------------- /src/app/field/ToggleField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import get from 'lodash.get'; 4 | import pure from 'recompose/pure'; 5 | 6 | import FalseIcon from 'material-ui/svg-icons/content/clear'; 7 | import TrueIcon from 'material-ui/svg-icons/action/done'; 8 | 9 | export const ToggleField = ({ source, record = {}, elStyle }) => { 10 | if (get(record, source) == null) { 11 | return ; 12 | } 13 | 14 | if (get(record, source) != null) { 15 | return ; 16 | } 17 | 18 | return ; 19 | }; 20 | 21 | ToggleField.propTypes = { 22 | addLabel: PropTypes.bool, 23 | elStyle: PropTypes.object, 24 | label: PropTypes.string, 25 | record: PropTypes.object, 26 | source: PropTypes.string.isRequired, 27 | }; 28 | 29 | const PureToggleField = pure(ToggleField); 30 | 31 | PureToggleField.defaultProps = { 32 | addLabel: true, 33 | elStyle: { 34 | display: 'block', 35 | margin: 'auto', 36 | }, 37 | }; 38 | 39 | export default PureToggleField; 40 | -------------------------------------------------------------------------------- /src/app/fields/appearanceTypes.js: -------------------------------------------------------------------------------- 1 | import { 2 | STRING, 3 | TEXT, 4 | } from './types'; 5 | 6 | export const TITLE = 'title'; 7 | export const PLAIN = 'plain'; 8 | export const MARKDOWN = 'markdown'; 9 | export const HTML = 'html'; 10 | export const NO_FORMAT = 'noFormat'; 11 | 12 | const TITLE_CHOICE = { id: TITLE, name: 'presentation.types.title' }; 13 | const PLAIN_CHOICE = { id: PLAIN, name: 'presentation.types.plain' }; 14 | const MARKDOWN_CHOICE = { id: MARKDOWN, name: 'presentation.types.markdown' }; 15 | const HTML_CHOICE = { id: HTML, name: 'presentation.types.html' }; 16 | const NO_FORMAT_CHOICE = { id: PLAIN, name: 'presentation.types.no_format' }; 17 | 18 | export const STRING_APPEARANCE_TYPES = [TITLE_CHOICE, PLAIN_CHOICE]; 19 | export const TEXT_APPEARANCE_TYPES = [MARKDOWN_CHOICE, HTML_CHOICE, NO_FORMAT_CHOICE]; 20 | 21 | export const appearanceTypesForFieldType = (type) => { 22 | switch (type) { 23 | case STRING: 24 | return STRING_APPEARANCE_TYPES; 25 | case TEXT: 26 | return TEXT_APPEARANCE_TYPES; 27 | } 28 | throw new Error(`Unsupported field type ${type}`); 29 | } -------------------------------------------------------------------------------- /src/app/itemTypes/AddFieldButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import FlatButton from 'material-ui/FlatButton'; 4 | import { translate } from 'admin-on-rest'; 5 | import { Link } from 'react-router-dom'; 6 | 7 | import AddIcon from 'material-ui/svg-icons/content/add'; 8 | 9 | const styles = { 10 | flat: { 11 | overflow: 'inherit', 12 | }, 13 | }; 14 | 15 | class AddFieldButton extends Component { 16 | render() { 17 | const { record, translate } = this.props; 18 | const link = record ? `/fields/create?itemTypeId=${record.id}` : `/fields/create`; 19 | return ( 20 | } 24 | icon={} 25 | style={styles.flat} 26 | /> 27 | ); 28 | } 29 | } 30 | 31 | AddFieldButton.propTypes = { 32 | record: PropTypes.object, 33 | translate: PropTypes.func, 34 | }; 35 | 36 | export default translate(AddFieldButton); 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Mihail Cristian Dumitru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/app/components/TextFieldHint.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import transitions from 'material-ui/styles/transitions'; 4 | import { 5 | translate, 6 | } from 'admin-on-rest'; 7 | 8 | const styles = { 9 | text: { 10 | position: 'relative', 11 | bottom: 2, 12 | fontSize: 12, 13 | lineHeight: '12px', 14 | color: 'rgba(0, 0, 0, 0.298039)', 15 | transition: transitions.easeOut(), 16 | }, 17 | }; 18 | 19 | class TextFieldHint extends Component { 20 | static propTypes = { 21 | text: PropTypes.node, 22 | label: PropTypes.string, 23 | labelProps: PropTypes.object, 24 | elStyle: PropTypes.object, 25 | }; 26 | 27 | static defaultProps = { 28 | text: null, 29 | label: null, 30 | labelProps: null, 31 | elStyle: {}, 32 | } 33 | 34 | render() { 35 | const { text, label, labelProps, elStyle, translate } = this.props; 36 | return ( 37 |
38 | {!!label ? translate(label, labelProps) : text} 39 |
40 | ); 41 | } 42 | } 43 | 44 | export default translate(TextFieldHint); -------------------------------------------------------------------------------- /webpack-production.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const TransferWebpackPlugin = require('transfer-webpack-plugin'); 4 | 5 | const config = { 6 | entry: { 7 | main: [ 8 | './src/app/app.js', 9 | ], 10 | }, 11 | // Render source-map file for final build 12 | devtool: 'source-map', 13 | // output config 14 | output: { 15 | path: path.resolve(__dirname, 'build'), // Path of output file 16 | filename: 'app.js', // Name of output file 17 | }, 18 | plugins: [ 19 | // Define production build to allow React to strip out unnecessary checks 20 | new webpack.DefinePlugin({ 21 | 'process.env':{ 22 | 'NODE_ENV': JSON.stringify('production') 23 | } 24 | }), 25 | // Minify the bundle 26 | new webpack.optimize.UglifyJsPlugin({ 27 | sourceMap: true, 28 | }), 29 | // Transfer Files 30 | new TransferWebpackPlugin([ 31 | {from: 'www'}, 32 | ], path.resolve(__dirname, 'src')), 33 | ], 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.js$/, 38 | exclude: /node_modules/, 39 | loader: 'babel-loader', 40 | query: { 41 | cacheDirectory: true, 42 | }, 43 | }, 44 | ], 45 | }, 46 | }; 47 | 48 | module.exports = config; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "headless-cms-dashboard", 3 | "description": "Headless CMS Dashboard built using adrmin-on-rest", 4 | "scripts": { 5 | "start": "webpack-dev-server --config webpack-dev-server.config.js --progress --hot --inline --colors", 6 | "build": "webpack --config webpack-production.config.js --progress --colors" 7 | }, 8 | "private": true, 9 | "devDependencies": { 10 | "admin-on-rest": "1.3.2", 11 | "aor-dependent-input": "1.2.0", 12 | "aor-json-rest-client": "~2.1.0", 13 | "aor-language-french": "~1.8.0", 14 | "aor-rich-text-input": "~1.0.0", 15 | "babel-core": "~6.25.0", 16 | "babel-loader": "~7.1.1", 17 | "babel-plugin-transform-react-jsx": "~6.8.0", 18 | "babel-polyfill": "~6.9.1", 19 | "babel-preset-es2015": "6.24.1", 20 | "babel-preset-react": "~6.11.1", 21 | "babel-preset-stage-0": "6.24.1", 22 | "css-loader": "~0.28.4", 23 | "extract-text-webpack-plugin": "~2.1.2", 24 | "lodash.get": "^4.4.2", 25 | "lodash.set": "^4.3.2", 26 | "material-ui": "~0.19.0", 27 | "query-string": "^6.0.0", 28 | "react": "~15.5.4", 29 | "react-dom": "~15.5.4", 30 | "react-redux": "^5.0.7", 31 | "react-router-dom": "^4.2.2", 32 | "redux": "^4.0.0", 33 | "redux-form": "^7.3.0", 34 | "style-loader": "~0.18.2", 35 | "transfer-webpack-plugin": "^0.1.4", 36 | "webpack": "~3.0.0", 37 | "webpack-dev-server": "~2.5.0" 38 | }, 39 | "dependencies": {} 40 | } 41 | -------------------------------------------------------------------------------- /src/app/AddUploadFeature.js: -------------------------------------------------------------------------------- 1 | const convertFileToBase64 = file => 2 | new Promise((resolve, reject) => { 3 | const reader = new FileReader(); 4 | reader.readAsDataURL(file); 5 | 6 | reader.onload = () => resolve(reader.result); 7 | reader.onerror = reject; 8 | }); 9 | 10 | const addUploadCapabilities = requestHandler => (type, resource, params) => { 11 | if (type === 'UPDATE' && resource === 'posts') { 12 | if (params.data.pictures && params.data.pictures.length) { 13 | // only freshly dropped pictures are instance of File 14 | const formerPictures = params.data.pictures.filter( 15 | p => !(p instanceof File) 16 | ); 17 | const newPictures = params.data.pictures.filter( 18 | p => p instanceof File 19 | ); 20 | 21 | return Promise.all(newPictures.map(convertFileToBase64)) 22 | .then(base64Pictures => 23 | base64Pictures.map(picture64 => ({ 24 | src: picture64, 25 | title: `${params.data.title}`, 26 | })) 27 | ) 28 | .then(transformedNewPictures => 29 | requestHandler(type, resource, { 30 | ...params, 31 | data: { 32 | ...params.data, 33 | pictures: [ 34 | ...transformedNewPictures, 35 | ...formerPictures, 36 | ], 37 | }, 38 | }) 39 | ); 40 | } 41 | } 42 | 43 | return requestHandler(type, resource, params); 44 | }; 45 | 46 | export default addUploadCapabilities; 47 | -------------------------------------------------------------------------------- /src/app/authClient.js: -------------------------------------------------------------------------------- 1 | import { 2 | AUTH_GET_PERMISSIONS, 3 | AUTH_LOGIN, 4 | AUTH_LOGOUT, 5 | AUTH_ERROR, 6 | AUTH_CHECK, 7 | } from 'admin-on-rest'; // eslint-disable-line import/no-unresolved 8 | 9 | // TODO: - remove this 10 | const URL = "http://localhost:3000" 11 | 12 | // Authenticatd by default 13 | const authClientFactory = (apiUrl) => { 14 | return (type, params) => { 15 | if (type === AUTH_LOGIN) { 16 | const { username, password } = params; 17 | const request = new Request(`${apiUrl}/api/signin`, { 18 | method: 'POST', 19 | body: JSON.stringify({ 20 | data: { 21 | type: "email_credentials", 22 | attributes: { 23 | email: username, 24 | password: password, 25 | } 26 | } 27 | }), 28 | headers: new Headers({ 'Content-Type': 'application/json' }), 29 | }) 30 | return fetch(request) 31 | .then(response => { 32 | if (response.status < 200 || response.status >= 300) { 33 | return response.json().then((body) => { 34 | if (body.error) { 35 | throw new Error(body.error); 36 | } 37 | throw new Error(response.statusText); 38 | }); 39 | } 40 | return response.json(); 41 | }) 42 | .then((response) => { 43 | localStorage.setItem('token', response.data.id); 44 | }); 45 | } 46 | return Promise.resolve(); 47 | }; 48 | }; 49 | 50 | export default authClientFactory(URL); -------------------------------------------------------------------------------- /src/app/fields/EnumValidationField.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Toggle from 'material-ui/Toggle'; 4 | import TextField from 'material-ui/TextField'; 5 | import { 6 | FieldTitle, 7 | SelectArrayInput, 8 | translate, 9 | required, 10 | } from 'admin-on-rest'; 11 | import { 12 | Field, 13 | } from 'redux-form'; 14 | 15 | import ToggleInput from '../input/ToggleInput'; 16 | 17 | const styles = { 18 | block: { 19 | margin: '1rem 0', 20 | maxWidth: 250, 21 | }, 22 | }; 23 | 24 | const isEmpty = value => 25 | typeof value === 'undefined' || value === null || value === '' || value.length === 0; 26 | 27 | const valueValidator = (value, _, props) => { 28 | return isEmpty(value) ? props.translate('aor.validation.required') : undefined; 29 | } 30 | 31 | const EnumValidationField = ({ elStyle, label, input, source, resource }) => ( 32 |
33 | 41 |
42 | {input.value && 43 | 49 | } 50 |
51 |
52 | ) 53 | 54 | EnumValidationField.propTypes = { 55 | addField: PropTypes.bool.isRequired, 56 | elStyle: PropTypes.object, 57 | input: PropTypes.object, 58 | isRequired: PropTypes.bool, 59 | label: PropTypes.string, 60 | resource: PropTypes.string, 61 | source: PropTypes.string, 62 | options: PropTypes.object, 63 | }; 64 | 65 | EnumValidationField.defaultProps = { 66 | addField: true, 67 | options: {}, 68 | }; 69 | 70 | export default translate(EnumValidationField); 71 | -------------------------------------------------------------------------------- /src/app/input/ToggleInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Toggle from 'material-ui/Toggle'; 4 | import { FieldTitle } from 'admin-on-rest'; 5 | 6 | const styles = { 7 | block: { 8 | margin: '1rem 0', 9 | maxWidth: 250, 10 | }, 11 | label: { 12 | color: 'rgba(0, 0, 0, 0.298039)', 13 | }, 14 | toggle: { 15 | marginBottom: 16, 16 | }, 17 | }; 18 | 19 | class ToggleInput extends Component { 20 | handleToggle(event, value) { 21 | this.props.input.onChange(value ? this.props.defaultValue : null); 22 | this.props.onChange(event, value); 23 | } 24 | 25 | render() { 26 | const { 27 | input, 28 | isRequired, 29 | label, 30 | source, 31 | elStyle, 32 | resource, 33 | options, 34 | } = this.props; 35 | 36 | return ( 37 |
38 | 50 | } 51 | {...options} 52 | /> 53 |
54 | ); 55 | } 56 | } 57 | 58 | ToggleInput.propTypes = { 59 | addField: PropTypes.bool.isRequired, 60 | elStyle: PropTypes.object, 61 | input: PropTypes.object, 62 | isRequired: PropTypes.bool, 63 | label: PropTypes.string, 64 | resource: PropTypes.string, 65 | source: PropTypes.string, 66 | options: PropTypes.object, 67 | defaultValue: PropTypes.object, 68 | onChange: PropTypes.func, 69 | }; 70 | 71 | ToggleInput.defaultProps = { 72 | addField: true, 73 | options: {}, 74 | defaultValue: {}, 75 | onChange: () => { }, 76 | }; 77 | 78 | export default ToggleInput; 79 | -------------------------------------------------------------------------------- /src/app/app.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-key: off */ 2 | import 'babel-polyfill'; 3 | import React from 'react'; 4 | import { render } from 'react-dom'; 5 | 6 | import { Admin, Resource, Delete } from 'admin-on-rest'; // eslint-disable-line import/no-unresolved 7 | 8 | import addUploadFeature from './addUploadFeature'; 9 | 10 | import { UserList, UserEdit, UserCreate, UserIcon, UserShow } from './users'; 11 | 12 | import { ItemTypeList, ItemTypeCreate, ItemTypeEdit, ItemTypeShow, ItemTypeIcon } from './itemTypes'; 13 | import { ItemList, ItemCreate, ItemEdit, ItemShow, ItemIcon } from './items'; 14 | import { FieldList, FieldCreate, FieldEdit, FieldIcon } from './fields'; 15 | 16 | import data from './data'; 17 | import messages from './i18n'; 18 | import authClient from './authClient'; 19 | import apiClient from './apiClient'; 20 | 21 | render( 22 | 29 | {permissions => [ 30 | , 39 | , 46 | , 55 | permissions ? ( 56 | 65 | ) : null, 66 | , 67 | ]} 68 | , 69 | document.getElementById('root') 70 | ); 71 | -------------------------------------------------------------------------------- /src/app/fields/validate.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | required, 5 | minLength, 6 | maxLength, 7 | regex, 8 | email, 9 | choices, 10 | translate, 11 | } from 'admin-on-rest'; 12 | import { EMAIL, URL } from './FormatValidationField'; 13 | import TextFieldHint from '../components/TextFieldHint'; 14 | 15 | export const requiredValidator = (_) => { 16 | return required; 17 | }; 18 | 19 | // TODO: - replace with server side validate 20 | export const uniqueValidator = (_) => (value, _, props) => undefined; 21 | 22 | export const lengthValidator = (validator) => { 23 | const { min, max, eq } = validator; 24 | return (value, _, props) => { 25 | let error = undefined; 26 | if (min != null) { 27 | error = error || minLength(min)(value, _, props); 28 | } 29 | if (max != null) { 30 | error = error || maxLength(max)(value, _, props); 31 | } 32 | if (eq != null) { 33 | error = error || minLength(eq)(value, _, props) || maxLength(eq)(value, _, props); 34 | } 35 | return error; 36 | }; 37 | }; 38 | 39 | // TODO: - replace with server side validate 40 | const urlRe = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi; 41 | export const formatValidator = (validator) => { 42 | const { predefinedPattern, customPattern } = validator; 43 | return (value, _, props) => { 44 | if (predefinedPattern != null) { 45 | switch (predefinedPattern) { 46 | case URL: 47 | try { 48 | return regex(new RegExp(urlRe), "validator.invalid_expression_format")(value, _, props); 49 | } catch (e) { 50 | return e; 51 | } 52 | case EMAIL: 53 | return email(value, _, props); 54 | } 55 | } 56 | if (customPattern != null) { 57 | try { 58 | return regex(new RegExp(customPattern), "validator.invalid_expression_format")(value, _, props); 59 | } catch (e) { 60 | return e; 61 | } 62 | } 63 | return undefined; 64 | }; 65 | }; 66 | 67 | export const enumValidator = (validator) => { 68 | const { values } = validator; 69 | return (value, _, props) => { 70 | return choices(values, "validator.invalid_expression_enum")(value, _, props); 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /webpack-dev-server.config.js: -------------------------------------------------------------------------------- 1 | // const webpack = require('webpack'); 2 | // const path = require('path'); 3 | // const TransferWebpackPlugin = require('transfer-webpack-plugin'); 4 | 5 | // const config = { 6 | // // Entry points to the project 7 | // entry: { 8 | // main: [ 9 | // // only- means to only hot reload for successful updates 10 | // 'webpack/hot/only-dev-server', 11 | // './src/app/app.js', 12 | // ], 13 | // }, 14 | // // Server Configuration options 15 | // devServer: { 16 | // contentBase: 'src/www', // Relative directory for base of server 17 | // hot: true, // Live-reload 18 | // inline: true, 19 | // port: 3001, // Port Number 20 | // host: 'localhost', // Change to '0.0.0.0' for external facing server 21 | // historyApiFallback: true, 22 | // }, 23 | // devtool: 'eval', 24 | // output: { 25 | // path: path.resolve(__dirname, 'build'), // Path of output file 26 | // filename: 'app.js', 27 | // }, 28 | // plugins: [ 29 | // // Enables Hot Modules Replacement 30 | // new webpack.HotModuleReplacementPlugin(), 31 | // // Moves files 32 | // new TransferWebpackPlugin([ 33 | // { from: 'www' }, 34 | // ], path.resolve(__dirname, 'src')), 35 | // ], 36 | // module: { 37 | // rules: [ 38 | // { 39 | // test: /\.js$/, 40 | // exclude: /node_modules/, 41 | // loader: 'babel-loader', 42 | // query: { 43 | // cacheDirectory: true, 44 | // }, 45 | // }, 46 | // ], 47 | // }, 48 | // }; 49 | 50 | // module.exports = config; 51 | 52 | const path = require('path'); 53 | 54 | module.exports = { 55 | devtool: 'eval', 56 | // Entry points to the project 57 | entry: { 58 | main: [ 59 | // only- means to only hot reload for successful updates 60 | 'webpack/hot/only-dev-server', 61 | './src/app/app.js', 62 | ], 63 | }, 64 | // Server Configuration options 65 | devServer: { 66 | contentBase: 'src/www', // Relative directory for base of server 67 | hot: true, // Live-reload 68 | inline: true, 69 | port: 3001, // Port Number 70 | host: 'localhost', // Change to '0.0.0.0' for external facing server 71 | }, 72 | output: { 73 | path: path.resolve(__dirname, 'build'), // Path of output file 74 | filename: 'app.js', 75 | }, 76 | module: { 77 | loaders: [ 78 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }, 79 | { test: /\.css$/, loader: 'style-loader!css-loader' }, 80 | ], 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /src/app/items/ItemField.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | ReferenceInput, 5 | SelectInput, 6 | GET_LIST, 7 | required, 8 | } from 'admin-on-rest'; 9 | import get from 'lodash.get'; 10 | 11 | import { STRING, TEXT } from '../fields/types'; 12 | import restClient from '../apiClient'; 13 | import StringInput from '../fields/StringInput'; 14 | import { Field } from 'redux-form'; 15 | 16 | const itemTypeIdSource = "relationships.itemType.data.id"; 17 | 18 | class ItemField extends Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = { 23 | fields: [], 24 | }; 25 | 26 | this.getItemTypeFields = this.getItemTypeFields.bind(this); 27 | this.handleItemTypeId = this.handleItemTypeId.bind(this); 28 | } 29 | 30 | componentDidMount() { 31 | this.getItemTypeFields(); 32 | } 33 | 34 | componentDidUpdate() { 35 | console.log("did update") 36 | } 37 | 38 | getItemTypeFields = () => { 39 | const { record } = this.props; 40 | console.log(record) 41 | 42 | const itemTypeId = get(record, itemTypeIdSource); 43 | if (itemTypeId == null) { 44 | return; 45 | } 46 | 47 | const props = { target: 'itemTypeId', id: itemTypeId }; 48 | restClient(GET_LIST, 'fields', props) 49 | .then((response) => { 50 | this.setState({ fields: response.data }); 51 | console.log("got response", response); 52 | }) 53 | .catch((e) => { 54 | console.error(e); 55 | }) 56 | } 57 | 58 | inputForField(field) { 59 | if (field == null || field.attributes == null) { 60 | return null; 61 | } 62 | switch (field.attributes.fieldType) { 63 | case STRING: { 64 | return ; 65 | } 66 | } 67 | return null; 68 | } 69 | 70 | handleItemTypeId() { 71 | this.getItemTypeFields(); 72 | } 73 | 74 | render() { 75 | const { 76 | record, 77 | source, 78 | elStyle, 79 | resource, 80 | } = this.props; 81 | const { fields } = this.state; 82 | 83 | const renderItemTypeField = (props) => ( 84 | 85 | 86 | 87 | ) 88 | 89 | const renderFields = fields.map((field) => this.inputForField(field)) 90 | return ( 91 |
92 | 93 | {renderFields} 94 |
95 | ); 96 | } 97 | } 98 | 99 | ItemField.propTypes = { 100 | addLabel: PropTypes.bool, 101 | elStyle: PropTypes.object, 102 | record: PropTypes.object, 103 | }; 104 | 105 | ItemField.defaultProps = { 106 | record: { 107 | type: 'item', 108 | attributes: {}, 109 | }, 110 | }; 111 | 112 | export default ItemField; -------------------------------------------------------------------------------- /src/app/fields/StringInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Field } from 'redux-form'; 4 | import { 5 | TextInput, 6 | translate, 7 | } from 'admin-on-rest'; 8 | 9 | import { 10 | requiredValidator, 11 | uniqueValidator, 12 | lengthValidator, 13 | formatValidator, 14 | enumValidator, 15 | } from './validate'; 16 | import FieldValidatorHintField from './FieldValidatorHintField'; 17 | 18 | /** 19 | * An Input component for a string 20 | * 21 | * @example 22 | * 23 | * 24 | * You can customize the `type` props (which defaults to "text"). 25 | * Note that, due to a React bug, you should use `` instead of using type="number". 26 | * @example 27 | * 28 | * 29 | * 30 | * The object passed as `options` props is passed to the material-ui component 31 | */ 32 | export class StringInput extends Component { 33 | constructor(props) { 34 | super(props); 35 | 36 | this.validators = this.validators.bind(this); 37 | 38 | this.state = { 39 | validators: this.validators(), 40 | }; 41 | } 42 | 43 | validators() { 44 | const { field } = this.props; 45 | const validators = field.attributes.validators; 46 | 47 | const requiredValue = validators.required; 48 | const uniqueValue = validators.unique; 49 | const lengthValue = validators.length; 50 | const formatValue = validators.format; 51 | const enumValue = validators.enum; 52 | 53 | let v = []; 54 | 55 | if (requiredValue != null) { 56 | v.push(requiredValidator(requiredValue)); 57 | } 58 | if (uniqueValue != null) { 59 | v.push(uniqueValidator(uniqueValue)); 60 | } 61 | if (lengthValue != null) { 62 | v.push(lengthValidator(lengthValue)); 63 | } 64 | if (formatValue != null) { 65 | v.push(formatValidator(formatValue)); 66 | } 67 | if (enumValue != null) { 68 | v.push(enumValidator(enumValue)); 69 | } 70 | 71 | return v; 72 | } 73 | 74 | render() { 75 | const { field, source } = this.props; 76 | const { validators } = this.state; 77 | return ( 78 |
79 | 86 | 87 |
88 | ); 89 | } 90 | } 91 | 92 | StringInput.propTypes = { 93 | addField: PropTypes.bool.isRequired, 94 | elStyle: PropTypes.object, 95 | input: PropTypes.object, 96 | label: PropTypes.string, 97 | meta: PropTypes.object, 98 | name: PropTypes.string, 99 | onChange: PropTypes.func, 100 | options: PropTypes.object, 101 | resource: PropTypes.string, 102 | source: PropTypes.string, 103 | field: PropTypes.object, 104 | }; 105 | 106 | StringInput.defaultProps = { 107 | addField: true, 108 | source: "attributes", 109 | onChange: () => { }, 110 | options: {}, 111 | }; 112 | 113 | export default StringInput; 114 | -------------------------------------------------------------------------------- /src/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Headless CMS 6 | 105 | 106 | 107 | 108 | 109 | 110 |
111 |
112 |
Loading...
113 |
114 |
115 | 116 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /src/app/fields/FieldValidatorHintField.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import TextFieldHint from '../components/TextFieldHint'; 5 | 6 | const RequiredField = () => ( 7 | 11 | ); 12 | 13 | const UniqueField = () => ( 14 | 18 | ); 19 | 20 | const LengthBetweenField = ({ min, max }) => ( 21 | 26 | ); 27 | const LengthAtLeastField = ({ min }) => ( 28 | 33 | ); 34 | const LengthAtMostField = ({ max }) => ( 35 | 40 | ); 41 | const LengthExactlyField = ({ eq }) => ( 42 | 47 | ); 48 | 49 | const FormatEmailField = () => ( 50 | 54 | ); 55 | 56 | const FormatURLField = () => ( 57 | 61 | ); 62 | 63 | const FormatCustomPatternField = ({ pattern }) => ( 64 | 69 | ); 70 | 71 | const EnumField = ({ values }) => ( 72 | 77 | ); 78 | 79 | class FieldValidatorHintField extends Component { 80 | 81 | static propTypes = { 82 | field: PropTypes.object.isRequired, 83 | elStyle: PropTypes.object, 84 | }; 85 | 86 | static defaultProps = { 87 | elStyle: {}, 88 | } 89 | 90 | render() { 91 | const { field, elStyle } = this.props; 92 | 93 | let fields = []; 94 | 95 | const validators = field.attributes.validators; 96 | 97 | const requiredValue = validators.required; 98 | const uniqueValue = validators.unique; 99 | const lengthValue = validators.length; 100 | const formatValue = validators.format; 101 | const enumValue = validators.enum; 102 | 103 | if (requiredValue != null) { 104 | fields.push(RequiredField()); 105 | } 106 | if (uniqueValue != null) { 107 | fields.push(UniqueField()); 108 | } 109 | if (lengthValue != null) { 110 | if (lengthValue.min != null && lengthValue.max != null) { 111 | fields.push(LengthBetweenField({ min: lengthValue.min, max: lengthValue.max })); 112 | } else if (lengthValue.min != null) { 113 | fields.push(LengthAtLeastField({ min: lengthValue.min })); 114 | } else if (lengthValue.max != null) { 115 | fields.push(LengthAtMostField({ max: lengthValue.max })); 116 | } else if (lengthValue.eq != null) { 117 | fields.push(LengthExactlyField({ eq: lengthValue.eq })); 118 | } 119 | } 120 | if (formatValue != null) { 121 | if (formatValue.predefinedPattern != null) { 122 | switch (formatValue.predefinedPattern) { 123 | case EMAIL: 124 | fields.push(FormatEmailField()); 125 | break; 126 | case URL: 127 | fields.push(FormatURLField()); 128 | break; 129 | } 130 | } else if (formatValue.customPattern != null) { 131 | fields.push(FormatCustomPatternField({ pattern: formatValue.customPattern })); 132 | } 133 | } 134 | if (enumValue != null) { 135 | fields.push(EnumField({ values: enumValue.values })); 136 | } 137 | 138 | return ( 139 |
{fields}
140 | ); 141 | } 142 | } 143 | 144 | export default FieldValidatorHintField; -------------------------------------------------------------------------------- /src/app/users.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-key: off */ 2 | import React from 'react'; 3 | import { 4 | Create, 5 | Datagrid, 6 | DisabledInput, 7 | Edit, 8 | EditButton, 9 | Filter, 10 | FormTab, 11 | List, 12 | Responsive, 13 | SaveButton, 14 | Show, 15 | ShowButton, 16 | SimpleForm, 17 | SimpleList, 18 | Tab, 19 | TabbedForm, 20 | TabbedShowLayout, 21 | TextField, 22 | TextInput, 23 | Toolbar, 24 | required, 25 | translate, 26 | } from 'admin-on-rest'; // eslint-disable-line import/no-unresolved 27 | 28 | export UserIcon from 'material-ui/svg-icons/social/people'; 29 | 30 | const UserFilter = ({ ...props }) => ( 31 | 32 | {permissions => [ 33 | , 34 | , 35 | permissions === 'admin' ? : null, 36 | ]} 37 | 38 | ); 39 | 40 | const titleFieldStyle = { 41 | maxWidth: '20em', 42 | overflow: 'hidden', 43 | textOverflow: 'ellipsis', 44 | whiteSpace: 'nowrap', 45 | }; 46 | export const UserList = ({ ...props }) => ( 47 | } 50 | sort={{ field: 'name', order: 'ASC' }} 51 | > 52 | {permissions => ( 53 | record.name} 57 | secondaryText={record => 58 | permissions === 'admin' ? record.role : null} 59 | /> 60 | } 61 | medium={ 62 | 63 | 64 | 65 | {permissions === 'admin' && } 66 | 67 | 68 | 69 | } 70 | /> 71 | )} 72 | 73 | ); 74 | 75 | const UserTitle = translate(({ record, translate }) => ( 76 | 77 | {record ? translate('users.edit.title', { title: record.name }) : ''} 78 | 79 | )); 80 | 81 | const UserCreateToolbar = ({ permissions, ...props }) => ( 82 | 83 | 88 | {permissions === 'admin' && ( 89 | 95 | )} 96 | 97 | ); 98 | 99 | export const UserCreate = ({ ...props }) => ( 100 | 101 | {permissions => ( 102 | } 104 | defaultValue={{ role: 'user' }} 105 | > 106 | 107 | {permissions === 'admin' && ( 108 | 109 | )} 110 | 111 | )} 112 | 113 | ); 114 | 115 | export const UserEdit = ({ ...props }) => ( 116 | } {...props}> 117 | {permissions => ( 118 | 119 | 120 | {permissions === 'admin' && } 121 | 122 | 123 | {permissions === 'admin' && ( 124 | 125 | 126 | 127 | )} 128 | 129 | )} 130 | 131 | ); 132 | 133 | export const UserShow = ({ ...props }) => ( 134 | } {...props}> 135 | {permissions => ( 136 | 137 | 138 | 139 | 140 | 141 | {permissions === 'admin' && ( 142 | 143 | 144 | 145 | )} 146 | 147 | )} 148 | 149 | ); 150 | -------------------------------------------------------------------------------- /src/app/itemTypes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | BooleanField, 4 | BooleanInput, 5 | CheckboxGroupInput, 6 | ChipField, 7 | Create, 8 | Datagrid, 9 | DateField, 10 | DeleteButton, 11 | DateInput, 12 | DisabledInput, 13 | Edit, 14 | EditButton, 15 | Filter, 16 | ImageField, 17 | ImageInput, 18 | List, 19 | LongTextInput, 20 | NumberField, 21 | NumberInput, 22 | ReferenceArrayField, 23 | ReferenceManyField, 24 | ReferenceArrayInput, 25 | Responsive, 26 | RefreshButton, 27 | RichTextField, 28 | SaveButton, 29 | SelectArrayInput, 30 | SelectField, 31 | SelectInput, 32 | Show, 33 | ShowButton, 34 | SimpleForm, 35 | SimpleList, 36 | SingleFieldList, 37 | SimpleShowLayout, 38 | ListButton, 39 | ReferenceField, 40 | TabbedForm, 41 | TabbedShowLayout, 42 | Tab, 43 | FormTab, 44 | TextField, 45 | TextInput, 46 | Toolbar, 47 | minValue, 48 | number, 49 | required, 50 | translate, 51 | } from 'admin-on-rest'; // eslint-disable-line import/no-unresolved 52 | import FlatButton from 'material-ui/FlatButton'; 53 | import { CardActions } from 'material-ui/Card'; 54 | 55 | import FieldReferenceManyField from '../fields/FieldReferenceManyField'; 56 | import AddFieldButton from './AddFieldButton'; 57 | 58 | export { ItemTypeIcon } from 'material-ui/svg-icons/action/book'; 59 | 60 | const titleFieldStyle = { 61 | maxWidth: '20em', 62 | overflow: 'hidden', 63 | textOverflow: 'ellipsis', 64 | whiteSpace: 'nowrap', 65 | }; 66 | 67 | export const ItemTypeList = ({ ...props }) => ( 68 | 71 | record.attributes.name} 75 | secondaryText={record => record.attributes.apiKey} 76 | /> 77 | } 78 | medium={ 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | } 87 | /> 88 | 89 | ); 90 | 91 | const ItemTypeCreateToolbar = props => ( 92 | 93 | 98 | 104 | 105 | ); 106 | 107 | export const ItemTypeCreate = ({ ...props }) => ( 108 | 109 | } 111 | defaultValue={{}} 112 | > 113 | 114 | 115 | 116 | 117 | ); 118 | 119 | const cardActionStyle = { 120 | zIndex: 2, 121 | display: 'inline-block', 122 | float: 'right', 123 | }; 124 | 125 | const ItemTypeEditActions = ({ basePath, data, refresh }) => ( 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | ) 134 | 135 | const ItemTypeTitle = translate(({ record, translate }) => ( 136 | 137 | {record ? translate('item-type.edit.title', { title: record.attributes.name }) : ''} 138 | 139 | )); 140 | 141 | export const ItemTypeEdit = ({ ...props }) => ( 142 | } actions={} {...props}> 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | ); 155 | 156 | const ItemTypeShowActions = ({ basePath, data, refresh }) => ( 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | ) 165 | 166 | export const ItemTypeShow = ({ ...props }) => ( 167 | } actions={} {...props}> 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | ); 180 | -------------------------------------------------------------------------------- /src/app/items/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | BooleanField, 4 | BooleanInput, 5 | CheckboxGroupInput, 6 | ChipField, 7 | Create, 8 | Datagrid, 9 | DateField, 10 | DeleteButton, 11 | DateInput, 12 | DisabledInput, 13 | Edit, 14 | EditButton, 15 | Filter, 16 | ImageField, 17 | ImageInput, 18 | List, 19 | LongTextInput, 20 | NumberField, 21 | NumberInput, 22 | ReferenceArrayField, 23 | ReferenceManyField, 24 | ReferenceArrayInput, 25 | Responsive, 26 | RefreshButton, 27 | RichTextField, 28 | ReferenceInput, 29 | SaveButton, 30 | SelectArrayInput, 31 | SelectField, 32 | SelectInput, 33 | Show, 34 | ShowButton, 35 | SimpleForm, 36 | SimpleList, 37 | SingleFieldList, 38 | SimpleShowLayout, 39 | ListButton, 40 | ReferenceField, 41 | TabbedForm, 42 | TabbedShowLayout, 43 | Tab, 44 | FormTab, 45 | TextField, 46 | TextInput, 47 | Toolbar, 48 | minValue, 49 | number, 50 | required, 51 | translate, 52 | } from 'admin-on-rest'; // eslint-disable-line import/no-unresolved 53 | import FlatButton from 'material-ui/FlatButton'; 54 | import { CardActions } from 'material-ui/Card'; 55 | 56 | import FieldReferenceManyField from '../fields/FieldReferenceManyField'; 57 | import ItemField from './ItemField'; 58 | 59 | export { ItemIcon } from 'material-ui/svg-icons/action/book'; 60 | 61 | const titleFieldStyle = { 62 | maxWidth: '20em', 63 | overflow: 'hidden', 64 | textOverflow: 'ellipsis', 65 | whiteSpace: 'nowrap', 66 | }; 67 | 68 | export const ItemList = ({ ...props }) => ( 69 | 72 | record.attributes.name} 76 | secondaryText={record => record.attributes.apiKey} 77 | /> 78 | } 79 | medium={ 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | } 88 | /> 89 | 90 | ); 91 | 92 | const ItemCreateToolbar = props => ( 93 | 94 | 99 | 105 | 106 | ); 107 | 108 | export const ItemCreate = ({ ...props }) => ( 109 | 110 | } 112 | defaultValue={{ 113 | type: 'item', 114 | attributes: {}, 115 | relationships: { 116 | itemType: { 117 | data: { 118 | type: 'item_type', 119 | id: '', 120 | } 121 | } 122 | } 123 | }} 124 | > 125 | 126 | 127 | 128 | 129 | 130 | 131 | ); 132 | 133 | const cardActionStyle = { 134 | zIndex: 2, 135 | display: 'inline-block', 136 | float: 'right', 137 | }; 138 | 139 | const ItemEditActions = ({ basePath, data, refresh }) => ( 140 | 141 | 142 | 143 | 144 | 145 | 146 | ) 147 | 148 | const ItemTitle = translate(({ record, translate }) => ( 149 | 150 | {record ? translate('item-type.edit.title', { title: record.attributes.name }) : ''} 151 | 152 | )); 153 | 154 | export const ItemEdit = ({ ...props }) => ( 155 | } actions={} {...props}> 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | ); 168 | 169 | const ItemShowActions = ({ basePath, data, refresh }) => ( 170 | 171 | 172 | 173 | 174 | 175 | 176 | ) 177 | 178 | export const ItemShow = ({ ...props }) => ( 179 | } actions={} {...props}> 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | ); 192 | -------------------------------------------------------------------------------- /src/app/fields/FormatValidationField.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Toggle from 'material-ui/Toggle'; 4 | import SelectField from 'material-ui/SelectField'; 5 | import MenuItem from 'material-ui/MenuItem'; 6 | import TextField from 'material-ui/TextField'; 7 | import { 8 | FieldTitle, 9 | translate, 10 | required, 11 | } from 'admin-on-rest'; 12 | import { 13 | Field, 14 | } from 'redux-form'; 15 | 16 | import ToggleInput from '../input/ToggleInput'; 17 | 18 | const styles = { 19 | block: { 20 | margin: '1rem 0', 21 | maxWidth: 250, 22 | }, 23 | }; 24 | 25 | export const URL = "url"; 26 | export const EMAIL = "email"; 27 | export const CUSTOM = "custom"; 28 | 29 | const renderValueField = ({ input, label, hintText, meta }) => ( 30 | 38 | } 39 | {...input} 40 | /> 41 | ); 42 | 43 | const valueValidator = (value, _, props) => { 44 | let regexpError = undefined; 45 | try { 46 | new RegExp(value); 47 | } catch (e) { 48 | regexpError = props.translate("validator.invalidRegexp"); 49 | } 50 | 51 | return required(value, _, props) || regexpError; 52 | } 53 | 54 | class FormatValidationField extends Component { 55 | constructor(props) { 56 | super(props); 57 | 58 | this.currentFormatType = this.currentFormatType.bind(this); 59 | this.handleToggle = this.handleToggle.bind(this); 60 | this.handleSelect = this.handleSelect.bind(this); 61 | this.handleCustomPattern = this.handleCustomPattern.bind(this); 62 | 63 | this.state = { 64 | type: this.currentFormatType(), 65 | }; 66 | } 67 | 68 | currentFormatType() { 69 | let value = this.props.input.value; 70 | 71 | if (!value) { 72 | return null; 73 | } 74 | 75 | let hasPredefinedPattern = !!value.predefinedPattern; 76 | let hasCustomPattern = !!value.customPattern; 77 | 78 | if (hasPredefinedPattern) { 79 | if (value.predefinedPattern == URL) { 80 | return URL; 81 | } else if (value.predefinedPattern == EMAIL) { 82 | return EMAIL; 83 | } 84 | } 85 | if (hasCustomPattern) { 86 | return CUSTOM; 87 | } 88 | 89 | return URL; 90 | } 91 | 92 | handleToggle(event, toggled) { 93 | if (toggled) { 94 | this.setState({ type: URL }); 95 | } 96 | } 97 | 98 | handleSelect(event, key, type) { 99 | this.setState({ type }); 100 | 101 | switch (type) { 102 | case URL: 103 | this.props.input.onChange({ predefinedPattern: URL, customPattern: null }); 104 | break; 105 | case EMAIL: 106 | this.props.input.onChange({ predefinedPattern: EMAIL, customPattern: null }); 107 | break; 108 | case CUSTOM: 109 | this.props.input.onChange({ predefinedPattern: null, customPattern: null }); 110 | break; 111 | } 112 | } 113 | 114 | handleCustomPattern(event, value) { 115 | this.props.input.onChange({ customPattern: value }); 116 | } 117 | 118 | render() { 119 | const { elStyle, label, input, source, resource } = this.props; 120 | const { type } = this.state; 121 | return ( 122 |
123 | 132 | {input.value && 133 |
134 | 138 | 142 | } /> 143 | 147 | } /> 148 | 152 | } /> 153 | 154 | 155 |
156 | {(type == CUSTOM) && 157 | 164 | } 165 |
166 | 167 |
168 | } 169 |
170 | ); 171 | } 172 | } 173 | 174 | FormatValidationField.propTypes = { 175 | addField: PropTypes.bool.isRequired, 176 | elStyle: PropTypes.object, 177 | input: PropTypes.object, 178 | isRequired: PropTypes.bool, 179 | label: PropTypes.string, 180 | resource: PropTypes.string, 181 | source: PropTypes.string, 182 | options: PropTypes.object, 183 | }; 184 | 185 | FormatValidationField.defaultProps = { 186 | addField: true, 187 | options: {}, 188 | }; 189 | 190 | export default translate(FormatValidationField); 191 | -------------------------------------------------------------------------------- /src/app/apiClient.js: -------------------------------------------------------------------------------- 1 | import { stringify } from 'query-string'; 2 | import { 3 | GET_LIST, 4 | GET_ONE, 5 | GET_MANY, 6 | GET_MANY_REFERENCE, 7 | CREATE, 8 | UPDATE, 9 | DELETE, 10 | fetchUtils, 11 | } from 'admin-on-rest' 12 | 13 | import { 14 | ITEM_TYPES, 15 | FIELDS, 16 | ITEMS 17 | } from "./resources" 18 | 19 | // TODO: - remove this 20 | const URL = "http://localhost:3000/api/projects/1" 21 | 22 | const client = (url, options = {}) => { 23 | if (!options.headers) { 24 | options.headers = new Headers({ Accept: 'application/json' }); 25 | } 26 | const token = localStorage.getItem('token'); 27 | options.headers.set('Authorization', `Bearer ${token}`); 28 | return fetchUtils.fetchJson(url, options); 29 | } 30 | 31 | /** 32 | * Adds the `type` to the params based on the resource and the request type. 33 | * E.g. it will add `item_type` to `data.type` for the CREATE operation of 34 | * an `item-type` object. 35 | * 36 | * @param {*} type 37 | * @param {*} resource 38 | * @param {*} params 39 | */ 40 | const addResourceType = (type, resource, params) => { 41 | switch (type) { 42 | // only CREATE and UPDATE contains the `data` parameter which needs a `type` 43 | case CREATE: 44 | case UPDATE: 45 | if (typeof params.data === "object" && !Array.isArray(params.data)) { 46 | switch (resource) { 47 | case ITEM_TYPES: { 48 | params.data.type = "item_type"; 49 | break; 50 | } 51 | case FIELDS: { 52 | params.data.type = "field"; 53 | if (params.data.relationships && params.data.relationships.itemType && params.data.relationships.itemType.data) { 54 | params.data.relationships.itemType.data.type = "item_type"; 55 | } 56 | break; 57 | } 58 | case ITEMS: { 59 | params.data.type = "item"; 60 | break; 61 | } 62 | default: { 63 | // ignore 64 | break; 65 | } 66 | } 67 | } 68 | break; 69 | default: { 70 | // ignore 71 | break; 72 | } 73 | } 74 | return (type, resource, params) 75 | } 76 | 77 | const urlForType = (apiUrl, type, resource, params) => { 78 | switch (type) { 79 | case GET_MANY: 80 | case GET_MANY_REFERENCE: 81 | case GET_LIST: { 82 | return `${apiUrl}/${resource}?${stringify(params)}`; 83 | } 84 | case GET_ONE: 85 | return `${apiUrl}/${resource}/${params.id}`; 86 | case UPDATE: 87 | return `${apiUrl}/${resource}/${params.id}`; 88 | case CREATE: 89 | if (typeof params.data === "object" && !Array.isArray(params.data)) { 90 | switch (resource) { 91 | case FIELDS: { 92 | return `${apiUrl}/item-types/${params.data.relationships.itemType.data.id}/fields`; 93 | } 94 | } 95 | } 96 | return `${apiUrl}/${resource}`; 97 | case DELETE: 98 | return `${apiUrl}/${resource}/${params.id}`; 99 | default: 100 | throw new Error(`Unsupported fetch action type ${type}`); 101 | } 102 | return `${apiUrl}/${resource}`; 103 | } 104 | 105 | /** 106 | * Maps admin-on-rest queries to a simple REST API 107 | * 108 | * The REST dialect is similar to the one of FakeRest 109 | * @see https://github.com/marmelab/FakeRest 110 | * @example 111 | * GET_LIST => GET http://my.api.url/posts?sort=['title','ASC']&range=[0, 24] 112 | * GET_ONE => GET http://my.api.url/posts/123 113 | * GET_MANY => GET http://my.api.url/posts?filter={ids:[123,456,789]} 114 | * UPDATE => PUT http://my.api.url/posts/123 115 | * CREATE => POST http://my.api.url/posts/123 116 | * DELETE => DELETE http://my.api.url/posts/123 117 | */ 118 | const apiClientFactory = (apiUrl, httpClient = client) => { 119 | /** 120 | * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE' 121 | * @param {String} resource Name of the resource to fetch, e.g. 'posts' 122 | * @param {Object} params The REST request params, depending on the type 123 | * @returns {Object} { url, options } The HTTP request parameters 124 | */ 125 | const convertRESTRequestToHTTP = (type, resource, params) => { 126 | const options = {}; 127 | type, resource, params = addResourceType(type, resource, params) 128 | let url = urlForType(apiUrl, type, resource, params); 129 | switch (type) { 130 | case GET_MANY: 131 | case GET_MANY_REFERENCE: 132 | case GET_LIST: { 133 | break; 134 | } 135 | case GET_ONE: 136 | break; 137 | case UPDATE: 138 | options.method = 'PUT'; 139 | options.body = JSON.stringify(params); 140 | break; 141 | case CREATE: 142 | options.method = 'POST'; 143 | options.body = JSON.stringify(params); 144 | break; 145 | case DELETE: 146 | options.method = 'DELETE'; 147 | break; 148 | default: 149 | throw new Error(`Unsupported fetch action type ${type}`); 150 | } 151 | return { url, options }; 152 | }; 153 | 154 | /** 155 | * @param {Object} response HTTP response from fetch() 156 | * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE' 157 | * @param {String} resource Name of the resource to fetch, e.g. 'posts' 158 | * @param {Object} params The REST request params, depending on the type 159 | * @returns {Object} REST response 160 | */ 161 | const convertHTTPResponseToREST = (response, type, resource, params) => { 162 | const { headers, json } = response; 163 | switch (type) { 164 | case GET_LIST: 165 | return { 166 | data: json.data, 167 | total: json.data.length, 168 | }; 169 | default: 170 | return json; 171 | } 172 | }; 173 | 174 | /** 175 | * @param {string} type Request type, e.g GET_LIST 176 | * @param {string} resource Resource name, e.g. "posts" 177 | * @param {Object} payload Request parameters. Depends on the request type 178 | * @returns {Promise} the Promise for a REST response 179 | */ 180 | return (type, resource, params) => { 181 | const { url, options } = convertRESTRequestToHTTP( 182 | type, 183 | resource, 184 | params 185 | ); 186 | return httpClient(url, options).then(response => 187 | convertHTTPResponseToREST(response, type, resource, params) 188 | ).catch((error) => { 189 | if (error.body && error.body.error) { 190 | throw new Error(error.body.error); 191 | } 192 | throw error; 193 | }); 194 | }; 195 | }; 196 | 197 | export default apiClientFactory(URL); -------------------------------------------------------------------------------- /src/app/i18n/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | resources: { 3 | posts: { 4 | name: 'Post |||| Posts', 5 | fields: { 6 | allow_comments: 'Allo comments?', 7 | average_note: 'Average note', 8 | body: 'Body', 9 | comments: 'Comments', 10 | commentable: 'Commentable', 11 | commentable_short: 'Com.', 12 | created_at: 'Created at', 13 | notifications: 'Notifications recipients', 14 | nb_view: 'Nb views', 15 | password: 'Password (if protected post)', 16 | pictures: 'Related Pictures', 17 | published_at: 'Published at', 18 | teaser: 'Teaser', 19 | tags: 'Tags', 20 | title: 'Title', 21 | views: 'Views', 22 | }, 23 | }, 24 | comments: { 25 | name: 'Comment |||| Comments', 26 | fields: { 27 | body: 'Body', 28 | created_at: 'Created at', 29 | post_id: 'Posts', 30 | author: { 31 | name: 'Author', 32 | }, 33 | }, 34 | }, 35 | users: { 36 | name: 'User |||| Users', 37 | fields: { 38 | name: 'Name', 39 | role: 'Role', 40 | }, 41 | }, 42 | 'item-types': { 43 | name: 'Item-Type |||| Item-Types', 44 | fields: { 45 | attributes: { 46 | id: 'ID', 47 | name: 'Name', 48 | apiKey: 'Item-Type Key', 49 | } 50 | }, 51 | }, 52 | 'fields': { 53 | name: 'Field |||| Fields', 54 | fields: { 55 | attributes: { 56 | id: 'ID', 57 | label: 'Name', 58 | apiKey: 'Field ID', 59 | fieldType: 'Type', 60 | hint: 'Help text', 61 | localized: 'Enable localization on this field?', 62 | validators: { 63 | required: 'Required', 64 | unique: 'Unique field', 65 | length: 'Limit character count', 66 | format: 'Match a specific pattern', 67 | enum: 'Accept only specific values', 68 | }, 69 | appearance: { 70 | type: 'Presentation mode', 71 | }, 72 | }, 73 | relationships: { 74 | itemType: { 75 | data: { 76 | id: 'Item-Type' 77 | } 78 | } 79 | } 80 | }, 81 | }, 82 | 'items': { 83 | name: 'Item |||| Items', 84 | fields: { 85 | attributes: { 86 | }, 87 | relationships: { 88 | itemType: { 89 | data: { 90 | id: 'Item-Type' 91 | } 92 | } 93 | } 94 | }, 95 | }, 96 | }, 97 | post: { 98 | list: { 99 | search: 'Search', 100 | }, 101 | form: { 102 | summary: 'Summary', 103 | body: 'Body', 104 | miscellaneous: 'Miscellaneous', 105 | comments: 'Comments', 106 | }, 107 | edit: { 108 | title: 'Post "%{title}"', 109 | }, 110 | action: { 111 | save_and_add: 'Save and Add', 112 | save_and_show: 'Save and Show', 113 | }, 114 | }, 115 | comment: { 116 | list: { 117 | about: 'About', 118 | }, 119 | }, 120 | user: { 121 | list: { 122 | search: 'Search', 123 | }, 124 | form: { 125 | summary: 'Summary', 126 | security: 'Security', 127 | }, 128 | edit: { 129 | title: 'User "%{title}"', 130 | }, 131 | action: { 132 | save_and_add: 'Save and Add', 133 | save_and_show: 'Save and Show', 134 | }, 135 | }, 136 | 'item-type': { 137 | form: { 138 | summary: 'Summary', 139 | fields: 'Fields', 140 | }, 141 | edit: { 142 | title: 'Item-Type "%{title}"', 143 | }, 144 | action: { 145 | save_and_add: 'Save and Add', 146 | save_and_show: 'Save and Show', 147 | add_field: 'Add field', 148 | }, 149 | }, 150 | 'field': { 151 | form: { 152 | summary: 'Summary', 153 | validations: 'Validations', 154 | presentation: 'Presentation', 155 | }, 156 | edit: { 157 | title: 'Field "%{title}"', 158 | }, 159 | action: { 160 | save_and_add: 'Save and Add', 161 | save_and_show: 'Save and Show', 162 | }, 163 | types: { 164 | string: 'Single-line string', 165 | text: 'Multiple-paragraph text', 166 | }, 167 | }, 168 | 'validator': { 169 | invalidRegexp: 'Invalid regexp', 170 | invalid_expression_format: 'Invalid expression format', 171 | invalid_expression_enum: 'Invalid expression enum', 172 | hints: { 173 | required: 'This field is required.', 174 | unique: 'This field must be unique.', 175 | length_between: 'This field must be between %{min} and %{max} characters long.', 176 | length_at_least: 'This field must be at least %{min} characters long.', 177 | length_at_most: 'This field must be at most %{max} characters long.', 178 | length_exactly: 'This field must be exactly %{eq} characters long.', 179 | format_email: 'This field must match an email.', 180 | format_url: 'This field must match an url.', 181 | format_custom: 'This field must match the regexp: %{pattern}', 182 | enum: 'This field must match one of the values: %{values}', 183 | }, 184 | length: { 185 | types: { 186 | between: 'Between', 187 | atLeast: 'At least', 188 | atMost: 'No more than', 189 | exactly: 'Exactly', 190 | }, 191 | values: { 192 | min: 'Minimum *', 193 | max: 'Maximum *', 194 | eq: 'Exactly *', 195 | and: 'and', 196 | }, 197 | }, 198 | format: { 199 | types: { 200 | url: 'URL', 201 | email: 'Email', 202 | custom: 'Custom format', 203 | }, 204 | values: { 205 | customPattern: 'Pattern', 206 | }, 207 | }, 208 | enum: { 209 | values: { 210 | values: 'Values', 211 | }, 212 | } 213 | }, 214 | 'presentation': { 215 | types: { 216 | 'title': 'Title', 217 | 'plain': 'Normal string', 218 | 'html': 'HTML Editor', 219 | 'markdown': 'Markdown Editor', 220 | 'no_format': 'No format', 221 | }, 222 | }, 223 | }; 224 | -------------------------------------------------------------------------------- /src/app/fields/LengthValidationField.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Toggle from 'material-ui/Toggle'; 4 | import SelectField from 'material-ui/SelectField'; 5 | import MenuItem from 'material-ui/MenuItem'; 6 | import TextField from 'material-ui/TextField'; 7 | import { Field } from "redux-form"; 8 | import { 9 | FieldTitle, 10 | translate, 11 | required, 12 | minValue, 13 | } from 'admin-on-rest'; 14 | 15 | import ToggleInput from '../input/ToggleInput'; 16 | 17 | const styles = { 18 | block: { 19 | margin: '1rem 0', 20 | maxWidth: 250, 21 | }, 22 | value: { 23 | maxWidth: 100, 24 | }, 25 | valuesContainer: { 26 | display: 'flex', 27 | alignItems: 'baseline', 28 | }, 29 | separator: { 30 | marginLeft: 8, 31 | marginRight: 8, 32 | }, 33 | }; 34 | 35 | export const BETWEEN = "between"; 36 | export const AT_LEAST = "atLeast"; 37 | export const AT_MOST = "atMost"; 38 | export const EXACTLY = "exactly"; 39 | 40 | const renderValueField = ({ input, label, hintText, meta }) => ( 41 | 50 | } 51 | {...input} 52 | /> 53 | ) 54 | 55 | const valueValidator = (value, _, props) => { 56 | return required(value, _, props) || minValue(0)(value, _, props); 57 | } 58 | 59 | class LengthValidationField extends Component { 60 | constructor(props) { 61 | super(props); 62 | 63 | this.currentLengthType = this.currentLengthType.bind(this); 64 | this.handleToggle = this.handleToggle.bind(this); 65 | this.handleSelect = this.handleSelect.bind(this); 66 | 67 | this.state = { 68 | type: this.currentLengthType(), 69 | }; 70 | } 71 | 72 | currentLengthType() { 73 | let value = this.props.input.value; 74 | 75 | if (!value) { 76 | return null; 77 | } 78 | 79 | let hasMin = !!value.min; 80 | let hasMax = !!value.max; 81 | let hasEq = !!value.eq; 82 | 83 | if (hasMin && hasMax) { 84 | return BETWEEN; 85 | } else if (hasMin) { 86 | return AT_LEAST; 87 | } else if (hasMax) { 88 | return AT_MOST; 89 | } else if (hasEq) { 90 | return EXACTLY; 91 | } 92 | 93 | return BETWEEN; 94 | } 95 | 96 | handleToggle(event, value) { 97 | if (value) { 98 | this.setState({ type: BETWEEN }); 99 | } 100 | } 101 | 102 | handleSelect(event, key, type) { 103 | this.setState({ type }); 104 | this.props.input.onChange({ min: "", max: "", eq: "" }); 105 | } 106 | 107 | render() { 108 | const { elStyle, label, input, source, resource } = this.props; 109 | const { type } = this.state; 110 | return ( 111 |
112 | 121 | {input.value && 122 |
123 | 127 | 131 | } /> 132 | 136 | } /> 137 | 141 | } /> 142 | 146 | } /> 147 | 148 | 149 |
150 | {(type == BETWEEN || type == AT_LEAST) && 151 | 158 | } 159 | {(type == BETWEEN) && 160 | 161 | 164 | 165 | } 166 | {(type == BETWEEN || type == AT_MOST) && 167 | 174 | } 175 | {(type == EXACTLY) && 176 | 183 | } 184 |
185 |
186 | } 187 |
188 | ); 189 | } 190 | } 191 | 192 | LengthValidationField.propTypes = { 193 | addField: PropTypes.bool.isRequired, 194 | elStyle: PropTypes.object, 195 | input: PropTypes.object, 196 | isRequired: PropTypes.bool, 197 | label: PropTypes.string, 198 | resource: PropTypes.string, 199 | source: PropTypes.string, 200 | options: PropTypes.object, 201 | }; 202 | 203 | LengthValidationField.defaultProps = { 204 | addField: true, 205 | options: {}, 206 | }; 207 | 208 | export default translate(LengthValidationField); 209 | -------------------------------------------------------------------------------- /src/app/fields/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | BooleanField, 4 | BooleanInput, 5 | CheckboxGroupInput, 6 | ChipField, 7 | Create, 8 | Datagrid, 9 | DateField, 10 | DateInput, 11 | DisabledInput, 12 | Edit, 13 | EditButton, 14 | Filter, 15 | ImageField, 16 | ImageInput, 17 | List, 18 | LongTextInput, 19 | NumberField, 20 | NumberInput, 21 | ReferenceArrayField, 22 | ReferenceManyField, 23 | ReferenceArrayInput, 24 | Responsive, 25 | RichTextField, 26 | SaveButton, 27 | SelectArrayInput, 28 | SelectField, 29 | SelectInput, 30 | Show, 31 | ShowButton, 32 | SimpleForm, 33 | SimpleList, 34 | SingleFieldList, 35 | SimpleShowLayout, 36 | ReferenceField, 37 | ReferenceInput, 38 | TextField, 39 | TextInput, 40 | TabbedForm, 41 | FormTab, 42 | Toolbar, 43 | minValue, 44 | number, 45 | required, 46 | translate, 47 | } from 'admin-on-rest'; // eslint-disable-line import/no-unresolved 48 | import { DependentField, DependentInput } from 'aor-dependent-input'; 49 | 50 | import ItemTypeReferenceField from '../itemTypes/ItemTypeReferenceField'; 51 | import LengthValidationField from './LengthValidationField'; 52 | import FormatValidationField from './FormatValidationField'; 53 | import EnumValidationField from './EnumValidationField'; 54 | import ToggleInput from '../input/ToggleInput' 55 | import ToggleField from '../field/ToggleField' 56 | import { 57 | STRING, 58 | TEXT, 59 | TYPES, 60 | } from './types'; 61 | import { 62 | TITLE, 63 | MARKDOWN, 64 | appearanceTypesForFieldType, 65 | } from './appearanceTypes'; 66 | 67 | export { FieldIcon } from 'material-ui/svg-icons/action/book'; 68 | 69 | const FieldCreateToolbar = props => ( 70 | 71 | 76 | 82 | 83 | ); 84 | 85 | export const FieldCreate = ({ ...props }) => ( 86 | 87 | } 89 | defaultValue={{ 90 | relationships: { 91 | itemType: { 92 | data: { 93 | id: new URLSearchParams(props.location.search).get("itemTypeId") 94 | } 95 | } 96 | }, 97 | }} 98 | > 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | ); 134 | 135 | const FieldEditToolbar = props => ( 136 | 137 | 141 | 142 | ); 143 | 144 | const FieldTitle = translate(({ record, translate }) => ( 145 | 146 | {record ? translate('field.edit.title', { title: record.attributes.label }) : ''} 147 | 148 | )); 149 | 150 | export const FieldEdit = ({ ...props }) => ( 151 | } {...props}> 152 | }> 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | ); 186 | -------------------------------------------------------------------------------- /src/app/data.js: -------------------------------------------------------------------------------- 1 | export default { 2 | posts: [ 3 | { 4 | id: 1, 5 | title: 6 | 'Accusantium qui nihil voluptatum quia voluptas maxime ab similique', 7 | teaser: 8 | 'In facilis aut aut odit hic doloribus. Fugit possimus perspiciatis sit molestias in. Sunt dignissimos sed quis at vitae veniam amet. Sint sunt perspiciatis quis doloribus aperiam numquam consequatur et. Blanditiis aut earum incidunt eos magnam et voluptatem. Minima iure voluptatum autem. At eaque sit aperiam minima aut in illum.', 9 | body: 10 | '

Rerum velit quos est similique. Consectetur tempora eos ullam velit nobis sit debitis. Magni explicabo omnis delectus labore vel recusandae.

Aut a minus laboriosam harum placeat quas minima fuga. Quos nulla fuga quam officia tempore. Rerum occaecati ut eum et tempore. Nam ab repudiandae et nemo praesentium.

Cumque corporis officia occaecati ducimus sequi laborum omnis ut. Nam aspernatur veniam fugit. Nihil eum libero ea dolorum ducimus impedit sed. Quidem inventore porro corporis debitis eum in. Nesciunt unde est est qui nulla. Esse sunt placeat molestiae molestiae sed quia. Sunt qui quidem quos velit reprehenderit quos blanditiis ducimus. Sint et molestiae maxime ut consequatur minima. Quaerat rem voluptates voluptatem quos. Corporis perferendis in provident iure. Commodi odit exercitationem excepturi et deserunt qui.

Optio iste necessitatibus velit non. Neque sed occaecati culpa porro culpa. Quia quam in molestias ratione et necessitatibus consequatur. Est est tempora consequatur voluptatem vel. Mollitia tenetur non quis omnis perspiciatis deserunt sed necessitatibus. Ad rerum reiciendis sunt aspernatur.

Est ullam ut magni aspernatur. Eum et sed tempore modi.

Earum aperiam sit neque quo laborum suscipit unde. Expedita nostrum itaque non non adipisci. Ut delectus quis delectus est at sint. Iste hic qui ea eaque eaque sed id. Hic placeat rerum numquam id velit deleniti voluptatem. Illum adipisci voluptas adipisci ut alias. Earum exercitationem iste quidem eveniet aliquid hic reiciendis. Exercitationem est sunt in minima consequuntur. Aut quaerat libero dolorem.

', 11 | views: 143, 12 | average_note: 2.72198, 13 | commentable: true, 14 | pictures: { 15 | first: { 16 | name: 'the picture name', 17 | url: 'http://www.photo-libre.fr/paysage/1.jpg', 18 | metas: { 19 | title: 'This is a great photo', 20 | definitions: ['72', '300'], 21 | authors: [ 22 | { 23 | name: 'Paul', 24 | email: 'paul@email.com', 25 | }, 26 | { 27 | name: 'Joe', 28 | email: 'joe@email.com', 29 | }, 30 | ], 31 | }, 32 | }, 33 | second: { 34 | name: 'better name', 35 | url: 'http://www.photo-libre.fr/paysage/2.jpg', 36 | }, 37 | }, 38 | published_at: new Date('2012-08-06'), 39 | tags: [1, 3], 40 | category: 'tech', 41 | subcategory: 'computers', 42 | backlinks: [ 43 | { 44 | date: '2012-08-09T00:00:00.000Z', 45 | url: 'http://example.com/bar/baz.html', 46 | }, 47 | ], 48 | notifications: [12, 31, 42], 49 | }, 50 | { 51 | id: 2, 52 | title: 'Sint dignissimos in architecto aut', 53 | teaser: 54 | 'Quam earum itaque corrupti labore quas nihil sed. Dolores sunt culpa voluptates exercitationem eveniet totam rerum. Molestias perspiciatis rem numquam accusamus.', 55 | body: 56 | '

Aliquam magni tempora quas enim. Perspiciatis libero corporis sunt eum nam. Molestias est sunt molestiae natus.

Blanditiis dignissimos autem culpa itaque. Explicabo perferendis ullam officia ut quia nemo. Eaque perspiciatis perspiciatis est hic non ullam et. Expedita exercitationem enim sit ut dolore.

Sed in sunt officia blanditiis ipsam maiores perspiciatis amet

Vero fugiat facere officiis aut quis rerum velit. Autem eius sint ullam. Nemo sunt molestiae nulla accusantium est voluptatem voluptas sed. In blanditiis neque libero voluptatem praesentium occaecati nulla libero. Perspiciatis eos voluptatem facere voluptatibus. Explicabo quo eveniet nihil culpa. Qui eos officia consequuntur sed esse praesentium dolorum. Eius perferendis qui quia autem nostrum sed. Illum in ex excepturi voluptas. Qui veniam sit alias delectus nihil. Impedit est ut alias illum repellendus qui.

Veniam est aperiam quisquam soluta. Magni blanditiis praesentium sed similique velit ipsam consequatur. Porro omnis magni sunt incidunt aspernatur ut.

', 57 | views: 563, 58 | average_note: 3.48121, 59 | commentable: true, 60 | published_at: new Date('2012-08-08'), 61 | tags: [3, 5], 62 | category: 'lifestyle', 63 | backlinks: [], 64 | notifications: [], 65 | }, 66 | { 67 | id: 3, 68 | title: 'Perspiciatis adipisci vero qui ipsam iure porro', 69 | teaser: 70 | 'Ut ad consequatur esse illum. Ex dolore porro et ut sit. Commodi qui sed et voluptatibus laudantium.', 71 | body: 72 | '

Voluptatibus fugit sit praesentium voluptas vero vel. Reprehenderit quam cupiditate deleniti ipsum nisi qui. Molestiae modi sequi vel quibusdam est aliquid doloribus. Necessitatibus et excepturi alias necessitatibus magnam ea.

Dolor illum dolores qui et pariatur inventore incidunt molestias. Exercitationem ipsum voluptatibus voluptatum velit sint vel qui. Odit mollitia minus vitae impedit voluptatem. Voluptas ullam temporibus inventore fugiat pariatur odit molestias.

Atque est qui alias eum. Quibusdam rem ut dolores voluptate totam. Sit cumque perferendis sed a iusto laudantium quae et. Voluptatibus vitae natus quia laboriosam et deserunt. Doloribus fuga aut quo tempora animi eaque consequatur laboriosam.

', 73 | views: 467, 74 | commentable: true, 75 | published_at: new Date('2012-08-08'), 76 | tags: [1, 2], 77 | category: 'tech', 78 | backlinks: [ 79 | { 80 | date: '2012-08-10T00:00:00.000Z', 81 | url: 'http://example.com/foo/bar.html', 82 | }, 83 | { 84 | date: '2012-08-14T00:00:00.000Z', 85 | url: 'https://blog.johndoe.com/2012/08/12/foobar.html', 86 | }, 87 | { 88 | date: '2012-08-22T00:00:00.000Z', 89 | url: 'https://foo.bar.com/lorem/ipsum', 90 | }, 91 | { 92 | date: '2012-08-29T00:00:00.000Z', 93 | url: 'http://dicta.es/nam_doloremque', 94 | }, 95 | ], 96 | notifications: [12, 31, 42], 97 | }, 98 | { 99 | id: 4, 100 | title: 'Maiores et itaque aut perspiciatis', 101 | teaser: 102 | 'Et quo voluptas odit veniam omnis dolores. Odit commodi consequuntur necessitatibus dolorem officia. Reiciendis quas exercitationem libero sed. Itaque non facilis sit tempore aut doloribus.', 103 | body: 104 | '

Sunt sunt aut est et consequatur ea dolores. Voluptatem rerum cupiditate dolore. Voluptas sit sapiente corrupti error ducimus. Qui enim aut possimus qui. Impedit voluptatem sed inventore iusto et ut et. Maxime sunt qui adipisci expedita quisquam. Velit ea ut in blanditiis eos doloribus.

Qui optio ad magnam eius. Est id velit ratione eum corrupti non vitae. Quam consequatur animi sed corrupti quae sed deserunt. Accusamus eius eos recusandae eum quia id.

Voluptas omnis omnis culpa est vel eum. Ut in tempore harum voluptates odit delectus sit et. Consequuntur quod nihil veniam natus placeat provident. Totam ut fuga vitae in. Possimus cumque quae voluptatem asperiores vitae officiis dolores. Qui autem eos dolores eius. Iure ut delectus quis voluptatem. Velit at incidunt minus laboriosam culpa. Pariatur ipsa ut enim dolor. Sed magni sunt molestiae voluptas ut illum. Sit consequuntur laborum aliquid delectus in. Consectetur dicta asperiores itaque aut mollitia. Minus praesentium officiis voluptas a officiis ad beatae.

', 105 | views: 685, 106 | average_note: 1.2319, 107 | commentable: false, 108 | published_at: new Date('2012-08-12'), 109 | tags: [], 110 | category: 'lifestyle', 111 | notifications: [12, 31, 42], 112 | }, 113 | { 114 | id: 5, 115 | title: 'Sed quo et et fugiat modi', 116 | teaser: 117 | 'Consequuntur id aut soluta aspernatur sit. Aut doloremque recusandae sit saepe ut quas earum. Quae pariatur iure et ducimus non. Cupiditate dolorem itaque in sit.', 118 | body: 119 | '

Aut molestiae quae explicabo voluptas. Assumenda ea ipsam quia. Rerum rerum magnam sunt doloremque dolorem nulla. Eveniet ut aliquam est dignissimos nisi molestias dicta. Dolorum et id esse illum. Ea omnis nesciunt tempore et aut. Ut ullam totam doloribus recusandae est natus voluptatum officiis. Ea quam eos velit ipsam non accusamus praesentium.

Animi et minima alias sint. Reiciendis qui ipsam autem fugit consequuntur veniam. Vel cupiditate voluptas enim dolore cum ad. Ut iusto eius et.

Quis praesentium aut aut aut voluptas et. Quam laudantium at laudantium amet. Earum quidem eos earum quaerat nihil libero quia sed.

Autem voluptatem nostrum ullam numquam quis. Et aut unde nesciunt officiis nam eos ut distinctio. Animi est explicabo voluptas officia quos necessitatibus. Omnis debitis unde et qui rerum. Nisi repudiandae autem mollitia dolorum veritatis aut. Rem temporibus labore repellendus enim consequuntur dicta autem. Illum illo inventore possimus officiis quidem.

Ullam accusantium eaque perspiciatis. Quidem dolor minus aut quidem. Praesentium earum beatae eos eligendi nostrum. Dolor nam quo aut.

Accusamus aut tempora omnis magni sit quos eos aut. Vitae ut inventore facere neque rerum. Qui esse rem cupiditate sit.

Est minus odio sint reprehenderit. Consectetur dolores eligendi et quaerat sint vel magni. Voluptatum hic cum placeat ad ea reiciendis laborum et. Eos ab id suscipit.

', 120 | views: 559, 121 | average_note: 3, 122 | commentable: true, 123 | published_at: new Date('2012-08-05'), 124 | category: 'tech', 125 | notifications: [12, 31, 42], 126 | }, 127 | { 128 | id: 6, 129 | title: 'Minima ea vero omnis odit officiis aut', 130 | teaser: 131 | 'Omnis rerum voluptatem illum. Amet totam minus id qui aspernatur. Adipisci commodi velit sapiente architecto et molestias. Maiores doloribus quis occaecati quidem laborum. Quae quia quaerat est itaque. Vero assumenda quia tempora libero dicta quis asperiores magnam. Necessitatibus accusantium saepe commodi ut.', 132 | body: 133 | '

Sit autem rerum inventore repellendus. Enim placeat est ea dolor voluptas nisi alias. Repellat quam laboriosam repudiandae illum similique omnis non exercitationem. Modi mollitia omnis sed vel et expedita fugiat. Esse laboriosam doloribus deleniti atque quidem praesentium aliquid. Error animi ab excepturi quia. Et voluptates voluptatem et est quibusdam aspernatur. Fugiat consequatur veritatis commodi enim quaerat sint. Quis quae fuga exercitationem dolorem enim laborum numquam. Iste necessitatibus repellat in ea nihil et rem. Corporis dolores sed vitae consectetur dolores qui dicta. Laudantium et suscipit odit quidem qui. Provident libero eveniet distinctio debitis odio cum id dolorum. Consequuntur laboriosam qui ut magni sit dicta. Distinctio fugit voluptatibus voluptatem suscipit incidunt ut cupiditate. Magni harum in aut alias veniam. Eos aut impedit ut et. Iure aliquid adipisci aliquam et ab et qui. Itaque quod consequuntur dolore asperiores architecto neque. Exercitationem eum voluptas ut quis hic quo. Omnis quas porro laudantium. Qui magnam et totam quibusdam in quo. Impedit laboriosam eum sint soluta facere ut voluptatem.

', 134 | views: 208, 135 | average_note: 3.1214, 136 | published_at: new Date('2012-09-05'), 137 | tags: [1, 4], 138 | category: 'tech', 139 | notifications: [42], 140 | }, 141 | { 142 | id: 7, 143 | title: 'Illum veritatis corrupti exercitationem sed velit', 144 | teaser: 145 | 'Omnis hic quo aperiam fugiat iure amet est. Molestias ratione aut et dolor earum magnam placeat. Ad a quam ea amet hic omnis rerum.', 146 | body: 147 | '

Omnis sunt maxime qui consequatur perspiciatis et dolor. Assumenda numquam sit rerum aut dolores. Repudiandae rerum et quisquam. Perferendis cupiditate sequi non similique eum accusamus voluptas.

Officiis in voluptatum culpa ut eaque laborum. Sit quos velit sed ad voluptates. Alias aut quo accusantium aut cumque perferendis. Numquam rerum vel et est delectus. Mollitia dolores voluptatum accusantium id rem. Autem dolorem similique earum. Deleniti qui iusto et vero. Enim quaerat ipsum omnis magni. Autem magnam vero nulla impedit distinctio. Sequi laudantium ut animi enim recusandae et voluptatum. Dicta architecto nostrum voluptas consequuntur ea. Porro odio illo praesentium qui. Quia sit sed labore porro. Minima odit nemo sint praesentium. Ea sapiente quis aut. Qui cumque aut repudiandae in. Ipsam mollitia ab vitae iusto maxime. Eaque qui impedit et ea dolor aut. Tenetur ut nihil sed. Eum doloremque harum ipsam vel eos ut enim.

', 148 | views: 133, 149 | average_note: null, 150 | commentable: true, 151 | published_at: new Date('2012-09-29'), 152 | tags: [3, 4], 153 | category: 'tech', 154 | notifications: [12, 31], 155 | }, 156 | { 157 | id: 8, 158 | title: 159 | 'Culpa possimus quibusdam nostrum enim tempore rerum odit excepturi', 160 | teaser: 161 | 'Qui quos exercitationem itaque quia. Repellat libero ut recusandae quidem repudiandae ipsam laudantium. Eveniet quos et quo omnis aut commodi incidunt.', 162 | body: 163 | '

Laudantium voluptatem non facere officiis qui natus natus. Ex perspiciatis quia dolor earum. In rerum deleniti voluptas quo quia adipisci voluptatibus.

Mollitia eos quaerat ad. Et non aliquam velit. Doloremque repudiandae earum suscipit deleniti.

Debitis voluptatem possimus saepe. Rerum nam est neque voluptate quae ratione et quaerat. Fugiat et ullam adipisci numquam. Atque qui cum quae quod qui reprehenderit. Veritatis odio eligendi est odit minima ut dolores. Blanditiis aut rem aliquam nulla esse odit. Quibusdam quam natus eos tenetur nemo eligendi velit nam. Consequatur libero eius quia impedit neque fuga. Accusantium sunt accusantium eaque illum dicta. Expedita explicabo quia soluta.

Dolores aperiam rem velit id provident quo ea. Modi illum voluptate corrupti recusandae optio. Voluptatem architecto numquam reiciendis quo nostrum suscipit. Dolore repellat deleniti nihil omnis illum explicabo nihil. Alias maxime hic minus voluptas odio id dolorum. Neque perferendis repellendus autem consequatur consequatur doloribus. Sit aspernatur nisi aliquam rem voluptas occaecati.

In eveniet nostrum culpa totam officia doloremque. Fugiat maxime magni aut magnam praesentium vel facere. Tempora soluta possimus omnis modi et qui minus. Consequatur et suscipit autem quia nulla.

Qui eum aliquid inventore at. Qui provident perspiciatis sed eum eos sunt eveniet autem. Ducimus velit tenetur sed. Quas laboriosam dicta ipsa id fugiat. Hic nihil laboriosam atque natus. Quam natus esse est error molestiae nulla. Odit ut dolorem laborum quidem quis alias. Labore sint porro et reprehenderit ut dolorem vel dolorum. Dolores suscipit ut dolores possimus id dicta cupiditate. Est cum dolorum dolores ducimus quia reprehenderit. Iste suscipit molestias voluptatem molestiae. Nostrum modi dicta qui deleniti. Reprehenderit voluptatem soluta non in labore. Voluptatem ut illo illo harum voluptas cumque. Tempora illo distinctio qui aut.

Eaque voluptatem eos omnis qui dolor non possimus. Distinctio ratione facere doloremque rerum qui voluptas et. Cum incidunt numquam molestias et labore odio sunt aut. Aut pariatur dignissimos est atque.

', 164 | views: 557, 165 | average_note: null, 166 | commentable: false, 167 | published_at: new Date('2012-10-02'), 168 | tags: [5, 1], 169 | category: 'lifestyle', 170 | notifications: [12, 31, 42], 171 | }, 172 | { 173 | id: 9, 174 | title: 'A voluptas eius eveniet ut commodi dolor', 175 | teaser: 176 | 'Sed necessitatibus nesciunt nesciunt aut non sunt. Quam ut in a sed ducimus eos qui sint. Commodi illo necessitatibus sint explicabo maiores. Maxime voluptates sit distinctio quo excepturi. Qui aliquid debitis repellendus distinctio et aut. Ex debitis et quasi id.', 177 | body: 178 | '

Consequatur temporibus explicabo vel laudantium totam. Voluptates nihil numquam accusamus ut unde quo. Molestiae dolores quas sit aliquam. Sit et fuga necessitatibus natus fugit voluptas et. Esse vitae sed sit eius.

Accusantium aliquam accusamus illo eum. Excepturi molestiae et earum qui. Iste dolor eligendi est vero iure eos nesciunt. Qui aspernatur repellendus id rerum consequatur ut. Quis ab quos fugit dicta aut voluptas. Rerum aut esse dolor. Illo iste ullam possimus nam nam assumenda molestiae est.

In porro nesciunt cumque in sint vel architecto. Aliquam et in numquam quae explicabo. Deserunt suscipit sunt excepturi optio molestiae. Facilis saepe eaque commodi provident ad voluptates eligendi.

Magnam et neque ad sed qui laborum et. Aut dolorem maxime harum. Molestias aut facere vitae voluptatem.

Excepturi odit doloremque eos quisquam sunt. Veniam repudiandae nisi dolorum nam quos. Qui voluptatem enim enim. Dolorum eveniet eaque expedita est tempore. Expedita amet blanditiis esse qui. Nam dolor odio nihil nobis quas quia exercitationem. Iusto ut ut reiciendis sint laudantium et distinctio. Vitae architecto accusamus quos dolores laudantium doloribus alias. Est est esse autem repellat. Assumenda officia aperiam sequi facere distinctio ut. Magnam qui assumenda eligendi sint. Architecto autem harum qui ea quos ut nesciunt et. Optio quidem sit ex quos provident. Et dolor dicta et laudantium. Incidunt id quo enim atque molestiae quam repudiandae omnis. Sed nam voluptatem dolores natus quisquam. Sit nostrum voluptate sed asperiores. Saepe eaque et illum aperiam. Maxime tenetur sunt reiciendis.

Ducimus quia dolorem voluptas ea. Fuga eum architecto eius cum est quibusdam eligendi est. In ut aperiam ea ut.

', 179 | views: 143, 180 | average_note: 3.1214, 181 | commentable: true, 182 | published_at: new Date('2012-10-16'), 183 | tags: [], 184 | category: 'tech', 185 | notifications: [12, 31, 42], 186 | }, 187 | { 188 | id: 10, 189 | title: 'Totam vel quasi a odio et nihil', 190 | teaser: 191 | 'Excepturi veritatis velit rerum nemo voluptatem illum tempora eos. Et impedit sed qui et iusto. A alias asperiores quia quo.', 192 | body: 193 | '

Voluptas iure consequatur repudiandae quibusdam iure. Quibusdam consequatur sit cupiditate aut eum iure. Provident ut aut est itaque ut eligendi sunt.

Odio ipsa dolore rem occaecati voluptatum neque. Quia est minima totam est dicta aliquid sed. Doloribus ea eligendi qui odit. Consectetur aut illum aspernatur exercitationem ut. Distinctio sapiente doloribus beatae natus mollitia. Nostrum cum magni autem expedita natus est nulla totam.

Et possimus quia aliquam est molestiae eum. Dicta nostrum ea rerum omnis. Ut hic amet sequi commodi voluptatem ut. Nulla magni totam placeat asperiores error.

', 194 | views: 721, 195 | average_note: 4.121, 196 | commentable: true, 197 | published_at: new Date('2012-10-19'), 198 | tags: [1, 4], 199 | category: 'lifestyle', 200 | notifications: [12, 31, 42], 201 | }, 202 | { 203 | id: 11, 204 | title: 'Omnis voluptate enim similique est possimus', 205 | teaser: 206 | 'Velit eos vero reprehenderit ut assumenda saepe qui. Quasi aut laboriosam quas voluptate voluptatem. Et eos officia repudiandae quaerat. Mollitia libero numquam laborum eos.', 207 | body: 208 | '

Ut qui a quis culpa impedit. Harum quae sunt aspernatur dolorem minima et dolorum. Consequatur sunt eveniet sit perspiciatis fuga praesentium. Quam voluptatem a ullam accusantium debitis eum consectetur.

Voluptas rem impedit omnis maiores saepe. Eum consequatur ut et consequatur repellat. Quos dolorem dolorum nihil dolor sit optio velit. Quasi quaerat enim omnis ipsum.

Officia asperiores ut doloribus. Architecto iste quia illo non. Deleniti enim odio aut amet eveniet. Modi sint aut excepturi quisquam error sed officia. Nostrum enim repellendus inventore minus. Itaque vitae ipsam quasi. Qui provident vero ab facere. Sit enim provident doloremque minus quam. Voluptatem expedita est maiores nihil est voluptatem error. Asperiores ut a est ducimus hic optio. Natus omnis ullam consectetur ducimus nisi sint ducimus odit. Soluta cupiditate ipsam magnam.

Illum magni aut autem in sed iure. Ea explicabo ducimus officia corrupti ipsam minima minima. Nihil ab similique modi sunt unde nisi. Iusto quis iste ut aut earum magni. Nisi nisi minima sapiente quos aut libero maxime. Ut consequuntur sit vel odio suscipit fugiat tempore et. Et eveniet aut voluptatibus aliquid accusantium quis qui et. Veniam rem ut et. Vel officiis et voluptatum eaque ipsum sit. Sed iste rem ipsam dolor maiores. Et animi aspernatur aut error. Quisquam veritatis voluptatem magnam id. Blanditiis dolorem quo et voluptatum.

', 209 | views: 294, 210 | average_note: 3.12942, 211 | commentable: true, 212 | published_at: new Date('2012-10-22'), 213 | tags: [4, 3], 214 | category: 'tech', 215 | subcategory: 'computers', 216 | pictures: null, 217 | backlinks: [ 218 | { 219 | date: '2012-10-29T00:00:00.000Z', 220 | url: 'http://dicta.es/similique_pariatur', 221 | }, 222 | ], 223 | notifications: [12, 31, 42], 224 | }, 225 | { 226 | id: 12, 227 | title: 'Qui tempore rerum et voluptates', 228 | teaser: 229 | 'Occaecati rem perferendis dolor aut numquam cupiditate. At tenetur dolores pariatur et libero asperiores porro voluptas. Officiis corporis sed eos repellendus perferendis distinctio hic consequatur.', 230 | body: 231 | '

Praesentium corrupti minus molestias eveniet mollitia. Sit dolores est tenetur eos veritatis. Vero aut molestias provident ducimus odit optio.

Minima amet accusantium dolores et. Iste eos necessitatibus iure provident rerum repellendus reiciendis eos. Voluptate dolorem dolore aliquid sed maiores.

Ut quia excepturi quidem quidem. Cupiditate qui est rerum praesentium consequatur ad. Minima rem et est. Ut odio nostrum fugit laborum. Quis vitae occaecati tenetur earum non architecto.

Minima est nobis accusamus sunt explicabo fuga. Ut ut ut officia labore ratione animi saepe et.

Accusamus quae ex rerum est eos nesciunt et. Nemo nam consequatur earum necessitatibus et. Eum corporis corporis quia at nihil consectetur accusamus. Ea eveniet et culpa maxime.

Et et quisquam odio sapiente. Voluptas ducimus beatae ratione et soluta esse ut animi. Ipsa architecto veritatis cumque in.

Voluptatem dolore sint aliquam excepturi. Pariatur quisquam a eum. Aut et sit quis et dolorem omnis. Molestias id cupiditate error ab.

Odio ut deleniti incidunt vel dolores eligendi. Nemo aut commodi accusamus alias reprehenderit dolorum eaque. Iure fugit quis occaecati aspernatur tempora iste.

Omnis repellat et sequi numquam accusantium doloribus eum totam. Ab assumenda facere qui voluptate. Temporibus non ipsa officia. Corrupti omnis ut dolores velit aliquam ut omnis consequuntur.

', 232 | views: 719, 233 | average_note: 2, 234 | commentable: true, 235 | published_at: new Date('2012-11-07'), 236 | tags: [], 237 | category: 'lifestyle', 238 | subcategory: 'fitness', 239 | pictures: { first: {}, second: {} }, 240 | backlinks: [ 241 | { 242 | date: '2012-08-07T00:00:00.000Z', 243 | url: 'http://example.com/foo/bar.html', 244 | }, 245 | { 246 | date: '2012-08-12T00:00:00.000Z', 247 | url: 'https://blog.johndoe.com/2012/08/12/foobar.html', 248 | }, 249 | ], 250 | notifications: [12, 31, 42], 251 | }, 252 | { 253 | id: 13, 254 | title: 'Fusce massa lorem, pulvinar a posuere ut, accumsan ac nisi', 255 | teaser: 256 | 'Quam earum itaque corrupti labore quas nihil sed. Dolores sunt culpa voluptates exercitationem eveniet totam rerum. Molestias perspiciatis rem numquam accusamus.', 257 | body: 258 | '

Curabitur eu odio ullamcorper, pretium sem at, blandit libero. Nulla sodales facilisis libero, eu gravida tellus ultrices nec. In ut gravida mi. Vivamus finibus tortor tempus egestas lacinia. Cras eu arcu nisl. Donec pretium dolor ipsum, eget feugiat urna iaculis ut.

Nullam lacinia accumsan diam, ac faucibus velit maximus ac. Donec eros ligula, ullamcorper sit amet varius eget, molestie nec sapien. Donec ac est non tellus convallis condimentum. Aliquam non vehicula mauris, ac rhoncus mi. Integer consequat ipsum a posuere ornare. Quisque mollis finibus libero scelerisque dapibus.

', 259 | views: 222, 260 | average_note: 4, 261 | commentable: true, 262 | published_at: new Date('2012-12-01'), 263 | tags: [3, 5], 264 | category: 'lifestyle', 265 | backlinks: [], 266 | notifications: [], 267 | }, 268 | ], 269 | comments: [ 270 | { 271 | id: 1, 272 | author: {}, 273 | post_id: 6, 274 | body: 275 | "Queen, tossing her head through the wood. 'If it had lost something; and she felt sure it.", 276 | created_at: new Date('2012-08-02'), 277 | }, 278 | { 279 | id: 2, 280 | author: { 281 | name: 'Kiley Pouros', 282 | email: 'kiley@gmail.com', 283 | }, 284 | post_id: 9, 285 | body: 286 | "White Rabbit: it was indeed: she was out of the ground--and I should frighten them out of its right paw round, 'lives a March Hare. 'Sixteenth,'.", 287 | created_at: new Date('2012-08-08'), 288 | }, 289 | { 290 | id: 3, 291 | author: { 292 | name: 'Justina Hegmann', 293 | }, 294 | post_id: 3, 295 | body: 296 | "I'm not Ada,' she said, 'and see whether it's marked \"poison\" or.", 297 | created_at: new Date('2012-08-02'), 298 | }, 299 | { 300 | id: 4, 301 | author: { 302 | name: 'Ms. Brionna Smitham MD', 303 | }, 304 | post_id: 6, 305 | body: 306 | "Dormouse. 'Fourteenth of March, I think I can say.' This was such a noise inside, no one else seemed inclined.", 307 | created_at: new Date('2014-09-24'), 308 | }, 309 | { 310 | id: 5, 311 | author: { 312 | name: 'Edmond Schulist', 313 | }, 314 | post_id: 1, 315 | body: 316 | "I ought to tell me your history, you know,' the Hatter and the happy summer days. THE.", 317 | created_at: new Date('2012-08-07'), 318 | }, 319 | { 320 | id: 6, 321 | author: { 322 | name: 'Danny Greenholt', 323 | }, 324 | post_id: 6, 325 | body: 326 | 'Duchess asked, with another hedgehog, which seemed to be lost: away went Alice after it, never once considering how in the other. In the very tones of.', 327 | created_at: new Date('2012-08-09'), 328 | }, 329 | { 330 | id: 7, 331 | author: { 332 | name: 'Luciano Berge', 333 | }, 334 | post_id: 5, 335 | body: 336 | "While the Panther were sharing a pie--' [later editions continued as follows.", 337 | created_at: new Date('2012-09-06'), 338 | }, 339 | { 340 | id: 8, 341 | author: { 342 | name: 'Annamarie Mayer', 343 | }, 344 | post_id: 5, 345 | body: 346 | "I tell you, you coward!' and at once and put it more clearly,' Alice.", 347 | created_at: new Date('2012-10-03'), 348 | }, 349 | { 350 | id: 9, 351 | author: { 352 | name: 'Breanna Gibson', 353 | }, 354 | post_id: 2, 355 | body: 356 | "THAT. Then again--\"BEFORE SHE HAD THIS FIT--\" you never tasted an egg!' 'I HAVE tasted eggs, certainly,' said Alice, as she spoke. Alice did not like to have it.", 357 | created_at: new Date('2012-11-06'), 358 | }, 359 | { 360 | id: 10, 361 | author: { 362 | name: 'Logan Schowalter', 363 | }, 364 | post_id: 3, 365 | body: 366 | "I'd been the whiting,' said the Hatter, it woke up again with a T!' said the Gryphon. '--you advance twice--' 'Each with a growl, And concluded the banquet--] 'What IS the fun?' said.", 367 | created_at: new Date('2012-12-07'), 368 | }, 369 | { 370 | id: 11, 371 | author: { 372 | name: 'Logan Schowalter', 373 | }, 374 | post_id: 1, 375 | body: 376 | "I don't want to be?' it asked. 'Oh, I'm not Ada,' she said, 'and see whether it's marked \"poison\" or not'; for she had asked it aloud; and in despair she put her hand on the end of the.", 377 | created_at: new Date('2012-08-05'), 378 | }, 379 | ], 380 | tags: [ 381 | { 382 | id: 1, 383 | name: 'Sport', 384 | published: 1, 385 | }, 386 | { 387 | id: 2, 388 | name: 'Technology', 389 | published: false, 390 | }, 391 | { 392 | id: 3, 393 | name: 'Code', 394 | published: true, 395 | }, 396 | { 397 | id: 4, 398 | name: 'Photo', 399 | published: false, 400 | }, 401 | { 402 | id: 5, 403 | name: 'Music', 404 | published: 1, 405 | }, 406 | ], 407 | users: [ 408 | { 409 | id: 1, 410 | name: 'Logan Schowalter', 411 | role: 'admin', 412 | }, 413 | { 414 | id: 2, 415 | name: 'Breanna Gibson', 416 | role: 'user', 417 | }, 418 | { 419 | id: 3, 420 | name: 'Annamarie Mayer', 421 | role: 'user', 422 | }, 423 | ], 424 | }; 425 | --------------------------------------------------------------------------------