├── index.js
├── .npmignore
├── .babelrc
├── public
├── favicon.ico
├── manifest.json
└── index.html
├── .gitignore
├── src
├── AppLib
│ ├── ReadManyLib
│ │ ├── defaultCellFormatter.js
│ │ ├── Paginate.js
│ │ ├── Search.js
│ │ ├── TdAction.js
│ │ ├── getReadManyInputQueryString.js
│ │ ├── Tr.js
│ │ └── ThField.js
│ ├── CreateOrReadOne.js
│ ├── formLib
│ │ ├── formatLabel.js
│ │ ├── Label.js
│ │ ├── TitleField.js
│ │ ├── CurrencyWidget.js
│ │ ├── HasOneWidget.js
│ │ ├── NumberWidget.js
│ │ ├── renderValue.js
│ │ ├── MultiSelectField.js
│ │ ├── FieldTemplate.js
│ │ ├── withOptionItems.js
│ │ ├── HasManyField.js
│ │ ├── TagIdsField.js
│ │ └── WysiwygWidget.js
│ ├── NavBar.js
│ ├── IndexRoute.js
│ ├── Login.js
│ ├── LoginLib
│ │ ├── CodeLogin.js
│ │ └── EmailPasswordLogin.js
│ ├── Create.js
│ ├── Form.js
│ ├── ReadOne.js
│ ├── Update.js
│ └── ReadMany.js
├── App.test.js
├── GqlCmsConfigLib
│ ├── ensureUniqKey.js
│ ├── getCRUDSchemaFromResource.js
│ └── jsonSchemaToGqlQuery.js
├── stylesheets
│ ├── react-jsonschemaform.css
│ ├── bootstrap-override.css
│ ├── react-toastify.css
│ └── react-selectize.css
├── alertFirstGqlMsg.js
├── removeTypename.js
├── formLib
│ ├── nullToUndefined.js
│ └── undefinedToNull.js
├── graphqlWithoutCache.js
├── StateHOF.js
├── App.js
└── index.js
├── .eslintrc
├── package.json
└── README.md
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./dist/App')
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .eslintrc
2 |
3 | node_modules/
4 | build/
5 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-2"]
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/poetic/byob-cms/master/public/favicon.ico
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log*
2 | yarn-debug.log*
3 | yarn-error.log*
4 |
5 | node_modules/
6 | build/
7 | dist/
8 |
--------------------------------------------------------------------------------
/src/AppLib/ReadManyLib/defaultCellFormatter.js:
--------------------------------------------------------------------------------
1 | function defaultCellFormatter (value) {
2 | return value
3 | }
4 |
5 | export default defaultCellFormatter
6 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/src/GqlCmsConfigLib/ensureUniqKey.js:
--------------------------------------------------------------------------------
1 | function ensureUniqKey (jsonSchema, uniqKey) {
2 | return {
3 | ...jsonSchema,
4 | properties: {
5 | [uniqKey]: { type: 'string' },
6 | ...jsonSchema.properties,
7 | }
8 | }
9 | }
10 |
11 | export default ensureUniqKey
12 |
--------------------------------------------------------------------------------
/src/stylesheets/react-jsonschemaform.css:
--------------------------------------------------------------------------------
1 | /* react-jsonschema-form use text instead of icon in buttons */
2 | i.glyphicon { display: none; }
3 | .btn-add::after { content: '+'; }
4 | .array-item-move-up::after { content: '▲'; }
5 | .array-item-move-down::after { content: '▼'; }
6 | .array-item-remove::after { content: '✕'; }
7 |
--------------------------------------------------------------------------------
/src/alertFirstGqlMsg.js:
--------------------------------------------------------------------------------
1 | import { toast } from 'react-toastify'
2 |
3 | function alertFirstGqlMsg (e) {
4 | let message
5 | try {
6 | message = e.graphQLErrors[0].message
7 | } catch (discardedError) {
8 | message = e.message
9 | }
10 | console.error(e)
11 | toast.error(message)
12 | }
13 |
14 | export default alertFirstGqlMsg
15 |
--------------------------------------------------------------------------------
/src/AppLib/CreateOrReadOne.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Create from './Create'
3 | import ReadOne from './ReadOne'
4 |
5 | function CreateOrReadOne (props) {
6 | const uniqKeyValue = props.match.params[props.resource.uniqKey]
7 | return uniqKeyValue === 'new' ? :
8 | }
9 |
10 | export default CreateOrReadOne
11 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/GqlCmsConfigLib/getCRUDSchemaFromResource.js:
--------------------------------------------------------------------------------
1 | function getCRUDSchemaFromResource ({ config, resource, crudType }) {
2 | const key = crudType + 'Schema'
3 | const schema = {
4 | ...config.defaultSchema,
5 | ...config[key],
6 | ...resource.defaultSchema,
7 | ...resource[key],
8 | }
9 | return schema
10 | }
11 |
12 | export default getCRUDSchemaFromResource
13 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/formatLabel.js:
--------------------------------------------------------------------------------
1 | import { endsWith, startCase } from 'lodash'
2 | import pluralize from 'pluralize'
3 |
4 | function formatLabel (label) {
5 | const startCased = startCase(label)
6 | if (endsWith(startCased, ' Ids')) {
7 | return pluralize(startCased.replace(/ Ids$/, ''))
8 | } else if (endsWith(startCased, ' Id')) {
9 | return startCased.replace(/ Id$/, '')
10 | } else {
11 | return startCased
12 | }
13 | }
14 |
15 | export default formatLabel
16 |
--------------------------------------------------------------------------------
/src/removeTypename.js:
--------------------------------------------------------------------------------
1 | function removeTypename (data) {
2 | if (Array.isArray(data)) {
3 | return data.map(removeTypename)
4 | } else if (data && typeof data === 'object') {
5 | const convertedData = {}
6 | for (const key in data) {
7 | if (key !== '__typename') {
8 | convertedData[key] = removeTypename(data[key])
9 | }
10 | }
11 | return convertedData
12 | } else {
13 | return data
14 | }
15 | }
16 |
17 | export default removeTypename
18 |
--------------------------------------------------------------------------------
/src/formLib/nullToUndefined.js:
--------------------------------------------------------------------------------
1 | function nullToUndefined (data) {
2 | if (data === null) {
3 | return undefined
4 | } else if (Array.isArray(data)) {
5 | return data.map(nullToUndefined)
6 | } else if (typeof data === 'object') {
7 | return Object.keys(data).reduce((acc, key) => {
8 | return {
9 | ...acc,
10 | [key]: nullToUndefined(data[key])
11 | }
12 | }, {})
13 | } else {
14 | return data
15 | }
16 | }
17 |
18 | export default nullToUndefined
19 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb"
4 | ],
5 | "parser": "babel-eslint",
6 | "parserOptions": {
7 | "ecmaVersion": 8,
8 | "ecmaFeatures": {
9 | "experimentalObjectRestSpread": true,
10 | "experimentalDecorators": true
11 | }
12 | },
13 | "plugins": [
14 | "react"
15 | ],
16 | "rules": {
17 | "semi": 0,
18 | "react/forbid-prop-types": 0,
19 | "react/jsx-filename-extension": 0
20 | },
21 | "globals": {
22 | "alert": true,
23 | "document": true,
24 | "localStorage": true,
25 | "window": true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/Label.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import formatLabel from './formatLabel';
3 |
4 | const REQUIRED_FIELD_SYMBOL = "*";
5 |
6 | function Label(props) {
7 | const { label, required, id } = props;
8 | if (!label) {
9 | // See #312: Ensure compatibility with old versions of React.
10 | return
;
11 | }
12 | const formattedLabel = formatLabel(label)
13 | return (
14 |
17 | );
18 | }
19 |
20 |
21 | export default Label;
22 |
--------------------------------------------------------------------------------
/src/AppLib/ReadManyLib/Paginate.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactPaginate from 'react-paginate'
3 |
4 | function Paginate ({ skip, limit, total, onSkipChange }) {
5 | const pageCount = Math.ceil(total / limit)
6 | const forcePage = Math.ceil(skip / limit)
7 | return onSkipChange(limit * selected)}
12 | pageCount={pageCount}
13 | pageRangeDisplayed={5}
14 | marginPagesDisplayed={3}
15 | />
16 | }
17 |
18 | export default Paginate
19 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/TitleField.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import formatLabel from './formatLabel';
4 |
5 | const REQUIRED_FIELD_SYMBOL = "*";
6 |
7 | function TitleField(props) {
8 | const { id, title, required } = props;
9 | const formattedLabel = formatLabel(title)
10 | const legend = required ? formattedLabel + REQUIRED_FIELD_SYMBOL : formattedLabel;
11 | return ;
12 | }
13 |
14 | if (process.env.NODE_ENV !== "production") {
15 | TitleField.propTypes = {
16 | id: PropTypes.string,
17 | title: PropTypes.string,
18 | required: PropTypes.bool,
19 | };
20 | }
21 |
22 | export default TitleField;
23 |
--------------------------------------------------------------------------------
/src/formLib/undefinedToNull.js:
--------------------------------------------------------------------------------
1 | function undefinedToNull (data) {
2 | if (data === undefined) {
3 | return null
4 | } else if (Array.isArray(data)) {
5 | // remove null in array
6 | return data.filter((value) => value != null)
7 | } else if (typeof data === 'object') {
8 | const obj = Object.keys(data).reduce((acc, key) => {
9 | return {
10 | ...acc,
11 | [key]: undefinedToNull(data[key])
12 | }
13 | }, {})
14 | // use null to replace empty object
15 | const isPresent = Object.values(obj).some((value) => value !== null)
16 | return isPresent ? obj : null
17 | } else {
18 | return data
19 | }
20 | }
21 |
22 | export default undefinedToNull
23 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/CurrencyWidget.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import NumerWidget from './NumberWidget';
4 |
5 | const CurrencyWidget = ({ options, ...otherProps }) => {
6 | const mergedOptions = {
7 | ...options,
8 | props: {
9 | decimalScale: 2,
10 | placeholder: '$',
11 | prefix: '$',
12 | ...options.props,
13 | },
14 | };
15 |
16 | return (
17 |
21 | );
22 | };
23 |
24 | CurrencyWidget.propTypes = {
25 | options: PropTypes.object,
26 | };
27 |
28 | CurrencyWidget.defaultProps = {
29 | options: {},
30 | };
31 |
32 | export default CurrencyWidget;
33 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/HasOneWidget.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { SimpleSelect } from 'react-selectize'
3 | import withOptionItems from './withOptionItems'
4 | import { sortBy } from 'lodash'
5 |
6 | function SimpleSelectWidget (props) {
7 | const { optionItems } = props
8 | const valueItem = optionItems.find(option => option.value === props.value)
9 |
10 | return label.toUpperCase())}
13 | value={valueItem}
14 | onValueChange={v => props.onChange(v && v.value)}
15 | />
16 | }
17 |
18 | const HasOneWidget = withOptionItems(SimpleSelectWidget, 'widget')
19 |
20 | export default HasOneWidget
21 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/NumberWidget.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import NumberFormat from 'react-number-format';
4 |
5 | const NumberWidget = ({ onChange, value, options }) => {
6 | const { props = {} } = options;
7 |
8 | return (
9 | {
12 | onChange(floatValue || undefined);
13 | }}
14 | value={value}
15 | thousandSeparator
16 | {...props}
17 | />
18 | );
19 | };
20 |
21 | NumberWidget.propTypes = {
22 | onChange: PropTypes.func.isRequired,
23 | options: PropTypes.object,
24 | value: PropTypes.number,
25 | };
26 |
27 | NumberWidget.defaultProps = {
28 | options: {},
29 | value: undefined,
30 | };
31 |
32 | export default NumberWidget;
33 |
--------------------------------------------------------------------------------
/src/GqlCmsConfigLib/jsonSchemaToGqlQuery.js:
--------------------------------------------------------------------------------
1 | function jsonSchemaToGqlQuery (jsonSchema) {
2 | const { type, properties, items } = jsonSchema
3 | switch (type) {
4 | case 'string':
5 | case 'number':
6 | case 'integer':
7 | case 'boolean':
8 | return '';
9 | case 'object':
10 | const fields = Object
11 | .keys(properties)
12 | .map(key => ({ key, query: jsonSchemaToGqlQuery(properties[key]) }))
13 | .map(({ key, query }) => query ? `${key} ${query}` : key)
14 | .join('\n')
15 | return `{
16 | ${fields}
17 | }`
18 | case 'array':
19 | const query = jsonSchemaToGqlQuery(items)
20 | return query
21 | default:
22 | throw new Error(`jsonSchemaToGqlQuery unrecognized type: ${type}`)
23 | }
24 | }
25 |
26 | export default jsonSchemaToGqlQuery
27 |
--------------------------------------------------------------------------------
/src/AppLib/ReadManyLib/Search.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | class Search extends React.Component {
4 | render() {
5 | const { search, onSearchChange } = this.props
6 | const style = {
7 | maxWidth: "500px",
8 | margin: "30px 0",
9 | }
10 | return
27 | }
28 | }
29 |
30 | export default Search
31 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/renderValue.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | // add a "x" so that the user can remove that item directly
4 | function renderValue(item, sortable, items, parent) {
5 | return (
6 |
7 |
8 | {sortable && item.index > 0 &&
9 | {
12 | parent.reorder(item.value, '<')
13 | }}
14 | >
15 | {'<'}
16 |
17 | }
18 | {
21 | parent.removeByValue(item.value)
22 | }}
23 | >
24 | x
25 |
26 | {item.label}
27 | {sortable && item.index < items.length - 1 &&
28 | {
31 | parent.reorder(item.value, '>')
32 | }}
33 | >
34 | {'>'}
35 |
36 | }
37 |
38 |
39 | );
40 | }
41 |
42 | export default renderValue
43 |
--------------------------------------------------------------------------------
/src/AppLib/ReadManyLib/TdAction.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom';
3 | import { get } from 'lodash'
4 |
5 | function TdAction ({ resource, row, handleDelete }) {
6 | const resourceName = resource.displayName || resource.name
7 |
8 | return
9 | {
10 | resource.crudMapping.update
11 | ?
15 | Edit
16 |
17 | : null
18 | }
19 | {
20 | resource.crudMapping.delete
21 | ?
27 | : null
28 | }
29 | {
30 | (resource.crudMapping.readOne && get(resource, 'readOneSchema.show'))
31 | ?
35 | View
36 |
37 | : null
38 | }
39 | |
40 | }
41 |
42 | export default TdAction
43 |
--------------------------------------------------------------------------------
/src/graphqlWithoutCache.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { withApollo } from 'react-apollo';
3 |
4 | class DataFetchingComponent extends React.Component {
5 | constructor(props) {
6 | super(props)
7 | this.state = {
8 | loading: true,
9 | }
10 | }
11 | componentDidMount() {
12 | const { client, Query, queryOptions } = this.props
13 | client
14 | .query({ ...queryOptions, query: Query, fetchPolicy: 'network-only' })
15 | .then((res) => {
16 | this.setState({
17 | loading: false,
18 | ...res.data,
19 | })
20 | })
21 | .catch((e) => {
22 | throw e;
23 | })
24 | }
25 | render() {
26 | const { Component } = this.props
27 | return
31 | }
32 | }
33 |
34 | function graphqlWithoutCache (Query, queryOptionsContainer={}) {
35 | return (Component) => {
36 | return withApollo((props) => )
42 | }
43 | }
44 |
45 | export default graphqlWithoutCache
46 |
--------------------------------------------------------------------------------
/src/AppLib/NavBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { connect } from 'react-redux'
4 | import { setAccessToken } from '../StateHOF'
5 |
6 | function NavBar (props) {
7 | const { accessToken, setAccessToken, config } = props
8 | const brand = config.brand || 'CMS'
9 |
10 | function handleClick () {
11 | setAccessToken(null)
12 | window.location = '/'
13 | }
14 |
15 | return
36 | }
37 |
38 | const NavBarWithState = connect(
39 | (state) => ({ accessToken: state.accessToken }),
40 | { setAccessToken }
41 | )(NavBar)
42 |
43 |
44 | export default NavBarWithState
45 |
--------------------------------------------------------------------------------
/src/stylesheets/bootstrap-override.css:
--------------------------------------------------------------------------------
1 | /* https://bootsnipp.com/snippets/featured/bootstrap-outline-buttons */
2 | .btn-outline {
3 | background-color: transparent;
4 | color: inherit;
5 | transition: all .5s;
6 | }
7 |
8 | .btn-primary.btn-outline {
9 | color: #428bca;
10 | }
11 |
12 | .btn-success.btn-outline {
13 | color: #5cb85c;
14 | }
15 |
16 | .btn-info.btn-outline {
17 | color: #5bc0de;
18 | }
19 |
20 | .btn-warning.btn-outline {
21 | color: #f0ad4e;
22 | }
23 |
24 | .btn-danger.btn-outline {
25 | color: #d9534f;
26 | }
27 |
28 | .btn-primary.btn-outline:hover,
29 | .btn-success.btn-outline:hover,
30 | .btn-info.btn-outline:hover,
31 | .btn-warning.btn-outline:hover,
32 | .btn-danger.btn-outline:hover {
33 | color: #fff;
34 | }
35 |
36 | /* change columns structure of the first level fields */
37 | @media (min-width: 1200px) {
38 | .rjsf {
39 | width: 50%;
40 | }
41 | /* .rjsf > .form-group.field.field-object fieldset > .form-group { */
42 | /* width: 50%; */
43 | /* } */
44 | }
45 |
46 | /* selectize */
47 | .rjsf .react-selectize.simple-select, .rjsf .react-selectize.multi-select {
48 | width: 100%;
49 | }
50 |
51 | /* form add padding to end */
52 | .rjsf {
53 | margin-bottom: 200px;
54 | }
55 |
--------------------------------------------------------------------------------
/src/AppLib/IndexRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route, Link, Redirect } from 'react-router-dom';
3 | import { startCase } from 'lodash'
4 | import pluralize from 'pluralize'
5 |
6 | function IndexRoute (props) {
7 | const { config, history } = props
8 | if (history.location.pathname === '/' && config.initialPath) {
9 | return
10 | }
11 |
12 | const indexRouteElement = {
16 | const resourceLinks = config
17 | .resources
18 | .reduce((acc, resource) => {
19 | if (resource.crudMapping.readMany) {
20 | const resourceName = resource.displayName || resource.name
21 |
22 | acc.push(
23 | |
24 |
25 | {startCase(pluralize(resourceName))}
26 |
27 | |
28 |
);
29 | }
30 | return acc;
31 | }, [])
32 |
33 | return
34 |
Site Administration
35 |
36 |
37 | {resourceLinks}
38 |
39 |
40 |
41 | }}
42 | />
43 |
44 | return indexRouteElement
45 | }
46 |
47 | export default IndexRoute
48 |
--------------------------------------------------------------------------------
/src/AppLib/ReadManyLib/getReadManyInputQueryString.js:
--------------------------------------------------------------------------------
1 | function getReadManyInputQueryString (schema, variables={}) {
2 | const { paginationStrategy, sortStrategy, searchStrategy } = schema
3 | const inputPairs = []
4 | if (paginationStrategy) {
5 | inputPairs.push({ key: 'skip', value: variables.skip })
6 | inputPairs.push({ key: 'limit', value: variables.limit })
7 | }
8 |
9 | if (sortStrategy) {
10 | // note: the replace is used to remove quotes from properties
11 | const sortValue = [].concat(variables.sort)
12 | const shouldAddDefaultSort = sortStrategy.type === 'SINGLE'
13 | && sortValue.length === 0
14 | && sortStrategy.defaultSortField
15 | if (shouldAddDefaultSort) {
16 | sortValue.push({
17 | field: sortStrategy.defaultSortField,
18 | order: 'ASC',
19 | })
20 | }
21 | inputPairs.push({
22 | key: 'sort',
23 | value: JSON.stringify(sortValue).replace(/"([^(")"]+)":/g,"$1:"),
24 | })
25 | }
26 |
27 | if (searchStrategy) {
28 | inputPairs.push({
29 | key: 'search',
30 | value: `"${variables.search}"`
31 | })
32 | }
33 |
34 | if (Object.keys(inputPairs).length === 0) {
35 | return ''
36 | }
37 |
38 | const string = inputPairs
39 | .map(({ key, value }) => {
40 | return [key, value].join(': ')
41 | })
42 | .join(', ')
43 |
44 | return `(${string})`
45 | }
46 |
47 | export default getReadManyInputQueryString
48 |
--------------------------------------------------------------------------------
/src/AppLib/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { graphql } from 'react-apollo'
3 | import LoginMutation from '../resolvers/Mutation/LoginMutation'
4 | import alertFirstGqlMsg from '../alertFirstGqlMsg'
5 |
6 | class Login extends React.Component {
7 | constructor(props) {
8 | super(props)
9 | this.state = {
10 | code: ''
11 | }
12 | }
13 |
14 | render() {
15 | const onSubmit = async (e) => {
16 | e.preventDefault();
17 | try {
18 | const response = await this.props.mutate({
19 | variables: {
20 | loginInput: {
21 | code: this.state.code
22 | }
23 | }
24 | })
25 | const token = response.data.login
26 | this.props.setAccessToken(token)
27 | } catch (e) {
28 | alertFirstGqlMsg(e)
29 | }
30 | }
31 |
32 | const containerStyle = {
33 | display: 'flex',
34 | flexDirection: 'column',
35 | justifyContent: 'center',
36 | alignItems: 'center'
37 | }
38 |
39 | return
40 |
Login
41 |
49 |
50 | }
51 | }
52 |
53 | const LoginFormWithData = graphql(LoginMutation)(Login)
54 |
55 | export default LoginFormWithData
56 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/MultiSelectField.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { MultiSelect } from 'react-selectize';
3 | import Label from './Label';
4 | import renderValue from './renderValue';
5 |
6 | class MultiSelectWidget extends React.Component {
7 | render() {
8 | const {
9 | loading,
10 | optionItems,
11 | name,
12 | uiSchema,
13 | idSchema,
14 | required,
15 | onChange,
16 | formData,
17 | disabled,
18 | readonly,
19 | } = this.props
20 |
21 | if (loading) {
22 | return null;
23 | }
24 |
25 | const id = idSchema.$id;
26 | const label = uiSchema["ui:title"] || name;
27 | const valueObjects = optionItems
28 | .filter(option => (formData || []).includes(option.value))
29 |
30 | // NOTE: the reason we do this is to fix a bug in react selectize
31 | // that package caches the renderValue function and it will use the
32 | // up to date closure data for the function, we use a instance method
33 | // to bypass this problem
34 | this.removeByValue = (value) => {
35 | const nextFormData = formData.filter(v => v !== value)
36 | onChange(nextFormData)
37 | }
38 |
39 | return
40 |
41 | {
46 | onChange(v.map(v => v.value))
47 | }}
48 | renderValue={item => renderValue(item, this)}
49 | />
50 |
51 | }
52 | }
53 |
54 | export default MultiSelectWidget
55 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/FieldTemplate.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import Label from './Label';
4 |
5 | function FieldTemplate(props) {
6 | const {
7 | id,
8 | classNames,
9 | label,
10 | children,
11 | errors,
12 | help,
13 | description,
14 | hidden,
15 | required,
16 | displayLabel,
17 | } = props;
18 | if (hidden) {
19 | return children;
20 | }
21 |
22 | return (
23 |
24 | {displayLabel && }
25 | {displayLabel && description ? description : null}
26 | {children}
27 | {errors}
28 | {help}
29 |
30 | );
31 | }
32 |
33 | if (process.env.NODE_ENV !== "production") {
34 | FieldTemplate.propTypes = {
35 | id: PropTypes.string,
36 | classNames: PropTypes.string,
37 | label: PropTypes.string,
38 | children: PropTypes.node.isRequired,
39 | errors: PropTypes.element,
40 | rawErrors: PropTypes.arrayOf(PropTypes.string),
41 | help: PropTypes.element,
42 | rawHelp: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
43 | description: PropTypes.element,
44 | rawDescription: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
45 | hidden: PropTypes.bool,
46 | required: PropTypes.bool,
47 | readonly: PropTypes.bool,
48 | displayLabel: PropTypes.bool,
49 | fields: PropTypes.object,
50 | formContext: PropTypes.object,
51 | };
52 | }
53 |
54 | FieldTemplate.defaultProps = {
55 | hidden: false,
56 | readonly: false,
57 | required: false,
58 | displayLabel: true,
59 | };
60 |
61 | export default FieldTemplate
62 |
--------------------------------------------------------------------------------
/src/AppLib/LoginLib/CodeLogin.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { gql, graphql } from 'react-apollo'
3 | import alertFirstGqlMsg from '../../alertFirstGqlMsg'
4 | import { setAccessToken } from '../../StateHOF';
5 | import { connect } from 'react-redux'
6 |
7 | const LoginMutation = gql`
8 | mutation LoginMutation($loginInput: LoginInput!) {
9 | login(loginInput: $loginInput)
10 | }
11 | `
12 |
13 | class Login extends React.Component {
14 | constructor(props) {
15 | super(props)
16 | this.state = {
17 | code: ''
18 | }
19 | }
20 |
21 | render() {
22 | const onSubmit = async (e) => {
23 | e.preventDefault();
24 | try {
25 | const response = await this.props.mutate({
26 | variables: {
27 | loginInput: {
28 | code: this.state.code
29 | }
30 | }
31 | })
32 | const token = response.data.login
33 | this.props.setAccessToken(token)
34 | } catch (e) {
35 | alertFirstGqlMsg(e)
36 | }
37 | }
38 |
39 | const containerStyle = {
40 | display: 'flex',
41 | flexDirection: 'column',
42 | justifyContent: 'center',
43 | alignItems: 'center'
44 | }
45 |
46 | return
47 |
Login
48 |
56 |
57 | }
58 | }
59 |
60 | const LoginFormWithData = graphql(LoginMutation)(Login)
61 |
62 | const LoginFormWithState = connect(
63 | (state) => ({ accessToken: state.accessToken }),
64 | { setAccessToken }
65 | )(LoginFormWithData)
66 |
67 | export default LoginFormWithState
68 |
--------------------------------------------------------------------------------
/src/AppLib/Create.js:
--------------------------------------------------------------------------------
1 | import { upperFirst, startCase } from 'lodash';
2 | import React from 'react'
3 | import { gql, graphql } from 'react-apollo'
4 | import { toast } from 'react-toastify'
5 | import Form from './Form';
6 | import getCRUDSchemaFromResource from '../GqlCmsConfigLib/getCRUDSchemaFromResource'
7 | import alertFirstGqlMsg from '../alertFirstGqlMsg'
8 | import undefinedToNull from '../formLib/undefinedToNull'
9 |
10 | function Create (props) {
11 | const { resource, mutate, config, history } = props
12 |
13 | const resourceName = resource.displayName || resource.name
14 |
15 | const onSubmit = async ({ formData }) => {
16 | const input = undefinedToNull(formData)
17 | try {
18 | await mutate({ variables: { input } })
19 | history.push(`/${resourceName}`)
20 | toast.success('Create Success')
21 | } catch (e) {
22 | alertFirstGqlMsg(e)
23 | }
24 | }
25 | const onCancel = () => {
26 | history.push(`/${resourceName}`)
27 | }
28 | const createSchema = getCRUDSchemaFromResource({
29 | config,
30 | resource,
31 | crudType: 'create'
32 | })
33 | return
34 |
Create {startCase(resourceName)}
35 |
43 |
44 | }
45 |
46 | function CreateWithData (props) {
47 | const { resource } = props;
48 | const { crudMapping } = resource;
49 | let Component = Create
50 | const CreateQuery = gql`
51 | mutation ${crudMapping.create}($input: ${upperFirst(resource.name + 'Input')}!) {
52 | ${crudMapping.create}(${resource.name} : $input)
53 | }
54 | `;
55 | Component = graphql(CreateQuery)(Component)
56 |
57 | return
58 | }
59 |
60 | export default CreateWithData
61 |
--------------------------------------------------------------------------------
/src/AppLib/ReadManyLib/Tr.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import { gql } from 'react-apollo';
4 | import { toast } from 'react-toastify';
5 | import TdAction from './TdAction';
6 | import alertFirstGqlMsg from '../../alertFirstGqlMsg';
7 |
8 | function Tr(props) {
9 | const {
10 | row,
11 | resource,
12 | cellFormatter,
13 | mutate,
14 | changeUrl,
15 | columnNames,
16 | } = props;
17 |
18 | const handleDelete = async () => {
19 | // eslint-disable-next-line no-alert
20 | const confirm = window.confirm('Are you sure you want to delete?');
21 |
22 | if (!confirm) {
23 | return;
24 | }
25 |
26 | const { crudMapping, uniqKey } = resource;
27 | const uniqKeyQuery = { [uniqKey]: row[uniqKey] };
28 |
29 | const deleteMutation = gql`
30 | mutation ${crudMapping.delete}($${uniqKey}: String!) {
31 | ${crudMapping.delete}(${uniqKey}: $${uniqKey})
32 | }
33 | `;
34 |
35 | try {
36 | await mutate({
37 | mutation: deleteMutation,
38 | variables: uniqKeyQuery,
39 | });
40 |
41 | changeUrl();
42 | toast.success('Delete Success');
43 | } catch (error) {
44 | alertFirstGqlMsg(error);
45 | }
46 | }
47 |
48 | const tdFieldElements = columnNames.map(columnName => (
49 |
50 | {cellFormatter(row[columnName], row, columnName)}
51 | |
52 | ));
53 |
54 | return (
55 |
56 | {tdFieldElements}
57 |
63 |
64 | );
65 | }
66 |
67 | Tr.propTypes = {
68 | row: PropTypes.object.isRequired,
69 | resource: PropTypes.object.isRequired,
70 | cellFormatter: PropTypes.func.isRequired,
71 | mutate: PropTypes.func.isRequired,
72 | changeUrl: PropTypes.func.isRequired,
73 | columnNames: PropTypes.arrayOf(PropTypes.string).isRequired,
74 | };
75 |
76 | export default Tr;
77 |
--------------------------------------------------------------------------------
/src/AppLib/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Form from 'react-jsonschema-form';
3 | import HasOneWidget from './formLib/HasOneWidget';
4 | import WysiwygWidget from './formLib/WysiwygWidget';
5 | import HasManyField from './formLib/HasManyField';
6 | import FieldTemplate from './formLib/FieldTemplate';
7 | // NOTE: TitleField is used for ArrayField
8 | import TitleField from './formLib/TitleField';
9 | import CurrencyWidget from './formLib/CurrencyWidget';
10 | import NumberWidget from './formLib/NumberWidget';
11 |
12 | function FormButtons(props) {
13 | const { onCancel, readOnly } = props;
14 |
15 | if (readOnly) {
16 | return (
17 |
18 |
21 |
24 |
25 | );
26 | }
27 |
28 | return (
29 |
30 |
33 |
36 |
37 | );
38 | }
39 |
40 | const defaultWidgets = {
41 | currencyWidget: CurrencyWidget,
42 | hasOneWidget: HasOneWidget,
43 | numberWidget: NumberWidget,
44 | wysiwygWidget: WysiwygWidget,
45 | };
46 |
47 | const defaultFields = {
48 | hasManyField: HasManyField,
49 | TitleField,
50 | };
51 |
52 | function ExtendedForm(props) {
53 | const jsonSchemaFormExtensions = props.jsonSchemaFormExtensions || {};
54 | const {
55 | widgets = {},
56 | fields = {},
57 | } = jsonSchemaFormExtensions;
58 |
59 | return (
60 |
71 | );
72 | }
73 |
74 | export default ExtendedForm;
75 |
--------------------------------------------------------------------------------
/src/AppLib/ReadOne.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { gql } from 'react-apollo'
3 | import Form from './Form';
4 | import jsonSchemaToGqlQuery from '../GqlCmsConfigLib/jsonSchemaToGqlQuery'
5 | import getCRUDSchemaFromResource from '../GqlCmsConfigLib/getCRUDSchemaFromResource'
6 | import removeTypename from '../removeTypename'
7 | import graphqlWithoutCache from '../graphqlWithoutCache'
8 |
9 | function ReadOne(props) {
10 | const { config, resource, data, history } = props;
11 | const purifiedFormData = removeTypename(data[resource.crudMapping.readOne])
12 | if (data.loading) {
13 | return null;
14 | }
15 |
16 | const resourceName = resource.displayName || resource.name
17 |
18 | const onCancel = () => {
19 | history.push(`/${resourceName}`)
20 | }
21 | const readOneSchema = getCRUDSchemaFromResource({
22 | config,
23 | resource,
24 | crudType: 'readOne'
25 | })
26 | const uiSchema = {
27 | 'ui:disabled': true,
28 | ...readOneSchema.uiSchema
29 | }
30 | return
31 |
40 |
41 | }
42 |
43 | function ReadOneWithData (props) {
44 | const { config, resource } = props;
45 | const { crudMapping, uniqKey } = resource;
46 | const readOneSchema = getCRUDSchemaFromResource({
47 | config,
48 | resource,
49 | crudType: 'readOne'
50 | })
51 |
52 | let Component = ReadOne
53 | const fieldsQuery = jsonSchemaToGqlQuery(readOneSchema.jsonSchema)
54 | const ReadOneQuery = gql`
55 | query ${crudMapping.readOne}($${uniqKey}: String!) {
56 | ${crudMapping.readOne}(${uniqKey}: $${uniqKey}) ${fieldsQuery}
57 | }
58 | `
59 | const uniqKeyValue = props.match.params[uniqKey]
60 | Component = graphqlWithoutCache(
61 | ReadOneQuery,
62 | { options: { variables: { [uniqKey]: uniqKeyValue } } }
63 | )(Component)
64 |
65 | return
66 | }
67 |
68 | export default ReadOneWithData
69 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | CMS
30 |
31 |
32 |
35 |
36 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "byob-cms",
3 | "version": "1.26.0",
4 | "description": "Bring your own backend! A client side only cms that speaks graphQL.",
5 | "main": "index.js",
6 | "dependencies": {
7 | "babel-polyfill": "^6.26.0",
8 | "draft-js": "^0.10.4",
9 | "draft-js-import-html": "^1.2.1",
10 | "draftjs-to-html": "^0.8.2",
11 | "draftjs-utils": "^0.9.1",
12 | "jwt-decode": "^2.2.0",
13 | "lodash": "^4.17.4",
14 | "prop-types": "^15.6.0",
15 | "qs": "^6.5.1",
16 | "react": "^15.6.1",
17 | "react-apollo": "^1.4.15",
18 | "react-dom": "^15.6.1",
19 | "react-dom-factories": "^1.0.1",
20 | "react-draft-wysiwyg": "^1.12.3",
21 | "react-jsonschema-form": "^0.49.0",
22 | "react-number-format": "^3.1.3",
23 | "react-paginate": "^4.4.4",
24 | "react-redux": "^5.0.6",
25 | "react-router": "^4.2.0",
26 | "react-router-dom": "^4.2.2",
27 | "react-scripts": "1.0.11",
28 | "react-selectize": "^3.0.1",
29 | "react-toastify": "2.1.0",
30 | "react-transition-group": "^1.1.2"
31 | },
32 | "scripts": {
33 | "start": "react-scripts start",
34 | "build": "rm -rf dist/; babel src -d dist",
35 | "test": "react-scripts test --env=jsdom",
36 | "eject": "react-scripts eject",
37 | "lint": "eslint src --ext .js",
38 | "prepublishOnly": "npm run build"
39 | },
40 | "repository": {
41 | "type": "git",
42 | "url": "git+https://github.com/poetic/byob-cms.git"
43 | },
44 | "keywords": [
45 | "cms",
46 | "graphql",
47 | "front-end"
48 | ],
49 | "author": "Chun Yang",
50 | "license": "ISC",
51 | "bugs": {
52 | "url": "https://github.com/poetic/byob-cms/issues"
53 | },
54 | "homepage": "https://github.com/poetic/byob-cms#readme",
55 | "devDependencies": {
56 | "babel-cli": "^6.26.0",
57 | "babel-eslint": "^8.2.1",
58 | "babel-preset-es2015": "^6.24.1",
59 | "babel-preset-react": "^6.24.1",
60 | "babel-preset-stage-2": "^6.24.1",
61 | "eslint": "^4.12.0",
62 | "eslint-config-airbnb": "^16.1.0",
63 | "eslint-plugin-import": "^2.8.0",
64 | "eslint-plugin-jsx-a11y": "^6.0.2",
65 | "eslint-plugin-react": "^7.5.1"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/withOptionItems.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { gql } from 'react-apollo'
3 | import { get, flow, upperFirst, last, endsWith } from 'lodash'
4 | import { withApollo } from 'react-apollo'
5 |
6 | function withGql (Component) {
7 | class WithGqlComponent extends React.Component {
8 | constructor(props) {
9 | super(props)
10 | this.state = {
11 | loading: true,
12 | optionItems: [],
13 | }
14 | }
15 |
16 | componentDidMount() {
17 | const { gqlOptionsName, client } = this.props
18 |
19 | const query = gql`
20 | query ${upperFirst(gqlOptionsName)}Query {
21 | ${gqlOptionsName} {
22 | value
23 | label
24 | }
25 | }`
26 |
27 | client
28 | .query({ query, fetchPolicy: 'network-only' })
29 | .then(({ data }) => {
30 | this.setState({
31 | loading: false,
32 | optionItems: data[gqlOptionsName],
33 | })
34 | })
35 | .catch((e) => {
36 | throw e;
37 | })
38 | }
39 |
40 | render() {
41 | if (this.state.loading) {
42 | return null
43 | }
44 | return
45 | }
46 | }
47 |
48 | return WithGqlComponent
49 | }
50 |
51 | function idToGqlOptionsName (id) {
52 | let name = last(id.split('_'))
53 | if (endsWith(name, 'Id')) {
54 | name = name.replace(/Id$/, '')
55 | } else if (endsWith(name, 'Ids')) {
56 | name = name.replace(/Ids$/, '')
57 | }
58 | return name + 'Options'
59 | }
60 |
61 | function withGqlOptionNameForWidget (Component) {
62 | function WithGqlOptionNameComponent (props) {
63 | const gqlOptionsName = props.options.gqlOptionsName
64 | || idToGqlOptionsName(props.id)
65 | return
66 | }
67 | return WithGqlOptionNameComponent
68 | }
69 |
70 | function withGqlOptionNameForField (Component) {
71 | function WithGqlOptionNameComponent (props) {
72 | const gqlOptionsName = get(props, 'uiSchema.ui:options.gqlOptionsName')
73 | || idToGqlOptionsName(props.name)
74 | return
75 | }
76 | return WithGqlOptionNameComponent
77 | }
78 |
79 | const withGqlOptionNameMap = {
80 | widget: withGqlOptionNameForWidget,
81 | field: withGqlOptionNameForField,
82 | }
83 |
84 | function withOptionItems (Component, type) {
85 | return flow(withGql, withGqlOptionNameMap[type], withApollo)(Component)
86 | }
87 |
88 | export default withOptionItems
89 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/HasManyField.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { MultiSelect } from 'react-selectize'
3 | import { sortBy } from 'lodash'
4 | import Label from './Label'
5 | import renderValue from './renderValue'
6 | import withOptionItems from './withOptionItems'
7 |
8 | class MultiSelectWidget extends React.Component {
9 | render() {
10 | const {
11 | optionItems,
12 | name,
13 | schema,
14 | uiSchema,
15 | idSchema,
16 | required,
17 | onChange,
18 | formData,
19 | disabled,
20 | readonly,
21 | } = this.props
22 | const id = idSchema.$id
23 | const lbl = uiSchema['ui:title'] || schema.title || name
24 | const valueItems = (formData || []).map(item => optionItems.find(opt => opt.value === item))
25 | const sortable = uiSchema['ui:options'] && uiSchema['ui:options'].sortable
26 | // NOTE: the reason we do this is to fix a bug in react selectize
27 | // that package caches the renderValue function and it will use the
28 | // up to date closure data for the function, we use a instance method
29 | // to bypass this problem
30 | this.removeByValue = (value) => {
31 | const nextFormData = formData.filter(v => v !== value)
32 | onChange(nextFormData)
33 | }
34 |
35 | this.resortItems = (values) => {
36 | const valItem = values.map(v => formData.find(f => f === v))
37 | onChange(valItem)
38 | }
39 |
40 | this.reorder = (value, direction) => {
41 | let values = valueItems.map(v => v.value)
42 | const indx = values.indexOf(value)
43 | if (direction === '<') {
44 | values = [
45 | ...values.slice(0, indx - 1),
46 | value,
47 | values[indx - 1],
48 | ...values.slice(indx + 1),
49 | ]
50 | } else {
51 | values = [
52 | ...values.slice(0, indx),
53 | values[indx + 1],
54 | value,
55 | ...values.slice(indx + 2),
56 | ]
57 | }
58 | this.resortItems(values)
59 | }
60 |
61 | return (
62 |
63 |
64 | ({ ...v, index: i }))}
67 | options={sortBy(optionItems, ({ label }) => label.toUpperCase())}
68 | onValuesChange={(v) => {
69 | onChange(v.map(val => val.value))
70 | }}
71 | renderValue={item => renderValue(item, sortable, valueItems, this)}
72 | />
73 |
74 | )
75 | }
76 | }
77 |
78 | const HasManyField = withOptionItems(MultiSelectWidget, 'field')
79 |
80 | export default HasManyField
81 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/TagIdsField.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { gql } from 'react-apollo'
3 | import { groupBy, difference, union, find } from 'lodash'
4 | import { MultiSelect } from 'react-selectize';
5 | import Label from './Label'
6 | import graphqlWithoutCache from '../../graphqlWithoutCache'
7 | import renderValue from './renderValue'
8 |
9 | class TagIdsField extends React.Component {
10 | // NOTE: this is used for MultiSelect
11 | removeByValue(value) {
12 | const { formData, onChange } = this.props
13 | const nextFormData = formData.filter(v => v !== value)
14 | onChange(nextFormData)
15 | }
16 |
17 | render() {
18 | const props = this.props;
19 | const {
20 | name,
21 | uiSchema,
22 | idSchema,
23 | required,
24 | onChange,
25 | formData,
26 | disabled,
27 | readonly,
28 | data: { loading, tags, tagQuestions },
29 | } = props
30 |
31 | if (loading) {
32 | return null
33 | }
34 |
35 | const id = idSchema.$id;
36 | const label = uiSchema["ui:title"] || name;
37 |
38 | const tagsGroundByQuestions = groupBy(tags, 'tagQuestionId')
39 | const tagsGroundByQuestionsElements = tagQuestions.map((tagQuestion) => {
40 | const optionItems = tagsGroundByQuestions[tagQuestion._id].map((t) => ({
41 | value: t._id,
42 | label: t.title,
43 | }))
44 | const valuesForQuestion = formData.filter((tagId) => {
45 | return find(tags, { _id: tagId }).tagQuestionId === tagQuestion._id
46 | })
47 | const valueObjects = optionItems.filter((optionItem) => {
48 | return valuesForQuestion.includes(optionItem.value)
49 | })
50 | return
51 |
52 | {
57 | const values = v.map(v => v.value)
58 | const nextFormData = union(difference(formData, valuesForQuestion), values)
59 | onChange(nextFormData)
60 | }}
61 | renderValue={item => renderValue(item, this)}
62 | />
63 |
64 | })
65 |
66 | return
67 |
68 | {tagsGroundByQuestionsElements}
69 |
70 | }
71 | }
72 |
73 | const Query = gql`
74 | query TagsAndTagQuestions {
75 | tags {
76 | _id
77 | title
78 | tagQuestionId
79 | }
80 | tagQuestions {
81 | _id
82 | title
83 | }
84 | }
85 | `
86 |
87 | const TagIdsFieldWithData = graphqlWithoutCache(Query)(TagIdsField)
88 | export default TagIdsFieldWithData
89 |
--------------------------------------------------------------------------------
/src/StateHOF.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
3 | import { ApolloClient, ApolloProvider, createNetworkInterface } from 'react-apollo';
4 | import { get, once } from 'lodash'
5 |
6 | const existingAccessToken = process.browser
7 | ? window.localStorage.getItem('accessToken')
8 | : null
9 |
10 | function accessToken (state=existingAccessToken, action) {
11 | switch (action.type) {
12 | case 'SET_ACCESS_TOKEN':
13 | return action.accessToken
14 | default:
15 | return state
16 | }
17 | }
18 |
19 | function setAccessToken (accessToken) {
20 | if (accessToken) {
21 | window.localStorage.setItem('accessToken', accessToken)
22 | } else {
23 | window.localStorage.removeItem('accessToken')
24 | }
25 | return {
26 | type: 'SET_ACCESS_TOKEN',
27 | accessToken,
28 | }
29 | }
30 |
31 | function createProvider ({ config: { graphqlUrl } }) {
32 | const networkInterface = createNetworkInterface({
33 | uri: graphqlUrl,
34 | })
35 |
36 | networkInterface.use([{
37 | applyMiddleware(req, next) {
38 | if (!req.options.headers) {
39 | req.options.headers = {}; // Create the header object if needed.
40 | }
41 | req.options.headers['authorization'] = localStorage.getItem('accessToken') || null;
42 | next();
43 | },
44 | }])
45 |
46 | networkInterface.useAfter([{
47 | async applyAfterware({ response }, next) {
48 | const text = await response.clone().text()
49 | const firstErrorMessage = get(JSON.parse(text), 'errors.0.message')
50 | if (firstErrorMessage === 'not-authorized') {
51 | store.dispatch(setAccessToken(null))
52 | }
53 | next();
54 | },
55 | }])
56 |
57 | const client = new ApolloClient({
58 | networkInterface,
59 | });
60 |
61 | const initialState = {};
62 |
63 | // If you are using the devToolsExtension, you can add it here also
64 | const reduxDevtoolMiddleware = (typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== 'undefined')
65 | ? window.__REDUX_DEVTOOLS_EXTENSION__()
66 | : f => f;
67 |
68 | const store = createStore(
69 | combineReducers({
70 | apollo: client.reducer(),
71 | accessToken,
72 | }),
73 | initialState,
74 | compose(
75 | applyMiddleware(client.middleware()),
76 | reduxDevtoolMiddleware,
77 | )
78 | );
79 |
80 | return (props) => {
81 | return
82 | {props.children}
83 |
84 | }
85 | }
86 |
87 | const createProviderOnce = once(createProvider)
88 |
89 | export default function StateHOF (Component) {
90 | return (props) => {
91 | const Provider = createProviderOnce(props)
92 | return
93 |
94 |
95 | }
96 | }
97 |
98 | export { accessToken, setAccessToken }
99 |
--------------------------------------------------------------------------------
/src/AppLib/Update.js:
--------------------------------------------------------------------------------
1 | import { startCase, upperFirst } from 'lodash';
2 | import React from 'react'
3 | import { gql, graphql } from 'react-apollo'
4 | import { toast } from 'react-toastify'
5 | import graphqlWithoutCache from '../graphqlWithoutCache'
6 | import Form from './Form';
7 | import jsonSchemaToGqlQuery from '../GqlCmsConfigLib/jsonSchemaToGqlQuery'
8 | import getCRUDSchemaFromResource from '../GqlCmsConfigLib/getCRUDSchemaFromResource'
9 | import removeTypename from '../removeTypename'
10 | import nullToUndefined from '../formLib/nullToUndefined'
11 | import undefinedToNull from '../formLib/undefinedToNull'
12 | import alertFirstGqlMsg from '../alertFirstGqlMsg'
13 |
14 | function Update (props) {
15 | const { config, resource, mutate, data, history } = props;
16 | const dirtyFormData = data[resource.crudMapping.readOne]
17 | const purifiedFormData = nullToUndefined(
18 | removeTypename(
19 | dirtyFormData
20 | )
21 | )
22 | if (data.loading) {
23 | return null;
24 | }
25 |
26 | const resourceName = resource.displayName || resource.name
27 |
28 | const onSubmit = async ({ formData }) => {
29 | const input = undefinedToNull(formData)
30 | try {
31 | await mutate({ variables: { input } })
32 | history.push(`/${resourceName}`)
33 | toast.success('Update Success')
34 | } catch (e) {
35 | alertFirstGqlMsg(e)
36 | }
37 | }
38 | const onCancel = () => {
39 | history.push(`/${resourceName}`)
40 | }
41 | const updateSchema = getCRUDSchemaFromResource({
42 | config,
43 | resource,
44 | crudType: 'update'
45 | })
46 | return
47 |
Update {startCase(resourceName)}
48 |
57 |
58 | }
59 |
60 | function UpdateWithData (props) {
61 | const { config, resource } = props;
62 | const { crudMapping, uniqKey } = resource;
63 | const updateSchema = getCRUDSchemaFromResource({
64 | config,
65 | resource,
66 | crudType: 'update'
67 | })
68 |
69 | let Component = Update
70 | const fieldsQuery = jsonSchemaToGqlQuery(updateSchema.jsonSchema)
71 | const ReadOneQuery = gql`
72 | query ${crudMapping.readOne}($${uniqKey}: String!) {
73 | ${crudMapping.readOne}(${uniqKey}: $${uniqKey}) ${fieldsQuery}
74 | }
75 | `
76 | const uniqKeyValue = props.match.params[uniqKey]
77 | Component = graphqlWithoutCache(
78 | ReadOneQuery,
79 | { options: { variables: { [uniqKey]: uniqKeyValue } } }
80 | )(Component)
81 |
82 | const UpdateQuery = gql`
83 | mutation ${crudMapping.update}($input: ${upperFirst(updateSchema.inputName || resource.name + 'Input')}!) {
84 | ${crudMapping.update}(${resource.name} : $input, ${uniqKey}: "${uniqKeyValue}")
85 | }
86 | `;
87 | Component = graphql(UpdateQuery)(Component)
88 |
89 | return
90 | }
91 |
92 | export default UpdateWithData
93 |
--------------------------------------------------------------------------------
/src/AppLib/LoginLib/EmailPasswordLogin.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { gql, graphql } from 'react-apollo'
3 | import alertFirstGqlMsg from '../../alertFirstGqlMsg'
4 | import { setAccessToken } from '../../StateHOF'
5 | import { connect } from 'react-redux'
6 |
7 | const LoginMutation = gql`
8 | mutation LoginMutation($loginInput: LoginInput!) {
9 | login(loginInput: $loginInput)
10 | }
11 | `
12 |
13 | class Login extends React.Component {
14 | constructor(props) {
15 | super(props)
16 | this.state = {
17 | email: '',
18 | password: '',
19 | loggingIn: false,
20 | }
21 | }
22 |
23 | componentWillUnmount() {
24 | this.setState({ loggingIn: false })
25 | }
26 |
27 | render() {
28 | const onSubmit = async (e) => {
29 | e.preventDefault()
30 |
31 | this.setState({ loggingIn: true })
32 |
33 | try {
34 | const response = await this.props.mutate({
35 | variables: {
36 | loginInput: {
37 | email: this.state.email,
38 | password: this.state.password,
39 | }
40 | }
41 | })
42 | const token = response.data.login
43 | this.props.setAccessToken(token)
44 | } catch (e) {
45 | this.setState({ loggingIn: false }, () => {
46 | alertFirstGqlMsg(e)
47 | })
48 | }
49 | }
50 |
51 | const containerStyle = {
52 | display: 'flex',
53 | flexDirection: 'column',
54 | justifyContent: 'center',
55 | alignItems: 'center',
56 | }
57 | const formStyle = {
58 | width: '300px',
59 | maxWidth: '100%',
60 | }
61 | const h1Style = {
62 | marginBottom: '20px',
63 | }
64 | const buttonStyle = {
65 | width: '100%',
66 | }
67 |
68 | const { email, password, loggingIn } = this.state
69 |
70 | return
71 |
Login
72 |
98 |
99 | }
100 | }
101 |
102 | const LoginFormWithData = graphql(LoginMutation)(Login)
103 |
104 | const LoginFormWithState = connect(
105 | (state) => ({ accessToken: state.accessToken }),
106 | { setAccessToken }
107 | )(LoginFormWithData)
108 |
109 | export default LoginFormWithState
110 |
--------------------------------------------------------------------------------
/src/AppLib/ReadManyLib/ThField.js:
--------------------------------------------------------------------------------
1 | import { get, startCase } from 'lodash'
2 | import React from 'react'
3 |
4 | function UpAndDown (props) {
5 | return
18 | }
19 |
20 | function Up (props) {
21 | return
30 | }
31 | function Down (props) {
32 | return
40 | }
41 |
42 | function SortButton ({ sortOrder, changeSort }) {
43 | switch (sortOrder) {
44 | case 'ASC':
45 | return changeSort('DESC')}/>
46 | case 'DESC':
47 | return changeSort('ASC')}/>
48 | default:
49 | return changeSort('ASC')}/>
50 | }
51 | }
52 |
53 | function ThFieldSort ({ columnName, readManySchema, sort, onSortChange }) {
54 | if (!readManySchema.sortStrategy) {
55 | return null
56 | }
57 |
58 | const { type } = readManySchema.sortStrategy
59 |
60 | function changeSort (order) {
61 | if (type === 'SINGLE') {
62 | onSortChange([{
63 | field: columnName,
64 | order,
65 | }])
66 | } else {
67 | throw new Error(`${type} is not supported.`)
68 | }
69 | }
70 |
71 | const sortOrder = get(sort.find(({ field }) => field === columnName), 'order')
72 | const style = {
73 | float: 'right',
74 | cursor: 'pointer',
75 | }
76 | return
77 |
78 |
79 | }
80 |
81 | function ThField ({ columnName, readManySchema, sort, onSortChange }) {
82 | return
83 | {startCase(columnName)}
84 |
90 | |
91 | }
92 |
93 | export default ThField
94 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill/dist/polyfill';
2 | import React, { Component } from 'react';
3 | import { connect } from 'react-redux';
4 | import StateHOF, { setAccessToken } from './StateHOF';
5 | import { BrowserRouter, Route } from 'react-router-dom';
6 | import CreateOrReadOne from './AppLib/CreateOrReadOne';
7 | import ReadMany from './AppLib/ReadMany';
8 | import Update from './AppLib/Update';
9 | import IndexRoute from './AppLib/IndexRoute';
10 | import NavBar from './AppLib/NavBar';
11 | import CodeLogin from './AppLib/LoginLib/CodeLogin';
12 | import EmailPasswordLogin from './AppLib/LoginLib/EmailPasswordLogin';
13 | import withOptionItems from './AppLib/formLib/withOptionItems';
14 | import Label from './AppLib/formLib/Label'
15 | import { ToastContainer, toast } from 'react-toastify';
16 |
17 | class App extends Component {
18 | componentDidMount() {
19 | document.title = this.props.config.title || 'CMS'
20 | }
21 | render() {
22 | const { config } = this.props
23 | const resourceRouteElements = config
24 | .resources
25 | .reduce((acc, resource) => {
26 | const { uniqKey } = resource
27 | const {
28 | create,
29 | readMany,
30 | readOne,
31 | update,
32 | } = resource.crudMapping
33 |
34 | const resourceName = resource.displayName || resource.name
35 |
36 | if (readMany) {
37 | const path = `/${resourceName}`
38 | acc.push( }
43 | />);
44 | }
45 | if (create || readOne) {
46 | const path = `/${resourceName}/:${uniqKey}`
47 | acc.push( }
52 | />);
53 | }
54 | if (update) {
55 | const path = `/${resourceName}/:${uniqKey}/edit`
56 | acc.push( }
61 | />);
62 | }
63 | return acc;
64 | }, [])
65 |
66 | return (
67 |
68 |
69 |
70 |
71 |
72 | } />
73 | {resourceRouteElements}
74 |
75 |
76 |
77 |
78 | );
79 | }
80 | }
81 |
82 | function DefaultLogin () {
83 | return
84 |
You need to provide a React Component to the "Login" option
85 |
86 | }
87 |
88 | function AppWithGuard (props) {
89 | const { config } = props
90 | const Login = config.Login || DefaultLogin
91 |
92 | const ComponentWithGuard = (props) => {
93 | if (props.accessToken) {
94 | return
95 | } else {
96 | return Login ? : null
97 | }
98 | }
99 |
100 | const ComponentWithState = connect(
101 | (state) => ({ accessToken: state.accessToken }),
102 | { setAccessToken }
103 | )(ComponentWithGuard)
104 |
105 | return
106 | }
107 |
108 | function AppWithToast (props) {
109 | return
121 | }
122 |
123 | export default StateHOF(AppWithToast);
124 | export {
125 | CodeLogin,
126 | EmailPasswordLogin,
127 | withOptionItems,
128 | Label,
129 | setAccessToken,
130 | StateHOF,
131 | ToastContainer,
132 | toast,
133 | }
134 |
--------------------------------------------------------------------------------
/src/AppLib/formLib/WysiwygWidget.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { isEmpty } from 'lodash';
3 | import { Editor } from 'react-draft-wysiwyg';
4 | import {
5 | EditorState,
6 | convertToRaw,
7 | ContentState,
8 | ContentBlock,
9 | Modifier,
10 | CharacterMetadata,
11 | genKey,
12 | } from 'draft-js';
13 | import { List, Repeat } from 'immutable';
14 | import draftToHtml from 'draftjs-to-html';
15 | import { stateFromHTML } from 'draft-js-import-html';
16 | import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
17 | import { getSelectedBlocksType } from 'draftjs-utils';
18 |
19 | class WysiwygWidget extends Component {
20 | constructor(props) {
21 | super(props);
22 |
23 | const { value } = this.props
24 | this.state = this.handleState( value, null, false )
25 | }
26 |
27 | // this method could switch between sync and async based on the updateState argument
28 | handleState (newHtml, newEditorState, updateState = true) {
29 | // we will not have a state at the instance construction, so lets make sure
30 | // we emulate an empty object for both state and props
31 | const { html, editorState } = this.state || {}
32 | const { blockType } = this.props || {}
33 | let state = {}
34 |
35 | // we dont have the html nor the editorState
36 | if ( isEmpty(newHtml) && ! newEditorState ) {
37 | state = {
38 | html: '',
39 | editorState: isEmpty( blockType ) ? (
40 | EditorState.createEmpty()
41 | ):(
42 | EditorState.createWithContent(
43 | ContentState.createFromBlockArray([
44 | new ContentBlock({
45 | type: blockType
46 | })
47 | ])
48 | )
49 | )
50 | }
51 |
52 | return updateState ? this.setState(state) : state
53 | }
54 |
55 | // we have a new html which is different from the state's html (editor state doesnt matter here)
56 | if ( ! isEmpty( newHtml ) && newHtml != html ) {
57 | state = {
58 | html: newHtml,
59 | editorState: EditorState.createWithContent(
60 | stateFromHTML(newHtml)
61 | )
62 | }
63 |
64 | return updateState ? this.setState(state) : state
65 | }
66 |
67 | // we have an updated editorState, lets create the html version from it
68 | if ( newEditorState ) {
69 | state = {
70 | html: draftToHtml(convertToRaw(newEditorState.getCurrentContent())),
71 | editorState: newEditorState,
72 | }
73 |
74 | return updateState ? this.setState(state) : state
75 | }
76 |
77 | // do nothing
78 | return {}
79 | }
80 |
81 | // lets take advantage of the lifecycle events to manage the state directly (advanced)
82 | componentWillReceiveProps = ({ value }) => {
83 | this.state = { ...this.state, ...this.handleState( value, null, false) }
84 | }
85 |
86 | handlePastedText = (text, html) => {
87 | const linesFromText = text.split('\n');
88 |
89 | const { editorState } = this.state;
90 |
91 | const currentBlockType = getSelectedBlocksType(editorState);
92 |
93 | const contentBlocksArray = linesFromText.map(line => {
94 | return new ContentBlock({
95 | key: genKey(),
96 | type: currentBlockType,
97 | characterList: new List(Repeat(CharacterMetadata.create(), line.length)),
98 | text: line,
99 | });
100 | });
101 |
102 | const blockMap = ContentState.createFromBlockArray(contentBlocksArray).blockMap;
103 |
104 | const newState = Modifier.replaceWithFragment(editorState.getCurrentContent(), editorState.getSelection(), blockMap);
105 |
106 | return this.handleState(
107 | null,
108 | EditorState.push(editorState, newState, 'insert-fragment')
109 | );
110 | }
111 |
112 | onEditorStateChange = async (editorState) => {
113 | this.handleState(null, editorState)
114 | const { html } = this.state
115 |
116 | const { onChange } = this.props
117 | if ( typeof onChange == 'function' ) {
118 | await onChange(html)
119 | }
120 | };
121 |
122 | render() {
123 | const { editorState } = this.state;
124 | const { options: { wysiwygConfig }} = this.props;
125 |
126 | return (
127 |
133 | )
134 | }
135 | }
136 |
137 | export default WysiwygWidget;
138 |
--------------------------------------------------------------------------------
/src/AppLib/ReadMany.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import qs from 'qs'
3 | import { gql, withApollo } from 'react-apollo';
4 | import { Link } from 'react-router-dom';
5 | import { get, startCase } from 'lodash'
6 | import pluralize from 'pluralize'
7 | import jsonSchemaToGqlQuery from '../GqlCmsConfigLib/jsonSchemaToGqlQuery';
8 | import getCRUDSchemaFromResource from '../GqlCmsConfigLib/getCRUDSchemaFromResource'
9 | import ensureUniqKey from '../GqlCmsConfigLib/ensureUniqKey'
10 | import getReadManyInputQueryString from './ReadManyLib/getReadManyInputQueryString'
11 | import defaultCellFormatter from './ReadManyLib/defaultCellFormatter'
12 | import Tr from './ReadManyLib/Tr'
13 | import Paginate from './ReadManyLib/Paginate'
14 | import Search from './ReadManyLib/Search'
15 | import ThField from './ReadManyLib/ThField'
16 | import alertFirstGqlMsg from '../alertFirstGqlMsg'
17 |
18 | const DEFAULT_ITEMS_PER_PAGE = 15
19 |
20 | class ReadMany extends React.Component {
21 | constructor(props) {
22 | super(props)
23 | // NOTE: loading is not used in render, we may need it later on
24 | this.state = {
25 | loading: true,
26 | items: [],
27 | total: 0,
28 | }
29 | }
30 | componentDidMount() {
31 | this.fetchReadMany(this.props)
32 | }
33 | componentWillReceiveProps(nextProps) {
34 | this.fetchReadMany(nextProps)
35 | }
36 | changeUrl(paramsOverride) {
37 | const { history, location, queryParams } = this.props
38 | const { skip, limit, search, sort } = { ...queryParams, skip: 0, ...paramsOverride }
39 | const queryParmasString = qs.stringify({
40 | skip,
41 | limit,
42 | search,
43 | sort,
44 | })
45 | history.push([location.pathname, queryParmasString].join('?'))
46 | }
47 | fetchReadMany(props) {
48 | const { readManySchema, resource, client } = props
49 | const { uniqKey, crudMapping } = resource;
50 | const fieldsQuery = jsonSchemaToGqlQuery(
51 | ensureUniqKey(readManySchema.jsonSchema, uniqKey)
52 | )
53 |
54 | const { limit, sort, search, skip } = props.queryParams
55 |
56 | const ReadManyInputQueryString = getReadManyInputQueryString(
57 | readManySchema,
58 | { skip, limit, sort, search }
59 | )
60 | const ReadManyQuery = gql`
61 | query ${crudMapping.readMany} {
62 | ${crudMapping.readMany} ${ReadManyInputQueryString} {
63 | items ${fieldsQuery}
64 | total
65 | }
66 | }
67 | `;
68 | this.setState({ loading: true })
69 | client
70 | .query({ query: ReadManyQuery, fetchPolicy: 'network-only' })
71 | .then(({ data }) => {
72 | const { items, total } = data[resource.crudMapping.readMany]
73 | this.setState({
74 | loading: false,
75 | items,
76 | total,
77 | })
78 | })
79 | .catch((e) => {
80 | alertFirstGqlMsg(e);
81 |
82 | throw e;
83 | })
84 | }
85 | render() {
86 | const {
87 | items,
88 | total,
89 | } = this.state
90 |
91 | const {
92 | queryParams: {
93 | skip,
94 | limit,
95 | sort,
96 | search,
97 | },
98 | client: { mutate },
99 | resource,
100 | readManySchema,
101 | } = this.props
102 |
103 | const { properties } = readManySchema.jsonSchema
104 | const columnNames = Object.keys(properties)
105 |
106 | const thActionsElement =
107 | Actions
108 | |
109 | const thFieldElements = columnNames.map((columnName) => {
110 | const { title } = properties[columnName]
111 |
112 | return {
118 | this.changeUrl({ sort: nextSort })
119 | }}
120 | />
121 | })
122 | const thElements = thFieldElements.concat(thActionsElement)
123 |
124 | const cellFormatter = readManySchema.cellFormatter || defaultCellFormatter
125 | const trElements = items.map((row, index) => this.changeUrl(paramsOverride)}
132 | columnNames={columnNames}
133 | />)
134 |
135 | const resourceName = resource.displayName || resource.name
136 |
137 | return
138 |
139 | List of { startCase(pluralize(resourceName)) }
140 | { readManySchema.showTotal ? ` (total: ${total})` : null }
141 | {
142 | resource.crudMapping.create
143 | ?
148 | Add New
149 |
150 | : null
151 | }
152 |
153 | {
154 | readManySchema.searchStrategy
155 | ?
this.changeUrl({search: nextSearch})}
158 | />
159 | : null
160 | }
161 |
162 |
163 |
164 |
165 | {thElements}
166 |
167 |
168 |
169 | {trElements}
170 |
171 |
172 | {
173 | readManySchema.paginationStrategy
174 | ?
this.changeUrl({ skip: nextSkip })}
179 | />
180 | : null
181 | }
182 |
183 |
184 | }
185 | }
186 |
187 | const ReadManyWithApollo = withApollo(ReadMany);
188 |
189 | function ReadManyWithData(props) {
190 | const {
191 | config,
192 | resource,
193 | location,
194 | } = props;
195 |
196 | const readManySchema = getCRUDSchemaFromResource({
197 | config,
198 | resource,
199 | crudType: 'readMany',
200 | });
201 |
202 | const { search } = location;
203 | const limit = get(
204 | readManySchema,
205 | 'paginationStrategy.itemsPerPage',
206 | DEFAULT_ITEMS_PER_PAGE,
207 | );
208 | const defaultQueryParams = { limit, skip: 0, sort: [], search: '' };
209 | const overrideQueryParams = search ? qs.parse(search.substring(1)) : {};
210 | const queryParams = { ...defaultQueryParams, ...overrideQueryParams };
211 |
212 | return (
213 |
218 | );
219 | }
220 |
221 | export default ReadManyWithData
222 |
--------------------------------------------------------------------------------
/src/stylesheets/react-toastify.css:
--------------------------------------------------------------------------------
1 | @keyframes toastify-bounceInRight {
2 | from, 60%, 75%, 90%, to {
3 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
4 | from {
5 | opacity: 0;
6 | transform: translate3d(3000px, 0, 0); }
7 | 60% {
8 | opacity: 1;
9 | transform: translate3d(-25px, 0, 0); }
10 | 75% {
11 | transform: translate3d(10px, 0, 0); }
12 | 90% {
13 | transform: translate3d(-5px, 0, 0); }
14 | to {
15 | transform: none; } }
16 | .toastify-bounceInRight, .toast-enter--top-right, .toast-enter--bottom-right {
17 | animation-name: toastify-bounceInRight; }
18 |
19 | @keyframes toastify-bounceOutRight {
20 | 20% {
21 | opacity: 1;
22 | transform: translate3d(-20px, 0, 0); }
23 | to {
24 | opacity: 0;
25 | transform: translate3d(2000px, 0, 0); } }
26 | .toastify-bounceOutRight, .toast-exit--top-right, .toast-exit--bottom-right {
27 | animation-name: toastify-bounceOutRight; }
28 |
29 | @keyframes toastify-bounceInLeft {
30 | from, 60%, 75%, 90%, to {
31 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
32 | 0% {
33 | opacity: 0;
34 | transform: translate3d(-3000px, 0, 0); }
35 | 60% {
36 | opacity: 1;
37 | transform: translate3d(25px, 0, 0); }
38 | 75% {
39 | transform: translate3d(-10px, 0, 0); }
40 | 90% {
41 | transform: translate3d(5px, 0, 0); }
42 | to {
43 | transform: none; } }
44 | .toastify-bounceInLeft, .toast-enter--top-left, .toast-enter--bottom-left {
45 | animation-name: toastify-bounceInLeft; }
46 |
47 | @keyframes toastify-bounceOutLeft {
48 | 20% {
49 | opacity: 1;
50 | transform: translate3d(20px, 0, 0); }
51 | to {
52 | opacity: 0;
53 | transform: translate3d(-2000px, 0, 0); } }
54 | .toastify-bounceOutLeft, .toast-exit--top-left, .toast-exit--bottom-left {
55 | animation-name: toastify-bounceOutLeft; }
56 |
57 | @keyframes toastify-bounceInUp {
58 | from, 60%, 75%, 90%, to {
59 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
60 | from {
61 | opacity: 0;
62 | transform: translate3d(0, 3000px, 0); }
63 | 60% {
64 | opacity: 1;
65 | transform: translate3d(0, -20px, 0); }
66 | 75% {
67 | transform: translate3d(0, 10px, 0); }
68 | 90% {
69 | transform: translate3d(0, -5px, 0); }
70 | to {
71 | transform: translate3d(0, 0, 0); } }
72 | .toastify-bounceInUp, .toast-enter--bottom-center {
73 | animation-name: toastify-bounceInUp; }
74 |
75 | @keyframes toastify-bounceOutUp {
76 | 20% {
77 | transform: translate3d(0, -10px, 0); }
78 | 40%, 45% {
79 | opacity: 1;
80 | transform: translate3d(0, 20px, 0); }
81 | to {
82 | opacity: 0;
83 | transform: translate3d(0, -2000px, 0); } }
84 | .toastify-bounceOutUp, .toast-exit--top-center {
85 | animation-name: toastify-bounceOutUp; }
86 |
87 | @keyframes toastify-bounceInDown {
88 | from, 60%, 75%, 90%, to {
89 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
90 | 0% {
91 | opacity: 0;
92 | transform: translate3d(0, -3000px, 0); }
93 | 60% {
94 | opacity: 1;
95 | transform: translate3d(0, 25px, 0); }
96 | 75% {
97 | transform: translate3d(0, -10px, 0); }
98 | 90% {
99 | transform: translate3d(0, 5px, 0); }
100 | to {
101 | transform: none; } }
102 | .toastify-bounceInDown, .toast-enter--top-center {
103 | animation-name: toastify-bounceInDown; }
104 |
105 | @keyframes toastify-bounceOutDown {
106 | 20% {
107 | transform: translate3d(0, 10px, 0); }
108 | 40%, 45% {
109 | opacity: 1;
110 | transform: translate3d(0, -20px, 0); }
111 | to {
112 | opacity: 0;
113 | transform: translate3d(0, 2000px, 0); } }
114 | .toastify-bounceOutDown, .toast-exit--bottom-center {
115 | animation-name: toastify-bounceOutDown; }
116 |
117 | .toastify-animated {
118 | animation-duration: 0.75s;
119 | animation-fill-mode: both; }
120 |
121 | .toastify {
122 | z-index: 999;
123 | position: fixed;
124 | padding: 4px;
125 | width: 320px;
126 | box-sizing: border-box;
127 | color: #fff; }
128 | .toastify--top-left {
129 | top: 1em;
130 | left: 1em; }
131 | .toastify--top-center {
132 | top: 1em;
133 | left: 50%;
134 | margin-left: -160px; }
135 | .toastify--top-right {
136 | top: 1em;
137 | right: 1em; }
138 | .toastify--bottom-left {
139 | bottom: 1em;
140 | left: 1em; }
141 | .toastify--bottom-center {
142 | bottom: 1em;
143 | left: 50%;
144 | margin-left: -160px; }
145 | .toastify--bottom-right {
146 | bottom: 1em;
147 | right: 1em; }
148 |
149 | @media only screen and (max-width: 480px) {
150 | .toastify {
151 | width: 100vw;
152 | padding: 0; }
153 |
154 | .toastify--top-left, .toastify--top-center, .toastify--top-right {
155 | left: 0;
156 | top: 0;
157 | margin: 0; }
158 |
159 | .toastify--bottom-left, .toastify--bottom-center, .toastify--bottom-right {
160 | left: 0;
161 | bottom: 0;
162 | margin: 0; } }
163 | .toastify__close {
164 | padding: 0;
165 | color: #fff;
166 | font-weight: bold;
167 | font-size: 14px;
168 | background: transparent;
169 | outline: none;
170 | border: none;
171 | cursor: pointer;
172 | opacity: .7;
173 | transition: .3s ease;
174 | align-self: flex-start; }
175 | .toastify__close:hover, .toastify__close:focus {
176 | opacity: 1; }
177 | .toastify-content--default .toastify__close {
178 | color: #000;
179 | opacity: 0.3; }
180 | .toastify-content--default .toastify__close:hover {
181 | opacity: 1; }
182 |
183 | .toastify-content {
184 | position: relative;
185 | min-height: 48px;
186 | margin-bottom: 1rem;
187 | padding: 8px;
188 | border-radius: 1px;
189 | box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.1), 0 2px 15px 0 rgba(0, 0, 0, 0.05);
190 | display: flex;
191 | justify-content: space-between;
192 | max-height: 800px;
193 | overflow: hidden;
194 | font-family: sans-serif;
195 | cursor: pointer; }
196 | .toastify-content--default {
197 | background: #fff;
198 | color: #aaa; }
199 | .toastify-content--info {
200 | background: #3498db; }
201 | .toastify-content--success {
202 | background: #07bc0c; }
203 | .toastify-content--warning {
204 | background: #f1c40f; }
205 | .toastify-content--error {
206 | background: #e74c3c; }
207 |
208 | .toastify__body {
209 | margin: auto 0;
210 | flex: 1; }
211 |
212 | @media only screen and (max-width: 480px) {
213 | .toastify-content {
214 | margin-bottom: 0; } }
215 | @keyframes track-progress {
216 | 0% {
217 | width: 100%; }
218 | 100% {
219 | width: 0; } }
220 | .toastify__progress {
221 | position: absolute;
222 | bottom: 0;
223 | left: 0;
224 | width: 0;
225 | height: 5px;
226 | z-index: 999;
227 | opacity: 0.7;
228 | animation: track-progress linear 1;
229 | background-color: rgba(255, 255, 255, 0.7); }
230 | .toastify__progress--default {
231 | background: linear-gradient(to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55); }
232 |
233 | /*# sourceMappingURL=ReactToastify.css.map */
234 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BYOB CMS
2 | byob-cms is a react component for client only CMS
3 | There are three basic concepts in byob-cms:
4 | - config
5 | "config" is an javascript object that contains all the info needed
6 | for byob-cms to build the cms.
7 | You should pass config into byob-cms component as a prop.
8 |
9 | - resource
10 | "resource" is a RESTful concept.
11 | You can do CRUD on the records in resources.
12 | You can think of a resource as a class and a record as an instance,
13 | or a resource as a model and a record as a document.
14 |
15 | - schema
16 | We use https://github.com/mozilla-services/react-jsonschema-form package
17 | to render forms in the cms. There for you can provide
18 | jsonSchema and uiSchema to define the structure of the form.
19 | We let you configure schemas for CRUD pages. "defaultSchema" is used
20 | when you do not provide a schema for that CRUD type.
21 |
22 | ## Usage
23 | You can import this package as a component and give it a config prop.
24 | ```
25 | import React from 'react';
26 | import ReactDOM from 'react-dom';
27 | import CMS from 'byob-cms'
28 |
29 | ReactDOM.render(
30 | ,
31 | document.getElementById('root')
32 | );
33 | ```
34 |
35 | ## Config example
36 | ```
37 | const config = {
38 | graphqlUrl: 'http://localhost:4000',
39 | resources: [
40 | {
41 | name: 'drop',
42 | uniqKey: '_id',
43 | crudMapping: {
44 | create: 'createDrop',
45 | readMany: 'drops',
46 | readOne: 'drop',
47 | update: 'updateDrop',
48 | // delete: 'deleteDrop',
49 | },
50 | readManySchema: {
51 | jsonSchema: {
52 | type: 'object',
53 | properties: {
54 | _id: 'string',
55 | title: 'string',
56 | }
57 | }
58 | },
59 | defaultSchema: {
60 | jsonSchema: {
61 | title: 'Drop',
62 | type: 'object',
63 | required: [
64 | 'title',
65 | 'content',
66 | 'tagIds',
67 | ],
68 | properties: {
69 | title: {
70 | type: 'string',
71 | },
72 | content: {
73 | type: 'string',
74 | },
75 | lifeEventIds: {
76 | type: 'array',
77 | default: [],
78 | items: {
79 | type: 'string'
80 | }
81 | },
82 | tagIds: {
83 | type: 'array',
84 | default: [],
85 | items: {
86 | type: 'string'
87 | }
88 | }
89 | },
90 | },
91 | uiSchema: {
92 | lifeEventIds: {
93 | 'ui:field': 'hasManyField'
94 | },
95 | tagIds: {
96 | 'ui:field': 'hasManyField'
97 | },
98 | },
99 | }
100 | },
101 | {
102 | name: 'tag',
103 | uniqKey: '_id',
104 | crudMapping: {
105 | readMany: 'tags',
106 | readOne: 'tag',
107 | create: 'createTag',
108 | update: 'updateTag',
109 | delete: 'deleteTag',
110 | },
111 | readManySchema: {
112 | jsonSchema: {
113 | type: 'object',
114 | properties: {
115 | _id: 'string',
116 | title: 'string',
117 | }
118 | }
119 | },
120 | defaultSchema: {
121 | jsonSchema: {
122 | title: 'Tag',
123 | type: 'object',
124 | required: [
125 | 'title',
126 | 'tagQuestionId',
127 | ],
128 | properties: {
129 | title: {
130 | type: 'string'
131 | },
132 | tagQuestionId: {
133 | type: 'string',
134 | },
135 | }
136 | },
137 | uiSchema: {
138 | tagQuestionId: {
139 | 'ui:widget': 'hasOneWidget',
140 | }
141 | },
142 | },
143 | },
144 | {
145 | name: 'tagQuestion',
146 | uniqKey: '_id',
147 | crudMapping: {
148 | readMany: 'tagQuestions',
149 | readOne: 'tagQuestion',
150 | create: 'createTagQuestion',
151 | update: 'updateTagQuestion',
152 | delete: 'deleteTagQuestion',
153 | },
154 | readManySchema: {
155 | jsonSchema: {
156 | type: 'object',
157 | properties: {
158 | _id: 'string',
159 | title: 'string',
160 | }
161 | }
162 | },
163 | defaultSchema: {
164 | jsonSchema: {
165 | title: 'Tag Question',
166 | type: 'object',
167 | required: [
168 | 'title',
169 | ],
170 | properties: {
171 | title: {
172 | type: 'string'
173 | },
174 | }
175 | },
176 | uiSchema: { },
177 | },
178 | },
179 | ]
180 | }
181 |
182 | ```
183 |
184 | ## config API reference
185 | - brand
186 | The text on the left of the navbar
187 | - graphqlUrl
188 | The backend graphql url used by byob-cms
189 | - resources
190 | - resources.[].name
191 | The name of the resource (needs to be singular). Used for GraphQL arguments and input types
192 | - resources.[].displayName
193 | The display name of the resource (should be singular). Used for titles and routes. If missing, defaults to resources.[].name
194 | - resources.[].uniqKey
195 | Unique key used in your records. Most of the time its 'id' or '\_id'.
196 | - resources.[].crudMapping
197 | GraphQL mutation and query names for crud options
198 | - resources.[].crudMapping.create
199 | ```
200 | mutation ${crudMapping.create}($input: ${upperFirst(resource.name + 'Input')}!) {
201 | ${crudMapping.create}(${resource.name} : $input)
202 | }
203 | ```
204 | - resources.[].crudMapping.update
205 | ```
206 | mutation ${crudMapping.update}($input: ${upperFirst(resource.name + 'Input')}!) {
207 | ${crudMapping.update}(${resource.name} : $input, ${uniqKey}: "${uniqKeyValue}")
208 | }
209 | ```
210 | - resources.[].crudMapping.delete
211 | ```
212 | mutation ${crudMapping.delete}($${uniqKey}: String!) {
213 | ${crudMapping.delete}(${uniqKey}: $${uniqKey})
214 | }
215 | ```
216 | - resources.[].crudMapping.readOne
217 | ```
218 | query ${crudMapping.readOne}($${uniqKey}: String!) {
219 | ${crudMapping.readOne}(${uniqKey}: $${uniqKey}) ${jsonSchemaToGqlQuery(readOneSchema.jsonSchema)}
220 | }
221 | ```
222 | - resources.[].crudMapping.readMany
223 | ```
224 | query ${crudMapping.readMany} {
225 | ${crudMapping.readMany} ${jsonSchemaToGqlQuery(readManySchema.jsonSchema)}
226 | }
227 | ```
228 | - createSchema
229 | - createSchema.jsonSchema
230 | - createSchema.uiSchema
231 | - updateSchema
232 | - updateSchema.jsonSchema
233 | - updateSchema.uiSchema
234 | - readOneSchema
235 | - readOneSchema.jsonSchema
236 | - readOneSchema.uiSchema
237 | - readManySchema
238 | - readManySchema.jsonSchema
239 | - readManySchema.uiSchema
240 | - readManySchema.cellFormatter
241 | cellFormatter is a function used to format each td in the table
242 | function signature: (value, object, fieldName) -> ReactElement
243 | default value:
244 | ```
245 | function defaultCellFormatter (value) {
246 | return
247 | {JSON.stringify(value, null, 2)}
248 |
249 | }
250 | ```
251 | - defaultSchema
252 | - defaultSchema.jsonSchema
253 | - defaultSchema.uiSchema
254 | - jsonSchemaFormExtensions
255 | You can extend json schema form by providing this object
256 | - jsonSchemaFormExtensions.widgets
257 | https://github.com/mozilla-services/react-jsonschema-form#custom-component-registration
258 | - jsonSchemaFormExtensions.fields
259 | https://github.com/mozilla-services/react-jsonschema-form#custom-field-components
260 | - Login
261 | You can define your own custom Login component, setAccessToken function is
262 | passed in as a prop. After you call it, the following graphql requests will
263 | contain this value in the header as "authorization".
264 | You can also use built in components like this:
265 | ```
266 | import { CodeLogin } from 'byob-cms';
267 | ```
268 |
269 | ## pre-defined jsonSchema widgets and fields
270 | - numberWidget, currencyWidget (both use [react-number-format](https://github.com/s-yadav/react-number-format))
271 | - Formats/filters text input and evaluates field as number value
272 | - Passing/overriding props:
273 | ```
274 | 'ui:options': {
275 | props: {
276 | decimalScale: 0, // Blocks floats
277 | allowNegative: false,
278 | },
279 | },
280 | ```
281 | - hasOneWidget
282 | ```
283 | // by default gqlOptionsName is `${fieldName}Options`
284 | query ${upperFirst(gqlOptionsName)}Query {
285 | ${gqlOptionsName} {
286 | value
287 | label
288 | }
289 | }
290 | ```
291 | props:
292 | - ui:options
293 | - ui:options.gqlOptionsName
294 | - wysiwygWidget
295 | Wysiwyg Widget use [React Draft Wysiwyg](https://github.com/jpuri/react-draft-wysiwyg).
296 | props:
297 | - ui:options
298 | - wysiwygConfig: An array of props passing directly to Editor Component. You can find the full list of props here https://github.com/jpuri/react-draft-wysiwyg/blob/master/src/config/defaultToolbar.js
299 | - blockType: This is a special prop that does not use directly in the Editor component. Use this if you want to start your wysiwyg with a specific container. For example, specify `blockType: 'unordered-list-item'` will begin your wysiwyg with an unordered list element.
300 | - hasManyField
301 | Same as hasOneWidget, but for array of values
302 |
--------------------------------------------------------------------------------
/src/stylesheets/react-selectize.css:
--------------------------------------------------------------------------------
1 | .react-selectize {
2 | color: #000;
3 | }
4 | .react-selectize.root-node {
5 | position: relative;
6 | width: 300px;
7 | }
8 | .react-selectize.root-node.disabled {
9 | pointer-events: none;
10 | }
11 | .react-selectize.root-node .react-selectize-control {
12 | cursor: pointer;
13 | display: flex;
14 | align-items: flex-start;
15 | position: relative;
16 | padding: 2px;
17 | }
18 | .react-selectize.root-node .react-selectize-control .react-selectize-placeholder {
19 | display: block;
20 | line-height: 30px;
21 | overflow: hidden;
22 | text-overflow: ellipsis;
23 | vertical-align: middle;
24 | white-space: nowrap;
25 | position: absolute;
26 | max-width: calc(100% - 56px);
27 | }
28 | .react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values {
29 | display: flex;
30 | min-height: 30px;
31 | flex-grow: 1;
32 | flex-wrap: wrap;
33 | }
34 | .react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values .resizable-input {
35 | background: none;
36 | border: none;
37 | outline: none;
38 | font-size: 1em;
39 | margin: 2px;
40 | padding: 4px 0px;
41 | vertical-align: middle;
42 | width: 0px;
43 | }
44 | .react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values .value-wrapper {
45 | display: flex;
46 | align-items: center;
47 | }
48 | .react-selectize.root-node .react-selectize-control .react-selectize-reset-button-container,
49 | .react-selectize.root-node .react-selectize-control .react-selectize-toggle-button-container {
50 | flex-grow: 0;
51 | flex-shrink: 0;
52 | cursor: pointer;
53 | display: flex;
54 | align-items: center;
55 | justify-content: center;
56 | height: 30px;
57 | }
58 | .react-selectize.root-node .react-selectize-control .react-selectize-reset-button-container {
59 | width: 16px;
60 | }
61 | .react-selectize.root-node .react-selectize-control .react-selectize-toggle-button-container {
62 | width: 32px;
63 | }
64 | .react-selectize.root-node .react-selectize-control .react-selectize-reset-button-container:hover .react-selectize-reset-button path {
65 | stroke: #c0392b;
66 | }
67 | .react-selectize.root-node .react-selectize-control .react-selectize-reset-button path {
68 | transition: stroke 0.5s 0s ease;
69 | stroke: #999;
70 | stroke-linecap: square;
71 | stroke-linejoin: mitter;
72 | }
73 | .react-selectize.root-node .react-selectize-control .react-selectize-toggle-button path {
74 | fill: #999;
75 | }
76 | .react-selectize.dropdown-menu-wrapper {
77 | position: absolute;
78 | }
79 | .react-selectize.dropdown-menu-wrapper.tethered {
80 | min-width: 300px;
81 | }
82 | .react-selectize.dropdown-menu-wrapper:not(.tethered) {
83 | width: 100%;
84 | }
85 | .react-selectize.dropdown-menu {
86 | box-sizing: border-box;
87 | overflow: auto;
88 | position: absolute;
89 | max-height: 200px;
90 | z-index: 10;
91 | }
92 | .react-selectize.dropdown-menu.tethered {
93 | min-width: 300px;
94 | }
95 | .react-selectize.dropdown-menu:not(.tethered) {
96 | width: 100%;
97 | }
98 | .react-selectize.dropdown-menu .groups.as-columns {
99 | display: flex;
100 | }
101 | .react-selectize.dropdown-menu .groups.as-columns > div {
102 | flex: 1;
103 | }
104 | .react-selectize.dropdown-menu .option-wrapper {
105 | cursor: pointer;
106 | outline: none;
107 | }
108 | .multi-select.react-selectize.root-node .simple-value {
109 | display: inline-block;
110 | margin: 2px;
111 | vertical-align: middle;
112 | }
113 | .multi-select.react-selectize.root-node .simple-value span {
114 | display: inline-block;
115 | padding: 2px 5px 4px;
116 | vertical-align: center;
117 | }
118 | .simple-select.react-selectize.root-node .simple-value {
119 | margin: 2px;
120 | }
121 | .simple-select.react-selectize.root-node .simple-value span {
122 | display: inline-block;
123 | vertical-align: center;
124 | }
125 | .react-selectize.default {
126 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
127 | }
128 | .react-selectize.default.root-node .react-selectize-control {
129 | background-color: #fff;
130 | border: 1px solid;
131 | border-color: #d9d9d9 #ccc #b3b3b3;
132 | border-radius: 4px;
133 | font-size: 1em;
134 | }
135 | .react-selectize.default.root-node .react-selectize-control .react-selectize-placeholder {
136 | color: #aaa;
137 | text-indent: 8px;
138 | }
139 | .react-selectize.default.root-node .react-selectize-control .react-selectize-search-field-and-selected-values {
140 | padding-left: 5px;
141 | }
142 | .react-selectize.default.root-node.open.flipped .react-selectize-control {
143 | border-bottom-left-radius: 4px;
144 | border-bottom-right-radius: 4px;
145 | border-top-left-radius: 0px;
146 | border-top-right-radius: 0px;
147 | }
148 | .react-selectize.default.root-node.open:not(.flipped) .react-selectize-control {
149 | border-bottom-left-radius: 0px;
150 | border-bottom-right-radius: 0px;
151 | border-top-left-radius: 4px;
152 | border-top-right-radius: 4px;
153 | }
154 | .react-selectize.dropdown-menu-wrapper.default {
155 | overflow: hidden;
156 | }
157 | .react-selectize.dropdown-menu-wrapper.default .dropdown-menu.custom-enter-active,
158 | .react-selectize.dropdown-menu-wrapper.default .dropdown-menu.custom-leave-active {
159 | transition: transform 0.2s 0s ease;
160 | }
161 | .react-selectize.dropdown-menu-wrapper.default .dropdown-menu.flipped.custom-enter {
162 | transform: translateY(100%);
163 | }
164 | .react-selectize.dropdown-menu-wrapper.default .dropdown-menu.flipped.custom-enter-active {
165 | transform: translateY(0%);
166 | }
167 | .react-selectize.dropdown-menu-wrapper.default .dropdown-menu.flipped.custom-leave {
168 | transform: translateY(0%);
169 | }
170 | .react-selectize.dropdown-menu-wrapper.default .dropdown-menu.flipped.custom-leave-active {
171 | transform: translateY(100%);
172 | }
173 | .react-selectize.dropdown-menu-wrapper.default .dropdown-menu:not(.flipped).custom-enter {
174 | transform: translateY(-100%);
175 | }
176 | .react-selectize.dropdown-menu-wrapper.default .dropdown-menu:not(.flipped).custom-enter-active {
177 | transform: translateY(0%);
178 | }
179 | .react-selectize.dropdown-menu-wrapper.default .dropdown-menu:not(.flipped).custom-leave {
180 | transform: translateY(0%);
181 | }
182 | .react-selectize.dropdown-menu-wrapper.default .dropdown-menu:not(.flipped).custom-leave-active {
183 | transform: translateY(-100%);
184 | }
185 | .react-selectize.dropdown-menu.default {
186 | background: #fff;
187 | border: 1px solid #ccc;
188 | margin-top: -1px;
189 | }
190 | .react-selectize.dropdown-menu.default.flipped {
191 | border-top-left-radius: 4px;
192 | border-top-right-radius: 4px;
193 | }
194 | .react-selectize.dropdown-menu.default:not(.flipped) {
195 | border-color: #b3b3b3 #ccc #d9d9d9;
196 | border-bottom-left-radius: 4px;
197 | border-bottom-right-radius: 4px;
198 | }
199 | .react-selectize.dropdown-menu.default .no-results-found {
200 | color: #aaa !important;
201 | font-style: oblique;
202 | padding: 8px 10px;
203 | }
204 | .react-selectize.dropdown-menu.default .simple-group-title {
205 | background-color: #fafafa;
206 | padding: 8px 8px;
207 | }
208 | .react-selectize.dropdown-menu.default .option-wrapper.highlight {
209 | background: #f2f9fc;
210 | color: #333;
211 | }
212 | .react-selectize.dropdown-menu.default .option-wrapper .simple-option {
213 | color: #666;
214 | cursor: pointer;
215 | padding: 8px 10px;
216 | }
217 | .react-selectize.dropdown-menu.default .option-wrapper .simple-option.not-selectable {
218 | background-color: #f8f8f8;
219 | color: #999;
220 | cursor: default;
221 | font-style: oblique;
222 | text-shadow: 0px 1px 0px #fff;
223 | }
224 | .multi-select.react-selectize.default.root-node .simple-value {
225 | background: #f2f9fc;
226 | border: 1px solid #c9e6f2;
227 | border-radius: 2px;
228 | color: #08c;
229 | }
230 | .simple-select.react-selectize.default.root-node.open .react-selectize-control {
231 | background-color: #fff;
232 | }
233 | .simple-select.react-selectize.default.root-node:not(.open) .react-selectize-control {
234 | background-color: #f9f9f9;
235 | background-image: linear-gradient(to bottom, #fefefe, #f2f2f2);
236 | }
237 | .react-selectize.bootstrap3 {
238 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
239 | }
240 | .react-selectize.bootstrap3.root-node.open .react-selectize-control {
241 | background-color: #fff;
242 | border: 1px solid #66afe9;
243 | box-shadow: inset 0 1px 1px rgba(0,0,0,0.075), 0 0 8px rgba(102,175,233,0.6);
244 | }
245 | .react-selectize.bootstrap3.root-node .react-selectize-control {
246 | border: 1px solid;
247 | border-color: #d9d9d9 #ccc #b3b3b3;
248 | border-radius: 4px;
249 | font-size: 1em;
250 | }
251 | .react-selectize.bootstrap3.root-node .react-selectize-control .react-selectize-placeholder {
252 | color: #aaa;
253 | text-indent: 8px;
254 | }
255 | .react-selectize.bootstrap3.root-node .react-selectize-control .react-selectize-search-field-and-selected-values {
256 | padding-left: 5px;
257 | }
258 | .react-selectize.bootstrap3.dropdown-menu-wrapper.flipped {
259 | margin-bottom: 5px;
260 | }
261 | .react-selectize.bootstrap3.dropdown-menu-wrapper:not(.flipped) {
262 | margin-top: 5px;
263 | }
264 | .react-selectize.bootstrap3.dropdown-menu-wrapper .dropdown-menu.custom-enter-active,
265 | .react-selectize.bootstrap3.dropdown-menu-wrapper .dropdown-menu.custom-leave-active {
266 | transition: opacity 0.2s 0s ease;
267 | }
268 | .react-selectize.bootstrap3.dropdown-menu-wrapper .dropdown-menu.custom-enter {
269 | opacity: 0;
270 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
271 | filter: alpha(opacity=0);
272 | }
273 | .react-selectize.bootstrap3.dropdown-menu-wrapper .dropdown-menu.custom-enter-active {
274 | opacity: 1;
275 | -ms-filter: none;
276 | filter: none;
277 | }
278 | .react-selectize.bootstrap3.dropdown-menu-wrapper .dropdown-menu.custom-leave {
279 | opacity: 1;
280 | -ms-filter: none;
281 | filter: none;
282 | }
283 | .react-selectize.bootstrap3.dropdown-menu-wrapper .dropdown-menu.custom-leave-active {
284 | opacity: 0;
285 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
286 | filter: alpha(opacity=0);
287 | }
288 | .react-selectize.bootstrap3.dropdown-menu {
289 | background: #fff;
290 | border: 1px solid #ccc;
291 | border-radius: 4px;
292 | box-shadow: 0 6px 12px rgba(0,0,0,0.175);
293 | }
294 | .react-selectize.bootstrap3.dropdown-menu.flipped {
295 | margin-bottom: 5px;
296 | }
297 | .react-selectize.bootstrap3.dropdown-menu:not(.flipped) {
298 | margin-top: 5px;
299 | }
300 | .react-selectize.bootstrap3.dropdown-menu .no-results-found {
301 | color: #aaa !important;
302 | font-style: oblique;
303 | padding: 8px 10px;
304 | }
305 | .react-selectize.bootstrap3.dropdown-menu .groups:not(.as-columns) > div:not(:first-child) {
306 | border-top: 1px solid #e5e5e5;
307 | margin: 12px 0px 0px 0px;
308 | }
309 | .react-selectize.bootstrap3.dropdown-menu .simple-group-title {
310 | background-color: #fff;
311 | color: #999;
312 | padding: 8px 8px;
313 | text-shadow: 0px 1px 0px rgba(0,0,0,0.05);
314 | }
315 | .react-selectize.bootstrap3.dropdown-menu .option-wrapper.highlight {
316 | background: #428bca;
317 | }
318 | .react-selectize.bootstrap3.dropdown-menu .option-wrapper.highlight .simple-option {
319 | color: #fff;
320 | }
321 | .react-selectize.bootstrap3.dropdown-menu .option-wrapper .simple-option {
322 | color: #333;
323 | cursor: pointer;
324 | padding: 8px 10px;
325 | }
326 | .react-selectize.bootstrap3.dropdown-menu .option-wrapper .simple-option.not-selectable {
327 | background-color: #f8f8f8;
328 | color: #999;
329 | cursor: default;
330 | font-style: oblique;
331 | text-shadow: 0px 1px 0px #fff;
332 | }
333 | .multi-select.react-selectize.bootstrap3.root-node .simple-value {
334 | background: #efefef;
335 | border-radius: 4px;
336 | color: #333;
337 | }
338 | .react-selectize.material {
339 | font-family: Roboto, sans-serif;
340 | }
341 | .react-selectize.material.root-node.open .react-selectize-control:after {
342 | transform: scaleX(1);
343 | }
344 | .react-selectize.material.root-node .react-selectize-control {
345 | border-bottom: 1px solid rgba(0,0,0,0.3);
346 | }
347 | .react-selectize.material.root-node .react-selectize-control:after {
348 | background-color: #00bcd4;
349 | content: "";
350 | transform: scaleX(0);
351 | transition: transform 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
352 | position: absolute;
353 | left: 0px;
354 | bottom: -1px;
355 | width: 100%;
356 | height: 2px;
357 | }
358 | .react-selectize.material.root-node .react-selectize-control .react-selectize-placeholder {
359 | color: rgba(0,0,0,0.3);
360 | text-indent: 4px;
361 | }
362 | .react-selectize.material.dropdown-menu-wrapper.flipped {
363 | margin-bottom: 8px;
364 | }
365 | .react-selectize.material.dropdown-menu-wrapper.flipped .dropdown-menu {
366 | transform-origin: 100% 100%;
367 | }
368 | .react-selectize.material.dropdown-menu-wrapper:not(.flipped) {
369 | margin-top: 8px;
370 | }
371 | .react-selectize.material.dropdown-menu-wrapper:not(.flipped) .dropdown-menu {
372 | transform-origin: 0% 0%;
373 | }
374 | .react-selectize.material.dropdown-menu-wrapper .dropdown-menu.custom-enter-active,
375 | .react-selectize.material.dropdown-menu-wrapper .dropdown-menu.custom-leave-active {
376 | transition: transform 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms, opacity 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;
377 | }
378 | .react-selectize.material.dropdown-menu-wrapper .dropdown-menu.custom-enter {
379 | opacity: 0;
380 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
381 | filter: alpha(opacity=0);
382 | transform: scale(0, 0);
383 | }
384 | .react-selectize.material.dropdown-menu-wrapper .dropdown-menu.custom-enter-active {
385 | opacity: 1;
386 | -ms-filter: none;
387 | filter: none;
388 | transform: scale(1, 1);
389 | }
390 | .react-selectize.material.dropdown-menu-wrapper .dropdown-menu.custom-leave {
391 | opacity: 1;
392 | -ms-filter: none;
393 | filter: none;
394 | transform: scale(1, 1);
395 | }
396 | .react-selectize.material.dropdown-menu-wrapper .dropdown-menu.custom-leave-active {
397 | opacity: 0;
398 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
399 | filter: alpha(opacity=0);
400 | }
401 | .react-selectize.material.dropdown-menu {
402 | background-color: #fff;
403 | border-radius: 2px;
404 | box-shadow: rgba(0,0,0,0.118) 0px 1px 6px, rgba(0,0,0,0.118) 0px 1px 4px;
405 | max-height: 250px;
406 | padding: 8px 0px;
407 | }
408 | .react-selectize.material.dropdown-menu.flipped {
409 | margin-bottom: 8px;
410 | }
411 | .react-selectize.material.dropdown-menu:not(.flipped) {
412 | margin-top: 8px;
413 | }
414 | .react-selectize.material.dropdown-menu .no-results-found {
415 | font-style: oblique;
416 | font-size: 16px;
417 | height: 32px;
418 | padding: 0px 16px;
419 | display: flex;
420 | align-items: center;
421 | }
422 | .react-selectize.material.dropdown-menu .groups:not(.as-columns) > div:not(:last-child) {
423 | border-bottom: 1px solid #e5e5e5;
424 | }
425 | .react-selectize.material.dropdown-menu .simple-group-title {
426 | color: #8f8f8f;
427 | display: flex;
428 | align-items: center;
429 | font-size: 14px;
430 | height: 48px;
431 | padding: 0px 10px;
432 | }
433 | .react-selectize.material.dropdown-menu .option-wrapper.highlight {
434 | background-color: rgba(0,0,0,0.098);
435 | }
436 | .react-selectize.material.dropdown-menu .option-wrapper .simple-option {
437 | color: rgba(0,0,0,0.875);
438 | cursor: pointer;
439 | display: flex;
440 | flex-direction: column;
441 | align-items: flex-start;
442 | justify-content: center;
443 | font-size: 16px;
444 | height: 48px;
445 | padding: 0px 16px;
446 | }
447 | .react-selectize.material.dropdown-menu .option-wrapper .simple-option.not-selectable {
448 | background-color: #f8f8f8;
449 | color: #999;
450 | cursor: default;
451 | font-style: oblique;
452 | text-shadow: 0px 1px 0px #fff;
453 | }
454 | .multi-select.react-selectize.material.root-node .simple-value span {
455 | padding: 0px;
456 | }
457 | .multi-select.react-selectize.material.root-node .simple-value span:after {
458 | content: ",";
459 | }
460 | .simple-select.react-selectize.material.root-node .simple-value {
461 | margin: 4px 3px 3px 2px;
462 | }
463 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import './stylesheets/react-selectize.css';
5 | import './stylesheets/react-jsonschemaform.css';
6 | import './stylesheets/bootstrap-override.css';
7 | import './stylesheets/react-toastify.css';
8 | import Cms, { EmailPasswordLogin, StateHOF } from './App';
9 | import { connect } from 'react-redux';
10 | import jwtDecode from 'jwt-decode';
11 | import { ToastContainer } from 'react-toastify'
12 |
13 | const graphqlUrl = 'http://localhost:4000/admin-graphql'
14 |
15 | const AdminGqlCmsConfig = {
16 | brand: 'Encouragement CMS',
17 | title: 'Encouragement CMS',
18 | graphqlUrl,
19 | jsonSchemaFormExtensions: {
20 | // widgets: {
21 | // imageWidget: ImageWidget,
22 | // },
23 | // fields: {
24 | // sortableField: SortableField,
25 | // },
26 | },
27 | readManySchema: {
28 | cellFormatter(value, object, fieldName) {
29 | if (value && typeof value === 'object') {
30 | return {JSON.stringify(value)}
31 | }
32 | return value
33 | },
34 | sortStrategy: {
35 | type: 'SINGLE',
36 | defaultSortField: 'title',
37 | },
38 | searchStrategy: {
39 | type: 'FULLTEXT'
40 | },
41 | paginationStrategy: {
42 | type: 'STATIC',
43 | },
44 | },
45 | resources: [
46 | {
47 | // NOTE: name is singular
48 | name: 'user',
49 | uniqKey: '_id',
50 | crudMapping: {
51 | readMany: 'users',
52 | readOne: 'user',
53 | },
54 | readManySchema: {
55 | jsonSchema: {
56 | type: 'object',
57 | properties: {
58 | fullName: {
59 | type: 'string'
60 | },
61 | email: {
62 | type: 'number'
63 | },
64 | }
65 | }
66 | },
67 | defaultSchema: {
68 | },
69 | },
70 | {
71 | // NOTE: name is singular
72 | name: 'lifeEventCategory',
73 | uniqKey: '_id',
74 | crudMapping: {
75 | create: 'createLifeEventCategory',
76 | readMany: 'lifeEventCategories',
77 | readOne: 'lifeEventCategory',
78 | update: 'updateLifeEventCategory',
79 | // delete: 'deleteLifeEventCategory',
80 | },
81 | readManySchema: {
82 | jsonSchema: {
83 | type: 'object',
84 | properties: {
85 | title: {
86 | type: 'string'
87 | },
88 | sortIndex: {
89 | type: 'number'
90 | },
91 | }
92 | }
93 | },
94 | defaultSchema: {
95 | jsonSchema: {
96 | title: 'Life Event Category',
97 | type: 'object',
98 | required: [
99 | 'title',
100 | ],
101 | properties: {
102 | title: {
103 | type: 'string'
104 | },
105 | sortIndex: {
106 | type: 'number',
107 | default: 1000,
108 | },
109 | lifeEventIds: {
110 | title: 'Sort Associated Life Events',
111 | type: 'array',
112 | default: [],
113 | items: {
114 | type: 'string'
115 | }
116 | }
117 | }
118 | },
119 | uiSchema: {
120 | lifeEventIds: {
121 | 'ui:field': 'sortableField',
122 | }
123 | },
124 | },
125 | },
126 | {
127 | // NOTE: name is singular
128 | name: 'lifeEvent',
129 | uniqKey: '_id',
130 | crudMapping: {
131 | create: 'createLifeEvent',
132 | readMany: 'lifeEvents',
133 | readOne: 'lifeEvent',
134 | update: 'updateLifeEvent',
135 | // delete: 'deleteLifeEvent',
136 | },
137 | readManySchema: {
138 | // NOTE for now this is a hack since we can only render
139 | // one field for this object
140 | cellFormatter(value, object, fieldName) {
141 | if (fieldName === 'published') {
142 | return value ? 'Yes' : 'No'
143 | }
144 | if (value && typeof value === 'object') {
145 | return value.title
146 | }
147 | return value
148 | },
149 | jsonSchema: {
150 | type: 'object',
151 | properties: {
152 | title: {
153 | type: 'string'
154 | },
155 | lifeEventCategory: {
156 | type: 'object',
157 | properties: {
158 | title: {
159 | type: 'string',
160 | }
161 | }
162 | },
163 | published: {
164 | type: 'boolean',
165 | }
166 | }
167 | }
168 | },
169 | defaultSchema: {
170 | jsonSchema: {
171 | title: 'Life Event',
172 | type: 'object',
173 | required: [
174 | 'title',
175 | 'subTitle',
176 | 'imageId',
177 | 'published',
178 | 'lifeEventCategoryId',
179 | 'dropIds'
180 | ],
181 | properties: {
182 | title: {
183 | type: 'string'
184 | },
185 | slug: {
186 | type: 'string',
187 | title: 'Url Slug',
188 | },
189 | published: {
190 | type: 'boolean',
191 | default: false
192 | },
193 | subTitle: {
194 | type: 'string'
195 | },
196 | imageId: {
197 | type: 'string'
198 | },
199 | durationDescription: {
200 | type: 'string'
201 | },
202 | sponsoredBy: {
203 | type: 'string'
204 | },
205 | lifeEventContentItems: {
206 | type: 'array',
207 | items: {
208 | type: 'object',
209 | properties: {
210 | itemTitle: {
211 | type: 'string'
212 | },
213 | itemContent: {
214 | type: 'string'
215 | }
216 | }
217 | },
218 | default: [
219 | {
220 | 'itemTitle': 'He or She Might Be Thinking About...',
221 | }, {
222 | 'itemTitle': 'Words That Might Be Encouraging',
223 | }, {
224 | 'itemTitle': 'Words That Might Be Discouraging',
225 | }],
226 | },
227 | lifeEventCategoryId: {
228 | type: 'string',
229 | },
230 | dropIds: {
231 | type: 'array',
232 | default: [],
233 | items: {
234 | type: 'string'
235 | }
236 | },
237 | }
238 | },
239 | uiSchema: {
240 | // imageId: {
241 | // 'ui:widget': 'imageWidget',
242 | // },
243 | lifeEventContentItems: {
244 | items: {
245 | itemContent: {
246 | 'ui:widget': 'wysiwygWidget',
247 | 'ui:options': {
248 | wysiwygConfig: {
249 | toolbar: {
250 | options: ['link', 'list'],
251 | link: {
252 | showOpenOptionOnHover: false,
253 | },
254 | list: {
255 | options: ['unordered'],
256 | },
257 | },
258 | editorClassName: 'form-control',
259 | },
260 | blockType: 'unordered-list-item'
261 | }
262 | }
263 | },
264 | },
265 | lifeEventCategoryId: {
266 | 'ui:widget': 'hasOneWidget',
267 | },
268 | dropIds: {
269 | 'ui:field': 'hasManyField'
270 | }
271 | },
272 | },
273 | },
274 | {
275 | name: 'drop',
276 | uniqKey: '_id',
277 | crudMapping: {
278 | create: 'createDrop',
279 | readMany: 'drops',
280 | readOne: 'drop',
281 | update: 'updateDrop',
282 | // delete: 'deleteDrop',
283 | },
284 | readManySchema: {
285 | jsonSchema: {
286 | type: 'object',
287 | properties: {
288 | title: {
289 | type: 'string'
290 | },
291 | }
292 | }
293 | },
294 | defaultSchema: {
295 | jsonSchema: {
296 | title: 'Drop',
297 | type: 'object',
298 | required: [
299 | 'title',
300 | 'content',
301 | 'type',
302 | 'lifeEventIds',
303 | 'tagIds',
304 | ],
305 | properties: {
306 | title: {
307 | type: 'string',
308 | },
309 | content: {
310 | type: 'string',
311 | },
312 | type: {
313 | type: 'string',
314 | enum: [
315 | 'DROP_TYPE_AUDI',
316 | 'DROP_TYPE_EXPE',
317 | 'DROP_TYPE_GIFT',
318 | 'DROP_TYPE_WORD',
319 | ],
320 | enumNames: [
321 | 'audio / video',
322 | 'experience',
323 | 'gift',
324 | 'word',
325 | ],
326 | },
327 | url: {
328 | type: 'string',
329 | },
330 | lifeEventIds: {
331 | type: 'array',
332 | default: [],
333 | items: {
334 | type: 'string'
335 | }
336 | },
337 | tagIds: {
338 | type: 'array',
339 | default: [],
340 | items: {
341 | type: 'string'
342 | }
343 | }
344 | },
345 | },
346 | uiSchema: {
347 | lifeEventIds: {
348 | 'ui:field': 'hasManyField'
349 | },
350 | tagIds: {
351 | 'ui:field': 'hasManyField'
352 | },
353 | },
354 | }
355 | },
356 | {
357 | name: 'customDrop',
358 | uniqKey: '_id',
359 | crudMapping: {
360 | // create: 'createDrop',
361 | readMany: 'customDrops',
362 | readOne: 'customDrop',
363 | // update: 'updateDrop',
364 | // delete: 'deleteDrop',
365 | },
366 | readOneSchema: {
367 | show: true
368 | },
369 | readManySchema: {
370 | jsonSchema: {
371 | type: 'object',
372 | properties: {
373 | title: {
374 | type: 'string'
375 | },
376 | content: {
377 | type: 'string',
378 | },
379 | }
380 | }
381 | },
382 | defaultSchema: {
383 | jsonSchema: {
384 | title: 'Custom Drop',
385 | type: 'object',
386 | required: [
387 | 'title',
388 | 'content',
389 | ],
390 | properties: {
391 | title: {
392 | type: 'string',
393 | },
394 | content: {
395 | type: 'string',
396 | },
397 | },
398 | },
399 | }
400 | },
401 | {
402 | name: 'tag',
403 | uniqKey: '_id',
404 | crudMapping: {
405 | readMany: 'tags',
406 | readOne: 'tag',
407 | create: 'createTag',
408 | update: 'updateTag',
409 | // delete: 'deleteTag',
410 | },
411 | readManySchema: {
412 | jsonSchema: {
413 | type: 'object',
414 | properties: {
415 | title: {
416 | type: 'string'
417 | },
418 | }
419 | }
420 | },
421 | defaultSchema: {
422 | jsonSchema: {
423 | title: 'Tag',
424 | type: 'object',
425 | required: [
426 | 'title',
427 | ],
428 | properties: {
429 | title: {
430 | type: 'string'
431 | },
432 | tagQuestionId: {
433 | type: 'string',
434 | },
435 | }
436 | },
437 | uiSchema: {
438 | tagQuestionId: {
439 | 'ui:widget': 'hasOneWidget',
440 | }
441 | },
442 | },
443 | },
444 | {
445 | name: 'tagQuestion',
446 | uniqKey: '_id',
447 | crudMapping: {
448 | readMany: 'tagQuestions',
449 | readOne: 'tagQuestion',
450 | create: 'createTagQuestion',
451 | update: 'updateTagQuestion',
452 | // delete: 'deleteTagQuestion',
453 | },
454 | readManySchema: {
455 | jsonSchema: {
456 | type: 'object',
457 | properties: {
458 | title: {
459 | type: 'string'
460 | },
461 | }
462 | }
463 | },
464 | defaultSchema: {
465 | jsonSchema: {
466 | title: 'Tag Question',
467 | type: 'object',
468 | required: [
469 | 'title',
470 | 'question',
471 | ],
472 | properties: {
473 | title: {
474 | type: 'string'
475 | },
476 | question: {
477 | type: 'string'
478 | },
479 | tagIds: {
480 | type: 'array',
481 | items: {
482 | type: 'string',
483 | }
484 | },
485 | }
486 | },
487 | uiSchema: {
488 | tagIds: {
489 | 'ui:field': 'hasManyField',
490 | }
491 | },
492 | },
493 | },
494 | {
495 | name: 'hallmarkHoliday',
496 | uniqKey: '_id',
497 | crudMapping: {
498 | readMany: 'hallmarkHolidays',
499 | readOne: 'hallmarkHoliday',
500 | create: 'createHallmarkHoliday',
501 | update: 'updateHallmarkHoliday',
502 | // delete: 'deleteHallmarkHoliday',
503 | },
504 | readManySchema: {
505 | jsonSchema: {
506 | type: 'object',
507 | properties: {
508 | title: {
509 | type: 'string'
510 | },
511 | }
512 | }
513 | },
514 | defaultSchema: {
515 | jsonSchema: {
516 | title: 'Tag Question',
517 | type: 'object',
518 | required: [
519 | 'title',
520 | 'type',
521 | ],
522 | properties: {
523 | title: {
524 | type: 'string'
525 | },
526 | content: {
527 | type: 'string'
528 | },
529 | // type: {
530 | // type: 'string',
531 | // enum: Object.keys(HALLMARK_HOLIDAY_TYPE_MAP),
532 | // enumNames: Object.values(HALLMARK_HOLIDAY_TYPE_MAP),
533 | // },
534 | // monthAndDay: {
535 | // type: 'object',
536 | // properties: {
537 | // month: {
538 | // type: 'number',
539 | // enum: Object.keys(MONTHS_MAP).map(Number),
540 | // enumNames: Object.values(MONTHS_MAP)
541 | // },
542 | // dayOfMonth: {
543 | // type: 'number',
544 | // enum: range(1, 32),
545 | // }
546 | // }
547 | // },
548 | // monthOccurrenceAndDayOfWeek: {
549 | // type: 'object',
550 | // properties: {
551 | // month: {
552 | // type: 'number',
553 | // enum: Object.keys(MONTHS_MAP).map(Number),
554 | // enumNames: Object.values(MONTHS_MAP)
555 | // },
556 | // occurrence: {
557 | // title: '',
558 | // type: 'number',
559 | // enum: [
560 | // 0,1,2,3,4,5
561 | // ],
562 | // enumNames: [
563 | // '1st',
564 | // '2nd',
565 | // '3rd',
566 | // '4th',
567 | // '5th',
568 | // '6th',
569 | // ]
570 | // },
571 | // // dayOfWeek: {
572 | // // type: 'number',
573 | // // enum: Object.keys(DAYS_OF_WEEK_MAP).map(Number),
574 | // // enumNames: Object.values(DAYS_OF_WEEK_MAP),
575 | // // },
576 | // },
577 | // },
578 | },
579 | },
580 | },
581 | },
582 | ]
583 | }
584 |
585 | const ContentEditorGqlCmsConfig = {
586 | brand: 'Encouragement CMS',
587 | title: 'Encouragement CMS',
588 | Login: EmailPasswordLogin,
589 | initialPath: 'lifeEvent',
590 | graphqlUrl,
591 | readManySchema: {
592 | cellFormatter(value, object, fieldName) {
593 | if (value && typeof value === 'object') {
594 | return {JSON.stringify(value)}
595 | }
596 | return value
597 | },
598 | sortStrategy: {
599 | type: 'SINGLE',
600 | defaultSortField: 'title',
601 | },
602 | searchStrategy: {
603 | type: 'FULLTEXT'
604 | },
605 | paginationStrategy: {
606 | type: 'STATIC',
607 | },
608 | },
609 | resources: [
610 | {
611 | // NOTE: name is singular
612 | name: 'lifeEvent',
613 | uniqKey: '_id',
614 | crudMapping: {
615 | create: 'createLifeEvent',
616 | readMany: 'lifeEvents',
617 | readOne: 'lifeEvent',
618 | update: 'updateLifeEvent',
619 | // delete: 'deleteLifeEvent',
620 | },
621 | readManySchema: {
622 | jsonSchema: {
623 | type: 'object',
624 | properties: {
625 | title: {
626 | type: 'string'
627 | },
628 | }
629 | }
630 | },
631 | defaultSchema: {
632 | jsonSchema: {
633 | title: 'Life Event',
634 | type: 'object',
635 | required: [
636 | 'title',
637 | 'subTitle',
638 | 'imageUrl',
639 | 'durationDescription',
640 | 'lifeEventCategoryId',
641 | 'dropIds'
642 | ],
643 | properties: {
644 | title: {
645 | type: 'string'
646 | },
647 | subTitle: {
648 | type: 'string'
649 | },
650 | imageUrl: {
651 | type: 'string'
652 | },
653 | durationDescription: {
654 | type: 'string'
655 | },
656 | lifeEventCategoryId: {
657 | type: 'string',
658 | },
659 | dropIds: {
660 | type: 'array',
661 | default: [],
662 | items: {
663 | type: 'string'
664 | }
665 | },
666 | }
667 | },
668 | uiSchema: {
669 | lifeEventCategoryId: {
670 | 'ui:widget': 'hasOneWidget',
671 | },
672 | dropIds: {
673 | 'ui:field': 'hasManyField'
674 | }
675 | },
676 | },
677 | },
678 | ]
679 | }
680 |
681 | function RoleRouter (props) {
682 | const { accessToken } = props
683 | if (!accessToken) {
684 | return
685 |
694 |
695 |
696 | }
697 | const { role } = jwtDecode(accessToken)
698 | switch (role) {
699 | case 'ADMIN':
700 | return
701 | case 'CONTENT_EDITOR':
702 | return
703 | default:
704 | return null
705 | }
706 | }
707 |
708 | const RoleRouterWithState = StateHOF(connect(
709 | (state) => ({ accessToken: state.accessToken }),
710 | {}
711 | )(RoleRouter))
712 |
713 | ReactDOM.render(, document.getElementById('root'));
714 |
--------------------------------------------------------------------------------